Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CS2MultiplayerMod/Core/Protocol/ProtocolConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ public static class ProtocolConstants
{
/// <summary>
/// Wire-format version. Bump when message layout changes to refuse handshake on mismatch.
/// v69 adds command id 33, fire ignition: one command per building or tree
/// fire start, carrying the target's prefab and position plus the ignition intensity.
/// Only starts travel; the burn, the spread and the extinguish run locally on every
/// machine, the same start-only shape as disaster events. A v68 peer does not know
/// id 33, so the bump refuses it at the handshake instead of dropping its fires silently.
/// v65 adds the barrier-only Begin stage: a join streams its world only to whoever joined,
/// and every other peer crosses the same barrier without being sent or installing one.
/// v65 also widens the accepted range of a course endpoint's split position. A
Expand Down Expand Up @@ -221,7 +226,7 @@ public static class ProtocolConstants
// v66 adds bounded display-only hover geometry to player presence updates.
// v68 batches one brush frame so dense tree strokes do not overflow or trickle in.
// Object-brush display markers are also excluded from terrain synchronization.
public const int ProtocolVersion = 68;
public const int ProtocolVersion = 69;

/// <summary>
/// Hard cap on a single payload, guarding against corrupt length prefixes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal static class GameplayCommandRegistry
GrowableLifecycleCommand.Id,
ModTypeTableCommand.Id, ModStateCommand.Id,
ObjectPlacementBatchCommand.Id, ObjectDeleteBatchCommand.Id,
FireIgniteCommand.Id,
};

internal static void Register(MultiplayerSession session)
Expand Down Expand Up @@ -73,6 +74,7 @@ internal static string Name(ushort id)
case ModStateCommand.Id: return "mod-state";
case ObjectPlacementBatchCommand.Id: return "object-place-batch";
case ObjectDeleteBatchCommand.Id: return "object-delete-batch";
case FireIgniteCommand.Id: return "fire-ignite";
default: return "unknown";
}
}
Expand Down
114 changes: 114 additions & 0 deletions CS2MultiplayerMod/Game/Sync/Commands/Simulation/FireIgniteCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using CS2MultiplayerMod.Core.Protocol;
using CS2MultiplayerMod.Core.Sync;

