diff --git a/src/TerraSharp/Core/Hotkeys.cs b/src/TerraSharp/Core/Hotkeys.cs index 693b1fe..76ea5ec 100644 --- a/src/TerraSharp/Core/Hotkeys.cs +++ b/src/TerraSharp/Core/Hotkeys.cs @@ -28,6 +28,7 @@ public static class Hotkeys new(InterfaceGroup, "Menu", "Open / close menu", () => WindowManager.Toggle(WindowManager.Menu)), new(InterfaceGroup, "ItemBrowser", "Item Browser", () => WindowManager.Toggle(WindowManager.Items)), new(InterfaceGroup, "BuffBrowser", "Buff Browser", () => WindowManager.Toggle(WindowManager.Buffs)), + new(InterfaceGroup, "NpcSpawner", "NPC Spawner", () => WindowManager.Toggle(WindowManager.Npcs)), Toggle("Player", "GodMode", "God Mode", () => Settings.GodMode, v => Settings.GodMode = v), Toggle("Player", "AutoHeal", "Auto Heal", () => Settings.AutoHeal, v => Settings.AutoHeal = v), @@ -69,6 +70,7 @@ public static class Hotkeys Toggle("Visuals", "Shine", "Shine", () => Settings.Shine, v => Settings.Shine = v), Toggle("World", "FreezeTime", "Freeze Time", () => Settings.FreezeTime, v => Settings.FreezeTime = v), + Toggle("World", "SpawnRate", "Spawn Rate", () => Settings.SpawnRateBoost, v => Settings.SpawnRateBoost = v), new("World", "SavePosition", "Save Current Position", Waypoints.AddHere), ]; diff --git a/src/TerraSharp/Core/Settings.Json.cs b/src/TerraSharp/Core/Settings.Json.cs index abac43a..8cf6db1 100644 --- a/src/TerraSharp/Core/Settings.Json.cs +++ b/src/TerraSharp/Core/Settings.Json.cs @@ -104,6 +104,8 @@ public static partial class Settings [nameof(BiomeSight)] = "Vision.BiomeSight", [nameof(FreezeTime)] = "World.FreezeTime", + [nameof(SpawnRateBoost)] = "World.SpawnRate.Enabled", + [nameof(SpawnRateMult)] = "World.SpawnRate.Multiplier", [nameof(LogBallistics)] = "Ballistics.Log", }; diff --git a/src/TerraSharp/Core/Settings.cs b/src/TerraSharp/Core/Settings.cs index 4678366..6990c62 100644 --- a/src/TerraSharp/Core/Settings.cs +++ b/src/TerraSharp/Core/Settings.cs @@ -48,7 +48,8 @@ public static partial class Settings public static Color ColLocal = Color.White, ColPlayers = new(70, 200, 255), ColHostile = new(235, 60, 60), ColCritters = new(200, 170, 120), ColTown = new(80, 220, 110), ColBoss = new(255, 120, 40), ColItems = new(255, 210, 70), ColProjHostile = new(255, 80, 200), ColProjFriendly = new(120, 160, 255), ColWaypoint = new(220, 90, 255); public static bool Spelunker, Hunter, Shine, NightOwl, DangerSense, BiomeSight; - public static bool FreezeTime; + public static bool FreezeTime, SpawnRateBoost; + public static float SpawnRateMult = 5f; public static bool LogBallistics = true; diff --git a/src/TerraSharp/Features/NpcSpawner.cs b/src/TerraSharp/Features/NpcSpawner.cs new file mode 100644 index 0000000..69010e1 --- /dev/null +++ b/src/TerraSharp/Features/NpcSpawner.cs @@ -0,0 +1,59 @@ +using System; +using Terraria; +using Terraria.DataStructures; +using Terraria.ID; +using TerraSharp.Core; + +namespace TerraSharp.Features; + +public static class NpcSpawner +{ + const float SpawnDistance = 10 * 16f; + const int MultiplayerClient = 1; + +#if TMODLOADER + static readonly EntitySource_DebugCommand Source = new(nameof(NpcSpawner)); +#else + static readonly EntitySource_DebugCommand Source = new(); +#endif + + public static bool Multiplayer => Main.netMode == MultiplayerClient; + + // The server only honors summon requests for these types. + public static bool CanSummon(int type) => type >= 0 && type < NPCID.Sets.MPAllowedEnemies.Length && NPCID.Sets.MPAllowedEnemies[type]; + + public static bool Available(int type) => !Multiplayer || CanSummon(type); + + public static void Spawn(int type, int count) + { + if (Multiplayer) + { + if (CanSummon(type)) + NetMessage.SendData(MessageID.SpawnBossUseLicenseStartEvent, -1, -1, null, Main.myPlayer, type); + return; + } + + Player player = Main.LocalPlayer; + int x = (int)(player.Center.X + player.direction * SpawnDistance); + int y = (int)player.Bottom.Y; + for (int i = 0; i < count; i++) + if (NPC.NewNPC(Source, x, y, type) >= Main.maxNPCs) + break; + } + + // Spawner.GetSpawnRate postfix; spawnRate is the 1-in-N chance per tick. + public static void ScaleSpawnRate(ref int spawnRate, ref int maxSpawns) + { + if (!Settings.SpawnRateBoost) + return; + + if (Settings.SpawnRateMult <= 0f) + { + maxSpawns = 0; + return; + } + + spawnRate = Math.Max(1, (int)(spawnRate / Settings.SpawnRateMult)); + maxSpawns = (int)Math.Ceiling(maxSpawns * Settings.SpawnRateMult); + } +} diff --git a/src/TerraSharp/Patches/HookRegistry.cs b/src/TerraSharp/Patches/HookRegistry.cs index 18b4a57..7ca93ae 100644 --- a/src/TerraSharp/Patches/HookRegistry.cs +++ b/src/TerraSharp/Patches/HookRegistry.cs @@ -21,6 +21,7 @@ internal sealed class HookRegistry : IDisposable MainPatches.Install(hooks); PlayerPatches.Install(hooks); NpcPatches.Install(hooks); + SpawnRatePatch.Install(hooks); ItemCheckShootPatch.Install(hooks); SandboxGuards.Install(hooks); return hooks; diff --git a/src/TerraSharp/Patches/SpawnRatePatch.cs b/src/TerraSharp/Patches/SpawnRatePatch.cs new file mode 100644 index 0000000..725a993 --- /dev/null +++ b/src/TerraSharp/Patches/SpawnRatePatch.cs @@ -0,0 +1,53 @@ +using System; +using Terraria; +using TerraSharp.Core; +using TerraSharp.Features; +#if TMODLOADER +using Terraria.ModLoader; +#endif + +namespace TerraSharp.Patches; + +// Natural spawns run only in single player and on the server. +internal static class SpawnRatePatch +{ +#if TMODLOADER + // tModLoader's NPC.SpawnNPC passes the final rate through NPCLoader.EditSpawnRate. + delegate void Original(Player player, ref int spawnRate, ref int maxSpawns); + delegate void Hooked(Original original, Player player, ref int spawnRate, ref int maxSpawns); + + internal static void Install(HookRegistry hooks) => hooks.Add(typeof(NPCLoader), nameof(NPCLoader.EditSpawnRate), (Hooked)EditSpawnRate); + + static void EditSpawnRate(Original original, Player player, ref int spawnRate, ref int maxSpawns) + { + original(player, ref spawnRate, ref maxSpawns); + Scale(ref spawnRate, ref maxSpawns); + } +#else + delegate void Original(NPC.Spawner self, Player player, out int spawnRate, out int maxSpawns); + delegate void Hooked(Original original, NPC.Spawner self, Player player, out int spawnRate, out int maxSpawns); + + internal static void Install(HookRegistry hooks) => hooks.Add(typeof(NPC.Spawner), "GetSpawnRate", (Hooked)GetSpawnRate); + + static void GetSpawnRate(Original original, NPC.Spawner self, Player player, out int spawnRate, out int maxSpawns) + { + original(self, player, out spawnRate, out maxSpawns); + Scale(ref spawnRate, ref maxSpawns); + } +#endif + + static void Scale(ref int spawnRate, ref int maxSpawns) + { + if (!Plugin.Active) + return; + + try + { + NpcSpawner.ScaleSpawnRate(ref spawnRate, ref maxSpawns); + } + catch (Exception e) + { + Log.Error("Spawn rate", e); + } + } +} diff --git a/src/TerraSharp/UI/IdIndex.cs b/src/TerraSharp/UI/IdIndex.cs index 26ef876..304fe19 100644 --- a/src/TerraSharp/UI/IdIndex.cs +++ b/src/TerraSharp/UI/IdIndex.cs @@ -27,6 +27,8 @@ public sealed class IdIndex(Func count, Func name) public string Name(int id) => id > 0 && id < Names.Length ? Names[id] : ""; + public void Refresh() => _lastQuery = null; + public void Filter(string query, Func fits) { query = query.Trim(); diff --git a/src/TerraSharp/UI/MainWindow.Pages.cs b/src/TerraSharp/UI/MainWindow.Pages.cs index abf29fb..3b680f1 100644 --- a/src/TerraSharp/UI/MainWindow.Pages.cs +++ b/src/TerraSharp/UI/MainWindow.Pages.cs @@ -228,6 +228,13 @@ public sealed partial class MainWindow if (WorldFeatures.InvasionName is { } active && Buttons($"Stop {active}") == 0) WorldFeatures.StopInvasion(); + Group("Spawning"); + Note(NpcSpawner.Multiplayer ? "Multiplayer: boss summons only, spawn rate is ignored." : "Spawn rate only affects single player."); + if (Toggle("Spawn Rate", ref Settings.SpawnRateBoost)) + Sub(() => Slider("Multiplier", ref Settings.SpawnRateMult, 0f, 20f, 5f, "x{0:0.0}", v => v < 0.05f ? "No spawns" : null)); + if (Buttons(WindowManager.Npcs.Open ? "Close NPC Spawner" : "NPC Spawner") == 0) + WindowManager.Toggle(WindowManager.Npcs); + Group("Waypoints"); Note("Saved per world."); if (Buttons("Save Current Position") == 0) diff --git a/src/TerraSharp/UI/MainWindow.cs b/src/TerraSharp/UI/MainWindow.cs index e38dc03..c7e6273 100644 --- a/src/TerraSharp/UI/MainWindow.cs +++ b/src/TerraSharp/UI/MainWindow.cs @@ -35,7 +35,7 @@ public sealed partial class MainWindow : Window new Page("Combat", CombatPage, () => Count(Settings.RapidAttack, Settings.AutoSwing, Settings.DamageBoost, Settings.NoManaCost, Settings.NoSpread, Settings.Aimbot, Settings.AutoAttack, Settings.KillAura, Settings.HitMagnet, Settings.FreezeEnemies, Settings.WeakenEnemies)), new Page("Items", ItemsPage, () => Count(Settings.InstantBreak, Settings.InfItems, Settings.ItemVacuum, Settings.AutoFish, Settings.Sonar)), new Page("Visuals", VisualsPage, () => Count(Settings.Esp, Settings.Spelunker, Settings.Hunter, Settings.Shine, Settings.NightOwl, Settings.DangerSense, Settings.BiomeSight)), - new Page("World", WorldPage, () => Count(Settings.FreezeTime)), + new Page("World", WorldPage, () => Count(Settings.FreezeTime, Settings.SpawnRateBoost)), new Page("Keys", KeysPage, () => 0), ]; diff --git a/src/TerraSharp/UI/NpcBrowser.cs b/src/TerraSharp/UI/NpcBrowser.cs new file mode 100644 index 0000000..c1b4b36 --- /dev/null +++ b/src/TerraSharp/UI/NpcBrowser.cs @@ -0,0 +1,62 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Terraria; +using Terraria.ID; +#if TMODLOADER +using Terraria.ModLoader; +#endif +using TerraSharp.Features; + +namespace TerraSharp.UI; + +public sealed class NpcBrowser() : GridBrowser("npcs", "NPC Spawner", new Vector2(660, 140), 532, 520, "Search NPCs by name or ID", "NPCs", new IdIndex(() => NpcCount, Lang.GetNPCNameValue), Categories) +{ + const int Batch = 10; + +#if TMODLOADER + static int NpcCount => NPCLoader.NPCCount; +#else + static int NpcCount => NPCID.Count; +#endif + + static readonly BrowserCategory[] Categories = [new("All", NpcSpawner.Available), new("Bosses", type => NpcSpawner.Available(type) && IsBoss(type)), new("Enemies", type => NpcSpawner.Available(type) && Sample(type) is { friendly: false, townNPC: false } && !IsBoss(type) && !IsCritter(type)), new("Town", type => NpcSpawner.Available(type) && Sample(type) is { townNPC: true }), new("Critters", type => NpcSpawner.Available(type) && IsCritter(type))]; + + bool _multiplayer; + + static NPC? Sample(int type) => ContentSamples.NpcsByNetId.TryGetValue(type, out NPC? npc) ? npc : null; + + static bool IsBoss(int type) => Sample(type) is { boss: true }; + + static bool IsCritter(int type) => NPCID.Sets.CountsAsCritter[type] || Main.npcCatchable[type]; + + protected override void DrawContent(Rectangle body) + { + if (_multiplayer != NpcSpawner.Multiplayer) + { + _multiplayer = NpcSpawner.Multiplayer; + Index.Refresh(); + } + + DrawSearchGrid(body, body.Y, body.Bottom - 30); + string hint = _multiplayer ? "Multiplayer: only boss summons the server accepts" : $"Click: spawn in front of you Shift click: spawn {Batch}"; + Render.TextMid(Widgets.SpriteBatch, hint, new Rectangle(body.X + 4, body.Bottom - 26, body.Width, 26), Widgets.Dim, 0.72f); + } + + protected override void DrawSlot(int type, Rectangle bounds, bool hot) + { + Widgets.Slot(bounds, hot); + Main.instance.LoadNPC(type); + if (GameTextures.Get("Npc", type) is { } texture) + Widgets.Icon(texture, new Rectangle(bounds.X + 4, bounds.Y + 4, bounds.Width - 8, bounds.Height - 8), GameTextures.Frame(texture, 1, Main.npcFrameCount[type], 0, 0)); + if (!hot) + return; + + string action = _multiplayer ? "Click to summon" : "Click to spawn"; + Widgets.Tooltip = $"{Index.Name(type)}\n{(IsBoss(type) ? "Boss" : "NPC")} #{type} {action}"; + if (!Widgets.Click) + return; + + NpcSpawner.Spawn(type, Widgets.Shift ? Batch : 1); + Widgets.Sound(12); + } +} diff --git a/src/TerraSharp/UI/WindowManager.cs b/src/TerraSharp/UI/WindowManager.cs index 79ed624..dab6dfa 100644 --- a/src/TerraSharp/UI/WindowManager.cs +++ b/src/TerraSharp/UI/WindowManager.cs @@ -13,11 +13,12 @@ public static class WindowManager public static readonly MainWindow Menu = new(); public static readonly ItemBrowser Items = new(); public static readonly BuffBrowser Buffs = new(); + public static readonly NpcBrowser Npcs = new(); const int TitleHeight = 40; const int MaxInputLength = 40; - static readonly List Stack = [Items, Buffs, Menu]; + static readonly List Stack = [Items, Buffs, Npcs, Menu]; static Window? _dragging; static Vector2 _dragOffset;