Merge branch 'general-devel' into patch-10

This commit is contained in:
Chris 2020-06-01 19:04:45 +09:30 committed by GitHub
commit 2edfef0dfe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 740 additions and 290 deletions

View file

@ -5,68 +5,62 @@ on: [push, pull_request]
jobs: jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
strategy:
matrix:
mode: ["Debug", "Release"]
steps: steps:
- uses: actions/checkout@v1 - name: Git checkout
uses: actions/checkout@v1
with: with:
submodules: recursive submodules: recursive
- name: Install nuget - name: Install NuGet client
run: choco install nuget.commandline uses: nuget/setup-nuget@v1
- name: OTAPI Debug - name: Restore NuGet packages
shell: cmd
run: | run: |
nuget restore .\TerrariaServerAPI\TShock.4.OTAPI.sln nuget restore .\TerrariaServerAPI\TShock.4.OTAPI.sln
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TerrariaServerAPI\TShock.4.OTAPI.sln /p:Configuration=Debug nuget restore TShock.sln
cd .\TerrariaServerAPI\TShock.Modifications.Bootstrapper\bin\Debug - name: Build OTAPI
TShock.Modifications.Bootstrapper.exe
- name: OTAPI Release
shell: cmd shell: cmd
run: | run: |
nuget restore .\TerrariaServerAPI\TShock.4.OTAPI.sln "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TerrariaServerAPI\TShock.4.OTAPI.sln /p:Configuration=${{ matrix.mode }}
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TerrariaServerAPI\TShock.4.OTAPI.sln /p:Configuration=Release cd .\TerrariaServerAPI\TShock.Modifications.Bootstrapper\bin\${{ matrix.mode }}
cd .\TerrariaServerAPI\TShock.Modifications.Bootstrapper\bin\Release
TShock.Modifications.Bootstrapper.exe TShock.Modifications.Bootstrapper.exe
- name: TerrariaServerAPI Debug - name: Build TerrariaServerAPI
shell: cmd shell: cmd
run: | run: |
cd .\TerrariaServerAPI cd .\TerrariaServerAPI
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TerrariaServerAPI\TerrariaServerAPI.csproj /p:Configuration=Debug "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TerrariaServerAPI\TerrariaServerAPI.csproj /p:Configuration=${{ matrix.mode }}
- name: TShock Debug - name: Build TShock
shell: cmd shell: cmd
run: | run: |
nuget restore TShock.sln "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TShockAPI\TShockAPI.csproj /p:Configuration=${{ matrix.mode }}
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TShockAPI\TShockAPI.csproj /p:Configuration=Debug
- name: TerrariaServerAPI Release
shell: cmd
run: |
cd .\TerrariaServerAPI
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TerrariaServerAPI\TerrariaServerAPI.csproj /p:Configuration=Release
- name: TShock Release
shell: cmd
run: |
nuget restore TShock.sln
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TShockAPI\TShockAPI.csproj /p:Configuration=Release
- name: Normalize release packaging - name: Normalize release packaging
shell: cmd shell: cmd
run: | run: |
xcopy /Y prebuilts\*.* TShockAPI\bin\Release xcopy /Y prebuilts\*.* TShockAPI\bin\${{ matrix.mode }}
xcopy /Y prebuilts\*.* TShockAPI\bin\Debug mkdir TShockAPI\bin\${{ matrix.mode }}\ServerPlugins
mkdir TShockAPI\bin\Debug\ServerPlugins move TShockAPI\bin\${{ matrix.mode }}\TShockAPI.dll TShockAPI\bin\${{ matrix.mode }}\ServerPlugins
mkdir TShockAPI\bin\Release\ServerPlugins - name: Upload TShock (Debug)
move TShockAPI\bin\Release\TShockAPI.dll TShockAPI\bin\Release\ServerPlugins if: contains(matrix.mode, 'Debug')
move TShockAPI\bin\Debug\TShockAPI.dll TShockAPI\bin\Debug\ServerPlugins uses: actions/upload-artifact@master
- uses: actions/upload-artifact@master
with:
name: Experimental TShock (not debug)
path: TShockAPI\bin\Release
- uses: actions/upload-artifact@master
with: with:
name: Experimental TShock (debug) name: Experimental TShock (debug)
path: TShockAPI\bin\Debug path: TShockAPI\bin\Debug
- uses: actions/upload-artifact@master - name: Upload OTAPI Bootstrapper (Debug)
if: contains(matrix.mode, 'Debug')
uses: actions/upload-artifact@master
with: with:
name: Experimental (debug) OTAPI Bootstrapper name: Experimental (debug) OTAPI Bootstrapper
path: .\TerrariaServerAPI\TShock.Modifications.Bootstrapper\bin\Debug\TShock.Modifications.Bootstrapper.exe path: .\TerrariaServerAPI\TShock.Modifications.Bootstrapper\bin\Debug\TShock.Modifications.Bootstrapper.exe
- uses: actions/upload-artifact@master - name: Upload TShock (Not Debug)
if: contains(matrix.mode, 'Release')
uses: actions/upload-artifact@master
with:
name: Experimental TShock (not debug)
path: TShockAPI\bin\Release
- name: Upload OTAPI Bootstrapper (Not Debug)
if: contains(matrix.mode, 'Release')
uses: actions/upload-artifact@master
with: with:
name: Experimental (not debug) OTAPI Bootstrapper name: Experimental (not debug) OTAPI Bootstrapper
path: .\TerrariaServerAPI\TShock.Modifications.Bootstrapper\bin\Release\TShock.Modifications.Bootstrapper.exe path: .\TerrariaServerAPI\TShock.Modifications.Bootstrapper\bin\Release\TShock.Modifications.Bootstrapper.exe

View file

