From 2b95aea852d50f535f72717269674bbec1a718e2 Mon Sep 17 00:00:00 2001 From: Codian <44289933+xxcodianxx@users.noreply.github.com> Date: Sun, 17 Dec 2023 21:04:44 +1030 Subject: [PATCH] Initial commit --- .github/PerPlayerLoot.png | Bin 0 -> 24448 bytes .gitignore | 398 +++++++++++++++++++++++ PerPlayerLoot.sln | 30 ++ PerPlayerLoot/FakeChestDatabase.cs | 272 ++++++++++++++++ PerPlayerLoot/PPLPlugin.cs | 215 ++++++++++++ PerPlayerLoot/PerPlayerLoot.csproj | 25 ++ PerPlayerLoot/Properties/AssemblyInfo.cs | 13 + README.md | 61 ++++ 8 files changed, 1014 insertions(+) create mode 100644 .github/PerPlayerLoot.png create mode 100644 .gitignore create mode 100644 PerPlayerLoot.sln create mode 100644 PerPlayerLoot/FakeChestDatabase.cs create mode 100644 PerPlayerLoot/PPLPlugin.cs create mode 100644 PerPlayerLoot/PerPlayerLoot.csproj create mode 100644 PerPlayerLoot/Properties/AssemblyInfo.cs create mode 100644 README.md diff --git a/.github/PerPlayerLoot.png b/.github/PerPlayerLoot.png new file mode 100644 index 0000000000000000000000000000000000000000..95e1f1e0db5d481fa862cd1356d526669a4afd84 GIT binary patch literal 24448 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°G^&st!!ZQ*-3d?YlBKepe$T6#u8NkeHvqq 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 literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a30d25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,398 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml diff --git a/PerPlayerLoot.sln b/PerPlayerLoot.sln new file mode 100644 index 0000000..1c0c146 --- /dev/null +++ b/PerPlayerLoot.sln @@ -0,0 +1,30 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34330.188 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PerPlayerLoot", "PerPlayerLoot\PerPlayerLoot.csproj", "{D74EA9A9-A748-45B6-8115-0B474B5E03FE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D74EA9A9-A748-45B6-8115-0B474B5E03FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D74EA9A9-A748-45B6-8115-0B474B5E03FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D74EA9A9-A748-45B6-8115-0B474B5E03FE}.Debug|x64.ActiveCfg = Debug|x64 + {D74EA9A9-A748-45B6-8115-0B474B5E03FE}.Debug|x64.Build.0 = Debug|x64 + {D74EA9A9-A748-45B6-8115-0B474B5E03FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D74EA9A9-A748-45B6-8115-0B474B5E03FE}.Release|Any CPU.Build.0 = Release|Any CPU + {D74EA9A9-A748-45B6-8115-0B474B5E03FE}.Release|x64.ActiveCfg = Release|x64 + {D74EA9A9-A748-45B6-8115-0B474B5E03FE}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DE255809-1CFB-43CC-A13F-3D6270EFADC6} + EndGlobalSection +EndGlobal diff --git a/PerPlayerLoot/FakeChestDatabase.cs b/PerPlayerLoot/FakeChestDatabase.cs new file mode 100644 index 0000000..35e38ca --- /dev/null +++ b/PerPlayerLoot/FakeChestDatabase.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +using System.Text; + +using Terraria; +using TShockAPI; +using TerrariaApi.Server; +using System.IO; +using System.IO.Streams; + +using Newtonsoft.Json; +using System.Data.SQLite; +using Newtonsoft.Json.Bson; +using System.Runtime.Serialization.Formatters.Binary; + +namespace PerPlayerLoot +{ + // barebones class representing item data which can be deserialized from json + public class JItem + { + public int id { get; set; } + public int stack { get; set; } + public byte prefix { get; set; } + } + + + // database of FakeChest's + public class FakeChestDatabase + { + // Map { UUID: { ChestID: Chest } } + public static Dictionary> fakeChestsMap = new Dictionary> { }; + + public static HashSet<(int, int)> playerPlacedChests = new HashSet<(int, int)>(); // tile x, y of player placed chests + + private static string connString = "Data Source=perplayerloot.sqlite;Version=3;"; + + public FakeChestDatabase() { } + + public void Initialize() + { + CreateTables(); + LoadFakeChests(); + } + + public void CreateTables() + { + TSPlayer.Server.SendInfoMessage("Setting up per-player chests database..."); + using (SQLiteConnection conn = new SQLiteConnection(connString)) + { + conn.Open(); + + string sql = @" + CREATE TABLE IF NOT EXISTS chests ( + id INTEGER NOT NULL, + playerUuid TEXT NOT NULL, + x INTEGER NOT NULL, + y INTEGER NOT NULL, + items BLOB NOT NULL, + PRIMARY KEY (id, playerUuid) + ); + + CREATE TABLE IF NOT EXISTS placed ( + x INTEGER NOT NULL, + y INTEGER NOT NULL, + PRIMARY KEY (x, y) + ); + "; + + using (var cmd = new SQLiteCommand(sql, conn)) + cmd.ExecuteNonQuery(); + + } + } + + public void LoadFakeChests() + { + TSPlayer.Server.SendInfoMessage("Loading per-player loot chest inventories..."); + int count = 0; + + using (SQLiteConnection conn = new SQLiteConnection(connString)) + { + conn.Open(); + + // load loot chests + using (var cmd = new SQLiteCommand("SELECT id, playerUuid, x, y, items FROM chests;", conn)) + { + SQLiteDataReader reader = cmd.ExecuteReader(); + + while (reader.Read()) + { + string playerUuid = Convert.ToString(reader["playerUuid"]); + int chestId = Convert.ToInt32(reader["id"]); + + // get the items list + List items = new List(); + + // read blob from column + MemoryStream itemsRaw = new MemoryStream((byte[]) reader["items"]); + // deserialize with bson + using (var br = new BsonReader(itemsRaw)) + { + br.ReadRootValueAsArray = true; + + // do the actual deserialization + var jItems = (new JsonSerializer()).Deserialize>(br); + + // convert each JItem to a real Item + foreach (var jItem in jItems) + { + if (jItem == null) + { + items.Add(null); + continue; + } + + var item = new Item(); + item.netDefaults(jItem.id); + item.stack = jItem.stack; + item.prefix = jItem.prefix; + + items.Add(item); + } + } + + Chest chest = new Chest(); // construct a terraria chest + chest.x = Convert.ToInt32(reader["x"]); + chest.y = Convert.ToInt32(reader["y"]); + chest.item = items.ToArray(); + + // save it in the fake chest map + var playerChests = fakeChestsMap.GetValueOrDefault(playerUuid, new Dictionary()); + fakeChestsMap[playerUuid] = playerChests; + + fakeChestsMap[playerUuid][chestId] = chest; + + count++; + } + } + + // load tile exclusions + using (var cmd = new SQLiteCommand("SELECT x, y FROM placed;", conn)) + { + SQLiteDataReader reader = cmd.ExecuteReader(); + + playerPlacedChests.Clear(); + + while (reader.Read()) + { + int x = Convert.ToInt32(reader["x"]); + int y = Convert.ToInt32(reader["y"]); + + playerPlacedChests.Add((x, y)); + } + } + } + + TSPlayer.Server.SendSuccessMessage($"Loaded {count} loot chest inventories, {playerPlacedChests.Count} player-placed chests."); + } + + public void SaveFakeChests() + { + TSPlayer.Server.SendInfoMessage("Saving per-player loot chest inventories..."); + int count = 0; + + using (SQLiteConnection conn = new SQLiteConnection(connString)) + { + conn.Open(); + + foreach (KeyValuePair> playerEntry in fakeChestsMap) + { + string playerUuid = playerEntry.Key; + var playerChests = playerEntry.Value; + + foreach (KeyValuePair chestEntry in playerChests) + { + int chestId = chestEntry.Key; + var chest = chestEntry.Value; + + List jItems = new List(chest.item.Length); + + foreach (var item in chest.item) + { + var jItem = new JItem(); + + jItem.id = item.type; + jItem.stack = item.stack; + jItem.prefix = item.prefix; + + jItems.Add(jItem); + } + + MemoryStream itemsMs = new MemoryStream(); + using (var writer = new BsonWriter(itemsMs)) + { + JsonSerializer serializer = new JsonSerializer(); + serializer.Serialize(writer, jItems); + } + + var sql = @"REPLACE INTO chests (id, playerUuid, x, y, items) VALUES (@id, @playerUuid, @x, @y, @items);"; + + using (var cmd = new SQLiteCommand(sql, conn)) + { + cmd.Parameters.AddWithValue("@id", chestId); + cmd.Parameters.AddWithValue("@playerUuid", playerUuid); + cmd.Parameters.AddWithValue("@x", chest.x); + cmd.Parameters.AddWithValue("@y", chest.y); + cmd.Parameters.AddWithValue("@items", itemsMs.ToArray()); + + cmd.ExecuteNonQuery(); + } + + count++; + } + } + + foreach ((int x, int y) in playerPlacedChests) + { + var sql = @"REPLACE INTO placed (x, y) VALUES (@x, @y);"; + + using (var cmd = new SQLiteCommand(sql, conn)) + { + cmd.Parameters.AddWithValue("@x", x); + cmd.Parameters.AddWithValue("@y", y); + + cmd.ExecuteNonQuery(); + } + } + } + + TSPlayer.Server.SendSuccessMessage($"Saved {count} loot chest inventories, {playerPlacedChests.Count} player-placed chests."); + } + + public Chest GetOrCreateFakeChest(int chestId, string playerUuid) + { + var playerChests = fakeChestsMap.GetValueOrDefault(playerUuid, new Dictionary()); + fakeChestsMap[playerUuid] = playerChests; + + if (!playerChests.ContainsKey(chestId)) + { + + Chest realChest = Main.chest[chestId]; + + // copy the chest data from the real untouched chest + Chest fakeChest = new Chest(); + fakeChest.x = realChest.x; + fakeChest.y = realChest.y; + realChest.item.CopyTo(fakeChest.item, 0); + + // save it in the fake chest list + fakeChestsMap[playerUuid][chestId] = fakeChest; + + // save the fake chests list to disk + SaveFakeChests(); + + return fakeChest; + } + + return playerChests[chestId]; + } + + public void SetChestPlayerPlaced(int tileX, int tileY) + { + playerPlacedChests.Add((tileX, tileY)); + } + + public bool IsChestPlayerPlaced(int tileX, int tileY) + { + return playerPlacedChests.Contains((tileX, tileY)); + } + } + +} diff --git a/PerPlayerLoot/PPLPlugin.cs b/PerPlayerLoot/PPLPlugin.cs new file mode 100644 index 0000000..3a87e55 --- /dev/null +++ b/PerPlayerLoot/PPLPlugin.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using System.Text; + +using Terraria; +using TShockAPI; +using TerrariaApi.Server; +using System.IO; +using System.IO.Streams; + +namespace PerPlayerLoot +{ + [ApiVersion(2, 1)] + public class PPLPlugin : TerrariaPlugin + { + #region info + public override string Name => "PerPlayerLoot"; + + public override Version Version => new Version(1, 0); + + public override string Author => "Codian"; + + public override string Description => "Duplicate loot chest inventories for each player."; + #endregion + + public static FakeChestDatabase fakeChestDb = new FakeChestDatabase(); + + public static bool enablePpl = true; + + public PPLPlugin(Main game) : base(game) {} + + public override void Initialize() + { + ServerApi.Hooks.GamePostInitialize.Register(this, OnWorldLoaded); + ServerApi.Hooks.WorldSave.Register(this, OnWorldSave); + + TShockAPI.GetDataHandlers.PlaceChest += OnChestPlace; + TShockAPI.GetDataHandlers.ChestOpen += OnChestOpen; + TShockAPI.GetDataHandlers.ChestItemChange += OnChestItemChange; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + ServerApi.Hooks.GamePostInitialize.Deregister(this, OnWorldLoaded); + ServerApi.Hooks.WorldSave.Deregister(this, OnWorldSave); + + TShockAPI.GetDataHandlers.PlaceChest -= OnChestPlace; + TShockAPI.GetDataHandlers.ChestOpen -= OnChestOpen; + TShockAPI.GetDataHandlers.ChestItemChange -= OnChestItemChange; + } + + base.Dispose(disposing); + } + + + private void OnWorldSave(WorldSaveEventArgs args) + { + fakeChestDb.SaveFakeChests(); + } + + private void OnWorldLoaded(EventArgs args) + { + fakeChestDb.Initialize(); + Commands.ChatCommands.Add(new Command("perplayerloot.toggle", ToggleCommand, "ppltoggle")); + } + + private void ToggleCommand(CommandArgs args) + { + enablePpl = !enablePpl; + if (enablePpl) { + args.Player.SendSuccessMessage("Per player loot is now enabled!"); + } else { + args.Player.SendSuccessMessage("Per player loot is now disabled! You can modify chests now and they will count as loot chests."); + } + } + + private void OnChestItemChange(object sender, GetDataHandlers.ChestItemEventArgs e) + { + if (!enablePpl) return; + + // get the chest object from id + Chest realChest = Main.chest[e.ID]; + if (realChest == null) + return; + + // check if it's a piggy bank or safe transaction + if (realChest.bankChest) + return; + + // check if this is a player placed chest + if (fakeChestDb.IsChestPlayerPlaced(realChest.x, realChest.y)) + return; + + // construct an item from the event data + Item item = new Item(); + item.netDefaults(e.Type); + item.stack = e.Stacks; + item.prefix = e.Prefix; + + // get the per-player chest + Chest fakeChest = fakeChestDb.GetOrCreateFakeChest(e.ID, e.Player.UUID); + + // update the slot with the item + fakeChest.item[e.Slot] = item; + + e.Handled = true; + } + + private byte[] ConstructSpoofedChestItemPacket(int chestId, int slot, Item item) + { + // NetMessage.SendData is hardcode tied to Main.chest, so this method is necessary to reimplement stuff :( + + MemoryStream memoryStream = new MemoryStream(); + OTAPI.PacketWriter packetWriter = new OTAPI.PacketWriter(memoryStream); + + packetWriter.BaseStream.Position = 0L; + long position = packetWriter.BaseStream.Position; + + packetWriter.BaseStream.Position += 2L; + packetWriter.Write((byte) PacketTypes.ChestItem); + + packetWriter.Write((short) chestId); + packetWriter.Write((byte) slot); + + short netId = (short)item.netID; + if (item.Name == null) + { + netId = 0; + } + + packetWriter.Write((short) item.stack); + packetWriter.Write(item.prefix); + packetWriter.Write(netId); + + int positionAfter = (int) packetWriter.BaseStream.Position; + + packetWriter.BaseStream.Position = position; + packetWriter.Write((ushort)positionAfter); + packetWriter.BaseStream.Position = positionAfter; + + return memoryStream.ToArray(); + } + + private void OnChestOpen(object sender, GetDataHandlers.ChestOpenEventArgs e) + { + if (e.Handled) return; + if (!enablePpl) return; + + // get the chest's id + int chestId = Chest.FindChest(e.X, e.Y); + if (chestId == -1) return; + + // retreive the chest object + Chest realChest = Main.chest[chestId]; + + // make sure it exists + if (realChest == null) + return; + + // piggy bank, safe, etc. + if (realChest.bankChest) + return; + + // check if it's player placed + if (fakeChestDb.IsChestPlayerPlaced(realChest.x, realChest.y)) + return; + + // make a per-player chest + Chest fakeChest = fakeChestDb.GetOrCreateFakeChest(chestId, e.Player.UUID); + + // Console.WriteLine($"Opening a fake chest for {e.Player.Name}."); + e.Player.SendInfoMessage("Loot in this chest is saved per-player!"); + + // spoof chest slots + for (int slot = 0; slot < Chest.maxItems; slot++) + { + // make a fake item stack + Item item = fakeChest.item[slot]; + + // spoof clientside slot + byte[] payload = ConstructSpoofedChestItemPacket(chestId, slot, item); + e.Player.SendRawData(payload); + } + + // trigger chest open + e.Player.SendData(PacketTypes.ChestOpen, "", chestId); + + // set the active chest on serverside + e.Player.ActiveChest = chestId; + Main.player[e.Player.Index].chest = chestId; + // notify the client to also update the clientside state + e.Player.SendData(PacketTypes.SyncPlayerChestIndex, null, e.Player.Index, chestId); + + // prevent anything else grabbing control + e.Handled = true; + return; + } + + private void OnChestPlace(object sender, GetDataHandlers.PlaceChestEventArgs e) + { + if (!enablePpl) return; + + if (!fakeChestDb.IsChestPlayerPlaced(e.TileX, e.TileY - 1)) + { + int chestId = Chest.FindChest(e.TileX, e.TileY - 1); + if (chestId != -1) + Main.chest[chestId].item = new Item[Chest.maxItems]; + } + + fakeChestDb.SetChestPlayerPlaced(e.TileX, e.TileY - 1); // this -1 is mysterious + } + } +} diff --git a/PerPlayerLoot/PerPlayerLoot.csproj b/PerPlayerLoot/PerPlayerLoot.csproj new file mode 100644 index 0000000..eba03d4 --- /dev/null +++ b/PerPlayerLoot/PerPlayerLoot.csproj @@ -0,0 +1,25 @@ + + + net6.0 + Library + false + PerPlayerLoot + PerPlayerLoot + Copyright © 2023 + 1.0.0.0 + 1.0.0.0 + AnyCPU;x64 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/PerPlayerLoot/Properties/AssemblyInfo.cs b/PerPlayerLoot/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..7ca6657 --- /dev/null +++ b/PerPlayerLoot/Properties/AssemblyInfo.cs @@ -0,0 +1,13 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d74ea9a9-a748-45b6-8115-0b474b5e03fe")] diff --git a/README.md b/README.md new file mode 100644 index 0000000..d332510 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +![Per Player Loot](.github/PerPlayerLoot.png) + +# Terraria Per Player Loot +A [TShock](https://github.com/Pryaxis/TShock) server plugin which makes +naturally spawned loot chests have a separate inventory for each player on your +server. Every player that finds a chest can loot it for themselves, even if it +has been looted by someone else before. + +## The Problem +Terraria multiplayer has one big issue - the world you play on is finite, and +there are only so many items and resources to go around. + +When you start a new world to play on with your friends, the few people who +choose to go surface exploring or caving are likely to find most of the chests, +and, as a result, most of the early game loot. + +This is not a problem with bosses in Expert or Master mode, because they drop a +loot bag for each player that damaged them, and everyone can be happy. + +This plugin makes it much more viable to run large Terraria multiplayer servers, +as there is always an incentive for exploring and caving - the loot is still +there for you to find! + +## Installation +1. Copy `PerPlayerLoot.dll` into the `ServerPlugins` directory of your TShock + server. +2. Copy `System.Data.SQLite.dll` into the `bin` directory of your TShock server. +3. Start a new world from scratch (yes, this is important, read below) and play! + +## How it Works +This plugin aims to not modify the server-side chest state +(`Terraria.Main.chest`) on chest interactions, in the hope that it can be as +least destructive to world save data as possible. + +It works by intercepting the `ChestPlace`, `ChestOpen` and `ChestItem` +packets and spoofing the contents of a loot chest by sending carefully crafted +`ChestItem` packets from an internal database. + +The per-player loot data is written on disk to the SQLite file +`perplayerloot.sqlite` on every world save, alongside a list of all +player-placed chest X and Y coordinates. **If your player-placed chests are not +in this exclusion list, they will be treated as world-generated loot chests.** + +This means that In order for player-placed chests to be unaffected, **you need +to have the plugin installed from the very beginning of your server.** If you +install it halfway through a playthrough, all chests in the world will be +treated as if they were generated and will duplicate their inventory contents +for each player. + +The only time when the `Main.chest` array is modified is when a loot chest is +possibly broken (which should rarely happen). In that case, the real chest in +`Main.chest` has its items zeroed. + +## Debug Commands +- `/ppltoggle` - Toggle the plugin packet hooks globally. **WARNING:** using + this command is unsupported and can lead to desynchronization of the + `Main.chest` array and the internal plugin state. + + Debug use only! When in a disabled state, any chests you place will become + loot chests, and any chest inventory accessed will be its **real inventory**, + not a per-player instanced one! \ No newline at end of file