Skip to content

Commit f46362f

Browse files
committed
release?
1 parent 73590e3 commit f46362f

8 files changed

Lines changed: 78 additions & 46 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ A BepInEx mod for Hollow Knight: Silksong that records your best time through ea
99
- Import/export replays and collections via clipboard or file
1010
- UI for managing replays and configuring the ghost
1111

12-
Website for improved sharing, visualizations, and more is still under works.
12+
Todo:
13+
- Website for improved sharing, visualizations, and more is still under works.
14+
- Triggers for starting/stopping/recording replays (like pink dot)
1315

1416
## Installation
1517

16-
Drop `ReplayTimerMod.dll` into `BepInEx/plugins/`
18+
Drop the `ReplayTimerMod` folder from [releases](https://github.com/adiprk/replaytimermod/releases) into `BepInEx/plugins/`
1719

1820
## UI
1921

ReplayTimerMod/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# ReplayTimerMod
2+
3+
A BepInEx mod for Hollow Knight: Silksong that records your best time through each room and allows replaying it as a ghost.
4+
5+
## Features
6+
7+
- Records Hornet's position and animation at 30fps per room, up to 180s.
8+
- Displays a ghost of your best run on subsequent attempts
9+
- Import/export replays and collections via clipboard or file
10+
- UI for managing replays and configuring the ghost
11+
12+
Todo:
13+
- Website for improved sharing, visualizations, and more is still under works.
14+
- Triggers for starting/stopping/recording replays (like pink dot)
15+
16+
## Installation
17+
18+
Drop the `ReplayTimerMod` folder from [releases](https://github.com/adiprk/replaytimermod/releases) into `BepInEx/plugins/`
19+
20+
## UI
21+
22+
Open the panel in game by pausing and clicking the menu icon (``) in the bottom-left corner.
23+
24+
## Ghost behaviour
25+
26+
The ghost follows the route that matches your current entry and exit scene. If multiple routes exist for the same entry scene, the fastest one is shown.
27+
28+
## Config
29+
30+
Settings are saved to `BepInEx/config/io.github.adiprk.replaytimermod.cfg`
31+
32+
```ini
33+
[Ghost]
34+
Enabled = true
35+
ColorR = 1
36+
ColorG = 1
37+
ColorB = 1
38+
Alpha = 0.4
39+
```

ReplayTimerMod/src/FrameRecorder.cs

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ public class FrameRecorder
1515
private bool recording = false;
1616
private float accumulatedTime = 0f;
1717

18+
// Cached animator reference
19+
private tk2dSpriteAnimator? cachedAnim = null;
20+
1821
public void StartRecording()
1922
{
2023
frames.Clear();
2124
recording = true;
22-
// Pre-fill the accumulator so the very first Tick() captures a frame
23-
// immediately (on the first LateUpdate after room entry). Without this,
24-
// frame 0 isn't stored until a full RECORD_INTERVAL has elapsed, which
25-
// means playback begins at the position Hornet was ~33 ms into the room
26-
// rather than at the room-entry position - making the ghost appear ahead.
25+
cachedAnim = null;
26+
// Pre-fill the accumulator so the very first Tick() captures a frame immediately
2727
accumulatedTime = RECORD_INTERVAL;
2828
}
2929

@@ -32,6 +32,7 @@ public void DiscardRecording()
3232
frames.Clear();
3333
recording = false;
3434
accumulatedTime = 0f;
35+
cachedAnim = null;
3536
}
3637

3738
public RecordedRoom? FinishRecording(RoomKey key, float totalLRTime)
@@ -40,50 +41,46 @@ public void DiscardRecording()
4041
{
4142
frames.Clear();
4243
recording = false;
44+
cachedAnim = null;
4345
return null;
4446
}
4547

4648
recording = false;
49+
cachedAnim = null;
4750
var result = new RecordedRoom(key, totalLRTime, frames.ToArray());
4851
frames.Clear();
4952
return result;
5053
}
5154