@ -2,7 +2,14 @@
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. 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 Release ## Upcoming release
* Fix all rope coils. (@Olink)
* Fixed a longstanding issue with SendTileSquare that could result in desyncs and visual errors. (@QuiCM)
* Fixed placement issues with Item Frames, Teleportation Pylons, etc. (@QuiCM)
* Doors are good now for real probably (@QuiCM, @Hakusaro, @Olink)
* Bump default max damage received cap to 42,000 to accommodate the Empress of Light's instant kill death amount. (@hakusaro, @moisterrific, @Irethia, @Ayrawei)
## TShock 4.4.0 (Pre-release 9)
* Fixed pet licenses. (@Olink) * Fixed pet licenses. (@Olink)
* Added initial support for Journey mode in SSC worlds. (@Olink) * Added initial support for Journey mode in SSC worlds. (@Olink)
* Made TShock database MySQL 8 compatible by escaping column names in our IQueryBuilder code. (Name `Groups` is a reserved element in this version, which is used in our `Region` table.) (@Patrikkk) * Made TShock database MySQL 8 compatible by escaping column names in our IQueryBuilder code. (Name `Groups` is a reserved element in this version, which is used in our `Region` table.) (@Patrikkk)
@ -28,6 +35,8 @@ This is the rolling changelog for TShock for Terraria. Use past tense when addin
* The default group that gets this permission is `Guest` for the time being. * The default group that gets this permission is `Guest` for the time being.
* To add this command to your guest group, give them `tshock.synclocalarea`, with `/group addperm guest tshock.synclocalarea`. * To add this command to your guest group, give them `tshock.synclocalarea`, with `/group addperm guest tshock.synclocalarea`.
* This command may be removed at any time in the future (and will likely be removed when send tile square handling is fixed). * This command may be removed at any time in the future (and will likely be removed when send tile square handling is fixed).
* Add FishOutNPC event handler, which is called whenever a player fishes out an NPC using a fishing rod. Added antihack to Bouncer, to prevent unathorized and invalid mob spawning, by checking player action, NPC IDs and range. (@Patrikkk, @moisterrific)
* Fixed smart door automatic door desync and deletion issue. (@hakusaro)
## TShock 4.4.0 (Pre-release 8) ## TShock 4.4.0 (Pre-release 8)
* Update for OTAPI 2.0.0.36 and Terraria 1.4.0.4. (@hakusaro, @Patrikkk, @DeathCradle) * Update for OTAPI 2.0.0.36 and Terraria 1.4.0.4. (@hakusaro, @Patrikkk, @DeathCradle)

View file

