Merge remote-tracking branch 'upstream/general-devel' into net9-upgrade

This commit is contained in:
Luke 2025-05-09 22:38:33 +10:00
commit df05e94226
15 changed files with 421 additions and 1293 deletions

View file

@ -1,3 +1,3 @@
<!-- Warning: If you create a pull request and wish to remain anonymous, you are highly advised to use Tails (https://tails.boum.org/) or a fresh git environment. We will *not* be able to help with anonymization after your pull request has been created. -->
<!-- Warning: If you do not update the changelog, your pull request will be ignored and eventually closed, without comment, without any support, and without any opinion or interaction from the TShock team. This is your only warning. -->
<!-- Put the text you want in the changelog in your PR body so we can add it to the changelog. -->

13
.github/workflows/wiki-notify.yml vendored Normal file
View file

@ -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

View file

@ -504,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));
@ -1072,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)
{
@ -1175,6 +1199,22 @@ namespace TShockAPI
int index = args.Index;
float[] ai = args.Ai;
// Clients do send NaN values so we can't just kick them
// See https://github.com/Pryaxis/TShock/issues/3076
if (!float.IsFinite(pos.X) || !float.IsFinite(pos.Y))
{
TShock.Log.ConsoleInfo(GetString("Bouncer / OnNewProjectile rejected set position to infinity or NaN from {0}", args.Player.Name));
args.Handled = true;
return;
}
if (!float.IsFinite(vel.X) || !float.IsFinite(vel.Y))
{
TShock.Log.ConsoleInfo(GetString("Bouncer / OnNewProjectile rejected set velocity to infinity or NaN from {0}", args.Player.Name));
args.Handled = true;
return;
}
if (index > Main.maxProjectiles)
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnNewProjectile rejected from above projectile limit from {0}", args.Player.Name));

View file

@ -1607,7 +1607,7 @@ namespace TShockAPI
}
}
if (banUuid)
if (banUuid && player.UUID.Length > 0)
{
banResult = DoBan($"{Identifier.UUID}{player.UUID}", reason, expiration);
}
@ -2531,7 +2531,7 @@ namespace TShockAPI
foreach (TSPlayer ply in TShock.Players.Where(p => p != null && p.Active && p.TPlayer.name.ToLower().Equals(args.Parameters[0].ToLower())))
{
//this will always tell the client that they have not done the quest today.
ply.SendData((PacketTypes)74, "");
ply.SendData(PacketTypes.AnglerQuest, "");
}
args.Player.SendSuccessMessage(GetString("Removed {0} players from the angler quest completion list for today.", result));
}

View file

