Terraria ModLoader  0.11.5
A framework for Terraria mods
ModNet.cs
Go to the documentation of this file.
1 using Newtonsoft.Json;
2 using Microsoft.Xna.Framework;
3 using Microsoft.Xna.Framework.Graphics;
4 using ReLogic.Graphics;
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 using Terraria.ID;
10 using Terraria.Localization;
12 using Terraria.ModLoader.Core;
13 using Terraria.ModLoader.UI.DownloadManager;
14 using Terraria.ModLoader.UI;
15 
16 namespace Terraria.ModLoader
17 {
18  public static class ModNet
19  {
20  internal class ModHeader
21  {
22  public string name;
23  public Version version;
24  public byte[] hash;
25  public bool signed;
26  public string path;
27 
28  public ModHeader(string name, Version version, byte[] hash, bool signed) {
29  this.name = name;
30  this.version = version;
31  this.hash = hash;
32  this.signed = signed;
33  path = Path.Combine(ModLoader.ModPath, name + ".tmod");
34  }
35 
36  public bool Matches(TmodFile mod) => name == mod.name && version == mod.version && hash.SequenceEqual(mod.hash);
37  public override string ToString() => name + " v" + version;
38  }
39 
40  internal class NetConfig
41  {
42  public string modname;
43  public string configname;
44  public string json;
45 
46  public NetConfig(string modname, string configname, string json) {
47  this.modname = modname;
48  this.configname = configname;
49  this.json = json;
50  }
51  }
52 
53  public static bool AllowVanillaClients { get; internal set; }
54  internal static bool downloadModsFromServers = true;
55  internal static bool onlyDownloadSignedMods = false;
56 
57  internal static bool[] isModdedClient = new bool[256];
58 
59  private static Mod[] netMods;
60 
61  public static bool IsModdedClient(int i) => isModdedClient[i];
62 
63  public static Mod GetMod(int netID) =>
64  netID >= 0 && netID < netMods.Length ? netMods[netID] : null;
65 
66  public static int NetModCount => netMods.Length;
67 
68  private static Queue<ModHeader> downloadQueue = new Queue<ModHeader>();
69  internal static List<NetConfig> pendingConfigs = new List<NetConfig>();
70  private static ModHeader downloadingMod;
71  private static FileStream downloadingFile;
72  private static long downloadingLength;
73 
74  internal static void AssignNetIDs() {
75  netMods = ModLoader.Mods.Where(mod => mod.Side != ModSide.Server).ToArray();
76  for (short i = 0; i < netMods.Length; i++)
77  netMods[i].netID = i;
78  }
79 
80  internal static void Unload() {
81  netMods = null;
82  if (!Main.dedServ && Main.netMode != 1) //disable vanilla client compatibility restrictions when reloading on a client
83  AllowVanillaClients = false;
84  }
85 
86  internal static void SyncMods(int clientIndex) {
87  var p = new ModPacket(MessageID.SyncMods);
88  p.Write(AllowVanillaClients);
89 
90  var syncMods = ModLoader.Mods.Where(mod => mod.Side == ModSide.Both).ToList();
91  AddNoSyncDeps(syncMods);
92 
93  p.Write(syncMods.Count);
94  foreach (var mod in syncMods) { // We only sync ServerSide configs for ModSide.Both. ModSide.Server can have
95  p.Write(mod.Name);
96  p.Write(mod.Version.ToString());
97  p.Write(mod.File.hash);
98  p.Write(mod.File.ValidModBrowserSignature);
99  SendServerConfigs(p, mod);
100  }
101 
102  p.Send(clientIndex);
103  }
104 
105  private static void AddNoSyncDeps(List<Mod> syncMods) {
106  var queue = new Queue<Mod>(syncMods.Where(m => m.Side == ModSide.Both));
107  while (queue.Count > 0) {
108  foreach (var dep in AssemblyManager.GetDependencies(queue.Dequeue())) {
109  if (dep.Side == ModSide.NoSync && !syncMods.Contains(dep)) {
110  syncMods.Add(dep);
111  queue.Enqueue(dep);
112  }
113  }
114  }
115  }
116 
117  private static void SendServerConfigs(ModPacket p, Mod mod) {
118  if (!ConfigManager.Configs.TryGetValue(mod, out var configs)) {
119  p.Write(0);
120  return;
121  }
122 
123  var serverConfigs = configs.Where(x => x.Mode == ConfigScope.ServerSide).ToArray();
124  p.Write(serverConfigs.Length);
125  foreach (var config in serverConfigs) {
126  string json = JsonConvert.SerializeObject(config, ConfigManager.serializerSettingsCompact);
127  Logging.Terraria.Info($"Sending Server Config {config.mod.Name} {config.Name}: {json}");
128 
129  p.Write(config.Name);
130  p.Write(json);
131  }
132  }
133 
134  internal static void SyncClientMods(BinaryReader reader) {
135  if (!SyncClientMods(reader, out bool needsReload))
136  return; //error syncing can't connect to server
137 
138  if (downloadQueue.Count > 0)
139  DownloadNextMod();
140  else
141  OnModsDownloaded(needsReload);
142  }
143 
144  // This method is split so that the local variables aren't held by the GC when reloading
145  internal static bool SyncClientMods(BinaryReader reader, out bool needsReload) {
146  AllowVanillaClients = reader.ReadBoolean();
147  Logging.tML.Info($"Server reports AllowVanillaClients set to {AllowVanillaClients}");
148 
149  Main.statusText = Language.GetTextValue("tModLoader.MPSyncingMods");
150  var clientMods = ModLoader.Mods;
151  var modFiles = ModOrganizer.FindMods();
152  needsReload = false;
153  downloadQueue.Clear();
154  pendingConfigs.Clear();
155  var syncSet = new HashSet<string>();
156  var blockedList = new List<ModHeader>();
157 
158  int n = reader.ReadInt32();
159  for (int i = 0; i < n; i++) {
160  var header = new ModHeader(reader.ReadString(), new Version(reader.ReadString()), reader.ReadBytes(20), reader.ReadBoolean());
161  syncSet.Add(header.name);
162 
163  int configCount = reader.ReadInt32();
164  for (int c = 0; c < configCount; c++)
165  pendingConfigs.Add(new NetConfig(header.name, reader.ReadString(), reader.ReadString()));
166 
167  var clientMod = clientMods.SingleOrDefault(m => m.Name == header.name);
168  if (clientMod != null && header.Matches(clientMod.File))
169  continue;
170 
171  needsReload = true;
172 
173  var localVersions = modFiles.Where(m => m.Name == header.name).ToArray();
174  var matching = Array.Find(localVersions, mod => header.Matches(mod.modFile));
175  if (matching != null) {
176  matching.Enabled = true;
177  continue;
178  }
179 
180  // overwrite an existing version of the mod if there is one
181  if (localVersions.Length > 0)
182  header.path = localVersions[0].modFile.path;
183 
184  if (downloadModsFromServers && (header.signed || !onlyDownloadSignedMods))
185  downloadQueue.Enqueue(header);
186  else
187  blockedList.Add(header);
188  }
189 
190  foreach (var mod in clientMods)
191  if (mod.Side == ModSide.Both && !syncSet.Contains(mod.Name)) {
192  ModLoader.DisableMod(mod.Name);
193  needsReload = true;
194  }
195 
196  if (blockedList.Count > 0) {
197  var msg = Language.GetTextValue("tModLoader.MPServerModsCantDownload");
198  msg += downloadModsFromServers
199  ? Language.GetTextValue("tModLoader.MPServerModsCantDownloadReasonSigned")
200  : Language.GetTextValue("tModLoader.MPServerModsCantDownloadReasonAutomaticDownloadDisabled");
201  msg += ".\n" + Language.GetTextValue("tModLoader.MPServerModsCantDownloadChangeSettingsHint") + "\n";
202  foreach (var mod in blockedList)
203  msg += "\n " + mod;
204 
205  Logging.tML.Warn(msg);
206  Interface.errorMessage.Show(msg, 0);
207  return false;
208  }
209 
210  // ready to connect, apply configs. Config manager will apply the configs on reload automatically
211  if (!needsReload) {
212  foreach (var pendingConfig in pendingConfigs)
213  JsonConvert.PopulateObject(pendingConfig.json, ConfigManager.GetConfig(pendingConfig), ConfigManager.serializerSettingsCompact);
214 
215  if (ConfigManager.AnyModNeedsReload()) {
216  needsReload = true;
217  }
218  else {
219  foreach (var pendingConfig in pendingConfigs)
220  ConfigManager.GetConfig(pendingConfig).OnChanged();
221  }
222  }
223 
224  return true;
225  }
226 
227  private static void DownloadNextMod() {
228  downloadingMod = downloadQueue.Dequeue();
229  downloadingFile = null;
230  var p = new ModPacket(MessageID.ModFile);
231  p.Write(downloadingMod.name);
232  p.Send();
233  }
234 
235  // Start sending the mod to the connecting client
236  // First, send the initial mod name and length of the file stream
237  // so the client knows what to expect
238  internal const int CHUNK_SIZE = 16384;
239  internal static void SendMod(string modName, int toClient) {
240  var mod = ModLoader.GetMod(modName);
241  var path = mod.File.path;
242  var fs = File.OpenRead(path);
243  {
244  //send file length
245  var p = new ModPacket(MessageID.ModFile);
246  p.Write(mod.DisplayName);
247  p.Write(fs.Length);
248  p.Send(toClient);
249  }
250 
251  var buf = new byte[CHUNK_SIZE];
252  int count;
253  while ((count = fs.Read(buf, 0, buf.Length)) > 0) {
254  var p = new ModPacket(MessageID.ModFile, CHUNK_SIZE + 3);
255  p.Write(buf, 0, count);
256  p.Send(toClient);
257  }
258 
259  fs.Close();
260  }
261 
262  // Receive a mod when connecting to server
263  internal static void ReceiveMod(BinaryReader reader) {
264  if (downloadingMod == null)
265  return;
266 
267  try {
268  if (downloadingFile == null) {
269  Interface.progress.Show(displayText: reader.ReadString(), cancel: CancelDownload);
270 
271  ModLoader.GetMod(downloadingMod.name)?.Close();
272  downloadingLength = reader.ReadInt64();
273  downloadingFile = new FileStream(downloadingMod.path, FileMode.Create);
274  return;
275  }
276 
277  var bytes = reader.ReadBytes((int)Math.Min(downloadingLength - downloadingFile.Position, CHUNK_SIZE));
278  downloadingFile.Write(bytes, 0, bytes.Length);
279  Interface.progress.Progress = downloadingFile.Position / (float)downloadingLength;
280 
281  if (downloadingFile.Position == downloadingLength) {
282  downloadingFile.Close();
283  var mod = new TmodFile(downloadingMod.path);
284  using (mod.Open()) { }
285 
286  if (!downloadingMod.Matches(mod))
287  throw new Exception(Language.GetTextValue("tModLoader.MPErrorModHashMismatch"));
288 
289  if (downloadingMod.signed && !mod.ValidModBrowserSignature)
290  throw new Exception(Language.GetTextValue("tModLoader.MPErrorModNotSigned"));
291 
292  ModLoader.EnableMod(mod.name);
293  if (downloadQueue.Count > 0) DownloadNextMod();
294  else OnModsDownloaded(true);
295  }
296  }
297  catch (Exception e) {
298  try {
299  downloadingFile?.Close();
300  File.Delete(downloadingMod.path);
301  }
302  catch (Exception exc2) {
303  Logging.tML.Error("Unknown error during mod sync", exc2);
304  }
305 
306  var msg = Language.GetTextValue("tModLoader.MPErrorModDownloadError", downloadingMod.name);
307  Logging.tML.Error(msg, e);
308  Interface.errorMessage.Show(msg + e, 0);
309 
310  Netplay.disconnect = true;
311  downloadingMod = null;
312  }
313  }
314 
315  private static void CancelDownload() {
316  try {
317  downloadingFile?.Close();
318  File.Delete(downloadingMod.path);
319  }
320  catch { }
321  downloadingMod = null;
322  Netplay.disconnect = true;
323  }
324 
325  private static void OnModsDownloaded(bool needsReload) {
326  if (needsReload) {
327  ModLoader.OnSuccessfulLoad = NetReload();
328  ModLoader.Reload();
329  return;
330  }
331 
332  downloadingMod = null;
333  netMods = null;
334  foreach (var mod in ModLoader.Mods)
335  mod.netID = -1;
336 
337  new ModPacket(MessageID.SyncMods).Send();
338  }
339 
340  internal static Action NetReload() {
341  // Main.ActivePlayerFileData gets cleared during reload
342  var path = Main.ActivePlayerFileData.Path;
343  var isCloudSave = Main.ActivePlayerFileData.IsCloudSave;
344  return () => {
345  // re-select the current player
346  Player.GetFileData(path, isCloudSave).SetAsActive();
347  //from Netplay.ClientLoopSetup
348  Main.player[Main.myPlayer].hostile = false;
349  Main.clientPlayer = (Player)Main.player[Main.myPlayer].clientClone();
350 
351  Main.menuMode = 10;
352  OnModsDownloaded(false);
353  };
354  }
355 
356  internal static void SendNetIDs(int toClient) {
357  var p = new ModPacket(MessageID.ModPacket);
358  p.Write(netMods.Length);
359  foreach (var mod in netMods)
360  p.Write(mod.Name);
361 
362  ItemLoader.WriteNetGlobalOrder(p);
363  WorldHooks.WriteNetWorldOrder(p);
364  p.Write(Player.MaxBuffs);
365 
366  p.Send(toClient);
367  }
368 
369  private static void ReadNetIDs(BinaryReader reader) {
370  var mods = ModLoader.Mods;
371  var list = new List<Mod>();
372  var n = reader.ReadInt32();
373  for (short i = 0; i < n; i++) {
374  var name = reader.ReadString();
375  var mod = mods.SingleOrDefault(m => m.Name == name);
376  list.Add(mod);
377  if (mod != null) //nosync mod that doesn't exist on the client
378  mod.netID = i;
379  }
380  netMods = list.ToArray();
381  SetupDiagnostics();
382 
383  ItemLoader.ReadNetGlobalOrder(reader);
384  WorldHooks.ReadNetWorldOrder(reader);
385  int serverMaxBuffs = reader.ReadInt32();
386  if (serverMaxBuffs != Player.MaxBuffs) {
387  Netplay.disconnect = true;
388  Main.statusText = $"The server expects Player.MaxBuffs of {serverMaxBuffs}\nbut this client reports {Player.MaxBuffs}.\nSome mod is behaving poorly.";
389  }
390  }
391 
392  internal static void HandleModPacket(BinaryReader reader, int whoAmI, int length) {
393  if (netMods == null) {
394  ReadNetIDs(reader);
395  return;
396  }
397 
398  var id = NetModCount < 256 ? reader.ReadByte() : reader.ReadInt16();
399  GetMod(id)?.HandlePacket(reader, whoAmI);
400 
401  if (Main.netMode == 1) {
402  rxMsgType[id]++;
403  rxDataType[id] += length;
404  }
405  }
406 
407  internal static bool HijackGetData(ref byte messageType, ref BinaryReader reader, int playerNumber) {
408  if (netMods == null) {
409  return false;
410  }
411 
412  bool hijacked = false;
413  long readerPos = reader.BaseStream.Position;
414  long biggestReaderPos = readerPos;
415  foreach (var mod in ModLoader.Mods) {
416  if (mod.HijackGetData(ref messageType, ref reader, playerNumber)) {
417  hijacked = true;
418  biggestReaderPos = Math.Max(reader.BaseStream.Position, biggestReaderPos);
419  }
420  reader.BaseStream.Position = readerPos;
421  }
422  if (hijacked) {
423  reader.BaseStream.Position = biggestReaderPos;
424  }
425  return hijacked;
426  }
427 
428  internal static bool HijackSendData(int whoAmI, int msgType, int remoteClient, int ignoreClient, NetworkText text, int number, float number2, float number3, float number4, int number5, int number6, int number7) {
429  bool hijacked = false;
430  foreach (Mod mod in ModLoader.Mods) {
431  hijacked |= mod.HijackSendData(whoAmI, msgType, remoteClient, ignoreClient, text, number, number2, number3, number4, number5, number6, number7);
432  }
433  return hijacked;
434  }
435 
436  // Mirror of Main class network diagnostic fields, but mod specific.
437  // Potential improvements: separate page from vanilla messageIDs, track automatic/ModWorld/etc sends per class or mod, sort by most active, moving average, NetStats console command in ModLoaderMod
438  // Currently we only update these on client
439  public static int[] rxMsgType;
440  public static int[] rxDataType;
441  public static int[] txMsgType;
442  public static int[] txDataType;
443 
444  private static void SetupDiagnostics() {
445  rxMsgType = new int[netMods.Length];
446  rxDataType = new int[netMods.Length];
447  txMsgType = new int[netMods.Length];
448  txDataType = new int[netMods.Length];
449  }
450 
451  internal static void ResetNetDiag() {
452  if (netMods == null || Main.netMode == 2) return;
453  for (int i = 0; i < netMods.Length; i++) {
454  rxMsgType[i] = 0;
455  rxDataType[i] = 0;
456  txMsgType[i] = 0;
457  txDataType[i] = 0;
458  }
459  }
460 
461  internal static void DrawModDiagnoseNet() {
462  if (netMods == null) return;
463  float scale = 0.7f;
464 
465  for (int j = -1; j < netMods.Length; j++) {
466  int i = j + Main.maxMsg + 2;
467  int x = 200;
468  int y = 120;
469  int xAdjust = i / 50;
470  x += xAdjust * 400;
471  y += (i - xAdjust * 50) * 13;
472  if (j == -1) {
473  Main.spriteBatch.DrawString(Main.fontMouseText, "Mod Received(#, Bytes) Sent(#, Bytes)", new Vector2((float)x, (float)y), Color.White, 0f, default(Vector2), scale, SpriteEffects.None, 0f);
474  continue;
475  }
476  Main.spriteBatch.DrawString(Main.fontMouseText, netMods[j].Name, new Vector2(x, y), Color.White, 0f, default(Vector2), scale, SpriteEffects.None, 0f);
477  x += 120;
478  Main.spriteBatch.DrawString(Main.fontMouseText, rxMsgType[j].ToString(), new Vector2(x, y), Color.White, 0f, default(Vector2), scale, SpriteEffects.None, 0f);
479  x += 30;
480  Main.spriteBatch.DrawString(Main.fontMouseText, rxDataType[j].ToString(), new Vector2(x, y), Color.White, 0f, default(Vector2), scale, SpriteEffects.None, 0f);
481  x += 80;
482  Main.spriteBatch.DrawString(Main.fontMouseText, txMsgType[j].ToString(), new Vector2(x, y), Color.White, 0f, default(Vector2), scale, SpriteEffects.None, 0f);
483  x += 30;
484  Main.spriteBatch.DrawString(Main.fontMouseText, txDataType[j].ToString(), new Vector2(x, y), Color.White, 0f, default(Vector2), scale, SpriteEffects.None, 0f);
485  }
486  }
487  }
488 }
static int[] rxDataType
Definition: ModNet.cs:440
static int[] txDataType
Definition: ModNet.cs:442
static void CancelDownload()
Definition: ModNet.cs:315
static ModHeader downloadingMod
Definition: ModNet.cs:70
static void AddNoSyncDeps(List< Mod > syncMods)
Definition: ModNet.cs:105
static void SendServerConfigs(ModPacket p, Mod mod)
Definition: ModNet.cs:117
This serves as the central class which loads mods. It contains many static fields and methods related...
Definition: ModLoader.cs:26
This is where all ModWorld hooks are gathered and called.
Definition: WorldHooks.cs:12
virtual void Close()
Close is called before Unload, and may be called at any time when mod unloading is imminent (such as ...
Definition: Mod.cs:136
static void SetupDiagnostics()
Definition: ModNet.cs:444
virtual bool HijackSendData(int whoAmI, int msgType, int remoteClient, int ignoreClient, NetworkText text, int number, float number2, float number3, float number4, int number5, int number6, int number7)
Hijacks the send data method. Only use if you absolutely know what you are doing. If any hooks return...
Definition: ModHooks.cs:61
void Send(int toClient=-1, int ignoreClient=-1)
Sends all the information you&#39;ve written between client and server. If the toClient parameter is non-...
Definition: ModPacket.cs:27
static void OnModsDownloaded(bool needsReload)
Definition: ModNet.cs:325
This class inherits from BinaryWriter. This means that you can use all of its writing functions to se...
Definition: ModPacket.cs:13
static FileStream downloadingFile
Definition: ModNet.cs:71
This serves as the central class from which item-related functions are carried out. It also stores a list of mod items by ID.
Definition: ItemLoader.cs:21
static void ReadNetIDs(BinaryReader reader)
Definition: ModNet.cs:369
virtual void OnChanged()
This hook is called anytime new config values have been set and are ready to take effect...
Definition: ModConfig.cs:43
virtual string Name
Stores the name of the mod. This name serves as the mod&#39;s identification, and also helps with saving ...
Definition: Mod.cs:42
Mod is an abstract class that you will override. It serves as a central place from which the mod&#39;s co...
Definition: Mod.cs:24
static Mod GetMod(string name)
Gets the instance of the Mod with the specified name.
Definition: ModLoader.cs:86
ModSide
A ModSide enum defines how mods are synced between clients and servers. You can set your mod&#39;s ModSid...
Definition: ModSide.cs:4
ConfigScope
Each ModConfig class has a different scope. Failure to use the correct mode will lead to bugs...
Definition: ModConfig.cs:91
static Mod[] netMods
Definition: ModNet.cs:59
static void DownloadNextMod()
Definition: ModNet.cs:227
static int[] txMsgType
Definition: ModNet.cs:441
static long downloadingLength
Definition: ModNet.cs:72
static int[] rxMsgType
Definition: ModNet.cs:439