@ -36,15 +36,19 @@ namespace TShockAPI
/// <summary>Bouncer is the TShock anti-hack and anti-cheat system.</summary> /// <summary>Bouncer is the TShock anti-hack and anti-cheat system.</summary>
internal sealed class Bouncer internal sealed class Bouncer
{ {
internal Handlers.SendTileSquareHandler STSHandler { get; set; }
/// <summary>Constructor call initializes Bouncer and related functionality.</summary> /// <summary>Constructor call initializes Bouncer and related functionality.</summary>
/// <returns>A new Bouncer.</returns> /// <returns>A new Bouncer.</returns>
internal Bouncer() internal Bouncer()
{ {
STSHandler = new Handlers.SendTileSquareHandler();
GetDataHandlers.SendTileSquare += STSHandler.OnReceiveSendTileSquare;
// Setup hooks // Setup hooks
GetDataHandlers.GetSection += OnGetSection; GetDataHandlers.GetSection += OnGetSection;
GetDataHandlers.PlayerUpdate += OnPlayerUpdate; GetDataHandlers.PlayerUpdate += OnPlayerUpdate;
GetDataHandlers.TileEdit += OnTileEdit; GetDataHandlers.TileEdit += OnTileEdit;
GetDataHandlers.SendTileSquare += OnSendTileSquare;
GetDataHandlers.ItemDrop += OnItemDrop; GetDataHandlers.ItemDrop += OnItemDrop;
GetDataHandlers.NewProjectile += OnNewProjectile; GetDataHandlers.NewProjectile += OnNewProjectile;
GetDataHandlers.NPCStrike += OnNPCStrike; GetDataHandlers.NPCStrike += OnNPCStrike;
@ -67,6 +71,7 @@ namespace TShockAPI
GetDataHandlers.MassWireOperation += OnMassWireOperation; GetDataHandlers.MassWireOperation += OnMassWireOperation;
GetDataHandlers.PlayerDamage += OnPlayerDamage; GetDataHandlers.PlayerDamage += OnPlayerDamage;
GetDataHandlers.KillMe += OnKillMe; GetDataHandlers.KillMe += OnKillMe;
GetDataHandlers.FishOutNPC += OnFishOutNPC;
GetDataHandlers.FoodPlatterTryPlacing += OnFoodPlatterTryPlacing; GetDataHandlers.FoodPlatterTryPlacing += OnFoodPlatterTryPlacing;
} }
@ -296,6 +301,19 @@ namespace TShockAPI
{ {
args.Player.LastKilledProjectile = 0; args.Player.LastKilledProjectile = 0;
} }
else if (CoilTileIds.Contains(editData))
{
//projectile should be the same X coordinate as all tile places
if (!args.Player.RecentlyCreatedProjectiles.Any(p => GetDataHandlers.projectileCreatesTile.ContainsKey(Main.projectile[p.Index].type) &&
Math.Abs((int)(Main.projectile[p.Index].position.X / 16f) - tileX) <= Math.Abs(Main.projectile[p.Index].velocity.X) &&
GetDataHandlers.projectileCreatesTile[Main.projectile[p.Index].type] == editData))
{
TShock.Log.ConsoleDebug("Bouncer / OnTileEdit rejected from (inconceivable rope coil) {0} {1} {2}", args.Player.Name, action, editData);
args.Player.SendTileSquare(tileX, tileY, 1);
args.Handled = true;
return;
}
}
else if (action == EditAction.PlaceTile || action == EditAction.PlaceWall) else if (action == EditAction.PlaceTile || action == EditAction.PlaceWall)
{ {
if ((action == EditAction.PlaceTile && TShock.Config.PreventInvalidPlaceStyle) && if ((action == EditAction.PlaceTile && TShock.Config.PreventInvalidPlaceStyle) &&
@ -517,206 +535,6 @@ namespace TShockAPI
} }
} }
/// <summary>Bouncer's SendTileSquare hook halts large scope world destruction.</summary>
/// <param name="sender">The object that triggered the event.</param>
/// <param name="args">The packet arguments that the event has.</param>
internal void OnSendTileSquare(object sender, GetDataHandlers.SendTileSquareEventArgs args)
{
short size = args.Size;
int tileX = args.TileX;
int tileY = args.TileY;
if (args.Player.HasPermission(Permissions.allowclientsideworldedit))
{
TShock.Log.ConsoleDebug("Bouncer / SendTileSquare accepted clientside world edit from {0}", args.Player.Name);
args.Handled = false;
return;
}
// From White:
// IIRC it's because 5 means a 5x5 square which is normal for a tile square, and anything bigger is a non-vanilla tile modification attempt
if (size > 5)
{
TShock.Log.ConsoleDebug("Bouncer / SendTileSquare rejected from non-vanilla tilemod from {0}", args.Player.Name);
args.Handled = true;
return;
}
if (args.Player.IsBouncerThrottled())
{
TShock.Log.ConsoleDebug("Bouncer / SendTileSquare rejected from throttle from {0}", args.Player.Name);
args.Player.SendTileSquare(tileX, tileY, size);
args.Handled = true;
return;
}
if (args.Player.IsBeingDisabled())
{
TShock.Log.ConsoleDebug("Bouncer / SendTileSquare rejected from being disabled from {0}", args.Player.Name);
args.Player.SendTileSquare(tileX, tileY, size);
args.Handled = true;
return;
}
bool changed = false;
bool failed = false;
try
{
var tiles = new NetTile[size, size];
for (int x = 0; x < size; x++)
{
for (int y = 0; y < size; y++)
{
tiles[x, y] = new NetTile(args.Data);
}
}
for (int x = 0; x < size; x++)
{
int realx = tileX + x;
if (realx < 0 || realx >= Main.maxTilesX)
continue;
for (int y = 0; y < size; y++)
{
int realy = tileY + y;
if (realy < 0 || realy >= Main.maxTilesY)
continue;
var tile = Main.tile[realx, realy];
var newtile = tiles[x, y];
if (!args.Player.HasBuildPermission(realx, realy) ||
!args.Player.IsInRange(realx, realy))
{
continue;
}
// Fixes the Flower Boots not creating flowers issue
if (size == 1 && args.Player.Accessories.Any(i => i.active && i.netID == ItemID.FlowerBoots))
{
if (Main.tile[realx, realy + 1].type == TileID.Grass && (newtile.Type == TileID.Plants || newtile.Type == TileID.Plants2))
{
args.Handled = false;
return;
}
if (Main.tile[realx, realy + 1].type == TileID.HallowedGrass && (newtile.Type == TileID.HallowedPlants || newtile.Type == TileID.HallowedPlants2))
{
args.Handled = false;
return;
}
if (Main.tile[realx, realy + 1].type == TileID.JungleGrass && newtile.Type == TileID.JunglePlants2)
{
args.Handled = false;
return;
}
}
// Junction Box
if (tile.type == TileID.WirePipe)
{
args.Handled = false;
return;
}
// Orientable tiles
if (tile.type == newtile.Type && orientableTiles.Contains(tile.type))
{
Main.tile[realx, realy].frameX = newtile.FrameX;
Main.tile[realx, realy].frameY = newtile.FrameY;
changed = true;
}
// Landmine
if (tile.type == TileID.LandMine && !newtile.Active)
{
Main.tile[realx, realy].active(false);
changed = true;
}
// Tile entities: sensors, item frames, training dummies
// here it handles all tile entities listed in `TileEntityID`
if ((newtile.Type == TileID.LogicSensor ||
newtile.Type == TileID.ItemFrame ||
newtile.Type == TileID.TargetDummy) &&
!Main.tile[realx, realy].active())
{
Main.tile[realx, realy].type = newtile.Type;
Main.tile[realx, realy].frameX = newtile.FrameX;
Main.tile[realx, realy].frameY = newtile.FrameY;
Main.tile[realx, realy].active(true);
changed = true;
}
if (tile.active() && newtile.Active && tile.type != newtile.Type)
{
// Grass <-> Grass
if ((TileID.Sets.Conversion.Grass[tile.type] && TileID.Sets.Conversion.Grass[newtile.Type]) ||
// Dirt <-> Dirt
((tile.type == 0 || tile.type == 59) &&
(newtile.Type == 0 || newtile.Type == 59)) ||
// Ice <-> Ice
(TileID.Sets.Conversion.Ice[tile.type] && TileID.Sets.Conversion.Ice[newtile.Type]) ||
// Stone <-> Stone
((TileID.Sets.Conversion.Stone[tile.type] || Main.tileMoss[tile.type]) &&
(TileID.Sets.Conversion.Stone[newtile.Type] || Main.tileMoss[newtile.Type])) ||
// Sand <-> Sand
(TileID.Sets.Conversion.Sand[tile.type] && TileID.Sets.Conversion.Sand[newtile.Type]) ||
// Sandstone <-> Sandstone
(TileID.Sets.Conversion.Sandstone[tile.type] && TileID.Sets.Conversion.Sandstone[newtile.Type]) ||
// Hardened Sand <-> Hardened Sand
(TileID.Sets.Conversion.HardenedSand[tile.type] && TileID.Sets.Conversion.HardenedSand[newtile.Type]))
{
Main.tile[realx, realy].type = newtile.Type;
changed = true;
}
}
// Stone wall <-> Stone wall
if (((tile.wall == 1 || tile.wall == 3 || tile.wall == 28 || tile.wall == 83) &&
(newtile.Wall == 1 || newtile.Wall == 3 || newtile.Wall == 28 || newtile.Wall == 83)) ||
// Leaf wall <-> Leaf wall
(((tile.wall >= 63 && tile.wall <= 70) || tile.wall == 81) &&
((newtile.Wall >= 63 && newtile.Wall <= 70) || newtile.Wall == 81)))
{
Main.tile[realx, realy].wall = newtile.Wall;
changed = true;
}
if ((tile.type == TileID.TrapdoorClosed && (newtile.Type == TileID.TrapdoorOpen || !newtile.Active)) ||
(tile.type == TileID.TrapdoorOpen && (newtile.Type == TileID.TrapdoorClosed || !newtile.Active)) ||
(!tile.active() && newtile.Active && (newtile.Type == TileID.TrapdoorOpen || newtile.Type == TileID.TrapdoorClosed)))
{
Main.tile[realx, realy].type = newtile.Type;
Main.tile[realx, realy].frameX = newtile.FrameX;
Main.tile[realx, realy].frameY = newtile.FrameY;
Main.tile[realx, realy].active(newtile.Active);
changed = true;
}
}
}
if (changed)
{
TSPlayer.All.SendTileSquare(tileX, tileY, size + 1);
WorldGen.RangeFrame(tileX, tileY, tileX + size, tileY + size);
}
else
{
args.Player.SendTileSquare(tileX, tileY, size);
}
}
catch
{
args.Player.SendTileSquare(tileX, tileY, size);
failed = true;
}
TShock.Log.ConsoleDebug("Bouncer / SendTileSquare from {0} {1} {2}", args.Player.Name, changed, failed);
args.Handled = true;
}
/// <summary>Registered when items fall to the ground to prevent cheating.</summary> /// <summary>Registered when items fall to the ground to prevent cheating.</summary>
/// <param name="sender">The object that triggered the event.</param> /// <param name="sender">The object that triggered the event.</param>
/// <param name="args">The packet arguments that the event has.</param> /// <param name="args">The packet arguments that the event has.</param>
@ -2036,7 +1854,7 @@ namespace TShockAPI
short id = args.PlayerId; short id = args.PlayerId;
PlayerDeathReason playerDeathReason = args.PlayerDeathReason; PlayerDeathReason playerDeathReason = args.PlayerDeathReason;
if (damage > 20000) //Abnormal values have the potential to cause infinite loops in the server. if (damage > 42000) //Abnormal values have the potential to cause infinite loops in the server.
{ {
TShock.Log.ConsoleDebug("Bouncer / OnKillMe rejected high damage from {0} {1}", args.Player.Name, damage); TShock.Log.ConsoleDebug("Bouncer / OnKillMe rejected high damage from {0} {1}", args.Player.Name, damage);
args.Player.Kick("Failed to shade polygon normals.", true, true); args.Player.Kick("Failed to shade polygon normals.", true, true);
@ -2065,6 +1883,34 @@ namespace TShockAPI
} }
} }
/// <summary>
/// Called when the player fishes out an NPC.
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
internal void OnFishOutNPC(object sender, GetDataHandlers.FishOutNPCEventArgs args)
{
var projectile = args.Player.RecentlyCreatedProjectiles.FirstOrDefault(p => Main.projectile[p.Index] != null && Main.projectile[p.Index].Name == "Bobber");
if (!FishingRodItemIDs.Contains(args.Player.SelectedItem.type) || Main.projectile[projectile.Index] == null || !FishableNpcIDs.Contains(args.NpcID))
{
TShock.Log.ConsoleDebug("Bouncer / OnFishOutNPC rejected invalid NPC spawning from {0}", args.Player.Name);
args.Handled = true;
return;
}
if (args.NpcID == NPCID.DukeFishron && !args.Player.HasPermission(Permissions.summonboss))
{
TShock.Log.ConsoleDebug("Bouncer / OnFishOutNPC rejected summon boss permissions from {0}", args.Player.Name);
args.Handled = true;
return;
}
if (args.Player.IsInRange(args.TileX, args.TileY, 55))
{
TShock.Log.ConsoleDebug("Bouncer / OnFishOutNPC rejected range checks from {0}", args.Player.Name);
args.Handled = true;
}
}
/// <summary> /// <summary>
/// Called when a player is trying to place an item into a food plate. /// Called when a player is trying to place an item into a food plate.
/// </summary> /// </summary>

