diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49ffe302..7be60f9a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,10 @@
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.
## TShock 4.4.0 (Pre-release 10)
diff --git a/TShockAPI/Bouncer.cs b/TShockAPI/Bouncer.cs
index 395bfddd..b9bb2e05 100644
--- a/TShockAPI/Bouncer.cs
+++ b/TShockAPI/Bouncer.cs
@@ -37,13 +37,21 @@ 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; }
/// 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.OnReceiveEmoji;
// 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 f86104d9..00172763 100644
--- a/TShockAPI/GetDataHandlers.cs
+++ b/TShockAPI/GetDataHandlers.cs
@@ -151,7 +151,8 @@ namespace TShockAPI
{ PacketTypes.CrystalInvasionStart, HandleOldOnesArmy },
{ PacketTypes.PlayerHurtV2, HandlePlayerDamageV2 },
{ PacketTypes.PlayerDeathV2, HandlePlayerKillMeV2 },
- { PacketTypes.SyncRevengeMarker, HandleSyncRevengeMarker },
+ { PacketTypes.Emoji, HandleEmoji },
+ { PacketTypes.SyncRevengeMarker, HandleSyncRevengeMarker },
{ PacketTypes.FishOutNPC, HandleFishOutNPC },
{ PacketTypes.FoodPlatterTryPlacing, HandleFoodPlatterTryPlacing },
{ PacketTypes.SyncCavernMonsterType, HandleSyncCavernMonsterType }
@@ -1867,6 +1868,40 @@ namespace TShockAPI
return args.Handled;
}
+ ///
+ /// 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 FishOutNPC event.
///
@@ -1952,6 +1987,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)
@@ -2209,11 +2278,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);
@@ -2222,7 +2290,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);
@@ -2929,21 +2997,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;
}
@@ -3226,160 +3294,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;
}
@@ -3577,7 +3497,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;
}
@@ -3671,8 +3591,19 @@ namespace TShockAPI
return false;
}
+
+ private static bool HandleEmoji(GetDataHandlerArgs args)
+ {
+ byte playerIndex = args.Data.ReadInt8();
+ byte emojiID = args.Data.ReadInt8();
- private static bool HandleSyncRevengeMarker(GetDataHandlerArgs args)
+ 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();
@@ -3683,8 +3614,8 @@ namespace TShockAPI
int coinsValue = args.Data.ReadInt32();
float baseValue = args.Data.ReadSingle();
bool spawnedFromStatus = args.Data.ReadBoolean();
-
- return false;
+
+ return false;
}
private static bool HandleFishOutNPC(GetDataHandlerArgs args)
@@ -3901,7 +3832,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..b21cd53e
--- /dev/null
+++ b/TShockAPI/Handlers/EmojiHandler.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TShockAPI.Handlers
+{
+ ///
+ /// Handles emoji packets and checks for validity and permissions
+ ///
+ public class EmojiHandler
+ {
+ public void OnReceiveEmoji(object sender, GetDataHandlers.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/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..fef6f380 100644
--- a/TShockAPI/TShockAPI.csproj
+++ b/TShockAPI/TShockAPI.csproj
@@ -88,6 +88,16 @@
+
+
+
+
+
+
+
+
+
+