@ -189,11 +189,15 @@ namespace TShockAPI.DB
{
List<string> identifiers = new List<string>
{
$"{Identifier.UUID}{player.UUID}",
$"{Identifier.Name}{player.Name}",
$"{Identifier.IP}{player.IP}"
};
if (player.UUID != null && player.UUID.Length > 0)
{
identifiers.Add($"{Identifier.UUID}{player.UUID}");
}
if (player.Account != null)
{
identifiers.Add($"{Identifier.Account}{player.Account.Name}");

View file

@ -79,7 +79,7 @@ namespace TShockAPI.DB
public PlayerData GetPlayerData(TSPlayer player, int acctid)
{
PlayerData playerData = new PlayerData(player);
PlayerData playerData = new PlayerData(true);
try
{
@ -189,7 +189,7 @@ namespace TShockAPI.DB
if (!player.IsLoggedIn)
return false;
if (player.State < 10)
if (player.State < (int)ConnectionState.Complete)
return false;
if (player.HasPermission(Permissions.bypassssc) && !fromCommand)

View file

@ -57,7 +57,7 @@ namespace TShockAPI.DB
if (creator.EnsureTableStructure(table))
{
// Add default groups if they don't exist
AddDefaultGroup("guest", "",
AddDefaultGroup(TShock.Config.Settings.DefaultGuestGroupName, "",
string.Join(",",
Permissions.canbuild,
Permissions.canregister,
@ -68,12 +68,14 @@ namespace TShockAPI.DB
Permissions.synclocalarea,
Permissions.sendemoji));
AddDefaultGroup("default", "guest",
AddDefaultGroup(TShock.Config.Settings.DefaultRegistrationGroupName, TShock.Config.Settings.DefaultGuestGroupName,
string.Join(",",
Permissions.warp,
Permissions.canchangepassword,
Permissions.canlogout,
Permissions.summonboss,
Permissions.spawnpets,
Permissions.worldupgrades,
Permissions.whisper,
Permissions.wormhole,
Permissions.canpaint,
@ -82,7 +84,7 @@ namespace TShockAPI.DB
Permissions.magicconch,
Permissions.demonconch));
AddDefaultGroup("vip", "default",
AddDefaultGroup("vip", TShock.Config.Settings.DefaultRegistrationGroupName,
string.Join(",",
Permissions.reservedslot,
Permissions.renamenpc,

View file

@ -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 },
@ -2618,18 +2619,19 @@ namespace TShockAPI
private static bool HandleConnecting(GetDataHandlerArgs args)
{
var account = TShock.UserAccounts.GetUserAccountByName(args.Player.Name);//
args.Player.DataWhenJoined = new PlayerData(args.Player);
var account = TShock.UserAccounts.GetUserAccountByName(args.Player.Name);
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)
{
if (account.UUID == args.Player.UUID)
{
if (args.Player.State == 1)
args.Player.State = 2;
if (args.Player.State == (int)ConnectionState.AssigningPlayerSlot)
args.Player.State = (int)ConnectionState.AwaitingPlayerInfo;
NetMessage.SendData((int)PacketTypes.WorldInfo, args.Player.Index);
var group = TShock.Groups.GetGroupByName(account.Group);
@ -2687,8 +2689,9 @@ namespace TShockAPI
return true;
}
if (args.Player.State == 1)
args.Player.State = 2;
if (args.Player.State == (int)ConnectionState.AssigningPlayerSlot)
args.Player.State = (int)ConnectionState.AwaitingPlayerInfo;
NetMessage.SendData((int)PacketTypes.WorldInfo, args.Player.Index);
return true;
}
@ -2719,52 +2722,75 @@ 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();
args.Player.FinishedHandshake = true;
if (args.Player.State >= (int)ConnectionState.RequestingWorldData && !args.Player.FinishedHandshake)
args.Player.FinishedHandshake = true; //If the player has requested world data before sending spawn player, they should be at the obvious ClientRequestedWorldData state. Also only set this once to remove redundant updates.
if (OnPlayerSpawn(args.Player, args.Data, player, spawnx, spawny, respawnTimer, numberOfDeathsPVE, numberOfDeathsPVP, context))
if (OnPlayerSpawn(args.Player, args.Data, player, spawnX, spawnY, respawnTimer, numberOfDeathsPVE, numberOfDeathsPVP, context))
return true;
if ((Main.ServerSideCharacter) && (spawnx == -1 && spawny == -1)) //this means they want to spawn to vanilla spawn
args.Player.Dead = respawnTimer > 0;
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.
// handle initial connection
if (args.Player.State == 3)
{
// server saved spawnpoint value
args.Player.initialSpawn = true;
args.Player.initialServerSpawnX = args.TPlayer.SpawnX;
args.Player.initialServerSpawnY = args.TPlayer.SpawnY;
// 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;
}
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)))
// 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.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)))
{
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));
}
// Player has changed his spawnpoint, client and server TPlayer.Spawn{X,Y} is now synced
args.Player.spawnSynced = true;
return false;
}
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)))
{
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));
}
}
// spawn the player before teleporting
NetMessage.SendData((int)PacketTypes.PlayerSpawn, -1, args.Player.Index, null, args.Player.Index, (int)PlayerSpawnContext.ReviveFromDeath);
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;
}
@ -2945,6 +2971,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();
@ -3218,8 +3251,9 @@ namespace TShockAPI
args.Player.RequiresPassword = false;
args.Player.PlayerData = TShock.CharacterDB.GetPlayerData(args.Player, account.ID);
if (args.Player.State == 1)
args.Player.State = 2;
if (args.Player.State == (int)ConnectionState.AssigningPlayerSlot)
args.Player.State = (int)ConnectionState.AwaitingPlayerInfo;
NetMessage.SendData((int)PacketTypes.WorldInfo, args.Player.Index);
var group = TShock.Groups.GetGroupByName(account.Group);
@ -3266,8 +3300,10 @@ namespace TShockAPI
if (TShock.Config.Settings.ServerPassword == password)
{
args.Player.RequiresPassword = false;
if (args.Player.State == 1)
args.Player.State = 2;
if (args.Player.State == (int)ConnectionState.AssigningPlayerSlot)
args.Player.State = (int)ConnectionState.AwaitingPlayerInfo;
NetMessage.SendData((int)PacketTypes.WorldInfo, args.Player.Index);
return true;
}
@ -3435,9 +3471,9 @@ namespace TShockAPI
if (buff == 10 && TShock.Config.Settings.DisableInvisPvP && args.TPlayer.hostile)
buff = 0;
if (Netplay.Clients[args.TPlayer.whoAmI].State < 2 && (buff == 156 || buff == 47 || buff == 149))
if (Netplay.Clients[args.TPlayer.whoAmI].State < (int)ConnectionState.AwaitingPlayerInfo && (buff == 156 || buff == 47 || buff == 149))
{
TShock.Log.ConsoleDebug(GetString("GetDataHandlers / HandlePlayerBuffList zeroed player buff due to below state 2 {0} {1}", args.Player.Name, buff));
TShock.Log.ConsoleDebug(GetString("GetDataHandlers / HandlePlayerBuffList zeroed player buff due to below state awaiting player information {0} {1}", args.Player.Name, buff));
buff = 0;
}
@ -3465,15 +3501,23 @@ namespace TShockAPI
if (OnNPCSpecial(args.Player, args.Data, id, type))
return true;
if (type == 1 && TShock.Config.Settings.DisableDungeonGuardian)
if (type == 1)
{
TShock.Log.ConsoleDebug(GetString("GetDataHandlers / HandleSpecial rejected type 1 for {0}", args.Player.Name));
args.Player.SendMessage(GetString("The Dungeon Guardian returned you to your spawn point."), Color.Purple);
args.Player.Spawn(PlayerSpawnContext.RecallFromItem);
if (!args.Player.HasPermission(Permissions.summonboss))
{
args.Player.SendErrorMessage(GetString("You do not have permission to summon the Skeletron."));
TShock.Log.ConsoleDebug(GetString($"GetDataHandlers / HandleNpcStrike rejected Skeletron summon from {args.Player.Name}"));
return true;
}
if (type == 3)
return false;
}
else if (type == 2)
{
// Plays SoundID.Item1
return false;
}
else if (type == 3)
{
if (!args.Player.HasPermission(Permissions.usesundial))
{
@ -3493,6 +3537,42 @@ namespace TShockAPI
return true;
}
}
else if (type == 4)
{
// Big Mimic Spawn Smoke
return false;
}
else if (type == 5)
{
// Register Kill for Torch God in Bestiary
return false;
}
else if (type == 6)
{
if (!args.Player.HasPermission(Permissions.usemoondial))
{
TShock.Log.ConsoleDebug(GetString($"GetDataHandlers / HandleSpecial rejected enchanted moondial permission {args.Player.Name}"));
args.Player.SendErrorMessage(GetString("You do not have permission to use the Enchanted Moondial."));
return true;
}
else if (TShock.Config.Settings.ForceTime != "normal")
{
TShock.Log.ConsoleDebug(GetString($"GetDataHandlers / HandleSpecial rejected enchanted moondial permission (ForceTime) {args.Player.Name}"));
if (!args.Player.HasPermission(Permissions.cfgreload))
{
args.Player.SendErrorMessage(GetString("You cannot use the Enchanted Moondial because time is stopped."));
}
else
args.Player.SendErrorMessage(GetString("You must set ForceTime to normal via config to use the Enchanted Moondial."));
return true;
}
}
else if (!args.Player.HasPermission($"tshock.specialeffects.{type}"))
{
args.Player.SendErrorMessage(GetString("You do not have permission to use this effect."));
TShock.Log.ConsoleError(GetString("Unrecognized special effect (Packet 51). Please report this to the TShock developers."));
return true;
}
return false;
}
@ -3542,8 +3622,9 @@ namespace TShockAPI
return false;
}
private static readonly int[] invasions = { -1, -2, -3, -4, -5, -6, -7, -8, -10, -11 };
private static readonly int[] invasions = { -1, -2, -3, -4, -5, -6, -7, -8, -10 };
private static readonly int[] pets = { -12, -13, -14, -15 };
private static readonly int[] upgrades = { -11, -17, -18 };
private static bool HandleSpawnBoss(GetDataHandlerArgs args)
{
if (args.Player.IsBouncerThrottled())
@ -3555,8 +3636,8 @@ namespace TShockAPI
var plr = args.Data.ReadInt16();
var thingType = args.Data.ReadInt16();
var isKnownBoss = thingType > 0 && thingType < Terraria.ID.NPCID.Count && NPCID.Sets.MPAllowedEnemies[thingType];
if ((isKnownBoss || thingType == -16) && !args.Player.HasPermission(Permissions.summonboss))
var isKnownBoss = (thingType > 0 && thingType < Terraria.ID.NPCID.Count && NPCID.Sets.MPAllowedEnemies[thingType]) || thingType == -16;
if (isKnownBoss && !args.Player.HasPermission(Permissions.summonboss))
{
TShock.Log.ConsoleDebug(GetString("GetDataHandlers / HandleSpawnBoss rejected boss {0} {1}", args.Player.Name, thingType));
args.Player.SendErrorMessage(GetString("You do not have permission to summon bosses."));
@ -3577,6 +3658,13 @@ namespace TShockAPI
return true;
}
if (upgrades.Contains(thingType) && !args.Player.HasPermission(Permissions.worldupgrades))
{
TShock.Log.ConsoleDebug(GetString("GetDataHandlers / HandleSpawnBoss rejected upgrade {0} {1}", args.Player.Name, thingType));
args.Player.SendErrorMessage(GetString("You do not have permission to use permanent boosters."));
return true;
}
if (plr != args.Player.Index)
return true;
@ -3642,10 +3730,15 @@ namespace TShockAPI
thing = GetString("{0} summoned the {1}!", args.Player.Name, npc.FullName);
break;
}
if (thingType < 0 || isKnownBoss)
{
if (TShock.Config.Settings.AnonymousBossInvasions)
TShock.Utils.SendLogs(thing, Color.PaleVioletRed, args.Player);
else
TShock.Utils.Broadcast(thing, 175, 75, 255);
}
return false;
}