View file

@ -151,6 +151,7 @@ namespace TShockAPI
{ PacketTypes.CrystalInvasionStart, HandleOldOnesArmy }, { PacketTypes.CrystalInvasionStart, HandleOldOnesArmy },
{ PacketTypes.PlayerHurtV2, HandlePlayerDamageV2 }, { PacketTypes.PlayerHurtV2, HandlePlayerDamageV2 },
{ PacketTypes.PlayerDeathV2, HandlePlayerKillMeV2 }, { PacketTypes.PlayerDeathV2, HandlePlayerKillMeV2 },
{ PacketTypes.FishOutNPC, HandleFishOutNPC },
{ PacketTypes.FoodPlatterTryPlacing, HandleFoodPlatterTryPlacing }, { PacketTypes.FoodPlatterTryPlacing, HandleFoodPlatterTryPlacing },
{ PacketTypes.SyncRevengeMarker, HandleSyncRevengeMarker } { PacketTypes.SyncRevengeMarker, HandleSyncRevengeMarker }
}; };
@ -1865,6 +1866,45 @@ namespace TShockAPI
return args.Handled; return args.Handled;
} }
/// <summary>
/// For use in a FishOutNPC event.
/// </summary>
public class FishOutNPCEventArgs : GetDataHandledEventArgs
{
/// <summary>
/// The X world position of the spawning NPC.
/// </summary>
public ushort TileX { get; set; }
/// <summary>
/// The Y world position of the spawning NPC.
/// </summary>
public ushort TileY { get; set; }
/// <summary>
/// The NPC type that is being spawned.
/// </summary>
public short NpcID { get; set; }
}
/// <summary>
/// Called when a player fishes out an NPC.
/// </summary>
public static HandlerList<FishOutNPCEventArgs> FishOutNPC = new HandlerList<FishOutNPCEventArgs>();
private static bool OnFishOutNPC(TSPlayer player, MemoryStream data, ushort tileX, ushort tileY, short npcID)
{
if (FishOutNPC == null)
return false;
var args = new FishOutNPCEventArgs
{
Player = player,
Data = data,
TileX = tileX,
TileY = tileY,
NpcID = npcID
};
FishOutNPC.Invoke(null, args);
return args.Handled;
}
public class FoodPlatterTryPlacingEventArgs : GetDataHandledEventArgs public class FoodPlatterTryPlacingEventArgs : GetDataHandledEventArgs
{ {
/// <summary> /// <summary>
@ -2317,14 +2357,14 @@ namespace TShockAPI
{ {
var player = args.Player; var player = args.Player;
var size = args.Data.ReadInt16(); var size = args.Data.ReadInt16();
var changeType = TileChangeType.None; var changeType = TileChangeType.None;
bool hasChangeType = ((size & 0x7FFF) & 0x8000) != 0; bool hasChangeType = ((size & 0x7FFF) & 0x8000) != 0;
if (hasChangeType) if (hasChangeType)
{ {
changeType = (TileChangeType)args.Data.ReadInt8(); changeType = (TileChangeType)args.Data.ReadInt8();
} }
var tileX = args.Data.ReadInt16(); var tileX = args.Data.ReadInt16();
var tileY = args.Data.ReadInt16(); var tileY = args.Data.ReadInt16();
var data = args.Data; var data = args.Data;
@ -3629,6 +3669,19 @@ namespace TShockAPI
return false; return false;
} }
private static bool HandleFishOutNPC(GetDataHandlerArgs args)
{
ushort tileX = args.Data.ReadUInt16();
ushort tileY = args.Data.ReadUInt16();
short npcType = args.Data.ReadInt16();
if (OnFishOutNPC(args.Player, args.Data, tileX, tileY, npcType))
return true;
return false;
}
private static bool HandleFoodPlatterTryPlacing(GetDataHandlerArgs args) private static bool HandleFoodPlatterTryPlacing(GetDataHandlerArgs args)
{ {
short tileX = args.Data.ReadInt16(); short tileX = args.Data.ReadInt16();
@ -3717,6 +3770,39 @@ namespace TShockAPI
TileID.Womannequin, TileID.Womannequin,
}; };
/// <summary>
/// List of Fishing rod item IDs.
/// </summary>
internal static readonly List<int> FishingRodItemIDs = new List<int>()
{
ItemID.WoodFishingPole,
ItemID.ReinforcedFishingPole,
ItemID.FiberglassFishingPole,
ItemID.FisherofSouls,
ItemID.GoldenFishingRod,
ItemID.MechanicsRod,
ItemID.SittingDucksFishingRod,
ItemID.Fleshcatcher,
ItemID.HotlineFishingHook,
ItemID.BloodFishingRod,
ItemID.ScarabFishingRod
};
/// <summary>
/// List of NPC IDs that can be fished out by the player.
/// </summary>
internal static readonly List<int> FishableNpcIDs = new List<int>()
{
NPCID.EyeballFlyingFish,
NPCID.ZombieMerman,
NPCID.GoblinShark,
NPCID.BloodEelHead,
NPCID.BloodEelBody,
NPCID.BloodEelTail,
NPCID.BloodNautilus,
NPCID.DukeFishron
};
/// <summary> /// <summary>
/// These projectiles create tiles on death. /// These projectiles create tiles on death.
/// </summary> /// </summary>
@ -3727,7 +3813,20 @@ namespace TShockAPI
{ ProjectileID.EbonsandBallGun, TileID.Ebonsand }, { ProjectileID.EbonsandBallGun, TileID.Ebonsand },
{ ProjectileID.PearlSandBallGun, TileID.Pearlsand }, { ProjectileID.PearlSandBallGun, TileID.Pearlsand },
{ ProjectileID.CrimsandBallGun, TileID.Crimsand }, { ProjectileID.CrimsandBallGun, TileID.Crimsand },
{ ProjectileID.MysticSnakeCoil, TileID.MysticSnakeRope } { ProjectileID.MysticSnakeCoil, TileID.MysticSnakeRope },
{ ProjectileID.RopeCoil, TileID.Rope },
{ ProjectileID.SilkRopeCoil, TileID.SilkRope },
{ ProjectileID.VineRopeCoil, TileID.VineRope },
{ ProjectileID.WebRopeCoil, TileID.WebRope }
};
internal static List<int> CoilTileIds = new List<int>()
{
TileID.MysticSnakeRope,
TileID.Rope,
TileID.SilkRope,
TileID.VineRope,
TileID.WebRope
}; };
internal static Dictionary<int, LiquidType> projectileCreatesLiquid = new Dictionary<int, LiquidType> internal static Dictionary<int, LiquidType> projectileCreatesLiquid = new Dictionary<int, LiquidType>

