diff --git a/GhostMod/GhostData.cs b/GhostMod/GhostData.cs index 05d0ebf..3d92b94 100644 --- a/GhostMod/GhostData.cs +++ b/GhostMod/GhostData.cs @@ -68,6 +68,7 @@ public static void ForAllGhosts(Session session, Func cb) public string SID; public AreaMode Mode; + public string From; public string Level; public string Target; diff --git a/GhostMod/GhostFrame.cs b/GhostMod/GhostFrame.cs index 1120286..cfd0420 100644 --- a/GhostMod/GhostFrame.cs +++ b/GhostMod/GhostFrame.cs @@ -22,6 +22,9 @@ public void Read(BinaryReader reader) { case "data": ReadChunkData(reader); break; + case "input": + ReadChunkInput(reader); + break; default: // Skip any unknown chunks. reader.BaseStream.Seek(length, SeekOrigin.Current); @@ -33,6 +36,8 @@ public void Read(BinaryReader reader) { public void Write(BinaryWriter writer) { WriteChunkData(writer); + WriteChunkInput(writer); + writer.WriteNullTerminatedString("\r\n"); } @@ -148,5 +153,204 @@ public void WriteChunkData(BinaryWriter writer) { WriteChunkEnd(writer, start); } + public bool HasInput; + + public int MoveX; + public int MoveY; + + public Vector2 Aim; + public Vector2 MountainAim; + + public int Buttons; + public bool ESC { + get { + return (Buttons & (int) ButtonMask.ESC) == (int) ButtonMask.ESC; + } + set { + Buttons &= (int) ~ButtonMask.ESC; + if (value) + Buttons |= (int) ButtonMask.ESC; + } + } + public bool Pause { + get { + return (Buttons & (int) ButtonMask.Pause) == (int) ButtonMask.Pause; + } + set { + Buttons &= (int) ~ButtonMask.Pause; + if (value) + Buttons |= (int) ButtonMask.Pause; + } + } + public bool MenuLeft { + get { + return (Buttons & (int) ButtonMask.MenuLeft) == (int) ButtonMask.MenuLeft; + } + set { + Buttons &= (int) ~ButtonMask.MenuLeft; + if (value) + Buttons |= (int) ButtonMask.MenuLeft; + } + } + public bool MenuRight { + get { + return (Buttons & (int) ButtonMask.MenuRight) == (int) ButtonMask.MenuRight; + } + set { + Buttons &= (int) ~ButtonMask.MenuRight; + if (value) + Buttons |= (int) ButtonMask.MenuRight; + } + } + public bool MenuUp { + get { + return (Buttons & (int) ButtonMask.MenuUp) == (int) ButtonMask.MenuUp; + } + set { + Buttons &= (int) ~ButtonMask.MenuUp; + if (value) + Buttons |= (int) ButtonMask.MenuUp; + } + } + public bool MenuDown { + get { + return (Buttons & (int) ButtonMask.MenuDown) == (int) ButtonMask.MenuDown; + } + set { + Buttons &= (int) ~ButtonMask.MenuDown; + if (value) + Buttons |= (int) ButtonMask.MenuDown; + } + } + public bool MenuConfirm { + get { + return (Buttons & (int) ButtonMask.MenuConfirm) == (int) ButtonMask.MenuConfirm; + } + set { + Buttons &= (int) ~ButtonMask.MenuConfirm; + if (value) + Buttons |= (int) ButtonMask.MenuConfirm; + } + } + public bool MenuCancel { + get { + return (Buttons & (int) ButtonMask.MenuCancel) == (int) ButtonMask.MenuCancel; + } + set { + Buttons &= (int) ~ButtonMask.MenuCancel; + if (value) + Buttons |= (int) ButtonMask.MenuCancel; + } + } + public bool MenuJournal { + get { + return (Buttons & (int) ButtonMask.MenuJournal) == (int) ButtonMask.MenuJournal; + } + set { + Buttons &= (int) ~ButtonMask.MenuJournal; + if (value) + Buttons |= (int) ButtonMask.MenuJournal; + } + } + public bool QuickRestart { + get { + return (Buttons & (int) ButtonMask.QuickRestart) == (int) ButtonMask.QuickRestart; + } + set { + Buttons &= (int) ~ButtonMask.QuickRestart; + if (value) + Buttons |= (int) ButtonMask.QuickRestart; + } + } + public bool Jump { + get { + return (Buttons & (int) ButtonMask.Jump) == (int) ButtonMask.Jump; + } + set { + Buttons &= (int) ~ButtonMask.Jump; + if (value) + Buttons |= (int) ButtonMask.Jump; + } + } + public bool Dash { + get { + return (Buttons & (int) ButtonMask.Dash) == (int) ButtonMask.Dash; + } + set { + Buttons &= (int) ~ButtonMask.Dash; + if (value) + Buttons |= (int) ButtonMask.Dash; + } + } + public bool Grab { + get { + return (Buttons & (int) ButtonMask.Grab) == (int) ButtonMask.Grab; + } + set { + Buttons &= (int) ~ButtonMask.Grab; + if (value) + Buttons |= (int) ButtonMask.Grab; + } + } + public bool Talk { + get { + return (Buttons & (int) ButtonMask.Talk) == (int) ButtonMask.Talk; + } + set { + Buttons &= (int) ~ButtonMask.Talk; + if (value) + Buttons |= (int) ButtonMask.Talk; + } + } + + public void ReadChunkInput(BinaryReader reader) { + HasInput = true; + + MoveX = reader.ReadInt32(); + MoveY = reader.ReadInt32(); + + Aim = new Vector2(reader.ReadSingle(), reader.ReadSingle()); + MountainAim = new Vector2(reader.ReadSingle(), reader.ReadSingle()); + + Buttons = reader.ReadInt32(); + } + + public void WriteChunkInput(BinaryWriter writer) { + if (!HasInput) + return; + long start = WriteChunkStart(writer, "input"); + + writer.Write(MoveX); + writer.Write(MoveY); + + writer.Write(Aim.X); + writer.Write(Aim.Y); + + writer.Write(MountainAim.X); + writer.Write(MountainAim.Y); + + writer.Write(Buttons); + + WriteChunkEnd(writer, start); + } + + [Flags] + public enum ButtonMask : int { + ESC = 1 << 0, + Pause = 1 << 1, + MenuLeft = 1 << 2, + MenuRight = 1 << 3, + MenuUp = 1 << 4, + MenuDown = 1 << 5, + MenuConfirm = 1 << 6, + MenuCancel = 1 << 7, + MenuJournal = 1 << 8, + QuickRestart = 1 << 9, + Jump = 1 << 10, + Dash = 1 << 11, + Grab = 1 << 12, + Talk = 1 << 13 + } + } } diff --git a/GhostMod/GhostInputNodes.cs b/GhostMod/GhostInputNodes.cs new file mode 100644 index 0000000..a1d7f69 --- /dev/null +++ b/GhostMod/GhostInputNodes.cs @@ -0,0 +1,60 @@ +using FMOD.Studio; +using Microsoft.Xna.Framework; +using Monocle; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using YamlDotNet.Serialization; + +namespace Celeste.Mod.Ghost { + public static class GhostInputNodes { + + public class MoveX : VirtualAxis.Node { + public GhostInputReplayer Replayer; + public MoveX(GhostInputReplayer replayer) { + Replayer = replayer; + } + public override float Value => Replayer.Frame.MoveX; + } + + public class MoveY : VirtualAxis.Node { + public GhostInputReplayer Replayer; + public MoveY(GhostInputReplayer replayer) { + Replayer = replayer; + } + public override float Value => Replayer.Frame.MoveY; + } + + public class Aim : VirtualJoystick.Node { + public GhostInputReplayer Replayer; + public Aim(GhostInputReplayer replayer) { + Replayer = replayer; + } + public override Vector2 Value => Replayer.Frame.Aim; + } + + public class MountainAim : VirtualJoystick.Node { + public GhostInputReplayer Replayer; + public MountainAim(GhostInputReplayer replayer) { + Replayer = replayer; + } + public override Vector2 Value => Replayer.Frame.MountainAim; + } + + public class Button : VirtualButton.Node { + public GhostInputReplayer Replayer; + public int Mask; + public Button(GhostInputReplayer replayer, GhostFrame.ButtonMask mask) { + Replayer = replayer; + Mask = (int) mask; + } + public override bool Check => !MInput.Disabled && (Replayer.Frame.Buttons & Mask) == Mask; + public override bool Pressed => !MInput.Disabled && (Replayer.Frame.Buttons & Mask) == Mask && (Replayer.PrevFrame.Buttons & Mask) == 0; + public override bool Released => !MInput.Disabled && (Replayer.Frame.Buttons & Mask) == 0 && (Replayer.PrevFrame.Buttons & Mask) == Mask; + } + + } +} \ No newline at end of file diff --git a/GhostMod/GhostInputReplayer.cs b/GhostMod/GhostInputReplayer.cs new file mode 100644 index 0000000..7f32633 --- /dev/null +++ b/GhostMod/GhostInputReplayer.cs @@ -0,0 +1,75 @@ +using FMOD.Studio; +using Microsoft.Xna.Framework; +using Monocle; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using YamlDotNet.Serialization; + +namespace Celeste.Mod.Ghost { + // We need this to work across scenes. + public class GhostInputReplayer : GameComponent { + + public GhostData Data; + public int FrameIndex = 0; + public GhostFrame Frame => Data == null ? default(GhostFrame) : Data[FrameIndex]; + public GhostFrame PrevFrame => Data == null ? default(GhostFrame) : Data[FrameIndex - 1]; + + public GhostInputReplayer(Game game, GhostData data) + : base(game) { + Data = data; + + Everest.Events.Input.OnInitialize += HookInput; + HookInput(); + } + + public void HookInput() { + Input.MoveX.Nodes.Add(new GhostInputNodes.MoveX(this)); + Input.MoveY.Nodes.Add(new GhostInputNodes.MoveY(this)); + + Input.Aim.Nodes.Add(new GhostInputNodes.Aim(this)); + Input.MountainAim.Nodes.Add(new GhostInputNodes.MountainAim(this)); + + Input.ESC.Nodes.Add(new GhostInputNodes.Button(this, GhostFrame.ButtonMask.ESC)); + Input.Pause.Nodes.Add(new GhostInputNodes.Button(this, GhostFrame.ButtonMask.Pause)); + Input.MenuLeft.Nodes.Add(new GhostInputNodes.Button(this, GhostFrame.ButtonMask.MenuLeft)); + Input.MenuRight.Nodes.Add(new GhostInputNodes.Button(this, GhostFrame.ButtonMask.MenuRight)); + Input.MenuUp.Nodes.Add(new GhostInputNodes.Button(this, GhostFrame.ButtonMask.MenuUp)); + Input.MenuDown.Nodes.Add(new GhostInputNodes.Button(this, GhostFrame.ButtonMask.MenuDown)); + Input.MenuConfirm.Nodes.Add(new GhostInputNodes.Button(this, GhostFrame.ButtonMask.MenuConfirm)); + Input.MenuCancel.Nodes.Add(new GhostInputNodes.Button(this, GhostFrame.ButtonMask.MenuCancel)); + Input.MenuJournal.Nodes.Add(new GhostInputNodes.Button(this, GhostFrame.ButtonMask.MenuJournal)); + Input.QuickRestart.Nodes.Add(new GhostInputNodes.Button(this, GhostFrame.ButtonMask.QuickRestart)); + Input.Jump.Nodes.Add(new GhostInputNodes.Button(this, GhostFrame.ButtonMask.Jump)); + Input.Dash.Nodes.Add(new GhostInputNodes.Button(this, GhostFrame.ButtonMask.Dash)); + Input.Grab.Nodes.Add(new GhostInputNodes.Button(this, GhostFrame.ButtonMask.Grab)); + Input.Talk.Nodes.Add(new GhostInputNodes.Button(this, GhostFrame.ButtonMask.Talk)); + + Logger.Log("ghost", "GhostReplayer hooked input."); + } + + public override void Update(GameTime gameTime) { + base.Update(gameTime); + + do { + FrameIndex++; + } while ( + (!Frame.HasInput && FrameIndex < Data.Frames.Count) // Skip any frames not containing the input chunk. + ); + + if (Data == null || FrameIndex >= Data.Frames.Count) + Remove(); + } + + public void Remove() { + Everest.Events.Input.OnInitialize -= HookInput; + Input.Initialize(); + Logger.Log("ghost", "GhostReplayer returned input."); + Game.Components.Remove(this); + } + + } +} diff --git a/GhostMod/GhostMod.csproj b/GhostMod/GhostMod.csproj index 5bff908..bef16d6 100644 --- a/GhostMod/GhostMod.csproj +++ b/GhostMod/GhostMod.csproj @@ -65,6 +65,7 @@ + @@ -72,6 +73,7 @@ + diff --git a/GhostMod/GhostRecorder.cs b/GhostMod/GhostRecorder.cs index f80dcae..12a9576 100644 --- a/GhostMod/GhostRecorder.cs +++ b/GhostMod/GhostRecorder.cs @@ -32,6 +32,15 @@ public override void Update() { RecordData(); } + public override void Render() { + base.Render(); + + if (Data == null) + return; + + RecordInput(); + } + public void RecordData() { if (Data == null) return; @@ -58,5 +67,54 @@ public void RecordData() { }); } + public void RecordInput() { + // Check if we've got a data-less input frame. If so, add input to it. + // If the frame already has got input, add a new input frame. + + bool inputDisabled = MInput.Disabled; + MInput.Disabled = false; + + GhostFrame frame; + bool isNew = false; + if (Data.Frames.Count == 0 || Data[Data.Frames.Count - 1].HasInput) { + frame = new GhostFrame(); + isNew = true; + } else { + frame = Data[Data.Frames.Count - 1]; + } + + frame.HasInput = true; + + frame.MoveX = Input.MoveX.Value; + frame.MoveY = Input.MoveY.Value; + + frame.Aim = Input.Aim.Value; + frame.MountainAim = Input.MountainAim.Value; + + frame.ESC = Input.ESC.Check; + frame.Pause = Input.Pause.Check; + frame.MenuLeft = Input.MenuLeft.Check; + frame.MenuRight = Input.MenuRight.Check; + frame.MenuUp = Input.MenuUp.Check; + frame.MenuDown = Input.MenuDown.Check; + frame.MenuConfirm = Input.MenuConfirm.Check; + frame.MenuCancel = Input.MenuCancel.Check; + frame.MenuJournal = Input.MenuJournal.Check; + frame.QuickRestart = Input.QuickRestart.Check; + frame.Jump = Input.Jump.Check; + frame.Dash = Input.Dash.Check; + frame.Grab = Input.Grab.Check; + frame.Talk = Input.Talk.Check; + + if (isNew) { + Data.Frames.Add(frame); + } else { + Data.Frames[Data.Frames.Count - 1] = frame; + } + + MInput.Disabled = inputDisabled; + + } + } }