diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 8275735a..75b69d48 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ # These are supported funding model platforms -github: [SignatureBeef, hakusaro, Stealownz, QuiCM] +github: [SignatureBeef, QuiCM] diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index deb811a2..3d47d3c6 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -5,6 +5,10 @@ on: [push, pull_request] jobs: build: runs-on: ubuntu-latest + permissions: + attestations: write + id-token: write + packages: write steps: - name: Checkout uses: actions/checkout@v4 @@ -14,12 +18,43 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Set up buildx uses: docker/setup-buildx-action@v3 + - name: Login to ghcr.io + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Generate version information + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=schedule + type=ref,event=branch + type=ref,event=tag,enable=${{ !startsWith(github.ref, 'refs/tags/v') }} + type=ref,event=pr + type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=semver,pattern={{major}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + flavor: | + latest=${{ startsWith(github.ref, 'refs/tags/v') }} - name: Build image - uses: docker/build-push-action@v5 + id: build + uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm64,linux/arm/v7,windows/amd64 - push: false + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} pull: true cache-from: type=gha, scope=${{ github.workflow }} cache-to: type=gha, scope=${{ github.workflow }} + - name: Generate build provenance attestation + if: ${{ github.event_name != 'pull_request' }} + uses: actions/attest-build-provenance@v2 + with: + subject-name: ghcr.io/${{ github.repository }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/ci-otapi3.yml b/.github/workflows/ci-otapi3.yml index f5599ffc..e72d8c22 100644 --- a/.github/workflows/ci-otapi3.yml +++ b/.github/workflows/ci-otapi3.yml @@ -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 diff --git a/README.md b/README.md index b5559b37..0369df53 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- TShock for Terraria
+ TShock for Terraria
AppVeyor Build Status diff --git a/README_cn.md b/README_cn.md index e038817e..65c5eba6 100644 --- a/README_cn.md +++ b/README_cn.md @@ -1,5 +1,5 @@

- TShock for Terraria
+ TShock for Terraria
AppVeyor Build Status diff --git a/TShockAPI/Bouncer.cs b/TShockAPI/Bouncer.cs index 6ff7fd1a..bfcd1836 100644 --- a/TShockAPI/Bouncer.cs +++ b/TShockAPI/Bouncer.cs @@ -424,7 +424,7 @@ namespace TShockAPI }; PlayerAddBuffWhitelist[BuffID.BrainOfConfusionBuff] = new BuffLimit { - MaxTicks = 240, + MaxTicks = 60 * 4, CanBeAddedWithoutHostile = true, CanOnlyBeAppliedToSender = true }; @@ -434,6 +434,12 @@ namespace TShockAPI CanBeAddedWithoutHostile = true, CanOnlyBeAppliedToSender = true }; + PlayerAddBuffWhitelist[BuffID.ParryDamageBuff] = new BuffLimit + { + MaxTicks = 60 * 5, + CanBeAddedWithoutHostile = true, + CanOnlyBeAppliedToSender = true + }; #endregion Whitelist } @@ -498,6 +504,14 @@ namespace TShockAPI return; } + if (!float.IsFinite(pos.X) || !float.IsFinite(pos.Y)) + { + TShock.Log.ConsoleInfo(GetString("Bouncer / OnPlayerUpdate force kicked (attempted to set position to infinity or NaN) from {0}", args.Player.Name)); + args.Player.Kick(GetString("Detected DOOM set to ON position."), true, true); + args.Handled = true; + return; + } + if (pos.X < 0 || pos.Y < 0 || pos.X >= Main.maxTilesX * 16 - 16 || pos.Y >= Main.maxTilesY * 16 - 16) { TShock.Log.ConsoleDebug(GetString("Bouncer / OnPlayerUpdate rejected from (position check) {0}", args.Player.Name)); @@ -1066,6 +1080,22 @@ namespace TShockAPI bool noDelay = args.NoDelay; short type = args.Type; + if (!float.IsFinite(pos.X) || !float.IsFinite(pos.Y)) + { + TShock.Log.ConsoleInfo(GetString("Bouncer / OnItemDrop force kicked (attempted to set position to infinity or NaN) from {0}", args.Player.Name)); + args.Player.Kick(GetString("Detected DOOM set to ON position."), true, true); + args.Handled = true; + return; + } + + if (!float.IsFinite(vel.X) || !float.IsFinite(vel.Y)) + { + TShock.Log.ConsoleInfo(GetString("Bouncer / OnItemDrop force kicked (attempted to set velocity to infinity or NaN) from {0}", args.Player.Name)); + args.Player.Kick(GetString("Detected DOOM set to ON position."), true, true); + args.Handled = true; + return; + } + // player is attempting to crash clients if (type < -48 || type >= Terraria.ID.ItemID.Count) { @@ -1169,6 +1199,22 @@ namespace TShockAPI int index = args.Index; float[] ai = args.Ai; + if (!float.IsFinite(pos.X) || !float.IsFinite(pos.Y)) + { + TShock.Log.ConsoleInfo(GetString("Bouncer / OnNewProjectile force kicked (attempted to set position to infinity or NaN) from {0}", args.Player.Name)); + args.Player.Kick(GetString("Detected DOOM set to ON position."), true, true); + args.Handled = true; + return; + } + + if (!float.IsFinite(vel.X) || !float.IsFinite(vel.Y)) + { + TShock.Log.ConsoleInfo(GetString("Bouncer / OnNewProjectile force kicked (attempted to set velocity to infinity or NaN) from {0}", args.Player.Name)); + args.Player.Kick(GetString("Detected DOOM set to ON position."), true, true); + args.Handled = true; + return; + } + if (index > Main.maxProjectiles) { TShock.Log.ConsoleDebug(GetString("Bouncer / OnNewProjectile rejected from above projectile limit from {0}", args.Player.Name)); @@ -1878,7 +1924,7 @@ namespace TShockAPI return; } - if (TShock.Players[id] == null) + if (TShock.Players[id] == null || !TShock.Players[id].Active) { TShock.Log.ConsoleDebug(GetString( "Bouncer / OnPlayerBuff rejected {0} ({1}) applying buff {2} to {3} for {4} ticks: target is null", args.Player.Name, @@ -2081,7 +2127,7 @@ namespace TShockAPI short amount = args.Amount; byte plr = args.TargetPlayerIndex; - if (amount <= 0 || Main.player[plr] == null || !Main.player[plr].active) + if (amount <= 0 || TShock.Players[plr] == null || !TShock.Players[plr].Active) { TShock.Log.ConsoleDebug(GetString("Bouncer / OnHealOtherPlayer rejected null checks")); args.Handled = true; @@ -2589,7 +2635,7 @@ namespace TShockAPI byte direction = args.Direction; PlayerDeathReason reason = args.PlayerDeathReason; - if (id >= Main.maxPlayers || TShock.Players[id] == null) + if (id >= Main.maxPlayers || TShock.Players[id] == null || !TShock.Players[id].Active) { TShock.Log.ConsoleDebug(GetString("Bouncer / OnPlayerDamage rejected null check")); args.Handled = true; @@ -2854,7 +2900,7 @@ namespace TShockAPI { BuffID.Poisoned, 3600 }, // BuffID: 20 { BuffID.OnFire, 1200 }, // BuffID: 24 { BuffID.Confused, short.MaxValue }, // BuffID: 31 Brain of Confusion Internal Item ID: 3223 - { BuffID.CursedInferno, 420 }, // BuffID: 39 + { BuffID.CursedInferno, 600 }, // BuffID: 39 { BuffID.Frostburn, 900 }, // BuffID: 44 { BuffID.Ichor, 1200 }, // BuffID: 69 { BuffID.Venom, 1800 }, // BuffID: 70 diff --git a/TShockAPI/Commands.cs b/TShockAPI/Commands.cs index d39b84e0..d82f10e2 100644 --- a/TShockAPI/Commands.cs +++ b/TShockAPI/Commands.cs @@ -148,24 +148,29 @@ namespace TShockAPI Permissions = new List(); } - public bool Run(string msg, bool silent, TSPlayer ply, List parms) + public bool Run(CommandArgs args) { - if (!CanRun(ply)) + if (!CanRun(args.Player)) return false; try { - CommandDelegate(new CommandArgs(msg, silent, ply, parms)); + CommandDelegate(args); } catch (Exception e) { - ply.SendErrorMessage(GetString("Command failed, check logs for more details.")); + args.Player.SendErrorMessage(GetString("Command failed, check logs for more details.")); TShock.Log.Error(e.ToString()); } return true; } + public bool Run(string msg, bool silent, TSPlayer ply, List parms) + { + return Run(new CommandArgs(msg, silent, ply, parms)); + } + public bool Run(string msg, TSPlayer ply, List parms) { return Run(msg, false, ply, parms); @@ -704,7 +709,12 @@ namespace TShockAPI TShock.Utils.SendLogs(GetString("{0} executed: {1}{2}.", player.Name, silent ? SilentSpecifier : Specifier, cmdText), Color.PaleVioletRed, player); else TShock.Utils.SendLogs(GetString("{0} executed (args omitted): {1}{2}.", player.Name, silent ? SilentSpecifier : Specifier, cmdName), Color.PaleVioletRed, player); - cmd.Run(cmdText, silent, player, args); + + CommandArgs arguments = new CommandArgs(cmdText, silent, player, args); + bool handled = PlayerHooks.OnPrePlayerCommand(cmd, ref arguments); + if (!handled) + cmd.Run(arguments); + PlayerHooks.OnPostPlayerCommand(cmd, arguments, handled); } } return true; @@ -1176,7 +1186,7 @@ namespace TShockAPI try { - TShock.UserAccounts.SetUserGroup(account, args.Parameters[2]); + TShock.UserAccounts.SetUserGroup(args.Player, account, args.Parameters[2]); TShock.Log.ConsoleInfo(GetString("{0} changed account {1} to group {2}.", args.Player.Name, account.Name, args.Parameters[2])); args.Player.SendSuccessMessage(GetString("Account {0} has been changed to group {1}.", account.Name, args.Parameters[2])); @@ -1193,6 +1203,10 @@ namespace TShockAPI { args.Player.SendErrorMessage(GetString($"User {account.Name} does not exist.")); } + catch (UserGroupUpdateLockedException) + { + args.Player.SendErrorMessage(GetString("Hook blocked the attempt to change the user group.")); + } catch (UserAccountManagerException e) { args.Player.SendErrorMessage(GetString($"User {account.Name} could not be added. Check console for details.")); @@ -2044,6 +2058,7 @@ namespace TShockAPI private static void OffNoSave(CommandArgs args) { string reason = ((args.Parameters.Count > 0) ? GetString("Server shutting down: ") + String.Join(" ", args.Parameters) : GetString("Server shutting down.")); + Netplay.SaveOnServerExit = false; TShock.Utils.StopServer(false, reason); } @@ -3070,12 +3085,12 @@ namespace TShockAPI args.Player.SendErrorMessage(GetString("You do not have permission to teleport all other players.")); return; } - for (int i = 0; i < Main.maxPlayers; i++) + foreach (var player in TShock.Players) { - if (Main.player[i].active && (Main.player[i] != args.TPlayer)) + if (player != null && player.Active && player.Index != args.Player.Index) { - if (TShock.Players[i].Teleport(args.TPlayer.position.X, args.TPlayer.position.Y)) - TShock.Players[i].SendSuccessMessage(GetString("You were teleported to {0}.", args.Player.Name)); + if (player.Teleport(args.TPlayer.position.X, args.TPlayer.position.Y)) + player.SendSuccessMessage(GetString("You were teleported to {0}.", args.Player.Name)); } } args.Player.SendSuccessMessage(GetString("Teleported everyone to yourself.")); @@ -4622,21 +4637,22 @@ namespace TShockAPI { if (args.Parameters.Count != 1) { - args.Player.SendErrorMessage(GetString("Invalid syntax. Proper syntax: {0}wind .", Specifier)); + args.Player.SendErrorMessage(GetString("Invalid syntax. Proper syntax: {0}wind .", Specifier)); return; } - int speed; - if (!int.TryParse(args.Parameters[0], out speed) || speed * 100 < 0) + float mph; + if (!float.TryParse(args.Parameters[0], out mph) || mph is < -40f or > 40f) { - args.Player.SendErrorMessage(GetString("Invalid wind speed.")); + args.Player.SendErrorMessage(GetString("Invalid wind speed (must be between -40 and 40).")); return; } + float speed = mph / 50f; // -40 to 40 mph -> -0.8 to 0.8 Main.windSpeedCurrent = speed; Main.windSpeedTarget = speed; TSPlayer.All.SendData(PacketTypes.WorldInfo); - TSPlayer.All.SendInfoMessage(GetString("{0} changed the wind speed to {1}.", args.Player.Name, speed)); + TSPlayer.All.SendInfoMessage(GetString("{0} changed the wind speed to {1}mph.", args.Player.Name, mph)); } #endregion Time/PvpFun Commands @@ -5347,7 +5363,7 @@ namespace TShockAPI foreach (TSPlayer ply in TShock.Players) { - if (ply != null && ply.Active) + if (ply != null && ply.Active && ply.FinishedHandshake) { if (displayIdsRequested) if (ply.Account != null) diff --git a/TShockAPI/Configuration/TShockConfig.cs b/TShockAPI/Configuration/TShockConfig.cs index 79c374b2..c91466fe 100644 --- a/TShockAPI/Configuration/TShockConfig.cs +++ b/TShockAPI/Configuration/TShockConfig.cs @@ -320,8 +320,8 @@ namespace TShockAPI.Configuration [Description("The reason given if banning a mediumcore player on death.")] public string MediumcoreBanReason = GetString("Death results in a ban"); - ///

