diff --git a/NebulaModel/Packets/Statistics/StatisticsReferenceSpeedTipPacket.cs b/NebulaModel/Packets/Statistics/StatisticsReferenceSpeedTipPacket.cs new file mode 100644 index 000000000..8d7bc5385 --- /dev/null +++ b/NebulaModel/Packets/Statistics/StatisticsReferenceSpeedTipPacket.cs @@ -0,0 +1,21 @@ +namespace NebulaModel.Packets.Statistics; + +public class StatisticsReferenceSpeedTipPacket +{ + public StatisticsReferenceSpeedTipPacket() { } + + public StatisticsReferenceSpeedTipPacket(int itemId, int astroFilter, int itemCycle, int productionProtoId, byte[] binaryData) + { + ItemId = itemId; + AstroFilter = astroFilter; + ItemCycle = itemCycle; + ProductionProtoId = productionProtoId; + BinaryData = binaryData; + } + + public int ItemId { get; set; } + public int AstroFilter { get; set; } + public int ItemCycle { get; set; } + public int ProductionProtoId { get; set; } + public byte[] BinaryData { get; set; } +} diff --git a/NebulaNetwork/PacketProcessors/Statistics/StatisticsReferenceSpeedTipProcessor.cs b/NebulaNetwork/PacketProcessors/Statistics/StatisticsReferenceSpeedTipProcessor.cs new file mode 100644 index 000000000..63f044bc7 --- /dev/null +++ b/NebulaNetwork/PacketProcessors/Statistics/StatisticsReferenceSpeedTipProcessor.cs @@ -0,0 +1,50 @@ +#region + +using System; +using System.IO; +using NebulaAPI.Packets; +using NebulaModel.Logger; +using NebulaModel.Networking; +using NebulaModel.Packets; +using NebulaModel.Packets.Statistics; +using NebulaWorld; + +#endregion + +namespace NebulaNetwork.PacketProcessors.Statistics; + +[RegisterPacketProcessor] +internal class StatisticsReferenceSpeedTipProcessor : PacketProcessor +{ + protected override void ProcessPacket(StatisticsReferenceSpeedTipPacket packet, NebulaConnection conn) + { + if (IsHost) + { + try + { + using var writer = new BinaryUtils.Writer(); + Multiplayer.Session.Statistics.GetReferenceSpeedTip(writer.BinaryWriter, packet.ItemId, packet.AstroFilter, packet.ItemCycle, packet.ProductionProtoId); + packet.BinaryData = writer.CloseAndGetBytes(); + conn.SendPacket(packet); + } + catch (Exception ex) + { + Log.Warn("StatisticsReferenceSpeedTipPacket request error!"); + Log.Warn(ex); + } + } + if (IsClient) + { + try + { + using var reader = new BinaryUtils.Reader(packet.BinaryData); + Multiplayer.Session.Statistics.SetReferenceSpeedTip(reader.BinaryReader, packet.ItemId, packet.AstroFilter, packet.ItemCycle, packet.ProductionProtoId); + } + catch (Exception ex) + { + Log.Warn("StatisticsReferenceSpeedTipPacket response error!"); + Log.Warn(ex); + } + } + } +} diff --git a/NebulaPatcher/Patches/Dynamic/UIReferenceSpeedTip_Patch.cs b/NebulaPatcher/Patches/Dynamic/UIReferenceSpeedTip_Patch.cs new file mode 100644 index 000000000..86ad75a45 --- /dev/null +++ b/NebulaPatcher/Patches/Dynamic/UIReferenceSpeedTip_Patch.cs @@ -0,0 +1,47 @@ +#region + +using System; +using HarmonyLib; +using NebulaModel.Packets.Statistics; +using NebulaWorld; +#pragma warning disable IDE0301 // Simplify collection initialization + +#endregion + +namespace NebulaPatcher.Patches.Dynamic; + +[HarmonyPatch(typeof(UIReferenceSpeedTip))] +internal class UIReferenceSpeedTip_Patch +{ + [HarmonyPrefix] + [HarmonyPatch(nameof(UIReferenceSpeedTip.AddEntryDataWithFactory))] + public static bool AddEntryDataWithFactory_Prefix() + { + // Client will use server response to update loadedEntryDatas and loadedSubTipDatas + return !Multiplayer.IsActive || Multiplayer.Session.LocalPlayer.IsHost; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(UIReferenceSpeedTip.SetTip))] + public static void SetTip_Prefix(UIReferenceSpeedTip __instance, int _itemId, int _astroFilter, UIReferenceSpeedTip.EItemCycle _itemCycle) + { + if (!Multiplayer.IsActive || Multiplayer.Session.LocalPlayer.IsHost) return; + + // Client: Send request to server when setting a new tip + if (__instance.itemId == _itemId && __instance.astroFilter == _astroFilter && __instance.itemCycle == _itemCycle) return; + Multiplayer.Session.Network.SendPacket(new StatisticsReferenceSpeedTipPacket( + _itemId, _astroFilter, (int)_itemCycle, 0, Array.Empty())); + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(UIReferenceSpeedTip.SetSubTip))] + public static void SetSubTip_Prefix(UIReferenceSpeedTip __instance, int _productionProtoId) + { + if (!Multiplayer.IsActive || Multiplayer.Session.LocalPlayer.IsHost) return; + + // Client: Send request to server when setting a valid subtip + if (_productionProtoId == 0) return; + Multiplayer.Session.Network.SendPacket(new StatisticsReferenceSpeedTipPacket( + __instance.itemId, __instance.astroFilter, (int)__instance.itemCycle, _productionProtoId, Array.Empty())); + } +} diff --git a/NebulaPatcher/Patches/Transpilers/UIReferenceSpeedTip_Transpiler.cs b/NebulaPatcher/Patches/Transpilers/UIReferenceSpeedTip_Transpiler.cs new file mode 100644 index 000000000..c23f57db9 --- /dev/null +++ b/NebulaPatcher/Patches/Transpilers/UIReferenceSpeedTip_Transpiler.cs @@ -0,0 +1,43 @@ +#region + +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Emit; +using HarmonyLib; +using NebulaModel.Logger; + +#endregion + +namespace NebulaPatcher.Patches.Transpilers; + +[HarmonyPatch(typeof(UIReferenceSpeedTip))] +public static class UIReferenceSpeedTip_Transpiler +{ + [HarmonyTranspiler] + [HarmonyPatch(nameof(UIReferenceSpeedTip.RefreshSubEntries))] + private static IEnumerable RefreshSubEntries_Transpiler(IEnumerable instructions) + { + //Remove planetData.factory != null condiction check + //Change: if (planetData != null && planetData.factory != null && this.loadedSubTipDatas[planetData.astroId].astroId == planetData.astroId) + //To: if (planetData != null && this.loadedSubTipDatas[planetData.astroId].astroId == planetData.astroId) + var codeInstructions = instructions as CodeInstruction[] ?? instructions.ToArray(); + try + { + return new CodeMatcher(instructions) + .MatchForward(true, + new CodeMatch(OpCodes.Ldfld, + AccessTools.Field(typeof(PlanetData), nameof(PlanetData.factory))), + new CodeMatch(OpCodes.Brfalse) + ) + .Repeat(matcher => matcher + .SetAndAdvance(OpCodes.Pop, null) + ) + .InstructionEnumeration(); + } + catch + { + Log.Error("Transpiler UIReferenceSpeedTip.RefreshSubEntries failed. Reference speed tip may not work properly."); + return codeInstructions; + } + } +} diff --git a/NebulaWorld/Statistics/StatisticsManager.cs b/NebulaWorld/Statistics/StatisticsManager.cs index a8447dba2..7c7682ab5 100644 --- a/NebulaWorld/Statistics/StatisticsManager.cs +++ b/NebulaWorld/Statistics/StatisticsManager.cs @@ -5,8 +5,12 @@ using System.IO; using NebulaAPI.DataStructures; using NebulaModel.DataStructures; +using NebulaModel.Logger; using NebulaModel.Networking; using NebulaModel.Packets.Statistics; +using UnityEngine; +#pragma warning disable IDE0007 // Use implicit type +#pragma warning disable IDE1006 // Naming rule #endregion @@ -22,12 +26,30 @@ public class StatisticsManager : IDisposable private List statisticalSnapShots = []; private long lastUpdateTime; + private readonly UIReferenceSpeedTip referenceSpeedTip; public bool IsStatisticsNeeded { get; set; } public long[] PowerEnergyStoredData { get; set; } public int FactoryCount { get; set; } public int TechHashedFor10Frames { get; set; } + public StatisticsManager() + { + var gameObject = GameObject.Find("UI Root/Overlay Canvas/In Game/Top Tips/Item Reference Speed Tips/ref-speed-tip"); + if (gameObject == null) + { + Log.Warn("StatisticsManager: Can't find ref-speed-tip!"); + } + else + { + referenceSpeedTip = gameObject.GetComponent(); + if (referenceSpeedTip == null) + { + Log.Warn("StatisticsManager: Can't find UIReferenceSpeedTip component!"); + } + } + } + public void Dispose() { statisticalSnapShots = null; @@ -268,6 +290,312 @@ public long UpdateTotalChargedEnergy(int factoryIndex) return PowerEnergyStoredData[factoryIndex]; } + public void GetReferenceSpeedTip(BinaryWriter bw, int itemId, int astroFilter, int itemCycle, int productionProtoId) + { + if (referenceSpeedTip == null) return; + + var tmpItemId = referenceSpeedTip.itemId; + var tmpAstroFilter = referenceSpeedTip.astroFilter; + var tmpItemCycle = referenceSpeedTip.itemCycle; + var tmpProductionProtoId = referenceSpeedTip.productionProtoId; + + referenceSpeedTip.itemId = itemId; + referenceSpeedTip.astroFilter = astroFilter; + referenceSpeedTip.itemCycle = (UIReferenceSpeedTip.EItemCycle)itemCycle; + referenceSpeedTip.productionProtoId = productionProtoId; + CalculateReferenceSpeedTip(); + + var list = new List(); + for (var i = 0; i < referenceSpeedTip.loadedEntryDatas.Length; i++) + { + if (referenceSpeedTip.loadedEntryDatas[i].productionProtoId != 0) list.Add(i); + } + bw.Write(list.Count); + foreach (var index in list) + { + ref var ptr = ref referenceSpeedTip.loadedEntryDatas[index]; + bw.Write(index); + bw.Write(ptr.productionProtoId); + bw.Write(ptr.normalCount); + bw.Write(ptr.normalSpeed); + bw.Write(ptr.useInc2IncCount); + bw.Write(ptr.useInc2IncSpeed); + bw.Write(ptr.useInc2AccCount); + bw.Write(ptr.useInc2AccSpeed); + bw.Write(ptr.outNetworkCount); + bw.Write(ptr.outNetworkSpeed); + } + + list.Clear(); + for (var i = 0; i < referenceSpeedTip.loadedSubTipDatas.Length; i++) + { + if (referenceSpeedTip.loadedSubTipDatas[i].astroId != 0) list.Add(i); + } + bw.Write(list.Count); + foreach (var index in list) + { + ref var ptr = ref referenceSpeedTip.loadedSubTipDatas[index]; + bw.Write(index); + bw.Write(ptr.astroId); + bw.Write(ptr.normalCount); + bw.Write(ptr.normalSpeed); + bw.Write(ptr.useInc2IncCount); + bw.Write(ptr.useInc2IncSpeed); + bw.Write(ptr.useInc2AccCount); + bw.Write(ptr.useInc2AccSpeed); + bw.Write(ptr.outNetworkCount); + bw.Write(ptr.outNetworkSpeed); + } + + referenceSpeedTip.itemId = tmpItemId; + referenceSpeedTip.astroFilter = tmpAstroFilter; + referenceSpeedTip.itemCycle = tmpItemCycle; + referenceSpeedTip.productionProtoId = tmpProductionProtoId; + CalculateReferenceSpeedTip(); + } + + public void SetReferenceSpeedTip(BinaryReader br, int itemId, int astroFilter, int itemCycle, int productionProtoId) + { + if (referenceSpeedTip == null) return; + if (referenceSpeedTip.itemId != itemId || referenceSpeedTip.astroFilter != astroFilter || (int)referenceSpeedTip.itemCycle != itemCycle) return; + + Array.Clear(referenceSpeedTip.loadedEntryDatas, 0, referenceSpeedTip.loadedEntryDatas.Length); + var listCount = br.ReadInt32(); + for (var i = 0; i < listCount; i++) + { + var index = br.ReadInt32(); + ref var ptr = ref referenceSpeedTip.loadedEntryDatas[index]; + ptr.productionProtoId = br.ReadInt32(); + ptr.normalCount = br.ReadInt32(); + ptr.normalSpeed = br.ReadSingle(); + ptr.useInc2IncCount = br.ReadInt32(); + ptr.useInc2IncSpeed = br.ReadSingle(); + ptr.useInc2AccCount = br.ReadInt32(); + ptr.useInc2AccSpeed = br.ReadSingle(); + ptr.outNetworkCount = br.ReadInt32(); + ptr.outNetworkSpeed = br.ReadSingle(); + } + // Refresh UI + RefreshReferenceSpeedTipEntries(); + + if (productionProtoId != 0 && referenceSpeedTip.productionProtoId == productionProtoId) + { + Array.Clear(referenceSpeedTip.loadedSubTipDatas, 0, referenceSpeedTip.loadedSubTipDatas.Length); + listCount = br.ReadInt32(); + for (var i = 0; i < listCount; i++) + { + var index = br.ReadInt32(); + ref var ptr = ref referenceSpeedTip.loadedSubTipDatas[index]; + ptr.astroId = br.ReadInt32(); + ptr.normalCount = br.ReadInt32(); + ptr.normalSpeed = br.ReadSingle(); + ptr.useInc2IncCount = br.ReadInt32(); + ptr.useInc2IncSpeed = br.ReadSingle(); + ptr.useInc2AccCount = br.ReadInt32(); + ptr.useInc2AccSpeed = br.ReadSingle(); + ptr.outNetworkCount = br.ReadInt32(); + ptr.outNetworkSpeed = br.ReadSingle(); + } + referenceSpeedTip.RefreshSubEntries(); + } + } + + private void CalculateReferenceSpeedTip() + { + // Part of UIReferenceSpeedTip.SetSubTip + if (referenceSpeedTip == null) return; + + if (referenceSpeedTip.loadedEntryDatas == null) + { + referenceSpeedTip.loadedEntryDatas = new RefSpeedTipEntryData[12000]; + } + if (referenceSpeedTip.loadedSubTipDatas == null) + { + referenceSpeedTip.loadedSubTipDatas = new RefSpeedSubTipEntryData[25700]; + } + Array.Clear(referenceSpeedTip.loadedEntryDatas, 0, referenceSpeedTip.loadedEntryDatas.Length); + Array.Clear(referenceSpeedTip.loadedSubTipDatas, 0, referenceSpeedTip.loadedSubTipDatas.Length); + var data = GameMain.data; + var history = data.history; + var itemProto = LDB.items.Select(2313); + var array = ((itemProto != null) ? itemProto.prefabDesc.incItemId : null); + var num = 0; + if (array != null) + { + for (var i = 0; i < array.Length; i++) + { + if (history.ItemUnlocked(array[i])) + { + var itemProto2 = LDB.items.Select(array[i]); + if (itemProto2 != null && itemProto2.Ability > num) + { + num = itemProto2.Ability; + } + } + } + } + var incMulti = 1f + (float)Cargo.incTableMilli[num]; + var accMulti = 1f + (float)Cargo.accTableMilli[num]; + var maxStack = 1; + if (history.TechUnlocked(1607)) + { + maxStack = 4; + } + var inserterStackOutput = history.inserterStackOutput; + var stationPilerLevel = history.stationPilerLevel; + if (inserterStackOutput > maxStack) + { + maxStack = inserterStackOutput; + } + if (inserterStackOutput > maxStack) + { + maxStack = stationPilerLevel; + } + if (referenceSpeedTip.astroFilter == 0) + { + var factoryCount = data.factoryCount; + for (var j = 0; j < factoryCount; j++) + { + referenceSpeedTip.AddEntryDataWithFactory(data.factories[j], incMulti, accMulti, maxStack, referenceSpeedTip.productionProtoId); + } + } + else if (referenceSpeedTip.astroFilter % 100 == 0) + { + var starData = data.galaxy.StarById(referenceSpeedTip.astroFilter / 100); + if (starData != null) + { + var planets = starData.planets; + for (var k = 0; k < starData.planetCount; k++) + { + var planetData = planets[k]; + if (planetData != null && planetData.factory != null) + { + referenceSpeedTip.AddEntryDataWithFactory(data.factories[planetData.factoryIndex], incMulti, accMulti, maxStack, referenceSpeedTip.productionProtoId); + } + } + } + else + { + var factoryCount = data.factoryCount; + for (var l = 0; l < factoryCount; l++) + { + referenceSpeedTip.AddEntryDataWithFactory(data.factories[l], incMulti, accMulti, maxStack, referenceSpeedTip.productionProtoId); + } + } + } + else + { + var planetData = data.galaxy.PlanetById(referenceSpeedTip.astroFilter); + if (planetData != null && planetData.factory != null) + { + referenceSpeedTip.AddEntryDataWithFactory(planetData.factory, incMulti, accMulti, maxStack, referenceSpeedTip.productionProtoId); + } + } + } + + private void RefreshReferenceSpeedTipEntries() + { + // Lower part of UIReferenceSpeedTip.SetTip + if (referenceSpeedTip == null) return; + + ItemProto[] dataArray = LDB.items.dataArray; + referenceSpeedTip.activeEntryCount = 0; + float num5 = 0f; + int num6 = (int)referenceSpeedTip.entryPrefab.rectTrans.anchoredPosition.y; + int num7 = (int)(referenceSpeedTip.entryPrefab.rectTrans.rect.width + 0.5f); + for (int m = 0; m < dataArray.Length; m++) + { + if (referenceSpeedTip.loadedEntryDatas[dataArray[m].ID].productionProtoId != 0) + { + if (referenceSpeedTip.activeEntryCount >= referenceSpeedTip.entries.Count) + { + UIReferenceSpeedTipEntry uireferenceSpeedTipEntry = UnityEngine.Object.Instantiate(referenceSpeedTip.entryPrefab, referenceSpeedTip.entryPrefab.rectTrans.parent); + referenceSpeedTip.entries.Add(uireferenceSpeedTipEntry); + } + ref RefSpeedTipEntryData ptr = ref referenceSpeedTip.loadedEntryDatas[dataArray[m].ID]; + referenceSpeedTip.entries[referenceSpeedTip.activeEntryCount].gameObject.SetActive(true); + referenceSpeedTip.entries[referenceSpeedTip.activeEntryCount].entryData = ptr; + referenceSpeedTip.entries[referenceSpeedTip.activeEntryCount].Refresh(); + referenceSpeedTip.entries[referenceSpeedTip.activeEntryCount].rectTrans.anchoredPosition = new Vector2(referenceSpeedTip.entryPrefab.rectTrans.anchoredPosition.x, (float)num6); + num6 -= referenceSpeedTip.entries[referenceSpeedTip.activeEntryCount].entryHeight; + if (num7 < referenceSpeedTip.entries[referenceSpeedTip.activeEntryCount].entryWidth) + { + num7 = referenceSpeedTip.entries[referenceSpeedTip.activeEntryCount].entryWidth; + } + num5 += ptr.normalSpeed + ptr.useInc2IncSpeed + ptr.useInc2AccSpeed + ptr.outNetworkSpeed; + referenceSpeedTip.activeEntryCount++; + } + } + for (int n = referenceSpeedTip.activeEntryCount; n < referenceSpeedTip.entries.Count; n++) + { + referenceSpeedTip.entries[n].gameObject.SetActive(false); + } + if (referenceSpeedTip.activeEntryCount == 0) + { + referenceSpeedTip.zeroCountTipText.gameObject.SetActive(true); + if (referenceSpeedTip.itemCycle == UIReferenceSpeedTip.EItemCycle.Production) + { + referenceSpeedTip.zeroCountTipText.text = "参考速率无生产设施".Translate(); + } + else if (referenceSpeedTip.itemCycle == UIReferenceSpeedTip.EItemCycle.Consumption) + { + referenceSpeedTip.zeroCountTipText.text = "参考速率无消耗设施".Translate(); + } + num6 -= (int)(referenceSpeedTip.zeroCountTipText.rectTransform.rect.height + 0.5f); + } + else + { + referenceSpeedTip.zeroCountTipText.gameObject.SetActive(false); + } + referenceSpeedTip.totalSpeedText.text = ((long)(num5 + 0.5f)).ToString("#,##0") + " / min"; + if (referenceSpeedTip.itemCycle == UIReferenceSpeedTip.EItemCycle.Production) + { + referenceSpeedTip.totalSpeedLabel.color = referenceSpeedTip.productColor; + referenceSpeedTip.totalSpeedText.color = referenceSpeedTip.productColor; + } + else if (referenceSpeedTip.itemCycle == UIReferenceSpeedTip.EItemCycle.Consumption) + { + referenceSpeedTip.totalSpeedLabel.color = referenceSpeedTip.consumeColor; + referenceSpeedTip.totalSpeedText.color = referenceSpeedTip.consumeColor; + } + else + { + referenceSpeedTip.totalSpeedLabel.color = Color.white; + referenceSpeedTip.totalSpeedText.color = Color.white; + } + referenceSpeedTip.rectTrans.SetParent(UIRoot.instance.itemReferenceSpeedTipTransform, true); + referenceSpeedTip.rectTrans.sizeDelta = new Vector2(num7 + 20f, (float)(-(float)num6) + 2f); + Rect rect = UIRoot.instance.itemReferenceSpeedTipTransform.rect; + float num8 = Mathf.RoundToInt(rect.width); + float num9 = Mathf.RoundToInt(rect.height); + Vector2 anchoredPosition = referenceSpeedTip.rectTrans.anchoredPosition; + float num10 = referenceSpeedTip.rectTrans.anchorMin.x * num8 + anchoredPosition.x; + float num11 = referenceSpeedTip.rectTrans.anchorMin.y * num9 + anchoredPosition.y; + Rect rect2 = referenceSpeedTip.rectTrans.rect; + rect2.x += num10; + rect2.y += num11; + Vector2 zero = Vector2.zero; + if (rect2.xMin < 0f) + { + zero.x -= rect2.xMin; + } + if (rect2.yMin < 0f) + { + zero.y -= rect2.yMin; + } + if (rect2.xMax > num8) + { + zero.x -= rect2.xMax - num8; + } + if (rect2.yMax > num9) + { + zero.y -= rect2.yMax - num9; + } + Vector2 vector2 = anchoredPosition + zero; + vector2 = new Vector2(Mathf.Round(vector2.x), Mathf.Round(vector2.y)); + referenceSpeedTip.rectTrans.anchoredPosition = vector2; + } + private sealed class ThreadSafe { internal readonly Dictionary Requestors = new();