-
-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathMultiplayer.cs
More file actions
379 lines (321 loc) · 15.2 KB
/
Multiplayer.cs
File metadata and controls
379 lines (321 loc) · 15.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
using HarmonyLib;
using Il2CppMicrosoft.AspNetCore.SignalR.Client;
using PolyMod.Multiplayer.ViewModels;
using Polytopia.Data;
using PolytopiaBackendBase;
using PolytopiaBackendBase.Common;
using PolytopiaBackendBase.Game;
using PolytopiaBackendBase.Game.BindingModels;
using UnityEngine;
using Newtonsoft.Json;
namespace PolyMod.Multiplayer;
public static class Client
{
internal const string DEFAULT_SERVER_URL = "https://dev.polydystopia.xyz";
internal const string LOCAL_SERVER_URL = "http://localhost:5051/";
private const string GldMarker = "##GLD:";
internal static bool allowGldMods = false;
// Cache parsed GLD by game Seed to handle rewinds/reloads
private static readonly Dictionary<int, GameLogicData> _gldCache = new();
private static readonly Dictionary<int, int> _versionCache = new(); // Seed -> modGldVersion
internal static void Init()
{
Harmony.CreateAndPatchAll(typeof(Client));
BuildConfig buildConfig = BuildConfigHelper.GetSelectedBuildConfig();
buildConfig.buildServerURL = BuildServerURL.Custom;
buildConfig.customServerURL = LOCAL_SERVER_URL;
Plugin.logger.LogInfo($"Multiplayer> Server URL set to: {Plugin.config.backendUrl}");
Plugin.logger.LogInfo("Multiplayer> GLD patches applied");
}
[HarmonyPostfix]
[HarmonyPatch(typeof(MultiplayerScreen), nameof(MultiplayerScreen.Show))]
public static void MultiplayerScreen_Show(MultiplayerScreen __instance)
{
__instance.multiplayerSelectionScreen.TournamentsButton.gameObject.SetActive(false);
}
[HarmonyPostfix]
[HarmonyPatch(typeof(StartScreen), nameof(StartScreen.Start))]
private static void StartScreen_Start(StartScreen __instance)
{
__instance.highscoreButton.gameObject.SetActive(false);
__instance.weeklyChallengesButton.gameObject.SetActive(false);
}
[HarmonyPostfix]
[HarmonyPatch(typeof(SystemInfo), nameof(SystemInfo.deviceUniqueIdentifier), MethodType.Getter)]
public static void SteamClient_get_SteamId(ref string __result)
{
if (Plugin.config.overrideDeviceId != string.Empty)
{
__result = Plugin.config.overrideDeviceId;
}
}
/// <summary>
/// After GameState deserialization, check for trailing GLD version ID and set mockedGameLogicData.
/// The server appends "##GLD:" + modGldVersion (int) after the normal serialized data.
/// </summary>
[HarmonyPostfix]
[HarmonyPatch(typeof(GameState), nameof(GameState.Deserialize))]
private static void Deserialize_Postfix(GameState __instance, BinaryReader __0)
{
if(!allowGldMods) return;
Plugin.logger?.LogDebug("Deserialize_Postfix: Entered");
try
{
var reader = __0;
if (reader == null)
{
Plugin.logger?.LogWarning("Deserialize_Postfix: reader is null");
return;
}
var position = reader.BaseStream.Position;
var length = reader.BaseStream.Length;
var remaining = length - position;
Plugin.logger?.LogDebug($"Deserialize_Postfix: Stream position={position}, length={length}, remaining={remaining}");
// Check if there's more data after normal deserialization
if (position >= length)
{
Plugin.logger?.LogDebug("Deserialize_Postfix: No trailing data (position >= length)");
var sd = __instance.Seed;
if (_gldCache.TryGetValue(sd, out var cachedGld))
{
__instance.mockedGameLogicData = cachedGld;
var cachedVersion = _versionCache.GetValueOrDefault(sd, -1);
Plugin.logger?.LogInfo($"Deserialize_Postfix: Applied cached GLD for Seed={sd}, ModGldVersion={cachedVersion}");
}
return;
}
Plugin.logger?.LogDebug($"Deserialize_Postfix: Found {remaining} bytes of trailing data, attempting to read marker");
var marker = reader.ReadString();
Plugin.logger?.LogDebug($"Deserialize_Postfix: Read marker string: '{marker}'");
if (marker != GldMarker)
{
Plugin.logger?.LogDebug($"Deserialize_Postfix: Marker mismatch - expected '{GldMarker}', got '{marker}'");
return;
}
Plugin.logger?.LogInfo($"Deserialize_Postfix: Found GLD marker '{GldMarker}'");
var modGldVersion = reader.ReadInt32();
Plugin.logger?.LogInfo($"Deserialize_Postfix: Found embedded ModGldVersion: {modGldVersion}");
Plugin.logger?.LogDebug($"Deserialize_Postfix: Fetching GLD from server for version {modGldVersion}");
var gldJson = FetchGldById(modGldVersion);
if (string.IsNullOrEmpty(gldJson))
{
Plugin.logger?.LogError($"Deserialize_Postfix: Failed to fetch GLD for ModGldVersion: {modGldVersion}");
return;
}
Plugin.logger?.LogDebug($"Deserialize_Postfix: Parsing GLD JSON ({gldJson.Length} chars)");
var customGld = new GameLogicData();
customGld.Parse(gldJson);
__instance.mockedGameLogicData = customGld;
// Cache for subsequent deserializations (rewinds, reloads)
var seed = __instance.Seed;
_gldCache[seed] = customGld;
_versionCache[seed] = modGldVersion;
Plugin.logger?.LogInfo($"Deserialize_Postfix: Successfully set mockedGameLogicData from ModGldVersion: {modGldVersion}, cached for Seed={seed}");
}
catch (EndOfStreamException)
{
Plugin.logger?.LogDebug("Deserialize_Postfix: EndOfStreamException - no trailing data");
}
catch (Exception ex)
{
Plugin.logger?.LogError($"Deserialize_Postfix: Exception: {ex.GetType().Name}: {ex.Message}");
Plugin.logger?.LogDebug($"Deserialize_Postfix: Stack trace: {ex.StackTrace}");
}
}
/// <summary>
/// Fetch GLD from server using ModGldVersion ID
/// </summary>
private static string? FetchGldById(int modGldVersion)
{
if(!allowGldMods) return null;
try
{
using var client = new HttpClient();
var url = $"{Plugin.config.backendUrl.TrimEnd('/')}/api/mods/gld/{modGldVersion}";
Plugin.logger?.LogDebug($"FetchGldById: Requesting URL: {url}");
var response = client.GetAsync(url).Result;
Plugin.logger?.LogDebug($"FetchGldById: Response status: {response.StatusCode}");
if (response.IsSuccessStatusCode)
{
var gld = response.Content.ReadAsStringAsync().Result;
Plugin.logger?.LogInfo($"FetchGldById: Successfully fetched mod GLD ({gld.Length} chars)");
return gld;
}
var errorContent = response.Content.ReadAsStringAsync().Result;
Plugin.logger?.LogError($"FetchGldById: Failed with status {response.StatusCode}: {errorContent}");
}
catch (Exception ex)
{
Plugin.logger?.LogError($"FetchGldById: Exception: {ex.GetType().Name}: {ex.Message}");
if (ex.InnerException != null)
{
Plugin.logger?.LogError($"FetchGldById: Inner exception: {ex.InnerException.Message}");
}
}
return null;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(BackendAdapter), nameof(BackendAdapter.StartLobbyGame))]
private static bool BackendAdapter_StartLobbyGame_Modded(
ref Il2CppSystem.Threading.Tasks.Task<ServerResponse<LobbyGameViewModel>> __result,
BackendAdapter __instance,
StartLobbyBindingModel model)
{
Plugin.logger.LogInfo("Multiplayer> BackendAdapter_StartLobbyGame_Modded");
var taskCompletionSource = new Il2CppSystem.Threading.Tasks.TaskCompletionSource<ServerResponse<LobbyGameViewModel>>();
_ = HandleStartLobbyGameModded(taskCompletionSource, __instance, model);
__result = taskCompletionSource.Task;
return false;
}
private static async System.Threading.Tasks.Task HandleStartLobbyGameModded(
Il2CppSystem.Threading.Tasks.TaskCompletionSource<ServerResponse<LobbyGameViewModel>> tcs,
BackendAdapter instance,
StartLobbyBindingModel model)
{
try
{
var lobbyResponse = await PolytopiaBackendAdapter.Instance.GetLobby(new GetLobbyBindingModel
{
LobbyId = model.LobbyId
});
Plugin.logger.LogInfo($"Multiplayer> Lobby processed {lobbyResponse.Success}");
LobbyGameViewModel lobbyGameViewModel = lobbyResponse.Data;
Plugin.logger.LogInfo("Multiplayer> Lobby received");
(byte[] serializedGameState, string gameSettingsJson) = CreateMultiplayerGame(
lobbyGameViewModel,
VersionManager.GameVersion,
VersionManager.GameLogicDataVersion
);
Plugin.logger.LogInfo("Multiplayer> GameState and Settiings created");
var setupGameDataViewModel = new SetupGameDataViewModel
{
lobbyId = lobbyGameViewModel.Id.ToString(),
serializedGameState = serializedGameState,
gameSettingsJson = gameSettingsJson
};
var setupData = System.Text.Json.JsonSerializer.Serialize(setupGameDataViewModel);
var serverResponse = await instance.HubConnection.InvokeAsync<ServerResponse<LobbyGameViewModel>>(
"StartLobbyGameModded",
setupData,
Il2CppSystem.Threading.CancellationToken.None
);
Plugin.logger.LogInfo("Multiplayer> Invoked StartLobbyGameModded");
tcs.SetResult(serverResponse);
}
catch (Exception ex)
{
Plugin.logger.LogError("Multiplayer> Error during HandleStartLobbyGameModded: " + ex.Message);
tcs.SetException(new Il2CppSystem.Exception(ex.Message));
}
}
public static (byte[] serializedGameState, string gameSettingsJson) CreateMultiplayerGame(LobbyGameViewModel lobby,
int gameVersion, int gameLogicVersion)
{
var lobbyMapSize = lobby.MapSize;
var settings = new GameSettings();
settings.ApplyLobbySettings(lobby);
if (settings.LiveGamePreset)
{
settings.SetLiveModePreset();
}
foreach (var participatorViewModel in lobby.Participators)
{
var humanPlayer = new PlayerData
{
type = PlayerDataType.LocalUser,
state = PlayerDataFriendshipState.Accepted,
knownTribe = true,
tribe = (TribeType)participatorViewModel.SelectedTribe,
tribeMix = (TribeType)participatorViewModel.SelectedTribe,
skinType = (SkinType)participatorViewModel.SelectedTribeSkin,
defaultName = participatorViewModel.GetNameInternal()
};
humanPlayer.profile.id = participatorViewModel.UserId;
humanPlayer.profile.SetName(participatorViewModel.GetNameInternal());
SerializationHelpers.FromByteArray<AvatarState>(participatorViewModel.AvatarStateData, out var avatarState);
humanPlayer.profile.avatarState = avatarState;
settings.AddPlayer(humanPlayer);
}
foreach (var botDifficulty in lobby.Bots)
{
var botGuid = Il2CppSystem.Guid.NewGuid();
var botPlayer = new PlayerData
{
type = PlayerDataType.Bot,
state = PlayerDataFriendshipState.Accepted,
knownTribe = true,
tribe = Enum.GetValues<TribeType>().Where(t => t != TribeType.None)
.OrderBy(x => Il2CppSystem.Guid.NewGuid()).First()
};
;
botPlayer.botDifficulty = (BotDifficulty)botDifficulty;
botPlayer.skinType = SkinType.Default;
botPlayer.defaultName = "Bot" + botGuid;
botPlayer.profile.id = botGuid;
settings.AddPlayer(botPlayer);
}
GameState gameState = new GameState()
{
Version = gameVersion,
Settings = settings,
PlayerStates = new Il2CppSystem.Collections.Generic.List<PlayerState>()
};
for (int index = 0; index < settings.GetPlayerCount(); ++index)
{
PlayerData player = settings.GetPlayer(index);
if (player.type != PlayerDataType.Bot)
{
var nullableGuid = new Il2CppSystem.Nullable<Il2CppSystem.Guid>(player.profile.id);
if (!nullableGuid.HasValue)
{
throw new Exception("GUID was not set properly!");
}
PlayerState playerState = new PlayerState()
{
Id = (byte)(index + 1),
AccountId = nullableGuid,
AutoPlay = player.type == PlayerDataType.Bot,
UserName = player.GetNameInternal(),
tribe = player.tribe,
tribeMix = player.tribeMix,
hasChosenTribe = true,
skinType = player.skinType
};
gameState.PlayerStates.Add(playerState);
Plugin.logger.LogInfo($"Multiplayer> Created player: {playerState}");
}
else
{
GameStateUtils.AddAIOpponent(gameState, GameStateUtils.GetRandomPickableTribe(gameState),
GameSettings.HandicapFromDifficulty(player.botDifficulty), player.skinType);
}
}
GameStateUtils.SetPlayerColors(gameState);
GameStateUtils.AddNaturePlayer(gameState);
Plugin.logger.LogInfo("Multiplayer> Creating world...");
ushort num = (ushort)Math.Max(lobbyMapSize,
(int)MapDataExtensions.GetMinimumMapSize(gameState.PlayerCount));
gameState.Map = new MapData(num, num);
MapGeneratorSettings generatorSettings = settings.GetMapGeneratorSettings();
new MapGenerator().Generate(gameState, generatorSettings);
Plugin.logger.LogInfo($"Multiplayer> Creating initial state for {gameState.PlayerCount} players...");
foreach (PlayerState player in gameState.PlayerStates)
{
foreach (PlayerState otherPlayer in gameState.PlayerStates)
player.aggressions[otherPlayer.Id] = 0;
if (player.Id != byte.MaxValue && gameState.GameLogicData.TryGetData(player.tribe, out TribeData tribeData))
{
player.Currency = tribeData.startingStars;
TileData tile = gameState.Map.GetTile(player.startTile);
UnitState unitState = ActionUtils.TrainUnitScored(gameState, player, tile, tribeData.startingUnit);
unitState.attacked = false;
unitState.moved = false;
}
}
Plugin.logger.LogInfo("Multiplayer> Session created successfully");
gameState.CommandStack.Add((CommandBase)new StartMatchCommand((byte)1));
var serializedGameState = SerializationHelpers.ToByteArray(gameState, gameState.Version);
return (serializedGameState,
JsonConvert.SerializeObject(gameState.Settings));
}
}