-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathExampleMod.cs
More file actions
179 lines (150 loc) · 6.02 KB
/
ExampleMod.cs
File metadata and controls
179 lines (150 loc) · 6.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
using System.IO;
using MelonLoader;
using ReplayMod.Replay;
using ReplayMod.Replay.Serialization;
using RumbleModUI;
using UnityEngine;
namespace ReplayMod.docs.Extensions;
public class ExampleMod : MelonMod
{
// This example demonstrates how to extend replays by recording
// and replaying a scene object (in this case, the Park bell [RIP]).
public static ExampleMod instance;
public ExampleMod() => instance = this;
// Used when reading frames to allow state to carry forward from delta-compression
// Delta-compression is not used in this example, but is highly recommended.
private static BellState lastState;
private ReplayExtension mod;
private static ModSetting<bool> recordBell;
public string currentScene = "Loader";
public override void OnLateInitializeMelon()
{
// The ID must remain the same or previously saved Replays
// will no longer associate with this extension.
mod = ReplayAPI.RegisterExtension(new BellExtension());
// Extensions can have their own settings.
recordBell = ReplayAPI.ReplayMod.AddToList("Record Bell", true, 0, "Toggles whether the bell is recorded.", new Tags());
mod.Settings.AddSetting(recordBell);
ReplayAPI.onReplayEnded += _ => {
lastState = null;
};
}
public override void OnSceneWasLoaded(int buildIndex, string sceneName)
{
currentScene = sceneName;
}
// Field identifiers used when writing frame data.
// These values are serialized as byte tags and must remain in a stable order.
private enum BellField : byte
{
Position,
Rotation
}
private class BellExtension : ReplayExtension
{
public override string Id => "BellSupport";
public override void OnRecordFrame(Frame frame, bool isBuffer)
{
if (instance.currentScene != "Park")
return;
if (!(bool)recordBell.SavedValue)
return;
var bell = new GameObject("Bell"); /*GameObjects.Park.INTERACTABLES.Bell.GetGameObject();*/ // RIP Bell
if (bell == null)
return;
// Capture transform state for this frame
frame.SetExtensionData(this, new BellState
{
Position = bell.transform.position,
Rotation = bell.transform.rotation
});
}
public override void OnWriteFrame(ReplayAPI.FrameExtensionWriter writer, Frame frame)
{
// If this frame has no recorded data, write nothing.
if (!frame.TryGetExtensionData(this, out BellState state))
return;
/*
* Each Write(field, value) call writes:
* - Field ID (1 byte)
* - Field payload length (1 byte)
* - The actual data (N bytes)
*
* The mod groups these field entries into a single chunk
* for this extension automatically.
*
* IMPORTANT:
* - Only write fields that changed between frames (delta encoding recommended).
* - Do NOT manually write field IDs or lengths using raw bw.Write.
* Always use the provided BinaryWriter.Write(field, value) overloads.
*/
writer.WriteChunk(0, w =>
{
w.Write(BellField.Position, state.Position);
w.Write(BellField.Rotation, state.Rotation);
});
}
public override void OnReadFrame(BinaryReader br, Frame frame, int subIndex)
{
/*
* ReadChunk builds a state object for this frame.
*
* The ctor function used to create the initial state for this frame.
* Each field encountered in the chunk mutates the state via the callback.
*
* When finished, ReadChunk returns the fully reconstructed state.
*
* Unknown fields are automatically skipped.
*
* Technically, the ctor function here is unnecessary due to our lack of delta-compression,
* but it is highly recommended to do so.
*/
var state = ReplaySerializer.ReadChunk<BellState, BellField>(
br,
() => lastState?.Clone() ?? new BellState(),
(s, field, size, reader) =>
{
switch (field)
{
case BellField.Position:
s.Position = reader.ReadVector3();
break;
case BellField.Rotation:
s.Rotation = reader.ReadQuaternion();
break;
}
});
frame.SetExtensionData(this, state);
lastState = state;
}
// NextFrame should be used for interpolation, though that isn't implemented here.
public override void OnPlaybackFrame(Frame frame, Frame nextFrame)
{
if (instance.currentScene != "Park")
return;
if (!frame.TryGetExtensionData(this, out BellState state))
return;
var bell = new GameObject("Bell"); /*GameObjects.Park.INTERACTABLES.Bell.GetGameObject();*/ // RIP Bell;
if (bell == null)
return;
// Apply reconstructed transform state to the live object.
bell.transform.position = state.Position;
bell.transform.rotation = state.Rotation;
}
}
// Simple container for bell transform state
private class BellState
{
public Vector3 Position;
public Quaternion Rotation;
// Used to preserve previous state during reconstruction.
public BellState Clone()
{
return new BellState
{
Position = Position,
Rotation = Rotation
};
}
}
}