View file

@ -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
/// <param name="player">The player the operation originates from.</param>
/// <param name="rect">The tile rectangle of the operation.</param>
/// <returns><see langword="true"/>, if the rect matches this operation and the changes have been applied, otherwise <see langword="false"/>.</returns>
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,18 +264,18 @@ 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++)
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()) // 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,18 +298,18 @@ 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++)
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;
}
}
}
@ -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;
}

View file

@ -331,6 +331,9 @@ namespace TShockAPI
[Description("Player can use the Enchanted Sundial item.")]
public static readonly string usesundial = "tshock.world.time.usesundial";
[Description("Player can use the Enchanted Moondial item.")]
public static readonly string usemoondial = "tshock.world.time.usemoondial";
[Description("User can grow plants.")]
public static readonly string grow = "tshock.world.grow";
@ -343,9 +346,13 @@ namespace TShockAPI
[Description("User can change the homes of NPCs.")]
public static readonly string movenpc = "tshock.world.movenpc";
[Obsolete("Feature no longer available.")]
[Description("User can convert hallow into corruption and vice-versa.")]
public static readonly string converthardmode = "tshock.world.converthardmode";
[Description("User can use world-based permanent boosters like Advanced Combat Techniques")]
public static readonly string worldupgrades = "tshock.world.worldupgrades";
[Description("User can force the server to Halloween mode.")]
public static readonly string halloween = "tshock.world.sethalloween";