View file

@ -0,0 +1,504 @@
using OTAPI.Tile;
using System;
using System.Collections.Generic;
using System.Linq;
using Terraria;
using Terraria.DataStructures;
using Terraria.GameContent.Tile_Entities;
using Terraria.ID;
using Terraria.ObjectData;
using TShockAPI.Net;
namespace TShockAPI.Handlers
{
/// <summary>
/// Provides processors for handling Tile Square packets
/// </summary>
public class SendTileSquareHandler
{
/// <summary>
/// Maps grass-type blocks to flowers that can be grown on them with flower boots
/// </summary>
Dictionary<ushort, List<ushort>> _grassToPlantMap = new Dictionary<ushort, List<ushort>>
{
{ TileID.Grass, new List<ushort> { TileID.Plants, TileID.Plants2 } },
{ TileID.HallowedGrass, new List<ushort> { TileID.HallowedPlants, TileID.HallowedPlants2 } },
{ TileID.JungleGrass, new List<ushort> { TileID.JunglePlants, TileID.JunglePlants2 } }
};
/// <summary>
/// Item IDs that can spawn flowers while you walk
/// </summary>
List<int> _flowerBootItems = new List<int>
{
ItemID.FlowerBoots,
ItemID.FairyBoots
};
/// <summary>
/// Maps TileIDs to Tile Entity IDs.
/// Note: <see cref="Terraria.ID.TileEntityID"/> 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
/// </summary>
Dictionary<int, int> _tileEntityIdToTileIdMap = new Dictionary<int, int>
{
{ TileID.TargetDummy, TETrainingDummy._myEntityID },
{ TileID.ItemFrame, TEItemFrame._myEntityID },
{ TileID.LogicSensor, TELogicSensor._myEntityID },
{ TileID.DisplayDoll, TEDisplayDoll._myEntityID },
{ TileID.WeaponsRack2, TEWeaponsRack._myEntityID },
{ TileID.HatRack, TEHatRack._myEntityID },
{ TileID.FoodPlatter, TEFoodPlatter._myEntityID },
{ TileID.TeleportationPylon, TETeleportationPylon._myEntityID }
};
/// <summary>
/// Invoked when a SendTileSquare packet is received
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
public void OnReceiveSendTileSquare(object sender, GetDataHandlers.SendTileSquareEventArgs args)
{
// By default, we'll handle everything
args.Handled = true;
if (ShouldSkipProcessing(args))
{
return;
}
bool[,] processed = new bool[args.Size, args.Size];
NetTile[,] tiles = ReadNetTilesFromStream(args.Data, args.Size);
Debug.VisualiseTileSetDiff(args.TileX, args.TileY, args.Size, args.Size, tiles);
IterateTileSquare(tiles, processed, args);
// Uncommenting this function will send the same tile square 10 blocks above you for visualisation. This will modify your world and overwrite existing blocks.
// Use in test worlds only.
//Debug.DisplayTileSetInGame(tileX, tileY - 10, size, size, tiles, args.Player);
// If we are handling this event then we have updated the server's Main.tile state the way we want it.
// At this point we should send our state back to the client so they remain in sync with the server
if (args.Handled == true)
{
args.Player.SendTileSquare(args.TileX, args.TileY, args.Size);
TShock.Log.ConsoleDebug("Bouncer / SendTileSquare reimplemented from carbonara from {0}", args.Player.Name);
}
}
/// <summary>
/// Iterates over each tile in the tile square and performs processing on individual tiles or multi-tile Tile Objects
/// </summary>
/// <param name="tiles"></param>
/// <param name="processed"></param>
/// <param name="args"></param>
internal void IterateTileSquare(NetTile[,] tiles, bool[,] processed, GetDataHandlers.SendTileSquareEventArgs args)
{
short size = args.Size;
int tileX = args.TileX;
int tileY = args.TileY;
for (int x = 0; x < size; x++)
{
for (int y = 0; y < size; y++)
{
// Do not process already processed tiles
if (processed[x, y])
{
continue;
}
int realX = tileX + x;
int realY = tileY + y;
// Do not process tiles outside of the world boundaries
if ((realX < 0 || realX >= Main.maxTilesX)
|| (realY < 0 || realY > Main.maxTilesY))
{
processed[x, y] = true;
continue;
}
// Do not process tiles that the player cannot update
if (!args.Player.HasBuildPermission(realX, realY) ||
!args.Player.IsInRange(realX, realY))
{
processed[x, y] = true;
continue;
}
NetTile newTile = tiles[x, y];
TileObjectData data;
// If the new tile has an associated TileObjectData object, we take the tile and the surrounding tiles that make up the tile object
// and process them as a tile object
if (newTile.Type < TileObjectData._data.Count && TileObjectData._data[newTile.Type] != null)
{
data = TileObjectData._data[newTile.Type];
NetTile[,] newTiles;
int width = data.Width;
int height = data.Height;
int offset = 0;
if (newTile.Type == TileID.TrapdoorClosed)
{
// Trapdoors can modify a 2x3 space. When it closes it will have leftover tiles either on top or bottom.
// If we don't update these tiles, the trapdoor gets confused and disappears.
// So we capture all 6 possible tiles and offset ourselves 1 tile above the closed trapdoor to capture the entire 2x3 area
width = 2;
height = 3;
offset = -1;
}
newTiles = new NetTile[width, height];
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
newTiles[i, j] = tiles[x + i, y + j + offset];
processed[x + i, y + j] = true;
}
}
ProcessTileObject(newTile.Type, realX, realY + offset, width, height, newTiles, args);
continue;
}
// If the new tile does not have an associated tile object, process it as an individual tile
ProcessSingleTile(realX, realY, newTile, size, args);
processed[x, y] = true;
}
}
}
/// <summary>
/// Processes a tile object consisting of multiple tiles from the tile square packet
/// </summary>
/// <param name="tileType">The tile type the object is comprised of</param>
/// <param name="newTiles">2D array of NetTile containing the new tiles properties</param>
/// <param name="realX">X position at the top left of the object</param>
/// <param name="realY">Y position at the top left of the object</param>
/// <param name="width">Width of the tile object</param>
/// <param name="height">Height of the tile object</param>
/// <param name="args">SendTileSquareEventArgs containing event information</param>
internal void ProcessTileObject(int tileType, int realX, int realY, int width, int height, NetTile[,] newTiles, GetDataHandlers.SendTileSquareEventArgs args)
{
// As long as the player has permission to build, we should allow a tile object to be placed
// More in depth checks should take place in handlers for the Place Object (79), Update Tile Entity (86), and Place Tile Entity (87) packets
if (!args.Player.HasBuildPermissionForTileObject(realX, realY, width, height))
{
TShock.Log.ConsoleDebug("Bouncer / SendTileSquare rejected from no permission for tile object from {0}", args.Player.Name);
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))
{
TileEntity.PlaceEntityNet(realX, realY, _tileEntityIdToTileIdMap[tileType]);
}
}
/// <summary>
/// Processes a single tile from the tile square packet
/// </summary>
/// <param name="realX">X position at the top left of the object</param>
/// <param name="realY">Y position at the top left of the object</param>
/// <param name="newTile">The NetTile containing new tile properties</param>
/// <param name="squareSize">The size of the tile square being received</param>
/// <param name="args">SendTileSquareEventArgs containing event information</param>
internal void ProcessSingleTile(int realX, int realY, NetTile newTile, int squareSize, GetDataHandlers.SendTileSquareEventArgs args)
{
// 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)))
{
ProcessFlowerBoots(realX, realY, newTile, args);
return;
}
ITile tile = Main.tile[realX, realY];
if (tile.type == TileID.LandMine && !newTile.Active)
{
UpdateServerTileState(tile, newTile);
}
if (tile.type == TileID.WirePipe)
{
UpdateServerTileState(tile, newTile);
}
ProcessConversionSpreads(Main.tile[realX, realY], newTile);
// All other single tile updates should not be processed.
}
/// <summary>
/// Applies changes to a tile if a tile square for flower-growing boots is valid
/// </summary>
/// <param name="realX">The tile x position of the tile square packet - this is where the flowers are intending to grow</param>
/// <param name="realY">The tile y position of the tile square packet - this is where the flowers are intending to grow</param>
/// <param name="newTile">The NetTile containing information about the flowers that are being grown</param>
/// <param name="args">SendTileSquareEventArgs containing event information</param>
internal void ProcessFlowerBoots(int realX, int realY, NetTile newTile, GetDataHandlers.SendTileSquareEventArgs args)
{
// We need to get the tile below the tile square to determine what grass types are allowed
if (!WorldGen.InWorld(realX, realY + 1))
{
// If the tile below the tile square isn't valid, we return here and don't update the server tile state
return;
}
ITile tile = Main.tile[realX, realY + 1];
if (!_grassToPlantMap.TryGetValue(tile.type, out List<ushort> 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;
}
UpdateServerTileState(Main.tile[realX, realY], newTile);
}
/// <summary>
/// Updates a single tile on the server if it is a valid conversion from one tile or wall type to another (eg stone -> corrupt stone)
/// </summary>
/// <param name="tile">The tile to update</param>
/// <param name="newTile">The NetTile containing new tile properties</param>
internal void ProcessConversionSpreads(ITile tile, NetTile newTile)
{
// Update if the existing tile or wall is convertible and the new tile or wall is a valid conversion
if (
((TileID.Sets.Conversion.Stone[tile.type] || Main.tileMoss[tile.type]) && (TileID.Sets.Conversion.Stone[newTile.Type] || Main.tileMoss[newTile.Type])) ||
((tile.type == 0 || tile.type == 59) && (newTile.Type == 0 || newTile.Type == 59)) ||
TileID.Sets.Conversion.Grass[tile.type] && TileID.Sets.Conversion.Grass[newTile.Type] ||
TileID.Sets.Conversion.Ice[tile.type] && TileID.Sets.Conversion.Ice[newTile.Type] ||
TileID.Sets.Conversion.Sand[tile.type] && TileID.Sets.Conversion.Sand[newTile.Type] ||
TileID.Sets.Conversion.Sandstone[tile.type] && TileID.Sets.Conversion.Sandstone[newTile.Type] ||
TileID.Sets.Conversion.HardenedSand[tile.type] && TileID.Sets.Conversion.HardenedSand[newTile.Type] ||
TileID.Sets.Conversion.Thorn[tile.type] && TileID.Sets.Conversion.Thorn[newTile.Type] ||
TileID.Sets.Conversion.Moss[tile.type] && TileID.Sets.Conversion.Moss[newTile.Type] ||
TileID.Sets.Conversion.MossBrick[tile.type] && TileID.Sets.Conversion.MossBrick[newTile.Type] ||
WallID.Sets.Conversion.Stone[tile.wall] && WallID.Sets.Conversion.Stone[newTile.Wall] ||
WallID.Sets.Conversion.Grass[tile.wall] && WallID.Sets.Conversion.Grass[newTile.Wall] ||
WallID.Sets.Conversion.Sandstone[tile.wall] && WallID.Sets.Conversion.Sandstone[newTile.Wall] ||
WallID.Sets.Conversion.HardenedSand[tile.wall] && WallID.Sets.Conversion.HardenedSand[newTile.Wall] ||
WallID.Sets.Conversion.PureSand[tile.wall] && WallID.Sets.Conversion.PureSand[newTile.Wall] ||
WallID.Sets.Conversion.NewWall1[tile.wall] && WallID.Sets.Conversion.NewWall1[newTile.Wall] ||
WallID.Sets.Conversion.NewWall2[tile.wall] && WallID.Sets.Conversion.NewWall2[newTile.Wall] ||
WallID.Sets.Conversion.NewWall3[tile.wall] && WallID.Sets.Conversion.NewWall3[newTile.Wall] ||
WallID.Sets.Conversion.NewWall4[tile.wall] && WallID.Sets.Conversion.NewWall4[newTile.Wall]
)
{
TShock.Log.ConsoleDebug("Bouncer / SendTileSquare processing a conversion update - [{0}|{1}] -> [{2}|{3}]", tile.type, tile.wall, newTile.Type, newTile.Wall);
UpdateServerTileState(tile, newTile);
}
}
/// <summary>
/// Updates a single tile's world state with a change from the tile square packet
/// </summary>
/// <param name="tile">The tile to update</param>
/// <param name="newTile">The NetTile containing the change</param>
public static void UpdateServerTileState(ITile tile, NetTile newTile)
{
tile.active(newTile.Active);
tile.type = newTile.Type;
if (newTile.FrameImportant)
{
tile.frameX = newTile.FrameX;
tile.frameY = newTile.FrameY;
}
if (newTile.HasWall)
{
tile.wall = newTile.Wall;
}
if (newTile.HasLiquid)
{
tile.liquid = newTile.Liquid;
tile.liquidType(newTile.LiquidType);
}
tile.wire(newTile.Wire);
tile.wire2(newTile.Wire2);
tile.wire3(newTile.Wire3);
tile.wire4(newTile.Wire4);
tile.halfBrick(newTile.IsHalf);
if (newTile.HasColor)
{
tile.color(newTile.TileColor);
}
if (newTile.HasWallColor)
{
tile.wallColor(newTile.WallColor);
}
byte slope = 0;
if (newTile.Slope)
{
slope += 1;
}
if (newTile.Slope2)
{
slope += 2;
}
if (newTile.Slope3)
{
slope += 4;
}
tile.slope(slope);
TShock.Log.ConsoleDebug("Bouncer / SendTileSquare updated a tile from type {0} to {1}", tile.type, newTile.Type);
}
/// <summary>
/// Performs <see cref="UpdateServerTileState(ITile, NetTile)"/> on multiple tiles
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <param name="newTiles"></param>
public static void UpdateMultipleServerTileStates(int x, int y, int width, int height, NetTile[,] newTiles)
{
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
UpdateServerTileState(Main.tile[x + i, y + j], newTiles[i, j]);
}
}
}
/// <summary>
/// Reads a set of NetTiles from a memory stream
/// </summary>
/// <param name="stream"></param>
/// <param name="size"></param>
/// <returns></returns>
static NetTile[,] ReadNetTilesFromStream(System.IO.MemoryStream stream, int size)
{
NetTile[,] tiles = new NetTile[size, size];
for (int x = 0; x < size; x++)
{
for (int y = 0; y < size; y++)
{
tiles[x, y] = new NetTile(stream);
}
}
return tiles;
}
/// <summary>
/// Determines whether or not the tile square should be immediately accepted or rejected
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
static bool ShouldSkipProcessing(GetDataHandlers.SendTileSquareEventArgs args)
{
if (args.Player.HasPermission(Permissions.allowclientsideworldedit))
{
TShock.Log.ConsoleDebug("Bouncer / SendTileSquare accepted clientside world edit from {0}", args.Player.Name);
args.Handled = false;
return true;
}
// 5x5 is the largest vanilla-sized tile square. Anything larger than this should not be seen in the vanilla game and should be rejected
if (args.Size > 5)
{
TShock.Log.ConsoleDebug("Bouncer / SendTileSquare rejected from non-vanilla tilemod from {0}", args.Player.Name);
return true;
}
if (args.Player.IsBouncerThrottled())
{
TShock.Log.ConsoleDebug("Bouncer / SendTileSquare rejected from throttle from {0}", args.Player.Name);
args.Player.SendTileSquare(args.TileX, args.TileY, args.Size);
return true;
}
if (args.Player.IsBeingDisabled())
{
TShock.Log.ConsoleDebug("Bouncer / SendTileSquare rejected from being disabled from {0}", args.Player.Name);
args.Player.SendTileSquare(args.TileX, args.TileY, args.Size);
return true;
}
return false;
}
class Debug
{
/// <summary>
/// Displays the difference in IDs between existing tiles and a set of NetTiles to the console
/// </summary>
/// <param name="tileX">X position at the top left of the square</param>
/// <param name="tileY">Y position at the top left of the square</param>
/// <param name="width">Width of the NetTile set</param>
/// <param name="height">Height of the NetTile set</param>
/// <param name="newTiles">New tiles to be visualised</param>
public static void VisualiseTileSetDiff(int tileX, int tileY, int width, int height, NetTile[,] newTiles)
{
if (TShock.Config.DebugLogs)
{
char pad = '0';
for (int y = 0; y < height; y++)
{
int realY = y + tileY;
for (int x = 0; x < width; x++)
{
int realX = x + tileX;
ushort type = Main.tile[realX, realY].type;
string type2 = type.ToString();
Console.Write((type2.ToString()).PadLeft(3, pad) + (Main.tile[realX, realY].active() ? "a" : "-") + " ");
}
Console.Write(" -> ");
for (int x = 0; x < width; x++)
{
int realX = x + tileX;
ushort type = newTiles[x, y].Type;
string type2 = type.ToString();
Console.Write((type2.ToString()).PadLeft(3, pad) + (newTiles[x, y].Active ? "a" : "-") + " ");
}
Console.Write("\n");
}
}
}
/// <summary>
/// Sends a tile square at the given (tileX, tileY) coordinate, using the given set of NetTiles information to update the tile square
/// </summary>
/// <param name="tileX">X position at the top left of the square</param>
/// <param name="tileY">Y position at the top left of the square</param>
/// <param name="width">Width of the NetTile set</param>
/// <param name="height">Height of the NetTile set</param>
/// <param name="newTiles">New tiles to place in the square</param>
/// <param name="player">Player to send the debug display to</param>
public static void DisplayTileSetInGame(int tileX, int tileY, int width, int height, NetTile[,] newTiles, TSPlayer player)
{
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
UpdateServerTileState(Main.tile[tileX + x, tileY + y], newTiles[x, y]);
}
//Add a line of dirt blocks at the bottom for safety
UpdateServerTileState(Main.tile[tileX + x, tileY + height], new NetTile { Active = true, Type = 0 });
}
player.SendTileSquare(tileX, tileY, Math.Max(width, height) + 1);
}
}
}
}

