diff --git a/scripts/Rhythia.cs b/scripts/Rhythia.cs index f14e3127..583c2847 100644 --- a/scripts/Rhythia.cs +++ b/scripts/Rhythia.cs @@ -195,6 +195,7 @@ public static void RegisterModifiers() Modifiers = [ new NoFailModifier(), + new AutoplayModifier(), new GhostModifier(), new StrobeModifier(), new ChaosModifier(), diff --git a/scripts/game/AutoplayHandler.cs b/scripts/game/AutoplayHandler.cs new file mode 100644 index 00000000..117dbe95 --- /dev/null +++ b/scripts/game/AutoplayHandler.cs @@ -0,0 +1,474 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using Godot; + +public class AutoplayHandler +{ + private readonly List processedData = []; + private int lastLoadedNote; + + private const float max_shift_multiplier = 0.25f; + private const int cache_version = 1; + + private readonly struct AutoNote(float x, float y, double millisecond) + { + public readonly float X = x; + public readonly float Y = y; + public readonly double Millisecond = millisecond; + + public Vector2 Position => new(X, Y); + + public AutoNote WithPosition(Vector2 position) => new(position.X, position.Y, Millisecond); + } + + public AutoplayHandler(Attempt attempt) + { + var notes = attempt.Map.Notes.Select(note => new AutoNote(note.X, note.Y, note.Millisecond)).ToList(); + + if (notes.Count == 0) + { + return; + } + + notes = mergeSimultaneousNotes(notes); + + string cachePath = getCachePath(notes); + if (tryLoadCache(cachePath, notes.Count)) + { + return; + } + + // this can be used for difficulty calculation + List preprocessedData = initialPreprocess(notes); + List secondaryPreprocessedData = compressStacks(preprocessedData); + shiftPreprocess(notes, preprocessedData, secondaryPreprocessedData); + saveCache(cachePath); + } + + public void Reset(double progress) + { + int index = lowerBound(processedData, progress); + lastLoadedNote = index < processedData.Count && processedData[index].Millisecond == progress ? index : Math.Max(0, index - 1); + } + + public Vector2 GetCursorPosition(double progress) + { + if (processedData.Count == 0) + { + return Vector2.Zero; + } + + if (progress < processedData[lastLoadedNote].Millisecond) + { + Reset(progress); + } + + while (lastLoadedNote + 1 < processedData.Count) + { + AutoNote note = processedData[lastLoadedNote + 1]; + + if (note.Millisecond > progress) + { + break; + } + + lastLoadedNote++; + } + + AutoNote note0 = processedData[Math.Max(lastLoadedNote - 1, 0)]; + AutoNote note1 = processedData[lastLoadedNote]; + AutoNote note2 = processedData[Math.Min(lastLoadedNote + 1, processedData.Count - 1)]; + AutoNote note3 = processedData[Math.Min(lastLoadedNote + 2, processedData.Count - 1)]; + + return getSplinePosition(note0, note1, note2, note3, progress).Clamp(-Constants.BOUNDS, Constants.BOUNDS); + } + + private List initialPreprocess(List notes) + { + List preprocessedData = []; + int i = 0; + + while (i < notes.Count) + { + AutoNote note = notes[i]; + i++; + + List collected = [note]; + + while (i < notes.Count) + { + AutoNote next = notes[i]; + + if (Math.Abs(next.Millisecond - note.Millisecond) <= 5 && checkHit(note, next, hitboxSize() * 0.9f)) + { + collected.Add(next); + i++; + } + else + { + break; + } + } + + Vector2 avgPos = Vector2.Zero; + + foreach (AutoNote collectedNote in collected) + { + avgPos += collectedNote.Position; + } + + avgPos /= collected.Count; + + if (preprocessedData.Count > 0) + { + AutoNote prevData = preprocessedData[^1]; + Vector2 prevDataPos = prevData.Position; + float timeElapsed = (float)(Math.Abs(prevData.Millisecond - note.Millisecond) / 1000.0); + + if (timeElapsed > 0) + { + float speed = prevDataPos.DistanceTo(avgPos) / (timeElapsed * 100); + avgPos *= 0.8f + (sigmoid(speed * 5) - 0.5f) * 2 * 0.2f; + } + + if (i > 2 && preprocessedData.Count >= 2) + { + AutoNote prevPrevData = preprocessedData[^2]; + Vector2 dir1 = (prevDataPos - prevPrevData.Position).Normalized(); + Vector2 dir2 = (avgPos - prevDataPos).Normalized(); + + avgPos *= 1 + Math.Max(-dir1.Dot(dir2) - 0.5f, 0) * (1 / (timeElapsed * 20 + 1)); + } + } + + AutoNote newNote = new(avgPos.X, avgPos.Y, note.Millisecond); + AutoNote previousNote = notes[Math.Max(i - 2, 0)]; + + if (collected.Count == 1 && note.X == previousNote.X && note.Y == previousNote.Y && preprocessedData.Count > 0) + { + newNote = new(preprocessedData[^1].X, preprocessedData[^1].Y, newNote.Millisecond); + } + + preprocessedData.Add(newNote); + } + + return preprocessedData; + } + + private List compressStacks(List notes) + { + List compressed = []; + + for (int i = 0; i < notes.Count; i++) + { + AutoNote note = notes[i]; + if (i > 0 && i + 1 < notes.Count && notes[i - 1].Position == note.Position && notes[i + 1].Position == note.Position) + { + continue; + } + + compressed.Add(note); + } + + return compressed; + } + + private void shiftPreprocess(List originalNotes, List preprocessedData, List secondaryPreprocessedData) + { + float maxRange = hitboxSize() * max_shift_multiplier; + + for (int i = 0; i + 1 < secondaryPreprocessedData.Count; i++) + { + AutoNote note0 = secondaryPreprocessedData[Math.Max(i - 2, 0)]; + AutoNote note1 = secondaryPreprocessedData[Math.Max(i - 1, 0)]; + AutoNote note2 = secondaryPreprocessedData[i]; + AutoNote note3 = secondaryPreprocessedData[i + 1]; + AutoNote note4 = secondaryPreprocessedData[Math.Min(i + 2, secondaryPreprocessedData.Count - 1)]; + + if (note1.Position == note2.Position || note2.Position == note3.Position) + { + processedData.Add(note2); + continue; + } + + Vector2 position = getSplinePosition(note0, note1, note3, note4, note2.Millisecond); + Vector2 shift = clampShift(position - note2.Position, maxRange); + AutoNote shifted = note2.WithPosition(note2.Position + shift); + List validation = [.. processedData.TakeLast(3), shifted, .. secondaryPreprocessedData.Skip(i + 1).Take(3)]; + + bool valid = true; + double low = validation[Math.Min(2, validation.Count - 1)].Millisecond; + double high = validation[Math.Max(0, validation.Count - 3)].Millisecond; + + for (int noteIndex = lowerBound(originalNotes, low); noteIndex < originalNotes.Count; noteIndex++) + { + AutoNote note = originalNotes[noteIndex]; + if (note.Millisecond > high) + break; + + bool hit = + checkHit(note, getCursorPositionFromNotes(validation, note.Millisecond + 1), hitboxSize() * 0.9f) + || checkHit(note, getCursorPositionFromNotes(validation, note.Millisecond + 5), hitboxSize() * 0.9f); + if (!hit) + { + valid = false; + break; + } + } + + processedData.Add(valid ? shifted : note2); + } + + processedData.Add(preprocessedData[^1]); + } + + private Vector2 getCursorPositionFromNotes(List noteData, double elapsed) + { + if (noteData.Count == 0) + { + return Vector2.Zero; + } + + int tempLastLoadedNote = 0; + + while (tempLastLoadedNote + 1 < noteData.Count) + { + AutoNote note = noteData[tempLastLoadedNote + 1]; + + if (note.Millisecond > elapsed) + { + break; + } + + tempLastLoadedNote++; + } + + AutoNote note0 = noteData[Math.Max(tempLastLoadedNote - 1, 0)]; + AutoNote note1 = noteData[tempLastLoadedNote]; + AutoNote note2 = noteData[Math.Min(tempLastLoadedNote + 1, noteData.Count - 1)]; + AutoNote note3 = noteData[Math.Min(tempLastLoadedNote + 2, noteData.Count - 1)]; + + return getSplinePosition(note0, note1, note2, note3, elapsed).Clamp(-Constants.BOUNDS, Constants.BOUNDS); + } + + private static bool checkHit(AutoNote notePos, AutoNote cursorPos, float size) => checkHit(notePos, cursorPos.Position, size); + + private static bool checkHit(AutoNote notePos, Vector2 cursorPos, float size) + { + Vector2 diff = (notePos.Position - cursorPos).Abs(); + return Math.Max(diff.X, diff.Y) < size; + } + + private static Vector2 getSplinePosition(AutoNote note0, AutoNote note1, AutoNote note2, AutoNote note3, double time) + { + double segmentDuration = note2.Millisecond - note1.Millisecond; + + if (segmentDuration <= 0) + { + return note1.Position; + } + + if (note1.Position == note2.Position) + { + return note1.Position; + } + + float u = (float)Math.Clamp((time - note1.Millisecond) / segmentDuration, 0, 1); + + return new( + catmullRomRaw(note0.X, note1.X, note2.X, note3.X, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond), + catmullRomRaw(note0.Y, note1.Y, note2.Y, note3.Y, u, note0.Millisecond, note1.Millisecond, note2.Millisecond, note3.Millisecond) + ); + } + + private static float catmullRomRaw( + float pos0, + float pos1, + float pos2, + float pos3, + float u, + double time0, + double time1, + double time2, + double time3 + ) + { + const float spline_alpha = 0.4f; + const float spline_tension = -1f; + + float t01 = Mathf.Pow(Math.Max(Math.Abs((float)(time0 - time1)), 1), spline_alpha); + float t12 = Mathf.Pow(Math.Max(Math.Abs((float)(time1 - time2)), 1), spline_alpha); + float t23 = Mathf.Pow(Math.Max(Math.Abs((float)(time2 - time3)), 1), spline_alpha); + + float m1 = (1 - spline_tension) * (pos2 - pos1 + t12 * ((pos1 - pos0) / t01 - (pos2 - pos0) / (t01 + t12))); + float m2 = (1 - spline_tension) * (pos2 - pos1 + t12 * ((pos3 - pos2) / t23 - (pos3 - pos1) / (t12 + t23))); + + if (!float.IsFinite(m1)) + { + m1 = 0; + } + + if (!float.IsFinite(m2)) + { + m2 = 0; + } + + float u2 = u * u; + + return (2 * (pos1 - pos2) + m1 + m2) * u2 * u + (-3 * (pos1 - pos2) - m1 - m1 - m2) * u2 + m1 * u + pos1; + } + + private static Vector2 clampShift(Vector2 shift, float limit) + { + if (shift.X == 0 || shift.Y == 0) + { + return shift.Clamp(Vector2.One * -limit, Vector2.One * limit); + } + + shift *= Math.Clamp(shift.X, -limit, limit) / shift.X; + shift *= Math.Clamp(shift.Y, -limit, limit) / shift.Y; + + return shift; + } + + private static float sigmoid(float value) => 1 / (1 + Mathf.Exp(-value)); + + private static float hitboxSize() => (float)(0.5 + Constants.HIT_BOX_SIZE); + + private static int lowerBound(List notes, double time) + { + int low = 0; + int high = notes.Count; + while (low < high) + { + int middle = low + (high - low) / 2; + if (notes[middle].Millisecond < time) + low = middle + 1; + else + high = middle; + } + + return low; + } + + private static List mergeSimultaneousNotes(List notes) + { + for (int i = 1; i < notes.Count; i++) + { + if (notes[i].Millisecond < notes[i - 1].Millisecond) + { + notes.Sort((a, b) => a.Millisecond.CompareTo(b.Millisecond)); + break; + } + } + + List merged = new(notes.Count); + int noteIndex = 0; + while (noteIndex < notes.Count) + { + AutoNote first = notes[noteIndex++]; + Vector2 position = first.Position; + int count = 1; + while (noteIndex < notes.Count && notes[noteIndex].Millisecond == first.Millisecond) + { + position += notes[noteIndex++].Position; + count++; + } + + merged.Add(first.WithPosition(position / count)); + } + + return merged; + } + + private static string getCachePath(List notes) + { + using var buffer = new MemoryStream(); + using var writer = new BinaryWriter(buffer); + writer.Write(cache_version); + writer.Write(Constants.HIT_BOX_SIZE); + writer.Write(Constants.BOUNDS.X); + writer.Write(Constants.BOUNDS.Y); + foreach (AutoNote note in notes) + { + writer.Write(note.X); + writer.Write(note.Y); + writer.Write(note.Millisecond); + } + + string hash = Convert.ToHexString(SHA256.HashData(buffer.GetBuffer().AsSpan(0, (int)buffer.Length))); + return Path.Combine(Constants.USER_FOLDER, "cache", "autoplay", $"{hash}.bin"); + } + + private bool tryLoadCache(string path, int noteCount) + { + try + { + using var stream = File.OpenRead(path); + using var reader = new BinaryReader(stream); + if (reader.ReadInt32() != cache_version) + return false; + + int count = reader.ReadInt32(); + if (count <= 0 || count > noteCount || stream.Length != 8L + count * 16L) + return false; + + List cached = new(count); + double previousTime = double.NegativeInfinity; + for (int i = 0; i < count; i++) + { + float x = reader.ReadSingle(); + float y = reader.ReadSingle(); + double time = reader.ReadDouble(); + if (!float.IsFinite(x) || !float.IsFinite(y) || !double.IsFinite(time) || time <= previousTime) + return false; + + cached.Add(new(x, y, time)); + previousTime = time; + } + + processedData.AddRange(cached); + return true; + } + catch (IOException) { } + catch (UnauthorizedAccessException) { } + + return false; + } + + private void saveCache(string path) + { + string temporaryPath = path + $".{Guid.NewGuid():N}.tmp"; + try + { + Directory.CreateDirectory(Path.GetDirectoryName(path)); + using (var writer = new BinaryWriter(File.Create(temporaryPath))) + { + writer.Write(cache_version); + writer.Write(processedData.Count); + foreach (AutoNote note in processedData) + { + writer.Write(note.X); + writer.Write(note.Y); + writer.Write(note.Millisecond); + } + } + + File.Move(temporaryPath, path, true); + } + catch (IOException) { } + catch (UnauthorizedAccessException) { } + finally + { + try + { + File.Delete(temporaryPath); + } + catch (IOException) { } + catch (UnauthorizedAccessException) { } + } + } +} diff --git a/scripts/game/AutoplayHandler.cs.uid b/scripts/game/AutoplayHandler.cs.uid new file mode 100644 index 00000000..756374cf --- /dev/null +++ b/scripts/game/AutoplayHandler.cs.uid @@ -0,0 +1 @@ +uid://dx76ccbly75a diff --git a/scripts/game/Runner.cs b/scripts/game/Runner.cs index a5ed7752..58eb0a8f 100644 --- a/scripts/game/Runner.cs +++ b/scripts/game/Runner.cs @@ -30,9 +30,12 @@ public partial class Runner : Node3D public bool StopQueued = false; private SettingsProfile settings; + private AutoplayHandler autoplayHandler; private double lastFrame = Time.GetTicksUsec(); private bool firstFrame = true; private bool eventsConnected = false; + private bool autoplayEnabled = false; + private double? hitResultProgressOverride = null; private double[] noteTimestamps; [ExportCategory("Settings")] @@ -103,8 +106,14 @@ public override void _Process(double delta) // Save replay frame + if (autoplayEnabled) + { + Game.Instance.CursorManager.UpdateAutoplayCursor(autoplayHandler.GetCursorPosition(Attempt.Progress)); + processAutoplayHits(); + } + // if not paused & record replays on & not a temporary map & time from now and last replay frame was 60 frames apart - if (!Attempt.Stopped && settings.RecordReplays && !Attempt.Map.Ephemeral && now - Attempt.LastReplayFrame >= 1000000 / 60) + if (!autoplayEnabled && !Attempt.Stopped && settings.RecordReplays && !Attempt.Map.Ephemeral && now - Attempt.LastReplayFrame >= 1000000 / 60) { if ( Attempt.ReplayFrames.Count == 0 @@ -216,6 +225,43 @@ public void RenderObjects(double delta) } } + private void processAutoplayHits() + { + if (!autoplayEnabled || Attempt.IsReplay || !Attempt.Objects.TryGetValue(typeof(Note), out var objects)) + { + return; + } + + int startIndex = ObjectIndicesStart[typeof(Note)]; + + for (int i = startIndex; i < objects.Count; i++) + { + Note note = (Note)objects[i]; + + if (note.Millisecond > Attempt.Progress) + { + break; + } + + if (note.Millisecond < Attempt.StartFrom || note.LastResult != HitResult.None) + { + continue; + } + + note.Hittable = true; + + try + { + hitResultProgressOverride = note.Millisecond; + note.Hit(this); + } + finally + { + hitResultProgressOverride = null; + } + } + } + public void Play() { if (Attempt == null) @@ -264,6 +310,26 @@ public void Play() } } + autoplayEnabled = Attempt.Modifiers.Any(mod => mod is AutoplayModifier) && !Attempt.IsReplay; + + if (autoplayEnabled) + { + foreach (var mod in Attempt.Modifiers.Where(mod => mod is AutoplayModifier)) + { + mod.Activate(Attempt); + + if (!NotesOnly) + { + HudManager.DisplayModifier(mod); + } + } + + autoplayHandler = new(Attempt); + } + else + { + autoplayHandler = null; + } settings = Attempt.IsReplay ? Attempt.Replays[0].Settings : SettingsManager.Instance.Settings; Camera.Fov = (float)(double)settings.FoV; @@ -335,6 +401,7 @@ public void Skip() public void Seek(double ms) { Attempt.Progress = ms; + autoplayHandler?.Reset(Attempt.Progress); foreach (var entry in Attempt.Objects) { @@ -407,7 +474,7 @@ public void Stop(bool results = true) } // dont want an infinite dependency loop so im just going to do this -fog - if (!Attempt.IsReplay && Game.Instance.ReplayManager.CurrentMode == ReplayManager.Mode.RECORD) + if (!autoplayEnabled && !Attempt.IsReplay && Game.Instance.ReplayManager.CurrentMode == ReplayManager.Mode.RECORD) { Game.Instance.ReplayManager.SaveReplay(Attempt); } @@ -481,9 +548,10 @@ public void Stop(bool results = true) private void onHitResultChanged(int noteIndex, HitResult hitResult) { + double judgmentProgress = hitResultProgressOverride ?? Attempt.Progress; float lateness = Attempt.IsReplay ? Attempt.HitsInfo[noteIndex] - : (float)(((int)Attempt.Progress - Attempt.Map.Notes[noteIndex].Millisecond) / Speed); + : (float)(((int)judgmentProgress - Attempt.Map.Notes[noteIndex].Millisecond) / Speed); float factor = 1 - Math.Max(0, lateness - 25) / 150f; uint hitScore = (uint)(100 * Attempt.ComboMultiplier * Attempt.ModsMultiplier * factor * ((Speed - 1) / 2.5 + 1)); diff --git a/scripts/game/managers/CursorManager.cs b/scripts/game/managers/CursorManager.cs index 53b197b6..be41756c 100644 --- a/scripts/game/managers/CursorManager.cs +++ b/scripts/game/managers/CursorManager.cs @@ -123,6 +123,31 @@ public void UpdateCursor(Vector2 inputDelta, int cursorIndex = 0) attempt.CameraMode.Process(attempt, replayManager, camera, cursors[cursorIndex], inputDelta, sensitivity); } + public void UpdateAutoplayCursor(Vector2 position) + { + EmitSignalOnCursorUpdated(position); + + var attempt = runner.Attempt; + attempt.RawCursorPosition = position; + attempt.CursorPosition = position.Clamp(-Constants.BOUNDS, Constants.BOUNDS); + + var origin = new Vector3(0, 0, attempt.CameraMode.Name == "Spin" ? 4f : 3.75f); + float parallax = (float)settings.CameraParallax; + camera.Position = origin + new Vector3(attempt.CursorPosition.X, attempt.CursorPosition.Y, 0) * parallax; + + if (attempt.CameraMode.Name != "Spin") + { + camera.Rotation = Vector3.Zero; + } + + Vector3 cursorPos = new(attempt.CursorPosition.X, attempt.CursorPosition.Y, 0); + + if (cursorPos.IsFinite()) + { + cursors[0].Position = cursorPos; + } + } + // Reset everything to zero so it doesn't have infinite sensitivity private void repositionAbsolute() { diff --git a/scripts/game/modifiers/AutoplayModifier.cs b/scripts/game/modifiers/AutoplayModifier.cs new file mode 100644 index 00000000..bc77780b --- /dev/null +++ b/scripts/game/modifiers/AutoplayModifier.cs @@ -0,0 +1,14 @@ +using Godot; + +public class AutoplayModifier : Modifier +{ + public override string Name => "Autoplay"; + + public override Color Color => new(0xffd166ff); + + public override void Activate(Attempt attempt) + { + base.Activate(attempt); + attempt.Qualifies = false; + } +} diff --git a/scripts/game/modifiers/AutoplayModifier.cs.uid b/scripts/game/modifiers/AutoplayModifier.cs.uid new file mode 100644 index 00000000..78531d58 --- /dev/null +++ b/scripts/game/modifiers/AutoplayModifier.cs.uid @@ -0,0 +1 @@ +uid://cobkyirweynfj diff --git a/scripts/map/Replay.cs b/scripts/map/Replay.cs index 1258b5d1..e0504d8c 100644 --- a/scripts/map/Replay.cs +++ b/scripts/map/Replay.cs @@ -132,6 +132,9 @@ public Replay(string path) case "NoFail": Modifiers.Add(new NoFailModifier()); break; + case "Autoplay": + Modifiers.Add(new AutoplayModifier()); + break; case "Ghost": Modifiers.Add(new GhostModifier()); break; diff --git a/scripts/scenes/Game.cs b/scripts/scenes/Game.cs index cbcd4b91..c8765e0b 100644 --- a/scripts/scenes/Game.cs +++ b/scripts/scenes/Game.cs @@ -178,7 +178,7 @@ public override void Load() { ReplayManager.CurrentMode = ReplayManager.Mode.PLAYBACK; } - else if (Runner.Attempt.Settings.RecordReplays) + else if (Runner.Attempt.Settings.RecordReplays && !Runner.Attempt.Modifiers.Exists(mod => mod is AutoplayModifier)) { ReplayManager.NewReplay(Runner.Attempt); ReplayManager.CurrentMode = ReplayManager.Mode.RECORD; diff --git a/scripts/skinning/SkinManager.cs b/scripts/skinning/SkinManager.cs index 1c908f6c..7978b843 100644 --- a/scripts/skinning/SkinManager.cs +++ b/scripts/skinning/SkinManager.cs @@ -120,6 +120,7 @@ public static void Load() skin.MapInfoCoverBackgroundImage = loadTexture("ui/play/mapinfo_cover_background.png"); skin.ModNoFailImage = loadTexture("modifiers/nofail.png"); + skin.ModAutoplayImage = loadTexture("modifiers/autoplay.png"); skin.ModGhostImage = loadTexture("modifiers/ghost.png"); skin.ModStrobeImage = loadTexture("modifiers/strobe.png"); skin.ModChaosImage = loadTexture("modifiers/chaos.png"); diff --git a/scripts/skinning/SkinProfile.cs b/scripts/skinning/SkinProfile.cs index 539457d5..751f9cfb 100644 --- a/scripts/skinning/SkinProfile.cs +++ b/scripts/skinning/SkinProfile.cs @@ -113,6 +113,8 @@ public partial class SkinProfile : RefCounted public ImageTexture ModNoFailImage { get; set; } = new(); + public ImageTexture ModAutoplayImage { get; set; } = new(); + public ImageTexture ModGhostImage { get; set; } = new(); public ImageTexture ModStrobeImage { get; set; } = new(); diff --git a/scripts/util/Misc.cs b/scripts/util/Misc.cs index b06287fb..3e4a550c 100644 --- a/scripts/util/Misc.cs +++ b/scripts/util/Misc.cs @@ -21,6 +21,9 @@ public static Texture2D GetModIcon(string mod) case "NoFail": tex = skin.ModNoFailImage; break; + case "Autoplay": + tex = skin.ModAutoplayImage; + break; case "Ghost": tex = skin.ModGhostImage; break; diff --git a/user/skins/default/modifiers/autoplay.png b/user/skins/default/modifiers/autoplay.png new file mode 100644 index 00000000..a8c47aca Binary files /dev/null and b/user/skins/default/modifiers/autoplay.png differ diff --git a/user/skins/default/modifiers/autoplay.png.import b/user/skins/default/modifiers/autoplay.png.import new file mode 100644 index 00000000..dfbfb9ec --- /dev/null +++ b/user/skins/default/modifiers/autoplay.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://pkytlqp7mt2g" +path="res://.godot/imported/autoplay.png-68ce854bc9795d220ef473f6447c55d9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://user/skins/default/modifiers/autoplay.png" +dest_files=["res://.godot/imported/autoplay.png-68ce854bc9795d220ef473f6447c55d9.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1