namespace CS2MultiplayerMod.Game.Sync.Commands
{
/// <summary>
/// "Something caught fire, here, this strongly." Carries a fire start the receiving
/// game cannot derive for itself - which building or tree ignited and how hard -
/// and never the burn itself. Each machine then runs the fire with its own
/// simulation (escalation, spread, rescue, extinguish), so one small message covers
/// a fire of any length. Same start-only shape as <see cref="DisasterEventCommand"/>.
///
/// Only starts travel, in both directions: every machine rolls its own ignitions
/// and reports them, so both cities converge on the union of fires. Ends stay local:
/// each simulation extinguishes on its own clock, and the damage left behind is
/// already host-authoritative through the growable condition sync.
/// </summary>
public sealed class FireIgniteCommand : ISimulationCommand
{
public const ushort Id = 33;
public const int MaxEncodedBytes = 256;

/// <summary>
/// Ceiling on the ignition intensity. The game's own range is small; this only
/// stops a forged unsurvivable inferno, it never constrains a real fire.
/// </summary>
public const float MaxIntensityValue = 1000f;

/// <summary>Name of the ignited building or tree prefab, resolved locally by the receiver.</summary>
public string PrefabName;

/// <summary>World position of the ignited target (buildings do not move).</summary>
public float X, Y, Z;

/// <summary>Ignition strength as the sender's simulation rolled it.</summary>
public float Intensity;

public ushort CommandId => Id;

public void Write(NetworkWriter writer)
{
ValidateForWrite();
writer.WriteString(PrefabName);
writer.WriteFloat(X); writer.WriteFloat(Y); writer.WriteFloat(Z);
writer.WriteFloat(Intensity);
}

public void Read(NetworkReader reader)
{
PrefabName = WireGuard.ReadName(reader);
X = WireGuard.ReadCoordinate(reader);
Y = WireGuard.ReadCoordinate(reader);
Z = WireGuard.ReadCoordinate(reader);
Intensity = ReadIntensity(reader);

if (reader.Remaining != 0)
throw new ProtocolException("Trailing bytes in fire ignite: " + reader.Remaining + ".");
}

public byte[] Encode()
{
var writer = new NetworkWriter(96);
Write(writer);
if (writer.Length > MaxEncodedBytes)
throw new ProtocolException("Fire ignite exceeds the " + MaxEncodedBytes + "-byte cap.");
return writer.ToArray();
}

public static FireIgniteCommand Decode(byte[] body)
{
if (body == null)
throw new ProtocolException("Missing fire ignite body.");
if (body.Length > MaxEncodedBytes)
throw new ProtocolException("Fire ignite exceeds the " + MaxEncodedBytes + "-byte cap.");
var command = new FireIgniteCommand();
command.Read(new NetworkReader(body));
return command;
}

private void ValidateForWrite()
{
if (string.IsNullOrEmpty(PrefabName) || PrefabName.Length > WireGuard.MaxNameLength)
throw new ProtocolException("Invalid fire target prefab name.");
for (int i = 0; i < PrefabName.Length; i++)
if (char.IsControl(PrefabName[i]))
throw new ProtocolException("Control character in fire target prefab name.");
ValidateCoordinate(X, "X");
ValidateCoordinate(Y, "Y");
ValidateCoordinate(Z, "Z");
ValidateIntensity(Intensity);
}

private static float ReadIntensity(NetworkReader reader)
{
float value = WireGuard.ReadFinite(reader);
ValidateIntensity(value);
return value;
}

private static void ValidateIntensity(float value)
{
if (float.IsNaN(value) || float.IsInfinity(value) ||
value < 0f || value > MaxIntensityValue)
throw new ProtocolException("Implausible fire intensity: " + value + ".");
}

private static void ValidateCoordinate(float value, string label)
{
if (float.IsNaN(value) || float.IsInfinity(value) ||
value < -WireGuard.MaxCoordinate || value > WireGuard.MaxCoordinate)
throw new ProtocolException("Invalid fire coordinate " + label + ".");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public partial class SyncRealizeSystem : GameSystemBase
private RouteSyncSystem _routeSync;
private TilePurchaseSyncSystem _tileSync;
private DisasterSyncSystem _disasterSync;
private FireSyncSystem _fireSync;
private GrowableSyncSystem _growableSync;
private Mods.ModStateSyncSystem _modStateSync;

Expand All @@ -45,6 +46,7 @@ protected override void OnCreate()
_routeSync = World.GetOrCreateSystemManaged<RouteSyncSystem>();
_tileSync = World.GetOrCreateSystemManaged<TilePurchaseSyncSystem>();
_disasterSync = World.GetOrCreateSystemManaged<DisasterSyncSystem>();
_fireSync = World.GetOrCreateSystemManaged<FireSyncSystem>();
_growableSync = World.GetOrCreateSystemManaged<GrowableSyncSystem>();
_modStateSync = World.GetOrCreateSystemManaged<Mods.ModStateSyncSystem>();
}
Expand Down Expand Up @@ -179,6 +181,9 @@ protected override void OnUpdate()
// dependency - but they must still be created here: the game's event initialization
// runs later this frame and only ever looks at freshly Created events.
Step("DisasterSync", _disasterSync.RealizePending);
// A realized ignition only sets OnFire on an existing building or tree -
// no definitions, no terrain - so it rides the same slot as disasters.
Step("FireSync", _fireSync.RealizePending);
// Last: what another mod stores is stored against a road, a junction or a building,
// so everything that could still be creating one this frame has to have run. A
// closure whose carrier is genuinely still in the backlog waits in its own hold
Expand Down
Loading