View file

@ -36,6 +36,7 @@ namespace TShockAPI.Net
public bool Wire { get; set; } public bool Wire { get; set; }
public bool Wire2 { get; set; } public bool Wire2 { get; set; }
public bool Wire3 { get; set; } public bool Wire3 { get; set; }
public bool Wire4 { get; set; }
public byte HalfBrick { get; set; } public byte HalfBrick { get; set; }
public byte Actuator { get; set; } public byte Actuator { get; set; }
public bool Inactive { get; set; } public bool Inactive { get; set; }
@ -83,6 +84,7 @@ namespace TShockAPI.Net
Wire = false; Wire = false;
Wire2 = false; Wire2 = false;
Wire3 = false; Wire3 = false;
Wire4 = false;
HalfBrick = 0; HalfBrick = 0;
Actuator = 0; Actuator = 0;
Inactive = false; Inactive = false;
@ -151,8 +153,10 @@ namespace TShockAPI.Net
if (Slope3) if (Slope3)
bits[6] = true; bits[6] = true;
if (Wire4)
bits[7] = true;
stream.WriteInt8((byte)bits); stream.WriteByte(bits);
if (HasColor) if (HasColor)
{ {
@ -175,7 +179,7 @@ namespace TShockAPI.Net
} }
if (HasWall) if (HasWall)
stream.WriteInt16((short)Wall);; stream.WriteInt16((short)Wall);
if (HasLiquid) if (HasLiquid)
{ {
@ -194,6 +198,7 @@ namespace TShockAPI.Net
Slope = flags2[4]; Slope = flags2[4];
Slope2 = flags2[5]; Slope2 = flags2[5];
Slope3 = flags2[6]; Slope3 = flags2[6];
Wire4 = flags2[7];
if (flags2[2]) if (flags2[2])
{ {

View file

@ -496,7 +496,7 @@ namespace TShockAPI
} }
var response = NetCreativeUnlocksModule.SerializeItemSacrifice(i, amount); var response = NetCreativeUnlocksModule.SerializeItemSacrifice(i, amount);
NetManager.Instance.SendToClient(response, player.TPlayer.whoAmI); NetManager.Instance.SendToClient(response, player.Index);
} }
} }
} }