View file

@ -23,6 +23,7 @@ using Terraria.Localization;
using Terraria.GameContent.NetModules;
using Terraria.Net;
using Terraria.ID;
using System;
namespace TShockAPI
{
@ -63,13 +64,22 @@ namespace TShockAPI
public int unlockedSuperCart;
public int enabledSuperCart;
public PlayerData(TSPlayer player)
/// <summary>
/// Sets the default values for the inventory.
/// </summary>
[Obsolete("The player argument is not used.")]
public PlayerData(TSPlayer player) : this(true) { }
/// <summary>
/// Sets the default values for the inventory.
/// </summary>
/// <param name="includingStarterInventory">Is it necessary to load items from TShock's config</param>
public PlayerData(bool includingStarterInventory = true)
{
for (int i = 0; i < NetItem.MaxInventory; i++)
{
this.inventory[i] = new NetItem();
}
if (includingStarterInventory)
for (int i = 0; i < TShock.ServerSideCharacterConfig.Settings.StartingInventory.Count; i++)
{
var item = TShock.ServerSideCharacterConfig.Settings.StartingInventory[i];
@ -86,12 +96,22 @@ namespace TShockAPI
/// <param name="stack"></param>
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));
}
/// <summary>
/// Stores an item at the specific storage slot
/// </summary>
/// <param name="slot"></param>
/// <param name="item"></param>
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;
}
/// <summary>
@ -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;
}
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;

