using Robust.Shared;
using Robust.Shared.ContentPack;
using Robust.Shared.Replays;
+using Robust.Shared.Serialization.Manager;
+using Robust.Shared.Serialization.Markdown;
+using Robust.Shared.Serialization.Markdown.Mapping;
+using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Utility;
namespace Content.Server.GameTicking;
{
[Dependency] private readonly IReplayRecordingManager _replays = default!;
[Dependency] private readonly IResourceManager _resourceManager = default!;
+ [Dependency] private readonly ISerializationManager _serialman = default!;
+
private ISawmill _sawmillReplays = default!;
private void InitializeReplays()
{
_replays.RecordingFinished += ReplaysOnRecordingFinished;
+ _replays.RecordingStopped += ReplaysOnRecordingStopped;
}
/// <summary>
data.Directory.Rename(data.Path, state.MoveToPath.Value);
}
+ private void ReplaysOnRecordingStopped(MappingDataNode metadata)
+ {
+ // Write round info like map and round end summery into the replay_final.yml file. Useful for external parsers.
+
+ metadata["map"] = new ValueDataNode(_gameMapManager.GetSelectedMap()?.MapName);
+ metadata["gamemode"] = new ValueDataNode(CurrentPreset != null ? Loc.GetString(CurrentPreset.ModeTitle) : string.Empty);
+ metadata["roundEndPlayers"] = _serialman.WriteValue(_replayRoundPlayerInfo);
+ metadata["roundEndText"] = new ValueDataNode(_replayRoundText);
+ metadata["server_id"] = new ValueDataNode(_configurationManager.GetCVar(CCVars.ServerId));
+ // These should be set to null to prepare them for the next round.
+ _replayRoundPlayerInfo = null;
+ _replayRoundText = null;
+ }
+
private ResPath GetAutoReplayPath()
{
var cfgValue = _cfg.GetCVar(CCVars.ReplayAutoRecordName);
[ViewVariables]
private GameRunLevel _runLevel;
+ private RoundEndMessageEvent.RoundEndPlayerInfo[]? _replayRoundPlayerInfo;
+
+ private string? _replayRoundText;
+
[ViewVariables]
public GameRunLevel RunLevel
{
PlayerOOCName = contentPlayerData?.Name ?? "(IMPOSSIBLE: REGISTERED MIND WITH NO OWNER)",
// Character name takes precedence over current entity name
PlayerICName = playerIcName,
+ PlayerGuid = userId,
PlayerNetEntity = GetNetEntity(entity),
Role = antag
? roles.First(role => role.Antagonist).Name
: roles.FirstOrDefault().Name ?? Loc.GetString("game-ticker-unknown-role"),
Antag = antag,
+ JobPrototypes = roles.Where(role => !role.Antagonist).Select(role => role.Prototype).ToArray(),
+ AntagPrototypes = roles.Where(role => role.Antagonist).Select(role => role.Prototype).ToArray(),
Observer = observer,
Connected = connected
};
RaiseNetworkEvent(new RoundEndMessageEvent(gamemodeTitle, roundEndText, roundDuration, RoundId,
listOfPlayerInfoFinal.Length, listOfPlayerInfoFinal, LobbySong, sound));
+
+ _replayRoundPlayerInfo = listOfPlayerInfoFinal;
+ _replayRoundText = roundEndText;
}
private async void SendRoundEndDiscordMessage()
using Content.Shared.Roles;
+using Robust.Shared.Network;
using Robust.Shared.Replays;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Markdown.Mapping;
}
}
- [Serializable, NetSerializable]
- public sealed class RoundEndMessageEvent : EntityEventArgs
+ [Serializable, NetSerializable, DataDefinition]
+ public sealed partial class RoundEndMessageEvent : EntityEventArgs
{
- [Serializable, NetSerializable]
- public struct RoundEndPlayerInfo
+ [Serializable, NetSerializable, DataDefinition]
+ public partial struct RoundEndPlayerInfo
{
+ [DataField]
public string PlayerOOCName;
+
+ [DataField]
public string? PlayerICName;
+
+ [DataField, NonSerialized]
+ public NetUserId? PlayerGuid;
+
public string Role;
+
+ [DataField, NonSerialized]
+ public string[] JobPrototypes;
+
+ [DataField, NonSerialized]
+ public string[] AntagPrototypes;
+
public NetEntity? PlayerNetEntity;
+
+ [DataField]
public bool Antag;
+
+ [DataField]
public bool Observer;
+
public bool Connected;
}
/// <param name="Name">Name of the role.</param>
/// <param name="Antagonist">Whether or not this role makes this player an antagonist.</param>
/// <param name="PlayTimeTrackerId">The <see cref="PlayTimeTrackerPrototype"/> id associated with the role.</param>
-public readonly record struct RoleInfo(Component Component, string Name, bool Antagonist, string? PlayTimeTrackerId);
+/// <param name="Prototype">The prototype ID of the role</param>
+public readonly record struct RoleInfo(Component Component, string Name, bool Antagonist, string? PlayTimeTrackerId, string Prototype);
private void OnJobGetAllRoles(EntityUid uid, JobComponent component, ref MindGetAllRolesEvent args)
{
var name = "game-ticker-unknown-role";
+ var prototype = "";
string? playTimeTracker = null;
if (component.Prototype != null && _prototypes.TryIndex(component.Prototype, out JobPrototype? job))
{
name = job.Name;
+ prototype = job.ID;
playTimeTracker = job.PlayTimeTracker;
}
name = Loc.GetString(name);
- args.Roles.Add(new RoleInfo(component, name, false, playTimeTracker));
+ args.Roles.Add(new RoleInfo(component, name, false, playTimeTracker, prototype));
}
protected void SubscribeAntagEvents<T>() where T : AntagonistRoleComponent
SubscribeLocalEvent((EntityUid _, T component, ref MindGetAllRolesEvent args) =>
{
var name = "game-ticker-unknown-role";
+ var prototype = "";
if (component.PrototypeId != null && _prototypes.TryIndex(component.PrototypeId, out AntagPrototype? antag))
{
name = antag.Name;
+ prototype = antag.ID;
}
name = Loc.GetString(name);
- args.Roles.Add(new RoleInfo(component, name, true, null));
+ args.Roles.Add(new RoleInfo(component, name, true, null, prototype));
});
SubscribeLocalEvent((EntityUid _, T _, ref MindIsAntagonistEvent args) => args.IsAntagonist = true);