diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8dc088af..58a50a10 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,12 @@
This is the rolling changelog for TShock for Terraria. Use past tense when adding new entries; sign your name off when you add or change something. This should primarily be things like user changes, not necessarily codebase changes unless it's really relevant or large.
## Upcoming Changes
+* New permission `tshock.tp.pylon` to enable teleporting via Teleportation Pylons (@QuiCM)
+* New permission `tshock.journey.research` to enable sharing research via item sacrifice (@QuiCM)
+* Add Emoji event to GetDataHandler. This packet is received when a player tries to display an emote.
+ * Adding EmojiHandler to handle an exploit. Adding `tshock.sendemoji` permission and checks. Added this permission to guest group by default.
+* Handling SyncCavernMonsterType packet to prevent an exploit where players could modify the server's cavern monster types and make the server spawn any NPCs - including bosses - onto other players.
+* Added LandGolfBallInCup event which is accessible for developers to work with, as well as LandGolfBallInCup handler to handle exploits where players could send direct packets to trigger and imitate golf ball cup landing anywhere in the game world. Added two public lists in Handlers.LandGolfBallInCupHandler: GolfBallProjectileIDs and GolfClubItemIDs. (@Patrikkk)
* Add SyncTilePicking event. This is called when a player damages a tile.
## TShock 4.4.0 (Pre-release 10)
diff --git a/TShockAPI/Bouncer.cs b/TShockAPI/Bouncer.cs
index 395bfddd..8029031b 100644
--- a/TShockAPI/Bouncer.cs
+++ b/TShockAPI/Bouncer.cs
@@ -37,13 +37,25 @@ namespace TShockAPI
internal sealed class Bouncer
{
internal Handlers.SendTileSquareHandler STSHandler { get; set; }
+ internal Handlers.NetModules.NetModulePacketHandler NetModuleHandler { get; set; }
+ internal Handlers.EmojiHandler EmojiHandler { get; set; }
+ internal Handlers.LandGolfBallInCupHandler LandGolfBallInCupHandler { get; set; }
/// Constructor call initializes Bouncer and related functionality.
/// A new Bouncer.
internal Bouncer()
{
STSHandler = new Handlers.SendTileSquareHandler();
- GetDataHandlers.SendTileSquare += STSHandler.OnReceiveSendTileSquare;
+ GetDataHandlers.SendTileSquare += STSHandler.OnReceive;
+
+ NetModuleHandler = new Handlers.NetModules.NetModulePacketHandler();
+ GetDataHandlers.ReadNetModule += NetModuleHandler.OnReceive;
+
+ EmojiHandler = new Handlers.EmojiHandler();
+ GetDataHandlers.Emoji += EmojiHandler.OnReceive;
+
+ LandGolfBallInCupHandler = new Handlers.LandGolfBallInCupHandler();
+ GetDataHandlers.LandGolfBallInCup += LandGolfBallInCupHandler.OnReceive;
// Setup hooks
GetDataHandlers.GetSection += OnGetSection;
diff --git a/TShockAPI/Commands.cs b/TShockAPI/Commands.cs
index 10d6f0da..4fc949b7 100644
--- a/TShockAPI/Commands.cs
+++ b/TShockAPI/Commands.cs
@@ -1987,7 +1987,7 @@ namespace TShockAPI
void FailedPermissionCheck()
{
- args.Player.SendErrorMessage("You do not have sufficient permissions to start the {0} event.", eventType);
+ args.Player.SendErrorMessage("You do not have permission to start the {0} event.", eventType);
return;
}
@@ -4873,7 +4873,7 @@ namespace TShockAPI
{
if (!args.Player.HasPermission(Permissions.tp))
{
- args.Player.SendErrorMessage("You don't have the necessary permission to do that.");
+ args.Player.SendErrorMessage("You do not have permission to teleport.");
break;
}
if (args.Parameters.Count <= 1)
@@ -5051,7 +5051,7 @@ namespace TShockAPI
}
if (displayIdsRequested && !args.Player.HasPermission(Permissions.seeids))
{
- args.Player.SendErrorMessage("You don't have the required permission to list player ids.");
+ args.Player.SendErrorMessage("You do not have permission to list player ids.");
return;
}
@@ -6025,7 +6025,7 @@ namespace TShockAPI
{
if (!args.Player.HasPermission(Permissions.godmodeother))
{
- args.Player.SendErrorMessage("You do not have permission to god mode another player!");
+ args.Player.SendErrorMessage("You do not have permission to god mode another player.");
return;
}
string plStr = String.Join(" ", args.Parameters);
diff --git a/TShockAPI/DB/GroupManager.cs b/TShockAPI/DB/GroupManager.cs
index 584ad374..3cff37a1 100644
--- a/TShockAPI/DB/GroupManager.cs
+++ b/TShockAPI/DB/GroupManager.cs
@@ -65,7 +65,8 @@ namespace TShockAPI.DB
Permissions.canpartychat,
Permissions.cantalkinthird,
Permissions.canchat,
- Permissions.synclocalarea));
+ Permissions.synclocalarea,
+ Permissions.sendemoji));
AddDefaultGroup("default", "guest",
string.Join(",",
diff --git a/TShockAPI/GetDataHandlers.cs b/TShockAPI/GetDataHandlers.cs
index 64f57b9c..0ae658fd 100644
--- a/TShockAPI/GetDataHandlers.cs
+++ b/TShockAPI/GetDataHandlers.cs
@@ -151,10 +151,13 @@ namespace TShockAPI
{ PacketTypes.CrystalInvasionStart, HandleOldOnesArmy },
{ PacketTypes.PlayerHurtV2, HandlePlayerDamageV2 },
{ PacketTypes.PlayerDeathV2, HandlePlayerKillMeV2 },
+ { PacketTypes.Emoji, HandleEmoji },
{ PacketTypes.SyncTilePicking, HandleSyncTilePicking },
{ PacketTypes.SyncRevengeMarker, HandleSyncRevengeMarker },
+ { PacketTypes.LandGolfBallInCup, HandleLandGolfBallInCup },
{ PacketTypes.FishOutNPC, HandleFishOutNPC },
- { PacketTypes.FoodPlatterTryPlacing, HandleFoodPlatterTryPlacing }
+ { PacketTypes.FoodPlatterTryPlacing, HandleFoodPlatterTryPlacing },
+ { PacketTypes.SyncCavernMonsterType, HandleSyncCavernMonsterType }
};
}
@@ -1900,8 +1903,6 @@ namespace TShockAPI
var args = new SyncTilePickingEventArgs
{
- Player = player,
- Data = data,
PlayerIndex = playerIndex,
TileX = tileX,
TileY = tileY,
@@ -1912,6 +1913,89 @@ namespace TShockAPI
}
+ /// For use in an Emoji event.
+ ///
+ public class EmojiEventArgs : GetDataHandledEventArgs
+ {
+ ///
+ /// The player index in the packet, who sends the emoji.
+ ///
+ public byte PlayerIndex { get; set; }
+ ///
+ /// The ID of the emoji, that is being received.
+ ///
+ public byte EmojiID { get; set; }
+ }
+ ///
+ /// Called when a player sends an emoji.
+ ///
+ public static HandlerList Emoji = new HandlerList();
+ private static bool OnEmoji(TSPlayer player, MemoryStream data, byte playerIndex, byte emojiID)
+ {
+ if (Emoji == null)
+ return false;
+
+ var args = new EmojiEventArgs
+ {
+ Player = player,
+ Data = data,
+ PlayerIndex = playerIndex,
+ EmojiID = emojiID
+ };
+ Emoji.Invoke(null, args);
+ return args.Handled;
+ }
+
+ ///
+ /// For use in a LandBallInCup event.
+ ///
+ public class LandGolfBallInCupEventArgs : GetDataHandledEventArgs
+ {
+ ///
+ /// The player index in the packet, who puts the ball in the cup.
+ ///
+ public byte PlayerIndex { get; set; }
+ ///
+ /// The X tile position of where the ball lands in a cup.
+ ///
+ public ushort TileX { get; set; }
+ ///
+ /// The Y tile position of where the ball lands in a cup.
+ ///
+ public ushort TileY { get; set; }
+ ///
+ /// The amount of hits it took for the player to land the ball in the cup.
+ ///
+ public ushort Hits { get; set; }
+ ///
+ /// The type of the projectile that was landed in the cup. A golfball in legit cases.
+ ///
+ public ushort ProjectileType { get; set; }
+ }
+
+ ///
+ /// Called when a player lands a golf ball in a cup.
+ ///
+ public static HandlerList LandGolfBallInCup = new HandlerList();
+ private static bool OnLandGolfBallInCup(TSPlayer player, MemoryStream data, byte playerIndex, ushort tileX, ushort tileY, ushort hits, ushort projectileType )
+ {
+ if (LandGolfBallInCup == null)
+ return false;
+
+ var args = new LandGolfBallInCupEventArgs
+ {
+ Player = player,
+ Data = data,
+ PlayerIndex = playerIndex,
+ TileX = tileX,
+ TileY = tileY,
+ Hits = hits,
+ ProjectileType = projectileType
+ };
+ LandGolfBallInCup.Invoke(null, args);
+ return args.Handled;
+ }
+
///
/// For use in a FishOutNPC event.
///
@@ -1997,6 +2081,40 @@ namespace TShockAPI
return args.Handled;
}
+ ///
+ /// Used when a net module is loaded
+ ///
+ public class ReadNetModuleEventArgs : GetDataHandledEventArgs
+ {
+ ///
+ /// The type of net module being loaded
+ ///
+ public NetModuleType ModuleType { get; set; }
+ }
+
+ ///
+ /// Called when a net module is received
+ ///
+ public static HandlerList ReadNetModule = new HandlerList();
+
+ private static bool OnReadNetModule(TSPlayer player, MemoryStream data, NetModuleType moduleType)
+ {
+ if (ReadNetModule == null)
+ {
+ return false;
+ }
+
+ var args = new ReadNetModuleEventArgs
+ {
+ Player = player,
+ Data = data,
+ ModuleType = moduleType
+ };
+
+ ReadNetModule.Invoke(null, args);
+ return args.Handled;
+ }
+
#endregion
private static bool HandlePlayerInfo(GetDataHandlerArgs args)
@@ -2254,11 +2372,10 @@ namespace TShockAPI
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 == 79)) && (WorldGen.StartRoomCheck(args.Player.sX, args.Player.sY - 1)))
+ 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("GetDataHandlers / HandleSpawn force teleport phase 1 {0}", args.Player.Name);
@@ -2267,7 +2384,7 @@ namespace TShockAPI
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 == 79)) && (WorldGen.StartRoomCheck(args.Player.sX, args.Player.sY - 1)))
+ 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("GetDataHandlers / HandleSpawn force teleport phase 2 {0}", args.Player.Name);
@@ -2974,21 +3091,21 @@ namespace TShockAPI
if (bosses.Contains(thingType) && !args.Player.HasPermission(Permissions.summonboss))
{
TShock.Log.ConsoleDebug("GetDataHandlers / HandleSpawnBoss rejected boss {0} {1}", args.Player.Name, thingType);
- args.Player.SendErrorMessage("You don't have permission to summon a boss.");
+ args.Player.SendErrorMessage("You do not have permission to summon a boss.");
return true;
}
if (invasions.Contains(thingType) && !args.Player.HasPermission(Permissions.startinvasion))
{
TShock.Log.ConsoleDebug("GetDataHandlers / HandleSpawnBoss rejected invasion {0} {1}", args.Player.Name, thingType);
- args.Player.SendErrorMessage("You don't have permission to start an invasion.");
+ args.Player.SendErrorMessage("You do not have permission to start an invasion.");
return true;
}
if (pets.Contains(thingType) && !args.Player.HasPermission(Permissions.spawnpets))
{
TShock.Log.ConsoleDebug("GetDataHandlers / HandleSpawnBoss rejected pet {0} {1}", args.Player.Name, thingType);
- args.Player.SendErrorMessage("You don't have permission to spawn pets.");
+ args.Player.SendErrorMessage("You do not have permission to spawn pets.");
return true;
}
@@ -3271,160 +3388,12 @@ namespace TShockAPI
private static bool HandleLoadNetModule(GetDataHandlerArgs args)
{
short moduleId = args.Data.ReadInt16();
- if (moduleId == (int)NetModulesTypes.CreativePowers)
+
+ if (OnReadNetModule(args.Player, args.Data, (NetModuleType)moduleId))
{
- CreativePowerTypes powerId = (CreativePowerTypes)args.Data.ReadInt16();
- switch (powerId)
- {
- case CreativePowerTypes.FreezeTime:
- {
- if (!args.Player.HasPermission(Permissions.journey_timefreeze))
- {
- args.Player.SendErrorMessage("You don't have permission to freeze the time of the server!");
- return true;
- }
- break;
- }
- case CreativePowerTypes.SetDawn:
- case CreativePowerTypes.SetNoon:
- case CreativePowerTypes.SetDusk:
- case CreativePowerTypes.SetMidnight:
- {
- if (!args.Player.HasPermission(Permissions.journey_timeset))
- {
- args.Player.SendErrorMessage("You don't have permission to modify the time of the server!");
- return true;
- }
- break;
- }
- case CreativePowerTypes.Godmode:
- {
- if (!args.Player.HasPermission(Permissions.journey_godmode))
- {
- args.Player.SendErrorMessage("You don't have permission to toggle godmode!");
- return true;
- }
- break;
- }
- case CreativePowerTypes.WindStrength:
- {
- if (!args.Player.HasPermission(Permissions.journey_windstrength))
- {
- args.Player.SendErrorMessage("You don't have permission to modify the wind strength of the server!");
- return true;
- }
- break;
- }
- case CreativePowerTypes.RainStrength:
- {
- if (!args.Player.HasPermission(Permissions.journey_rainstrength))
- {
- args.Player.SendErrorMessage("You don't have permission to modify the rain strength of the server!");
- return true;
- }
- break;
- }
- case CreativePowerTypes.TimeSpeed:
- {
- if (!args.Player.HasPermission(Permissions.journey_timespeed))
- {
- args.Player.SendErrorMessage("You don't have permission to modify the time speed of the server!");
- return true;
- }
- break;
- }
- case CreativePowerTypes.RainFreeze:
- {
- if (!args.Player.HasPermission(Permissions.journey_rainfreeze))
- {
- args.Player.SendErrorMessage("You don't have permission to freeze the rain strength of the server!");
- return true;
- }
- break;
- }
- case CreativePowerTypes.WindFreeze:
- {
- if (!args.Player.HasPermission(Permissions.journey_windfreeze))
- {
- args.Player.SendErrorMessage("You don't have permission to freeze the wind strength of the server!");
- return true;
- }
- break;
- }
- case CreativePowerTypes.IncreasePlacementRange:
- {
- if (!args.Player.HasPermission(Permissions.journey_placementrange))
- {
- args.Player.SendErrorMessage("You don't have permission to modify the tile placement range of your character!");
- return true;
- }
- break;
- }
- case CreativePowerTypes.WorldDifficulty:
- {
- if (!args.Player.HasPermission(Permissions.journey_setdifficulty))
- {
- args.Player.SendErrorMessage("You don't have permission to modify the world difficulty of the server!");
- return true;
- }
- break;
- }
- case CreativePowerTypes.BiomeSpreadFreeze:
- {
- if (!args.Player.HasPermission(Permissions.journey_biomespreadfreeze))
- {
- args.Player.SendErrorMessage("You don't have permission to freeze the biome spread of the server!");
- return true;
- }
- break;
- }
- case CreativePowerTypes.SetSpawnRate:
- {
- // This is a monkeypatch because the 1.4.0.4 seemingly at random sends NPC spawn rate changes even outside of journey mode
- // (with SSC on) -- particles, May 25, 2 Reiwa
- if (!Main.GameModeInfo.IsJourneyMode)
- {
- return true;
- }
- if (!args.Player.HasPermission(Permissions.journey_setspawnrate))
- {
- args.Player.SendErrorMessage("You don't have permission to modify the NPC spawn rate of the server!");
- return true;
- }
- break;
- }
- default:
- {
- return true;
- }
- }
- } else if (moduleId == (int)NetModulesTypes.CreativeUnlocksPlayerReport && Main.GameModeInfo.IsJourneyMode)
- {
- var unknownField = args.Data.ReadByte();
-
- if (unknownField == 0) //this is required or something???
- {
- var itemId = args.Data.ReadUInt16();
- var amount = args.Data.ReadUInt16();
-
- var totalSacrificed = TShock.ResearchDatastore.SacrificeItem(itemId, amount, args.Player);
-
- var response = NetCreativeUnlocksModule.SerializeItemSacrifice(itemId, totalSacrificed);
- NetManager.Instance.Broadcast(response);
- }
+ return true;
}
- // As of 1.4.x.x, this is now used for more things:
- // NetCreativePowersModule
- // NetCreativePowerPermissionsModule
- // NetLiquidModule
- // NetParticlesModule
- // NetPingModule
- // NetTeleportPylonModule
- // NetTextModule
- // I (particles) have disabled the original return here, which means that we need to
- // handle this more. In the interm, this unbreaks parts of vanilla. Originally
- // we just blocked this because it was a liquid exploit.
return false;
}
@@ -3622,7 +3591,7 @@ namespace TShockAPI
if (!args.Player.HasPermission(Permissions.startdd2))
{
TShock.Log.ConsoleDebug("GetDataHandlers / HandleOldOnesArmy rejected permissions {0}", args.Player.Name);
- args.Player.SendErrorMessage("You don't have permission to start the Old One's Army event.");
+ args.Player.SendErrorMessage("You do not have permission to start the Old One's Army event.");
return true;
}
@@ -3716,6 +3685,46 @@ namespace TShockAPI
return false;
}
+
+ private static bool HandleEmoji(GetDataHandlerArgs args)
+ {
+ byte playerIndex = args.Data.ReadInt8();
+ byte emojiID = args.Data.ReadInt8();
+
+ if (OnEmoji(args.Player, args.Data, playerIndex, emojiID))
+ return true;
+
+ return false;
+ }
+
+ private static bool HandleSyncRevengeMarker(GetDataHandlerArgs args)
+ {
+ int uniqueID = args.Data.ReadInt32();
+ Vector2 location = args.Data.ReadVector2();
+ int netId = args.Data.ReadInt32();
+ float npcHpPercent = args.Data.ReadSingle();
+ int npcTypeAgainstDiscouragement = args.Data.ReadInt32(); //tfw the argument is Type Against Discouragement
+ int npcAiStyleAgainstDiscouragement = args.Data.ReadInt32(); //see ^
+ int coinsValue = args.Data.ReadInt32();
+ float baseValue = args.Data.ReadSingle();
+ bool spawnedFromStatus = args.Data.ReadBoolean();
+
+ return false;
+ }
+
+ private static bool HandleLandGolfBallInCup(GetDataHandlerArgs args)
+ {
+ byte playerIndex = args.Data.ReadInt8();
+ ushort tileX = args.Data.ReadUInt16();
+ ushort tileY = args.Data.ReadUInt16();
+ ushort hits = args.Data.ReadUInt16();
+ ushort projectileType = args.Data.ReadUInt16();
+
+ if (OnLandGolfBallInCup(args.Player, args.Data, playerIndex, tileX, tileY, hits, projectileType))
+ return true;
+
+ return false;
+ }
private static bool HandleSyncTilePicking(GetDataHandlerArgs args)
{
@@ -3771,6 +3780,13 @@ namespace TShockAPI
return false;
}
+ private static bool HandleSyncCavernMonsterType(GetDataHandlerArgs args)
+ {
+ args.Player.Kick("Exploit attempt detected!");
+ TShock.Log.ConsoleDebug($"HandleSyncCavernMonsterType: Player is trying to modify NPC cavernMonsterType; this is a crafted packet! - From {args.Player.Name}");
+ return true;
+ }
+
public enum EditAction
{
KillTile = 0,
@@ -3952,7 +3968,7 @@ namespace TShockAPI
public bool Killed { get; internal set; }
}
- public enum NetModulesTypes
+ public enum NetModuleType
{
Liquid,
Text,
diff --git a/TShockAPI/Handlers/EmojiHandler.cs b/TShockAPI/Handlers/EmojiHandler.cs
new file mode 100644
index 00000000..f32993d1
--- /dev/null
+++ b/TShockAPI/Handlers/EmojiHandler.cs
@@ -0,0 +1,32 @@
+using static TShockAPI.GetDataHandlers;
+
+namespace TShockAPI.Handlers
+{
+ ///
+ /// Handles emoji packets and checks for validity and permissions
+ ///
+ public class EmojiHandler : IPacketHandler
+ {
+ ///
+ /// Invoked when an emoji is sent in chat. Rejects the emoji packet if the player is spoofing IDs or does not have emoji permissions
+ ///
+ ///
+ ///
+ public void OnReceive(object sender, EmojiEventArgs args)
+ {
+ if (args.PlayerIndex != args.Player.Index)
+ {
+ TShock.Log.ConsoleError($"EmojiHandler: Emoji packet rejected for ID spoofing. Expected {args.Player.Index}, received {args.PlayerIndex} from {args.Player.Name}.");
+ args.Handled = true;
+ return;
+ }
+
+ if (!args.Player.HasPermission(Permissions.sendemoji))
+ {
+ args.Player.SendErrorMessage("You do not have permission to send emotes!");
+ args.Handled = true;
+ return;
+ }
+ }
+ }
+}
diff --git a/TShockAPI/Handlers/IPacketHandler.cs b/TShockAPI/Handlers/IPacketHandler.cs
new file mode 100644
index 00000000..9b2e0444
--- /dev/null
+++ b/TShockAPI/Handlers/IPacketHandler.cs
@@ -0,0 +1,16 @@
+namespace TShockAPI.Handlers
+{
+ ///
+ /// Describes a packet handler that receives a packet from a GetDataHandler
+ ///
+ ///
+ public interface IPacketHandler where TEventArgs : GetDataHandledEventArgs
+ {
+ ///
+ /// Invoked when the packet is received
+ ///
+ ///
+ ///
+ void OnReceive(object sender, TEventArgs args);
+ }
+}
diff --git a/TShockAPI/Handlers/LandGolfBallInCupHandler.cs b/TShockAPI/Handlers/LandGolfBallInCupHandler.cs
new file mode 100644
index 00000000..43e95a35
--- /dev/null
+++ b/TShockAPI/Handlers/LandGolfBallInCupHandler.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Terraria;
+using Terraria.ID;
+using static TShockAPI.GetDataHandlers;
+
+namespace TShockAPI.Handlers
+{
+ ///
+ /// Handles client side exploits of LandGolfBallInCup packet.
+ ///
+ public class LandGolfBallInCupHandler : IPacketHandler
+ {
+ ///
+ /// List of golf ball projectile IDs.
+ ///
+ public static readonly List GolfBallProjectileIDs = new List()
+ {
+ ProjectileID.DirtGolfBall,
+ ProjectileID.GolfBallDyedBlack,
+ ProjectileID.GolfBallDyedBlue,
+ ProjectileID.GolfBallDyedBrown,
+ ProjectileID.GolfBallDyedCyan,
+ ProjectileID.GolfBallDyedGreen,
+ ProjectileID.GolfBallDyedLimeGreen,
+ ProjectileID.GolfBallDyedOrange,
+ ProjectileID.GolfBallDyedPink,
+ ProjectileID.GolfBallDyedPurple,
+ ProjectileID.GolfBallDyedRed,
+ ProjectileID.GolfBallDyedSkyBlue,
+ ProjectileID.GolfBallDyedTeal,
+ ProjectileID.GolfBallDyedViolet,
+ ProjectileID.GolfBallDyedYellow
+ };
+
+ ///
+ /// List of golf club item IDs
+ ///
+ public static readonly List GolfClubItemIDs = new List()
+ {
+ ItemID.GolfClubChlorophyteDriver,
+ ItemID.GolfClubDiamondWedge,
+ ItemID.GolfClubShroomitePutter,
+ ItemID.Fake_BambooChest,
+ ItemID.GolfClubTitaniumIron,
+ ItemID.GolfClubGoldWedge,
+ ItemID.GolfClubLeadPutter,
+ ItemID.GolfClubMythrilIron,
+ ItemID.GolfClubWoodDriver,
+ ItemID.GolfClubBronzeWedge,
+ ItemID.GolfClubRustyPutter,
+ ItemID.GolfClubStoneIron,
+ ItemID.GolfClubPearlwoodDriver,
+ ItemID.GolfClubIron,
+ ItemID.GolfClubDriver,
+ ItemID.GolfClubWedge,
+ ItemID.GolfClubPutter
+ };
+
+ ///
+ /// Invoked when a player lands a golf ball in a cup.
+ ///
+ ///
+ ///
+ public void OnReceive(object sender, LandGolfBallInCupEventArgs args)
+ {
+ if (args.PlayerIndex != args.Player.Index)
+ {
+ TShock.Log.ConsoleDebug($"LandGolfBallInCupHandler: Packet rejected for ID spoofing. Expected {args.PlayerIndex} , received {args.PlayerIndex} from {args.Player.Name}.");
+ args.Handled = true;
+ return;
+ }
+
+ if (args.TileX > Main.maxTilesX || args.TileX < 0
+ || args.TileY > Main.maxTilesY || args.TileY < 0)
+ {
+ TShock.Log.ConsoleDebug($"LandGolfBallInCupHandler: X and Y position is out of world bounds! - From {args.Player.Name}");
+ args.Handled = true;
+ return;
+ }
+
+ if (!Main.tile[args.TileX, args.TileY].active() && Main.tile[args.TileX, args.TileY].type != TileID.GolfHole)
+ {
+ TShock.Log.ConsoleDebug($"LandGolfBallInCupHandler: Tile at packet position X:{args.TileX} Y:{args.TileY} is not a golf hole! - From {args.Player.Name}");
+ args.Handled = true;
+ return;
+ }
+
+ if (!GolfBallProjectileIDs.Contains(args.ProjectileType))
+ {
+ TShock.Log.ConsoleDebug($"LandGolfBallInCupHandler: Invalid golf ball projectile ID {args.ProjectileType}! - From {args.Player.Name}");
+ args.Handled = true;
+ return;
+ }
+
+ var usedGolfBall = args.Player.RecentlyCreatedProjectiles.Any(e => GolfBallProjectileIDs.Contains(e.Type));
+ var usedGolfClub = args.Player.RecentlyCreatedProjectiles.Any(e => e.Type == ProjectileID.GolfClubHelper);
+ if (!usedGolfClub && !usedGolfBall)
+ {
+ TShock.Log.ConsoleDebug($"GolfPacketHandler: Player did not have create a golf club projectile the last 5 seconds! - From {args.Player.Name}");
+ args.Handled = true;
+ return;
+ }
+
+ if (!GolfClubItemIDs.Contains(args.Player.SelectedItem.type))
+ {
+ TShock.Log.ConsoleDebug($"LandGolfBallInCupHandler: Item selected is not a golf club! - From {args.Player.Name}");
+ args.Handled = true;
+ return;
+ }
+ }
+ }
+}
diff --git a/TShockAPI/Handlers/NetModules/AmbienceHandler.cs b/TShockAPI/Handlers/NetModules/AmbienceHandler.cs
new file mode 100644
index 00000000..0252c0c3
--- /dev/null
+++ b/TShockAPI/Handlers/NetModules/AmbienceHandler.cs
@@ -0,0 +1,28 @@
+using System.IO;
+
+namespace TShockAPI.Handlers.NetModules
+{
+ ///
+ /// Rejects ambience new modules from clients
+ ///
+ public class AmbienceHandler : INetModuleHandler
+ {
+ ///
+ /// No deserialization needed. This should never be received by the server
+ ///
+ ///
+ public void Deserialize(MemoryStream data)
+ {
+ }
+
+ ///
+ /// This should never be received by the server
+ ///
+ ///
+ ///
+ public void HandlePacket(TSPlayer player, out bool rejectPacket)
+ {
+ rejectPacket = true;
+ }
+ }
+}
diff --git a/TShockAPI/Handlers/NetModules/BestiaryHandler.cs b/TShockAPI/Handlers/NetModules/BestiaryHandler.cs
new file mode 100644
index 00000000..6338b1ce
--- /dev/null
+++ b/TShockAPI/Handlers/NetModules/BestiaryHandler.cs
@@ -0,0 +1,28 @@
+using System.IO;
+
+namespace TShockAPI.Handlers.NetModules
+{
+ ///
+ /// Rejects client->server bestiary net modules as the client should never send this to the server
+ ///
+ public class BestiaryHandler : INetModuleHandler
+ {
+ ///
+ /// No deserialization needed. This should never be received by the server
+ ///
+ ///
+ public void Deserialize(MemoryStream data)
+ {
+ }
+
+ ///
+ /// This should never be received by the server
+ ///
+ ///
+ ///
+ public void HandlePacket(TSPlayer player, out bool rejectPacket)
+ {
+ rejectPacket = true;
+ }
+ }
+}
diff --git a/TShockAPI/Handlers/NetModules/CreativePowerHandler.cs b/TShockAPI/Handlers/NetModules/CreativePowerHandler.cs
new file mode 100644
index 00000000..470890d1
--- /dev/null
+++ b/TShockAPI/Handlers/NetModules/CreativePowerHandler.cs
@@ -0,0 +1,113 @@
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Streams;
+using static TShockAPI.GetDataHandlers;
+
+namespace TShockAPI.Handlers.NetModules
+{
+ ///
+ /// Provides handling for the Creative Power net module. Checks permissions on all creative powers
+ ///
+ public class CreativePowerHandler : INetModuleHandler
+ {
+ ///
+ /// The power type being activated
+ ///
+ public CreativePowerTypes PowerType { get; set; }
+
+ ///
+ /// Reads the power type from the stream
+ ///
+ ///
+ public void Deserialize(MemoryStream data)
+ {
+ PowerType = (CreativePowerTypes)data.ReadInt16();
+ }
+
+ ///
+ /// Determines if the player has permission to use the power type
+ ///
+ ///
+ ///
+ public void HandlePacket(TSPlayer player, out bool rejectPacket)
+ {
+ if (!HasPermission(PowerType, player))
+ {
+ rejectPacket = true;
+ return;
+ }
+
+ rejectPacket = false;
+ }
+
+ ///
+ /// Determines if a player has permission to use a specific creative power
+ ///
+ ///
+ ///
+ ///
+ public static bool HasPermission(CreativePowerTypes powerType, TSPlayer player)
+ {
+ if (!PowerToPermissionMap.ContainsKey(powerType))
+ {
+ TShock.Log.ConsoleDebug("CreativePowerHandler received permission check request for unknown creative power");
+ return false;
+ }
+
+ string permission = PowerToPermissionMap[powerType];
+
+ if (!player.HasPermission(permission))
+ {
+ player.SendErrorMessage("You do not have permission to {0}.", PermissionToDescriptionMap[permission]);
+ return false;
+ }
+
+ return true;
+ }
+
+
+ ///
+ /// Maps creative powers to permission nodes
+ ///
+ public static Dictionary PowerToPermissionMap = new Dictionary
+ {
+ { CreativePowerTypes.FreezeTime, Permissions.journey_timefreeze },
+ { CreativePowerTypes.SetDawn, Permissions.journey_timeset },
+ { CreativePowerTypes.SetNoon, Permissions.journey_timeset },
+ { CreativePowerTypes.SetDusk, Permissions.journey_timeset },
+ { CreativePowerTypes.SetMidnight, Permissions.journey_timeset },
+ { CreativePowerTypes.Godmode, Permissions.journey_godmode },
+ { CreativePowerTypes.WindStrength, Permissions.journey_windstrength },
+ { CreativePowerTypes.RainStrength, Permissions.journey_rainstrength },
+ { CreativePowerTypes.TimeSpeed, Permissions.journey_timespeed },
+ { CreativePowerTypes.RainFreeze, Permissions.journey_rainfreeze },
+ { CreativePowerTypes.WindFreeze, Permissions.journey_windfreeze },
+ { CreativePowerTypes.IncreasePlacementRange, Permissions.journey_placementrange },
+ { CreativePowerTypes.WorldDifficulty, Permissions.journey_setdifficulty },
+ { CreativePowerTypes.BiomeSpreadFreeze, Permissions.journey_biomespreadfreeze },
+ { CreativePowerTypes.SetSpawnRate, Permissions.journey_setspawnrate },
+ };
+
+ ///
+ /// Maps journey mode permission nodes to descriptions of what the permission allows
+ ///
+ public static Dictionary PermissionToDescriptionMap = new Dictionary
+ {
+ { Permissions.journey_timefreeze, "freeze the time of the server" },
+ { Permissions.journey_timeset, "modify the time of the server" },
+ { Permissions.journey_timeset, "modify the time of the server" },
+ { Permissions.journey_timeset, "modify the time of the server" },
+ { Permissions.journey_timeset, "modify the time of the server" },
+ { Permissions.journey_godmode, "toggle godmode" },
+ { Permissions.journey_windstrength, "modify the wind strength of the server" },
+ { Permissions.journey_rainstrength, "modify the rain strength of the server" },
+ { Permissions.journey_timespeed, "modify the time speed of the server" },
+ { Permissions.journey_rainfreeze, "freeze the rain strength of the server" },
+ { Permissions.journey_windfreeze, "freeze the wind strength of the server" },
+ { Permissions.journey_placementrange, "modify the tile placement range of your character" },
+ { Permissions.journey_setdifficulty, "modify the world difficulty of the server" },
+ { Permissions.journey_biomespreadfreeze, "freeze the biome spread of the server" },
+ { Permissions.journey_setspawnrate, "modify the NPC spawn rate of the server" },
+ };
+ }
+}
diff --git a/TShockAPI/Handlers/NetModules/CreativeUnlocksHandler.cs b/TShockAPI/Handlers/NetModules/CreativeUnlocksHandler.cs
new file mode 100644
index 00000000..68fe4f1a
--- /dev/null
+++ b/TShockAPI/Handlers/NetModules/CreativeUnlocksHandler.cs
@@ -0,0 +1,90 @@
+using System.IO;
+using System.IO.Streams;
+using Terraria;
+using Terraria.GameContent.NetModules;
+using Terraria.Net;
+
+namespace TShockAPI.Handlers.NetModules
+{
+ ///
+ /// Handles creative unlock requests
+ ///
+ public class CreativeUnlocksHandler : INetModuleHandler
+ {
+ ///
+ /// An unknown field. If this does not have a value of '0' the packet should be rejected.
+ ///
+ public byte UnknownField { get; set; }
+ ///
+ /// ID of the item being sacrificed
+ ///
+ public ushort ItemId { get; set; }
+ ///
+ /// Stack size of the item being sacrificed
+ ///
+ public ushort Amount { get; set; }
+
+ ///
+ /// Reads the unlock data from the stream
+ ///
+ ///
+ public void Deserialize(MemoryStream data)
+ {
+ // For whatever reason Terraria writes '0' to the stream at the beginning of this packet.
+ // If this value is not 0 then its been crafted by a non-vanilla client.
+ // We don't actually know why the 0 is written, so we're just going to call this UnknownField for now
+ UnknownField = data.ReadInt8();
+ if (UnknownField == 0)
+ {
+ ItemId = data.ReadUInt16();
+ Amount = data.ReadUInt16();
+ }
+ }
+
+ ///
+ /// Determines if the unlock is valid and the player has permission to perform the unlock.
+ /// Syncs unlock status if the packet is accepted
+ ///
+ ///
+ ///
+ public void HandlePacket(TSPlayer player, out bool rejectPacket)
+ {
+ if (!Main.GameModeInfo.IsJourneyMode)
+ {
+ TShock.Log.ConsoleDebug(
+ "NetModuleHandler received attempt to unlock sacrifice while not in journey mode from",
+ player.Name
+ );
+
+ rejectPacket = true;
+ return;
+ }
+
+ if (UnknownField != 0)
+ {
+ TShock.Log.ConsoleDebug(
+ "CreativeUnlocksHandler received non-vanilla unlock request. Random field value: {0} but should be 0 from {1}",
+ UnknownField,
+ player.Name
+ );
+
+ rejectPacket = true;
+ return;
+ }
+
+ if (!player.HasPermission(Permissions.journey_contributeresearch))
+ {
+ player.SendErrorMessage("You do not have permission to contribute research.");
+ rejectPacket = true;
+ return;
+ }
+
+ var totalSacrificed = TShock.ResearchDatastore.SacrificeItem(ItemId, Amount, player);
+
+ var response = NetCreativeUnlocksModule.SerializeItemSacrifice(ItemId, totalSacrificed);
+ NetManager.Instance.Broadcast(response);
+
+ rejectPacket = false;
+ }
+ }
+}
diff --git a/TShockAPI/Handlers/NetModules/INetModuleHandler.cs b/TShockAPI/Handlers/NetModules/INetModuleHandler.cs
new file mode 100644
index 00000000..459d22f4
--- /dev/null
+++ b/TShockAPI/Handlers/NetModules/INetModuleHandler.cs
@@ -0,0 +1,23 @@
+using System.IO;
+
+namespace TShockAPI.Handlers.NetModules
+{
+ ///
+ /// Describes a handler for a net module
+ ///
+ public interface INetModuleHandler
+ {
+ ///
+ /// Reads the net module's data from the given stream
+ ///
+ ///
+ void Deserialize(MemoryStream data);
+
+ ///
+ /// Provides handling for the packet and determines if it should be accepted or rejected
+ ///
+ ///
+ ///
+ void HandlePacket(TSPlayer player, out bool rejectPacket);
+ }
+}
diff --git a/TShockAPI/Handlers/NetModules/LiquidHandler.cs b/TShockAPI/Handlers/NetModules/LiquidHandler.cs
new file mode 100644
index 00000000..e7fe647f
--- /dev/null
+++ b/TShockAPI/Handlers/NetModules/LiquidHandler.cs
@@ -0,0 +1,29 @@
+using System.IO;
+
+namespace TShockAPI.Handlers.NetModules
+{
+ ///
+ /// Handles the NetLiquidModule. Rejects all incoming net liquid requests, as clients should never send them
+ ///
+ public class LiquidHandler : INetModuleHandler
+ {
+ ///
+ /// Does nothing. We should not deserialize this data
+ ///
+ ///
+ public void Deserialize(MemoryStream data)
+ {
+ // No need to deserialize
+ }
+
+ ///
+ /// Rejects the packet. Clients should not send this to us
+ ///
+ ///
+ ///
+ public void HandlePacket(TSPlayer player, out bool rejectPacket)
+ {
+ rejectPacket = true;
+ }
+ }
+}
diff --git a/TShockAPI/Handlers/NetModules/NetModulePacketHandler.cs b/TShockAPI/Handlers/NetModules/NetModulePacketHandler.cs
new file mode 100644
index 00000000..16d58640
--- /dev/null
+++ b/TShockAPI/Handlers/NetModules/NetModulePacketHandler.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using Terraria;
+using static TShockAPI.GetDataHandlers;
+
+namespace TShockAPI.Handlers.NetModules
+{
+ ///
+ /// Handles packet 82 - Load Net Module packets
+ ///
+ public class NetModulePacketHandler : IPacketHandler
+ {
+ ///
+ /// Maps net module types to handlers for the net module type. Add to or edit this dictionary to customise handling
+ ///
+ public static Dictionary NetModulesToHandlersMap = new Dictionary
+ {
+ { NetModuleType.CreativePowers, typeof(CreativePowerHandler) },
+ { NetModuleType.CreativeUnlocksPlayerReport, typeof(CreativeUnlocksHandler) },
+ { NetModuleType.TeleportPylon, typeof(PylonHandler) },
+ { NetModuleType.Liquid, typeof(LiquidHandler) },
+ { NetModuleType.Bestiary, typeof(BestiaryHandler) },
+ { NetModuleType.Ambience, typeof(AmbienceHandler) }
+ };
+
+ ///
+ /// Invoked when a load net module packet is received. This method picks a based on the
+ /// net module type being loaded, then forwards the data to the chosen handler to process
+ ///
+ ///
+ ///
+ public void OnReceive(object sender, ReadNetModuleEventArgs args)
+ {
+ INetModuleHandler handler;
+
+ if (NetModulesToHandlersMap.ContainsKey(args.ModuleType))
+ {
+ handler = (INetModuleHandler)Activator.CreateInstance(NetModulesToHandlersMap[args.ModuleType]);
+ }
+ else
+ {
+ // We don't have handlers for NetModuleType.Ping and NetModuleType.Particles.
+ // These net modules are fairly innocuous and can be processed normally by the game
+ args.Handled = false;
+ return;
+ }
+
+ handler.Deserialize(args.Data);
+ handler.HandlePacket(args.Player, out bool rejectPacket);
+
+ args.Handled = rejectPacket;
+ }
+ }
+}
diff --git a/TShockAPI/Handlers/NetModules/PylonHandler.cs b/TShockAPI/Handlers/NetModules/PylonHandler.cs
new file mode 100644
index 00000000..75ca0c1d
--- /dev/null
+++ b/TShockAPI/Handlers/NetModules/PylonHandler.cs
@@ -0,0 +1,62 @@
+using System.IO;
+using System.IO.Streams;
+using Terraria.GameContent;
+using static Terraria.GameContent.NetModules.NetTeleportPylonModule;
+
+namespace TShockAPI.Handlers.NetModules
+{
+ ///
+ /// Handles a pylon net module
+ ///
+ public class PylonHandler : INetModuleHandler
+ {
+ ///
+ /// Event occuring
+ ///
+ public SubPacketType PylonEventType { get; set; }
+ ///
+ /// Tile X coordinate of the pylon
+ ///
+ public short TileX { get; set; }
+ ///
+ /// Tile Y coordinate of the pylon
+ ///
+ public short TileY { get; set; }
+ ///
+ /// Type of Pylon
+ ///
+ public TeleportPylonType PylonType { get; set; }
+
+ ///
+ /// Reads the pylon data from the net module
+ ///
+ ///
+ public void Deserialize(MemoryStream data)
+ {
+ PylonEventType = (SubPacketType)data.ReadInt8();
+ TileX = data.ReadInt16();
+ TileY = data.ReadInt16();
+ PylonType = (TeleportPylonType)data.ReadInt8();
+ }
+
+ ///
+ /// Rejects a pylon teleport request if the player does not have permission
+ ///
+ ///
+ ///
+ public void HandlePacket(TSPlayer player, out bool rejectPacket)
+ {
+ if (PylonEventType == SubPacketType.PlayerRequestsTeleport)
+ {
+ if (!player.HasPermission(Permissions.pylon))
+ {
+ rejectPacket = true;
+ player.SendErrorMessage("You do not have permission to teleport with pylons.");
+ return;
+ }
+ }
+
+ rejectPacket = false;
+ }
+ }
+}
diff --git a/TShockAPI/Handlers/SendTileSquareHandler.cs b/TShockAPI/Handlers/SendTileSquareHandler.cs
index 48462a03..39a7e751 100644
--- a/TShockAPI/Handlers/SendTileSquareHandler.cs
+++ b/TShockAPI/Handlers/SendTileSquareHandler.cs
@@ -14,12 +14,12 @@ namespace TShockAPI.Handlers
///
/// Provides processors for handling Tile Square packets
///
- public class SendTileSquareHandler
+ public class SendTileSquareHandler : IPacketHandler
{
///
/// Maps grass-type blocks to flowers that can be grown on them with flower boots
///
- Dictionary> _grassToPlantMap = new Dictionary>
+ public static Dictionary> GrassToPlantMap = new Dictionary>
{
{ TileID.Grass, new List { TileID.Plants, TileID.Plants2 } },
{ TileID.HallowedGrass, new List { TileID.HallowedPlants, TileID.HallowedPlants2 } },
@@ -29,7 +29,7 @@ namespace TShockAPI.Handlers
///
/// Item IDs that can spawn flowers while you walk
///
- List _flowerBootItems = new List
+ public static List FlowerBootItems = new List
{
ItemID.FlowerBoots,
ItemID.FairyBoots
@@ -40,7 +40,7 @@ namespace TShockAPI.Handlers
/// Note: is empty at the time of writing, but entities are dynamically assigned their ID at initialize time
/// which is why we can use the _myEntityId field on each entity type
///
- Dictionary _tileEntityIdToTileIdMap = new Dictionary
+ public static Dictionary TileEntityIdToTileIdMap = new Dictionary
{
{ TileID.TargetDummy, TETrainingDummy._myEntityID },
{ TileID.ItemFrame, TEItemFrame._myEntityID },
@@ -57,7 +57,7 @@ namespace TShockAPI.Handlers
///
///
///
- public void OnReceiveSendTileSquare(object sender, GetDataHandlers.SendTileSquareEventArgs args)
+ public void OnReceive(object sender, GetDataHandlers.SendTileSquareEventArgs args)
{
// By default, we'll handle everything
args.Handled = true;
@@ -191,14 +191,20 @@ namespace TShockAPI.Handlers
TShock.Log.ConsoleDebug("Bouncer / SendTileSquare rejected from no permission for tile object from {0}", args.Player.Name);
return;
}
+
+ if (TShock.TileBans.TileIsBanned((short)tileType))
+ {
+ TShock.Log.ConsoleDebug("Bouncer / SendTileSquare rejected for banned tile");
+ return;
+ }
// Update all tiles in the tile object. These will be sent back to the player later
UpdateMultipleServerTileStates(realX, realY, width, height, newTiles);
// Tile entities have special placements that we should let the game deal with
- if (_tileEntityIdToTileIdMap.ContainsKey(tileType))
+ if (TileEntityIdToTileIdMap.ContainsKey(tileType))
{
- TileEntity.PlaceEntityNet(realX, realY, _tileEntityIdToTileIdMap[tileType]);
+ TileEntity.PlaceEntityNet(realX, realY, TileEntityIdToTileIdMap[tileType]);
}
}
@@ -214,7 +220,7 @@ namespace TShockAPI.Handlers
{
// Some boots allow growing flowers on grass. This process sends a 1x1 tile square to grow the flowers
// The square size must be 1 and the player must have an accessory that allows growing flowers in order for this square to be valid
- if (squareSize == 1 && args.Player.Accessories.Any(a => a != null && _flowerBootItems.Contains(a.type)))
+ if (squareSize == 1 && args.Player.Accessories.Any(a => a != null && FlowerBootItems.Contains(a.type)))
{
ProcessFlowerBoots(realX, realY, newTile, args);
return;
@@ -254,7 +260,7 @@ namespace TShockAPI.Handlers
}
ITile tile = Main.tile[realX, realY + 1];
- if (!_grassToPlantMap.TryGetValue(tile.type, out List plantTiles) && !plantTiles.Contains(newTile.Type))
+ if (!GrassToPlantMap.TryGetValue(tile.type, out List plantTiles) && !plantTiles.Contains(newTile.Type))
{
// If the tile below the tile square isn't a valid plant tile (eg grass) then we don't update the server tile state
return;
diff --git a/TShockAPI/Permissions.cs b/TShockAPI/Permissions.cs
index baf21a73..643882f8 100644
--- a/TShockAPI/Permissions.cs
+++ b/TShockAPI/Permissions.cs
@@ -267,6 +267,9 @@ namespace TShockAPI
[Description("User can use wormhole potions.")]
public static readonly string wormhole = "tshock.tp.wormhole";
+
+ [Description("User can use pylons to teleport")]
+ public static readonly string pylon = "tshock.tp.pylon";
#endregion
#region tshock.world nodes
@@ -409,6 +412,9 @@ namespace TShockAPI
[Description("User can use Creative UI to set the NPC spawn rate of the world.")]
public static readonly string journey_setspawnrate = "tshock.journey.setspawnrate";
+
+ [Description("User can contribute research by sacrificing items")]
+ public static readonly string journey_contributeresearch = "tshock.journey.research";
#endregion
#region Non-grouped
@@ -468,6 +474,9 @@ namespace TShockAPI
[Description("Player can resync themselves with server state.")]
public static readonly string synclocalarea = "tshock.synclocalarea";
+
+ [Description("Player can send emotes.")]
+ public static readonly string sendemoji = "tshock.sendemoji";
#endregion
///
/// Lists all commands associated with a given permission
diff --git a/TShockAPI/TSPlayer.cs b/TShockAPI/TSPlayer.cs
index 54be5041..ade16234 100644
--- a/TShockAPI/TSPlayer.cs
+++ b/TShockAPI/TSPlayer.cs
@@ -664,13 +664,13 @@ namespace TShockAPI
switch (failure)
{
case BuildPermissionFailPoint.GeneralBuild:
- SendErrorMessage("You lack permission to build on this server.");
+ SendErrorMessage("You do not have permission to build on this server.");
break;
case BuildPermissionFailPoint.SpawnProtect:
- SendErrorMessage("You lack permission to build in the spawn point.");
+ SendErrorMessage("You do not have permission to build in the spawn point.");
break;
case BuildPermissionFailPoint.Regions:
- SendErrorMessage("You lack permission to build in this region.");
+ SendErrorMessage("You do not have permission to build in this region.");
break;
}
diff --git a/TShockAPI/TShockAPI.csproj b/TShockAPI/TShockAPI.csproj
index ea6f4f48..495127c8 100644
--- a/TShockAPI/TShockAPI.csproj
+++ b/TShockAPI/TShockAPI.csproj
@@ -88,6 +88,17 @@
+
+
+
+
+
+
+
+
+
+
+