View file

@ -35,7 +35,6 @@ using TShockAPI.Net;
using Timer = System.Timers.Timer;
using System.Linq;
using Terraria.GameContent.Creative;
namespace TShockAPI
{
/// <summary>
@ -62,6 +61,49 @@ namespace TShockAPI
WriteToLogAndConsole
}
/// <summary>
/// An enum based on the current client's connection state to the server.
/// </summary>
public enum ConnectionState : int
{
/// <summary>
/// The server is password protected and the connection is pending until a password is sent by the client.
/// </summary>
AwaitingPassword = -1,
/// <summary>
/// The connection has been established, and the client must verify its version.
/// </summary>
AwaitingVersionCheck = 0,
/// <summary>
/// The server has accepted the client's password to connect and/or the server has verified the client's version string as being correct. The client is now being assigned a player slot.
/// </summary>
AssigningPlayerSlot = 1,
/// <summary>
/// The player slot has been received by the client, and the server is now waiting for the player information.
/// </summary>
AwaitingPlayerInfo = 2,
/// <summary>
/// Player information has been received, and the client is requesting world data.
/// </summary>
RequestingWorldData = 3,
/// <summary>
/// The world data is being sent to the client.
/// </summary>
ReceivingWorldData = 4,
/// <summary>
/// The world data has been received, and the client is now finalizing the load.
/// </summary>
FinalizingWorldLoad = 5,
/// <summary>
/// The client is requesting tile data.
/// </summary>
RequestingTileData = 6,
/// <summary>
/// The connection process is complete (The player has spawned), and the client has fully joined the game.
/// </summary>
Complete = 10
}
public class TSPlayer
{
/// <summary>
@ -177,8 +219,13 @@ namespace TShockAPI
/// </summary>
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;
/// <summary>
/// A queue of tiles destroyed by the player for reverting.
@ -1287,7 +1334,7 @@ namespace TShockAPI
}
}
PlayerData = new PlayerData(this);
PlayerData = new PlayerData();
Group = TShock.Groups.GetGroupByName(TShock.Config.Settings.DefaultGuestGroupName);
tempGroup = null;
if (tempGroupTimer != null)
@ -1324,6 +1371,9 @@ namespace TShockAPI
FakePlayer = new Player { name = playerName, whoAmI = -1 };
Group = Group.DefaultGroup;
AwaitingResponse = new Dictionary<string, Action<object>>();
if (playerName == "All" || playerName == "Server")
FinishedHandshake = true; //Hot fix for the all player object not getting packets like TimeSet, etc because they have no state and finished handshake will always be false.
}
/// <summary>
@ -1383,6 +1433,25 @@ namespace TShockAPI
return true;
}
/// <summary>
/// Teleports the player to their spawnpoint.
/// Teleports to main spawnpoint if their bed is not active.
/// Supports SSC.
/// </summary>
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);
}
/// <summary>
/// Heals the player.
/// </summary>
@ -1396,16 +1465,9 @@ namespace TShockAPI
/// Spawns the player at his spawn point.
/// </summary>
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);
}
}
/// <summary>
/// Spawns the player at the given coordinates.
@ -2102,6 +2164,27 @@ namespace TShockAPI
SendData(PacketTypes.PlayerAddBuff, number: Index, number2: type, number3: time);
}
/// <summary>
/// The list of necessary packets to make sure gets through to the player upon connection (before they finish the handshake).
/// </summary>
private static readonly HashSet<PacketTypes> HandshakeNecessaryPackets = new()
{
PacketTypes.ContinueConnecting,
PacketTypes.WorldInfo,
PacketTypes.Status,
PacketTypes.Disconnect,
PacketTypes.TileFrameSection,
PacketTypes.TileSendSection,
PacketTypes.PlayerSpawnSelf
};
/// <summary>
/// Determines if an outgoing packet is necessary to send to a player before they have finished the connection handshake.
/// </summary>
/// <param name="msgType">The packet type to check against the necessary list.</param>
/// <returns>Whether the packet is necessary for connection or not</returns>
private bool NecessaryPacket(PacketTypes msgType) => HandshakeNecessaryPackets.Contains(msgType);
//Todo: Separate this into a few functions. SendTo, SendToAll, etc
/// <summary>
/// Sends data to the player.
@ -2119,6 +2202,12 @@ namespace TShockAPI
if (RealPlayer && !ConnectionAlive)
return;
if (!NecessaryPacket(msgType) && !FinishedHandshake)
return;
if (FakePlayer != null && FakePlayer.whoAmI != -1 && msgType == PacketTypes.WorldInfo && State < (int)ConnectionState.RequestingWorldData) //So.. the All player doesn't have a state, so we cannot check this, skip over them if their index is -1 (server/all)
return;
NetMessage.SendData((int)msgType, Index, -1, text == null ? null : NetworkText.FromLiteral(text), number, number2, number3, number4, number5);
}