Disbales IP bans by default, if no arguments are passed to the ban command. - [Description("Disbales IP bans by default, if no arguments are passed to the ban command.")] + /// Disables IP bans by default, if no arguments are passed to the ban command. + [Description("Disables IP bans by default, if no arguments are passed to the ban command.")] public bool DisableDefaultIPBan; /// Enable or disable the whitelist based on IP addresses in the whitelist.txt file. diff --git a/TShockAPI/DB/CharacterManager.cs b/TShockAPI/DB/CharacterManager.cs index 5a5e13a6..357dc746 100644 --- a/TShockAPI/DB/CharacterManager.cs +++ b/TShockAPI/DB/CharacterManager.cs @@ -79,7 +79,7 @@ namespace TShockAPI.DB public PlayerData GetPlayerData(TSPlayer player, int acctid) { - PlayerData playerData = new PlayerData(player); + PlayerData playerData = new PlayerData(false); try { diff --git a/TShockAPI/DB/UserManager.cs b/TShockAPI/DB/UserManager.cs index 0354989e..6fc50d98 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.Hooks; namespace TShockAPI.DB { @@ -166,7 +167,41 @@ namespace TShockAPI.DB if (null == grp) throw new GroupNotExistsException(group); - if (_database.Query("UPDATE Users SET UserGroup = @0 WHERE Username = @1;", group, account.Name) == 0) + 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) + throw new UserAccountNotExistException(account.Name); + + try + { + // Update player group reference for any logged in player + foreach (var player in TShock.Players.Where(p => p != null && p.Account != null && p.Account.Name == account.Name)) + { + player.Group = grp; + } + } + catch (Exception ex) + { + throw new UserAccountManagerException(GetString("SetUserGroup SQL returned an error"), ex); + } + } + /// + /// Sets the group for a given username + /// + /// Who changes the group + /// The user account + /// The user account group to be set + public void SetUserGroup(TSPlayer author, UserAccount account, string group) + { + Group grp = TShock.Groups.GetGroupByName(group); + if (null == grp) + throw new GroupNotExistsException(group); + + 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) throw new UserAccountNotExistException(account.Name); try @@ -619,7 +654,7 @@ namespace TShockAPI.DB public class UserAccountNotExistException : UserAccountManagerException { /// Creates a new UserAccountNotExistException object, with the user account name in the message. - /// The user account name to be pasesd in the message. + /// The user account name to be passed in the message. /// A new UserAccountNotExistException object with a message containing the user account name that does not exist. public UserAccountNotExistException(string name) : base(GetString($"User account {name} does not exist")) @@ -627,6 +662,20 @@ namespace TShockAPI.DB } } + /// The UserGroupUpdateLockedException used when the user group update failed and the request failed as a result.. + [Serializable] + public class UserGroupUpdateLockedException : UserAccountManagerException + { + /// Creates a new UserGroupUpdateLockedException object. + /// The name of the user who failed to change the group. + /// New UserGroupUpdateLockedException object with a message containing the name of the user account that failed to change the group. + public UserGroupUpdateLockedException(string name) : + base(GetString($"Unable to update group of user {name}.")) + { + } + } + + /// A GroupNotExistsException, used when a group does not exist. [Serializable] public class GroupNotExistsException : UserAccountManagerException diff --git a/TShockAPI/GetDataHandlers.cs b/TShockAPI/GetDataHandlers.cs index 7377c7d1..e4c8527e 100644 --- a/TShockAPI/GetDataHandlers.cs +++ b/TShockAPI/GetDataHandlers.cs @@ -95,6 +95,7 @@ namespace TShockAPI { PacketTypes.TileSendSquare, HandleSendTileRect }, { PacketTypes.ItemDrop, HandleItemDrop }, { PacketTypes.ItemOwner, HandleItemOwner }, + { PacketTypes.NpcItemStrike, HandleNpcItemStrike }, { PacketTypes.ProjectileNew, HandleProjectileNew }, { PacketTypes.NpcStrike, HandleNpcStrike }, { PacketTypes.ProjectileDestroy, HandleProjectileKill }, @@ -2248,7 +2249,7 @@ namespace TShockAPI var args = new SyncTilePickingEventArgs { - Player = player, + Player = player, PlayerIndex = playerIndex, TileX = tileX, TileY = tileY, @@ -2619,9 +2620,9 @@ namespace TShockAPI private static bool HandleConnecting(GetDataHandlerArgs args) { var account = TShock.UserAccounts.GetUserAccountByName(args.Player.Name);// - args.Player.DataWhenJoined = new PlayerData(args.Player); + args.Player.DataWhenJoined = new PlayerData(false); args.Player.DataWhenJoined.CopyCharacter(args.Player); - args.Player.PlayerData = new PlayerData(args.Player); + args.Player.PlayerData = new PlayerData(false); args.Player.PlayerData.CopyCharacter(args.Player); if (account != null && !TShock.Config.Settings.DisableUUIDLogin) @@ -2719,49 +2720,71 @@ namespace TShockAPI } byte player = args.Data.ReadInt8(); - short spawnx = args.Data.ReadInt16(); - short spawny = args.Data.ReadInt16(); + short spawnX = args.Data.ReadInt16(); + short spawnY = args.Data.ReadInt16(); int respawnTimer = args.Data.ReadInt32(); short numberOfDeathsPVE = args.Data.ReadInt16(); short numberOfDeathsPVP = args.Data.ReadInt16(); PlayerSpawnContext context = (PlayerSpawnContext)args.Data.ReadByte(); - if (OnPlayerSpawn(args.Player, args.Data, player, spawnx, spawny, respawnTimer, numberOfDeathsPVE, numberOfDeathsPVP, context)) + args.Player.FinishedHandshake = true; + + if (OnPlayerSpawn(args.Player, args.Data, player, spawnX, spawnY, respawnTimer, numberOfDeathsPVE, numberOfDeathsPVP, context)) return true; + + args.Player.Dead = respawnTimer > 0; - if ((Main.ServerSideCharacter) && (spawnx == -1 && spawny == -1)) //this means they want to spawn to vanilla spawn + if (Main.ServerSideCharacter) { - args.Player.sX = Main.spawnTileX; - args.Player.sY = Main.spawnTileY; - args.Player.Teleport(args.Player.sX * 16, (args.Player.sY * 16) - 48); - TShock.Log.ConsoleDebug(GetString("GetDataHandlers / HandleSpawn force teleport 'vanilla spawn' {0}", args.Player.Name)); - } + // As long as the player has not changed his spawnpoint since initial connection, + // we should not use the client's spawnpoint value. This is because the spawnpoint + // value is not saved on the client when SSC is enabled. Hence, we have to assert + // the server-saved spawnpoint value until we can detect that the player has changed + // his spawn. Once we detect the spawnpoint changed, the client's spawnpoint value + // becomes the correct one to use. + // + // Note that spawnpoint changes (right-clicking beds) are not broadcasted to the + // server. Hence, the only way to detect spawnpoint changes is from the + // PlayerSpawn packet. - else if ((Main.ServerSideCharacter) && (args.Player.sX > 0) && (args.Player.sY > 0) && (args.TPlayer.SpawnX > 0) && ((args.TPlayer.SpawnX != args.Player.sX) && (args.TPlayer.SpawnY != args.Player.sY))) - { - args.Player.sX = args.TPlayer.SpawnX; - args.Player.sY = args.TPlayer.SpawnY; - - if (((Main.tile[args.Player.sX, args.Player.sY - 1].active() && Main.tile[args.Player.sX, args.Player.sY - 1].type == TileID.Beds)) && (WorldGen.StartRoomCheck(args.Player.sX, args.Player.sY - 1))) + // handle initial connection + if (args.Player.State == 3) { - args.Player.Teleport(args.Player.sX * 16, (args.Player.sY * 16) - 48); - TShock.Log.ConsoleDebug(GetString("GetDataHandlers / HandleSpawn force teleport phase 1 {0}", args.Player.Name)); - } - } + // server saved spawnpoint value + args.Player.initialSpawn = true; + args.Player.initialServerSpawnX = args.TPlayer.SpawnX; + args.Player.initialServerSpawnY = args.TPlayer.SpawnY; - else if ((Main.ServerSideCharacter) && (args.Player.sX > 0) && (args.Player.sY > 0)) - { - if (((Main.tile[args.Player.sX, args.Player.sY - 1].active() && Main.tile[args.Player.sX, args.Player.sY - 1].type == TileID.Beds)) && (WorldGen.StartRoomCheck(args.Player.sX, args.Player.sY - 1))) + // initial client spawn point, do not use this to spawn the player + // we only use it to detect if the spawnpoint has changed during this session + args.Player.initialClientSpawnX = spawnX; + args.Player.initialClientSpawnY = spawnY; + + // we first let the game handle completing the connection (state 3 => 10), + // then we will spawn the player at the saved spawnpoint in the next second, + // by reasserting the correct spawnpoint value + return false; + } + + // once we detect the client has changed his spawnpoint in the current session, + // the client spawnpoint value will be correct for the rest of the session + if (args.Player.spawnSynced || args.Player.initialClientSpawnX != spawnX || args.Player.initialClientSpawnY != spawnY) { - args.Player.Teleport(args.Player.sX * 16, (args.Player.sY * 16) - 48); - TShock.Log.ConsoleDebug(GetString("GetDataHandlers / HandleSpawn force teleport phase 2 {0}", args.Player.Name)); + // Player has changed his spawnpoint, client and server TPlayer.Spawn{X,Y} is now synced + args.Player.spawnSynced = true; + return false; } - } - if (respawnTimer > 0) - args.Player.Dead = true; - else - args.Player.Dead = false; + // 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)); + args.Player.TeleportSpawnpoint(); + + args.TPlayer.respawnTimer = respawnTimer; + args.TPlayer.numberOfDeathsPVE = numberOfDeathsPVE; + args.TPlayer.numberOfDeathsPVP = numberOfDeathsPVP; + return true; + } return false; } @@ -2942,6 +2965,13 @@ namespace TShockAPI return false; } + private static bool HandleNpcItemStrike(GetDataHandlerArgs args) + { + // Never sent by vanilla client, ignore this + TShock.Log.ConsoleDebug(GetString("GetDataHandlers / HandleNpcItemStrike surprise packet! Someone tell the TShock team! {0}", args.Player.Name)); + return true; + } + private static bool HandleProjectileNew(GetDataHandlerArgs args) { short ident = args.Data.ReadInt16(); @@ -4489,6 +4519,11 @@ namespace TShockAPI return true; } + // Don't modify the player data if it isn't there. + // This is the case whilst the player is connecting, as we receive the SyncLoadout packet before the ContinueConnecting2 packet. + if (args.Player.PlayerData == null) + return false; + // The client does not sync slot changes when changing loadouts, it only tells the server the loadout index changed, // and the server will replicate the changes the client did. This means that PlayerData.StoreSlot is never called, so we need to // swap around the PlayerData items ourself. diff --git a/TShockAPI/Group.cs b/TShockAPI/Group.cs index ff2ba2e9..45086eef 100644 --- a/TShockAPI/Group.cs +++ b/TShockAPI/Group.cs @@ -20,6 +20,8 @@ using System; using System.Linq; using System.Collections.Generic; +using Microsoft.Xna.Framework; + namespace TShockAPI { /// @@ -52,17 +54,17 @@ namespace TShockAPI /// /// The group that this group inherits permissions from. /// - public Group Parent { get; set; } + public virtual Group Parent { get; set; } /// /// The chat prefix for this group. /// - public string Prefix { get; set; } + public virtual string Prefix { get; set; } /// /// The chat suffix for this group. /// - public string Suffix { get; set; } + public virtual string Suffix { get; set; } /// /// The name of the parent, not particularly sure why this is here. @@ -164,6 +166,20 @@ namespace TShockAPI /// public byte B = 255; + /// + /// Simplifies work with the , , properties. + /// + public virtual Color Color + { + get => new Color(R, G, B); + set + { + R = value.R; + G = value.G; + B = value.B; + } + } + /// /// The default group attributed to unregistered users. /// @@ -242,7 +258,7 @@ namespace TShockAPI /// Adds a permission to the list of negated permissions. /// /// The permission to negate. - public void NegatePermission(string permission) + public virtual void NegatePermission(string permission) { // Avoid duplicates if (!negatedpermissions.Contains(permission)) @@ -256,7 +272,7 @@ namespace TShockAPI /// Adds a permission to the list of permissions. /// /// The permission to add. - public void AddPermission(string permission) + public virtual void AddPermission(string permission) { if (permission.StartsWith("!")) { @@ -276,7 +292,7 @@ namespace TShockAPI /// will parse "!permission" and add it to the negated permissions. /// /// The new list of permissions to associate with the group. - public void SetPermission(List permission) + public virtual void SetPermission(List permission) { permissions.Clear(); negatedpermissions.Clear(); @@ -288,7 +304,7 @@ namespace TShockAPI /// where "!permission" will remove a negated permission. /// /// - public void RemovePermission(string permission) + public virtual void RemovePermission(string permission) { if (permission.StartsWith("!")) { @@ -302,7 +318,7 @@ namespace TShockAPI /// Assigns all fields of this instance to another. /// /// The other instance. - public void AssignTo(Group otherGroup) + public virtual void AssignTo(Group otherGroup) { otherGroup.Name = Name; otherGroup.Parent = Parent; diff --git a/TShockAPI/Handlers/SendTileRectHandler.cs b/TShockAPI/Handlers/SendTileRectHandler.cs index d85185b8..d66c3ef6 100644 --- a/TShockAPI/Handlers/SendTileRectHandler.cs +++ b/TShockAPI/Handlers/SendTileRectHandler.cs @@ -78,6 +78,13 @@ namespace TShockAPI.Handlers Removal, } + public enum MatchResult + { + NotMatched, + RejectChanges, + BroadcastChanges, + } + private readonly int Width; private readonly int Height; @@ -179,11 +186,11 @@ namespace TShockAPI.Handlers /// The player the operation originates from. /// The tile rectangle of the operation. /// , if the rect matches this operation and the changes have been applied, otherwise . - public bool Matches(TSPlayer player, TileRect rect) + public MatchResult Matches(TSPlayer player, TileRect rect) { if (rect.Width != Width || rect.Height != Height) { - return false; + return MatchResult.NotMatched; } for (int x = 0; x < rect.Width; x++) @@ -195,7 +202,7 @@ namespace TShockAPI.Handlers { if (tile.Type != TileType) { - return false; + return MatchResult.NotMatched; } } if (Type is MatchType.Placement or MatchType.StateChange) @@ -204,7 +211,7 @@ namespace TShockAPI.Handlers { if (tile.FrameX < 0 || tile.FrameX > MaxFrameX || tile.FrameX % FrameXStep != 0) { - return false; + return MatchResult.NotMatched; } } if (MaxFrameY != IGNORE_FRAME) @@ -214,7 +221,7 @@ namespace TShockAPI.Handlers // this is the only tile type sent in a tile rect where the frame have a different pattern (56, 74, 92 instead of 54, 72, 90) if (!(TileType == TileID.LunarMonolith && tile.FrameY % FrameYStep == 2)) { - return false; + return MatchResult.NotMatched; } } } @@ -223,7 +230,7 @@ namespace TShockAPI.Handlers { if (tile.Active) { - return false; + return MatchResult.NotMatched; } } } @@ -236,7 +243,7 @@ namespace TShockAPI.Handlers if (!player.HasBuildPermission(x, y)) { // for simplicity, let's pretend that the edit was valid, but do not execute it - return true; + return MatchResult.RejectChanges; } } } @@ -257,10 +264,10 @@ namespace TShockAPI.Handlers } } - return false; + return MatchResult.NotMatched; } - private bool MatchPlacement(TSPlayer player, TileRect rect) + private MatchResult MatchPlacement(TSPlayer player, TileRect rect) { for (int x = rect.X; x < rect.Y + rect.Width; x++) { @@ -268,7 +275,7 @@ namespace TShockAPI.Handlers { if (Main.tile[x, y].active()) // the client will kill tiles that auto break before placing the object { - return false; + return MatchResult.NotMatched; } } } @@ -277,7 +284,7 @@ namespace TShockAPI.Handlers if (TShock.TileBans.TileIsBanned((short)TileType, player)) { // for simplicity, let's pretend that the edit was valid, but do not execute it - return true; + return MatchResult.RejectChanges; } for (int x = 0; x < rect.Width; x++) @@ -291,10 +298,10 @@ namespace TShockAPI.Handlers } } - return true; + return MatchResult.BroadcastChanges; } - private bool MatchStateChange(TSPlayer player, TileRect rect) + private MatchResult MatchStateChange(TSPlayer player, TileRect rect) { for (int x = rect.X; x < rect.Y + rect.Width; x++) { @@ -302,7 +309,7 @@ namespace TShockAPI.Handlers { if (!Main.tile[x, y].active() || Main.tile[x, y].type != TileType) { - return false; + return MatchResult.NotMatched; } } } @@ -322,18 +329,18 @@ namespace TShockAPI.Handlers } } - return true; + return MatchResult.BroadcastChanges; } - private bool MatchRemoval(TSPlayer player, TileRect rect) + private MatchResult MatchRemoval(TSPlayer player, TileRect rect) { - for (int x = rect.X; x < rect.Y + rect.Width; x++) + for (int x = rect.X; x < rect.X + rect.Width; x++) { for (int y = rect.Y; y < rect.Y + rect.Height; y++) { if (!Main.tile[x, y].active() || Main.tile[x, y].type != TileType) { - return false; + return MatchResult.NotMatched; } } } @@ -348,7 +355,7 @@ namespace TShockAPI.Handlers } } - return true; + return MatchResult.BroadcastChanges; } } @@ -364,7 +371,7 @@ namespace TShockAPI.Handlers TileRectMatch.Placement(2, 3, TileID.TargetDummy, 54, 36, 18, 18), TileRectMatch.Placement(3, 4, TileID.TeleportationPylon, 468, 54, 18, 18), TileRectMatch.Placement(2, 3, TileID.DisplayDoll, 126, 36, 18, 18), - TileRectMatch.Placement(2, 3, TileID.HatRack, 90, 54, 18, 18), + TileRectMatch.Placement(3, 4, TileID.HatRack, 90, 54, 18, 18), TileRectMatch.Placement(2, 2, TileID.ItemFrame, 162, 18, 18, 18), TileRectMatch.Placement(3, 3, TileID.WeaponsRack2, 90, 36, 18, 18), TileRectMatch.Placement(1, 1, TileID.FoodPlatter, 18, 0, 18, 18), @@ -436,7 +443,7 @@ namespace TShockAPI.Handlers TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from throttle from {args.Player.Name}")); // send correcting data - args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + args.Player.SendTileRect(args.TileX, args.TileY, args.Width, args.Length); return; } @@ -446,7 +453,7 @@ namespace TShockAPI.Handlers TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from being disabled from {args.Player.Name}")); // send correcting data - args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + args.Player.SendTileRect(args.TileX, args.TileY, args.Width, args.Length); return; } @@ -468,7 +475,7 @@ namespace TShockAPI.Handlers TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from {args.Player.Name}")); // send correcting data - args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + args.Player.SendTileRect(args.TileX, args.TileY, args.Width, args.Length); return; } @@ -478,7 +485,7 @@ namespace TShockAPI.Handlers TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from out of range from {args.Player.Name}")); // send correcting data - args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + args.Player.SendTileRect(args.TileX, args.TileY, args.Width, args.Length); return; } @@ -488,19 +495,23 @@ namespace TShockAPI.Handlers TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from {args.Player.Name}")); // send correcting data - args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + args.Player.SendTileRect(args.TileX, args.TileY, args.Width, args.Length); return; } // check if the rect matches any valid operation foreach (TileRectMatch match in Matches) { - if (match.Matches(args.Player, rect)) + var result = match.Matches(args.Player, rect); + if (result != TileRectMatch.MatchResult.NotMatched) { TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from {args.Player.Name}")); // send correcting data - args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + if (result == TileRectMatch.MatchResult.RejectChanges) + args.Player.SendTileRect(args.TileX, args.TileY, args.Width, args.Length); + if (result == TileRectMatch.MatchResult.BroadcastChanges) + TSPlayer.All.SendTileRect(args.TileX, args.TileY, args.Width, args.Length); return; } } @@ -511,14 +522,14 @@ namespace TShockAPI.Handlers TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from {args.Player.Name}")); // send correcting data - args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + args.Player.SendTileRect(args.TileX, args.TileY, args.Width, args.Length); return; } TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from matches from {args.Player.Name}")); // send correcting data - args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + args.Player.SendTileRect(args.TileX, args.TileY, args.Width, args.Length); return; } @@ -910,7 +921,7 @@ namespace TShockAPI.Handlers } } - /* + /* * This is a copy of the `WorldGen.Convert` method with the following precise changes: * - Added a `MockTile tile` parameter * - Changed the `i` and `j` parameters to `k` and `l` @@ -921,7 +932,7 @@ namespace TShockAPI.Handlers * - Removed the ifs checking the bounds of the tile and wall types * - Removed branches that would call `WorldGen.KillTile` * - Changed branches depending on randomness to instead set the property to both values after one another - * + * * This overall leads to a method that can be called on a MockTile and real-world coordinates and will spit out the proper conversion changes into the MockTile. */ diff --git a/TShockAPI/Hooks/AccountHooks.cs b/TShockAPI/Hooks/AccountHooks.cs index 9c08b26d..ae9fff24 100644 --- a/TShockAPI/Hooks/AccountHooks.cs +++ b/TShockAPI/Hooks/AccountHooks.cs @@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ +using System.ComponentModel; using TShockAPI.DB; namespace TShockAPI.Hooks { @@ -39,6 +40,31 @@ namespace TShockAPI.Hooks } } + public class AccountGroupUpdateEventArgs : HandledEventArgs + { + public string AccountName { get; private set; } + public Group Group { get; set; } + + public AccountGroupUpdateEventArgs(string accountName, Group group) + { + this.AccountName = accountName; + this.Group = group; + } + } + + public class AccountGroupUpdateByPlayerEventArgs : AccountGroupUpdateEventArgs + { + /// + /// The player who updated the user's group + /// + public TSPlayer Player { get; private set; } + + public AccountGroupUpdateByPlayerEventArgs(TSPlayer player, string accountName, Group group) : base(accountName, group) + { + this.Player = player; + } + } + public class AccountHooks { public delegate void AccountCreateD(AccountCreateEventArgs e); @@ -62,5 +88,25 @@ namespace TShockAPI.Hooks AccountDelete(new AccountDeleteEventArgs(u)); } + + public delegate void AccountGroupUpdateD(AccountGroupUpdateEventArgs e); + public static event AccountGroupUpdateD AccountGroupUpdate; + + public static bool OnAccountGroupUpdate(UserAccount account, TSPlayer author, ref Group group) + { + AccountGroupUpdateEventArgs args = new AccountGroupUpdateByPlayerEventArgs(author, account.Name, group); + AccountGroupUpdate?.Invoke(args); + group = args.Group; + + return args.Handled; + } + public static bool OnAccountGroupUpdate(UserAccount account, ref Group group) + { + AccountGroupUpdateEventArgs args = new AccountGroupUpdateEventArgs(account.Name, group); + AccountGroupUpdate?.Invoke(args); + group = args.Group; + + return args.Handled; + } } } diff --git a/TShockAPI/Hooks/PlayerHooks.cs b/TShockAPI/Hooks/PlayerHooks.cs index 7a3e2067..43756464 100644 --- a/TShockAPI/Hooks/PlayerHooks.cs +++ b/TShockAPI/Hooks/PlayerHooks.cs @@ -16,6 +16,7 @@ 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.ComponentModel; using TShockAPI.DB; @@ -119,6 +120,49 @@ namespace TShockAPI.Hooks public string CommandPrefix { get; set; } } + /// + /// EventArgs used for the event. + /// + public class PrePlayerCommandEventArgs : HandledEventArgs + { + /// + /// The command entered by the player. + /// + public Command Command { get; } + /// + /// Command arguments. + /// + public CommandArgs Arguments { get; set; } + + public PrePlayerCommandEventArgs(Command command, CommandArgs args) + { + Command = command; + Arguments = args; + } + } + + /// + /// EventArgs used for the event. + /// + public class PostPlayerCommandEventArgs : HandledEventArgs + { + /// + /// The command entered by the player. + /// + public Command Command { get; } + /// + /// Command arguments. + /// + public CommandArgs Arguments { get; } + + public PostPlayerCommandEventArgs(Command command, CommandArgs arguments, bool handled) + { + Command = command; + Arguments = arguments; + Handled = handled; + } + } + /// /// EventArgs used for the event. /// @@ -343,6 +387,26 @@ namespace TShockAPI.Hooks /// public static event PlayerCommandD PlayerCommand; + /// + /// The delegate of the event. + /// + /// The EventArgs for this event. + public delegate void PrePlayerCommandD(PrePlayerCommandEventArgs e); + /// + /// Fired before a command is run. + /// + public static event PrePlayerCommandD PrePlayerCommand; + + /// + /// The delegate of the event. + /// + /// The EventArgs for this event. + public delegate void PostPlayerCommandD(PostPlayerCommandEventArgs e); + /// + /// Fired after a command is run. + /// + public static event PostPlayerCommandD PostPlayerCommand; + /// /// The delegate of the event. /// @@ -449,6 +513,40 @@ namespace TShockAPI.Hooks return playerCommandEventArgs.Handled; } + /// + /// Fires the event. + /// + /// Command to be executed + /// Command arguments + /// True if the event has been handled. + public static bool OnPrePlayerCommand(Command cmd, ref CommandArgs arguments) + { + if (PrePlayerCommand == null) + return false; + + PrePlayerCommandEventArgs args = new PrePlayerCommandEventArgs(cmd, arguments); + + PrePlayerCommand(args); + + arguments = args.Arguments; + return args.Handled; + } + + /// + /// Fires the event. + /// + /// Executed command. + /// Command arguments. + /// Is the command executed. + public static void OnPostPlayerCommand(Command cmd, CommandArgs arguments, bool handled) + { + if (PostPlayerCommand == null) + return; + + PostPlayerCommandEventArgs args = new PostPlayerCommandEventArgs(cmd, arguments, handled); + PostPlayerCommand(args); + } + /// /// Fires the event. /// diff --git a/TShockAPI/I18n.cs b/TShockAPI/I18n.cs index e8f5eedb..ac2985d0 100644 --- a/TShockAPI/I18n.cs +++ b/TShockAPI/I18n.cs @@ -60,6 +60,12 @@ namespace TShockAPI } } + if (LanguageManager.Instance.ActiveCulture == GameCulture.DefaultCulture) + { + var bf = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static; + // LanguageManager.SetLanguage will change this so we need to reset it back to null + typeof(CultureInfo).GetField("s_currentThreadUICulture", bf)?.SetValue(null, null); + } return CultureInfo.CurrentUICulture; } } diff --git a/TShockAPI/Localization/EnglishLanguage.cs b/TShockAPI/Localization/EnglishLanguage.cs index 1ec4ecea..334c4780 100644 --- a/TShockAPI/Localization/EnglishLanguage.cs +++ b/TShockAPI/Localization/EnglishLanguage.cs @@ -20,7 +20,9 @@ using System; using System.Collections.Generic; using System.Linq; using Terraria; +using Terraria.Initializers; using Terraria.Localization; +using Terraria.UI.Chat; namespace TShockAPI.Localization { @@ -37,6 +39,8 @@ namespace TShockAPI.Localization private static readonly Dictionary Buffs = new Dictionary(); + private static readonly Dictionary VanillaCommandsPrefixs = new Dictionary(); + internal static void Initialize() { var culture = Language.ActiveCulture; @@ -71,6 +75,15 @@ namespace TShockAPI.Localization var i = (int)field.GetValue(null); Prefixs.Add(i, Lang.prefix[i].Value); } + + ChatInitializer.Load(); + foreach (var command in ChatManager.Commands._localizedCommands) + { + if (VanillaCommandsPrefixs.ContainsKey(command.Value._name)) + continue; + VanillaCommandsPrefixs.Add(command.Value._name,command.Key.Value); + } + ChatManager.Commands._localizedCommands.Clear(); } finally { @@ -136,5 +149,18 @@ namespace TShockAPI.Localization return null; } + + /// + /// Get vanilla command prefix in English + /// + /// vanilla command name + /// vanilla command prefix in English + public static string GetCommandPrefixByName(string name) + { + string commandText; + if (VanillaCommandsPrefixs.TryGetValue(name, out commandText)) + return commandText; + return null; + } } } diff --git a/TShockAPI/NetItem.cs b/TShockAPI/NetItem.cs index 278fda1c..0f0dc6e3 100644 --- a/TShockAPI/NetItem.cs +++ b/TShockAPI/NetItem.cs @@ -16,7 +16,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ - using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -155,13 +155,39 @@ namespace TShockAPI /// The net ID. /// The stack. /// The prefix ID. - public NetItem(int netId, int stack, byte prefixId) + public NetItem(int netId, int stack = 1, byte prefixId = 0) { _netId = netId; _stack = stack; _prefixId = prefixId; } + /// + /// Creates a new . + /// + /// Item in the game. + public NetItem(Item item) + { + _netId = item.netID; + _stack = item.stack; + _prefixId = item.prefix; + } + + /// + /// Creates based on data from this structure. + /// + /// A copy of the item. + public Item ToItem() + { + Item item = new Item(); + + item.netDefaults(_netId); + item.stack = _stack; + item.prefix = _prefixId; + + return item; + } + /// /// Converts the to a string. /// diff --git a/TShockAPI/Permissions.cs b/TShockAPI/Permissions.cs index 6aea9859..4dd84c67 100644 --- a/TShockAPI/Permissions.cs +++ b/TShockAPI/Permissions.cs @@ -532,18 +532,9 @@ namespace TShockAPI field.GetCustomAttributes(false).FirstOrDefault(o => o is DescriptionAttribute) as DescriptionAttribute; var desc = descattr != null && !string.IsNullOrWhiteSpace(descattr.Description) ? descattr.Description : GetString("No description available."); - var commands = GetCommands(name); - foreach (var c in commands) - { - for (var i = 0; i < c.Names.Count; i++) - { - c.Names[i] = "/" + c.Names[i]; - } - } - var strs = - commands.Select( - c => - c.Name + (c.Names.Count > 1 ? " ({0})".SFormat(string.Join(" ", c.Names.ToArray(), 1, c.Names.Count - 1)) : "")); + var strs = GetCommands(name).Select(c => c.Names.Count > 1 + ? $"/{c.Name} (/{string.Join(" /", c.Names.Skip(1))})" + : $"/{c.Name}"); sb.AppendLine($"## {name}"); sb.AppendLine($"{desc}"); diff --git a/TShockAPI/PlayerData.cs b/TShockAPI/PlayerData.cs index 2a8dd41f..c84da7c7 100644 --- a/TShockAPI/PlayerData.cs +++ b/TShockAPI/PlayerData.cs @@ -23,6 +23,7 @@ using Terraria.Localization; using Terraria.GameContent.NetModules; using Terraria.Net; using Terraria.ID; +using System; namespace TShockAPI { @@ -63,18 +64,27 @@ namespace TShockAPI public int unlockedSuperCart; public int enabledSuperCart; - public PlayerData(TSPlayer player) + /// + /// Sets the default values for the inventory. + /// + [Obsolete("The player argument is not used.")] + public PlayerData(TSPlayer player) : this(true) { } + + /// + /// Sets the default values for the inventory. + /// + /// Is it necessary to load items from TShock's config + public PlayerData(bool includingStarterInventory = true) { for (int i = 0; i < NetItem.MaxInventory; i++) - { this.inventory[i] = new NetItem(); - } - for (int i = 0; i < TShock.ServerSideCharacterConfig.Settings.StartingInventory.Count; i++) - { - var item = TShock.ServerSideCharacterConfig.Settings.StartingInventory[i]; - StoreSlot(i, item.NetId, item.PrefixId, item.Stack); - } + if (includingStarterInventory) + for (int i = 0; i < TShock.ServerSideCharacterConfig.Settings.StartingInventory.Count; i++) + { + var item = TShock.ServerSideCharacterConfig.Settings.StartingInventory[i]; + StoreSlot(i, item.NetId, item.PrefixId, item.Stack); + } } /// @@ -86,12 +96,22 @@ namespace TShockAPI /// public void StoreSlot(int slot, int netID, byte prefix, int stack) { - if (slot > (this.inventory.Length - 1)) //if the slot is out of range then dont save + StoreSlot(slot, new NetItem(netID, stack, prefix)); + } + + /// + /// Stores an item at the specific storage slot + /// + /// + /// + public void StoreSlot(int slot, NetItem item) + { + if (slot > (this.inventory.Length - 1) || slot < 0) //if the slot is out of range then dont save { return; } - this.inventory[slot] = new NetItem(netID, stack, prefix); + this.inventory[slot] = item; } /// @@ -104,16 +124,8 @@ namespace TShockAPI this.maxHealth = player.TPlayer.statLifeMax; this.mana = player.TPlayer.statMana; this.maxMana = player.TPlayer.statManaMax; - if (player.sX > 0 && player.sY > 0) - { - this.spawnX = player.sX; - this.spawnY = player.sY; - } - else - { - this.spawnX = player.TPlayer.SpawnX; - this.spawnY = player.TPlayer.SpawnY; - } + this.spawnX = player.TPlayer.SpawnX; + this.spawnY = player.TPlayer.SpawnY; extraSlot = player.TPlayer.extraAccessory ? 1 : 0; this.skinVariant = player.TPlayer.skinVariant; this.hair = player.TPlayer.hair; @@ -266,8 +278,6 @@ namespace TShockAPI player.TPlayer.statManaMax = this.maxMana; player.TPlayer.SpawnX = this.spawnX; player.TPlayer.SpawnY = this.spawnY; - player.sX = this.spawnX; - player.sY = this.spawnY; player.TPlayer.hairDye = this.hairDye; player.TPlayer.anglerQuestsFinished = this.questsCompleted; player.TPlayer.UsingBiomeTorches = this.usingBiomeTorches == 1; diff --git a/TShockAPI/Rest/Rest.cs b/TShockAPI/Rest/Rest.cs index 58cc23d8..a6e681e2 100644 --- a/TShockAPI/Rest/Rest.cs +++ b/TShockAPI/Rest/Rest.cs @@ -351,7 +351,6 @@ namespace Rests { str = string.Format("{0}({1});", jsonp, str); } - e.Response.Connection.Type = ConnectionType.Close; e.Response.ContentType = new ContentTypeHeader("application/json; charset=utf-8"); e.Response.Add(serverHeader); var bytes = Encoding.UTF8.GetBytes(str); diff --git a/TShockAPI/Rest/RestManager.cs b/TShockAPI/Rest/RestManager.cs index c41e7767..5dd2ff20 100644 --- a/TShockAPI/Rest/RestManager.cs +++ b/TShockAPI/Rest/RestManager.cs @@ -402,7 +402,7 @@ namespace TShockAPI {"serverversion", Main.versionNumber}, {"tshockversion", TShock.VersionNum}, {"port", TShock.Config.Settings.ServerPort}, - {"playercount", Main.player.Where(p => null != p && p.active).Count()}, + {"playercount", TShock.Utils.GetActivePlayerCount()}, {"maxplayers", TShock.Config.Settings.MaxSlots}, {"world", (TShock.Config.Settings.UseServerName ? TShock.Config.Settings.ServerName : Main.worldName)}, {"uptime", (DateTime.Now - System.Diagnostics.Process.GetCurrentProcess().StartTime).ToString(@"d'.'hh':'mm':'ss")}, @@ -555,7 +555,8 @@ namespace TShockAPI { try { - TShock.UserAccounts.SetUserGroup(account, group); + TShock.UserAccounts.SetUserGroup(new TSRestPlayer(args.TokenData.Username, TShock.Groups.GetGroupByName(args.TokenData.UserGroupName)), + account, group); response.Add("group-response", "Group updated successfully"); } catch (Exception e) @@ -944,8 +945,8 @@ namespace TShockAPI [Token] private object PlayerList(RestRequestArgs args) { - var activeplayers = Main.player.Where(p => null != p && p.active).ToList(); - return new RestObject() { { "players", string.Join(", ", activeplayers.Select(p => p.name)) } }; + var activeplayers = TShock.Players.Where(p => null != p && p.Active).Select(p => p.Name); + return new RestObject() { { "players", string.Join(", ", activeplayers) } }; } [Description("Fetches detailed user information on all connected users, and can be filtered by specifying a key value pair filter users where the key is a field and the value is a users field value.")] diff --git a/TShockAPI/TSPlayer.cs b/TShockAPI/TSPlayer.cs index c9194c36..f6c06f85 100644 --- a/TShockAPI/TSPlayer.cs +++ b/TShockAPI/TSPlayer.cs @@ -177,8 +177,13 @@ namespace TShockAPI /// public int RPPending = 0; - public int sX = -1; - public int sY = -1; + + public bool initialSpawn = false; + public int initialServerSpawnX = -2; + public int initialServerSpawnY = -2; + public bool spawnSynced = false; + public int initialClientSpawnX = -2; + public int initialClientSpawnY = -2; /// /// A queue of tiles destroyed by the player for reverting. @@ -351,6 +356,9 @@ namespace TShockAPI /// Determines if the player is disabled for not clearing their trash. A re-login is the only way to reset this. public bool IsDisabledPendingTrashRemoval; + /// Determines if the player has finished the handshake (Sent all necessary packets for connection, such as Request World Data, Spawn Player, etc). A normal client would do all of this no problem. + public bool FinishedHandshake = false; + /// Checks to see if active throttling is happening on events by Bouncer. Rejects repeated events by malicious clients in a short window. /// If the player is currently being throttled by Bouncer, or not. public bool IsBouncerThrottled() @@ -1284,7 +1292,7 @@ namespace TShockAPI } } - PlayerData = new PlayerData(this); + PlayerData = new PlayerData(); Group = TShock.Groups.GetGroupByName(TShock.Config.Settings.DefaultGuestGroupName); tempGroup = null; if (tempGroupTimer != null) @@ -1380,6 +1388,25 @@ namespace TShockAPI return true; } + /// + /// Teleports the player to their spawnpoint. + /// Teleports to main spawnpoint if their bed is not active. + /// Supports SSC. + /// + public bool TeleportSpawnpoint() + { + // NOTE: it is vanilla behaviour to not permanently override the spawnpoint if the bed spawn is broken/invalid + int x = TPlayer.SpawnX; + int y = TPlayer.SpawnY; + if ((x == -1 && y == -1) || + !Main.tile[x, y - 1].active() || Main.tile[x, y - 1].type != TileID.Beds || !WorldGen.StartRoomCheck(x, y - 1)) + { + x = Main.spawnTileX; + y = Main.spawnTileY; + } + return Teleport(x * 16, y * 16 - 48); + } + /// /// Heals the player. /// @@ -1394,14 +1421,7 @@ namespace TShockAPI /// public void Spawn(PlayerSpawnContext context, int? respawnTimer = null) { - if (this.sX > 0 && this.sY > 0) - { - Spawn(this.sX, this.sY, context, respawnTimer); - } - else - { - Spawn(TPlayer.SpawnX, TPlayer.SpawnY, context, respawnTimer); - } + Spawn(TPlayer.SpawnX, TPlayer.SpawnY, context, respawnTimer); } /// diff --git a/TShockAPI/TShock.cs b/TShockAPI/TShock.cs index 054e31df..c5f2b968 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 = "Intensity"; + public static readonly string VersionCodename = "East"; /// 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"; @@ -428,6 +428,8 @@ namespace TShockAPI Hooks.AccountHooks.AccountDelete += OnAccountDelete; Hooks.AccountHooks.AccountCreate += OnAccountCreate; + On.Terraria.RemoteClient.Reset += RemoteClient_Reset; + GetDataHandlers.InitGetDataHandler(); Commands.InitCommands(); @@ -496,6 +498,12 @@ 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) @@ -1174,16 +1182,16 @@ namespace TShockAPI if (player.RecentFuse > 0) player.RecentFuse--; - if ((Main.ServerSideCharacter) && (player.TPlayer.SpawnX > 0) && (player.sX != player.TPlayer.SpawnX)) + if (Main.ServerSideCharacter && player.initialSpawn) { - player.sX = player.TPlayer.SpawnX; - player.sY = player.TPlayer.SpawnY; - } + player.initialSpawn = false; - if ((Main.ServerSideCharacter) && (player.sX > 0) && (player.sY > 0) && (player.TPlayer.SpawnX < 0)) - { - player.TPlayer.SpawnX = player.sX; - player.TPlayer.SpawnY = player.sY; + // reassert the correct spawnpoint value after the game's Spawn handler changed it + player.TPlayer.SpawnX = player.initialServerSpawnX; + player.TPlayer.SpawnY = player.initialServerSpawnY; + + player.TeleportSpawnpoint(); + TShock.Log.ConsoleDebug(GetString("OnSecondUpdate / initial ssc spawn for {0} at ({1}, {2})", player.Name, player.TPlayer.SpawnX, player.TPlayer.SpawnY)); } if (player.RPPending > 0) @@ -1368,6 +1376,8 @@ namespace TShockAPI } } } + + Bans.CheckBan(player); Players[args.Who] = player; } @@ -1389,7 +1399,8 @@ namespace TShockAPI return; } - Bans.CheckBan(player); + if (Bans.CheckBan(player)) + return; } /// OnLeave - Called when a player leaves the server. @@ -1429,7 +1440,7 @@ namespace TShockAPI if (tsplr.ReceivedInfo) { - if (!tsplr.SilentKickInProgress && tsplr.State >= 3) + if (!tsplr.SilentKickInProgress && tsplr.State >= 3 && tsplr.FinishedHandshake) //The player has left, do not broadcast any clients exploiting the behaviour of not spawning their player. Utils.Broadcast(GetString("{0} has left.", tsplr.Name), Color.Yellow); Log.Info(GetString("{0} disconnected.", tsplr.Name)); @@ -1450,6 +1461,9 @@ namespace TShockAPI } } + + tsplr.FinishedHandshake = false; + // Fire the OnPlayerLogout hook too, if the player was logged in and they have a TSPlayer object. if (tsplr.IsLoggedIn) { @@ -1479,6 +1493,12 @@ namespace TShockAPI return; } + if (!tsplr.FinishedHandshake) + { + args.Handled = true; + return; + } + if (args.Text.Length > 500) { tsplr.Kick(GetString("Crash attempt via long chat packet."), true); @@ -1497,11 +1517,11 @@ namespace TShockAPI { if (!String.IsNullOrEmpty(text)) { - text = item.Key.Value + ' ' + text; + text = EnglishLanguage.GetCommandPrefixByName(item.Value._name) + ' ' + text; } else { - text = item.Key.Value; + text = EnglishLanguage.GetCommandPrefixByName(item.Value._name); } break; } @@ -1695,14 +1715,14 @@ namespace TShockAPI Log.Info(GetString("{0} ({1}) from '{2}' group from '{3}' joined. ({4}/{5})", player.Name, player.IP, player.Group.Name, player.Country, TShock.Utils.GetActivePlayerCount(), TShock.Config.Settings.MaxSlots)); - if (!player.SilentJoinInProgress) + if (!player.SilentJoinInProgress && player.FinishedHandshake) Utils.Broadcast(GetString("{0} ({1}) has joined.", player.Name, player.Country), Color.Yellow); } else { Log.Info(GetString("{0} ({1}) from '{2}' group joined. ({3}/{4})", player.Name, player.IP, player.Group.Name, TShock.Utils.GetActivePlayerCount(), TShock.Config.Settings.MaxSlots)); - if (!player.SilentJoinInProgress) + if (!player.SilentJoinInProgress && player.FinishedHandshake) Utils.Broadcast(GetString("{0} has joined.", player.Name), Color.Yellow); } diff --git a/TShockAPI/TShockAPI.csproj b/TShockAPI/TShockAPI.csproj index a3731709..88f61606 100644 --- a/TShockAPI/TShockAPI.csproj +++ b/TShockAPI/TShockAPI.csproj @@ -18,7 +18,7 @@ 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.0 + 5.2.2 TShock for Terraria Pryaxis & TShock Contributors TShockAPI @@ -34,7 +34,7 @@ - + diff --git a/TShockAPI/Utils.cs b/TShockAPI/Utils.cs index 4ee41c01..613e7a8a 100644 --- a/TShockAPI/Utils.cs +++ b/TShockAPI/Utils.cs @@ -172,7 +172,7 @@ namespace TShockAPI foreach (TSPlayer player in TShock.Players) { if (player != null && player != excludedPlayer && player.Active && player.HasPermission(Permissions.logs) && - player.DisplayLogs && TShock.Config.Settings.DisableSpewLogs == false) + player.DisplayLogs && !TShock.Config.Settings.DisableSpewLogs) player.SendMessage(log, color); } } @@ -183,7 +183,7 @@ namespace TShockAPI /// The number of active players on the server. public int GetActivePlayerCount() { - return Main.player.Where(p => null != p && p.active).Count(); + return TShock.Players.Count(p => null != p && p.Active && p.FinishedHandshake); } //Random should not be generated in a method @@ -1149,11 +1149,15 @@ namespace TShockAPI /// If the server is empty; determines if we should use Utils.GetActivePlayerCount() for player count or 0. internal void SetConsoleTitle(bool empty) { + if (ShouldSkipTitle) + return; Console.Title = GetString("{0}{1}/{2} on {3} @ {4}:{5} (TShock for Terraria v{6})", !string.IsNullOrWhiteSpace(TShock.Config.Settings.ServerName) ? TShock.Config.Settings.ServerName + " - " : "", empty ? 0 : GetActivePlayerCount(), TShock.Config.Settings.MaxSlots, Main.worldName, Netplay.ServerIP.ToString(), Netplay.ListenPort, TShock.VersionNum); } + // Some terminals doesn't supports XTerm escape sequences for setting the title + private static bool ShouldSkipTitle = !System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows) && !(Environment.GetEnvironmentVariable("TERM")?.Contains("xterm") ?? false); /// Determines the distance between two vectors. /// The first vector location. diff --git a/TShockInstaller/TShockInstaller.csproj b/TShockInstaller/TShockInstaller.csproj index 9b73d66a..ac3bfff4 100644 --- a/TShockInstaller/TShockInstaller.csproj +++ b/TShockInstaller/TShockInstaller.csproj @@ -12,6 +12,6 @@ - + diff --git a/TShockLauncher/Program.cs b/TShockLauncher/Program.cs index 4a139370..0163cc2d 100644 --- a/TShockLauncher/Program.cs +++ b/TShockLauncher/Program.cs @@ -29,12 +29,30 @@ along with this program. If not, see . using System.Reflection; using TShockPluginManager; +// On occasion, users have been seen extracting TShock into their client installation directory -- this is of course incorrect, and is known +// to cause issues. Let's attempt to catch this before anything happens (specifically, before Terraria assemblies are resolved) and prevent +// TShock from launching. +if (File.Exists("TerrariaServer.exe")) +{ + Console.ForegroundColor = ConsoleColor.Red; + Console.Error.WriteLine("A \"TerrariaServer.exe\" file has been found in the current working directory."); + Console.Error.WriteLine( + "This indicates either installation into a Terraria client directory, or installation into a legacy (TShock 4 or older) TShock directory."); + Console.Error.WriteLine( + "TShock is never to be installed inside a Terraria client directory. You should instead extract your TShock installation into it's own directory."); + Console.Error.WriteLine( + "If you are updating a legacy TShock installation, please follow the following documentation to update: https://ikebukuro.tshock.co/#/?id=upgrading-from-tshock-4"); + Console.Error.WriteLine("The launcher will now exit."); + Console.ResetColor(); + return 1; +} + if (args.Length > 0 && args[0].ToLower() == "plugins") { var items = args.ToList(); items.RemoveAt(0); await NugetCLI.Main(items); - return; + return 0; } @@ -42,7 +60,7 @@ Dictionary _cache = new Dictionary(); System.Runtime.Loader.AssemblyLoadContext.Default.Resolving += Default_Resolving; -Start(); +return Start(); /// /// Resolves a module from the ./bin folder, either with a .dll by preference or .exe @@ -70,7 +88,8 @@ 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() +int Start() { TerrariaApi.Server.Program.Main(args); + return 0; } diff --git a/TShockLauncher/TShockLauncher.csproj b/TShockLauncher/TShockLauncher.csproj index e3c4ac32..fbe428bb 100644 --- a/TShockLauncher/TShockLauncher.csproj +++ b/TShockLauncher/TShockLauncher.csproj @@ -30,7 +30,7 @@ - + diff --git a/TShockPluginManager/Nuget.cs b/TShockPluginManager/Nuget.cs index 9fdefb6b..1d00fe1c 100644 --- a/TShockPluginManager/Nuget.cs +++ b/TShockPluginManager/Nuget.cs @@ -30,7 +30,6 @@ using NuGet.Versioning; namespace TShockPluginManager { - public class Nugetter { // this object can figure out the right framework folders to use from a set of packages @@ -82,15 +81,13 @@ namespace TShockPluginManager // make sure the source repository can actually tell us about dependencies var dependencyInfoResource = await sourceRepository.GetResourceAsync(); // get the try and dependencies - // (the above function returns a nullable value, but doesn't properly indicate it as such) - #pragma warning disable CS8602 - var dependencyInfo = await dependencyInfoResource?.ResolvePackage( + if (dependencyInfoResource is null) continue; + var dependencyInfo = await dependencyInfoResource.ResolvePackage( package, framework, cacheContext, logger, CancellationToken.None); - #pragma warning restore CS8602 // oop, we don't have the ability to get dependency info from this repository, or // it wasn't found. let's try the next source repository! - if (dependencyInfo == null) continue; + if (dependencyInfo is null) continue; availablePackages.Add(dependencyInfo); foreach (var dependency in dependencyInfo.Dependencies) @@ -302,8 +299,11 @@ namespace TShockPluginManager var relativeFolder = Path.GetDirectoryName(packageRelativeFilePath); var targetFolder = Path.Join(isPlugin ? "./ServerPlugins" : "./bin", relativeFolder); - Directory.CreateDirectory(targetFolder); - File.Copy(filePath, Path.Join(targetFolder, Path.GetFileName(filePath)), true); + if (File.Exists(filePath)) + { + Directory.CreateDirectory(targetFolder); + File.Copy(filePath, Path.Join(targetFolder, Path.GetFileName(filePath)), true); + } } } } diff --git a/TShockPluginManager/TShockPluginManager.csproj b/TShockPluginManager/TShockPluginManager.csproj index 33c503fd..1d53f320 100644 --- a/TShockPluginManager/TShockPluginManager.csproj +++ b/TShockPluginManager/TShockPluginManager.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/TerrariaServerAPI b/TerrariaServerAPI index 8a3fffd7..d4bb7e3a 160000 --- a/TerrariaServerAPI +++ b/TerrariaServerAPI @@ -1 +1 @@ -Subproject commit 8a3fffd71db401736ea80619122c70c449c10ff3 +Subproject commit d4bb7e3a21e875cfeb23bcf5cf847c85d9470ccf diff --git a/docs/changelog.md b/docs/changelog.md index 489afcce..49a579ed 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -78,6 +78,33 @@ Use past tense when adding new entries; sign your name off when you add or chang * If there is no section called "Upcoming changes" below this line, please add one with `## Upcoming changes` as the first line, and then a bulleted item directly after with the first change. --> ## Upcoming changes +* Added a variable for handshake (True upon spawn player), clients no longer notify others of their presence and cant chat if this is never set to true. (@ohayo) +* Fixed a security issue with how bans are handled on join. (@ohayo) +* Fixed `/dump-reference-data` mutate the command names. (#2943, @sgkoishi) +* Added `ParryDamageBuff` (Striking Moment with Brand of the Inferno and shield) for player, updated `CursedInferno` buff for NPC (@sgkoishi, #3005) +* Changed the use of `Player.active` to `TSPlayer.Active` for consistency. (@sgkoishi, #2939) +* Fix typo in config for IP bans. (@redchess64) +* Updated `TShockAPI.NetItem` (@AgaSpace): + * Added constructor overload with parameter `Terraria.Item`. + * Added the `ToItem` method to get a copy of `Terraria.Item`. + * In the constructor `stack` and `prefix` are now optional parameters. +* Fixed unable to transfer long response body for REST API. (@sgkoishi, #2925) +* Fixed the `/wind` command not being very helpful. (@punchready) +* Fixed /help, /me, and /p commands can't work in non-English languages. (@ACaiCat) +* Added a hook `AccountHooks.AccountGroupUpdate`, which is called when you change the user group. (@AgaSpace) +* * Ensured `TSPlayer.PlayerData` is non-null whilst syncing loadouts. (@drunderscore) +* * Detected invalid installations, by checking for a file named `TerrariaServer.exe`. (@drunderscore) + * This made the two most common installation mistakes (extracting into the Terraria client directory, and extracting TShock 5 or newer into a TShock 4 or older install) prompt the user with a more useful diagnostic, rather than (likely) crashing moments later. Rewrote bed spawning for SSC. (@PotatoCider) + * Removed `TSPlayer.s{X,Y}` in favour of using desyncing client and server spawnpoint values (`Terraria.Player.Spawn{X,Y}`) until the player has changed their spawnpoint per session. + * Partially fixed the bed spawning bug when SSC is enabled. Players would need to spawn at their beds at least once to tell TShock that the player's spawnpoint has changed. +* Changed Bouncer to block updates which set the following fields to infinity or NaN: player position, projectile position, projectile velocity, item position, and item velocity. (@Arthri) +* Updated `TShockAPI.Handlers.SendTileRectHandler` (@LaoSparrow): + * Fixed incorrect validating range in `TileRectMatch.MatchRemoval`. + * Fixed tile rect changes (e.g. turning on and off campfires) are not synced between clients. + * Fixed unable to place Hat Rack without permission `tshock.ignore.sendtilesquare`. +* Updated `GetDataHandlers` to ignore `NpcItemStrike(msgid 24)`, which should never be sent by a vanilla client. (@LaoSparrow) + +## TShock 5.2.1 * Updated `TSPlayer.GodMode`. (@AgaSpace) * Previously the field was used as some kind of dataset changed by /godmode command, but now it is a property that receives/changes data in journey mode. * Added the `TSPlayer.Client` property. It allows the developer to get the `RemoteClient` player, without an additional call to `Terraria.Netplay.Clients`. (@AgaSpace) @@ -90,12 +117,16 @@ Use past tense when adding new entries; sign your name off when you add or chang * Added a property `TSPlayer.Hostile`, which gets pvp player mode. (@AgaSpace) * Fixed bug where when the `UseSqlLogs` config property is true, an empty log file would still get created. (@ZakFahey) * Fixed typo in `/gbuff`. (@sgkoishi, #2955) -* Rewrote the `.dockerignore` file into a denylist. (@timschumi) -* Added CI for Docker images. (@timschumi) -* Added a new permission, `tshock.world.time.usemoondial`, for regulating use of Enchanted Moondial. (@Arthri) + +Rewrote the Added a new permission, `tshock.world.time.usemoondial`, for regulating use of Enchanted Moondial. (@Arthri) * Added a set of new permissions, `tshock.specialeffects.{type}`, for regulating use of new special effects(Packet 51) which are not yet recognized by TShock. (@Arthri) * Added check for `tshock.npc.summonboss` permission for Skeletron summoning. (@Arthri) * Fixed `DisableDungeonGuardian` disabling Skeletron summon instead. The config option is useless as of writing. (@Arthri) +* Added a constructor for `TShockAPI.PlayerData` that accepts the `includingStarterInventory` parameter, which is responsible for loading the TShock inventory. +* Declared the constructor `TShockAPI.PlayerData` accepting the argument `TShockAPI.TSPlayer` obsolete. +* Updated the `PlayerData.StoreSlot` method: Added an overload that takes `TShockAPI.NetItem`. +* Added `PlayerHooks.PrePlayerCommand` hook, which fired before command execution. (@AgaSpace) +* Added `PlayerHooks.PostPlayerCommand` hook, which fired after command execution. (@AgaSpace) ## TShock 5.2 * An additional option `pvpwithnoteam` is added at `PvPMode` to enable PVP with no team. (@CelestialAnarchy, #2617, @ATFGK) @@ -129,7 +160,6 @@ Use past tense when adding new entries; sign your name off when you add or chang * Relaxed custom death message restrictions to allow Inferno potions in PvP. (@drunderscore) * Allowed Flower Boots to place Ash Flowers on Ash Grass blocks. (@punchready) * Removed unnecessary range check that artifically shortened quick stack reach. (@boddyn, #2885, @bcat) -* Re-wrote tile rect handling from scratch, fixing a certain exploitable flaw in the old code and significantly reducing the potential exploit surface, potentially even down to zero. (@punchready) ## TShock 5.1.3 * Added support for Terraria 1.4.4.9 via OTAPI 3.1.20. (@SignatureBeef) diff --git a/docs/docker.md b/docs/docker.md index ca018510..afc4bdfc 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -14,32 +14,27 @@ Open ports can also be passed through using `-p :`. For Example: ```bash -# Building the image using buildx and loading it into docker -docker buildx build -t tshock:latest --load . - -# Running the image docker run -p 7777:7777 -p 7878:7878 \ -v /home/cider/tshock/:/tshock \ -v /home/cider/.local/share/Terraria/Worlds:/worlds \ -v /home/cider/tshock/plugins:/plugins \ - --rm -it tshock:latest \ + --rm -it ghcr.io/pryaxis/tshock:latest \ -world /worlds/backflip.wld -motd "OMFG DOCKER" ``` -## Building for Other Platforms +## Building custom images -Using `docker buildx`, you could build [multi-platform images](https://docs.docker.com/build/building/multi-platform/) for TShock. +Occasionally, it may be necessary to adjust TShock with customizations that are not included in the upstream project. +Therefore, these changes are also not available in the officially provided Docker images. + +To build and load a Docker image from your local checkout, use the following `buildx` command: -For Example: ```bash -# Building the image using buildx and loading it into docker -docker buildx build -t tshock:linux-arm64 --platform linux/arm64 --load . - -# Running the image -docker run -p 7777:7777 -p 7878:7878 \ - -v /home/cider/tshock/:/tshock \ - -v /home/cider/.local/share/Terraria/Worlds:/worlds \ - -v /home/cider/tshock/plugins:/plugins \ - --rm -it tshock:linux-arm64 \ - -world /worlds/backflip.wld -motd "ARM64 ftw" +docker buildx build -t tshock:latest --load . +``` + +It is also possible to build [multi-platform images](https://docs.docker.com/build/building/multi-platform/) for TShock (e.g. an image targeting `arm64`, on a host that is not `arm64`): + +```bash +docker buildx build -t tshock:linux-arm64 --platform linux/arm64 --load . ``` diff --git a/docs/index.html b/docs/index.html index 2ae1df82..0cfccf11 100644 --- a/docs/index.html +++ b/docs/index.html @@ -11,12 +11,7 @@
diff --git a/renovate.json b/renovate.json deleted file mode 100644 index 159ec1e2..00000000 --- a/renovate.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:base" - ], - "git-submodules": { - "enabled": true - }, - "packageRules": [ - { - "matchPackageNames": ["OTAPI.Upcoming", "ModFramework", "TerrariaServerAPI"], - "ignoreUnstable": "false", - "bumpVersion": "prerelease", - "groupName": "OTAPI things" - } - ] -}