52-
// Called every LateUpdate with the pre-computed shouldTick value from
53-
// the plugin (LoadRemover.ShouldTick() is stateful and must only be
54-
// called once per frame - see ReplayTimerModPlugin.LateUpdate).
55+
// Called every LateUpdate if LoadRemover.ShouldTick()
5556
public void Tick(bool shouldTick)
5657
{
5758
if (!recording) return;
5859
if (HeroController.instance == null) return;
5960
if (!shouldTick) return;
6061

61-
// Use Time.deltaTime (scaled) to match the playback cursor, so that
62-
// recording and playback always advance at the same rate regardless of
63-
// timeScale. At 0.5x both sides accumulate half as fast, keeping the
64-
// ghost in sync. RoomTracker.CurrentRoomTime stays on unscaledDeltaTime
65-
// so PB times remain valid wall-clock comparisons.
62+
// Use Time.deltaTime (scaled) to match the playback cursor
6663
accumulatedTime += Time.deltaTime;
6764
if (accumulatedTime < RECORD_INTERVAL) return;
6865
accumulatedTime -= RECORD_INTERVAL;
6966

7067
bool facingRight = HeroController.instance.transform.localScale.x > 0f;
7168
Vector3 pos = HeroController.instance.transform.position;
7269

73-
// Capture animation state. tk2dSpriteAnimator.CurrentFrame is an
74-
// integer index into CurrentClip.frames[] - no normalisation required.
70+
if (cachedAnim == null)
71+
cachedAnim = HeroController.instance.GetComponent<tk2dSpriteAnimator>();
72+
7573
string clipName = "";
7674
int clipFrame = 0;
7775
try
7876
{
79-
var anim = HeroController.instance.GetComponent<tk2dSpriteAnimator>();
80-
if (anim?.CurrentClip != null)
77+
if (cachedAnim?.CurrentClip != null)
8178
{
82-
clipName = anim.CurrentClip.name;
83-
clipFrame = anim.CurrentFrame;
79+
clipName = cachedAnim.CurrentClip.name;
80+
clipFrame = cachedAnim.CurrentFrame;
8481
}
8582
}
86-
catch { }
83+
catch { cachedAnim = null; } // guh
8784

8885
frames.Add(new FrameData
8986
{

ReplayTimerMod/src/UI/ReplayUI.Actions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ private void OnExportAllClicked()
1616
return;
1717
}
1818
GUIUtility.systemCopyBuffer = ReplayShareEncoder.EncodeCollection(all);
19-
ShowExportFeedback($"{all.Count} copied", UIStyle.Accent);
19+
ShowExportFeedback($"{all.Count} copied", UIStyle.Accent);
2020
Log.LogInfo($"[ReplayUI] Copied {all.Count} replays to clipboard");
2121
}
2222

@@ -110,7 +110,7 @@ private void OnExportSceneClicked()
110110
return;
111111
}
112112
GUIUtility.systemCopyBuffer = ReplayShareEncoder.EncodeCollection(entries);
113-
ShowPasteStatus($"{entries.Count} routes copied", UIStyle.Accent);
113+
ShowPasteStatus($"{entries.Count} routes copied", UIStyle.Accent);
114114
Log.LogInfo($"[ReplayUI] Exported {entries.Count} routes for {selectedScene}");
115115
}
116116

@@ -193,7 +193,7 @@ private void OnPasteClicked()
193193
if (collection.Count > 0) selectedScene = collection[0].Key.SceneName;
194194
RebuildLeft();
195195
if (selectedScene != null) RebuildRight(selectedScene);
196-
ShowPasteStatus($"{collection.Count} replays", UIStyle.Gold);
196+
ShowPasteStatus($"{collection.Count} replays", UIStyle.Gold);
197197
Log.LogInfo($"[ReplayUI] Pasted collection: {collection.Count} replays");
198198
return;
199199
}
@@ -212,7 +212,7 @@ private void OnPasteClicked()
212212
selectedScene = room.Key.SceneName;
213213
RebuildLeft();
214214
RebuildRight(selectedScene);
215-
ShowPasteStatus($"{room.Key.SceneName}", UIStyle.Gold);
215+
ShowPasteStatus($"{room.Key.SceneName}", UIStyle.Gold);
216216
Log.LogInfo($"[ReplayUI] Pasted {room.Key}");
217217
}
218218