View file

@ -620,7 +620,8 @@ namespace TShockAPI
/// <summary>Determines if the player can build on a given point.</summary> /// <summary>Determines if the player can build on a given point.</summary>
/// <param name="x">The x coordinate they want to build at.</param> /// <param name="x">The x coordinate they want to build at.</param>
/// <param name="y">The y coordinate they want to paint at.</param> /// <param name="y">The y coordinate they want to build at.</param>
/// <param name="shouldWarnPlayer">Whether or not the player should be warned if their build attempt fails</param>
/// <returns>True if the player can build at the given point from build, spawn, and region protection.</returns> /// <returns>True if the player can build at the given point from build, spawn, and region protection.</returns>
public bool HasBuildPermission(int x, int y, bool shouldWarnPlayer = true) public bool HasBuildPermission(int x, int y, bool shouldWarnPlayer = true)
{ {
@ -679,6 +680,32 @@ namespace TShockAPI
return false; return false;
} }
/// <summary>
/// Determines if the player can build a multi-block tile object on a given point.
/// Tile objects include things like Doors, Trap Doors, Item Frames, Beds, and Dressers.
/// </summary>
/// <param name="x">The x coordinate they want to build at.</param>
/// <param name="y">The y coordinate they want to build at.</param>
/// <param name="width">The width of the tile object</param>
/// <param name="height">The height of the tile object</param>
/// <param name="shouldWarnPlayer">Whether or not the player should be warned if their build attempt fails</param>
/// <returns>True if the player can build at the given point from build, spawn, and region protection.</returns>
public bool HasBuildPermissionForTileObject(int x, int y, int width, int height, bool shouldWarnPlayer = true)
{
for (int realx = x; realx < x + width; realx++)
{
for (int realy = y; realy < y + height; realy++)
{
if (!HasBuildPermission(realx, realy, shouldWarnPlayer))
{
return false;
}
}
}
return true;
}
/// <summary>Determines if the player can paint on a given point. Checks general build permissions, then paint.</summary> /// <summary>Determines if the player can paint on a given point. Checks general build permissions, then paint.</summary>
/// <param name="x">The x coordinate they want to paint at.</param> /// <param name="x">The x coordinate they want to paint at.</param>
/// <param name="y">The y coordinate they want to paint at.</param> /// <param name="y">The y coordinate they want to paint at.</param>
@ -1241,50 +1268,14 @@ namespace TShockAPI
/// <param name="x">The x coordinate to send.</param> /// <param name="x">The x coordinate to send.</param>
/// <param name="y">The y coordinate to send.</param> /// <param name="y">The y coordinate to send.</param>
/// <param name="size">The size square set of tiles to send.</param> /// <param name="size">The size square set of tiles to send.</param>
/// <returns>Status if the tile square was sent successfully (i.e. no exceptions).</returns> /// <returns>true if the tile square was sent successfully, else false</returns>
public virtual bool SendTileSquare(int x, int y, int size = 10) public virtual bool SendTileSquare(int x, int y, int size = 10)
{ {
try try
{ {
int num = (size - 1) / 2; SendData(PacketTypes.TileSendSquare, "", size, x, y);
int m_x = 0;
int m_y = 0;
if (x - num < 0)
{
m_x = 0;
}
else
{
m_x = x - num;
}
if (y - num < 0)
{
m_y = 0;
}
else
{
m_y = y - num;
}
if (m_x + size > Main.maxTilesX)
{
m_x = Main.maxTilesX - size;
}
if (m_y + size > Main.maxTilesY)
{
m_y = Main.maxTilesY - size;
}
SendData(PacketTypes.TileSendSquare, "", size, m_x, m_y);
return true; return true;
} }
catch (IndexOutOfRangeException)
{
// This is expected if square exceeds array.
}
catch (Exception ex) catch (Exception ex)
{ {
TShock.Log.Error(ex.ToString()); TShock.Log.Error(ex.ToString());

View file

@ -1639,7 +1639,8 @@ namespace TShockAPI
if (e.number >= 0 && e.number < Main.projectile.Length) if (e.number >= 0 && e.number < Main.projectile.Length)
{ {
var projectile = Main.projectile[e.number]; var projectile = Main.projectile[e.number];
if (projectile.active && projectile.owner >= 0 && GetDataHandlers.projectileCreatesLiquid.ContainsKey(projectile.type)) if (projectile.active && projectile.owner >= 0 &&
(GetDataHandlers.projectileCreatesLiquid.ContainsKey(projectile.type) || GetDataHandlers.projectileCreatesTile.ContainsKey(projectile.type)))
{ {
var player = Players[projectile.owner]; var player = Players[projectile.owner];
if (player != null) if (player != null)

View file

@ -88,6 +88,7 @@
<Compile Include="DB\ResearchDatastore.cs" /> <Compile Include="DB\ResearchDatastore.cs" />
<Compile Include="DB\TileManager.cs" /> <Compile Include="DB\TileManager.cs" />
<Compile Include="Extensions\ExceptionExt.cs" /> <Compile Include="Extensions\ExceptionExt.cs" />
<Compile Include="Handlers\SendTileSquareHandler.cs" />
<Compile Include="Hooks\AccountHooks.cs" /> <Compile Include="Hooks\AccountHooks.cs" />
<Compile Include="Hooks\GeneralHooks.cs" /> <Compile Include="Hooks\GeneralHooks.cs" />
<Compile Include="Hooks\PlayerHooks.cs" /> <Compile Include="Hooks\PlayerHooks.cs" />
@ -211,7 +212,7 @@
</PropertyGroup> </PropertyGroup>
<ProjectExtensions> <ProjectExtensions>
<VisualStudio> <VisualStudio>
<UserProperties BuildVersion_UpdateAssemblyVersion="True" BuildVersion_UpdateFileVersion="True" BuildVersion_BuildAction="Both" BuildVersion_BuildVersioningStyle="None.None.None.MonthAndDayStamp" BuildVersion_StartDate="2011/6/17" BuildVersion_IncrementBeforeBuild="False" /> <UserProperties BuildVersion_IncrementBeforeBuild="False" BuildVersion_StartDate="2011/6/17" BuildVersion_BuildVersioningStyle="None.None.None.MonthAndDayStamp" BuildVersion_BuildAction="Both" BuildVersion_UpdateFileVersion="True" BuildVersion_UpdateAssemblyVersion="True" />
</VisualStudio> </VisualStudio>
</ProjectExtensions> </ProjectExtensions>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.