From 91c578815cbee3f0b4565100284192c6ce56fe24 Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 11 Jan 2025 11:19:16 +1000 Subject: [PATCH 01/35] Update net6 refs to net9 --- .github/workflows/ci-otapi3.yml | 12 ++++++------ .vscode/launch.json | 6 +++--- .vscode/tasks.json | 2 +- Dockerfile | 2 +- README.md | 4 ++-- README_cn.md | 4 ++-- TShockAPI/TShockAPI.csproj | 2 +- TShockInstaller/TShockInstaller.csproj | 2 +- TShockLauncher.Tests/TShockLauncher.Tests.csproj | 2 +- TShockLauncher/TShockLauncher.csproj | 8 ++++---- TShockPluginManager/Nuget.cs | 2 +- TShockPluginManager/TShockPluginManager.csproj | 2 +- appveyor.yml | 2 +- 13 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci-otapi3.yml b/.github/workflows/ci-otapi3.yml index f5599ffc..a0a4a07b 100644 --- a/.github/workflows/ci-otapi3.yml +++ b/.github/workflows/ci-otapi3.yml @@ -39,27 +39,27 @@ jobs: - name: Produce installer run: | cd TShockInstaller - dotnet publish -r ${{ matrix.arch }} -f net6.0 -c Release -p:PublishSingleFile=true --self-contained true + dotnet publish -r ${{ matrix.arch }} -f net9.0 -c Release -p:PublishSingleFile=true --self-contained true - name: Produce build run: | cd TShockLauncher - dotnet publish -r ${{ matrix.arch }} -f net6.0 -c Release -p:PublishSingleFile=true --self-contained false + dotnet publish -r ${{ matrix.arch }} -f net9.0 -c Release -p:PublishSingleFile=true --self-contained false - name: Chmod scripts if: ${{ matrix.arch != 'win-x64' }} run: | - chmod +x TShockLauncher/bin/Release/net6.0/${{ matrix.arch }}/publish/TShock.Server + chmod +x TShockLauncher/bin/Release/net9.0/${{ matrix.arch }}/publish/TShock.Server - name: Copy installer run: | - cp TShockInstaller/bin/Release/net6.0/${{ matrix.arch }}/publish/* TShockLauncher/bin/Release/net6.0/${{ matrix.arch }}/publish/ + cp TShockInstaller/bin/Release/net9.0/${{ matrix.arch }}/publish/* TShockLauncher/bin/Release/net9.0/${{ matrix.arch }}/publish/ # preserve file perms: https://github.com/actions/upload-artifact#maintaining-file-permissions-and-case-sensitive-files - name: Tarball artifact (non-Windows) if: ${{ matrix.arch != 'win-x64' }} run: | - cd TShockLauncher/bin/Release/net6.0/${{ matrix.arch }}/publish/ + cd TShockLauncher/bin/Release/net9.0/${{ matrix.arch }}/publish/ tar -cvf ../../../../../../TShock-Beta-${{ matrix.arch }}-Release.tar * - name: Upload artifact (non-Windows) @@ -74,4 +74,4 @@ jobs: if: ${{ matrix.arch == 'win-x64' }} with: name: TShock-Beta-${{ matrix.arch }}-Release - path: TShockLauncher/bin/Release/net6.0/${{ matrix.arch }}/publish/ + path: TShockLauncher/bin/Release/net9.0/${{ matrix.arch }}/publish/ diff --git a/.vscode/launch.json b/.vscode/launch.json index a4795b3a..78bc651f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,12 +9,12 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/TShockLauncher/bin/Debug/net6.0/TShock.Run.dll", + "program": "${workspaceFolder}/TShockLauncher/bin/Debug/net9.0/TShock.Run.dll", "windows": { - "program": "${workspaceFolder}/TShockLauncher/bin/Debug/net6.0/TShock.dll", + "program": "${workspaceFolder}/TShockLauncher/bin/Debug/net9.0/TShock.dll", }, "args": [], - "cwd": "${workspaceFolder}/TShockLauncher/bin/Debug/net6.0/", + "cwd": "${workspaceFolder}/TShockLauncher/bin/Debug/net9.0/", "console": "integratedTerminal", "stopAtEntry": false }, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e8db5b18..a9081116 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -35,7 +35,7 @@ { "label": "Remote Publish", "options": { - "cwd": "TShockLauncher/bin/Debug/net6.0/linux-arm64" + "cwd": "TShockLauncher/bin/Debug/net9.0/linux-arm64" }, "command": "C:\\Program Files\\PuTTY\\pscp.exe", "type": "process", diff --git a/Dockerfile b/Dockerfile index 87024fb9..f9071c0b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,7 @@ RUN \ *) echo "Error: Unsupported platform ${TARGETPLATFORM}" && exit 1 \ ;; \ esac && \ - dotnet publish -o output/ -r "${ARCH}" -v m -f net6.0 -c Release -p:PublishSingleFile=true --self-contained false + dotnet publish -o output/ -r "${ARCH}" -v m -f net9.0 -c Release -p:PublishSingleFile=true --self-contained false # Runtime image FROM --platform=${TARGETPLATFORM} mcr.microsoft.com/dotnet/runtime:6.0 AS runner diff --git a/README.md b/README.md index 0369df53..bf738a07 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ If you want to run the `TShockLauncher` (which runs a server), run: To produce a packaged release (suitable for distribution), run: 1. `cd TShockLauncher` -1. `dotnet publish -r win-x64 -f net6.0 -c Release -p:PublishSingleFile=true --self-contained false` +1. `dotnet publish -r win-x64 -f net9.0 -c Release -p:PublishSingleFile=true --self-contained false` -Note that in this example, you'd be building for `win-x64`. You can build for `win-x64`, `osx-x64`, `linux-x64`, `linux-arm64`, `linux-arm`. Your release will be in the `TShockLauncher/bin/Release/net6.0/` folder under the architecture you specified. +Note that in this example, you'd be building for `win-x64`. You can build for `win-x64`, `osx-x64`, `linux-x64`, `linux-arm64`, `linux-arm`. Your release will be in the `TShockLauncher/bin/Release/net9.0/` folder under the architecture you specified. ### Working with Terraria diff --git a/README_cn.md b/README_cn.md index 65c5eba6..a31c9345 100644 --- a/README_cn.md +++ b/README_cn.md @@ -30,9 +30,9 @@ TShock是为泰拉瑞亚服务器和社区开发的一个工具箱。这个工 如果要生成打包后的发行版,运行: 1. `cd TShockLauncher` -1. `dotnet publish -r win-x64 -f net6.0 -c Release -p:PublishSingleFile=true --self-contained false` +1. `dotnet publish -r win-x64 -f net9.0 -c Release -p:PublishSingleFile=true --self-contained false` -注意在这个例子中你将会生成`win-x64`架构的版本。你也可以生成`win-x64`、`osx-x64`、`linux-x64`、`linux-arm64`、`linux-arm`的版本。你可以在`TShockLauncher/bin/Release/net6.0/`文件夹下对应架构的文件夹里找到生成后的发行版。 +注意在这个例子中你将会生成`win-x64`架构的版本。你也可以生成`win-x64`、`osx-x64`、`linux-x64`、`linux-arm64`、`linux-arm`的版本。你可以在`TShockLauncher/bin/Release/net9.0/`文件夹下对应架构的文件夹里找到生成后的发行版。 ### 跟泰拉瑞亚本体代码交互 diff --git a/TShockAPI/TShockAPI.csproj b/TShockAPI/TShockAPI.csproj index 9da8cd08..30389bb4 100644 --- a/TShockAPI/TShockAPI.csproj +++ b/TShockAPI/TShockAPI.csproj @@ -3,7 +3,7 @@ Library - net6.0 + net9.0 true True @@ -37,15 +37,15 @@ - + PreserveNewest true - + PreserveNewest true - + PreserveNewest true diff --git a/TShockPluginManager/Nuget.cs b/TShockPluginManager/Nuget.cs index 9fdefb6b..4e22209b 100644 --- a/TShockPluginManager/Nuget.cs +++ b/TShockPluginManager/Nuget.cs @@ -53,7 +53,7 @@ namespace TShockPluginManager public Nugetter() { FrameworkReducer = new FrameworkReducer(); - NuGetFramework = NuGetFramework.ParseFolder("net6.0"); + NuGetFramework = NuGetFramework.ParseFolder("net9.0"); Settings = NuGet.Configuration.Settings.LoadDefaultSettings(root: null); PathContext = NuGetPathContext.Create(Settings); PackageSourceProvider = new PackageSourceProvider(Settings); diff --git a/TShockPluginManager/TShockPluginManager.csproj b/TShockPluginManager/TShockPluginManager.csproj index 1d53f320..10dad2f9 100644 --- a/TShockPluginManager/TShockPluginManager.csproj +++ b/TShockPluginManager/TShockPluginManager.csproj @@ -1,7 +1,7 @@ - net6.0 + net9.0 enable enable diff --git a/appveyor.yml b/appveyor.yml index a96c0049..7b71d778 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,5 +9,5 @@ build_script: dotnet test artifacts: -- path: ./TShockLauncher/bin/Debug/net6.0 +- path: ./TShockLauncher/bin/Debug/net9.0 name: TShockAVDebug From d404b5a64c3aa404d24b3a3ee2c00859c188b890 Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 11 Jan 2025 11:20:51 +1000 Subject: [PATCH 02/35] Update to OTAPI 3.2.4 --- TShockLauncher/TShockLauncher.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TShockLauncher/TShockLauncher.csproj b/TShockLauncher/TShockLauncher.csproj index 64d835ba..80cd8bd5 100644 --- a/TShockLauncher/TShockLauncher.csproj +++ b/TShockLauncher/TShockLauncher.csproj @@ -32,11 +32,11 @@ - + - + PreserveNewest true From b08801e9a774fd3f2486a0c8aaeba89d6888d471 Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 11 Jan 2025 14:13:50 +1000 Subject: [PATCH 03/35] Update vscode launch profile --- .vscode/launch.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 78bc651f..43386a5d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,10 +9,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/TShockLauncher/bin/Debug/net9.0/TShock.Run.dll", - "windows": { - "program": "${workspaceFolder}/TShockLauncher/bin/Debug/net9.0/TShock.dll", - }, + "program": "${workspaceFolder}/TShockLauncher/bin/Debug/net9.0/TShock.Server.dll", "args": [], "cwd": "${workspaceFolder}/TShockLauncher/bin/Debug/net9.0/", "console": "integratedTerminal", From d9de3c1fc00c443b6ca7b3bc0456ad3009d56763 Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 11 Jan 2025 14:14:04 +1000 Subject: [PATCH 04/35] Update installer runtime urls for net9 --- TShockInstaller/Program.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TShockInstaller/Program.cs b/TShockInstaller/Program.cs index 03606b51..09ad8bf6 100644 --- a/TShockInstaller/Program.cs +++ b/TShockInstaller/Program.cs @@ -27,11 +27,11 @@ if (arch is null) string? url = null; if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - url = $"https://dotnetcli.azureedge.net/dotnet/Runtime/6.0.11/dotnet-runtime-6.0.11-osx-{arch}.tar.gz"; + url = $"https://dotnetcli.azureedge.net/dotnet/Runtime/9.0.0/dotnet-runtime-9.0.0-osx-{arch}.tar.gz"; else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - url = $"https://dotnetcli.azureedge.net/dotnet/Runtime/6.0.11/dotnet-runtime-6.0.11-win-{arch}.zip"; + url = $"https://dotnetcli.azureedge.net/dotnet/Runtime/9.0.0/dotnet-runtime-9.0.0-win-{arch}.zip"; else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - url = $"https://dotnetcli.azureedge.net/dotnet/Runtime/6.0.11/dotnet-runtime-6.0.11-linux-{arch}.tar.gz"; + url = $"https://dotnetcli.azureedge.net/dotnet/Runtime/9.0.0/dotnet-runtime-9.0.0-linux-{arch}.tar.gz"; if(url is null) { From 6a3e8c3d5df5db0cd8a0683100453f930a39d36c Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 11 Jan 2025 14:32:24 +1000 Subject: [PATCH 05/35] Improve launcher assembly resolution This addresses dev instances unable to resolve binaries, and types being requested by the plugin manager before the resolver is attached --- TShockLauncher/Program.cs | 23 +++++++++++------------ TShockLauncher/TShockLauncher.csproj | 8 +++++++- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/TShockLauncher/Program.cs b/TShockLauncher/Program.cs index 4a139370..84ddedc1 100644 --- a/TShockLauncher/Program.cs +++ b/TShockLauncher/Program.cs @@ -27,22 +27,12 @@ along with this program. If not, see . */ using System.Reflection; -using TShockPluginManager; - -if (args.Length > 0 && args[0].ToLower() == "plugins") -{ - var items = args.ToList(); - items.RemoveAt(0); - await NugetCLI.Main(items); - return; -} - Dictionary _cache = new Dictionary(); System.Runtime.Loader.AssemblyLoadContext.Default.Resolving += Default_Resolving; -Start(); +await StartAsync(); /// /// Resolves a module from the ./bin folder, either with a .dll by preference or .exe @@ -53,6 +43,7 @@ Assembly? Default_Resolving(System.Runtime.Loader.AssemblyLoadContext arg1, Asse if (_cache.TryGetValue(arg2.Name, out Assembly? asm) && asm is not null) return asm; var loc = Path.Combine(AppContext.BaseDirectory, "bin", arg2.Name + ".dll"); + if (File.Exists(loc)) asm = arg1.LoadFromAssemblyPath(loc); @@ -70,7 +61,15 @@ Assembly? Default_Resolving(System.Runtime.Loader.AssemblyLoadContext arg1, Asse /// Initiates the TSAPI server. /// /// This method exists so that the resolver can attach before TSAPI needs its dependencies. -void Start() +async Task StartAsync() { + if (args.Length > 0 && args[0].ToLower() == "plugins") + { + var items = args.ToList(); + items.RemoveAt(0); + await TShockPluginManager.NugetCLI.Main(items); + return; + } + TerrariaApi.Server.Program.Main(args); } diff --git a/TShockLauncher/TShockLauncher.csproj b/TShockLauncher/TShockLauncher.csproj index 80cd8bd5..0e1c5b76 100644 --- a/TShockLauncher/TShockLauncher.csproj +++ b/TShockLauncher/TShockLauncher.csproj @@ -91,7 +91,13 @@ - + + + + + + + From 337f15c0efc40a7ea906e6a13d93f476fcc5c6b0 Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 11 Jan 2025 14:35:38 +1000 Subject: [PATCH 06/35] Remote reset patch and MM dep This is addressed in https://github.com/SignatureBeef/Open-Terraria-API/tree/24d30d37b3eb1f606c6616a357c91aa23626420a --- TShockAPI/TShock.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/TShockAPI/TShock.cs b/TShockAPI/TShock.cs index bcd72aa3..054e31df 100644 --- a/TShockAPI/TShock.cs +++ b/TShockAPI/TShock.cs @@ -428,8 +428,6 @@ namespace TShockAPI Hooks.AccountHooks.AccountDelete += OnAccountDelete; Hooks.AccountHooks.AccountCreate += OnAccountCreate; - On.Terraria.RemoteClient.Reset += RemoteClient_Reset; - GetDataHandlers.InitGetDataHandler(); Commands.InitCommands(); @@ -498,12 +496,6 @@ namespace TShockAPI } } - private static void RemoteClient_Reset(On.Terraria.RemoteClient.orig_Reset orig, RemoteClient client) - { - client.ClientUUID = null; - orig(client); - } - private static void OnAchievementInitializerLoad(ILContext il) { // Modify AchievementInitializer.Load to remove the Main.netMode == 2 check (occupies the first 4 IL instructions) From 90f1d49887d5338e0b87f80b0872e10530334df4 Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 11 Jan 2025 14:39:14 +1000 Subject: [PATCH 07/35] Replace AchievementInitializer MM IL with static hook --- TShockAPI/TShock.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/TShockAPI/TShock.cs b/TShockAPI/TShock.cs index 054e31df..2be5cfe7 100644 --- a/TShockAPI/TShock.cs +++ b/TShockAPI/TShock.cs @@ -447,7 +447,7 @@ namespace TShockAPI // Initialize the AchievementManager, which is normally only done on clients. Game._achievements = new AchievementManager(); - IL.Terraria.Initializers.AchievementInitializer.Load += OnAchievementInitializerLoad; + OTAPI.Hooks.Initializers.AchievementInitializerLoad += OnAchievementInitializerLoad; // Actually call AchievementInitializer.Load, which is also normally only done on clients. AchievementInitializer.Load(); @@ -496,11 +496,9 @@ namespace TShockAPI } } - private static void OnAchievementInitializerLoad(ILContext il) + private static void OnAchievementInitializerLoad(object sender, OTAPI.Hooks.Initializers.AchievementInitializerLoadEventArgs args) { - // Modify AchievementInitializer.Load to remove the Main.netMode == 2 check (occupies the first 4 IL instructions) - for (var i = 0; i < 4; i++) - il.Body.Instructions.RemoveAt(0); + args.ShouldLoad = true; } protected void CrashReporter_HeapshotRequesting(object sender, EventArgs e) @@ -524,7 +522,7 @@ namespace TShockAPI } SaveManager.Instance.Dispose(); - IL.Terraria.Initializers.AchievementInitializer.Load -= OnAchievementInitializerLoad; + OTAPI.Hooks.Initializers.AchievementInitializerLoad -= OnAchievementInitializerLoad; ModuleManager.Dispose(); From 756f3138dd4ed642eadba5d8db296009dcc28f6b Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 11 Jan 2025 14:43:05 +1000 Subject: [PATCH 08/35] Update workflows actions to @v4 and net9 --- .github/workflows/ci-otapi3-nuget.yml | 6 +++--- .github/workflows/ci-otapi3.yml | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci-otapi3-nuget.yml b/.github/workflows/ci-otapi3-nuget.yml index 04b03a95..7834ccf0 100644 --- a/.github/workflows/ci-otapi3-nuget.yml +++ b/.github/workflows/ci-otapi3-nuget.yml @@ -11,13 +11,13 @@ jobs: environment: release steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: 'recursive' - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.400 + dotnet-version: 9.0.x - name: Restore dependencies run: dotnet restore - name: Build diff --git a/.github/workflows/ci-otapi3.yml b/.github/workflows/ci-otapi3.yml index a0a4a07b..f7e50037 100644 --- a/.github/workflows/ci-otapi3.yml +++ b/.github/workflows/ci-otapi3.yml @@ -7,13 +7,13 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: 'recursive' - - uses: actions/setup-dotnet@v3 + - uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.100' + dotnet-version: 9.0.x - name: Run tests run: dotnet test @@ -25,13 +25,13 @@ jobs: arch: ["win-x64", "osx-x64", "linux-x64", "linux-arm64", "linux-arm"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: 'recursive' - - uses: actions/setup-dotnet@v3 + - uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.100' + dotnet-version: 9.0.x - name: Install msgfmt run: sudo apt-get install -y gettext @@ -63,14 +63,14 @@ jobs: tar -cvf ../../../../../../TShock-Beta-${{ matrix.arch }}-Release.tar * - name: Upload artifact (non-Windows) - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ matrix.arch != 'win-x64' }} with: name: TShock-Beta-${{ matrix.arch }}-Release path: TShock-Beta-${{ matrix.arch }}-Release.tar - name: Upload artifact (Windows) - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ matrix.arch == 'win-x64' }} with: name: TShock-Beta-${{ matrix.arch }}-Release From 9b6a245c2c07d6d98eb5d6364534b8094a9f1dc2 Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 11 Jan 2025 14:44:50 +1000 Subject: [PATCH 09/35] Update dockerfile to use dotnet/sdk:9.0 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f9071c0b..4640b168 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # TARGETPLATFORM and BUILDPLATFORM are automatically filled in by Docker buildx. # They should not be set in the global scope manually. -FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/dotnet/sdk:6.0 AS builder +FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/dotnet/sdk:9.0 AS builder # Copy build context WORKDIR /TShock From 0c4c7db52e5ec77a09c924b5d71ba4526a5e4f60 Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 11 Jan 2025 19:56:38 +1000 Subject: [PATCH 10/35] Update nuget packages --- TShockAPI/TShockAPI.csproj | 6 +++--- TShockLauncher.Tests/GroupTests.cs | 6 +++--- TShockLauncher.Tests/ServerInitTests.cs | 2 +- TShockLauncher.Tests/TShockLauncher.Tests.csproj | 13 ++++++++----- TShockLauncher/TShockLauncher.csproj | 6 +++--- TShockPluginManager/TShockPluginManager.csproj | 10 +++++----- 6 files changed, 23 insertions(+), 20 deletions(-) diff --git a/TShockAPI/TShockAPI.csproj b/TShockAPI/TShockAPI.csproj index 30389bb4..41bc03ff 100644 --- a/TShockAPI/TShockAPI.csproj +++ b/TShockAPI/TShockAPI.csproj @@ -33,9 +33,9 @@ - - - + + + diff --git a/TShockLauncher.Tests/GroupTests.cs b/TShockLauncher.Tests/GroupTests.cs index 5eff651f..01a058ca 100644 --- a/TShockLauncher.Tests/GroupTests.cs +++ b/TShockLauncher.Tests/GroupTests.cs @@ -21,17 +21,17 @@ public class GroupTests groups.AddPermissions("test", new() { "abc" }); var hasperm = groups.GetGroupByName("test").Permissions.Contains("abc"); - Assert.IsTrue(hasperm); + Assert.That(hasperm, Is.True); groups.DeletePermissions("test", new() { "abc" }); hasperm = groups.GetGroupByName("test").Permissions.Contains("abc"); - Assert.IsFalse(hasperm); + Assert.That(hasperm, Is.False); groups.DeleteGroup("test"); var g = groups.GetGroupByName("test"); - Assert.IsNull(g); + Assert.That(g, Is.Null); } } diff --git a/TShockLauncher.Tests/ServerInitTests.cs b/TShockLauncher.Tests/ServerInitTests.cs index dc5f6731..e0926c66 100644 --- a/TShockLauncher.Tests/ServerInitTests.cs +++ b/TShockLauncher.Tests/ServerInitTests.cs @@ -27,7 +27,7 @@ public class ServerInitTests On.Terraria.Main.DedServ -= cb; - Assert.IsTrue(hit); + Assert.That(hit, Is.True); } } diff --git a/TShockLauncher.Tests/TShockLauncher.Tests.csproj b/TShockLauncher.Tests/TShockLauncher.Tests.csproj index e58ca178..9be3968a 100644 --- a/TShockLauncher.Tests/TShockLauncher.Tests.csproj +++ b/TShockLauncher.Tests/TShockLauncher.Tests.csproj @@ -7,11 +7,14 @@ - - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/TShockLauncher/TShockLauncher.csproj b/TShockLauncher/TShockLauncher.csproj index 0e1c5b76..875e6287 100644 --- a/TShockLauncher/TShockLauncher.csproj +++ b/TShockLauncher/TShockLauncher.csproj @@ -30,10 +30,10 @@ - - + + - + diff --git a/TShockPluginManager/TShockPluginManager.csproj b/TShockPluginManager/TShockPluginManager.csproj index 10dad2f9..7ad2429f 100644 --- a/TShockPluginManager/TShockPluginManager.csproj +++ b/TShockPluginManager/TShockPluginManager.csproj @@ -7,12 +7,12 @@ - - - + + + - - + + From 7ffb431550f6deb8b5d0b3000a9234740f170c67 Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 11 Jan 2025 20:00:23 +1000 Subject: [PATCH 11/35] Update server init test to static hook --- TShockLauncher.Tests/ServerInitTests.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/TShockLauncher.Tests/ServerInitTests.cs b/TShockLauncher.Tests/ServerInitTests.cs index e0926c66..2ab72bd1 100644 --- a/TShockLauncher.Tests/ServerInitTests.cs +++ b/TShockLauncher.Tests/ServerInitTests.cs @@ -14,18 +14,19 @@ public class ServerInitTests public void EnsureBoots() { var are = new AutoResetEvent(false); - On.Terraria.Main.hook_DedServ cb = (On.Terraria.Main.orig_DedServ orig, Terraria.Main instance) => + HookEvents.HookDelegate cb = (instance, args) => { + args.ContinueExecution = false; are.Set(); Debug.WriteLine("Server init process successful"); }; - On.Terraria.Main.DedServ += cb; + HookEvents.Terraria.Main.DedServ += cb; - new Thread(() => TerrariaApi.Server.Program.Main(new string[] { })).Start(); + new Thread(() => TerrariaApi.Server.Program.Main([])).Start(); var hit = are.WaitOne(TimeSpan.FromSeconds(10)); - On.Terraria.Main.DedServ -= cb; + HookEvents.Terraria.Main.DedServ -= cb; Assert.That(hit, Is.True); } From 608111cfb9643fa9425ddb3e19eea9cf8349f912 Mon Sep 17 00:00:00 2001 From: Luke Date: Sun, 26 Jan 2025 21:16:04 +1000 Subject: [PATCH 12/35] Update TerrariaServerAPI --- TerrariaServerAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TerrariaServerAPI b/TerrariaServerAPI index 8a3fffd7..2c82f673 160000 --- a/TerrariaServerAPI +++ b/TerrariaServerAPI @@ -1 +1 @@ -Subproject commit 8a3fffd71db401736ea80619122c70c449c10ff3 +Subproject commit 2c82f6739e4c76e9e01c5fe7feadd91d9f6fd0ec From d2030991c4b0d249f987adaeac786c2e785e7b39 Mon Sep 17 00:00:00 2001 From: Luke Date: Wed, 29 Jan 2025 07:19:13 +1000 Subject: [PATCH 13/35] Update Dockerfile Co-authored-by: Arthri <41360489+Arthri@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4640b168..d1dcd97e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,7 +30,7 @@ RUN \ dotnet publish -o output/ -r "${ARCH}" -v m -f net9.0 -c Release -p:PublishSingleFile=true --self-contained false # Runtime image -FROM --platform=${TARGETPLATFORM} mcr.microsoft.com/dotnet/runtime:6.0 AS runner +FROM --platform=${TARGETPLATFORM} mcr.microsoft.com/dotnet/runtime:9.0 AS runner WORKDIR /server COPY --from=builder /TShock/TShockLauncher/output ./ From 17e37dd8bd39194d77d794be39bcc7a3fd28f2e2 Mon Sep 17 00:00:00 2001 From: Luke Date: Sun, 2 Feb 2025 16:36:28 +1000 Subject: [PATCH 14/35] Utilise conditional docker base image windows is no longer part of the 9.0 manifest --- Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d1dcd97e..f489d46c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,7 +30,11 @@ RUN \ dotnet publish -o output/ -r "${ARCH}" -v m -f net9.0 -c Release -p:PublishSingleFile=true --self-contained false # Runtime image -FROM --platform=${TARGETPLATFORM} mcr.microsoft.com/dotnet/runtime:9.0 AS runner +FROM mcr.microsoft.com/dotnet/runtime:9.0 AS linux_base +FROM mcr.microsoft.com/dotnet/runtime:9.0-nanoserver-ltsc2022 AS windows_base + +FROM ${TARGETOS}_base AS final + WORKDIR /server COPY --from=builder /TShock/TShockLauncher/output ./ From 57a3173a4d577cf50c6fe9577188ad6ae2a0be52 Mon Sep 17 00:00:00 2001 From: Cai <13110818005@qq.com> Date: Fri, 4 Apr 2025 01:58:55 +0800 Subject: [PATCH 15/35] fix(SpawnHandler): player "break" after respawning --- TShockAPI/GetDataHandlers.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TShockAPI/GetDataHandlers.cs b/TShockAPI/GetDataHandlers.cs index a4969cf0..6ed0f8de 100644 --- a/TShockAPI/GetDataHandlers.cs +++ b/TShockAPI/GetDataHandlers.cs @@ -2778,6 +2778,9 @@ namespace TShockAPI return false; } + // spawn the player before teleporting + NetMessage.SendData((int)PacketTypes.PlayerSpawn, -1, args.Player.Index, null, args.Player.Index, (int)PlayerSpawnContext.ReviveFromDeath); + // the player has not changed his spawnpoint yet, so we assert the server-saved spawnpoint // by teleporting the player instead of letting the game use the client's incorrect spawnpoint. TShock.Log.ConsoleDebug(GetString("GetDataHandlers / HandleSpawn force ssc teleport for {0} at ({1},{2})", args.Player.Name, args.TPlayer.SpawnX, args.TPlayer.SpawnY)); From 0021f9884d9eb664561607b9c5ff4484ebe33a11 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Mon, 21 Apr 2025 14:04:07 +0200 Subject: [PATCH 16/35] refactor(server/db): Move database connection logic to separate class Implements a DbBuilder class to streamline the creation of database connections for both SQLite and MySQL storage types. Enhances error handling for database setup and ensures that necessary directories are created dynamically based on configuration settings. This refactor improves code maintainability and readability, consolidating connection logic into a dedicated builder class. --- TShockAPI/DB/DbBuilder.cs | 89 +++++++++++++++++++++++++++++++++++++++ TShockAPI/TShock.cs | 34 ++------------- 2 files changed, 92 insertions(+), 31 deletions(-) create mode 100644 TShockAPI/DB/DbBuilder.cs diff --git a/TShockAPI/DB/DbBuilder.cs b/TShockAPI/DB/DbBuilder.cs new file mode 100644 index 00000000..d02f993b --- /dev/null +++ b/TShockAPI/DB/DbBuilder.cs @@ -0,0 +1,89 @@ +using System.Data; +using System.Diagnostics; +using System.IO; +using Microsoft.Data.Sqlite; +using MySql.Data.MySqlClient; +using TerrariaApi.Server; +using TShockAPI.Configuration; + +namespace TShockAPI.DB; + +/// +/// Provides logic to build a DB connection. +/// +public sealed class DbBuilder +{ + private readonly TShock _caller; + private readonly TShockConfig _config; + private readonly string _savePath; + + /// + /// Initializes a new instance of the class. + /// + /// The TShock instance calling this DbBuilder. + /// The TShock configuration, supplied by at init. + /// The savePath registered by TShock. See . + public DbBuilder(TShock caller, TShockConfig config, string savePath) + { + _caller = caller; + _config = config; + _savePath = savePath; + } + + /// + /// Builds a DB connection based on the provided configuration. + /// + /// The TShock configuration. + /// + /// Default settings will result in a local sqlite database file named "tshock.db" in the current directory to be used as server DB. + /// + public IDbConnection BuildDbConnection() + { + string dbType = _config.Settings.StorageType.ToLowerInvariant(); + + return dbType switch + { + "sqlite" => BuildSqliteConnection(), + "mysql" => BuildMySqlConnection(), + _ => throw new("Invalid storage type") + }; + } + + private SqliteConnection BuildSqliteConnection() + { + string dbFilePath = Path.Combine(_savePath, _config.Settings.SqliteDBPath); + + if (Path.GetDirectoryName(dbFilePath) is not { } dbDirPath) + { + throw new DirectoryNotFoundException($"The SQLite database path '{dbFilePath}' could not be found."); + } + + Directory.CreateDirectory(dbDirPath); + + return new($"Data Source={dbFilePath}"); + } + + private MySqlConnection BuildMySqlConnection() + { + try + { + string[] hostport = _config.Settings.MySqlHost.Split(':'); + + MySqlConnectionStringBuilder connStrBuilder = new() + { + Server = hostport[0], + Port = hostport.Length > 1 ? uint.Parse(hostport[1]) : 3306, + Database = _config.Settings.MySqlDbName, + UserID = _config.Settings.MySqlUsername, + Password = _config.Settings.MySqlPassword + }; + + return new(connStrBuilder.ToString()); + } + catch (MySqlException e) + { + ServerApi.LogWriter.PluginWriteLine(_caller, e.ToString(), TraceLevel.Error); + throw new("MySql not setup correctly", e); + } + } +} diff --git a/TShockAPI/TShock.cs b/TShockAPI/TShock.cs index bd2a113e..0e453b40 100644 --- a/TShockAPI/TShock.cs +++ b/TShockAPI/TShock.cs @@ -314,37 +314,9 @@ namespace TShockAPI // Further exceptions are written to TShock's log from now on. try { - if (Config.Settings.StorageType.ToLower() == "sqlite") - { - string sql = Path.Combine(SavePath, Config.Settings.SqliteDBPath); - Directory.CreateDirectory(Path.GetDirectoryName(sql)); - DB = new Microsoft.Data.Sqlite.SqliteConnection(string.Format("Data Source={0}", sql)); - } - else if (Config.Settings.StorageType.ToLower() == "mysql") - { - try - { - var hostport = Config.Settings.MySqlHost.Split(':'); - DB = new MySqlConnection(); - DB.ConnectionString = - String.Format("Server={0}; Port={1}; Database={2}; Uid={3}; Pwd={4};", - hostport[0], - hostport.Length > 1 ? hostport[1] : "3306", - Config.Settings.MySqlDbName, - Config.Settings.MySqlUsername, - Config.Settings.MySqlPassword - ); - } - catch (MySqlException ex) - { - ServerApi.LogWriter.PluginWriteLine(this, ex.ToString(), TraceLevel.Error); - throw new Exception("MySql not setup correctly"); - } - } - else - { - throw new Exception("Invalid storage type"); - } + // Build database + DbBuilder dbBuilder = new(this, Config, SavePath); + DB = dbBuilder.BuildDbConnection(); if (Config.Settings.UseSqlLogs) Log = new SqlLog(DB, logFilename, LogClear); From 8a75b1fdc52b7829975e4231f52a93c3b9173c6f Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Mon, 28 Apr 2025 14:05:29 +0200 Subject: [PATCH 17/35] refactor(db): Move DbQueryBuilders to separate namespace Removes obsolete query builder interface and implementations. Introduces a new namespace for query-related classes to enhance organization. Updates various database managers to use the new query utilities, promoting code reuse and maintainability. Enhances query functionality, ensuring better consistency across the codebase. --- TShockAPI/DB/BanManager.cs | 1 + TShockAPI/DB/CharacterManager.cs | 1 + TShockAPI/DB/GroupManager.cs | 1 + TShockAPI/DB/IQueryBuilder.cs | 401 -------------------- TShockAPI/DB/ItemManager.cs | 1 + TShockAPI/DB/ProjectileManager.cs | 1 + TShockAPI/DB/Queries/GenericQueryCreator.cs | 136 +++++++ TShockAPI/DB/Queries/IQueryBuilder.cs | 92 +++++ TShockAPI/DB/Queries/MysqlQueryCreator.cs | 88 +++++ TShockAPI/DB/Queries/SqliteQueryCreator.cs | 103 +++++ TShockAPI/DB/RegionManager.cs | 1 + TShockAPI/DB/RememberedPosManager.cs | 1 + TShockAPI/DB/ResearchDatastore.cs | 1 + TShockAPI/DB/SqlTable.cs | 1 + TShockAPI/DB/SqlValue.cs | 1 + TShockAPI/DB/TileManager.cs | 1 + TShockAPI/DB/UserManager.cs | 1 + TShockAPI/DB/WarpsManager.cs | 1 + TShockAPI/Extensions/StringExt.cs | 4 +- 19 files changed, 434 insertions(+), 403 deletions(-) delete mode 100644 TShockAPI/DB/IQueryBuilder.cs create mode 100644 TShockAPI/DB/Queries/GenericQueryCreator.cs create mode 100644 TShockAPI/DB/Queries/IQueryBuilder.cs create mode 100644 TShockAPI/DB/Queries/MysqlQueryCreator.cs create mode 100644 TShockAPI/DB/Queries/SqliteQueryCreator.cs diff --git a/TShockAPI/DB/BanManager.cs b/TShockAPI/DB/BanManager.cs index 3e93a3b7..be05b45c 100644 --- a/TShockAPI/DB/BanManager.cs +++ b/TShockAPI/DB/BanManager.cs @@ -22,6 +22,7 @@ using System.Collections.Generic; using System.Data; using MySql.Data.MySqlClient; using System.Collections.ObjectModel; +using TShockAPI.DB.Queries; namespace TShockAPI.DB { diff --git a/TShockAPI/DB/CharacterManager.cs b/TShockAPI/DB/CharacterManager.cs index c0551b65..be09da1d 100644 --- a/TShockAPI/DB/CharacterManager.cs +++ b/TShockAPI/DB/CharacterManager.cs @@ -23,6 +23,7 @@ using System.Linq; using System.Text; using MySql.Data.MySqlClient; using Terraria; +using TShockAPI.DB.Queries; namespace TShockAPI.DB { diff --git a/TShockAPI/DB/GroupManager.cs b/TShockAPI/DB/GroupManager.cs index 4a9f8afb..5d32419e 100644 --- a/TShockAPI/DB/GroupManager.cs +++ b/TShockAPI/DB/GroupManager.cs @@ -23,6 +23,7 @@ using System.Data; using System.Diagnostics; using System.Linq; using MySql.Data.MySqlClient; +using TShockAPI.DB.Queries; namespace TShockAPI.DB { diff --git a/TShockAPI/DB/IQueryBuilder.cs b/TShockAPI/DB/IQueryBuilder.cs deleted file mode 100644 index c2be131d..00000000 --- a/TShockAPI/DB/IQueryBuilder.cs +++ /dev/null @@ -1,401 +0,0 @@ -/* -TShock, a server mod for Terraria -Copyright (C) 2011-2019 Pryaxis & TShock Contributors - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -using MySql.Data.MySqlClient; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; -using TShockAPI.Extensions; - -namespace TShockAPI.DB -{ - /// - /// Interface for various SQL related utilities. - /// - public interface IQueryBuilder - { - /// - /// Creates a table from a SqlTable object. - /// - /// The SqlTable to create the table from - /// The sql query for the table creation. - string CreateTable(SqlTable table); - - /// - /// Alter a table from source to destination - /// - /// Must have name and column names. Column types are not required - /// Must have column names and column types. - /// The SQL Query - string AlterTable(SqlTable from, SqlTable to); - - /// - /// Converts the MySqlDbType enum to it's string representation. - /// - /// The MySqlDbType type - /// The length of the datatype - /// The string representation - string DbTypeToString(MySqlDbType type, int? length); - - /// - /// A UPDATE Query - /// - /// The table to update - /// The values to change - /// - /// The SQL query - string UpdateValue(string table, List values, List wheres); - - /// - /// A INSERT query - /// - /// The table to insert to - /// - /// The SQL Query - string InsertValues(string table, List values); - - /// - /// A SELECT query to get all columns - /// - /// The table to select from - /// - /// The SQL query - string ReadColumn(string table, List wheres); - - /// - /// Deletes row(s). - /// - /// The table to delete the row from - /// - /// The SQL query - string DeleteRow(string table, List wheres); - - /// - /// Renames the given table. - /// - /// Old name of the table - /// New name of the table - /// The sql query for renaming the table. - string RenameTable(string from, string to); - } - - /// - /// Query Creator for Sqlite - /// - public class SqliteQueryCreator : GenericQueryCreator, IQueryBuilder - { - /// - /// Creates a table from a SqlTable object. - /// - /// The SqlTable to create the table from - /// The sql query for the table creation. - public override string CreateTable(SqlTable table) - { - ValidateSqlColumnType(table.Columns); - var columns = - table.Columns.Select( - c => - "'{0}' {1} {2} {3} {4} {5}".SFormat(c.Name, - DbTypeToString(c.Type, c.Length), - c.Primary ? "PRIMARY KEY" : "", - c.AutoIncrement ? "AUTOINCREMENT" : "", - c.NotNull ? "NOT NULL" : "", - c.DefaultCurrentTimestamp ? "DEFAULT CURRENT_TIMESTAMP" : "")); - var uniques = table.Columns.Where(c => c.Unique).Select(c => c.Name); - return "CREATE TABLE {0} ({1} {2})".SFormat(EscapeTableName(table.Name), - string.Join(", ", columns), - uniques.Count() > 0 ? ", UNIQUE({0})".SFormat(string.Join(", ", uniques)) : ""); - } - - /// - /// Renames the given table. - /// - /// Old name of the table - /// New name of the table - /// The sql query for renaming the table. - public override string RenameTable(string from, string to) - { - return "ALTER TABLE {0} RENAME TO {1}".SFormat(from, to); - } - - private static readonly Dictionary TypesAsStrings = new Dictionary - { - { MySqlDbType.VarChar, "TEXT" }, - { MySqlDbType.String, "TEXT" }, - { MySqlDbType.Text, "TEXT" }, - { MySqlDbType.TinyText, "TEXT" }, - { MySqlDbType.MediumText, "TEXT" }, - { MySqlDbType.LongText, "TEXT" }, - { MySqlDbType.Float, "REAL" }, - { MySqlDbType.Double, "REAL" }, - { MySqlDbType.Int32, "INTEGER" }, - { MySqlDbType.Blob, "BLOB" }, - { MySqlDbType.Int64, "BIGINT"}, - { MySqlDbType.DateTime, "DATETIME"}, - }; - - /// - /// Converts the MySqlDbType enum to it's string representation. - /// - /// The MySqlDbType type - /// The length of the datatype - /// The string representation - public string DbTypeToString(MySqlDbType type, int? length) - { - string ret; - if (TypesAsStrings.TryGetValue(type, out ret)) - return ret; - throw new NotImplementedException(Enum.GetName(typeof(MySqlDbType), type)); - } - - /// - /// Escapes the table name - /// - /// The name of the table to be escaped - /// - protected override string EscapeTableName(string table) - { - return $"\'{table}\'"; - } - } - - /// - /// Query Creator for MySQL - /// - public class MysqlQueryCreator : GenericQueryCreator, IQueryBuilder - { - /// - /// Creates a table from a SqlTable object. - /// - /// The SqlTable to create the table from - /// The sql query for the table creation. - public override string CreateTable(SqlTable table) - { - ValidateSqlColumnType(table.Columns); - var columns = - table.Columns.Select( - c => - "`{0}` {1} {2} {3} {4} {5}".SFormat(c.Name, DbTypeToString(c.Type, c.Length), - c.Primary ? "PRIMARY KEY" : "", - c.AutoIncrement ? "AUTO_INCREMENT" : "", - c.NotNull ? "NOT NULL" : "", - c.DefaultCurrentTimestamp ? "DEFAULT CURRENT_TIMESTAMP" : "")); - var uniques = table.Columns.Where(c => c.Unique).Select(c => $"`{c.Name}`"); - return "CREATE TABLE {0} ({1} {2})".SFormat(EscapeTableName(table.Name), string.Join(", ", columns), - uniques.Count() > 0 - ? ", UNIQUE({0})".SFormat(string.Join(", ", uniques)) - : ""); - } - - /// - /// Renames the given table. - /// - /// Old name of the table - /// New name of the table - /// The sql query for renaming the table. - public override string RenameTable(string from, string to) - { - return "RENAME TABLE {0} TO {1}".SFormat(from, to); - } - - private static readonly Dictionary TypesAsStrings = new Dictionary - { - { MySqlDbType.VarChar, "VARCHAR" }, - { MySqlDbType.String, "CHAR" }, - { MySqlDbType.Text, "TEXT" }, - { MySqlDbType.TinyText, "TINYTEXT" }, - { MySqlDbType.MediumText, "MEDIUMTEXT" }, - { MySqlDbType.LongText, "LONGTEXT" }, - { MySqlDbType.Float, "FLOAT" }, - { MySqlDbType.Double, "DOUBLE" }, - { MySqlDbType.Int32, "INT" }, - { MySqlDbType.Int64, "BIGINT"}, - { MySqlDbType.DateTime, "DATETIME"}, - }; - - /// - /// Converts the MySqlDbType enum to it's string representation. - /// - /// The MySqlDbType type - /// The length of the datatype - /// The string representation - public string DbTypeToString(MySqlDbType type, int? length) - { - string ret; - if (TypesAsStrings.TryGetValue(type, out ret)) - return ret + (length != null ? "({0})".SFormat((int)length) : ""); - throw new NotImplementedException(Enum.GetName(typeof(MySqlDbType), type)); - } - - /// - /// Escapes the table name - /// - /// The name of the table to be escaped - /// - protected override string EscapeTableName(string table) - { - return table.SFormat("`{0}`", table); - } - } - - /// - /// A Generic Query Creator (abstract) - /// - public abstract class GenericQueryCreator - { - protected static Random rand = new Random(); - - /// - /// Escapes the table name - /// - /// The name of the table to be escaped - /// - protected abstract string EscapeTableName(string table); - - /// - /// Creates a table from a SqlTable object. - /// - /// The SqlTable to create the table from - /// The sql query for the table creation. - public abstract string CreateTable(SqlTable table); - - /// - /// Renames the given table. - /// - /// Old name of the table - /// New name of the table - /// The sql query for renaming the table. - public abstract string RenameTable(string from, string to); - - /// - /// Alter a table from source to destination - /// - /// Must have name and column names. Column types are not required - /// Must have column names and column types. - /// The SQL Query - public string AlterTable(SqlTable from, SqlTable to) - { - var rstr = rand.NextString(20); - var escapedTable = EscapeTableName(from.Name); - var tmpTable = EscapeTableName("{0}_{1}".SFormat(rstr, from.Name)); - var alter = RenameTable(escapedTable, tmpTable); - var create = CreateTable(to); - // combine all columns in the 'from' variable excluding ones that aren't in the 'to' variable. - // exclude the ones that aren't in 'to' variable because if the column is deleted, why try to import the data? - var columns = string.Join(", ", from.Columns.Where(c => to.Columns.Any(c2 => c2.Name == c.Name)).Select(c => $"`{c.Name}`")); - var insert = "INSERT INTO {0} ({1}) SELECT {1} FROM {2}".SFormat(escapedTable, columns, tmpTable); - var drop = "DROP TABLE {0}".SFormat(tmpTable); - return "{0}; {1}; {2}; {3};".SFormat(alter, create, insert, drop); - } - - /// - /// Check for errors in the columns. - /// - /// - /// - public void ValidateSqlColumnType(List columns) - { - columns.ForEach(x => - { - if (x.DefaultCurrentTimestamp && x.Type != MySqlDbType.DateTime) - { - throw new SqlColumnException(GetString("Can't set to true SqlColumn.DefaultCurrentTimestamp when the MySqlDbType is not DateTime")); - } - }); - } - - /// - /// Deletes row(s). - /// - /// The table to delete the row from - /// - /// The SQL query - public string DeleteRow(string table, List wheres) - { - return "DELETE FROM {0} {1}".SFormat(EscapeTableName(table), BuildWhere(wheres)); - } - - /// - /// A UPDATE Query - /// - /// The table to update - /// The values to change - /// - /// The SQL query - public string UpdateValue(string table, List values, List wheres) - { - if (0 == values.Count) - throw new ArgumentException(GetString("No values supplied")); - - return "UPDATE {0} SET {1} {2}".SFormat(EscapeTableName(table), string.Join(", ", values.Select(v => v.Name + " = " + v.Value)), BuildWhere(wheres)); - } - - /// - /// A SELECT query to get all columns - /// - /// The table to select from - /// - /// The SQL query - public string ReadColumn(string table, List wheres) - { - return "SELECT * FROM {0} {1}".SFormat(EscapeTableName(table), BuildWhere(wheres)); - } - - /// - /// A INSERT query - /// - /// The table to insert to - /// - /// The SQL Query - public string InsertValues(string table, List values) - { - var sbnames = new StringBuilder(); - var sbvalues = new StringBuilder(); - int count = 0; - foreach (SqlValue value in values) - { - sbnames.Append(value.Name); - sbvalues.Append(value.Value.ToString()); - - if (count != values.Count - 1) - { - sbnames.Append(", "); - sbvalues.Append(", "); - } - count++; - } - - return "INSERT INTO {0} ({1}) VALUES ({2})".SFormat(EscapeTableName(table), sbnames, sbvalues); - } - - /// - /// Builds the SQL WHERE clause - /// - /// - /// - protected static string BuildWhere(List wheres) - { - if (0 == wheres.Count) - return string.Empty; - - return "WHERE {0}".SFormat(string.Join(", ", wheres.Select(v => $"{v.Name}" + " = " + v.Value))); - } - } -} diff --git a/TShockAPI/DB/ItemManager.cs b/TShockAPI/DB/ItemManager.cs index 012132dc..e707406c 100644 --- a/TShockAPI/DB/ItemManager.cs +++ b/TShockAPI/DB/ItemManager.cs @@ -21,6 +21,7 @@ using System.Collections.Generic; using System.Data; using System.Linq; using MySql.Data.MySqlClient; +using TShockAPI.DB.Queries; using TShockAPI.Hooks; namespace TShockAPI.DB diff --git a/TShockAPI/DB/ProjectileManager.cs b/TShockAPI/DB/ProjectileManager.cs index 0e6cabe0..f07280b5 100644 --- a/TShockAPI/DB/ProjectileManager.cs +++ b/TShockAPI/DB/ProjectileManager.cs @@ -21,6 +21,7 @@ using System.Collections.Generic; using System.Data; using System.Linq; using MySql.Data.MySqlClient; +using TShockAPI.DB.Queries; using TShockAPI.Hooks; namespace TShockAPI.DB diff --git a/TShockAPI/DB/Queries/GenericQueryCreator.cs b/TShockAPI/DB/Queries/GenericQueryCreator.cs new file mode 100644 index 00000000..f9fc2038 --- /dev/null +++ b/TShockAPI/DB/Queries/GenericQueryCreator.cs @@ -0,0 +1,136 @@ +/* +TShock, a server mod for Terraria +Copyright (C) 2011-2019 Pryaxis & TShock Contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using MySql.Data.MySqlClient; +using TShockAPI.Extensions; + +namespace TShockAPI.DB.Queries; + +/// +/// A Generic Query Creator (abstract) +/// +public abstract class GenericQueryCreator : IQueryBuilder +{ + protected static Random rand = new Random(); + + /// + /// Escapes the table name + /// + /// The name of the table to be escaped + /// + protected abstract string EscapeTableName(string table); + + /// + public abstract string CreateTable(SqlTable table); + + /// + public abstract string RenameTable(string from, string to); + + /// + public string AlterTable(SqlTable from, SqlTable to) + { + var rstr = rand.NextString(20); + var escapedTable = EscapeTableName(from.Name); + var tmpTable = EscapeTableName("{0}_{1}".SFormat(rstr, from.Name)); + var alter = RenameTable(escapedTable, tmpTable); + var create = CreateTable(to); + // combine all columns in the 'from' variable excluding ones that aren't in the 'to' variable. + // exclude the ones that aren't in 'to' variable because if the column is deleted, why try to import the data? + var columns = string.Join(", ", from.Columns.Where(c => to.Columns.Any(c2 => c2.Name == c.Name)).Select(c => $"`{c.Name}`")); + var insert = "INSERT INTO {0} ({1}) SELECT {1} FROM {2}".SFormat(escapedTable, columns, tmpTable); + var drop = "DROP TABLE {0}".SFormat(tmpTable); + return "{0}; {1}; {2}; {3};".SFormat(alter, create, insert, drop); + } + + /// + public abstract string DbTypeToString(MySqlDbType type, int? length); + + /// + /// Check for errors in the columns. + /// + /// + /// + protected static void ValidateSqlColumnType(List columns) + { + columns.ForEach(x => + { + if (x.DefaultCurrentTimestamp && x.Type != MySqlDbType.DateTime) + { + throw new SqlColumnException(GetString("Can't set to true SqlColumn.DefaultCurrentTimestamp when the MySqlDbType is not DateTime")); + } + }); + } + + + /// + public string DeleteRow(string table, List wheres) + { + return "DELETE FROM {0} {1}".SFormat(EscapeTableName(table), BuildWhere(wheres)); + } + + /// + public string UpdateValue(string table, List values, List wheres) + { + if (0 == values.Count) + throw new ArgumentException(GetString("No values supplied")); + + return "UPDATE {0} SET {1} {2}".SFormat(EscapeTableName(table), string.Join(", ", values.Select(v => v.Name + " = " + v.Value)), BuildWhere(wheres)); + } + + + /// + public string ReadColumn(string table, List wheres) + { + return "SELECT * FROM {0} {1}".SFormat(EscapeTableName(table), BuildWhere(wheres)); + } + + + public string InsertValues(string table, List values) + { + var sbnames = new StringBuilder(); + var sbvalues = new StringBuilder(); + int count = 0; + foreach (SqlValue value in values) + { + sbnames.Append(value.Name); + sbvalues.Append(value.Value.ToString()); + + if (count != values.Count - 1) + { + sbnames.Append(", "); + sbvalues.Append(", "); + } + count++; + } + + return "INSERT INTO {0} ({1}) VALUES ({2})".SFormat(EscapeTableName(table), sbnames, sbvalues); + } + + /// + /// Builds the SQL WHERE clause + /// + /// + /// + protected static string BuildWhere(List wheres) => wheres.Count > 0 + ? string.Empty + : "WHERE {0}".SFormat(string.Join(", ", wheres.Select(v => $"{v.Name} = {v.Value}"))); +} diff --git a/TShockAPI/DB/Queries/IQueryBuilder.cs b/TShockAPI/DB/Queries/IQueryBuilder.cs new file mode 100644 index 00000000..dc61370f --- /dev/null +++ b/TShockAPI/DB/Queries/IQueryBuilder.cs @@ -0,0 +1,92 @@ +/* +TShock, a server mod for Terraria +Copyright (C) 2011-2019 Pryaxis & TShock Contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +using System.Collections.Generic; +using MySql.Data.MySqlClient; + +namespace TShockAPI.DB.Queries; + +/// +/// Interface for various SQL related utilities. +/// +public interface IQueryBuilder +{ + /// + /// Creates a table from a SqlTable object. + /// + /// The SqlTable to create the table from + /// The sql query for the table creation. + string CreateTable(SqlTable table); + + /// + /// Alter a table from source to destination + /// + /// Must have name and column names. Column types are not required + /// Must have column names and column types. + /// The SQL Query + string AlterTable(SqlTable from, SqlTable to); + + /// + /// Converts the MySqlDbType enum to it's string representation. + /// + /// The MySqlDbType type + /// The length of the datatype + /// The string representation + string DbTypeToString(MySqlDbType type, int? length); + + /// + /// A UPDATE Query + /// + /// The table to update + /// The values to change + /// + /// The SQL query + string UpdateValue(string table, List values, List wheres); + + /// + /// A INSERT query + /// + /// The table to insert to + /// + /// The SQL Query + string InsertValues(string table, List values); + + /// + /// A SELECT query to get all columns + /// + /// The table to select from + /// + /// The SQL query + string ReadColumn(string table, List wheres); + + /// + /// Deletes row(s). + /// + /// The table to delete the row from + /// + /// The SQL query + string DeleteRow(string table, List wheres); + + /// + /// Renames the given table. + /// + /// Old name of the table + /// New name of the table + /// The sql query for renaming the table. + string RenameTable(string from, string to); +} diff --git a/TShockAPI/DB/Queries/MysqlQueryCreator.cs b/TShockAPI/DB/Queries/MysqlQueryCreator.cs new file mode 100644 index 00000000..1c0ec24f --- /dev/null +++ b/TShockAPI/DB/Queries/MysqlQueryCreator.cs @@ -0,0 +1,88 @@ +/* +TShock, a server mod for Terraria +Copyright (C) 2011-2019 Pryaxis & TShock Contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using MySql.Data.MySqlClient; + +namespace TShockAPI.DB.Queries; + +/// +/// Query Creator for MySQL +/// +public class MysqlQueryCreator : GenericQueryCreator, IQueryBuilder +{ + /// + /// Creates a table from a SqlTable object. + /// + /// The SqlTable to create the table from + /// The sql query for the table creation. + public override string CreateTable(SqlTable table) + { + ValidateSqlColumnType(table.Columns); + + var columns = + table.Columns.Select( + c => + "`{0}` {1} {2} {3} {4} {5}".SFormat(c.Name, DbTypeToString(c.Type, c.Length), + c.Primary ? "PRIMARY KEY" : "", + c.AutoIncrement ? "AUTO_INCREMENT" : "", + c.NotNull ? "NOT NULL" : "", + c.DefaultCurrentTimestamp ? "DEFAULT CURRENT_TIMESTAMP" : "")); + + var uniques = table.Columns.Where(c => c.Unique).Select(c => $"`{c.Name}`"); + return "CREATE TABLE {0} ({1} {2})".SFormat(EscapeTableName(table.Name), string.Join(", ", columns), + uniques.Any() + ? ", UNIQUE({0})".SFormat(string.Join(", ", uniques)) + : ""); + } + + + /// + public override string RenameTable(string from, string to) => /*lang=mysql*/"RENAME TABLE {0} TO {1}".SFormat(from, to); + + private static readonly Dictionary TypesAsStrings = new() + { + { MySqlDbType.VarChar, "VARCHAR" }, + { MySqlDbType.String, "CHAR" }, + { MySqlDbType.Text, "TEXT" }, + { MySqlDbType.TinyText, "TINYTEXT" }, + { MySqlDbType.MediumText, "MEDIUMTEXT" }, + { MySqlDbType.LongText, "LONGTEXT" }, + { MySqlDbType.Float, "FLOAT" }, + { MySqlDbType.Double, "DOUBLE" }, + { MySqlDbType.Int32, "INT" }, + { MySqlDbType.Int64, "BIGINT"}, + { MySqlDbType.DateTime, "DATETIME"}, + }; + + /// + public override string DbTypeToString(MySqlDbType type, int? length) + { + if (TypesAsStrings.TryGetValue(type, out string ret)) + { + return ret + (length is not null ? "({0})".SFormat((int)length) : ""); + } + + throw new NotImplementedException(Enum.GetName(typeof(MySqlDbType), type)); + } + + /// + protected override string EscapeTableName(string table) => table.SFormat("`{0}`", table); +} diff --git a/TShockAPI/DB/Queries/SqliteQueryCreator.cs b/TShockAPI/DB/Queries/SqliteQueryCreator.cs new file mode 100644 index 00000000..229ba49b --- /dev/null +++ b/TShockAPI/DB/Queries/SqliteQueryCreator.cs @@ -0,0 +1,103 @@ +/* +TShock, a server mod for Terraria +Copyright (C) 2011-2019 Pryaxis & TShock Contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using MySql.Data.MySqlClient; + +namespace TShockAPI.DB.Queries; + +/// +/// Query Creator for Sqlite +/// +public class SqliteQueryCreator : GenericQueryCreator, IQueryBuilder +{ + /// + /// Creates a table from a SqlTable object. + /// + /// The SqlTable to create the table from + /// The sql query for the table creation. + public override string CreateTable(SqlTable table) + { + ValidateSqlColumnType(table.Columns); + var columns = + table.Columns.Select( + c => + "'{0}' {1} {2} {3} {4} {5}".SFormat(c.Name, + DbTypeToString(c.Type, c.Length), + c.Primary ? "PRIMARY KEY" : "", + c.AutoIncrement ? "AUTOINCREMENT" : "", + c.NotNull ? "NOT NULL" : "", + c.DefaultCurrentTimestamp ? "DEFAULT CURRENT_TIMESTAMP" : "")); + var uniques = table.Columns.Where(c => c.Unique).Select(c => c.Name); + return "CREATE TABLE {0} ({1} {2})".SFormat(EscapeTableName(table.Name), + string.Join(", ", columns), + uniques.Count() > 0 ? ", UNIQUE({0})".SFormat(string.Join(", ", uniques)) : ""); + } + + /// + /// Renames the given table. + /// + /// Old name of the table + /// New name of the table + /// The sql query for renaming the table. + public override string RenameTable(string from, string to) + { + return "ALTER TABLE {0} RENAME TO {1}".SFormat(from, to); + } + + private static readonly Dictionary TypesAsStrings = new Dictionary + { + { MySqlDbType.VarChar, "TEXT" }, + { MySqlDbType.String, "TEXT" }, + { MySqlDbType.Text, "TEXT" }, + { MySqlDbType.TinyText, "TEXT" }, + { MySqlDbType.MediumText, "TEXT" }, + { MySqlDbType.LongText, "TEXT" }, + { MySqlDbType.Float, "REAL" }, + { MySqlDbType.Double, "REAL" }, + { MySqlDbType.Int32, "INTEGER" }, + { MySqlDbType.Blob, "BLOB" }, + { MySqlDbType.Int64, "BIGINT"}, + { MySqlDbType.DateTime, "DATETIME"}, + }; + + /// + /// Converts the MySqlDbType enum to it's string representation. + /// + /// The MySqlDbType type + /// The length of the datatype + /// The string representation + public override string DbTypeToString(MySqlDbType type, int? length) + { + if (TypesAsStrings.TryGetValue(type, out string ret)) + { + return ret; + } + + throw new NotImplementedException(Enum.GetName(typeof(MySqlDbType), type)); + } + + /// + /// Escapes the table name + /// + /// The name of the table to be escaped + /// + protected override string EscapeTableName(string table) => $"\'{table}\'"; +} diff --git a/TShockAPI/DB/RegionManager.cs b/TShockAPI/DB/RegionManager.cs index 1a284ff3..0950f92f 100644 --- a/TShockAPI/DB/RegionManager.cs +++ b/TShockAPI/DB/RegionManager.cs @@ -23,6 +23,7 @@ using System.Linq; using MySql.Data.MySqlClient; using Terraria; using Microsoft.Xna.Framework; +using TShockAPI.DB.Queries; namespace TShockAPI.DB { diff --git a/TShockAPI/DB/RememberedPosManager.cs b/TShockAPI/DB/RememberedPosManager.cs index c49c5912..297877f7 100644 --- a/TShockAPI/DB/RememberedPosManager.cs +++ b/TShockAPI/DB/RememberedPosManager.cs @@ -21,6 +21,7 @@ using System.Data; using MySql.Data.MySqlClient; using Terraria; using Microsoft.Xna.Framework; +using TShockAPI.DB.Queries; namespace TShockAPI.DB { diff --git a/TShockAPI/DB/ResearchDatastore.cs b/TShockAPI/DB/ResearchDatastore.cs index 0d2429d7..cb51ab18 100644 --- a/TShockAPI/DB/ResearchDatastore.cs +++ b/TShockAPI/DB/ResearchDatastore.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading.Tasks; using Terraria; using Terraria.ID; +using TShockAPI.DB.Queries; namespace TShockAPI.DB { diff --git a/TShockAPI/DB/SqlTable.cs b/TShockAPI/DB/SqlTable.cs index 1f0c4593..299ac851 100644 --- a/TShockAPI/DB/SqlTable.cs +++ b/TShockAPI/DB/SqlTable.cs @@ -21,6 +21,7 @@ using System.Collections.Generic; using System.Data; using System.Linq; using MySql.Data.MySqlClient; +using TShockAPI.DB.Queries; namespace TShockAPI.DB { diff --git a/TShockAPI/DB/SqlValue.cs b/TShockAPI/DB/SqlValue.cs index aa70e2d6..3df5d995 100644 --- a/TShockAPI/DB/SqlValue.cs +++ b/TShockAPI/DB/SqlValue.cs @@ -18,6 +18,7 @@ along with this program. If not, see . using System.Collections.Generic; using System.Data; +using TShockAPI.DB.Queries; namespace TShockAPI.DB { diff --git a/TShockAPI/DB/TileManager.cs b/TShockAPI/DB/TileManager.cs index bd918e73..61903186 100644 --- a/TShockAPI/DB/TileManager.cs +++ b/TShockAPI/DB/TileManager.cs @@ -21,6 +21,7 @@ using System.Collections.Generic; using System.Data; using System.Linq; using MySql.Data.MySqlClient; +using TShockAPI.DB.Queries; using TShockAPI.Hooks; namespace TShockAPI.DB diff --git a/TShockAPI/DB/UserManager.cs b/TShockAPI/DB/UserManager.cs index 6fc50d98..a3e050f7 100644 --- a/TShockAPI/DB/UserManager.cs +++ b/TShockAPI/DB/UserManager.cs @@ -25,6 +25,7 @@ using MySql.Data.MySqlClient; using System.Text.RegularExpressions; using BCrypt.Net; using System.Security.Cryptography; +using TShockAPI.DB.Queries; using TShockAPI.Hooks; namespace TShockAPI.DB diff --git a/TShockAPI/DB/WarpsManager.cs b/TShockAPI/DB/WarpsManager.cs index bd13ce4d..f3287441 100644 --- a/TShockAPI/DB/WarpsManager.cs +++ b/TShockAPI/DB/WarpsManager.cs @@ -24,6 +24,7 @@ using System.Linq; using MySql.Data.MySqlClient; using Terraria; using Microsoft.Xna.Framework; +using TShockAPI.DB.Queries; namespace TShockAPI.DB { diff --git a/TShockAPI/Extensions/StringExt.cs b/TShockAPI/Extensions/StringExt.cs index 56f46215..8f886031 100644 --- a/TShockAPI/Extensions/StringExt.cs +++ b/TShockAPI/Extensions/StringExt.cs @@ -24,9 +24,9 @@ namespace TShockAPI public static class StringExt { //Can't name it Format :( - public static String SFormat(this String str, params object[] args) + public static string SFormat(this string str, params object[] args) { - return String.Format(str, args); + return string.Format(str, args); } /// From f28f1bf536afc19bfe53e201f613ae2938533946 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Mon, 28 Apr 2025 14:05:51 +0200 Subject: [PATCH 18/35] feat(db): Add PostgreSQL query creator implementation Implements a query creator for PostgreSQL, allowing for proper database type mapping and table creation functionalities. Enhances the library's database support by incorporating PostgreSQL-specific features such as SERIAL/BIGSERIAL for auto-increment columns and improved escape handling for table names. Improves the extensibility and compatibility of the database framework with different SQL databases. --- TShockAPI/DB/Queries/PostgresQueryCreator.cs | 89 ++++++++++++++++++++ TShockAPI/SqlLog.cs | 1 + TShockAPI/TShockAPI.csproj | 1 + 3 files changed, 91 insertions(+) create mode 100644 TShockAPI/DB/Queries/PostgresQueryCreator.cs diff --git a/TShockAPI/DB/Queries/PostgresQueryCreator.cs b/TShockAPI/DB/Queries/PostgresQueryCreator.cs new file mode 100644 index 00000000..779e38b0 --- /dev/null +++ b/TShockAPI/DB/Queries/PostgresQueryCreator.cs @@ -0,0 +1,89 @@ +/* +TShock, a server mod for Terraria +Copyright (C) 2011-2019 Pryaxis & TShock Contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using MySql.Data.MySqlClient; + +namespace TShockAPI.DB.Queries; + +/// +/// Query Creator for PostgreSQL +/// +public class PostgresQueryCreator : GenericQueryCreator +{ + /// + public override string DbTypeToString(MySqlDbType type, int? length) => type switch + { + MySqlDbType.VarChar when length is not null => "VARCHAR({0})".SFormat(length), + MySqlDbType.String when length is not null => "CHAR({0})".SFormat(length), + MySqlDbType.Text => "TEXT", + MySqlDbType.TinyText => "TEXT", + MySqlDbType.MediumText => "TEXT", + MySqlDbType.LongText => "TEXT", + MySqlDbType.Float => "REAL", + MySqlDbType.Double => "DOUBLE PRECISION", + MySqlDbType.Int32 => "INT", + MySqlDbType.Int64 => "BIGINT", + MySqlDbType.DateTime => "TIMESTAMP", + + _ => throw new NotImplementedException(Enum.GetName(typeof(MySqlDbType), type)) + }; + + /// + protected override string EscapeTableName(string table) => table.SFormat("\"{0}\"", table); + + /// + public override string CreateTable(SqlTable table) + { + ValidateSqlColumnType(table.Columns); + + IEnumerable columns = table.Columns.Select(c => + { + // Handle PostgreSQL-specific auto-increment using SERIAL/BIGSERIAL + string dataType; + + if (c.AutoIncrement) + { + dataType = c.Type is MySqlDbType.Int32 ? "SERIAL" : "BIGSERIAL"; + } + else + { + dataType = DbTypeToString(c.Type, c.Length); + } + + return "\"{0}\" {1} {2} {3} {4}".SFormat(c.Name, + dataType, + c.Primary ? "PRIMARY KEY" : "", + c.NotNull && !c.AutoIncrement ? "NOT NULL" : "", // SERIAL implies NOT NULL + c.DefaultCurrentTimestamp ? "DEFAULT CURRENT_TIMESTAMP" : ""); + }); + + string[] uniques = table.Columns + .Where(c => c.Unique).Select(c => $"\"{c.Name}\"") + .ToArray(); // No re-enumeration + + return "CREATE TABLE {0} ({1} {2})".SFormat(EscapeTableName(table.Name), + string.Join(", ", columns), + uniques.Any() ? ", UNIQUE({0})".SFormat(string.Join(", ", uniques)) : ""); + } + + /// + public override string RenameTable(string from, string to) => /*lang=postgresql*/"ALTER TABLE {0} RENAME TO {1}".SFormat(from, to); +} diff --git a/TShockAPI/SqlLog.cs b/TShockAPI/SqlLog.cs index 3e79aaab..2e5a55e3 100644 --- a/TShockAPI/SqlLog.cs +++ b/TShockAPI/SqlLog.cs @@ -24,6 +24,7 @@ using System.Globalization; using System.Linq; using MySql.Data.MySqlClient; using TShockAPI.DB; +using TShockAPI.DB.Queries; namespace TShockAPI { diff --git a/TShockAPI/TShockAPI.csproj b/TShockAPI/TShockAPI.csproj index a01bb022..a152bd03 100644 --- a/TShockAPI/TShockAPI.csproj +++ b/TShockAPI/TShockAPI.csproj @@ -36,6 +36,7 @@ + From 27fde1f9ac4a9f145cc50514c470fe27de75885e Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Mon, 28 Apr 2025 14:17:59 +0200 Subject: [PATCH 19/35] feat(db): Add Postgres to SQL connection types + Refactor matching logic Implements pattern matching for easier identification of database types. Adds support for Postgres alongside existing Sqlite and MySQL types, enhancing flexibility for database connections. Updates enum to include Postgres type. --- TShockAPI/Extensions/DbExt.cs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/TShockAPI/Extensions/DbExt.cs b/TShockAPI/Extensions/DbExt.cs index b5a2c618..c6aa0ab9 100644 --- a/TShockAPI/Extensions/DbExt.cs +++ b/TShockAPI/Extensions/DbExt.cs @@ -20,6 +20,9 @@ using System; using System.Collections.Generic; using System.Data; using System.Diagnostics.CodeAnalysis; +using Microsoft.Data.Sqlite; +using MySql.Data.MySqlClient; +using Npgsql; namespace TShockAPI.DB { @@ -143,15 +146,13 @@ namespace TShockAPI.DB return clone; } - public static SqlType GetSqlType(this IDbConnection conn) + public static SqlType GetSqlType(this IDbConnection conn) => conn switch { - var name = conn.GetType().Name; - if (name == "SqliteConnection" || name == "SQLiteConnection") - return SqlType.Sqlite; - if (name == "MySqlConnection") - return SqlType.Mysql; - return SqlType.Unknown; - } + SqliteConnection => SqlType.Sqlite, + MySqlConnection => SqlType.Mysql, + NpgsqlConnection => SqlType.Postgres, + _ => SqlType.Unknown + }; private static readonly Dictionary> ReadFuncs = new Dictionary > @@ -267,7 +268,8 @@ namespace TShockAPI.DB { Unknown, Sqlite, - Mysql + Mysql, + Postgres } public class QueryResult : IDisposable From 084411f84761f336ed10a780f6a5c1b33d6c6529 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Mon, 28 Apr 2025 15:50:55 +0200 Subject: [PATCH 20/35] refactor(db): Update SqlQueryBuilder references + Various refactors Consolidates the creation of SQL query builders across multiple classes to ensure a unified approach for database operations. Replaces manual type checks and specific query creators with a generic method for better maintainability and to prevent errors. Improves code readability and reduces duplication, facilitating easier updates in the future. --- TShockAPI/DB/BanManager.cs | 135 +++++++++++----------- TShockAPI/DB/CharacterManager.cs | 114 +++++++++---------- TShockAPI/DB/GroupManager.cs | 161 +++++++++++++-------------- TShockAPI/DB/ItemManager.cs | 55 ++++----- TShockAPI/DB/ProjectileManager.cs | 52 +++------ TShockAPI/DB/RegionManager.cs | 133 +++++++++------------- TShockAPI/DB/RememberedPosManager.cs | 35 +++--- TShockAPI/DB/ResearchDatastore.cs | 22 ++-- TShockAPI/DB/SqlTable.cs | 47 +++++--- TShockAPI/DB/SqlValue.cs | 8 +- TShockAPI/DB/TileManager.cs | 26 ++--- TShockAPI/DB/UserManager.cs | 53 ++++----- TShockAPI/DB/WarpsManager.cs | 30 +++-- TShockAPI/Extensions/DbExt.cs | 9 ++ TShockAPI/SqlLog.cs | 7 +- 15 files changed, 399 insertions(+), 488 deletions(-) diff --git a/TShockAPI/DB/BanManager.cs b/TShockAPI/DB/BanManager.cs index be05b45c..c56e3d0d 100644 --- a/TShockAPI/DB/BanManager.cs +++ b/TShockAPI/DB/BanManager.cs @@ -69,10 +69,9 @@ namespace TShockAPI.DB new SqlColumn("Date", MySqlDbType.Int64), new SqlColumn("Expiration", MySqlDbType.Int64) ); - var creator = new SqlTableCreator(db, - db.GetSqlType() == SqlType.Sqlite - ? (IQueryBuilder)new SqliteQueryCreator() - : new MysqlQueryCreator()); + + var creator = new SqlTableCreator(db, db.GetSqlQueryBuilder()); + try { creator.EnsureTableStructure(table); @@ -106,15 +105,12 @@ namespace TShockAPI.DB /// public void TryConvertBans() { - int res; - if (database.GetSqlType() == SqlType.Mysql) + int res = database.GetSqlType() switch { - res = database.QueryScalar("SELECT COUNT(table_name) FROM information_schema.tables WHERE table_schema = @0 and table_name = 'Bans'", TShock.Config.Settings.MySqlDbName); - } - else - { - res = database.QueryScalar("SELECT COUNT(name) FROM sqlite_master WHERE type='table' AND name = 'Bans'"); - } + SqlType.Mysql => database.QueryScalar("SELECT COUNT(table_name) FROM information_schema.tables WHERE table_schema = @0 and table_name = 'Bans'", TShock.Config.Settings.MySqlDbName), + SqlType.Sqlite => database.QueryScalar("SELECT COUNT(name) FROM sqlite_master WHERE type='table' AND name = 'Bans'"), + SqlType.Postgres => database.QueryScalar("SELECT COUNT(table_name) FROM information_schema.tables WHERE table_name = 'Bans'"), + }; if (res != 0) { @@ -301,16 +297,13 @@ namespace TShockAPI.DB return new AddBanResult { Message = message }; } - string query = "INSERT INTO PlayerBans (Identifier, Reason, BanningUser, Date, Expiration) VALUES (@0, @1, @2, @3, @4);"; - - if (database.GetSqlType() == SqlType.Mysql) + string query = "INSERT INTO PlayerBans (Identifier, Reason, BanningUser, Date, Expiration) VALUES (@0, @1, @2, @3, @4)" + database.GetSqlType() switch { - query += "SELECT LAST_INSERT_ID();"; - } - else - { - query += "SELECT CAST(last_insert_rowid() as INT);"; - } + SqlType.Mysql => /*lang=mysql*/"; SELECT LAST_INSERT_ID();", + SqlType.Sqlite => /*lang=sqlite*/"; SELECT last_insert_rowid();", + SqlType.Postgres => /*lang=postgresql*/"RETURNING \"Identifier\";", + _ => null + }; int ticketId = database.QueryScalar(query, args.Identifier, args.Reason, args.BanningUser, args.BanDateTime.Ticks, args.ExpirationDateTime.Ticks); @@ -362,19 +355,18 @@ namespace TShockAPI.DB return Bans[id]; } - using (var reader = database.QueryReader("SELECT * FROM PlayerBans WHERE TicketNumber=@0", id)) - { - if (reader.Read()) - { - var ticketNumber = reader.Get("TicketNumber"); - var identifier = reader.Get("Identifier"); - var reason = reader.Get("Reason"); - var banningUser = reader.Get("BanningUser"); - var date = reader.Get("Date"); - var expiration = reader.Get("Expiration"); + using var reader = database.QueryReader("SELECT * FROM PlayerBans WHERE TicketNumber=@0", id); - return new Ban(ticketNumber, identifier, reason, banningUser, date, expiration); - } + if (reader.Read()) + { + var ticketNumber = reader.Get("TicketNumber"); + var identifier = reader.Get("Identifier"); + var reason = reader.Get("Reason"); + var banningUser = reader.Get("BanningUser"); + var date = reader.Get("Date"); + var expiration = reader.Get("Expiration"); + + return new Ban(ticketNumber, identifier, reason, banningUser, date, expiration); } return null; @@ -394,19 +386,18 @@ namespace TShockAPI.DB query += $" AND Expiration > {DateTime.UtcNow.Ticks}"; } - using (var reader = database.QueryReader(query, identifier)) - { - while (reader.Read()) - { - var ticketNumber = reader.Get("TicketNumber"); - var ident = reader.Get("Identifier"); - var reason = reader.Get("Reason"); - var banningUser = reader.Get("BanningUser"); - var date = reader.Get("Date"); - var expiration = reader.Get("Expiration"); + using var reader = database.QueryReader(query, identifier); - yield return new Ban(ticketNumber, ident, reason, banningUser, date, expiration); - } + while (reader.Read()) + { + var ticketNumber = reader.Get("TicketNumber"); + var ident = reader.Get("Identifier"); + var reason = reader.Get("Reason"); + var banningUser = reader.Get("BanningUser"); + var date = reader.Get("Date"); + var expiration = reader.Get("Expiration"); + + yield return new Ban(ticketNumber, ident, reason, banningUser, date, expiration); } } @@ -419,27 +410,27 @@ namespace TShockAPI.DB public IEnumerable GetBansByIdentifiers(bool currentOnly = true, params string[] identifiers) { //Generate a sequence of '@0, @1, @2, ... etc' - var parameters = string.Join(", ", Enumerable.Range(0, identifiers.Count()).Select(p => $"@{p}")); + var parameters = string.Join(", ", Enumerable.Range(0, identifiers.Length).Select(p => $"@{p}")); string query = $"SELECT * FROM PlayerBans WHERE Identifier IN ({parameters})"; + if (currentOnly) { query += $" AND Expiration > {DateTime.UtcNow.Ticks}"; } - using (var reader = database.QueryReader(query, identifiers)) - { - while (reader.Read()) - { - var ticketNumber = reader.Get("TicketNumber"); - var identifier = reader.Get("Identifier"); - var reason = reader.Get("Reason"); - var banningUser = reader.Get("BanningUser"); - var date = reader.Get("Date"); - var expiration = reader.Get("Expiration"); + using var reader = database.QueryReader(query, identifiers); - yield return new Ban(ticketNumber, identifier, reason, banningUser, date, expiration); - } + while (reader.Read()) + { + var ticketNumber = reader.Get("TicketNumber"); + var identifier = reader.Get("Identifier"); + var reason = reader.Get("Reason"); + var banningUser = reader.Get("BanningUser"); + var date = reader.Get("Date"); + var expiration = reader.Get("Expiration"); + + yield return new Ban(ticketNumber, identifier, reason, banningUser, date, expiration); } } @@ -458,21 +449,19 @@ namespace TShockAPI.DB List banlist = new List(); try { - var orderBy = SortToOrderByMap[sortMethod]; - using (var reader = database.QueryReader($"SELECT * FROM PlayerBans ORDER BY {orderBy}")) - { - while (reader.Read()) - { - var ticketNumber = reader.Get("TicketNumber"); - var identifier = reader.Get("Identifier"); - var reason = reader.Get("Reason"); - var banningUser = reader.Get("BanningUser"); - var date = reader.Get("Date"); - var expiration = reader.Get("Expiration"); + using var reader = database.QueryReader($"SELECT * FROM PlayerBans ORDER BY {SortToOrderByMap[sortMethod]}"); - var ban = new Ban(ticketNumber, identifier, reason, banningUser, date, expiration); - banlist.Add(ban); - } + while (reader.Read()) + { + var ticketNumber = reader.Get("TicketNumber"); + var identifier = reader.Get("Identifier"); + var reason = reader.Get("Reason"); + var banningUser = reader.Get("BanningUser"); + var date = reader.Get("Date"); + var expiration = reader.Get("Expiration"); + + var ban = new Ban(ticketNumber, identifier, reason, banningUser, date, expiration); + banlist.Add(ban); } } catch (Exception ex) @@ -501,7 +490,7 @@ namespace TShockAPI.DB return false; } - internal Dictionary SortToOrderByMap = new Dictionary + private readonly Dictionary SortToOrderByMap = new() { { BanSortMethod.AddedNewestToOldest, "Date DESC" }, { BanSortMethod.AddedOldestToNewest, "Date ASC" }, diff --git a/TShockAPI/DB/CharacterManager.cs b/TShockAPI/DB/CharacterManager.cs index be09da1d..3b9890a3 100644 --- a/TShockAPI/DB/CharacterManager.cs +++ b/TShockAPI/DB/CharacterManager.cs @@ -71,10 +71,8 @@ namespace TShockAPI.DB new SqlColumn("unlockedSuperCart", MySqlDbType.Int32), new SqlColumn("enabledSuperCart", MySqlDbType.Int32) ); - var creator = new SqlTableCreator(db, - db.GetSqlType() == SqlType.Sqlite - ? (IQueryBuilder) new SqliteQueryCreator() - : new MysqlQueryCreator()); + + SqlTableCreator creator = new(db, db.GetSqlQueryBuilder()); creator.EnsureTableStructure(table); } @@ -84,59 +82,57 @@ namespace TShockAPI.DB try { - using (var reader = database.QueryReader("SELECT * FROM tsCharacter WHERE Account=@0", acctid)) + using var reader = database.QueryReader("SELECT * FROM tsCharacter WHERE Account=@0", acctid); + if (reader.Read()) { - if (reader.Read()) + playerData.exists = true; + playerData.health = reader.Get("Health"); + playerData.maxHealth = reader.Get("MaxHealth"); + playerData.mana = reader.Get("Mana"); + playerData.maxMana = reader.Get("MaxMana"); + List inventory = reader.Get("Inventory").Split('~').Select(NetItem.Parse).ToList(); + if (inventory.Count < NetItem.MaxInventory) { - playerData.exists = true; - playerData.health = reader.Get("Health"); - playerData.maxHealth = reader.Get("MaxHealth"); - playerData.mana = reader.Get("Mana"); - playerData.maxMana = reader.Get("MaxMana"); - List inventory = reader.Get("Inventory").Split('~').Select(NetItem.Parse).ToList(); - if (inventory.Count < NetItem.MaxInventory) - { - //TODO: unhardcode this - stop using magic numbers and use NetItem numbers - //Set new armour slots empty - inventory.InsertRange(67, new NetItem[2]); - //Set new vanity slots empty - inventory.InsertRange(77, new NetItem[2]); - //Set new dye slots empty - inventory.InsertRange(87, new NetItem[2]); - //Set the rest of the new slots empty - inventory.AddRange(new NetItem[NetItem.MaxInventory - inventory.Count]); - } - playerData.inventory = inventory.ToArray(); - playerData.extraSlot = reader.Get("extraSlot"); - playerData.spawnX = reader.Get("spawnX"); - playerData.spawnY = reader.Get("spawnY"); - playerData.skinVariant = reader.Get("skinVariant"); - playerData.hair = reader.Get("hair"); - playerData.hairDye = (byte)reader.Get("hairDye"); - playerData.hairColor = TShock.Utils.DecodeColor(reader.Get("hairColor")); - playerData.pantsColor = TShock.Utils.DecodeColor(reader.Get("pantsColor")); - playerData.shirtColor = TShock.Utils.DecodeColor(reader.Get("shirtColor")); - playerData.underShirtColor = TShock.Utils.DecodeColor(reader.Get("underShirtColor")); - playerData.shoeColor = TShock.Utils.DecodeColor(reader.Get("shoeColor")); - playerData.hideVisuals = TShock.Utils.DecodeBoolArray(reader.Get("hideVisuals")); - playerData.skinColor = TShock.Utils.DecodeColor(reader.Get("skinColor")); - playerData.eyeColor = TShock.Utils.DecodeColor(reader.Get("eyeColor")); - playerData.questsCompleted = reader.Get("questsCompleted"); - playerData.usingBiomeTorches = reader.Get("usingBiomeTorches"); - playerData.happyFunTorchTime = reader.Get("happyFunTorchTime"); - playerData.unlockedBiomeTorches = reader.Get("unlockedBiomeTorches"); - playerData.currentLoadoutIndex = reader.Get("currentLoadoutIndex"); - playerData.ateArtisanBread = reader.Get("ateArtisanBread"); - playerData.usedAegisCrystal = reader.Get("usedAegisCrystal"); - playerData.usedAegisFruit = reader.Get("usedAegisFruit"); - playerData.usedArcaneCrystal = reader.Get("usedArcaneCrystal"); - playerData.usedGalaxyPearl = reader.Get("usedGalaxyPearl"); - playerData.usedGummyWorm = reader.Get("usedGummyWorm"); - playerData.usedAmbrosia = reader.Get("usedAmbrosia"); - playerData.unlockedSuperCart = reader.Get("unlockedSuperCart"); - playerData.enabledSuperCart = reader.Get("enabledSuperCart"); - return playerData; + //TODO: unhardcode this - stop using magic numbers and use NetItem numbers + //Set new armour slots empty + inventory.InsertRange(67, new NetItem[2]); + //Set new vanity slots empty + inventory.InsertRange(77, new NetItem[2]); + //Set new dye slots empty + inventory.InsertRange(87, new NetItem[2]); + //Set the rest of the new slots empty + inventory.AddRange(new NetItem[NetItem.MaxInventory - inventory.Count]); } + playerData.inventory = inventory.ToArray(); + playerData.extraSlot = reader.Get("extraSlot"); + playerData.spawnX = reader.Get("spawnX"); + playerData.spawnY = reader.Get("spawnY"); + playerData.skinVariant = reader.Get("skinVariant"); + playerData.hair = reader.Get("hair"); + playerData.hairDye = (byte)reader.Get("hairDye"); + playerData.hairColor = TShock.Utils.DecodeColor(reader.Get("hairColor")); + playerData.pantsColor = TShock.Utils.DecodeColor(reader.Get("pantsColor")); + playerData.shirtColor = TShock.Utils.DecodeColor(reader.Get("shirtColor")); + playerData.underShirtColor = TShock.Utils.DecodeColor(reader.Get("underShirtColor")); + playerData.shoeColor = TShock.Utils.DecodeColor(reader.Get("shoeColor")); + playerData.hideVisuals = TShock.Utils.DecodeBoolArray(reader.Get("hideVisuals")); + playerData.skinColor = TShock.Utils.DecodeColor(reader.Get("skinColor")); + playerData.eyeColor = TShock.Utils.DecodeColor(reader.Get("eyeColor")); + playerData.questsCompleted = reader.Get("questsCompleted"); + playerData.usingBiomeTorches = reader.Get("usingBiomeTorches"); + playerData.happyFunTorchTime = reader.Get("happyFunTorchTime"); + playerData.unlockedBiomeTorches = reader.Get("unlockedBiomeTorches"); + playerData.currentLoadoutIndex = reader.Get("currentLoadoutIndex"); + playerData.ateArtisanBread = reader.Get("ateArtisanBread"); + playerData.usedAegisCrystal = reader.Get("usedAegisCrystal"); + playerData.usedAegisFruit = reader.Get("usedAegisFruit"); + playerData.usedArcaneCrystal = reader.Get("usedArcaneCrystal"); + playerData.usedGalaxyPearl = reader.Get("usedGalaxyPearl"); + playerData.usedGummyWorm = reader.Get("usedGummyWorm"); + playerData.usedAmbrosia = reader.Get("usedAmbrosia"); + playerData.unlockedSuperCart = reader.Get("unlockedSuperCart"); + playerData.enabledSuperCart = reader.Get("enabledSuperCart"); + return playerData; } } catch (Exception ex) @@ -155,7 +151,7 @@ namespace TShockAPI.DB if (items.Count < NetItem.MaxInventory) items.AddRange(new NetItem[NetItem.MaxInventory - items.Count]); - string initialItems = String.Join("~", items.Take(NetItem.MaxInventory)); + string initialItems = string.Join("~", items.Take(NetItem.MaxInventory)); try { database.Query("INSERT INTO tsCharacter (Account, Health, MaxHealth, Mana, MaxMana, Inventory, spawnX, spawnY, questsCompleted) VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8);", @@ -205,7 +201,7 @@ namespace TShockAPI.DB { database.Query( "INSERT INTO tsCharacter (Account, Health, MaxHealth, Mana, MaxMana, Inventory, extraSlot, spawnX, spawnY, skinVariant, hair, hairDye, hairColor, pantsColor, shirtColor, underShirtColor, shoeColor, hideVisuals, skinColor, eyeColor, questsCompleted, usingBiomeTorches, happyFunTorchTime, unlockedBiomeTorches, currentLoadoutIndex,ateArtisanBread, usedAegisCrystal, usedAegisFruit, usedArcaneCrystal, usedGalaxyPearl, usedGummyWorm, usedAmbrosia, unlockedSuperCart, enabledSuperCart) VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8, @9, @10, @11, @12, @13, @14, @15, @16, @17, @18, @19, @20, @21, @22, @23, @24, @25, @26, @27, @28, @29, @30, @31, @32, @33);", - player.Account.ID, playerData.health, playerData.maxHealth, playerData.mana, playerData.maxMana, String.Join("~", playerData.inventory), playerData.extraSlot, player.TPlayer.SpawnX, player.TPlayer.SpawnY, player.TPlayer.skinVariant, player.TPlayer.hair, player.TPlayer.hairDye, TShock.Utils.EncodeColor(player.TPlayer.hairColor), TShock.Utils.EncodeColor(player.TPlayer.pantsColor),TShock.Utils.EncodeColor(player.TPlayer.shirtColor), TShock.Utils.EncodeColor(player.TPlayer.underShirtColor), TShock.Utils.EncodeColor(player.TPlayer.shoeColor), TShock.Utils.EncodeBoolArray(player.TPlayer.hideVisibleAccessory), TShock.Utils.EncodeColor(player.TPlayer.skinColor),TShock.Utils.EncodeColor(player.TPlayer.eyeColor), player.TPlayer.anglerQuestsFinished, player.TPlayer.UsingBiomeTorches ? 1 : 0, player.TPlayer.happyFunTorchTime ? 1 : 0, player.TPlayer.unlockedBiomeTorches ? 1 : 0, player.TPlayer.CurrentLoadoutIndex, player.TPlayer.ateArtisanBread ? 1 : 0, player.TPlayer.usedAegisCrystal ? 1 : 0, player.TPlayer.usedAegisFruit ? 1 : 0, player.TPlayer.usedArcaneCrystal ? 1 : 0, player.TPlayer.usedGalaxyPearl ? 1 : 0, player.TPlayer.usedGummyWorm ? 1 : 0, player.TPlayer.usedAmbrosia ? 1 : 0, player.TPlayer.unlockedSuperCart ? 1 : 0, player.TPlayer.enabledSuperCart ? 1 : 0); + player.Account.ID, playerData.health, playerData.maxHealth, playerData.mana, playerData.maxMana, string.Join("~", playerData.inventory), playerData.extraSlot, player.TPlayer.SpawnX, player.TPlayer.SpawnY, player.TPlayer.skinVariant, player.TPlayer.hair, player.TPlayer.hairDye, TShock.Utils.EncodeColor(player.TPlayer.hairColor), TShock.Utils.EncodeColor(player.TPlayer.pantsColor),TShock.Utils.EncodeColor(player.TPlayer.shirtColor), TShock.Utils.EncodeColor(player.TPlayer.underShirtColor), TShock.Utils.EncodeColor(player.TPlayer.shoeColor), TShock.Utils.EncodeBoolArray(player.TPlayer.hideVisibleAccessory), TShock.Utils.EncodeColor(player.TPlayer.skinColor),TShock.Utils.EncodeColor(player.TPlayer.eyeColor), player.TPlayer.anglerQuestsFinished, player.TPlayer.UsingBiomeTorches ? 1 : 0, player.TPlayer.happyFunTorchTime ? 1 : 0, player.TPlayer.unlockedBiomeTorches ? 1 : 0, player.TPlayer.CurrentLoadoutIndex, player.TPlayer.ateArtisanBread ? 1 : 0, player.TPlayer.usedAegisCrystal ? 1 : 0, player.TPlayer.usedAegisFruit ? 1 : 0, player.TPlayer.usedArcaneCrystal ? 1 : 0, player.TPlayer.usedGalaxyPearl ? 1 : 0, player.TPlayer.usedGummyWorm ? 1 : 0, player.TPlayer.usedAmbrosia ? 1 : 0, player.TPlayer.unlockedSuperCart ? 1 : 0, player.TPlayer.enabledSuperCart ? 1 : 0); return true; } catch (Exception ex) @@ -219,7 +215,7 @@ namespace TShockAPI.DB { database.Query( "UPDATE tsCharacter SET Health = @0, MaxHealth = @1, Mana = @2, MaxMana = @3, Inventory = @4, spawnX = @6, spawnY = @7, hair = @8, hairDye = @9, hairColor = @10, pantsColor = @11, shirtColor = @12, underShirtColor = @13, shoeColor = @14, hideVisuals = @15, skinColor = @16, eyeColor = @17, questsCompleted = @18, skinVariant = @19, extraSlot = @20, usingBiomeTorches = @21, happyFunTorchTime = @22, unlockedBiomeTorches = @23, currentLoadoutIndex = @24, ateArtisanBread = @25, usedAegisCrystal = @26, usedAegisFruit = @27, usedArcaneCrystal = @28, usedGalaxyPearl = @29, usedGummyWorm = @30, usedAmbrosia = @31, unlockedSuperCart = @32, enabledSuperCart = @33 WHERE Account = @5;", - playerData.health, playerData.maxHealth, playerData.mana, playerData.maxMana, String.Join("~", playerData.inventory), player.Account.ID, player.TPlayer.SpawnX, player.TPlayer.SpawnY, player.TPlayer.hair, player.TPlayer.hairDye, TShock.Utils.EncodeColor(player.TPlayer.hairColor), TShock.Utils.EncodeColor(player.TPlayer.pantsColor), TShock.Utils.EncodeColor(player.TPlayer.shirtColor), TShock.Utils.EncodeColor(player.TPlayer.underShirtColor), TShock.Utils.EncodeColor(player.TPlayer.shoeColor), TShock.Utils.EncodeBoolArray(player.TPlayer.hideVisibleAccessory), TShock.Utils.EncodeColor(player.TPlayer.skinColor), TShock.Utils.EncodeColor(player.TPlayer.eyeColor), player.TPlayer.anglerQuestsFinished, player.TPlayer.skinVariant, player.TPlayer.extraAccessory ? 1 : 0, player.TPlayer.UsingBiomeTorches ? 1 : 0, player.TPlayer.happyFunTorchTime ? 1 : 0, player.TPlayer.unlockedBiomeTorches ? 1 : 0, player.TPlayer.CurrentLoadoutIndex, player.TPlayer.ateArtisanBread ? 1 : 0, player.TPlayer.usedAegisCrystal ? 1 : 0, player.TPlayer.usedAegisFruit ? 1 : 0, player.TPlayer.usedArcaneCrystal ? 1 : 0, player.TPlayer.usedGalaxyPearl ? 1 : 0, player.TPlayer.usedGummyWorm ? 1 : 0, player.TPlayer.usedAmbrosia ? 1 : 0, player.TPlayer.unlockedSuperCart ? 1 : 0, player.TPlayer.enabledSuperCart ? 1 : 0); + playerData.health, playerData.maxHealth, playerData.mana, playerData.maxMana, string.Join("~", playerData.inventory), player.Account.ID, player.TPlayer.SpawnX, player.TPlayer.SpawnY, player.TPlayer.hair, player.TPlayer.hairDye, TShock.Utils.EncodeColor(player.TPlayer.hairColor), TShock.Utils.EncodeColor(player.TPlayer.pantsColor), TShock.Utils.EncodeColor(player.TPlayer.shirtColor), TShock.Utils.EncodeColor(player.TPlayer.underShirtColor), TShock.Utils.EncodeColor(player.TPlayer.shoeColor), TShock.Utils.EncodeBoolArray(player.TPlayer.hideVisibleAccessory), TShock.Utils.EncodeColor(player.TPlayer.skinColor), TShock.Utils.EncodeColor(player.TPlayer.eyeColor), player.TPlayer.anglerQuestsFinished, player.TPlayer.skinVariant, player.TPlayer.extraAccessory ? 1 : 0, player.TPlayer.UsingBiomeTorches ? 1 : 0, player.TPlayer.happyFunTorchTime ? 1 : 0, player.TPlayer.unlockedBiomeTorches ? 1 : 0, player.TPlayer.CurrentLoadoutIndex, player.TPlayer.ateArtisanBread ? 1 : 0, player.TPlayer.usedAegisCrystal ? 1 : 0, player.TPlayer.usedAegisFruit ? 1 : 0, player.TPlayer.usedArcaneCrystal ? 1 : 0, player.TPlayer.usedGalaxyPearl ? 1 : 0, player.TPlayer.usedGummyWorm ? 1 : 0, player.TPlayer.usedAmbrosia ? 1 : 0, player.TPlayer.unlockedSuperCart ? 1 : 0, player.TPlayer.enabledSuperCart ? 1 : 0); return true; } catch (Exception ex) @@ -280,7 +276,7 @@ namespace TShockAPI.DB playerData.maxHealth, playerData.mana, playerData.maxMana, - String.Join("~", playerData.inventory), + string.Join("~", playerData.inventory), playerData.extraSlot, playerData.spawnX, playerData.spawnX, @@ -326,7 +322,7 @@ namespace TShockAPI.DB playerData.maxHealth, playerData.mana, playerData.maxMana, - String.Join("~", playerData.inventory), + string.Join("~", playerData.inventory), player.Account.ID, playerData.spawnX, playerData.spawnX, diff --git a/TShockAPI/DB/GroupManager.cs b/TShockAPI/DB/GroupManager.cs index 5d32419e..553a1a33 100644 --- a/TShockAPI/DB/GroupManager.cs +++ b/TShockAPI/DB/GroupManager.cs @@ -51,10 +51,9 @@ namespace TShockAPI.DB new SqlColumn("Prefix", MySqlDbType.Text), new SqlColumn("Suffix", MySqlDbType.Text) ); - var creator = new SqlTableCreator(db, - db.GetSqlType() == SqlType.Sqlite - ? (IQueryBuilder)new SqliteQueryCreator() - : new MysqlQueryCreator()); + + SqlTableCreator creator = new(db, db.GetSqlQueryBuilder()); + if (creator.EnsureTableStructure(table)) { // Add default groups if they don't exist @@ -294,7 +293,7 @@ namespace TShockAPI.DB /// parent of group /// permissions /// chatcolor - public void AddGroup(String name, string parentname, String permissions, String chatcolor) + public void AddGroup(string name, string parentname, string permissions, string chatcolor) { if (GroupExists(name)) { @@ -383,7 +382,7 @@ namespace TShockAPI.DB /// The group's name. /// The new name. /// The result from the operation to be sent back to the user. - public String RenameGroup(string name, string newName) + public string RenameGroup(string name, string newName) { if (!GroupExists(name)) { @@ -395,87 +394,83 @@ namespace TShockAPI.DB throw new GroupExistsException(newName); } - using (var db = database.CloneEx()) + using var db = database.CloneEx(); + db.Open(); + using var transaction = db.BeginTransaction(); + try { - db.Open(); - using (var transaction = db.BeginTransaction()) + using (var command = db.CreateCommand()) { - try - { - using (var command = db.CreateCommand()) - { - command.CommandText = "UPDATE GroupList SET GroupName = @0 WHERE GroupName = @1"; - command.AddParameter("@0", newName); - command.AddParameter("@1", name); - command.ExecuteNonQuery(); - } + command.CommandText = "UPDATE GroupList SET GroupName = @0 WHERE GroupName = @1"; + command.AddParameter("@0", newName); + command.AddParameter("@1", name); + command.ExecuteNonQuery(); + } - var oldGroup = GetGroupByName(name); - var newGroup = new Group(newName, oldGroup.Parent, oldGroup.ChatColor, oldGroup.Permissions) - { - Prefix = oldGroup.Prefix, - Suffix = oldGroup.Suffix - }; - groups.Remove(oldGroup); - groups.Add(newGroup); + var oldGroup = GetGroupByName(name); + var newGroup = new Group(newName, oldGroup.Parent, oldGroup.ChatColor, oldGroup.Permissions) + { + Prefix = oldGroup.Prefix, + Suffix = oldGroup.Suffix + }; + groups.Remove(oldGroup); + groups.Add(newGroup); - // We need to check if the old group has been referenced as a parent and update those references accordingly - using (var command = db.CreateCommand()) - { - command.CommandText = "UPDATE GroupList SET Parent = @0 WHERE Parent = @1"; - command.AddParameter("@0", newName); - command.AddParameter("@1", name); - command.ExecuteNonQuery(); - } - foreach (var group in groups.Where(g => g.Parent != null && g.Parent == oldGroup)) - { - group.Parent = newGroup; - } + // We need to check if the old group has been referenced as a parent and update those references accordingly + using (var command = db.CreateCommand()) + { + command.CommandText = "UPDATE GroupList SET Parent = @0 WHERE Parent = @1"; + command.AddParameter("@0", newName); + command.AddParameter("@1", name); + command.ExecuteNonQuery(); + } + foreach (var group in groups.Where(g => g.Parent != null && g.Parent == oldGroup)) + { + group.Parent = newGroup; + } - // Read the config file to prevent the possible loss of any unsaved changes - TShock.Config.Read(FileTools.ConfigPath, out bool writeConfig); - if (TShock.Config.Settings.DefaultGuestGroupName == oldGroup.Name) - { - TShock.Config.Settings.DefaultGuestGroupName = newGroup.Name; - Group.DefaultGroup = newGroup; - } - if (TShock.Config.Settings.DefaultRegistrationGroupName == oldGroup.Name) - { - TShock.Config.Settings.DefaultRegistrationGroupName = newGroup.Name; - } - if (writeConfig) - { - TShock.Config.Write(FileTools.ConfigPath); - } + // Read the config file to prevent the possible loss of any unsaved changes + TShock.Config.Read(FileTools.ConfigPath, out bool writeConfig); + if (TShock.Config.Settings.DefaultGuestGroupName == oldGroup.Name) + { + TShock.Config.Settings.DefaultGuestGroupName = newGroup.Name; + Group.DefaultGroup = newGroup; + } + if (TShock.Config.Settings.DefaultRegistrationGroupName == oldGroup.Name) + { + TShock.Config.Settings.DefaultRegistrationGroupName = newGroup.Name; + } + if (writeConfig) + { + TShock.Config.Write(FileTools.ConfigPath); + } - // We also need to check if any users belong to the old group and automatically apply changes - using (var command = db.CreateCommand()) - { - command.CommandText = "UPDATE Users SET Usergroup = @0 WHERE Usergroup = @1"; - command.AddParameter("@0", newName); - command.AddParameter("@1", name); - command.ExecuteNonQuery(); - } - foreach (var player in TShock.Players.Where(p => p?.Group == oldGroup)) - { - player.Group = newGroup; - } + // We also need to check if any users belong to the old group and automatically apply changes + using (var command = db.CreateCommand()) + { + command.CommandText = "UPDATE Users SET Usergroup = @0 WHERE Usergroup = @1"; + command.AddParameter("@0", newName); + command.AddParameter("@1", name); + command.ExecuteNonQuery(); + } + foreach (var player in TShock.Players.Where(p => p?.Group == oldGroup)) + { + player.Group = newGroup; + } - transaction.Commit(); - return GetString($"Group {name} has been renamed to {newName}."); - } - catch (Exception ex) - { - TShock.Log.Error(GetString($"An exception has occurred during database transaction: {ex.Message}")); - try - { - transaction.Rollback(); - } - catch (Exception rollbackEx) - { - TShock.Log.Error(GetString($"An exception has occurred during database rollback: {rollbackEx.Message}")); - } - } + transaction.Commit(); + return GetString($"Group {name} has been renamed to {newName}."); + } + catch (Exception ex) + { + TShock.Log.Error(GetString($"An exception has occurred during database transaction: {ex.Message}")); + try + { + transaction.Rollback(); + } + catch (Exception rollbackEx) + { + TShock.Log.Error(GetString($"An exception has occurred during database rollback: {rollbackEx.Message}")); } } @@ -488,7 +483,7 @@ namespace TShockAPI.DB /// The group's name. /// Whether exceptions will be thrown in case something goes wrong. /// The result from the operation to be sent back to the user. - public String DeleteGroup(String name, bool exceptions = false) + public string DeleteGroup(string name, bool exceptions = false) { if (!GroupExists(name)) { @@ -521,7 +516,7 @@ namespace TShockAPI.DB /// The group name. /// The permission list. /// The result from the operation to be sent back to the user. - public String AddPermissions(String name, List permissions) + public string AddPermissions(string name, List permissions) { if (!GroupExists(name)) return GetString($"Group {name} doesn't exist."); @@ -544,7 +539,7 @@ namespace TShockAPI.DB /// The group name. /// The permission list. /// The result from the operation to be sent back to the user. - public String DeletePermissions(String name, List permissions) + public string DeletePermissions(string name, List permissions) { if (!GroupExists(name)) return GetString($"Group {name} doesn't exist."); diff --git a/TShockAPI/DB/ItemManager.cs b/TShockAPI/DB/ItemManager.cs index e707406c..e533d7c0 100644 --- a/TShockAPI/DB/ItemManager.cs +++ b/TShockAPI/DB/ItemManager.cs @@ -39,10 +39,9 @@ namespace TShockAPI.DB new SqlColumn("ItemName", MySqlDbType.VarChar, 50) {Primary = true}, new SqlColumn("AllowedGroups", MySqlDbType.Text) ); - var creator = new SqlTableCreator(db, - db.GetSqlType() == SqlType.Sqlite - ? (IQueryBuilder) new SqliteQueryCreator() - : new MysqlQueryCreator()); + + SqlTableCreator creator = new(db, db.GetSqlQueryBuilder()); + creator.EnsureTableStructure(table); UpdateItemBans(); } @@ -51,14 +50,13 @@ namespace TShockAPI.DB { ItemBans.Clear(); - using (var reader = database.QueryReader("SELECT * FROM ItemBans")) + using var reader = database.QueryReader("SELECT * FROM ItemBans"); + + while (reader != null && reader.Read()) { - while (reader != null && reader.Read()) - { - ItemBan ban = new ItemBan(reader.Get("ItemName")); - ban.SetAllowedGroups(reader.Get("AllowedGroups")); - ItemBans.Add(ban); - } + ItemBan ban = new ItemBan(reader.Get("ItemName")); + ban.SetAllowedGroups(reader.Get("AllowedGroups")); + ItemBans.Add(ban); } } @@ -92,14 +90,7 @@ namespace TShockAPI.DB } } - public bool ItemIsBanned(string name) - { - if (ItemBans.Contains(new ItemBan(name))) - { - return true; - } - return false; - } + public bool ItemIsBanned(string name) => ItemBans.Contains(new(name)); public bool ItemIsBanned(string name, TSPlayer ply) { @@ -109,13 +100,12 @@ namespace TShockAPI.DB public bool AllowGroup(string item, string name) { - string groupsNew = ""; ItemBan b = GetItemBanByName(item); if (b != null) { try { - groupsNew = String.Join(",", b.AllowedGroups); + string groupsNew = string.Join(",", b.AllowedGroups); if (groupsNew.Length > 0) groupsNew += ","; groupsNew += name; @@ -123,7 +113,7 @@ namespace TShockAPI.DB int q = database.Query("UPDATE ItemBans SET AllowedGroups=@0 WHERE ItemName=@1", groupsNew, item); - + return q > 0; } catch (Exception ex) @@ -141,12 +131,12 @@ namespace TShockAPI.DB if (b != null) { try - { + { b.RemoveGroup(group); string groups = string.Join(",", b.AllowedGroups); int q = database.Query("UPDATE ItemBans SET AllowedGroups=@0 WHERE ItemName=@1", groups, item); - + if (q > 0) return true; } @@ -158,7 +148,7 @@ namespace TShockAPI.DB return false; } - public ItemBan GetItemBanByName(String name) + public ItemBan GetItemBanByName(string name) { for (int i = 0; i < ItemBans.Count; i++) { @@ -189,10 +179,7 @@ namespace TShockAPI.DB AllowedGroups = new List(); } - public bool Equals(ItemBan other) - { - return Name == other.Name; - } + public bool Equals(ItemBan other) => Name == other.Name; public bool HasPermissionToUseItem(TSPlayer ply) { @@ -225,12 +212,12 @@ namespace TShockAPI.DB // could add in the other permissions in this class instead of a giant if switch. } - public void SetAllowedGroups(String groups) + public void SetAllowedGroups(string groups) { // prevent null pointer exceptions if (!string.IsNullOrEmpty(groups)) { - List groupArr = groups.Split(',').ToList(); + List groupArr = groups.Split(',').ToList(); for (int i = 0; i < groupArr.Count; i++) { @@ -245,10 +232,10 @@ namespace TShockAPI.DB { return AllowedGroups.Remove(groupName); } - + public override string ToString() { - return Name + (AllowedGroups.Count > 0 ? " (" + String.Join(",", AllowedGroups) + ")" : ""); + return Name + (AllowedGroups.Count > 0 ? " (" + string.Join(",", AllowedGroups) + ")" : ""); } } -} \ No newline at end of file +} diff --git a/TShockAPI/DB/ProjectileManager.cs b/TShockAPI/DB/ProjectileManager.cs index f07280b5..f9372f57 100644 --- a/TShockAPI/DB/ProjectileManager.cs +++ b/TShockAPI/DB/ProjectileManager.cs @@ -39,10 +39,8 @@ namespace TShockAPI.DB new SqlColumn("ProjectileID", MySqlDbType.Int32) {Primary = true}, new SqlColumn("AllowedGroups", MySqlDbType.Text) ); - var creator = new SqlTableCreator(db, - db.GetSqlType() == SqlType.Sqlite - ? (IQueryBuilder) new SqliteQueryCreator() - : new MysqlQueryCreator()); + + SqlTableCreator creator = new(db, db.GetSqlQueryBuilder()); creator.EnsureTableStructure(table); UpdateBans(); } @@ -51,14 +49,13 @@ namespace TShockAPI.DB { ProjectileBans.Clear(); - using (var reader = database.QueryReader("SELECT * FROM ProjectileBans")) + using var reader = database.QueryReader("SELECT * FROM ProjectileBans"); + + while (reader != null && reader.Read()) { - while (reader != null && reader.Read()) - { - ProjectileBan ban = new ProjectileBan((short) reader.Get("ProjectileID")); - ban.SetAllowedGroups(reader.Get("AllowedGroups")); - ProjectileBans.Add(ban); - } + ProjectileBan ban = new ProjectileBan((short) reader.Get("ProjectileID")); + ban.SetAllowedGroups(reader.Get("AllowedGroups")); + ProjectileBans.Add(ban); } } @@ -93,14 +90,7 @@ namespace TShockAPI.DB } } - public bool ProjectileIsBanned(short id) - { - if (ProjectileBans.Contains(new ProjectileBan(id))) - { - return true; - } - return false; - } + public bool ProjectileIsBanned(short id) => ProjectileBans.Contains(new(id)); public bool ProjectileIsBanned(short id, TSPlayer ply) { @@ -114,13 +104,12 @@ namespace TShockAPI.DB public bool AllowGroup(short id, string name) { - string groupsNew = ""; ProjectileBan b = GetBanById(id); if (b != null) { - try + try { - groupsNew = String.Join(",", b.AllowedGroups); + string groupsNew = string.Join(",", b.AllowedGroups); if (groupsNew.Length > 0) groupsNew += ","; groupsNew += name; @@ -194,10 +183,7 @@ namespace TShockAPI.DB AllowedGroups = new List(); } - public bool Equals(ProjectileBan other) - { - return ID == other.ID; - } + public bool Equals(ProjectileBan other) => ID == other.ID; public bool HasPermissionToCreateProjectile(TSPlayer ply) { @@ -230,12 +216,12 @@ namespace TShockAPI.DB // could add in the other permissions in this class instead of a giant if switch. } - public void SetAllowedGroups(String groups) + public void SetAllowedGroups(string groups) { // prevent null pointer exceptions if (!string.IsNullOrEmpty(groups)) { - List groupArr = groups.Split(',').ToList(); + List groupArr = groups.Split(',').ToList(); for (int i = 0; i < groupArr.Count; i++) { @@ -246,14 +232,8 @@ namespace TShockAPI.DB } } - public bool RemoveGroup(string groupName) - { - return AllowedGroups.Remove(groupName); - } + public bool RemoveGroup(string groupName) => AllowedGroups.Remove(groupName); - public override string ToString() - { - return ID + (AllowedGroups.Count > 0 ? " (" + String.Join(",", AllowedGroups) + ")" : ""); - } + public override string ToString() => ID + (AllowedGroups.Count > 0 ? $" ({string.Join(",", AllowedGroups)})" : ""); } } diff --git a/TShockAPI/DB/RegionManager.cs b/TShockAPI/DB/RegionManager.cs index 0950f92f..d657980a 100644 --- a/TShockAPI/DB/RegionManager.cs +++ b/TShockAPI/DB/RegionManager.cs @@ -1,4 +1,4 @@ -/* +/* TShock, a server mod for Terraria Copyright (C) 2011-2019 Pryaxis & TShock Contributors @@ -56,10 +56,7 @@ namespace TShockAPI.DB new SqlColumn("Owner", MySqlDbType.VarChar, 50), new SqlColumn("Z", MySqlDbType.Int32){ DefaultValue = "0" } ); - var creator = new SqlTableCreator(db, - db.GetSqlType() == SqlType.Sqlite - ? (IQueryBuilder) new SqliteQueryCreator() - : new MysqlQueryCreator()); + SqlTableCreator creator = new(db, db.GetSqlQueryBuilder()); creator.EnsureTableStructure(table); } @@ -70,49 +67,46 @@ namespace TShockAPI.DB { try { - using (var reader = database.QueryReader("SELECT * FROM Regions WHERE WorldID=@0", Main.worldID.ToString())) + using var reader = database.QueryReader("SELECT * FROM Regions WHERE WorldID=@0", Main.worldID.ToString()); + + Regions.Clear(); + while (reader.Read()) { - Regions.Clear(); - while (reader.Read()) + int id = reader.Get("Id"); + int X1 = reader.Get("X1"); + int Y1 = reader.Get("Y1"); + int height = reader.Get("height"); + int width = reader.Get("width"); + int Protected = reader.Get("Protected"); + string mergedids = reader.Get("UserIds"); + string name = reader.Get("RegionName"); + string owner = reader.Get("Owner"); + string groups = reader.Get("Groups"); + int z = reader.Get("Z"); + + string[] splitids = mergedids.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); + + Region r = new Region(id, new Rectangle(X1, Y1, width, height), name, owner, Protected != 0, Main.worldID.ToString(), z); + r.SetAllowedGroups(groups); + try { - int id = reader.Get("Id"); - int X1 = reader.Get("X1"); - int Y1 = reader.Get("Y1"); - int height = reader.Get("height"); - int width = reader.Get("width"); - int Protected = reader.Get("Protected"); - string mergedids = reader.Get("UserIds"); - string name = reader.Get("RegionName"); - string owner = reader.Get("Owner"); - string groups = reader.Get("Groups"); - int z = reader.Get("Z"); - - string[] splitids = mergedids.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); - - Region r = new Region(id, new Rectangle(X1, Y1, width, height), name, owner, Protected != 0, Main.worldID.ToString(), z); - r.SetAllowedGroups(groups); - try + foreach (string t in splitids) { - for (int i = 0; i < splitids.Length; i++) - { - int userid; - - if (Int32.TryParse(splitids[i], out userid)) // if unparsable, it's not an int, so silently skip - r.AllowedIDs.Add(userid); - else - TShock.Log.Warn(GetString($"One of your UserIDs is not a usable integer: {splitids[i]}")); - } + if (int.TryParse(t, out int userid)) // if unparsable, it's not an int, so silently skip + r.AllowedIDs.Add(userid); + else + TShock.Log.Warn(GetString($"One of your UserIDs is not a usable integer: {t}")); } - catch (Exception e) - { - TShock.Log.Error(GetString("Your database contains invalid UserIDs (they should be integers).")); - TShock.Log.Error(GetString("A lot of things will fail because of this. You must manually delete and re-create the allowed field.")); - TShock.Log.Error(e.ToString()); - TShock.Log.Error(e.StackTrace); - } - - Regions.Add(r); } + catch (Exception e) + { + TShock.Log.Error(GetString("Your database contains invalid UserIDs (they should be integers).")); + TShock.Log.Error(GetString("A lot of things will fail because of this. You must manually delete and re-create the allowed field.")); + TShock.Log.Error(e.ToString()); + TShock.Log.Error(e.StackTrace); + } + + Regions.Add(r); } } catch (Exception ex) @@ -296,10 +290,7 @@ namespace TShockAPI.DB /// X coordinate /// Y coordinate /// Whether any regions exist at the given (x, y) coordinate - public bool InArea(int x, int y) - { - return Regions.Any(r => r.InArea(x, y)); - } + public bool InArea(int x, int y) => Regions.Any(r => r.InArea(x, y)); /// /// Checks if any regions exist at the given (x, y) coordinate @@ -308,10 +299,7 @@ namespace TShockAPI.DB /// X coordinate /// Y coordinate /// The names of any regions that exist at the given (x, y) coordinate - public IEnumerable InAreaRegionName(int x, int y) - { - return Regions.Where(r => r.InArea(x, y)).Select(r => r.Name); - } + public IEnumerable InAreaRegionName(int x, int y) => Regions.Where(r => r.InArea(x, y)).Select(r => r.Name); /// /// Checks if any regions exist at the given (x, y) coordinate @@ -320,10 +308,7 @@ namespace TShockAPI.DB /// X coordinate /// Y coordinate /// The IDs of any regions that exist at the given (x, y) coordinate - public IEnumerable InAreaRegionID(int x, int y) - { - return Regions.Where(r => r.InArea(x, y)).Select(r => r.ID); - } + public IEnumerable InAreaRegionID(int x, int y) => Regions.Where(r => r.InArea(x, y)).Select(r => r.ID); /// /// Checks if any regions exist at the given (x, y) coordinate @@ -332,10 +317,7 @@ namespace TShockAPI.DB /// X coordinate /// Y coordinate /// The objects of any regions that exist at the given (x, y) coordinate - public IEnumerable InAreaRegion(int x, int y) - { - return Regions.Where(r => r.InArea(x, y)); - } + public IEnumerable InAreaRegion(int x, int y) => Regions.Where(r => r.InArea(x, y)); /// /// Changes the size of a given region @@ -414,7 +396,6 @@ namespace TShockAPI.DB /// true if renamed successfully, false otherwise public bool RenameRegion(string oldName, string newName) { - Region region = null; string worldID = Main.worldID.ToString(); bool result = false; @@ -426,7 +407,7 @@ namespace TShockAPI.DB if (q > 0) { - region = Regions.First(r => r.Name == oldName && r.WorldID == worldID); + Region region = Regions.First(r => r.Name == oldName && r.WorldID == worldID); region.Name = newName; Hooks.RegionHooks.OnRegionRenamed(region, oldName, newName); result = true; @@ -523,7 +504,7 @@ namespace TShockAPI.DB { try { - Region region = Regions.First(r => String.Equals(regionName, r.Name, StringComparison.OrdinalIgnoreCase)); + Region region = Regions.First(r => string.Equals(regionName, r.Name, StringComparison.OrdinalIgnoreCase)); region.Area = new Rectangle(x, y, width, height); if (database.Query("UPDATE Regions SET X1 = @0, Y1 = @1, width = @2, height = @3 WHERE RegionName = @4 AND WorldID = @5", @@ -547,11 +528,9 @@ namespace TShockAPI.DB var regions = new List(); try { - using (var reader = database.QueryReader("SELECT RegionName FROM Regions WHERE WorldID=@0", worldid)) - { - while (reader.Read()) - regions.Add(new Region {Name = reader.Get("RegionName")}); - } + using var reader = database.QueryReader("SELECT RegionName FROM Regions WHERE WorldID=@0", worldid); + while (reader.Read()) + regions.Add(new Region {Name = reader.Get("RegionName")}); } catch (Exception ex) { @@ -565,20 +544,14 @@ namespace TShockAPI.DB /// /// Region name /// The region with the given name, or null if not found - public Region GetRegionByName(String name) - { - return Regions.FirstOrDefault(r => r.Name.Equals(name) && r.WorldID == Main.worldID.ToString()); - } + public Region GetRegionByName(string name) => Regions.FirstOrDefault(r => r.Name.Equals(name) && r.WorldID == Main.worldID.ToString()); /// /// Returns a region with the given ID /// /// Region ID /// The region with the given ID, or null if not found - public Region GetRegionByID(int id) - { - return Regions.FirstOrDefault(r => r.ID == id && r.WorldID == Main.worldID.ToString()); - } + public Region GetRegionByID(int id) => Regions.FirstOrDefault(r => r.ID == id && r.WorldID == Main.worldID.ToString()); /// /// Changes the owner of the region with the given name @@ -799,12 +772,12 @@ namespace TShockAPI.DB /// Sets the user IDs which are allowed to use the region /// /// String of IDs to set - public void SetAllowedIDs(String ids) + public void SetAllowedIDs(string ids) { - String[] idArr = ids.Split(','); + string[] idArr = ids.Split(','); List idList = new List(); - foreach (String id in idArr) + foreach (string id in idArr) { int i = 0; if (int.TryParse(id, out i) && i != 0) @@ -819,12 +792,12 @@ namespace TShockAPI.DB /// Sets the group names which are allowed to use the region /// /// String of group names to set - public void SetAllowedGroups(String groups) + public void SetAllowedGroups(string groups) { // prevent null pointer exceptions if (!string.IsNullOrEmpty(groups)) { - List groupList = groups.Split(',').ToList(); + List groupList = groups.Split(',').ToList(); for (int i = 0; i < groupList.Count; i++) { diff --git a/TShockAPI/DB/RememberedPosManager.cs b/TShockAPI/DB/RememberedPosManager.cs index 297877f7..8d2f6986 100644 --- a/TShockAPI/DB/RememberedPosManager.cs +++ b/TShockAPI/DB/RememberedPosManager.cs @@ -40,10 +40,7 @@ namespace TShockAPI.DB new SqlColumn("Y", MySqlDbType.Int32), new SqlColumn("WorldID", MySqlDbType.Text) ); - var creator = new SqlTableCreator(db, - db.GetSqlType() == SqlType.Sqlite - ? (IQueryBuilder) new SqliteQueryCreator() - : new MysqlQueryCreator()); + SqlTableCreator creator = new(db, db.GetSqlQueryBuilder()); creator.EnsureTableStructure(table); } @@ -51,19 +48,17 @@ namespace TShockAPI.DB { try { - using (var reader = database.QueryReader("SELECT * FROM RememberedPos WHERE Name=@0", name)) + using var reader = database.QueryReader("SELECT * FROM RememberedPos WHERE Name=@0", name); + if (reader.Read()) { - if (reader.Read()) - { - int checkX=reader.Get("X"); - int checkY=reader.Get("Y"); - //fix leftover inconsistencies - if (checkX==0) - checkX++; - if (checkY==0) - checkY++; - return new Vector2(checkX, checkY); - } + int checkX=reader.Get("X"); + int checkY=reader.Get("Y"); + //fix leftover inconsistencies + if (checkX==0) + checkX++; + if (checkY==0) + checkY++; + return new Vector2(checkX, checkY); } } catch (Exception ex) @@ -80,12 +75,10 @@ namespace TShockAPI.DB { try { - using (var reader = database.QueryReader("SELECT * FROM RememberedPos WHERE Name=@0 AND IP=@1 AND WorldID=@2", name, IP, Main.worldID.ToString())) + using var reader = database.QueryReader("SELECT * FROM RememberedPos WHERE Name=@0 AND IP=@1 AND WorldID=@2", name, IP, Main.worldID.ToString()); + if (reader.Read()) { - if (reader.Read()) - { - return new Vector2(reader.Get("X"), reader.Get("Y")); - } + return new Vector2(reader.Get("X"), reader.Get("Y")); } } catch (Exception ex) diff --git a/TShockAPI/DB/ResearchDatastore.cs b/TShockAPI/DB/ResearchDatastore.cs index cb51ab18..6f318a91 100644 --- a/TShockAPI/DB/ResearchDatastore.cs +++ b/TShockAPI/DB/ResearchDatastore.cs @@ -41,10 +41,9 @@ namespace TShockAPI.DB new SqlColumn("AmountSacrificed", MySqlDbType.Int32), new SqlColumn("TimeSacrificed", MySqlDbType.DateTime) ); - var creator = new SqlTableCreator(db, - db.GetSqlType() == SqlType.Sqlite - ? (IQueryBuilder)new SqliteQueryCreator() - : new MysqlQueryCreator()); + + SqlTableCreator creator = new(db, db.GetSqlQueryBuilder()); + try { creator.EnsureTableStructure(table); @@ -85,15 +84,14 @@ namespace TShockAPI.DB where WorldId = @0 group by itemId"; - try { - using (var reader = database.QueryReader(sql, Main.worldID)) + try + { + using var reader = database.QueryReader(sql, Main.worldID); + while (reader.Read()) { - while (reader.Read()) - { - var itemId = reader.Get("itemId"); - var amount = reader.Get("totalSacrificed"); - sacrificedItems[itemId] = amount; - } + var itemId = reader.Get("itemId"); + var amount = reader.Get("totalSacrificed"); + sacrificedItems[itemId] = amount; } } catch (Exception ex) diff --git a/TShockAPI/DB/SqlTable.cs b/TShockAPI/DB/SqlTable.cs index 299ac851..6bf7b8a8 100644 --- a/TShockAPI/DB/SqlTable.cs +++ b/TShockAPI/DB/SqlTable.cs @@ -75,31 +75,44 @@ namespace TShockAPI.DB public List GetColumns(SqlTable table) { - var ret = new List(); - var name = database.GetSqlType(); - if (name == SqlType.Sqlite) + List ret = new(); + switch (database.GetSqlType()) { - using (var reader = database.QueryReader("PRAGMA table_info({0})".SFormat(table.Name))) + case SqlType.Sqlite: { + using QueryResult reader = database.QueryReader("PRAGMA table_info({0})".SFormat(table.Name)); while (reader.Read()) + { ret.Add(reader.Get("name")); + } + + break; } - } - else if (name == SqlType.Mysql) - { - using ( - var reader = - database.QueryReader( - "SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME=@0 AND TABLE_SCHEMA=@1", table.Name, - database.Database)) + case SqlType.Mysql: { + using QueryResult reader = + database.QueryReader("SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME=@0 AND TABLE_SCHEMA=@1", table.Name, database.Database); + while (reader.Read()) + { ret.Add(reader.Get("COLUMN_NAME")); + } + + break; } - } - else - { - throw new NotSupportedException(); + case SqlType.Postgres: + { + using QueryResult reader = + database.QueryReader("SELECT column_name FROM information_schema.columns WHERE table_name=@0", table.Name); + + while (reader.Read()) + { + ret.Add(reader.Get("column_name")); + } + + break; + } + default: throw new NotSupportedException(); } return ret; @@ -110,4 +123,4 @@ namespace TShockAPI.DB database.Query(creator.DeleteRow(table, wheres)); } } -} \ No newline at end of file +} diff --git a/TShockAPI/DB/SqlValue.cs b/TShockAPI/DB/SqlValue.cs index 3df5d995..eaeaa106 100644 --- a/TShockAPI/DB/SqlValue.cs +++ b/TShockAPI/DB/SqlValue.cs @@ -59,11 +59,9 @@ namespace TShockAPI.DB { List values = new List(); - using (var reader = database.QueryReader(creator.ReadColumn(table, wheres))) - { - while (reader.Read()) - values.Add(reader.Reader.Get(column)); - } + using var reader = database.QueryReader(creator.ReadColumn(table, wheres)); + while (reader.Read()) + values.Add(reader.Reader.Get(column)); return values; } diff --git a/TShockAPI/DB/TileManager.cs b/TShockAPI/DB/TileManager.cs index 61903186..4a1b5d29 100644 --- a/TShockAPI/DB/TileManager.cs +++ b/TShockAPI/DB/TileManager.cs @@ -39,10 +39,8 @@ namespace TShockAPI.DB new SqlColumn("TileId", MySqlDbType.Int32) { Primary = true }, new SqlColumn("AllowedGroups", MySqlDbType.Text) ); - var creator = new SqlTableCreator(db, - db.GetSqlType() == SqlType.Sqlite - ? (IQueryBuilder)new SqliteQueryCreator() - : new MysqlQueryCreator()); + + SqlTableCreator creator = new(db, db.GetSqlQueryBuilder()); creator.EnsureTableStructure(table); UpdateBans(); } @@ -51,14 +49,12 @@ namespace TShockAPI.DB { TileBans.Clear(); - using (var reader = database.QueryReader("SELECT * FROM TileBans")) + using var reader = database.QueryReader("SELECT * FROM TileBans"); + while (reader != null && reader.Read()) { - while (reader != null && reader.Read()) - { - TileBan ban = new TileBan((short)reader.Get("TileId")); - ban.SetAllowedGroups(reader.Get("AllowedGroups")); - TileBans.Add(ban); - } + TileBan ban = new TileBan((short)reader.Get("TileId")); + ban.SetAllowedGroups(reader.Get("AllowedGroups")); + TileBans.Add(ban); } } @@ -120,7 +116,7 @@ namespace TShockAPI.DB { try { - groupsNew = String.Join(",", b.AllowedGroups); + groupsNew = string.Join(",", b.AllowedGroups); if (groupsNew.Length > 0) groupsNew += ","; groupsNew += name; @@ -230,12 +226,12 @@ namespace TShockAPI.DB // could add in the other permissions in this class instead of a giant if switch. } - public void SetAllowedGroups(String groups) + public void SetAllowedGroups(string groups) { // prevent null pointer exceptions if (!string.IsNullOrEmpty(groups)) { - List groupArr = groups.Split(',').ToList(); + List groupArr = groups.Split(',').ToList(); for (int i = 0; i < groupArr.Count; i++) { @@ -253,7 +249,7 @@ namespace TShockAPI.DB public override string ToString() { - return ID + (AllowedGroups.Count > 0 ? " (" + String.Join(",", AllowedGroups) + ")" : ""); + return ID + (AllowedGroups.Count > 0 ? " (" + string.Join(",", AllowedGroups) + ")" : ""); } } } diff --git a/TShockAPI/DB/UserManager.cs b/TShockAPI/DB/UserManager.cs index a3e050f7..e705e3fc 100644 --- a/TShockAPI/DB/UserManager.cs +++ b/TShockAPI/DB/UserManager.cs @@ -53,10 +53,8 @@ namespace TShockAPI.DB new SqlColumn("LastAccessed", MySqlDbType.Text), new SqlColumn("KnownIPs", MySqlDbType.Text) ); - var creator = new SqlTableCreator(db, - db.GetSqlType() == SqlType.Sqlite - ? (IQueryBuilder) new SqliteQueryCreator() - : new MysqlQueryCreator()); + + SqlTableCreator creator = new(db, db.GetSqlQueryBuilder()); creator.EnsureTableStructure(table); } @@ -241,12 +239,10 @@ namespace TShockAPI.DB { try { - using (var reader = _database.QueryReader("SELECT * FROM Users WHERE Username=@0", username)) + using var reader = _database.QueryReader("SELECT * FROM Users WHERE Username=@0", username); + if (reader.Read()) { - if (reader.Read()) - { - return reader.Get("ID"); - } + return reader.Get("ID"); } } catch (Exception ex) @@ -310,16 +306,14 @@ namespace TShockAPI.DB try { - using (var result = _database.QueryReader(query, arg)) + using var result = _database.QueryReader(query, arg); + if (result.Read()) { - if (result.Read()) - { - account = LoadUserAccountFromResult(account, result); - // Check for multiple matches - if (!result.Read()) - return account; - multiple = true; - } + account = LoadUserAccountFromResult(account, result); + // Check for multiple matches + if (!result.Read()) + return account; + multiple = true; } } catch (Exception ex) @@ -339,14 +333,12 @@ namespace TShockAPI.DB try { List accounts = new List(); - using (var reader = _database.QueryReader("SELECT * FROM Users")) + using var reader = _database.QueryReader("SELECT * FROM Users"); + while (reader.Read()) { - while (reader.Read()) - { - accounts.Add(LoadUserAccountFromResult(new UserAccount(), reader)); - } - return accounts; + accounts.Add(LoadUserAccountFromResult(new UserAccount(), reader)); } + return accounts; } catch (Exception ex) { @@ -367,14 +359,13 @@ namespace TShockAPI.DB { List accounts = new List(); string search = notAtStart ? string.Format("%{0}%", username) : string.Format("{0}%", username); - using (var reader = _database.QueryReader("SELECT * FROM Users WHERE Username LIKE @0", - search)) + using var reader = _database.QueryReader("SELECT * FROM Users WHERE Username LIKE @0", + search); + while (reader.Read()) { - while (reader.Read()) - { - accounts.Add(LoadUserAccountFromResult(new UserAccount(), reader)); - } + accounts.Add(LoadUserAccountFromResult(new UserAccount(), reader)); } + return accounts; } catch (Exception ex) @@ -497,7 +488,7 @@ namespace TShockAPI.DB int currentWorkFactor; try { - currentWorkFactor = Int32.Parse((Password.Split('$')[2])); + currentWorkFactor = int.Parse((Password.Split('$')[2])); } catch (FormatException) { diff --git a/TShockAPI/DB/WarpsManager.cs b/TShockAPI/DB/WarpsManager.cs index f3287441..06723281 100644 --- a/TShockAPI/DB/WarpsManager.cs +++ b/TShockAPI/DB/WarpsManager.cs @@ -49,10 +49,8 @@ namespace TShockAPI.DB new SqlColumn("WorldID", MySqlDbType.VarChar, 50) { Unique = true }, new SqlColumn("Private", MySqlDbType.Text) ); - var creator = new SqlTableCreator(db, - db.GetSqlType() == SqlType.Sqlite - ? (IQueryBuilder) new SqliteQueryCreator() - : new MysqlQueryCreator()); + + SqlTableCreator creator = new(db, db.GetSqlQueryBuilder()); creator.EnsureTableStructure(table); } @@ -88,16 +86,14 @@ namespace TShockAPI.DB { Warps.Clear(); - using (var reader = database.QueryReader("SELECT * FROM Warps WHERE WorldID = @0", - Main.worldID.ToString())) + using var reader = database.QueryReader("SELECT * FROM Warps WHERE WorldID = @0", + Main.worldID.ToString()); + while (reader.Read()) { - while (reader.Read()) - { - Warps.Add(new Warp( - new Point(reader.Get("X"), reader.Get("Y")), - reader.Get("WarpName"), - (reader.Get("Private") ?? "0") != "0")); - } + Warps.Add(new Warp( + new Point(reader.Get("X"), reader.Get("Y")), + reader.Get("WarpName"), + (reader.Get("Private") ?? "0") != "0")); } } @@ -113,7 +109,7 @@ namespace TShockAPI.DB if (database.Query("DELETE FROM Warps WHERE WarpName = @0 AND WorldID = @1", warpName, Main.worldID.ToString()) > 0) { - Warps.RemoveAll(w => String.Equals(w.Name, warpName, StringComparison.OrdinalIgnoreCase)); + Warps.RemoveAll(w => string.Equals(w.Name, warpName, StringComparison.OrdinalIgnoreCase)); return true; } } @@ -131,7 +127,7 @@ namespace TShockAPI.DB /// The warp, if it exists, or else null. public Warp Find(string warpName) { - return Warps.FirstOrDefault(w => String.Equals(w.Name, warpName, StringComparison.OrdinalIgnoreCase)); + return Warps.FirstOrDefault(w => string.Equals(w.Name, warpName, StringComparison.OrdinalIgnoreCase)); } /// @@ -148,7 +144,7 @@ namespace TShockAPI.DB if (database.Query("UPDATE Warps SET X = @0, Y = @1 WHERE WarpName = @2 AND WorldID = @3", x, y, warpName, Main.worldID.ToString()) > 0) { - Warps.Find(w => String.Equals(w.Name, warpName, StringComparison.OrdinalIgnoreCase)).Position = new Point(x, y); + Warps.Find(w => string.Equals(w.Name, warpName, StringComparison.OrdinalIgnoreCase)).Position = new Point(x, y); return true; } } @@ -172,7 +168,7 @@ namespace TShockAPI.DB if (database.Query("UPDATE Warps SET Private = @0 WHERE WarpName = @1 AND WorldID = @2", state ? "1" : "0", warpName, Main.worldID.ToString()) > 0) { - Warps.Find(w => String.Equals(w.Name, warpName, StringComparison.OrdinalIgnoreCase)).IsPrivate = state; + Warps.Find(w => string.Equals(w.Name, warpName, StringComparison.OrdinalIgnoreCase)).IsPrivate = state; return true; } } diff --git a/TShockAPI/Extensions/DbExt.cs b/TShockAPI/Extensions/DbExt.cs index c6aa0ab9..d58a966e 100644 --- a/TShockAPI/Extensions/DbExt.cs +++ b/TShockAPI/Extensions/DbExt.cs @@ -23,6 +23,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Data.Sqlite; using MySql.Data.MySqlClient; using Npgsql; +using TShockAPI.DB.Queries; namespace TShockAPI.DB { @@ -154,6 +155,14 @@ namespace TShockAPI.DB _ => SqlType.Unknown }; + public static IQueryBuilder GetSqlQueryBuilder(this IDbConnection db) => db.GetSqlType() switch + { + SqlType.Sqlite => new SqliteQueryCreator(), + SqlType.Mysql => new MysqlQueryCreator(), + SqlType.Postgres => new PostgresQueryCreator(), + _ => throw new NotSupportedException("Database type not supported.") + }; + private static readonly Dictionary> ReadFuncs = new Dictionary > { diff --git a/TShockAPI/SqlLog.cs b/TShockAPI/SqlLog.cs index 2e5a55e3..5fd9a2bc 100644 --- a/TShockAPI/SqlLog.cs +++ b/TShockAPI/SqlLog.cs @@ -62,7 +62,7 @@ namespace TShockAPI /// public SqlLog(IDbConnection db, string textlogFilepath, bool clearTextLog) { - FileName = string.Format("{0}://database", db.GetSqlType()); + FileName = $"{db.GetSqlType()}://database"; _database = db; _backupLog = new TextLog(textlogFilepath, clearTextLog); @@ -74,10 +74,7 @@ namespace TShockAPI new SqlColumn("Message", MySqlDbType.Text) ); - var creator = new SqlTableCreator(db, - db.GetSqlType() == SqlType.Sqlite - ? (IQueryBuilder) new SqliteQueryCreator() - : new MysqlQueryCreator()); + SqlTableCreator creator = new(db, db.GetSqlQueryBuilder()); creator.EnsureTableStructure(table); } From 69b98980f156da4bebccde3aef36d96a23eb59fa Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Mon, 28 Apr 2025 15:51:16 +0200 Subject: [PATCH 21/35] feat(db): add Postgres support to configuration Extends database configuration to support Postgres in addition to existing SQLite and MySQL options. Includes new settings for Postgres host, database name, username, and password. Implements a connection builder for Postgres, ensuring proper error handling when connecting. Updates dependency to include Npgsql for Postgres connectivity. --- TShockAPI/Configuration/TShockConfig.cs | 18 ++++++++++++++++- TShockAPI/DB/DbBuilder.cs | 26 +++++++++++++++++++++++++ TShockLauncher/TShockLauncher.csproj | 3 ++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/TShockAPI/Configuration/TShockConfig.cs b/TShockAPI/Configuration/TShockConfig.cs index c91466fe..0a9e1a1a 100644 --- a/TShockAPI/Configuration/TShockConfig.cs +++ b/TShockAPI/Configuration/TShockConfig.cs @@ -529,7 +529,7 @@ namespace TShockAPI.Configuration #region MySQL Settings /// The type of database to use when storing data (either "sqlite" or "mysql"). - [Description("The type of database to use when storing data (either \"sqlite\" or \"mysql\").")] + [Description("The type of database to use when storing data (either \"sqlite\", \"mysql\" or \"postgres\").")] public string StorageType = "sqlite"; /// The path of sqlite db. @@ -552,6 +552,22 @@ namespace TShockAPI.Configuration [Description("The password used when connecting to a MySQL database.")] public string MySqlPassword = ""; + ///The Postgres hostname and port to direct connections to. + [Description("The Postgres hostname and port to direct connections to.")] + public string PostgresHost = ""; + + /// The database name to connect to when using Postgres as the database type. + [Description("The database name to connect to when using Postgres as the database type.")] + public string PostgresDbName = ""; + + /// The username used when connecting to a Postgres database. + [Description("The username used when connecting to a Postgres database.")] + public string PostgresUsername = ""; + + /// The password used when connecting to a Postgres database. + [Description("The password used when connecting to a Postgres database.")] + public string PostgresPassword = ""; + /// Whether or not to save logs to the SQL database instead of a text file. [Description("Whether or not to save logs to the SQL database instead of a text file.\nDefault = false.")] public bool UseSqlLogs = false; diff --git a/TShockAPI/DB/DbBuilder.cs b/TShockAPI/DB/DbBuilder.cs index d02f993b..dc6e20df 100644 --- a/TShockAPI/DB/DbBuilder.cs +++ b/TShockAPI/DB/DbBuilder.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.IO; using Microsoft.Data.Sqlite; using MySql.Data.MySqlClient; +using Npgsql; using TerrariaApi.Server; using TShockAPI.Configuration; @@ -45,6 +46,7 @@ public sealed class DbBuilder { "sqlite" => BuildSqliteConnection(), "mysql" => BuildMySqlConnection(), + "postgres" => BuildPostgresConnection(), _ => throw new("Invalid storage type") }; } @@ -86,4 +88,28 @@ public sealed class DbBuilder throw new("MySql not setup correctly", e); } } + + private NpgsqlConnection BuildPostgresConnection() + { + try + { + string[] hostport = _config.Settings.PostgresHost.Split(':'); + + NpgsqlConnectionStringBuilder connStrBuilder = new() + { + Host = hostport[0], + Port = hostport.Length > 1 ? int.Parse(hostport[1]) : 5432, + Database = _config.Settings.PostgresDbName, + Username = _config.Settings.PostgresUsername, + Password = _config.Settings.PostgresPassword + }; + + return new(connStrBuilder.ToString()); + } + catch (NpgsqlException e) + { + ServerApi.LogWriter.PluginWriteLine(_caller, e.ToString(), TraceLevel.Error); + throw new("Postgres not setup correctly", e); + } + } } diff --git a/TShockLauncher/TShockLauncher.csproj b/TShockLauncher/TShockLauncher.csproj index fbe428bb..3c04ae52 100644 --- a/TShockLauncher/TShockLauncher.csproj +++ b/TShockLauncher/TShockLauncher.csproj @@ -33,7 +33,8 @@ - + + From 2d839e36090b3103fb63ca01d60e4fc4d849a00e Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Tue, 29 Apr 2025 00:47:19 +0200 Subject: [PATCH 22/35] fix(db/postgres): Resolve SQL identifier casing issues Improves SQL query execution by ensuring proper casing for identifiers across various database types, particularly for Postgres. Enhances security and compatibility by using an identifier escaping method, preventing potential errors due to case sensitivity in SQL queries. Addresses potential issues with existing queries for better reliability and consistency. --- TShockAPI/DB/BanManager.cs | 27 +++++++++-------- TShockAPI/DB/CharacterManager.cs | 2 +- TShockAPI/DB/GroupManager.cs | 24 ++++++++++----- TShockAPI/DB/Queries/PostgresQueryCreator.cs | 4 +-- TShockAPI/DB/RegionManager.cs | 17 +++++++---- TShockAPI/DB/SqlTable.cs | 9 ++++-- TShockAPI/DB/UserManager.cs | 12 ++++---- TShockAPI/DB/WarpsManager.cs | 2 +- TShockAPI/Extensions/DbExt.cs | 31 ++++++++++++++------ 9 files changed, 81 insertions(+), 47 deletions(-) diff --git a/TShockAPI/DB/BanManager.cs b/TShockAPI/DB/BanManager.cs index c56e3d0d..2dfb6322 100644 --- a/TShockAPI/DB/BanManager.cs +++ b/TShockAPI/DB/BanManager.cs @@ -78,7 +78,7 @@ namespace TShockAPI.DB } catch (DllNotFoundException) { - System.Console.WriteLine(GetString("Possible problem with your database - is Sqlite3.dll present?")); + Console.WriteLine(GetString("Possible problem with your database - is Sqlite3.dll present?")); throw new Exception(GetString("Could not find a database library (probably Sqlite3.dll)")); } @@ -355,7 +355,9 @@ namespace TShockAPI.DB return Bans[id]; } - using var reader = database.QueryReader("SELECT * FROM PlayerBans WHERE TicketNumber=@0", id); + string query = $"SELECT * FROM PlayerBans WHERE {"TicketNumber".EscapeSqlId(database)}=@0"; + + using var reader = database.QueryReader(query, id); if (reader.Read()) { @@ -380,10 +382,11 @@ namespace TShockAPI.DB /// public IEnumerable RetrieveBansByIdentifier(string identifier, bool currentOnly = true) { - string query = "SELECT * FROM PlayerBans WHERE Identifier=@0"; + string query = $"SELECT * FROM PlayerBans WHERE {"Identifier".EscapeSqlId(database)}=@0"; + if (currentOnly) { - query += $" AND Expiration > {DateTime.UtcNow.Ticks}"; + query += $" AND {"Expiration".EscapeSqlId(database)} > {DateTime.UtcNow.Ticks}"; } using var reader = database.QueryReader(query, identifier); @@ -412,11 +415,11 @@ namespace TShockAPI.DB //Generate a sequence of '@0, @1, @2, ... etc' var parameters = string.Join(", ", Enumerable.Range(0, identifiers.Length).Select(p => $"@{p}")); - string query = $"SELECT * FROM PlayerBans WHERE Identifier IN ({parameters})"; + string query = $"SELECT * FROM PlayerBans WHERE {"Identifier".EscapeSqlId(database)} IN ({parameters})"; if (currentOnly) { - query += $" AND Expiration > {DateTime.UtcNow.Ticks}"; + query += $" AND {"Expiration".EscapeSqlId(database)} > {DateTime.UtcNow.Ticks}"; } using var reader = database.QueryReader(query, identifiers); @@ -449,7 +452,7 @@ namespace TShockAPI.DB List banlist = new List(); try { - using var reader = database.QueryReader($"SELECT * FROM PlayerBans ORDER BY {SortToOrderByMap[sortMethod]}"); + using var reader = database.QueryReader($"SELECT * FROM PlayerBans ORDER BY {SortToOrderByMap(sortMethod)}"); while (reader.Read()) { @@ -490,12 +493,12 @@ namespace TShockAPI.DB return false; } - private readonly Dictionary SortToOrderByMap = new() + private string SortToOrderByMap(BanSortMethod sortMethod) => sortMethod switch { - { BanSortMethod.AddedNewestToOldest, "Date DESC" }, - { BanSortMethod.AddedOldestToNewest, "Date ASC" }, - { BanSortMethod.ExpirationSoonestToLatest, "Expiration ASC" }, - { BanSortMethod.ExpirationLatestToSoonest, "Expiration DESC" } + BanSortMethod.AddedNewestToOldest => $"{"Date".EscapeSqlId(database)} DESC", + BanSortMethod.AddedOldestToNewest => $"{"Date".EscapeSqlId(database)} ASC", + BanSortMethod.ExpirationSoonestToLatest => $"{"Expiration".EscapeSqlId(database)} ASC", + BanSortMethod.ExpirationLatestToSoonest => $"{"Expiration".EscapeSqlId(database)} DESC" }; } diff --git a/TShockAPI/DB/CharacterManager.cs b/TShockAPI/DB/CharacterManager.cs index 3b9890a3..08d6d1ec 100644 --- a/TShockAPI/DB/CharacterManager.cs +++ b/TShockAPI/DB/CharacterManager.cs @@ -82,7 +82,7 @@ namespace TShockAPI.DB try { - using var reader = database.QueryReader("SELECT * FROM tsCharacter WHERE Account=@0", acctid); + using var reader = database.QueryReader($"SELECT * FROM tsCharacter WHERE {"Account".EscapeSqlId(database)}=@0", acctid); if (reader.Read()) { playerData.exists = true; diff --git a/TShockAPI/DB/GroupManager.cs b/TShockAPI/DB/GroupManager.cs index 553a1a33..ba82225a 100644 --- a/TShockAPI/DB/GroupManager.cs +++ b/TShockAPI/DB/GroupManager.cs @@ -314,15 +314,22 @@ namespace TShockAPI.DB group.Parent = parent; } - string query = (TShock.Config.Settings.StorageType.ToLower() == "sqlite") - ? "INSERT OR IGNORE INTO GroupList (GroupName, Parent, Commands, ChatColor) VALUES (@0, @1, @2, @3);" - : "INSERT IGNORE INTO GroupList SET GroupName=@0, Parent=@1, Commands=@2, ChatColor=@3"; - if (database.Query(query, name, parentname, permissions, chatcolor) == 1) + string query = database.GetSqlType() switch + { + SqlType.Sqlite => "INSERT OR IGNORE INTO GroupList (GroupName, Parent, Commands, ChatColor) VALUES (@0, @1, @2, @3);", + SqlType.Mysql => "INSERT IGNORE INTO GroupList SET GroupName=@0, Parent=@1, Commands=@2, ChatColor=@3", + SqlType.Postgres => "INSERT INTO GroupList (\"GroupName\", \"Parent\", \"Commands\", \"ChatColor\") VALUES (@0, @1, @2, @3) ON CONFLICT (\"GroupName\") DO NOTHING", + _ => throw new NotSupportedException(GetString("Unsupported database type.")) + }; + + if (database.Query(query, name, parentname, permissions, chatcolor) is 1) { groups.Add(group); } else + { throw new GroupManagerException(GetString($"Failed to add group {name}.")); + } } /// @@ -362,9 +369,12 @@ namespace TShockAPI.DB } // Ensure any group validation is also persisted to the DB. - var newGroup = new Group(name, parent, chatcolor, permissions); - newGroup.Prefix = prefix; - newGroup.Suffix = suffix; + var newGroup = new Group(name, parent, chatcolor, permissions) + { + Prefix = prefix, + Suffix = suffix + }; + string query = "UPDATE GroupList SET Parent=@0, Commands=@1, ChatColor=@2, Suffix=@3, Prefix=@4 WHERE GroupName=@5"; if (database.Query(query, parentname, newGroup.Permissions, newGroup.ChatColor, suffix, prefix, name) != 1) throw new GroupManagerException(GetString($"Failed to update group \"{name}\".")); diff --git a/TShockAPI/DB/Queries/PostgresQueryCreator.cs b/TShockAPI/DB/Queries/PostgresQueryCreator.cs index 779e38b0..5637d689 100644 --- a/TShockAPI/DB/Queries/PostgresQueryCreator.cs +++ b/TShockAPI/DB/Queries/PostgresQueryCreator.cs @@ -79,9 +79,7 @@ public class PostgresQueryCreator : GenericQueryCreator .Where(c => c.Unique).Select(c => $"\"{c.Name}\"") .ToArray(); // No re-enumeration - return "CREATE TABLE {0} ({1} {2})".SFormat(EscapeTableName(table.Name), - string.Join(", ", columns), - uniques.Any() ? ", UNIQUE({0})".SFormat(string.Join(", ", uniques)) : ""); + return $"CREATE TABLE {EscapeTableName(table.Name)} ({string.Join(", ", columns)} {(uniques.Any() ? ", UNIQUE({0})".SFormat(string.Join(", ", uniques)) : "")})"; } /// diff --git a/TShockAPI/DB/RegionManager.cs b/TShockAPI/DB/RegionManager.cs index d657980a..cc7eacdb 100644 --- a/TShockAPI/DB/RegionManager.cs +++ b/TShockAPI/DB/RegionManager.cs @@ -67,9 +67,9 @@ namespace TShockAPI.DB { try { - using var reader = database.QueryReader("SELECT * FROM Regions WHERE WorldID=@0", Main.worldID.ToString()); - + using var reader = database.QueryReader($"SELECT * FROM Regions WHERE {"WorldID".EscapeSqlId(database)}=@0", Main.worldID.ToString()); Regions.Clear(); + while (reader.Read()) { int id = reader.Get("Id"); @@ -135,10 +135,17 @@ namespace TShockAPI.DB } try { - database.Query( - "INSERT INTO Regions (X1, Y1, width, height, RegionName, WorldID, UserIds, Protected, `Groups`, Owner, Z) VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8, @9, @10);", - tx, ty, width, height, regionname, worldid, "", 1, "", owner, z); + string query = database.GetSqlType() switch + { + SqlType.Postgres => "INSERT INTO Regions (\"X1\", \"Y1\", \"width\", \"height\", \"RegionName\", \"WorldID\", \"UserIds\", \"Protected\", \"Groups\", \"Owner\", \"Z\") VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8, @9, @10);", + _ => "INSERT INTO Regions (X1, Y1, width, height, RegionName, WorldID, UserIds, Protected, Groups, Owner, Z) VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8, @9, @10);", + + }; + + database.Query(query, tx, ty, width, height, regionname, worldid, "", 1, "", owner, z); + int id; + using (QueryResult res = database.QueryReader("SELECT Id FROM Regions WHERE RegionName = @0 AND WorldID = @1", regionname, worldid)) { if (res.Read()) diff --git a/TShockAPI/DB/SqlTable.cs b/TShockAPI/DB/SqlTable.cs index 6bf7b8a8..ce955dbf 100644 --- a/TShockAPI/DB/SqlTable.cs +++ b/TShockAPI/DB/SqlTable.cs @@ -59,7 +59,9 @@ namespace TShockAPI.DB var columns = GetColumns(table); if (columns.Count > 0) { - if (!table.Columns.All(c => columns.Contains(c.Name)) || !columns.All(c => table.Columns.Any(c2 => c2.Name == c))) + // Use OrdinalIgnoreCase to account for pgsql automatically lowering cases. + if (!table.Columns.All(c => columns.Contains(c.Name, StringComparer.OrdinalIgnoreCase)) + || !columns.All(c => table.Columns.Any(c2 => c2.Name.Equals(c, StringComparison.OrdinalIgnoreCase)))) { var from = new SqlTable(table.Name, columns.Select(s => new SqlColumn(s, MySqlDbType.String)).ToList()); database.Query(creator.AlterTable(from, table)); @@ -70,6 +72,7 @@ namespace TShockAPI.DB database.Query(creator.CreateTable(table)); return true; } + return false; } @@ -102,8 +105,8 @@ namespace TShockAPI.DB } case SqlType.Postgres: { - using QueryResult reader = - database.QueryReader("SELECT column_name FROM information_schema.columns WHERE table_name=@0", table.Name); + // HACK: Using "ilike" op to ignore case, due to weird case issues adapting for pgsql + using QueryResult reader = database.QueryReader("SELECT column_name FROM information_schema.columns WHERE table_name ILIKE @0", table.Name); while (reader.Read()) { diff --git a/TShockAPI/DB/UserManager.cs b/TShockAPI/DB/UserManager.cs index e705e3fc..47a1c15f 100644 --- a/TShockAPI/DB/UserManager.cs +++ b/TShockAPI/DB/UserManager.cs @@ -239,7 +239,7 @@ namespace TShockAPI.DB { try { - using var reader = _database.QueryReader("SELECT * FROM Users WHERE Username=@0", username); + using var reader = _database.QueryReader($"SELECT * FROM Users WHERE {"Username".EscapeSqlId(_database)}=@0", username); if (reader.Read()) { return reader.Get("ID"); @@ -293,13 +293,13 @@ namespace TShockAPI.DB object arg; if (account.ID != 0) { - query = "SELECT * FROM Users WHERE ID=@0"; + query = $"SELECT * FROM Users WHERE {"ID".EscapeSqlId(_database)}=@0"; arg = account.ID; type = "id"; } else { - query = "SELECT * FROM Users WHERE Username=@0"; + query = $"SELECT * FROM Users WHERE {"Username".EscapeSqlId(_database)}=@0"; arg = account.Name; type = "name"; } @@ -358,9 +358,9 @@ namespace TShockAPI.DB try { List accounts = new List(); - string search = notAtStart ? string.Format("%{0}%", username) : string.Format("{0}%", username); - using var reader = _database.QueryReader("SELECT * FROM Users WHERE Username LIKE @0", - search); + string search = $"{(notAtStart ? "%" : "")}{username}%"; + using var reader = _database.QueryReader($"SELECT * FROM Users WHERE {"Username".EscapeSqlId(_database)} LIKE @0", search); + while (reader.Read()) { accounts.Add(LoadUserAccountFromResult(new UserAccount(), reader)); diff --git a/TShockAPI/DB/WarpsManager.cs b/TShockAPI/DB/WarpsManager.cs index 06723281..fb28f1a4 100644 --- a/TShockAPI/DB/WarpsManager.cs +++ b/TShockAPI/DB/WarpsManager.cs @@ -86,7 +86,7 @@ namespace TShockAPI.DB { Warps.Clear(); - using var reader = database.QueryReader("SELECT * FROM Warps WHERE WorldID = @0", + using var reader = database.QueryReader($"SELECT * FROM Warps WHERE {"WorldID".EscapeSqlId(database)} = @0", Main.worldID.ToString()); while (reader.Read()) { diff --git a/TShockAPI/Extensions/DbExt.cs b/TShockAPI/Extensions/DbExt.cs index d58a966e..55a681dd 100644 --- a/TShockAPI/Extensions/DbExt.cs +++ b/TShockAPI/Extensions/DbExt.cs @@ -20,6 +20,7 @@ using System; using System.Collections.Generic; using System.Data; using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; using Microsoft.Data.Sqlite; using MySql.Data.MySqlClient; using Npgsql; @@ -42,17 +43,17 @@ namespace TShockAPI.DB [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")] public static int Query(this IDbConnection olddb, string query, params object[] args) { - using (var db = olddb.CloneEx()) + using IDbConnection db = olddb.CloneEx(); + db.Open(); + using IDbCommand com = db.CreateCommand(); + com.CommandText = query; + + for (int i = 0; i < args.Length; i++) { - db.Open(); - using (var com = db.CreateCommand()) - { - com.CommandText = query; - for (int i = 0; i < args.Length; i++) - com.AddParameter("@" + i, args[i] ?? DBNull.Value); - return com.ExecuteNonQuery(); - } + com.AddParameter("@" + i, args[i] ?? DBNull.Value); } + + return com.ExecuteNonQuery(); } /// @@ -271,6 +272,18 @@ namespace TShockAPI.DB return (T)reader.GetValue(column); } + + /// + /// Escapes an identifier for use in a SQL query. + /// + /// The identifier to escape, typically a table or column name. + /// The escaped identifier. + [Pure] + public static string EscapeSqlId(this string id, IDbConnection db) => db.GetSqlType() switch + { + SqlType.Postgres => $"\"{id}\"", // The main PITA and culprit + _ => id // Default case for agnostic SQL + }; } public enum SqlType From de602a91d438ce32ed6cd3f8bf9c0669940d1cc9 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Tue, 29 Apr 2025 01:38:32 +0200 Subject: [PATCH 23/35] fix(db): Correct casing and escaping in DB queries Updates the database queries to handle casing inconsistencies and improves SQL query parameter escaping for better security and compatibility. Refactors group existence checks for simplicity, enhancing readability and maintainability. Addresses issues related to unique constraints in user registration by improving error handling for duplicate usernames. --- TShockAPI/DB/GroupManager.cs | 19 +++----------- TShockAPI/DB/UserManager.cs | 48 ++++++++++++++++++++++-------------- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/TShockAPI/DB/GroupManager.cs b/TShockAPI/DB/GroupManager.cs index ba82225a..f655b987 100644 --- a/TShockAPI/DB/GroupManager.cs +++ b/TShockAPI/DB/GroupManager.cs @@ -253,13 +253,7 @@ namespace TShockAPI.DB /// /// The group. /// true if it does; otherwise, false. - public bool GroupExists(string group) - { - if (group == "superadmin") - return true; - - return groups.Any(g => g.Name.Equals(group)); - } + public bool GroupExists(string group) => group is "superadmin" || groups.Any(g => g.Name.Equals(group)); IEnumerator IEnumerable.GetEnumerator() { @@ -270,21 +264,14 @@ namespace TShockAPI.DB /// Gets the enumerator. /// /// The enumerator. - public IEnumerator GetEnumerator() - { - return groups.GetEnumerator(); - } + public IEnumerator GetEnumerator() => groups.GetEnumerator(); /// /// Gets the group matching the specified name. /// /// The name. /// The group. - public Group GetGroupByName(string name) - { - var ret = groups.Where(g => g.Name == name); - return 1 == ret.Count() ? ret.ElementAt(0) : null; - } + public Group GetGroupByName(string name) => groups.FirstOrDefault(g => g.Name == name); /// /// Adds group with name and permissions if it does not exist. diff --git a/TShockAPI/DB/UserManager.cs b/TShockAPI/DB/UserManager.cs index 47a1c15f..e5077ce1 100644 --- a/TShockAPI/DB/UserManager.cs +++ b/TShockAPI/DB/UserManager.cs @@ -70,15 +70,22 @@ namespace TShockAPI.DB int ret; try { - ret = _database.Query("INSERT INTO Users (Username, Password, UUID, UserGroup, Registered) VALUES (@0, @1, @2, @3, @4);", account.Name, - account.Password, account.UUID, account.Group, DateTime.UtcNow.ToString("s")); + string query = _database.GetSqlType() switch + { + SqlType.Postgres => "INSERT INTO Users (\"Username\", \"Password\", \"UUID\", \"Usergroup\", \"Registered\") VALUES (@0, @1, @2, @3, @4);", + _ => "INSERT INTO Users (Username, Password, UUID, Usergroup, Registered) VALUES (@0, @1, @2, @3, @4);" + }; + + ret = _database.Query(query, account.Name, account.Password, account.UUID, account.Group, DateTime.UtcNow.ToString("s")); } - catch (Exception ex) + // Detect duplicate user using a regexp as Sqlite doesn't have well structured exceptions + catch (Exception e) when (Regex.IsMatch(e.Message, "Username.*not unique|UNIQUE constraint failed: Users\\.Username")) { - // Detect duplicate user using a regexp as Sqlite doesn't have well structured exceptions - if (Regex.IsMatch(ex.Message, "Username.*not unique|UNIQUE constraint failed: Users\\.Username")) - throw new UserAccountExistsException(account.Name); - throw new UserAccountManagerException(GetString($"AddUser SQL returned an error ({ex.Message})"), ex); + throw new UserAccountExistsException(account.Name); + } + catch (Exception e) + { + throw new UserAccountManagerException(GetString($"AddUser SQL returned an error ({e.Message})"), e); } if (1 > ret) @@ -99,7 +106,7 @@ namespace TShockAPI.DB TShock.Players.Where(p => p?.IsLoggedIn == true && p.Account.Name == account.Name).ForEach(p => p.Logout()); UserAccount tempuser = GetUserAccount(account); - int affected = _database.Query("DELETE FROM Users WHERE Username=@0", account.Name); + int affected = _database.Query($"DELETE FROM Users WHERE {"Username".EscapeSqlId(_database)}=@0", account.Name); if (affected < 1) throw new UserAccountNotExistException(account.Name); @@ -124,10 +131,11 @@ namespace TShockAPI.DB { account.CreateBCryptHash(password); - if ( - _database.Query("UPDATE Users SET Password = @0 WHERE Username = @1;", account.Password, - account.Name) == 0) + if (_database.Query($"UPDATE Users SET {"Password".EscapeSqlId(_database)} = @0 WHERE {"Username".EscapeSqlId(_database)} = @1;", + account.Password, account.Name) is 0) + { throw new UserAccountNotExistException(account.Name); + } } catch (Exception ex) { @@ -144,10 +152,10 @@ namespace TShockAPI.DB { try { - if ( - _database.Query("UPDATE Users SET UUID = @0 WHERE Username = @1;", uuid, - account.Name) == 0) + if (_database.Query(/*lang=sql*/$"UPDATE Users SET {"UUID".EscapeSqlId(_database)} = @0 WHERE {"Username".EscapeSqlId(_database)} = @1;", uuid, account.Name) is 0) + { throw new UserAccountNotExistException(account.Name); + } } catch (Exception ex) { @@ -169,7 +177,7 @@ namespace TShockAPI.DB if (AccountHooks.OnAccountGroupUpdate(account, ref grp)) throw new UserGroupUpdateLockedException(account.Name); - if (_database.Query("UPDATE Users SET UserGroup = @0 WHERE Username = @1;", grp.Name, account.Name) == 0) + if (_database.Query($"UPDATE Users SET {"UserGroup".EscapeSqlId(_database)} = @0 WHERE {"Username".EscapeSqlId(_database)} = @1;", grp.Name, account.Name) == 0) throw new UserAccountNotExistException(account.Name); try @@ -200,7 +208,7 @@ namespace TShockAPI.DB if (AccountHooks.OnAccountGroupUpdate(account, author, ref grp)) throw new UserGroupUpdateLockedException(account.Name); - if (_database.Query("UPDATE Users SET UserGroup = @0 WHERE Username = @1;", grp.Name, account.Name) == 0) + if (_database.Query($"UPDATE Users SET {"UserGroup".EscapeSqlId(_database)} = @0 WHERE {"Username".EscapeSqlId(_database)} = @1;", grp.Name, account.Name) == 0) throw new UserAccountNotExistException(account.Name); try @@ -223,8 +231,12 @@ namespace TShockAPI.DB { try { - if (_database.Query("UPDATE Users SET LastAccessed = @0, KnownIps = @1 WHERE Username = @2;", DateTime.UtcNow.ToString("s"), account.KnownIps, account.Name) == 0) + if (_database.Query( + $"UPDATE Users SET {"LastAccessed".EscapeSqlId(_database)} = @0, {"KnownIPs".EscapeSqlId(_database)} = @1 WHERE {"Username".EscapeSqlId(_database)} = @2;", + DateTime.UtcNow.ToString("s"), account.KnownIps, account.Name) is 0 + ) { throw new UserAccountNotExistException(account.Name); + } } catch (Exception ex) { @@ -388,7 +400,7 @@ namespace TShockAPI.DB account.Name = result.Get("Username"); account.Registered = result.Get("Registered"); account.LastAccessed = result.Get("LastAccessed"); - account.KnownIps = result.Get("KnownIps"); + account.KnownIps = result.Get("KnownIPs"); return account; } } From 4c13084eb38c8b9fd2b45d7d1583cd773af86d6a Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Tue, 29 Apr 2025 02:05:51 +0200 Subject: [PATCH 24/35] fix(db): Fix casing for regions and warps --- TShockAPI/DB/RegionManager.cs | 10 ++++++---- TShockAPI/DB/WarpsManager.cs | 16 +++++++++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/TShockAPI/DB/RegionManager.cs b/TShockAPI/DB/RegionManager.cs index cc7eacdb..27acd7a6 100644 --- a/TShockAPI/DB/RegionManager.cs +++ b/TShockAPI/DB/RegionManager.cs @@ -146,7 +146,8 @@ namespace TShockAPI.DB int id; - using (QueryResult res = database.QueryReader("SELECT Id FROM Regions WHERE RegionName = @0 AND WorldID = @1", regionname, worldid)) + using (QueryResult res = database.QueryReader( + $"SELECT {"Id".EscapeSqlId(database)} FROM Regions WHERE {"RegionName".EscapeSqlId(database)} = @0 AND {"WorldID".EscapeSqlId(database)} = @1", regionname, worldid)) { if (res.Read()) { @@ -157,6 +158,7 @@ namespace TShockAPI.DB return false; } } + Region region = new Region(id, new Rectangle(tx, ty, width, height), regionname, owner, true, worldid, z); Regions.Add(region); Hooks.RegionHooks.OnRegionCreated(region); @@ -178,7 +180,7 @@ namespace TShockAPI.DB { try { - database.Query("DELETE FROM Regions WHERE Id=@0 AND WorldID=@1", id, Main.worldID.ToString()); + database.Query($"DELETE FROM Regions WHERE Id=@0 AND {"WorldID".EscapeSqlId(database)}=@1", id, Main.worldID.ToString()); var worldid = Main.worldID.ToString(); var region = Regions.FirstOrDefault(r => r.ID == id && r.WorldID == worldid); Regions.RemoveAll(r => r.ID == id && r.WorldID == worldid); @@ -201,7 +203,7 @@ namespace TShockAPI.DB { try { - database.Query("DELETE FROM Regions WHERE RegionName=@0 AND WorldID=@1", name, Main.worldID.ToString()); + database.Query($"DELETE FROM Regions WHERE {"RegionName".EscapeSqlId(database)}=@0 AND {"WorldID".EscapeSqlId(database)}=@1", name, Main.worldID.ToString()); var worldid = Main.worldID.ToString(); var region = Regions.FirstOrDefault(r => r.Name == name && r.WorldID == worldid); Regions.RemoveAll(r => r.Name == name && r.WorldID == worldid); @@ -251,7 +253,7 @@ namespace TShockAPI.DB { try { - database.Query("UPDATE Regions SET Protected=@0 WHERE RegionName=@1 AND WorldID=@2", state ? 1 : 0, name, + database.Query($"UPDATE Regions SET {"Protected".EscapeSqlId(database)}=@0 WHERE {"RegionName".EscapeSqlId(database)}=@1 AND {"WorldID".EscapeSqlId(database)}=@2", state ? 1 : 0, name, Main.worldID.ToString()); var region = GetRegionByName(name); if (region != null) diff --git a/TShockAPI/DB/WarpsManager.cs b/TShockAPI/DB/WarpsManager.cs index fb28f1a4..0bd49d0a 100644 --- a/TShockAPI/DB/WarpsManager.cs +++ b/TShockAPI/DB/WarpsManager.cs @@ -65,8 +65,13 @@ namespace TShockAPI.DB { try { - if (database.Query("INSERT INTO Warps (X, Y, WarpName, WorldID) VALUES (@0, @1, @2, @3);", - x, y, name, Main.worldID.ToString()) > 0) + string query = database.GetSqlType() switch + { + SqlType.Postgres => "INSERT INTO Warps (\"X\", \"Y\", \"WarpName\", \"WorldID\") VALUES (@0, @1, @2, @3);", + _ => "INSERT INTO Warps (X, Y, WarpName, WorldID) VALUES (@0, @1, @2, @3);" + }; + + if (database.Query(query, x, y, name, Main.worldID.ToString()) > 0) { Warps.Add(new Warp(new Point(x, y), name)); return true; @@ -76,6 +81,7 @@ namespace TShockAPI.DB { TShock.Log.Error(ex.ToString()); } + return false; } @@ -106,7 +112,7 @@ namespace TShockAPI.DB { try { - if (database.Query("DELETE FROM Warps WHERE WarpName = @0 AND WorldID = @1", + if (database.Query($"DELETE FROM Warps WHERE {"WarpName".EscapeSqlId(database)} = @0 AND {"WorldID".EscapeSqlId(database)} = @1", warpName, Main.worldID.ToString()) > 0) { Warps.RemoveAll(w => string.Equals(w.Name, warpName, StringComparison.OrdinalIgnoreCase)); @@ -141,7 +147,7 @@ namespace TShockAPI.DB { try { - if (database.Query("UPDATE Warps SET X = @0, Y = @1 WHERE WarpName = @2 AND WorldID = @3", + if (database.Query($"UPDATE Warps SET X = @0, Y = @1 WHERE {"WarpName".EscapeSqlId(database)} = @2 AND WorldID = @3", x, y, warpName, Main.worldID.ToString()) > 0) { Warps.Find(w => string.Equals(w.Name, warpName, StringComparison.OrdinalIgnoreCase)).Position = new Point(x, y); @@ -165,7 +171,7 @@ namespace TShockAPI.DB { try { - if (database.Query("UPDATE Warps SET Private = @0 WHERE WarpName = @1 AND WorldID = @2", + if (database.Query($"UPDATE Warps SET {"Private".EscapeSqlId(database)} = @0 WHERE {"WarpName".EscapeSqlId(database)} = @1 AND {"WorldID".EscapeSqlId(database)} = @2", state ? "1" : "0", warpName, Main.worldID.ToString()) > 0) { Warps.Find(w => string.Equals(w.Name, warpName, StringComparison.OrdinalIgnoreCase)).IsPrivate = state; From 9c473e35a6ddb6a9a80544093b00e7002b54bc45 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Sat, 3 May 2025 19:20:21 +0200 Subject: [PATCH 25/35] refactor(db/pgsql): Revert SQL queries, lower column casing for PGSQL Reverts SQL query identifier escaping to simplify queries and improves overall readability by using lowercase column names. Enhances maintainability and alignment with database conventions by adopting a uniform casing scheme across all SQL operations. Removes unnecessary complexity in query construction, streamlining the database operations performed within the application. --- TShockAPI/DB/BanManager.cs | 27 ++++----- TShockAPI/DB/CharacterManager.cs | 2 +- TShockAPI/DB/GroupManager.cs | 30 ++++++---- TShockAPI/DB/Queries/PostgresQueryCreator.cs | 6 +- TShockAPI/DB/RegionManager.cs | 27 +++------ TShockAPI/DB/UserManager.cs | 60 ++++++++------------ TShockAPI/DB/WarpsManager.cs | 18 ++---- TShockAPI/Extensions/DbExt.cs | 20 ++----- 8 files changed, 79 insertions(+), 111 deletions(-) diff --git a/TShockAPI/DB/BanManager.cs b/TShockAPI/DB/BanManager.cs index 2dfb6322..c56e3d0d 100644 --- a/TShockAPI/DB/BanManager.cs +++ b/TShockAPI/DB/BanManager.cs @@ -78,7 +78,7 @@ namespace TShockAPI.DB } catch (DllNotFoundException) { - Console.WriteLine(GetString("Possible problem with your database - is Sqlite3.dll present?")); + System.Console.WriteLine(GetString("Possible problem with your database - is Sqlite3.dll present?")); throw new Exception(GetString("Could not find a database library (probably Sqlite3.dll)")); } @@ -355,9 +355,7 @@ namespace TShockAPI.DB return Bans[id]; } - string query = $"SELECT * FROM PlayerBans WHERE {"TicketNumber".EscapeSqlId(database)}=@0"; - - using var reader = database.QueryReader(query, id); + using var reader = database.QueryReader("SELECT * FROM PlayerBans WHERE TicketNumber=@0", id); if (reader.Read()) { @@ -382,11 +380,10 @@ namespace TShockAPI.DB /// public IEnumerable RetrieveBansByIdentifier(string identifier, bool currentOnly = true) { - string query = $"SELECT * FROM PlayerBans WHERE {"Identifier".EscapeSqlId(database)}=@0"; - + string query = "SELECT * FROM PlayerBans WHERE Identifier=@0"; if (currentOnly) { - query += $" AND {"Expiration".EscapeSqlId(database)} > {DateTime.UtcNow.Ticks}"; + query += $" AND Expiration > {DateTime.UtcNow.Ticks}"; } using var reader = database.QueryReader(query, identifier); @@ -415,11 +412,11 @@ namespace TShockAPI.DB //Generate a sequence of '@0, @1, @2, ... etc' var parameters = string.Join(", ", Enumerable.Range(0, identifiers.Length).Select(p => $"@{p}")); - string query = $"SELECT * FROM PlayerBans WHERE {"Identifier".EscapeSqlId(database)} IN ({parameters})"; + string query = $"SELECT * FROM PlayerBans WHERE Identifier IN ({parameters})"; if (currentOnly) { - query += $" AND {"Expiration".EscapeSqlId(database)} > {DateTime.UtcNow.Ticks}"; + query += $" AND Expiration > {DateTime.UtcNow.Ticks}"; } using var reader = database.QueryReader(query, identifiers); @@ -452,7 +449,7 @@ namespace TShockAPI.DB List banlist = new List(); try { - using var reader = database.QueryReader($"SELECT * FROM PlayerBans ORDER BY {SortToOrderByMap(sortMethod)}"); + using var reader = database.QueryReader($"SELECT * FROM PlayerBans ORDER BY {SortToOrderByMap[sortMethod]}"); while (reader.Read()) { @@ -493,12 +490,12 @@ namespace TShockAPI.DB return false; } - private string SortToOrderByMap(BanSortMethod sortMethod) => sortMethod switch + private readonly Dictionary SortToOrderByMap = new() { - BanSortMethod.AddedNewestToOldest => $"{"Date".EscapeSqlId(database)} DESC", - BanSortMethod.AddedOldestToNewest => $"{"Date".EscapeSqlId(database)} ASC", - BanSortMethod.ExpirationSoonestToLatest => $"{"Expiration".EscapeSqlId(database)} ASC", - BanSortMethod.ExpirationLatestToSoonest => $"{"Expiration".EscapeSqlId(database)} DESC" + { BanSortMethod.AddedNewestToOldest, "Date DESC" }, + { BanSortMethod.AddedOldestToNewest, "Date ASC" }, + { BanSortMethod.ExpirationSoonestToLatest, "Expiration ASC" }, + { BanSortMethod.ExpirationLatestToSoonest, "Expiration DESC" } }; } diff --git a/TShockAPI/DB/CharacterManager.cs b/TShockAPI/DB/CharacterManager.cs index 08d6d1ec..3b9890a3 100644 --- a/TShockAPI/DB/CharacterManager.cs +++ b/TShockAPI/DB/CharacterManager.cs @@ -82,7 +82,7 @@ namespace TShockAPI.DB try { - using var reader = database.QueryReader($"SELECT * FROM tsCharacter WHERE {"Account".EscapeSqlId(database)}=@0", acctid); + using var reader = database.QueryReader("SELECT * FROM tsCharacter WHERE Account=@0", acctid); if (reader.Read()) { playerData.exists = true; diff --git a/TShockAPI/DB/GroupManager.cs b/TShockAPI/DB/GroupManager.cs index f655b987..060b4606 100644 --- a/TShockAPI/DB/GroupManager.cs +++ b/TShockAPI/DB/GroupManager.cs @@ -253,7 +253,13 @@ namespace TShockAPI.DB /// /// The group. /// true if it does; otherwise, false. - public bool GroupExists(string group) => group is "superadmin" || groups.Any(g => g.Name.Equals(group)); + public bool GroupExists(string group) + { + if (group == "superadmin") + return true; + + return groups.Any(g => g.Name.Equals(group)); + } IEnumerator IEnumerable.GetEnumerator() { @@ -264,14 +270,21 @@ namespace TShockAPI.DB /// Gets the enumerator. /// /// The enumerator. - public IEnumerator GetEnumerator() => groups.GetEnumerator(); + public IEnumerator GetEnumerator() + { + return groups.GetEnumerator(); + } /// /// Gets the group matching the specified name. /// /// The name. /// The group. - public Group GetGroupByName(string name) => groups.FirstOrDefault(g => g.Name == name); + public Group GetGroupByName(string name) + { + var ret = groups.Where(g => g.Name == name); + return 1 == ret.Count() ? ret.ElementAt(0) : null; + } /// /// Adds group with name and permissions if it does not exist. @@ -305,7 +318,7 @@ namespace TShockAPI.DB { SqlType.Sqlite => "INSERT OR IGNORE INTO GroupList (GroupName, Parent, Commands, ChatColor) VALUES (@0, @1, @2, @3);", SqlType.Mysql => "INSERT IGNORE INTO GroupList SET GroupName=@0, Parent=@1, Commands=@2, ChatColor=@3", - SqlType.Postgres => "INSERT INTO GroupList (\"GroupName\", \"Parent\", \"Commands\", \"ChatColor\") VALUES (@0, @1, @2, @3) ON CONFLICT (\"GroupName\") DO NOTHING", + SqlType.Postgres => "INSERT INTO GroupList (GroupName, Parent, Commands, ChatColor) VALUES (@0, @1, @2, @3) ON CONFLICT (GroupName) DO NOTHING", _ => throw new NotSupportedException(GetString("Unsupported database type.")) }; @@ -356,12 +369,9 @@ namespace TShockAPI.DB } // Ensure any group validation is also persisted to the DB. - var newGroup = new Group(name, parent, chatcolor, permissions) - { - Prefix = prefix, - Suffix = suffix - }; - + var newGroup = new Group(name, parent, chatcolor, permissions); + newGroup.Prefix = prefix; + newGroup.Suffix = suffix; string query = "UPDATE GroupList SET Parent=@0, Commands=@1, ChatColor=@2, Suffix=@3, Prefix=@4 WHERE GroupName=@5"; if (database.Query(query, parentname, newGroup.Permissions, newGroup.ChatColor, suffix, prefix, name) != 1) throw new GroupManagerException(GetString($"Failed to update group \"{name}\".")); diff --git a/TShockAPI/DB/Queries/PostgresQueryCreator.cs b/TShockAPI/DB/Queries/PostgresQueryCreator.cs index 5637d689..c12e2ec8 100644 --- a/TShockAPI/DB/Queries/PostgresQueryCreator.cs +++ b/TShockAPI/DB/Queries/PostgresQueryCreator.cs @@ -68,7 +68,7 @@ public class PostgresQueryCreator : GenericQueryCreator dataType = DbTypeToString(c.Type, c.Length); } - return "\"{0}\" {1} {2} {3} {4}".SFormat(c.Name, + return "{0} {1} {2} {3} {4}".SFormat(c.Name, dataType, c.Primary ? "PRIMARY KEY" : "", c.NotNull && !c.AutoIncrement ? "NOT NULL" : "", // SERIAL implies NOT NULL @@ -76,12 +76,12 @@ public class PostgresQueryCreator : GenericQueryCreator }); string[] uniques = table.Columns - .Where(c => c.Unique).Select(c => $"\"{c.Name}\"") + .Where(c => c.Unique).Select(c => c.Name) .ToArray(); // No re-enumeration return $"CREATE TABLE {EscapeTableName(table.Name)} ({string.Join(", ", columns)} {(uniques.Any() ? ", UNIQUE({0})".SFormat(string.Join(", ", uniques)) : "")})"; } /// - public override string RenameTable(string from, string to) => /*lang=postgresql*/"ALTER TABLE {0} RENAME TO {1}".SFormat(from, to); + public override string RenameTable(string from, string to) => "ALTER TABLE {0} RENAME TO {1}".SFormat(from, to); } diff --git a/TShockAPI/DB/RegionManager.cs b/TShockAPI/DB/RegionManager.cs index 27acd7a6..d657980a 100644 --- a/TShockAPI/DB/RegionManager.cs +++ b/TShockAPI/DB/RegionManager.cs @@ -67,9 +67,9 @@ namespace TShockAPI.DB { try { - using var reader = database.QueryReader($"SELECT * FROM Regions WHERE {"WorldID".EscapeSqlId(database)}=@0", Main.worldID.ToString()); - Regions.Clear(); + using var reader = database.QueryReader("SELECT * FROM Regions WHERE WorldID=@0", Main.worldID.ToString()); + Regions.Clear(); while (reader.Read()) { int id = reader.Get("Id"); @@ -135,19 +135,11 @@ namespace TShockAPI.DB } try { - string query = database.GetSqlType() switch - { - SqlType.Postgres => "INSERT INTO Regions (\"X1\", \"Y1\", \"width\", \"height\", \"RegionName\", \"WorldID\", \"UserIds\", \"Protected\", \"Groups\", \"Owner\", \"Z\") VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8, @9, @10);", - _ => "INSERT INTO Regions (X1, Y1, width, height, RegionName, WorldID, UserIds, Protected, Groups, Owner, Z) VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8, @9, @10);", - - }; - - database.Query(query, tx, ty, width, height, regionname, worldid, "", 1, "", owner, z); - + database.Query( + "INSERT INTO Regions (X1, Y1, width, height, RegionName, WorldID, UserIds, Protected, `Groups`, Owner, Z) VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8, @9, @10);", + tx, ty, width, height, regionname, worldid, "", 1, "", owner, z); int id; - - using (QueryResult res = database.QueryReader( - $"SELECT {"Id".EscapeSqlId(database)} FROM Regions WHERE {"RegionName".EscapeSqlId(database)} = @0 AND {"WorldID".EscapeSqlId(database)} = @1", regionname, worldid)) + using (QueryResult res = database.QueryReader("SELECT Id FROM Regions WHERE RegionName = @0 AND WorldID = @1", regionname, worldid)) { if (res.Read()) { @@ -158,7 +150,6 @@ namespace TShockAPI.DB return false; } } - Region region = new Region(id, new Rectangle(tx, ty, width, height), regionname, owner, true, worldid, z); Regions.Add(region); Hooks.RegionHooks.OnRegionCreated(region); @@ -180,7 +171,7 @@ namespace TShockAPI.DB { try { - database.Query($"DELETE FROM Regions WHERE Id=@0 AND {"WorldID".EscapeSqlId(database)}=@1", id, Main.worldID.ToString()); + database.Query("DELETE FROM Regions WHERE Id=@0 AND WorldID=@1", id, Main.worldID.ToString()); var worldid = Main.worldID.ToString(); var region = Regions.FirstOrDefault(r => r.ID == id && r.WorldID == worldid); Regions.RemoveAll(r => r.ID == id && r.WorldID == worldid); @@ -203,7 +194,7 @@ namespace TShockAPI.DB { try { - database.Query($"DELETE FROM Regions WHERE {"RegionName".EscapeSqlId(database)}=@0 AND {"WorldID".EscapeSqlId(database)}=@1", name, Main.worldID.ToString()); + database.Query("DELETE FROM Regions WHERE RegionName=@0 AND WorldID=@1", name, Main.worldID.ToString()); var worldid = Main.worldID.ToString(); var region = Regions.FirstOrDefault(r => r.Name == name && r.WorldID == worldid); Regions.RemoveAll(r => r.Name == name && r.WorldID == worldid); @@ -253,7 +244,7 @@ namespace TShockAPI.DB { try { - database.Query($"UPDATE Regions SET {"Protected".EscapeSqlId(database)}=@0 WHERE {"RegionName".EscapeSqlId(database)}=@1 AND {"WorldID".EscapeSqlId(database)}=@2", state ? 1 : 0, name, + database.Query("UPDATE Regions SET Protected=@0 WHERE RegionName=@1 AND WorldID=@2", state ? 1 : 0, name, Main.worldID.ToString()); var region = GetRegionByName(name); if (region != null) diff --git a/TShockAPI/DB/UserManager.cs b/TShockAPI/DB/UserManager.cs index e5077ce1..e705e3fc 100644 --- a/TShockAPI/DB/UserManager.cs +++ b/TShockAPI/DB/UserManager.cs @@ -70,22 +70,15 @@ namespace TShockAPI.DB int ret; try { - string query = _database.GetSqlType() switch - { - SqlType.Postgres => "INSERT INTO Users (\"Username\", \"Password\", \"UUID\", \"Usergroup\", \"Registered\") VALUES (@0, @1, @2, @3, @4);", - _ => "INSERT INTO Users (Username, Password, UUID, Usergroup, Registered) VALUES (@0, @1, @2, @3, @4);" - }; - - ret = _database.Query(query, account.Name, account.Password, account.UUID, account.Group, DateTime.UtcNow.ToString("s")); + ret = _database.Query("INSERT INTO Users (Username, Password, UUID, UserGroup, Registered) VALUES (@0, @1, @2, @3, @4);", account.Name, + account.Password, account.UUID, account.Group, DateTime.UtcNow.ToString("s")); } - // Detect duplicate user using a regexp as Sqlite doesn't have well structured exceptions - catch (Exception e) when (Regex.IsMatch(e.Message, "Username.*not unique|UNIQUE constraint failed: Users\\.Username")) + catch (Exception ex) { - throw new UserAccountExistsException(account.Name); - } - catch (Exception e) - { - throw new UserAccountManagerException(GetString($"AddUser SQL returned an error ({e.Message})"), e); + // Detect duplicate user using a regexp as Sqlite doesn't have well structured exceptions + if (Regex.IsMatch(ex.Message, "Username.*not unique|UNIQUE constraint failed: Users\\.Username")) + throw new UserAccountExistsException(account.Name); + throw new UserAccountManagerException(GetString($"AddUser SQL returned an error ({ex.Message})"), ex); } if (1 > ret) @@ -106,7 +99,7 @@ namespace TShockAPI.DB TShock.Players.Where(p => p?.IsLoggedIn == true && p.Account.Name == account.Name).ForEach(p => p.Logout()); UserAccount tempuser = GetUserAccount(account); - int affected = _database.Query($"DELETE FROM Users WHERE {"Username".EscapeSqlId(_database)}=@0", account.Name); + int affected = _database.Query("DELETE FROM Users WHERE Username=@0", account.Name); if (affected < 1) throw new UserAccountNotExistException(account.Name); @@ -131,11 +124,10 @@ namespace TShockAPI.DB { account.CreateBCryptHash(password); - if (_database.Query($"UPDATE Users SET {"Password".EscapeSqlId(_database)} = @0 WHERE {"Username".EscapeSqlId(_database)} = @1;", - account.Password, account.Name) is 0) - { + if ( + _database.Query("UPDATE Users SET Password = @0 WHERE Username = @1;", account.Password, + account.Name) == 0) throw new UserAccountNotExistException(account.Name); - } } catch (Exception ex) { @@ -152,10 +144,10 @@ namespace TShockAPI.DB { try { - if (_database.Query(/*lang=sql*/$"UPDATE Users SET {"UUID".EscapeSqlId(_database)} = @0 WHERE {"Username".EscapeSqlId(_database)} = @1;", uuid, account.Name) is 0) - { + if ( + _database.Query("UPDATE Users SET UUID = @0 WHERE Username = @1;", uuid, + account.Name) == 0) throw new UserAccountNotExistException(account.Name); - } } catch (Exception ex) { @@ -177,7 +169,7 @@ namespace TShockAPI.DB if (AccountHooks.OnAccountGroupUpdate(account, ref grp)) throw new UserGroupUpdateLockedException(account.Name); - if (_database.Query($"UPDATE Users SET {"UserGroup".EscapeSqlId(_database)} = @0 WHERE {"Username".EscapeSqlId(_database)} = @1;", grp.Name, account.Name) == 0) + if (_database.Query("UPDATE Users SET UserGroup = @0 WHERE Username = @1;", grp.Name, account.Name) == 0) throw new UserAccountNotExistException(account.Name); try @@ -208,7 +200,7 @@ namespace TShockAPI.DB if (AccountHooks.OnAccountGroupUpdate(account, author, ref grp)) throw new UserGroupUpdateLockedException(account.Name); - if (_database.Query($"UPDATE Users SET {"UserGroup".EscapeSqlId(_database)} = @0 WHERE {"Username".EscapeSqlId(_database)} = @1;", grp.Name, account.Name) == 0) + if (_database.Query("UPDATE Users SET UserGroup = @0 WHERE Username = @1;", grp.Name, account.Name) == 0) throw new UserAccountNotExistException(account.Name); try @@ -231,12 +223,8 @@ namespace TShockAPI.DB { try { - if (_database.Query( - $"UPDATE Users SET {"LastAccessed".EscapeSqlId(_database)} = @0, {"KnownIPs".EscapeSqlId(_database)} = @1 WHERE {"Username".EscapeSqlId(_database)} = @2;", - DateTime.UtcNow.ToString("s"), account.KnownIps, account.Name) is 0 - ) { + if (_database.Query("UPDATE Users SET LastAccessed = @0, KnownIps = @1 WHERE Username = @2;", DateTime.UtcNow.ToString("s"), account.KnownIps, account.Name) == 0) throw new UserAccountNotExistException(account.Name); - } } catch (Exception ex) { @@ -251,7 +239,7 @@ namespace TShockAPI.DB { try { - using var reader = _database.QueryReader($"SELECT * FROM Users WHERE {"Username".EscapeSqlId(_database)}=@0", username); + using var reader = _database.QueryReader("SELECT * FROM Users WHERE Username=@0", username); if (reader.Read()) { return reader.Get("ID"); @@ -305,13 +293,13 @@ namespace TShockAPI.DB object arg; if (account.ID != 0) { - query = $"SELECT * FROM Users WHERE {"ID".EscapeSqlId(_database)}=@0"; + query = "SELECT * FROM Users WHERE ID=@0"; arg = account.ID; type = "id"; } else { - query = $"SELECT * FROM Users WHERE {"Username".EscapeSqlId(_database)}=@0"; + query = "SELECT * FROM Users WHERE Username=@0"; arg = account.Name; type = "name"; } @@ -370,9 +358,9 @@ namespace TShockAPI.DB try { List accounts = new List(); - string search = $"{(notAtStart ? "%" : "")}{username}%"; - using var reader = _database.QueryReader($"SELECT * FROM Users WHERE {"Username".EscapeSqlId(_database)} LIKE @0", search); - + string search = notAtStart ? string.Format("%{0}%", username) : string.Format("{0}%", username); + using var reader = _database.QueryReader("SELECT * FROM Users WHERE Username LIKE @0", + search); while (reader.Read()) { accounts.Add(LoadUserAccountFromResult(new UserAccount(), reader)); @@ -400,7 +388,7 @@ namespace TShockAPI.DB account.Name = result.Get("Username"); account.Registered = result.Get("Registered"); account.LastAccessed = result.Get("LastAccessed"); - account.KnownIps = result.Get("KnownIPs"); + account.KnownIps = result.Get("KnownIps"); return account; } } diff --git a/TShockAPI/DB/WarpsManager.cs b/TShockAPI/DB/WarpsManager.cs index 0bd49d0a..06723281 100644 --- a/TShockAPI/DB/WarpsManager.cs +++ b/TShockAPI/DB/WarpsManager.cs @@ -65,13 +65,8 @@ namespace TShockAPI.DB { try { - string query = database.GetSqlType() switch - { - SqlType.Postgres => "INSERT INTO Warps (\"X\", \"Y\", \"WarpName\", \"WorldID\") VALUES (@0, @1, @2, @3);", - _ => "INSERT INTO Warps (X, Y, WarpName, WorldID) VALUES (@0, @1, @2, @3);" - }; - - if (database.Query(query, x, y, name, Main.worldID.ToString()) > 0) + if (database.Query("INSERT INTO Warps (X, Y, WarpName, WorldID) VALUES (@0, @1, @2, @3);", + x, y, name, Main.worldID.ToString()) > 0) { Warps.Add(new Warp(new Point(x, y), name)); return true; @@ -81,7 +76,6 @@ namespace TShockAPI.DB { TShock.Log.Error(ex.ToString()); } - return false; } @@ -92,7 +86,7 @@ namespace TShockAPI.DB { Warps.Clear(); - using var reader = database.QueryReader($"SELECT * FROM Warps WHERE {"WorldID".EscapeSqlId(database)} = @0", + using var reader = database.QueryReader("SELECT * FROM Warps WHERE WorldID = @0", Main.worldID.ToString()); while (reader.Read()) { @@ -112,7 +106,7 @@ namespace TShockAPI.DB { try { - if (database.Query($"DELETE FROM Warps WHERE {"WarpName".EscapeSqlId(database)} = @0 AND {"WorldID".EscapeSqlId(database)} = @1", + if (database.Query("DELETE FROM Warps WHERE WarpName = @0 AND WorldID = @1", warpName, Main.worldID.ToString()) > 0) { Warps.RemoveAll(w => string.Equals(w.Name, warpName, StringComparison.OrdinalIgnoreCase)); @@ -147,7 +141,7 @@ namespace TShockAPI.DB { try { - if (database.Query($"UPDATE Warps SET X = @0, Y = @1 WHERE {"WarpName".EscapeSqlId(database)} = @2 AND WorldID = @3", + if (database.Query("UPDATE Warps SET X = @0, Y = @1 WHERE WarpName = @2 AND WorldID = @3", x, y, warpName, Main.worldID.ToString()) > 0) { Warps.Find(w => string.Equals(w.Name, warpName, StringComparison.OrdinalIgnoreCase)).Position = new Point(x, y); @@ -171,7 +165,7 @@ namespace TShockAPI.DB { try { - if (database.Query($"UPDATE Warps SET {"Private".EscapeSqlId(database)} = @0 WHERE {"WarpName".EscapeSqlId(database)} = @1 AND {"WorldID".EscapeSqlId(database)} = @2", + if (database.Query("UPDATE Warps SET Private = @0 WHERE WarpName = @1 AND WorldID = @2", state ? "1" : "0", warpName, Main.worldID.ToString()) > 0) { Warps.Find(w => string.Equals(w.Name, warpName, StringComparison.OrdinalIgnoreCase)).IsPrivate = state; diff --git a/TShockAPI/Extensions/DbExt.cs b/TShockAPI/Extensions/DbExt.cs index 55a681dd..ba2cfd1f 100644 --- a/TShockAPI/Extensions/DbExt.cs +++ b/TShockAPI/Extensions/DbExt.cs @@ -20,7 +20,6 @@ using System; using System.Collections.Generic; using System.Data; using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; using Microsoft.Data.Sqlite; using MySql.Data.MySqlClient; using Npgsql; @@ -43,14 +42,15 @@ namespace TShockAPI.DB [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")] public static int Query(this IDbConnection olddb, string query, params object[] args) { - using IDbConnection db = olddb.CloneEx(); + using var db = olddb.CloneEx(); db.Open(); - using IDbCommand com = db.CreateCommand(); + + using var com = db.CreateCommand(); com.CommandText = query; for (int i = 0; i < args.Length; i++) { - com.AddParameter("@" + i, args[i] ?? DBNull.Value); + com.AddParameter($"@{i}", args[i] ?? DBNull.Value); } return com.ExecuteNonQuery(); @@ -272,18 +272,6 @@ namespace TShockAPI.DB return (T)reader.GetValue(column); } - - /// - /// Escapes an identifier for use in a SQL query. - /// - /// The identifier to escape, typically a table or column name. - /// The escaped identifier. - [Pure] - public static string EscapeSqlId(this string id, IDbConnection db) => db.GetSqlType() switch - { - SqlType.Postgres => $"\"{id}\"", // The main PITA and culprit - _ => id // Default case for agnostic SQL - }; } public enum SqlType From 76b6f56a8fe44962112e9a2be801ad0fa7a7c0ba Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Tue, 6 May 2025 11:51:59 +0200 Subject: [PATCH 26/35] feat: Add spawning pets perm to default group Add permission for users to spawn pets for default usergroup. --- TShockAPI/DB/GroupManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/TShockAPI/DB/GroupManager.cs b/TShockAPI/DB/GroupManager.cs index 4a9f8afb..44f0c994 100644 --- a/TShockAPI/DB/GroupManager.cs +++ b/TShockAPI/DB/GroupManager.cs @@ -74,6 +74,7 @@ namespace TShockAPI.DB Permissions.canchangepassword, Permissions.canlogout, Permissions.summonboss, + Permissions.spawnpets, Permissions.worldupgrades, Permissions.whisper, Permissions.wormhole, From f479e27da601d854445f94ae3c7e27e26cb888bb Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Tue, 6 May 2025 12:08:50 +0200 Subject: [PATCH 27/35] feat: Add schema filtering for Postgres table col scanning Modifies the query to ensure it correctly retrieves column names by including the schema check. --- TShockAPI/DB/SqlTable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TShockAPI/DB/SqlTable.cs b/TShockAPI/DB/SqlTable.cs index ce955dbf..22591ff8 100644 --- a/TShockAPI/DB/SqlTable.cs +++ b/TShockAPI/DB/SqlTable.cs @@ -106,7 +106,7 @@ namespace TShockAPI.DB case SqlType.Postgres: { // HACK: Using "ilike" op to ignore case, due to weird case issues adapting for pgsql - using QueryResult reader = database.QueryReader("SELECT column_name FROM information_schema.columns WHERE table_name ILIKE @0", table.Name); + using QueryResult reader = database.QueryReader("SELECT column_name FROM information_schema.columns WHERE table_schema=current_schema() AND table_name ILIKE @0", table.Name); while (reader.Read()) { From bd2aafe01a99e1a682074ec5989647735e88a433 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Wed, 7 May 2025 15:25:49 +0200 Subject: [PATCH 28/35] Fix console title not updating and world not saving when the last player disconnects --- TShockAPI/TShock.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TShockAPI/TShock.cs b/TShockAPI/TShock.cs index bd2a113e..e2dd3aa1 100644 --- a/TShockAPI/TShock.cs +++ b/TShockAPI/TShock.cs @@ -1469,8 +1469,8 @@ namespace TShockAPI Hooks.PlayerHooks.OnPlayerLogout(tsplr); } - // The last player will leave after this hook is executed. - if (Utils.GetActivePlayerCount() == 1) + // If this is the last player online, update the console title and save the world if needed + if (Utils.GetActivePlayerCount() == 0) { if (Config.Settings.SaveWorldOnLastPlayerExit) SaveManager.Instance.SaveWorld(); From e12950b50e5a2fb557df57a75a3989cffc576f2e Mon Sep 17 00:00:00 2001 From: Lucas Nicodemus Date: Thu, 8 May 2025 09:00:49 +0900 Subject: [PATCH 29/35] Add workflow for notifying discord for wiki changes --- .github/workflows/wiki-notify.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/workflows/wiki-notify.yml diff --git a/.github/workflows/wiki-notify.yml b/.github/workflows/wiki-notify.yml new file mode 100644 index 00000000..835c5b40 --- /dev/null +++ b/.github/workflows/wiki-notify.yml @@ -0,0 +1,13 @@ +name: Wiki Changed Discord Notification + +on: + gollum + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - uses: 'oznu/gh-wiki-edit-discord-notification@dfc866fd048f04c239ad113eef3c6c73504d333e' + with: + discord-webhook-url: ${{ secrets.DISCORD_WEBHOOK_WIKI_EDIT }} + ignore-collaborators: false From 952a6685b10e61a61264f452cdfd8330667448e0 Mon Sep 17 00:00:00 2001 From: Lucas Nicodemus Date: Fri, 9 May 2025 16:28:53 +0900 Subject: [PATCH 30/35] Version tick: 5.2.4 --- TShockAPI/TShock.cs | 2 +- TShockAPI/TShockAPI.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TShockAPI/TShock.cs b/TShockAPI/TShock.cs index e2dd3aa1..0771bbfe 100644 --- a/TShockAPI/TShock.cs +++ b/TShockAPI/TShock.cs @@ -63,7 +63,7 @@ namespace TShockAPI /// VersionNum - The version number the TerrariaAPI will return back to the API. We just use the Assembly info. public static readonly Version VersionNum = Assembly.GetExecutingAssembly().GetName().Version; /// VersionCodename - The version codename is displayed when the server starts. Inspired by software codenames conventions. - public static readonly string VersionCodename = "Stargazer"; + public static readonly string VersionCodename = "Hopefully SSC works somewhat correctly now edition"; /// SavePath - This is the path TShock saves its data in. This path is relative to the TerrariaServer.exe (not in ServerPlugins). public static string SavePath = "tshock"; diff --git a/TShockAPI/TShockAPI.csproj b/TShockAPI/TShockAPI.csproj index a01bb022..eb7894b7 100644 --- a/TShockAPI/TShockAPI.csproj +++ b/TShockAPI/TShockAPI.csproj @@ -18,11 +18,11 @@ Also, be sure to release on github with the exact assembly version tag as below so that the update manager works correctly (via the Github releases api and mimic) --> - 5.2.3 + 5.2.4 TShock for Terraria Pryaxis & TShock Contributors TShockAPI - Copyright © Pryaxis & TShock Contributors 2011-2023 + Copyright © Pryaxis & TShock Contributors 2011-2025 True GPL-3.0-or-later From fbb25f79f5ed3a9f65da6b7e01e1e0ce8ca7524c Mon Sep 17 00:00:00 2001 From: Luke Date: Fri, 9 May 2025 22:41:20 +1000 Subject: [PATCH 31/35] Update TSAPI submodule --- TerrariaServerAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TerrariaServerAPI b/TerrariaServerAPI index 2c82f673..29dc46f4 160000 --- a/TerrariaServerAPI +++ b/TerrariaServerAPI @@ -1 +1 @@ -Subproject commit 2c82f6739e4c76e9e01c5fe7feadd91d9f6fd0ec +Subproject commit 29dc46f4e1c7b41f9e88e41187dd8d5b208e257a From 9abeb3c14ab82d985155b826e00c919c85a2fff4 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Sat, 10 May 2025 03:41:12 +0200 Subject: [PATCH 32/35] build: Update Npgsql package version to 9.0.3 in project files --- TShockAPI/TShockAPI.csproj | 4 ++-- TShockLauncher/TShockLauncher.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TShockAPI/TShockAPI.csproj b/TShockAPI/TShockAPI.csproj index 5792c1e1..b83c9251 100644 --- a/TShockAPI/TShockAPI.csproj +++ b/TShockAPI/TShockAPI.csproj @@ -1,4 +1,4 @@ - + @@ -36,7 +36,7 @@ - + diff --git a/TShockLauncher/TShockLauncher.csproj b/TShockLauncher/TShockLauncher.csproj index 486b638e..d59ad73d 100644 --- a/TShockLauncher/TShockLauncher.csproj +++ b/TShockLauncher/TShockLauncher.csproj @@ -34,7 +34,7 @@ - + From f0b9424296c9671b9f1b219bf32318cbfefc2736 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Sat, 10 May 2025 14:55:29 +0200 Subject: [PATCH 33/35] style: Correct indentation and formatting Adjusts comment indentation and aligns property attribute formatting for clarity and consistency in configuration definitions. --- TShockAPI/Configuration/TShockConfig.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/TShockAPI/Configuration/TShockConfig.cs b/TShockAPI/Configuration/TShockConfig.cs index 0a9e1a1a..64fbf77c 100644 --- a/TShockAPI/Configuration/TShockConfig.cs +++ b/TShockAPI/Configuration/TShockConfig.cs @@ -552,21 +552,21 @@ namespace TShockAPI.Configuration [Description("The password used when connecting to a MySQL database.")] public string MySqlPassword = ""; - ///The Postgres hostname and port to direct connections to. - [Description("The Postgres hostname and port to direct connections to.")] - public string PostgresHost = ""; + ///The Postgres hostname and port to direct connections to. + [Description("The Postgres hostname and port to direct connections to.")] + public string PostgresHost = ""; - /// The database name to connect to when using Postgres as the database type. - [Description("The database name to connect to when using Postgres as the database type.")] - public string PostgresDbName = ""; + /// The database name to connect to when using Postgres as the database type. + [Description("The database name to connect to when using Postgres as the database type.")] + public string PostgresDbName = ""; - /// The username used when connecting to a Postgres database. - [Description("The username used when connecting to a Postgres database.")] - public string PostgresUsername = ""; + /// The username used when connecting to a Postgres database. + [Description("The username used when connecting to a Postgres database.")] + public string PostgresUsername = ""; - /// The password used when connecting to a Postgres database. - [Description("The password used when connecting to a Postgres database.")] - public string PostgresPassword = ""; + /// The password used when connecting to a Postgres database. + [Description("The password used when connecting to a Postgres database.")] + public string PostgresPassword = ""; /// Whether or not to save logs to the SQL database instead of a text file. [Description("Whether or not to save logs to the SQL database instead of a text file.\nDefault = false.")] From 22a3f7727141ebdcbd60fb80c3d945acb9417bf7 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Sat, 10 May 2025 15:03:05 +0200 Subject: [PATCH 34/35] chore: Remove invalid comment on Npgsql package reference --- TShockLauncher/TShockLauncher.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TShockLauncher/TShockLauncher.csproj b/TShockLauncher/TShockLauncher.csproj index d59ad73d..95b85859 100644 --- a/TShockLauncher/TShockLauncher.csproj +++ b/TShockLauncher/TShockLauncher.csproj @@ -32,9 +32,10 @@ + + - From f5c1bf24c0d037e9e4ea2aac6caa2100be0961c7 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Sat, 10 May 2025 15:28:56 +0200 Subject: [PATCH 35/35] refactor: Rename query builder classes for consistency Standardizes class names for different database query builders, aligning naming conventions across SQLite, MySQL, PostgreSQL implementations, and updating related factory method calls to improve code clarity and maintainability. --- .../{GenericQueryCreator.cs => GenericQueryBuilder.cs} | 6 +++--- TShockAPI/DB/Queries/IQueryBuilder.cs | 2 +- .../Queries/{MysqlQueryCreator.cs => MysqlQueryBuilder.cs} | 4 ++-- .../{PostgresQueryCreator.cs => PostgresQueryBuilder.cs} | 4 ++-- .../{SqliteQueryCreator.cs => SqliteQueryBuilder.cs} | 4 ++-- TShockAPI/Extensions/DbExt.cs | 6 +++--- 6 files changed, 13 insertions(+), 13 deletions(-) rename TShockAPI/DB/Queries/{GenericQueryCreator.cs => GenericQueryBuilder.cs} (97%) rename TShockAPI/DB/Queries/{MysqlQueryCreator.cs => MysqlQueryBuilder.cs} (95%) rename TShockAPI/DB/Queries/{PostgresQueryCreator.cs => PostgresQueryBuilder.cs} (96%) rename TShockAPI/DB/Queries/{SqliteQueryCreator.cs => SqliteQueryBuilder.cs} (96%) diff --git a/TShockAPI/DB/Queries/GenericQueryCreator.cs b/TShockAPI/DB/Queries/GenericQueryBuilder.cs similarity index 97% rename from TShockAPI/DB/Queries/GenericQueryCreator.cs rename to TShockAPI/DB/Queries/GenericQueryBuilder.cs index f9fc2038..b62d3925 100644 --- a/TShockAPI/DB/Queries/GenericQueryCreator.cs +++ b/TShockAPI/DB/Queries/GenericQueryBuilder.cs @@ -1,6 +1,6 @@ /* TShock, a server mod for Terraria -Copyright (C) 2011-2019 Pryaxis & TShock Contributors +Copyright (C) 2011-2025 Pryaxis & TShock Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -28,7 +28,7 @@ namespace TShockAPI.DB.Queries; /// /// A Generic Query Creator (abstract) /// -public abstract class GenericQueryCreator : IQueryBuilder +public abstract class GenericQueryBuilder : IQueryBuilder { protected static Random rand = new Random(); @@ -130,7 +130,7 @@ public abstract class GenericQueryCreator : IQueryBuilder /// /// /// - protected static string BuildWhere(List wheres) => wheres.Count > 0 + protected static string BuildWhere(List wheres) => wheres.Count > 0 ? string.Empty : "WHERE {0}".SFormat(string.Join(", ", wheres.Select(v => $"{v.Name} = {v.Value}"))); } diff --git a/TShockAPI/DB/Queries/IQueryBuilder.cs b/TShockAPI/DB/Queries/IQueryBuilder.cs index dc61370f..fe2d6b53 100644 --- a/TShockAPI/DB/Queries/IQueryBuilder.cs +++ b/TShockAPI/DB/Queries/IQueryBuilder.cs @@ -1,6 +1,6 @@ /* TShock, a server mod for Terraria -Copyright (C) 2011-2019 Pryaxis & TShock Contributors +Copyright (C) 2011-2025 Pryaxis & TShock Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/TShockAPI/DB/Queries/MysqlQueryCreator.cs b/TShockAPI/DB/Queries/MysqlQueryBuilder.cs similarity index 95% rename from TShockAPI/DB/Queries/MysqlQueryCreator.cs rename to TShockAPI/DB/Queries/MysqlQueryBuilder.cs index 1c0ec24f..85185960 100644 --- a/TShockAPI/DB/Queries/MysqlQueryCreator.cs +++ b/TShockAPI/DB/Queries/MysqlQueryBuilder.cs @@ -1,6 +1,6 @@ /* TShock, a server mod for Terraria -Copyright (C) 2011-2019 Pryaxis & TShock Contributors +Copyright (C) 2011-2025 Pryaxis & TShock Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -26,7 +26,7 @@ namespace TShockAPI.DB.Queries; /// /// Query Creator for MySQL /// -public class MysqlQueryCreator : GenericQueryCreator, IQueryBuilder +public class MysqlQueryBuilder : GenericQueryBuilder, IQueryBuilder { /// /// Creates a table from a SqlTable object. diff --git a/TShockAPI/DB/Queries/PostgresQueryCreator.cs b/TShockAPI/DB/Queries/PostgresQueryBuilder.cs similarity index 96% rename from TShockAPI/DB/Queries/PostgresQueryCreator.cs rename to TShockAPI/DB/Queries/PostgresQueryBuilder.cs index c12e2ec8..dd92121e 100644 --- a/TShockAPI/DB/Queries/PostgresQueryCreator.cs +++ b/TShockAPI/DB/Queries/PostgresQueryBuilder.cs @@ -1,6 +1,6 @@ /* TShock, a server mod for Terraria -Copyright (C) 2011-2019 Pryaxis & TShock Contributors +Copyright (C) 2011-2025 Pryaxis & TShock Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -26,7 +26,7 @@ namespace TShockAPI.DB.Queries; /// /// Query Creator for PostgreSQL /// -public class PostgresQueryCreator : GenericQueryCreator +public class PostgresQueryBuilder : GenericQueryBuilder { /// public override string DbTypeToString(MySqlDbType type, int? length) => type switch diff --git a/TShockAPI/DB/Queries/SqliteQueryCreator.cs b/TShockAPI/DB/Queries/SqliteQueryBuilder.cs similarity index 96% rename from TShockAPI/DB/Queries/SqliteQueryCreator.cs rename to TShockAPI/DB/Queries/SqliteQueryBuilder.cs index 229ba49b..72de17d5 100644 --- a/TShockAPI/DB/Queries/SqliteQueryCreator.cs +++ b/TShockAPI/DB/Queries/SqliteQueryBuilder.cs @@ -1,6 +1,6 @@ /* TShock, a server mod for Terraria -Copyright (C) 2011-2019 Pryaxis & TShock Contributors +Copyright (C) 2011-2025 Pryaxis & TShock Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -26,7 +26,7 @@ namespace TShockAPI.DB.Queries; /// /// Query Creator for Sqlite /// -public class SqliteQueryCreator : GenericQueryCreator, IQueryBuilder +public class SqliteQueryBuilder : GenericQueryBuilder, IQueryBuilder { /// /// Creates a table from a SqlTable object. diff --git a/TShockAPI/Extensions/DbExt.cs b/TShockAPI/Extensions/DbExt.cs index ba2cfd1f..efbf68b7 100644 --- a/TShockAPI/Extensions/DbExt.cs +++ b/TShockAPI/Extensions/DbExt.cs @@ -158,9 +158,9 @@ namespace TShockAPI.DB public static IQueryBuilder GetSqlQueryBuilder(this IDbConnection db) => db.GetSqlType() switch { - SqlType.Sqlite => new SqliteQueryCreator(), - SqlType.Mysql => new MysqlQueryCreator(), - SqlType.Postgres => new PostgresQueryCreator(), + SqlType.Sqlite => new SqliteQueryBuilder(), + SqlType.Mysql => new MysqlQueryBuilder(), + SqlType.Postgres => new PostgresQueryBuilder(), _ => throw new NotSupportedException("Database type not supported.") };