├── .github
└── PerPlayerLoot.png
├── PerPlayerLoot
├── Properties
│ └── AssemblyInfo.cs
├── PerPlayerLoot.csproj
├── PPLPlugin.cs
└── FakeChestDatabase.cs
├── PerPlayerLoot.sln
├── README.md
└── .gitignore
/.github/PerPlayerLoot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xcodian/PerPlayerLoot/HEAD/.github/PerPlayerLoot.png
--------------------------------------------------------------------------------
/PerPlayerLoot/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 | [assembly: AssemblyTrademark("")]
5 | [assembly: AssemblyCulture("")]
6 |
7 | // Setting ComVisible to false makes the types in this assembly not visible
8 | // to COM components. If you need to access a type in this assembly from
9 | // COM, set the ComVisible attribute to true on that type.
10 | [assembly: ComVisible(false)]
11 |
12 | // The following GUID is for the ID of the typelib if this project is exposed to COM
13 | [assembly: Guid("d74ea9a9-a748-45b6-8115-0b474b5e03fe")]
14 |
--------------------------------------------------------------------------------
/PerPlayerLoot/PerPlayerLoot.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0
4 | Library
5 | false
6 | PerPlayerLoot
7 | PerPlayerLoot
8 | Copyright © 2023
9 | 1.0.0.0
10 | 1.0.0.0
11 | AnyCPU;x64
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/PerPlayerLoot.sln:
--------------------------------------------------------------------------------
1 | Microsoft Visual Studio Solution File, Format Version 12.00
2 | # Visual Studio Version 17
3 | VisualStudioVersion = 17.8.34330.188
4 | MinimumVisualStudioVersion = 10.0.40219.1
5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PerPlayerLoot", "PerPlayerLoot\PerPlayerLoot.csproj", "{D74EA9A9-A748-45B6-8115-0B474B5E03FE}"
6 | EndProject
7 | Global
8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
9 | Debug|Any CPU = Debug|Any CPU
10 | Debug|x64 = Debug|x64
11 | Release|Any CPU = Release|Any CPU
12 | Release|x64 = Release|x64
13 | EndGlobalSection
14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
15 | {D74EA9A9-A748-45B6-8115-0B474B5E03FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
16 | {D74EA9A9-A748-45B6-8115-0B474B5E03FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
17 | {D74EA9A9-A748-45B6-8115-0B474B5E03FE}.Debug|x64.ActiveCfg = Debug|x64
18 | {D74EA9A9-A748-45B6-8115-0B474B5E03FE}.Debug|x64.Build.0 = Debug|x64
19 | {D74EA9A9-A748-45B6-8115-0B474B5E03FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
20 | {D74EA9A9-A748-45B6-8115-0B474B5E03FE}.Release|Any CPU.Build.0 = Release|Any CPU
21 | {D74EA9A9-A748-45B6-8115-0B474B5E03FE}.Release|x64.ActiveCfg = Release|x64
22 | {D74EA9A9-A748-45B6-8115-0B474B5E03FE}.Release|x64.Build.0 = Release|x64
23 | EndGlobalSection
24 | GlobalSection(SolutionProperties) = preSolution
25 | HideSolutionNode = FALSE
26 | EndGlobalSection
27 | GlobalSection(ExtensibilityGlobals) = postSolution
28 | SolutionGuid = {DE255809-1CFB-43CC-A13F-3D6270EFADC6}
29 | EndGlobalSection
30 | EndGlobal
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Terraria Per Player Loot
4 | A [TShock](https://github.com/Pryaxis/TShock) server plugin which makes
5 | naturally spawned loot chests have a separate inventory for each player on your
6 | server. Every player that finds a chest can loot it for themselves, even if it
7 | has been looted by someone else before.
8 |
9 | ## The Problem
10 | Terraria multiplayer has one big issue - the world you play on is finite, and
11 | there are only so many items and resources to go around.
12 |
13 | When you start a new world to play on with your friends, the few people who
14 | choose to go surface exploring or caving are likely to find most of the chests,
15 | and, as a result, most of the early game loot.
16 |
17 | This is not a problem with bosses in Expert or Master mode, because they drop a
18 | loot bag for each player that damaged them, and everyone can be happy.
19 |
20 | This plugin makes it much more viable to run large Terraria multiplayer servers,
21 | as there is always an incentive for exploring and caving - the loot is still
22 | there for you to find!
23 |
24 | ## Installation
25 | 1. Copy `PerPlayerLoot.dll` into the `ServerPlugins` directory of your TShock
26 | server. You can download this from the [GitHub Releases](https://github.com/xxcodianxx/PerPlayerLoot/releases/) of this repository.
27 | 3. Start a new world from scratch (yes, this is important, read below) and play!
28 |
29 | > [!WARNING]
30 | > If migrating from version `1.0` of the plugin, you need to move `perplayerloot.sqlite`
31 | > from the server root directory to the `tshock` folder. You should see the file as `tshock/perplayerloot.sqlite`.
32 | > If you do not do this, your existing world chests will behave like loot chests!
33 |
34 | ## How it Works
35 | This plugin aims to not modify the server-side chest state
36 | (`Terraria.Main.chest`) on chest interactions, in the hope that it can be as
37 | least destructive to world save data as possible.
38 |
39 | It works by intercepting the `ChestPlace`, `ChestOpen` and `ChestItem`
40 | packets and spoofing the contents of a loot chest by sending carefully crafted
41 | `ChestItem` packets from an internal database.
42 |
43 | The per-player loot data is written on disk to the SQLite file
44 | `perplayerloot.sqlite` on every world save, alongside a list of all
45 | player-placed chest X and Y coordinates. **If your player-placed chests are not
46 | in this exclusion list, they will be treated as world-generated loot chests.**
47 |
48 | This means that In order for player-placed chests to be unaffected, **you need
49 | to have the plugin installed from the very beginning of your server.** If you
50 | install it halfway through a playthrough, all chests in the world will be
51 | treated as if they were generated and will duplicate their inventory contents
52 | for each player.
53 |
54 | The only time when the `Main.chest` array is modified is when a loot chest is
55 | possibly broken (which should rarely happen). In that case, the real chest in
56 | `Main.chest` has its items zeroed.
57 |
58 | ## Debug Commands
59 | - `/ppltoggle` - Toggle the plugin packet hooks globally. **WARNING:** using
60 | this command is unsupported and can lead to desynchronization of the
61 | `Main.chest` array and the internal plugin state.
62 |
63 | Debug use only! When in a disabled state, any chests you place will become
64 | loot chests, and any chest inventory accessed will be its **real inventory**,
65 | not a per-player instanced one!
66 |
--------------------------------------------------------------------------------
/PerPlayerLoot/PPLPlugin.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | using Terraria;
6 | using TShockAPI;
7 | using TerrariaApi.Server;
8 | using System.IO;
9 | using System.IO.Streams;
10 |
11 | namespace PerPlayerLoot
12 | {
13 | [ApiVersion(2, 1)]
14 | public class PPLPlugin : TerrariaPlugin
15 | {
16 | #region info
17 | public override string Name => "PerPlayerLoot";
18 |
19 | public override Version Version => new Version(2, 0);
20 |
21 | public override string Author => "Codian";
22 |
23 | public override string Description => "Duplicate loot chest inventories for each player.";
24 | #endregion
25 |
26 | public static FakeChestDatabase fakeChestDb = new FakeChestDatabase();
27 |
28 | public static bool enablePpl = true;
29 |
30 | public PPLPlugin(Main game) : base(game) {}
31 |
32 | public override void Initialize()
33 | {
34 | ServerApi.Hooks.GamePostInitialize.Register(this, OnWorldLoaded);
35 | ServerApi.Hooks.WorldSave.Register(this, OnWorldSave);
36 |
37 | TShockAPI.GetDataHandlers.PlaceChest += OnChestPlace;
38 | TShockAPI.GetDataHandlers.ChestOpen += OnChestOpen;
39 | TShockAPI.GetDataHandlers.ChestItemChange += OnChestItemChange;
40 | }
41 |
42 | protected override void Dispose(bool disposing)
43 | {
44 | if (disposing)
45 | {
46 | ServerApi.Hooks.GamePostInitialize.Deregister(this, OnWorldLoaded);
47 | ServerApi.Hooks.WorldSave.Deregister(this, OnWorldSave);
48 |
49 | TShockAPI.GetDataHandlers.PlaceChest -= OnChestPlace;
50 | TShockAPI.GetDataHandlers.ChestOpen -= OnChestOpen;
51 | TShockAPI.GetDataHandlers.ChestItemChange -= OnChestItemChange;
52 | }
53 |
54 | base.Dispose(disposing);
55 | }
56 |
57 |
58 | private void OnWorldSave(WorldSaveEventArgs args)
59 | {
60 | fakeChestDb.SaveFakeChests();
61 | }
62 |
63 | private void OnWorldLoaded(EventArgs args)
64 | {
65 | fakeChestDb.Initialize();
66 | Commands.ChatCommands.Add(new Command("perplayerloot.toggle", ToggleCommand, "ppltoggle"));
67 | }
68 |
69 | private void ToggleCommand(CommandArgs args)
70 | {
71 | enablePpl = !enablePpl;
72 | if (enablePpl) {
73 | args.Player.SendSuccessMessage("Per player loot is now enabled!");
74 | } else {
75 | args.Player.SendSuccessMessage("Per player loot is now disabled! You can modify chests now and they will count as loot chests.");
76 | }
77 | }
78 |
79 | private void OnChestItemChange(object sender, GetDataHandlers.ChestItemEventArgs e)
80 | {
81 | if (!enablePpl) return;
82 |
83 | // get the chest object from id
84 | Chest realChest = Main.chest[e.ID];
85 | if (realChest == null)
86 | return;
87 |
88 | // check if it's a piggy bank or safe transaction
89 | if (realChest.bankChest)
90 | return;
91 |
92 | // check if this is a player placed chest
93 | if (fakeChestDb.IsChestPlayerPlaced(realChest.x, realChest.y))
94 | return;
95 |
96 | // construct an item from the event data
97 | Item item = new Item();
98 | item.netDefaults(e.Type);
99 | item.stack = e.Stacks;
100 | item.prefix = e.Prefix;
101 |
102 | // get the per-player chest
103 | Chest fakeChest = fakeChestDb.GetOrCreateFakeChest(e.ID, e.Player.UUID);
104 |
105 | // update the slot with the item
106 | fakeChest.item[e.Slot] = item;
107 |
108 | e.Handled = true;
109 | }
110 |
111 | private byte[] ConstructSpoofedChestItemPacket(int chestId, int slot, Item item)
112 | {
113 | // NetMessage.SendData is hardcode tied to Main.chest, so this method is necessary to reimplement stuff :(
114 |
115 | MemoryStream memoryStream = new MemoryStream();
116 | OTAPI.PacketWriter packetWriter = new OTAPI.PacketWriter(memoryStream);
117 |
118 | packetWriter.BaseStream.Position = 0L;
119 | long position = packetWriter.BaseStream.Position;
120 |
121 | packetWriter.BaseStream.Position += 2L;
122 | packetWriter.Write((byte) PacketTypes.ChestItem);
123 |
124 | packetWriter.Write((short) chestId);
125 | packetWriter.Write((byte) slot);
126 |
127 | short netId = (short)item.netID;
128 | if (item.Name == null)
129 | {
130 | netId = 0;
131 | }
132 |
133 | packetWriter.Write((short) item.stack);
134 | packetWriter.Write(item.prefix);
135 | packetWriter.Write(netId);
136 |
137 | int positionAfter = (int) packetWriter.BaseStream.Position;
138 |
139 | packetWriter.BaseStream.Position = position;
140 | packetWriter.Write((ushort)positionAfter);
141 | packetWriter.BaseStream.Position = positionAfter;
142 |
143 | return memoryStream.ToArray();
144 | }
145 |
146 | private void OnChestOpen(object sender, GetDataHandlers.ChestOpenEventArgs e)
147 | {
148 | if (e.Handled) return;
149 | if (!enablePpl) return;
150 |
151 | // get the chest's id
152 | int chestId = Chest.FindChest(e.X, e.Y);
153 | if (chestId == -1) return;
154 |
155 | // retreive the chest object
156 | Chest realChest = Main.chest[chestId];
157 |
158 | // make sure it exists
159 | if (realChest == null)
160 | return;
161 |
162 | // piggy bank, safe, etc.
163 | if (realChest.bankChest)
164 | return;
165 |
166 | // check if it's player placed
167 | if (fakeChestDb.IsChestPlayerPlaced(realChest.x, realChest.y))
168 | return;
169 |
170 | // make a per-player chest
171 | Chest fakeChest = fakeChestDb.GetOrCreateFakeChest(chestId, e.Player.UUID);
172 |
173 | // Console.WriteLine($"Opening a fake chest for {e.Player.Name}.");
174 | e.Player.SendInfoMessage("Loot in this chest is saved per-player!");
175 |
176 | // spoof chest slots
177 | for (int slot = 0; slot < Chest.maxItems; slot++)
178 | {
179 | // make a fake item stack
180 | Item item = fakeChest.item[slot];
181 |
182 | // spoof clientside slot
183 | byte[] payload = ConstructSpoofedChestItemPacket(chestId, slot, item);
184 | e.Player.SendRawData(payload);
185 | }
186 |
187 | // trigger chest open
188 | e.Player.SendData(PacketTypes.ChestOpen, "", chestId);
189 |
190 | // set the active chest on serverside
191 | e.Player.ActiveChest = chestId;
192 | Main.player[e.Player.Index].chest = chestId;
193 | // notify the client to also update the clientside state
194 | e.Player.SendData(PacketTypes.SyncPlayerChestIndex, null, e.Player.Index, chestId);
195 |
196 | // prevent anything else grabbing control
197 | e.Handled = true;
198 | return;
199 | }
200 |
201 | private void OnChestPlace(object sender, GetDataHandlers.PlaceChestEventArgs e)
202 | {
203 | if (!enablePpl) return;
204 |
205 | if (!fakeChestDb.IsChestPlayerPlaced(e.TileX, e.TileY - 1))
206 | {
207 | int chestId = Chest.FindChest(e.TileX, e.TileY - 1);
208 | if (chestId != -1)
209 | Main.chest[chestId].item = new Item[Chest.maxItems];
210 | }
211 |
212 | fakeChestDb.SetChestPlayerPlaced(e.TileX, e.TileY - 1); // this -1 is mysterious
213 | }
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET Core
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # ASP.NET Scaffolding
66 | ScaffoldingReadMe.txt
67 |
68 | # StyleCop
69 | StyleCopReport.xml
70 |
71 | # Files built by Visual Studio
72 | *_i.c
73 | *_p.c
74 | *_h.h
75 | *.ilk
76 | *.meta
77 | *.obj
78 | *.iobj
79 | *.pch
80 | *.pdb
81 | *.ipdb
82 | *.pgc
83 | *.pgd
84 | *.rsp
85 | *.sbr
86 | *.tlb
87 | *.tli
88 | *.tlh
89 | *.tmp
90 | *.tmp_proj
91 | *_wpftmp.csproj
92 | *.log
93 | *.tlog
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
298 | *.vbp
299 |
300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
301 | *.dsw
302 | *.dsp
303 |
304 | # Visual Studio 6 technical files
305 | *.ncb
306 | *.aps
307 |
308 | # Visual Studio LightSwitch build output
309 | **/*.HTMLClient/GeneratedArtifacts
310 | **/*.DesktopClient/GeneratedArtifacts
311 | **/*.DesktopClient/ModelManifest.xml
312 | **/*.Server/GeneratedArtifacts
313 | **/*.Server/ModelManifest.xml
314 | _Pvt_Extensions
315 |
316 | # Paket dependency manager
317 | .paket/paket.exe
318 | paket-files/
319 |
320 | # FAKE - F# Make
321 | .fake/
322 |
323 | # CodeRush personal settings
324 | .cr/personal
325 |
326 | # Python Tools for Visual Studio (PTVS)
327 | __pycache__/
328 | *.pyc
329 |
330 | # Cake - Uncomment if you are using it
331 | # tools/**
332 | # !tools/packages.config
333 |
334 | # Tabs Studio
335 | *.tss
336 |
337 | # Telerik's JustMock configuration file
338 | *.jmconfig
339 |
340 | # BizTalk build output
341 | *.btp.cs
342 | *.btm.cs
343 | *.odx.cs
344 | *.xsd.cs
345 |
346 | # OpenCover UI analysis results
347 | OpenCover/
348 |
349 | # Azure Stream Analytics local run output
350 | ASALocalRun/
351 |
352 | # MSBuild Binary and Structured Log
353 | *.binlog
354 |
355 | # NVidia Nsight GPU debugger configuration file
356 | *.nvuser
357 |
358 | # MFractors (Xamarin productivity tool) working folder
359 | .mfractor/
360 |
361 | # Local History for Visual Studio
362 | .localhistory/
363 |
364 | # Visual Studio History (VSHistory) files
365 | .vshistory/
366 |
367 | # BeatPulse healthcheck temp database
368 | healthchecksdb
369 |
370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
371 | MigrationBackup/
372 |
373 | # Ionide (cross platform F# VS Code tools) working folder
374 | .ionide/
375 |
376 | # Fody - auto-generated XML schema
377 | FodyWeavers.xsd
378 |
379 | # VS Code files for those working on multiple tools
380 | .vscode/*
381 | !.vscode/settings.json
382 | !.vscode/tasks.json
383 | !.vscode/launch.json
384 | !.vscode/extensions.json
385 | *.code-workspace
386 |
387 | # Local History for Visual Studio Code
388 | .history/
389 |
390 | # Windows Installer files from build outputs
391 | *.cab
392 | *.msi
393 | *.msix
394 | *.msm
395 | *.msp
396 |
397 | # JetBrains Rider
398 | *.sln.iml
399 |
--------------------------------------------------------------------------------
/PerPlayerLoot/FakeChestDatabase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | using Terraria;
6 | using TShockAPI;
7 | using TerrariaApi.Server;
8 | using System.IO;
9 | using System.IO.Streams;
10 |
11 | using Newtonsoft.Json;
12 | using Newtonsoft.Json.Bson;
13 | using System.Runtime.Serialization.Formatters.Binary;
14 | using Microsoft.Data.Sqlite;
15 |
16 | namespace PerPlayerLoot
17 | {
18 | // barebones class representing item data which can be deserialized from json
19 | public class JItem
20 | {
21 | public int id { get; set; }
22 | public int stack { get; set; }
23 | public byte prefix { get; set; }
24 | }
25 |
26 |
27 | // database of FakeChest's
28 | public class FakeChestDatabase
29 | {
30 | // Map { UUID: { ChestID: Chest } }
31 | public static Dictionary> fakeChestsMap = new Dictionary> { };
32 |
33 | public static HashSet<(int, int)> playerPlacedChests = new HashSet<(int, int)>(); // tile x, y of player placed chests
34 |
35 | private static string connString = "Data Source=tshock/perplayerloot.sqlite";
36 |
37 | public FakeChestDatabase() { }
38 |
39 | public void Initialize()
40 | {
41 | CreateTables();
42 | LoadFakeChests();
43 | }
44 |
45 | public void CreateTables()
46 | {
47 | TSPlayer.Server.SendInfoMessage("Setting up per-player chests database...");
48 | using (SqliteConnection conn = new SqliteConnection(connString))
49 | {
50 | conn.Open();
51 |
52 | string sql = @"
53 | CREATE TABLE IF NOT EXISTS chests (
54 | id INTEGER NOT NULL,
55 | playerUuid TEXT NOT NULL,
56 | x INTEGER NOT NULL,
57 | y INTEGER NOT NULL,
58 | items BLOB NOT NULL,
59 | PRIMARY KEY (id, playerUuid)
60 | );
61 |
62 | CREATE TABLE IF NOT EXISTS placed (
63 | x INTEGER NOT NULL,
64 | y INTEGER NOT NULL,
65 | PRIMARY KEY (x, y)
66 | );
67 | ";
68 |
69 | using (var cmd = new SqliteCommand(sql, conn))
70 | cmd.ExecuteNonQuery();
71 |
72 | }
73 | }
74 |
75 | public void LoadFakeChests()
76 | {
77 | TSPlayer.Server.SendInfoMessage("Loading per-player loot chest inventories...");
78 | int count = 0;
79 |
80 | using (SqliteConnection conn = new SqliteConnection(connString))
81 | {
82 | conn.Open();
83 |
84 | // load loot chests
85 | using (var cmd = new SqliteCommand("SELECT id, playerUuid, x, y, items FROM chests;", conn))
86 | {
87 | SqliteDataReader reader = cmd.ExecuteReader();
88 |
89 | while (reader.Read())
90 | {
91 | string playerUuid = Convert.ToString(reader["playerUuid"]);
92 | int chestId = Convert.ToInt32(reader["id"]);
93 |
94 | // get the items list
95 | List- items = new List
- ();
96 |
97 | // read blob from column
98 | MemoryStream itemsRaw = new MemoryStream((byte[]) reader["items"]);
99 | // deserialize with bson
100 | using (var br = new BsonReader(itemsRaw))
101 | {
102 | br.ReadRootValueAsArray = true;
103 |
104 | // do the actual deserialization
105 | var jItems = (new JsonSerializer()).Deserialize>(br);
106 |
107 | // convert each JItem to a real Item
108 | foreach (var jItem in jItems)
109 | {
110 | if (jItem == null)
111 | {
112 | items.Add(null);
113 | continue;
114 | }
115 |
116 | var item = new Item();
117 | item.netDefaults(jItem.id);
118 | item.stack = jItem.stack;
119 | item.prefix = jItem.prefix;
120 |
121 | items.Add(item);
122 | }
123 | }
124 |
125 | Chest chest = new Chest(); // construct a terraria chest
126 | chest.x = Convert.ToInt32(reader["x"]);
127 | chest.y = Convert.ToInt32(reader["y"]);
128 | chest.item = items.ToArray();
129 |
130 | // save it in the fake chest map
131 | var playerChests = fakeChestsMap.GetValueOrDefault(playerUuid, new Dictionary());
132 | fakeChestsMap[playerUuid] = playerChests;
133 |
134 | fakeChestsMap[playerUuid][chestId] = chest;
135 |
136 | count++;
137 | }
138 | }
139 |
140 | // load tile exclusions
141 | using (var cmd = new SqliteCommand("SELECT x, y FROM placed;", conn))
142 | {
143 | SqliteDataReader reader = cmd.ExecuteReader();
144 |
145 | playerPlacedChests.Clear();
146 |
147 | while (reader.Read())
148 | {
149 | int x = Convert.ToInt32(reader["x"]);
150 | int y = Convert.ToInt32(reader["y"]);
151 |
152 | playerPlacedChests.Add((x, y));
153 | }
154 | }
155 | }
156 |
157 | // TSPlayer.Server.SendSuccessMessage($"Loaded {count} loot chest inventories, {playerPlacedChests.Count} player-placed chests.");
158 | //I think it's too frequent, I suggest commenting out.
159 | }
160 |
161 | public void SaveFakeChests(string? PlayerUuid = null, int? ChestId = null)
162 | {
163 | //TSPlayer.Server.SendInfoMessage("Saving per-player loot chest inventories...");
164 | //I think it's too frequent, I suggest commenting out.
165 | int count = 0;
166 |
167 | using (SqliteConnection conn = new SqliteConnection(connString))
168 | {
169 | conn.Open();
170 |
171 | foreach (KeyValuePair> playerEntry in fakeChestsMap)
172 | {
173 | string playerUuid = playerEntry.Key;
174 | // If a player UUID is specified and the current player does not match, then skip
175 | if (PlayerUuid != null && playerUuid != PlayerUuid)
176 | {
177 | continue;
178 | }
179 | var playerChests = playerEntry.Value;
180 |
181 | foreach (KeyValuePair chestEntry in playerChests)
182 | {
183 | int chestId = chestEntry.Key;
184 | // If a chest ID is specified and the current chest does not match, then skip
185 | if (ChestId != null && chestId != ChestId)
186 | {
187 | continue;
188 | }
189 | var chest = chestEntry.Value;
190 |
191 | List jItems = new List(chest.item.Length);
192 |
193 | foreach (var item in chest.item)
194 | {
195 | var jItem = new JItem();
196 |
197 | jItem.id = item.type;
198 | jItem.stack = item.stack;
199 | jItem.prefix = item.prefix;
200 |
201 | jItems.Add(jItem);
202 | }
203 |
204 | MemoryStream itemsMs = new MemoryStream();
205 | using (var writer = new BsonWriter(itemsMs))
206 | {
207 | JsonSerializer serializer = new JsonSerializer();
208 | serializer.Serialize(writer, jItems);
209 | }
210 |
211 | var sql = @"REPLACE INTO chests (id, playerUuid, x, y, items) VALUES (@id, @playerUuid, @x, @y, @items);";
212 |
213 | using (var cmd = new SqliteCommand(sql, conn))
214 | {
215 | cmd.Parameters.AddWithValue("@id", chestId);
216 | cmd.Parameters.AddWithValue("@playerUuid", playerUuid);
217 | cmd.Parameters.AddWithValue("@x", chest.x);
218 | cmd.Parameters.AddWithValue("@y", chest.y);
219 | cmd.Parameters.AddWithValue("@items", itemsMs.ToArray());
220 |
221 | cmd.ExecuteNonQuery();
222 | }
223 |
224 | count++;
225 | }
226 | }
227 |
228 | foreach ((int x, int y) in playerPlacedChests)
229 | {
230 | var sql = @"REPLACE INTO placed (x, y) VALUES (@x, @y);";
231 |
232 | using (var cmd = new SqliteCommand(sql, conn))
233 | {
234 | cmd.Parameters.AddWithValue("@x", x);
235 | cmd.Parameters.AddWithValue("@y", y);
236 |
237 | cmd.ExecuteNonQuery();
238 | }
239 | }
240 | }
241 |
242 | TSPlayer.Server.SendSuccessMessage($"Saved {count} loot chest inventories, {playerPlacedChests.Count} player-placed chests.");
243 | }
244 |
245 | public Chest GetOrCreateFakeChest(int chestId, string playerUuid)
246 | {
247 | var playerChests = fakeChestsMap.GetValueOrDefault(playerUuid, new Dictionary());
248 | fakeChestsMap[playerUuid] = playerChests;
249 |
250 | if (!playerChests.ContainsKey(chestId))
251 | {
252 | var realChest = Main.chest[chestId];
253 |
254 | // copy the chest data from the real untouched chest
255 | var fakeChest = new Chest
256 | {
257 | x = realChest.x,
258 | y = realChest.y
259 | };
260 | realChest.item.CopyTo(fakeChest.item, 0);
261 |
262 | // save it in the fake chest list
263 | fakeChestsMap[playerUuid][chestId] = fakeChest;
264 |
265 | // save the fake chests list to disk
266 | SaveFakeChests(playerUuid, chestId);
267 |
268 | return fakeChest;
269 | }
270 |
271 | return playerChests[chestId];
272 | }
273 |
274 | public void SetChestPlayerPlaced(int tileX, int tileY)
275 | {
276 | playerPlacedChests.Add((tileX, tileY));
277 | }
278 |
279 | public bool IsChestPlayerPlaced(int tileX, int tileY)
280 | {
281 | return playerPlacedChests.Contains((tileX, tileY));
282 | }
283 | }
284 |
285 | }
286 |
--------------------------------------------------------------------------------