View file

@ -63,7 +63,7 @@ namespace TShockAPI
/// <summary>VersionNum - The version number the TerrariaAPI will return back to the API. We just use the Assembly info.</summary>
public static readonly Version VersionNum = Assembly.GetExecutingAssembly().GetName().Version;
/// <summary>VersionCodename - The version codename is displayed when the server starts. Inspired by software codenames conventions.</summary>
public static readonly string VersionCodename = "East";
public static readonly string VersionCodename = "Hopefully SSC works somewhat correctly now edition";
/// <summary>SavePath - This is the path TShock saves its data in. This path is relative to the TerrariaServer.exe (not in ServerPlugins).</summary>
public static string SavePath = "tshock";
@ -1172,16 +1172,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)
@ -1430,7 +1430,7 @@ namespace TShockAPI
if (tsplr.ReceivedInfo)
{
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.
if (!tsplr.SilentKickInProgress && tsplr.State >= (int)ConnectionState.RequestingWorldData && 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));
@ -1451,7 +1451,6 @@ namespace TShockAPI
}
}
tsplr.FinishedHandshake = false;
// Fire the OnPlayerLogout hook too, if the player was logged in and they have a TSPlayer object.
@ -1460,8 +1459,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();
@ -1485,6 +1484,7 @@ namespace TShockAPI
if (!tsplr.FinishedHandshake)
{
tsplr.Kick(GetString("Your client didn't send the right connection information."), true);
args.Handled = true;
return;
}
@ -1668,7 +1668,7 @@ namespace TShockAPI
return;
}
if ((player.State < 10 || player.Dead) && (int)type > 12 && (int)type != 16 && (int)type != 42 && (int)type != 50 &&
if ((player.State < (int)ConnectionState.Complete || player.Dead) && (int)type > 12 && (int)type != 16 && (int)type != 42 && (int)type != 50 &&
(int)type != 38 && (int)type != 21 && (int)type != 22 && type != PacketTypes.SyncLoadout)
{
e.Handled = true;

View file

@ -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)
-->
<Version>5.2.2</Version>
<Version>5.2.4</Version>
<AssemblyTitle>TShock for Terraria</AssemblyTitle>
<Company>Pryaxis &amp; TShock Contributors</Company>
<Product>TShockAPI</Product>
<Copyright>Copyright © Pryaxis &amp; TShock Contributors 2011-2023</Copyright>
<Copyright>Copyright © Pryaxis &amp; TShock Contributors 2011-2025</Copyright>
<!-- extras for nuget -->
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageLicenseExpression>GPL-3.0-or-later</PackageLicenseExpression>

File diff suppressed because it is too large Load diff