Files
OpenRA/OpenRA.Game/Network/GameSave.cs
RoosterDragon b58c1ea5bc Provide names and pools when creating MiniYaml.
- Rename the filename parameter to name and make it mandatory. Review all callers and ensure a useful string is provided as input, to ensure sufficient context is included for logging and debugging. This can be a filename, url, or any arbitrary text so include whatever context seems reasonable.
- When several MiniYamls are created that have similar content, provide a shared string pool. This allows strings that are common between all the yaml to be shared, reducing long term memory usage. We also change the pool from a dictionary to a set. Originally a Dictionary had to be used so we could call TryGetValue to get a reference to the pooled string. Now that more recent versions of dotnet provide a TryGetValue on HashSet, we can use a set directly without the memory wasted by having to store both keys and values in a dictionary.
2024-01-21 12:39:10 +02:00

324 lines
10 KiB
C#

#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using OpenRA.Primitives;
using OpenRA.Server;
namespace OpenRA.Network
{
public class SlotClient
{
public readonly Color Color;
public readonly string Faction;
public readonly int SpawnPoint;
public readonly int Team;
public readonly int Handicap;
public readonly string Slot;
public readonly string Bot;
public readonly bool IsAdmin;
public readonly string BotName;
public SlotClient() { }
public SlotClient(Session.Client client)
{
Color = client.Color;
Faction = client.Faction;
SpawnPoint = client.SpawnPoint;
Team = client.Team;
Handicap = client.Handicap;
Slot = client.Slot;
Bot = client.Bot;
IsAdmin = client.IsAdmin;
if (client.Bot != null)
BotName = client.Name;
}
public void ApplyTo(Session.Client client)
{
client.Color = Color;
client.Faction = Faction;
client.SpawnPoint = SpawnPoint;
client.Team = Team;
client.Handicap = Handicap;
client.Slot = Slot;
client.Bot = Bot;
client.IsAdmin = IsAdmin;
if (Bot != null)
client.Name = BotName;
}
public static SlotClient Deserialize(MiniYaml data)
{
return FieldLoader.Load<SlotClient>(data);
}
public MiniYamlNode Serialize(string key)
{
return new MiniYamlNode($"SlotClient@{key}", FieldSaver.Save(this));
}
}
public class GameSave
{
public const int EOFMarker = -2;
public const int MetadataMarker = -1;
public const int TraitDataMarker = -3;
readonly MemoryStream ordersStream = new();
// Loaded from file and updated during gameplay
public int LastOrdersFrame { get; private set; }
public int LastSyncFrame { get; private set; }
byte[] lastSyncPacket = Array.Empty<byte>();
// Loaded from file or set on game start
public Session.Global GlobalSettings { get; private set; }
public Dictionary<string, Session.Slot> Slots { get; private set; }
public Dictionary<string, SlotClient> SlotClients { get; private set; }
public Dictionary<int, MiniYaml> TraitData = new();
// Set on game start
int[] clientsBySlotIndex = Array.Empty<int>();
int firstBotSlotIndex = -1;
public GameSave()
{
LastOrdersFrame = -1;
Slots = new Dictionary<string, Session.Slot>();
}
public GameSave(string filepath)
{
using (var rs = File.OpenRead(filepath))
{
rs.Seek(-12, SeekOrigin.End);
var metadataOffset = rs.ReadInt32();
var traitDataOffset = rs.ReadInt32();
if (rs.ReadInt32() != EOFMarker)
throw new InvalidDataException("Invalid orasav file");
rs.Seek(metadataOffset, SeekOrigin.Begin);
if (rs.ReadInt32() != MetadataMarker)
throw new InvalidDataException("Invalid orasav file");
LastOrdersFrame = rs.ReadInt32();
LastSyncFrame = rs.ReadInt32();
lastSyncPacket = rs.ReadBytes(Order.SyncHashOrderLength);
var globalSettings = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:globalSettings");
GlobalSettings = Session.Global.Deserialize(globalSettings[0].Value);
var slots = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:slots");
Slots = new Dictionary<string, Session.Slot>();
foreach (var s in slots)
{
var slot = Session.Slot.Deserialize(s.Value);
Slots.Add(slot.PlayerReference, slot);
}
var slotClients = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:slotClients");
SlotClients = new Dictionary<string, SlotClient>();
foreach (var s in slotClients)
{
var slotClient = SlotClient.Deserialize(s.Value);
SlotClients.Add(slotClient.Slot, slotClient);
}
if (rs.Position != traitDataOffset || rs.ReadInt32() != TraitDataMarker)
throw new InvalidDataException("Invalid orasav file");
var traitData = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:traitData");
foreach (var td in traitData)
TraitData.Add(Exts.ParseInt32Invariant(td.Key), td.Value);
rs.Seek(0, SeekOrigin.Begin);
ordersStream.Write(rs.ReadBytes(metadataOffset), 0, metadataOffset);
}
}
public void StartGame(Session lobbyInfo, MapPreview map)
{
// Game orders are mapped from a client index to the slot that they occupy
// Orders from spectators are ignored, which is not a problem in practice
// because all immediate orders are also ignored
clientsBySlotIndex = lobbyInfo.Slots.Keys.Select(s =>
{
var client = lobbyInfo.ClientInSlot(s);
return client != null ? client.Index : -1;
}).ToArray();
// Perform a deep clone by round-tripping the data
GlobalSettings = Session.Global.Deserialize(lobbyInfo.GlobalSettings.Serialize().Value);
Slots = new Dictionary<string, Session.Slot>();
SlotClients = new Dictionary<string, SlotClient>();
foreach (var s in lobbyInfo.Slots)
{
Slots[s.Key] = Session.Slot.Deserialize(s.Value.Serialize().Value);
var playerReference = map.Players.Players[s.Value.PlayerReference];
var client = lobbyInfo.ClientInSlot(s.Key);
// Only save the client state relevant to the game (faction, team, etc).
// Admin and bot controller state is inherited and/or reassigned by the server at load time
if (playerReference.Playable && client != null)
{
SlotClients[s.Key] = new SlotClient(client);
// See HACK comment in DispatchOrders about reassigning bot orders
if (client.Bot != null && firstBotSlotIndex < 0)
firstBotSlotIndex = clientsBySlotIndex.IndexOf(client.Index);
}
}
}
public void DispatchOrders(Connection conn, int frame, byte[] data)
{
// Sync packet - we only care about the last value
if (data.Length > 0 && data[0] == (byte)OrderType.SyncHash && frame > LastSyncFrame)
{
if (data.Length != Order.SyncHashOrderLength)
{
Log.Write("debug", $"Dropped sync order with length {data.Length}. Expected length {Order.SyncHashOrderLength}.");
return;
}
LastSyncFrame = frame;
lastSyncPacket = data;
}
if (frame <= LastOrdersFrame)
return;
// Ignore immediate orders
if (data.Length > 0 && data[0] == 0xFE)
return;
var clientSlot = clientsBySlotIndex.IndexOf(conn.PlayerIndex);
// Handle orders that were sent by spectators
if (clientSlot == -1)
{
// HACK: Assume that this is a bot order sent by its controller client
// who is a spectator. The network data doesn't contain enough information
// for us to confirm this, or to know which bot this is supposed to belong to...
//
// For skirmish games it is sufficient to map everything to the first bot,
// because even if the bot choice is wrong, the bot-to-client remapping in ParseOrders
// will give the right client as there is only one human client to choose from!
// TODO: This will need to be fixed properly before implementing multiplayer saves
clientSlot = firstBotSlotIndex;
}
ordersStream.Write(data.Length + 8);
ordersStream.Write(frame);
ordersStream.Write(clientSlot);
ordersStream.Write(data);
LastOrdersFrame = frame;
}
public void ParseOrders(Session lobbyInfo, Action<int, int, byte[]> packetFn)
{
// Send the trait data first to guarantee that it is available when needed
foreach (var kv in TraitData)
{
var data = new List<MiniYamlNode>() { new(kv.Key.ToStringInvariant(), kv.Value) }.WriteToString();
packetFn(0, 0, Order.FromTargetString("SaveTraitData", data, true).Serialize());
}
ordersStream.Seek(0, SeekOrigin.Begin);
while (ordersStream.Position < ordersStream.Length)
{
var dataLength = ordersStream.ReadInt32() - 8;
var frame = ordersStream.ReadInt32();
var slot = ordersStream.ReadInt32();
var data = ordersStream.ReadBytes(dataLength);
// Remap bot orders to their controller client
var clientIndex = clientsBySlotIndex[slot];
var client = lobbyInfo.ClientWithIndex(clientIndex);
if (client.Bot != null)
clientIndex = client.BotControllerClientIndex;
packetFn(frame, clientIndex, data);
}
// Send sync hash to validate restore
packetFn(LastSyncFrame, 0, lastSyncPacket);
}
public void AddTraitData(int traitIndex, MiniYaml data)
{
TraitData[traitIndex] = data;
}
public void Save(string path)
{
// File format:
// - List of orders in network frame format
// - Metadata start marker
// - Last frame number containing orders (int32)
// - Last frame number containing sync hash (int32)
// - Last sync packet (5 x byte)
// - Lobby global settings (yaml)
// - Lobby slots (yaml)
// - Lobby slot-client data (yaml)
// - Trait data start marker
// - Custom trait yaml
// - File offset of metadata start marker
// - File offset of custom trait data
// - Metadata end marker
using (var file = File.Create(path))
{
ordersStream.Seek(0, SeekOrigin.Begin);
ordersStream.CopyTo(file);
file.Write(MetadataMarker);
file.Write(LastOrdersFrame);
file.Write(LastSyncFrame);
file.Write(lastSyncPacket, 0, Order.SyncHashOrderLength);
var globalSettingsNodes = new List<MiniYamlNode>() { GlobalSettings.Serialize() };
file.WriteLengthPrefixedString(Encoding.UTF8, globalSettingsNodes.WriteToString());
var slotNodes = Slots
.Select(s => s.Value.Serialize())
.ToList();
file.WriteLengthPrefixedString(Encoding.UTF8, slotNodes.WriteToString());
var slotClientNodes = SlotClients
.Select(s => s.Value.Serialize(s.Key))
.ToList();
file.WriteLengthPrefixedString(Encoding.UTF8, slotClientNodes.WriteToString());
var traitDataOffset = file.Length;
file.Write(TraitDataMarker);
var traitDataNodes = TraitData
.Select(kv => new MiniYamlNode(kv.Key.ToStringInvariant(), kv.Value))
.ToList();
file.WriteLengthPrefixedString(Encoding.UTF8, traitDataNodes.WriteToString());
file.Write((int)ordersStream.Length);
file.Write((int)traitDataOffset);
file.Write(EOFMarker);
}
}
}
}