Implement game save/load backend.

This commit is contained in:
Paul Chote
2019-04-14 20:58:11 +00:00
committed by reaperrr
parent 877215c86a
commit 1f3b30c2d2
9 changed files with 618 additions and 10 deletions

View File

@@ -54,6 +54,7 @@ namespace OpenRA.Server
// Managed by LobbyCommands
public MapPreview Map;
public GameSave GameSave = null;
readonly int randomSeed;
readonly TcpListener listener;
@@ -558,6 +559,9 @@ namespace OpenRA.Server
InterpretServerOrders(conn, data);
else
DispatchOrdersToClients(conn, frame, data);
if (GameSave != null && conn != null)
GameSave.DispatchOrders(conn, frame, data);
}
void InterpretServerOrders(Connection conn, byte[] data)
@@ -661,6 +665,119 @@ namespace OpenRA.Server
SyncClientPing();
break;
}
case "GameSaveTraitData":
{
if (GameSave != null)
{
var data = MiniYaml.FromString(so.Data)[0];
GameSave.AddTraitData(int.Parse(data.Key), data.Value);
}
break;
}
case "CreateGameSave":
{
if (GameSave != null)
{
// Sanitize potentially malicious input
var filename = so.Data;
var invalidIndex = -1;
var invalidChars = Path.GetInvalidFileNameChars();
while ((invalidIndex = filename.IndexOfAny(invalidChars)) != -1)
filename = filename.Remove(invalidIndex, 1);
var baseSavePath = Platform.ResolvePath(
Platform.SupportDirPrefix,
"Saves",
ModData.Manifest.Id,
ModData.Manifest.Metadata.Version);
if (!Directory.Exists(baseSavePath))
Directory.CreateDirectory(baseSavePath);
GameSave.Save(Path.Combine(baseSavePath, filename));
DispatchOrdersToClients(null, 0, new ServerOrder("GameSaved", filename).Serialize());
}
break;
}
case "LoadGameSave":
{
if (Dedicated || State >= ServerState.GameStarted)
break;
// Sanitize potentially malicious input
var filename = so.Data;
var invalidIndex = -1;
var invalidChars = Path.GetInvalidFileNameChars();
while ((invalidIndex = filename.IndexOfAny(invalidChars)) != -1)
filename = filename.Remove(invalidIndex, 1);
var savePath = Platform.ResolvePath(
Platform.SupportDirPrefix,
"Saves",
ModData.Manifest.Id,
ModData.Manifest.Metadata.Version,
filename);
GameSave = new GameSave(savePath);
LobbyInfo.GlobalSettings = GameSave.GlobalSettings;
LobbyInfo.Slots = GameSave.Slots;
// Reassign clients to slots
// - Bot ordering is preserved
// - Humans are assigned on a first-come-first-serve basis
// - Leftover humans become spectators
// Start by removing all bots and assigning all players as spectators
foreach (var c in LobbyInfo.Clients)
{
if (c.Bot != null)
{
LobbyInfo.Clients.Remove(c);
var ping = LobbyInfo.PingFromClient(c);
if (ping != null)
LobbyInfo.ClientPings.Remove(ping);
}
else
c.Slot = null;
}
// Rebuild/remap the saved client state
// TODO: Multiplayer saves should leave all humans as spectators so they can manually pick slots
var adminClientIndex = LobbyInfo.Clients.First(c => c.IsAdmin).Index;
foreach (var kv in GameSave.SlotClients)
{
if (kv.Value.Bot != null)
{
var bot = new Session.Client()
{
Index = ChooseFreePlayerIndex(),
State = Session.ClientState.NotReady,
BotControllerClientIndex = adminClientIndex
};
kv.Value.ApplyTo(bot);
LobbyInfo.Clients.Add(bot);
}
else
{
// This will throw if the server doesn't have enough human clients to fill all player slots
// See TODO above - this isn't a problem in practice because MP saves won't use this
var client = LobbyInfo.Clients.First(c => c.Slot == null);
kv.Value.ApplyTo(client);
}
}
SyncLobbyInfo();
SyncLobbyClients();
SyncClientPing();
break;
}
}
@@ -810,6 +927,12 @@ namespace OpenRA.Server
if (LobbyInfo.NonBotClients.Count() == 1)
LobbyInfo.GlobalSettings.OrderLatency = 1;
// Enable game saves for singleplayer missions only
// TODO: Enable for skirmish once the AI supports state-restoration
// TODO: Enable for multiplayer (non-dedicated servers only) once the lobby UI has been created
LobbyInfo.GlobalSettings.GameSavesEnabled = !Dedicated && Map.Visibility == MapVisibility.MissionSelector &&
LobbyInfo.NonBotClients.Count() == 1;
SyncLobbyInfo();
State = ServerState.GameStarted;
@@ -817,11 +940,37 @@ namespace OpenRA.Server
foreach (var d in Conns)
DispatchOrdersToClient(c, d.PlayerIndex, 0x7FFFFFFF, new byte[] { 0xBF });
if (GameSave == null && LobbyInfo.GlobalSettings.GameSavesEnabled)
GameSave = new GameSave();
var startGameData = "";
if (GameSave != null)
{
GameSave.StartGame(LobbyInfo, Map);
if (GameSave.LastOrdersFrame >= 0)
{
startGameData = new List<MiniYamlNode>()
{
new MiniYamlNode("SaveLastOrdersFrame", GameSave.LastOrdersFrame.ToString()),
new MiniYamlNode("SaveSyncFrame", GameSave.LastSyncFrame.ToString())
}.WriteToString();
}
}
DispatchOrders(null, 0,
new ServerOrder("StartGame", "").Serialize());
new ServerOrder("StartGame", startGameData).Serialize());
foreach (var t in serverTraits.WithInterface<IStartGame>())
t.GameStarted(this);
if (GameSave != null && GameSave.LastOrdersFrame >= 0)
{
GameSave.ParseOrders(LobbyInfo, (frame, client, data) =>
{
foreach (var c in Conns)
DispatchOrdersToClient(c, client, frame, data);
});
}
}
}
}