diff --git a/CelesteNet.Client/Components/CelesteNetMainComponent.cs b/CelesteNet.Client/Components/CelesteNetMainComponent.cs index f97af0c3..9f728d0d 100644 --- a/CelesteNet.Client/Components/CelesteNetMainComponent.cs +++ b/CelesteNet.Client/Components/CelesteNetMainComponent.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -34,6 +34,12 @@ public class CelesteNetMainComponent : CelesteNetGameComponent { private bool WasInteractive; private int SentHairLength = 0; + private int? SentDashes; + private Color SentP_DashColor; + private Color SentP_DashColor2; + private static Color LastP_DashColor = Player.P_DashA.Color; + private static Color LastP_DashColor2 = Player.P_DashA.Color2; + public HashSet ForceIdle = new(); public bool StateUpdated; @@ -41,6 +47,7 @@ public class CelesteNetMainComponent : CelesteNetGameComponent { public GhostEmote PlayerIdleTag; public ConcurrentDictionary Ghosts = new(); public ConcurrentDictionary LastFrames = new(); + public ConcurrentDictionary LastDashExt = new(); public ConcurrentDictionary SpriteAnimationIDs = new(StringComparer.OrdinalIgnoreCase); public HashSet UnsupportedSpriteModes = new(); @@ -86,6 +93,7 @@ public override void Initialize() { On.Celeste.PlayerHair.GetHairScale += OnGetHairScale; On.Celeste.PlayerHair.GetHairTexture += OnGetHairTexture; On.Celeste.TrailManager.Add_Vector2_Image_PlayerHair_Vector2_Color_int_float_bool_bool += OnDashTrailAdd; + IL.Celeste.Player.DashUpdate += IlDashUpdate; MethodInfo transitionRoutine = typeof(Level).GetNestedType("d__24", BindingFlags.NonPublic) @@ -113,6 +121,7 @@ protected override void Dispose(bool disposing) { On.Celeste.PlayerHair.GetHairScale -= OnGetHairScale; On.Celeste.PlayerHair.GetHairTexture -= OnGetHairTexture; On.Celeste.TrailManager.Add_Vector2_Image_PlayerHair_Vector2_Color_int_float_bool_bool -= OnDashTrailAdd; + IL.Celeste.Player.DashUpdate -= IlDashUpdate; ILHookTransitionRoutine?.Dispose(); ILHookTransitionRoutine = null; @@ -133,6 +142,7 @@ public void Cleanup() { Session = null; WasIdle = false; WasInteractive = false; + SentDashes = null; foreach (Ghost ghost in Ghosts.Values) ghost?.RemoveSelf(); @@ -175,6 +185,7 @@ public void Handle(CelesteNetConnection con, DataPlayerInfo player) { ghost.RunOnUpdate(ghost => ghost.NameTag.Name = ""); Ghosts.TryRemove(player.ID, out _); LastFrames.TryRemove(player.ID, out _); + LastDashExt.TryRemove(player.ID, out _); Client.Data.FreeOrder(player.ID); return; } @@ -298,7 +309,7 @@ public void Handle(CelesteNetConnection con, DataPlayerFrame frame) { ghost.UpdateGeneric(frame.Position, frame.Scale, frame.Color, frame.Facing, frame.Speed); ghost.UpdateAnimation(frame.CurrentAnimationID, frame.CurrentAnimationFrame); ghost.UpdateHair(frame.Facing, frame.HairColors, frame.HairTexture0, frame.HairSimulateMotion && !state.Idle); - ghost.UpdateDash(frame.DashWasB, frame.DashDir); // TODO: Get rid of this, sync particles separately! + ghost.UpdateDash(frame.DashWasB, frame.DashDir); ghost.UpdateDead(frame.Dead && state.Level == session?.Level); ghost.UpdateFollowers((Settings.InGame.Entities & CelesteNetClientSettings.SyncMode.Receive) == 0 ? Dummy.EmptyArray : frame.Followers); ghost.UpdateHolding((Settings.InGame.Entities & CelesteNetClientSettings.SyncMode.Receive) == 0 ? null : frame.Holding); @@ -546,6 +557,26 @@ public void Handle(CelesteNetConnection con, DataPlayerGrabPlayer grab) { Release: SendReleaseMe(); + } + public void Handle(CelesteNetConnection con, DataPlayerDashExt dashExt) { + if (Client?.Data == null) + return; + LastDashExt[dashExt.Player.ID] = dashExt; + + Session session = Session; + Level level = PlayerBody?.Scene as Level; + bool outside = IsGhostOutside(session, level, dashExt.Player, out DataPlayerState state); + if (!Ghosts.TryGetValue(dashExt.Player.ID, out Ghost ghost) || ghost == null || (ghost.Active && ghost.Scene != level) || outside) { + RemoveGhost(dashExt.Player); + return; + } + if (level == null || outside) + return; + ghost.RunOnUpdate(ghost => { + if (string.IsNullOrEmpty(ghost.NameTag.Name)) + return; + ghost.UpdateDashExt(dashExt.Dashes, dashExt.P_DashColor, dashExt.P_DashColor2); + }); } #endregion @@ -622,6 +653,10 @@ protected Ghost CreateGhost(Level level, DataPlayerInfo player, DataPlayerGraphi ghost.UpdateGraphics(graphics); }); ghost.UpdateGraphics(graphics); + if (LastDashExt.TryGetValue(player.ID, out var lastDashExt)) { + ghost.UpdateDashExt(lastDashExt.Dashes, lastDashExt.P_DashColor, lastDashExt.P_DashColor2); + } + SentDashes = null; // There is a new ghost!... Re-send DataPlayerDashExt to let it sync with we } return ghost; } @@ -936,6 +971,18 @@ private void ILTransitionRoutine(ILContext il) { c.Emit(OpCodes.Pop); c.Emit(OpCodes.Ldc_I4_0); } + } + private void IlDashUpdate(ILContext il) { + ILCursor c = new(il); + if (c.TryGotoNext(MoveType.After, i => i.MatchCallOrCallvirt("Emit"))) { + c.Emit(OpCodes.Ldloc_S, (byte)7); + c.Emit(OpCodes.Ldfld, typeof(ParticleType).GetField("Color", BindingFlags.Public | BindingFlags.Instance)); + c.Emit(OpCodes.Stsfld, typeof(CelesteNetMainComponent).GetField("LastP_DashColor", BindingFlags.NonPublic | BindingFlags.Static)); + + c.Emit(OpCodes.Ldloc_S, (byte)7); + c.Emit(OpCodes.Ldfld, typeof(ParticleType).GetField("Color2", BindingFlags.Public | BindingFlags.Instance)); + c.Emit(OpCodes.Stsfld, typeof(CelesteNetMainComponent).GetField("LastP_DashColor2", BindingFlags.NonPublic | BindingFlags.Static)); + } } #endregion @@ -1093,10 +1140,22 @@ public void SendFrame() { // TODO: Get rid of this, sync particles separately! DashWasB = player.StateMachine.State == Player.StDash ? player.GetWasDashB() : null, - DashDir = player.StateMachine.State == Player.StDash ? player.DashDir : null, + DashDir = player.StateMachine.State == Player.StDash ? player.DashDir : null, Dead = player.Dead - }); + }); + if (SentDashes != player.Dashes || LastP_DashColor != SentP_DashColor || LastP_DashColor2 != SentP_DashColor2) { + SentDashes = player.Dashes; + SentP_DashColor = LastP_DashColor; + SentP_DashColor2 = LastP_DashColor2; + Client?.Send(new DataPlayerDashExt { + Player = Client.PlayerInfo, + + Dashes = player.Dashes, + P_DashColor = SentP_DashColor, + P_DashColor2 = SentP_DashColor, + }); + } } catch (Exception e) { Logger.Log(LogLevel.INF, "client-main", $"Error in SendFrame:\n{e}"); Context.DisposeSafe(); diff --git a/CelesteNet.Client/Entities/Ghost.cs b/CelesteNet.Client/Entities/Ghost.cs index a3962c87..82ef38ad 100644 --- a/CelesteNet.Client/Entities/Ghost.cs +++ b/CelesteNet.Client/Entities/Ghost.cs @@ -29,7 +29,11 @@ public class Ghost : Actor, ITickReceiver { public GhostNameTag NameTag; public GhostEmote IdleTag; - public Color[] HairColors = new[] { Color.White }; + public Color[] HairColors = new[] { Color.White }; + + public int Dashes = 1; + public Color? P_DashColor; + public Color? P_DashColor2; public bool? DashWasB; public Vector2? DashDir; @@ -229,9 +233,18 @@ public override void Update() { if (Holding != null && Holding.Scene != level) level.Add(Holding); - // TODO: Get rid of this, sync particles separately! - if (CelesteNetClientModule.Settings.InGame.OtherPlayerOpacity != 0 && DashWasB != null && DashDir != null && level != null && Speed != Vector2.Zero && level.OnRawInterval(0.02f)) - level.ParticlesFG.Emit(DashWasB.Value ? Player.P_DashB : Player.P_DashA, Center + Calc.Random.Range(Vector2.One * -2f, Vector2.One * 2f), DashDir.Value.Angle()); + if (CelesteNetClientModule.Settings.InGame.OtherPlayerOpacity != 0 && DashWasB != null && DashDir != null && level != null && Speed != Vector2.Zero && level.OnRawInterval(0.02f)) { + ParticleType particle; + if (P_DashColor == null || P_DashColor2 == null) { + particle = DashWasB.Value ? Player.P_DashB : Player.P_DashA; + } else { + particle = new(Player.P_DashA) { + Color = P_DashColor.Value, + Color2 = P_DashColor2.Value + }; + } + level.ParticlesFG.Emit(particle, Center + Calc.Random.Range(Vector2.One * -2f, Vector2.One * 2f), DashDir.Value.Angle()); + } } private void OpacityAdjustAlpha() { @@ -356,6 +369,8 @@ public void UpdateHair(Facings facing, Color[] colors, string texture0, bool sim if (colors.Length <= 0) colors = new[] { Color.White }; + else + Hair.Color = colors[0]; if (PlayerGraphics.HairCount < colors.Length) Array.Resize(ref colors, PlayerGraphics.HairCount); @@ -369,6 +384,12 @@ public void UpdateHair(Facings facing, Color[] colors, string texture0, bool sim public void UpdateDash(bool? wasB, Vector2? dir) { DashWasB = wasB; DashDir = dir; + } + + public void UpdateDashExt(int dashes, Color p_color, Color p_color2) { + Dashes = dashes; + P_DashColor = p_color; + P_DashColor2 = p_color2; } public void UpdateDead(bool dead) { diff --git a/CelesteNet.Shared/CelesteNetClientOptions.cs b/CelesteNet.Shared/CelesteNetClientOptions.cs index f4411881..b956d99b 100644 --- a/CelesteNet.Shared/CelesteNetClientOptions.cs +++ b/CelesteNet.Shared/CelesteNetClientOptions.cs @@ -12,6 +12,6 @@ public class CelesteNetClientOptions { [Flags] public enum CelesteNetSupportedClientFeatures : ulong { None = 0, - LocateCommand = 1 << 0 + LocateCommand = 1 << 0, } } \ No newline at end of file diff --git a/CelesteNet.Shared/DataTypes/DataPlayerDashExt.cs b/CelesteNet.Shared/DataTypes/DataPlayerDashExt.cs new file mode 100644 index 00000000..db6db526 --- /dev/null +++ b/CelesteNet.Shared/DataTypes/DataPlayerDashExt.cs @@ -0,0 +1,52 @@ +using Microsoft.Xna.Framework; +using Monocle; +using System; + +namespace Celeste.Mod.CelesteNet.DataTypes { + public class DataPlayerDashExt : DataType { + public DataPlayerInfo? Player; + public int Dashes; + public Color P_DashColor; + public Color P_DashColor2; + + static DataPlayerDashExt() { + DataID = "playerDashExt"; + } + + public override bool FilterHandle(DataContext ctx) + => Player != null; + + public override MetaType[] GenerateMeta(DataContext ctx) + => new MetaType[] { + new MetaPlayerUpdate(Player), + new MetaOrderedUpdate(Player?.ID ?? uint.MaxValue) + }; + + public override void FixupMeta(DataContext ctx) { + MetaPlayerUpdate playerUpd = Get(ctx); + MetaOrderedUpdate order = Get(ctx); + + order.ID = playerUpd; + Player = playerUpd; + } + + public override void ReadAll(CelesteNetBinaryReader reader) { + Player = reader.ReadRef(); + + Dashes = reader.Read7BitEncodedInt(); + P_DashColor = reader.ReadColor(); + P_DashColor2 = reader.ReadColor(); + + Meta = GenerateMeta(reader.Data); + } + + public override void WriteAll(CelesteNetBinaryWriter writer) { + FixupMeta(writer.Data); + writer.WriteRef(Player); + + writer.Write7BitEncodedInt(Dashes); + writer.Write(P_DashColor); + writer.Write(P_DashColor2); + } + } +} diff --git a/CelesteNet.Shared/DataTypes/DataPlayerFrame.cs b/CelesteNet.Shared/DataTypes/DataPlayerFrame.cs index 663cdd34..309c9b77 100644 --- a/CelesteNet.Shared/DataTypes/DataPlayerFrame.cs +++ b/CelesteNet.Shared/DataTypes/DataPlayerFrame.cs @@ -32,7 +32,6 @@ static DataPlayerFrame() { public Entity? Holding; - // TODO: Get rid of this, sync particles separately! public bool? DashWasB; public Vector2? DashDir; @@ -75,7 +74,7 @@ public override void ReadAll(CelesteNetBinaryReader reader) { CurrentAnimationID = reader.Read7BitEncodedInt(); CurrentAnimationFrame = reader.Read7BitEncodedInt(); - + HairColors = new Color[reader.ReadByte()]; Color lastHairCol = Color.White; for (int i = 0; i < HairColors.Length;) { @@ -250,7 +249,7 @@ private enum Flags { Dashing = 0b00000100, DashB = 0b00001000, Holding = 0b00010000, - Dead = 0b00100000 + Dead = 0b00100000, } public class Entity {