ReplayTimerMod/src/UI/ReplayUI.Columns.cs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,6 @@ private void AddSceneRow(Transform parent, string scene)
3939
bool selected = scene == selectedScene;
4040
bool isCurrent = scene == RoomTracker.CurrentScene;
4141

42-
// Visual priority: current room > selected.
43-
// current + selected → brighter gold tint to show both states at once.
44-
// current only → subtle gold tint.
45-
// selected only → standard overlay/accent.
46-
// neither → transparent.
4742
Color bgColor = isCurrent
4843
? UIStyle.Gold with { a = selected ? 0.28f : 0.14f }
4944
: (selected ? UIStyle.Overlay : Color.clear);
@@ -52,7 +47,6 @@ private void AddSceneRow(Transform parent, string scene)
5247
? UIStyle.Gold
5348
: (selected ? UIStyle.Accent : UIStyle.Text);
5449

55-
// ● prefix makes the current room scannable without reading every label.
5650
string label = isCurrent ? $"● {scene}" : scene;
5751

5852
var row = MakeGO("SceneRow", parent);
@@ -71,13 +65,7 @@ private void SelectScene(string scene)
7165
RebuildRight(scene);
7266
}
7367

74-
// Programmatically scrolls the left list so the given scene row is
75-
// centred in the viewport. Called by OnJumpToCurrentClicked().
76-
//
77-
// The VerticalLayoutGroup places rows in sorted order with 1px spacing,
78-
// so the top of row[idx] is always idx * (RH + 1) from the content top.
79-
// We compute the normalised scroll position from that offset and the
80-
// measured content / viewport heights.
68+
// Scrolls to given scene
8169
private void ScrollToScene(string scene)
8270
{
8371
if (leftScrollRect == null || leftContent == null) return;

ReplayTimerMod/src/UI/ReplayUI.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ public partial class ReplayUI
3939
private bool expanded = false;
4040
private string? selectedScene = null;
4141

42+
// Set by OnPBUpdated() whenever a new PB is recorded
43+
private bool rebuildPending = false;
44+
4245
// Two-click confirm for the global clear-all button.
4346
private bool clearAllPending = false;
4447
private Image? clearAllBtnImg;
@@ -60,12 +63,10 @@ public partial class ReplayUI
6063
private Text? rightHeader; // scene name in right sub-header
6164
private Text? pasteStatus; // brief feedback next to [Paste]
6265

63-
// Left column scroll control - used by ScrollToScene() to programmatically
64-
// reposition the list when jumping to the current room.
66+
// Left column scroll control
6567
private ScrollRect? leftScrollRect;
6668

67-
// "Go to current room" button label - updated to show feedback when the
68-
// current room has no recorded PBs.
69+
// "Go to current room" button label
6970
private Text? jumpToCurrentBtnLbl;
7071
private Image? jumpToCurrentBtnImg;
7172

@@ -138,6 +139,13 @@ public void Tick()
138139

139140
tabGO!.SetActive(true);
140141
panelGO!.SetActive(expanded);
142+
143+
if (expanded && rebuildPending)
144+
{
145+
rebuildPending = false;
146+
RebuildLeft();
147+
if (selectedScene != null) RebuildRight(selectedScene);
148+
}
141149
}
142150

143151
private static bool IsPaused()
@@ -156,9 +164,7 @@ private static bool IsPaused()
156164
// ─────────────────────────────────────────────────────────────────────
157165
public void OnPBUpdated()
158166
{
159-
if (!expanded || !IsPaused()) return;
160-
RebuildLeft();
161-
if (selectedScene != null) RebuildRight(selectedScene);
167+
rebuildPending = true;
162168
}
163169

164170
// ─────────────────────────────────────────────────────────────────────
692 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)