Merge branch 'general-devel' into tileconv

This commit is contained in:
Stargazing Koishi 2022-12-10 21:37:52 +00:00 committed by GitHub
commit 104598394b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 36285 additions and 21712 deletions

View file

@ -36,6 +36,11 @@ jobs:
- name: Install msgfmt
run: sudo apt-get install -y gettext
- name: Produce installer
run: |
cd TShockInstaller
dotnet publish -r ${{ matrix.arch }} -f net6.0 -c Release -p:PublishSingleFile=true --self-contained true
- name: Produce build
run: |
cd TShockLauncher
@ -46,6 +51,10 @@ jobs:
run: |
chmod +x TShockLauncher/bin/Release/net6.0/${{ matrix.arch }}/publish/TShock.Server
- name: Copy installer
run: |
cp TShockInstaller/bin/Release/net6.0/${{ matrix.arch }}/publish/* TShockLauncher/bin/Release/net6.0/${{ matrix.arch }}/publish/
# preserve file perms: https://github.com/actions/upload-artifact#maintaining-file-permissions-and-case-sensitive-files
- name: Tarball artifact (non-Windows)
if: ${{ matrix.arch != 'win-x64' }}

View file

@ -26,6 +26,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TShockLauncher", "TShockLau
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TShockLauncher.Tests", "TShockLauncher.Tests\TShockLauncher.Tests.csproj", "{90AB47F3-8220-48FC-BDAB-D6E97BFDA51B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TShockInstaller", "TShockInstaller\TShockInstaller.csproj", "{17AC4DD0-8334-4B5C-ABED-77EAF52D75FA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TShockPluginManager", "TShockPluginManager\TShockPluginManager.csproj", "{9FFABC7D-B042-4B58-98F5-7FA787B9A757}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -102,6 +106,38 @@ Global
{90AB47F3-8220-48FC-BDAB-D6E97BFDA51B}.Release|x64.Build.0 = Release|Any CPU
{90AB47F3-8220-48FC-BDAB-D6E97BFDA51B}.Release|x86.ActiveCfg = Release|Any CPU
{90AB47F3-8220-48FC-BDAB-D6E97BFDA51B}.Release|x86.Build.0 = Release|Any CPU
{17AC4DD0-8334-4B5C-ABED-77EAF52D75FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{17AC4DD0-8334-4B5C-ABED-77EAF52D75FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17AC4DD0-8334-4B5C-ABED-77EAF52D75FA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{17AC4DD0-8334-4B5C-ABED-77EAF52D75FA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{17AC4DD0-8334-4B5C-ABED-77EAF52D75FA}.Debug|x64.ActiveCfg = Debug|Any CPU
{17AC4DD0-8334-4B5C-ABED-77EAF52D75FA}.Debug|x64.Build.0 = Debug|Any CPU
{17AC4DD0-8334-4B5C-ABED-77EAF52D75FA}.Debug|x86.ActiveCfg = Debug|Any CPU
{17AC4DD0-8334-4B5C-ABED-77EAF52D75FA}.Debug|x86.Build.0 = Debug|Any CPU
{17AC4DD0-8334-4B5C-ABED-77EAF52D75FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17AC4DD0-8334-4B5C-ABED-77EAF52D75FA}.Release|Any CPU.Build.0 = Release|Any CPU
{17AC4DD0-8334-4B5C-ABED-77EAF52D75FA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{17AC4DD0-8334-4B5C-ABED-77EAF52D75FA}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{17AC4DD0-8334-4B5C-ABED-77EAF52D75FA}.Release|x64.ActiveCfg = Release|Any CPU
{17AC4DD0-8334-4B5C-ABED-77EAF52D75FA}.Release|x64.Build.0 = Release|Any CPU
{17AC4DD0-8334-4B5C-ABED-77EAF52D75FA}.Release|x86.ActiveCfg = Release|Any CPU
{17AC4DD0-8334-4B5C-ABED-77EAF52D75FA}.Release|x86.Build.0 = Release|Any CPU
{9FFABC7D-B042-4B58-98F5-7FA787B9A757}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9FFABC7D-B042-4B58-98F5-7FA787B9A757}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9FFABC7D-B042-4B58-98F5-7FA787B9A757}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{9FFABC7D-B042-4B58-98F5-7FA787B9A757}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{9FFABC7D-B042-4B58-98F5-7FA787B9A757}.Debug|x64.ActiveCfg = Debug|Any CPU
{9FFABC7D-B042-4B58-98F5-7FA787B9A757}.Debug|x64.Build.0 = Debug|Any CPU
{9FFABC7D-B042-4B58-98F5-7FA787B9A757}.Debug|x86.ActiveCfg = Debug|Any CPU
{9FFABC7D-B042-4B58-98F5-7FA787B9A757}.Debug|x86.Build.0 = Debug|Any CPU
{9FFABC7D-B042-4B58-98F5-7FA787B9A757}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9FFABC7D-B042-4B58-98F5-7FA787B9A757}.Release|Any CPU.Build.0 = Release|Any CPU
{9FFABC7D-B042-4B58-98F5-7FA787B9A757}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{9FFABC7D-B042-4B58-98F5-7FA787B9A757}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{9FFABC7D-B042-4B58-98F5-7FA787B9A757}.Release|x64.ActiveCfg = Release|Any CPU
{9FFABC7D-B042-4B58-98F5-7FA787B9A757}.Release|x64.Build.0 = Release|Any CPU
{9FFABC7D-B042-4B58-98F5-7FA787B9A757}.Release|x86.ActiveCfg = Release|Any CPU
{9FFABC7D-B042-4B58-98F5-7FA787B9A757}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -400,7 +400,7 @@ namespace TShockAPI
};
PlayerAddBuffWhitelist[BuffID.OnFire3] = new BuffLimit
{
MaxTicks = 60 * 5,
MaxTicks = 60 * 6,
CanBeAddedWithoutHostile = false,
CanOnlyBeAppliedToSender = false
};
@ -416,6 +416,24 @@ namespace TShockAPI
CanBeAddedWithoutHostile = false,
CanOnlyBeAppliedToSender = false
};
PlayerAddBuffWhitelist[BuffID.ShadowCandle] = new BuffLimit
{
MaxTicks = 2,
CanBeAddedWithoutHostile = true,
CanOnlyBeAppliedToSender = true
};
PlayerAddBuffWhitelist[BuffID.BrainOfConfusionBuff] = new BuffLimit
{
MaxTicks = 240,
CanBeAddedWithoutHostile = true,
CanOnlyBeAppliedToSender = true
};
PlayerAddBuffWhitelist[BuffID.WindPushed] = new BuffLimit
{
MaxTicks = 2,
CanBeAddedWithoutHostile = true,
CanOnlyBeAppliedToSender = true
};
#endregion Whitelist
}
@ -674,12 +692,10 @@ namespace TShockAPI
}
}
if (action == EditAction.KillTile && !Main.tileCut[tile.type] && !breakableTiles.Contains(tile.type))
if (action == EditAction.KillTile && !Main.tileCut[tile.type] && !breakableTiles.Contains(tile.type) && args.Player.RecentFuse == 0)
{
// TPlayer.mount.Type 8 => Drill Containment Unit.
// If the tile is an axe tile and they aren't selecting an axe, they're hacking.
if (Main.tileAxe[tile.type] && ((args.Player.TPlayer.mount.Type != 8 && selectedItem.axe == 0) && !ItemID.Sets.Explosives[selectedItem.netID] && args.Player.RecentFuse == 0))
if (Main.tileAxe[tile.type] && ((args.Player.TPlayer.mount.Type != MountID.Drill && selectedItem.axe == 0) && !ItemID.Sets.Explosives[selectedItem.netID]))
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnTileEdit rejected from (axe) {0} {1} {2}", args.Player.Name, action, editData));
args.Player.SendTileSquareCentered(tileX, tileY, 4);
@ -687,7 +703,7 @@ namespace TShockAPI
return;
}
// If the tile is a hammer tile and they aren't selecting a hammer, they're hacking.
else if (Main.tileHammer[tile.type] && ((args.Player.TPlayer.mount.Type != 8 && selectedItem.hammer == 0) && !ItemID.Sets.Explosives[selectedItem.netID] && args.Player.RecentFuse == 0))
else if (Main.tileHammer[tile.type] && ((args.Player.TPlayer.mount.Type != MountID.Drill && selectedItem.hammer == 0) && !ItemID.Sets.Explosives[selectedItem.netID]))
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnTileEdit rejected from (hammer) {0} {1} {2}", args.Player.Name, action, editData));
args.Player.SendTileSquareCentered(tileX, tileY, 4);
@ -699,11 +715,12 @@ namespace TShockAPI
// also add an exception for snake coils, they can be removed when the player places a new one or after x amount of time
// If the tile is part of the breakable when placing set, it might be getting broken by a placement.
else if (tile.type != TileID.ItemFrame && tile.type != TileID.MysticSnakeRope
&& !Main.tileAxe[tile.type] && !Main.tileHammer[tile.type] && tile.wall == 0 &&
args.Player.TPlayer.mount.Type != MountID.Drill && selectedItem.pick == 0 &&
selectedItem.type != ItemID.GravediggerShovel &&
!ItemID.Sets.Explosives[selectedItem.netID] && args.Player.RecentFuse == 0
&& !TileID.Sets.BreakableWhenPlacing[tile.type])
&& !ItemID.Sets.Explosives[selectedItem.netID]
&& !TileID.Sets.BreakableWhenPlacing[tile.type]
&& !Main.tileAxe[tile.type] && !Main.tileHammer[tile.type] && tile.wall == 0
&& selectedItem.pick == 0 && selectedItem.type != ItemID.GravediggerShovel
&& args.Player.TPlayer.mount.Type != MountID.Drill
&& args.Player.TPlayer.mount.Type != MountID.DiggingMoleMinecart)
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnTileEdit rejected from (pick) {0} {1} {2}", args.Player.Name, action,
editData));
@ -764,7 +781,7 @@ namespace TShockAPI
if ((action == EditAction.PlaceTile || action == EditAction.ReplaceTile) && editData != selectedItem.createTile)
{
/// These would get caught up in the below check because Terraria does not set their createTile field.
if (selectedItem.netID != ItemID.IceRod && selectedItem.netID != ItemID.DirtBomb && selectedItem.netID != ItemID.StickyBomb)
if (selectedItem.netID != ItemID.IceRod && selectedItem.netID != ItemID.DirtBomb && selectedItem.netID != ItemID.StickyBomb && (args.Player.TPlayer.mount.Type != MountID.DiggingMoleMinecart || editData != TileID.MinecartTrack))
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnTileEdit rejected from tile placement not matching selected item createTile {0} {1} {2} selectedItemID:{3} createTile:{4}", args.Player.Name, action, editData, selectedItem.netID, selectedItem.createTile));
args.Player.SendTileSquareCentered(tileX, tileY, 4);
@ -1333,6 +1350,7 @@ namespace TShockAPI
|| type == ProjectileID.Dynamite
|| type == ProjectileID.StickyBomb
|| type == ProjectileID.StickyDynamite
|| type == ProjectileID.BombFish
|| type == ProjectileID.ScarabBomb
|| type == ProjectileID.DirtBomb))
{
@ -1704,132 +1722,108 @@ namespace TShockAPI
// Liquid anti-cheat
// Arguably the banned buckets bit should be in the item bans system
if (amount != 0)
if (amount != 0 && !wasThereABombNearby)
{
int bucket = -1;
int selectedItemType = args.Player.TPlayer.inventory[args.Player.TPlayer.selectedItem].type;
if (selectedItemType == ItemID.EmptyBucket)
{
bucket = 0;
}
else if (selectedItemType == ItemID.WaterBucket)
{
bucket = 1;
}
else if (selectedItemType == ItemID.LavaBucket)
{
bucket = 2;
}
else if (selectedItemType == ItemID.HoneyBucket)
{
bucket = 3;
}
else if (selectedItemType == ItemID.BottomlessBucket ||
selectedItemType == ItemID.SuperAbsorbantSponge)
{
bucket = 4;
}
else if (selectedItemType == ItemID.LavaAbsorbantSponge)
{
bucket = 5;
}
else if (selectedItemType == ItemID.BottomlessLavaBucket)
{
bucket = 6;
}
else if (selectedItemType == ItemID.BottomlessHoneyBucket
|| selectedItemType == ItemID.HoneyAbsorbantSponge)
{
bucket = 7;
}
else if (selectedItemType == ItemID.BottomlessShimmerBucket)
{
bucket = 8;
}
else if (selectedItemType == ItemID.UltraAbsorbantSponge)
{
bucket = 9;
}
if (!wasThereABombNearby && type == LiquidType.Lava && !(bucket == 2 || bucket == 0 || bucket == 5 || bucket == 6 || bucket == 9))
void Reject(string reason)
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnLiquidSet rejected bucket check 1 from {0}", args.Player.Name));
TShock.Log.ConsoleDebug(GetString("Bouncer / OnLiquidSet rejected liquid type {0} from {1} holding {2}", type, args.Player.Name, selectedItemType));
args.Player.SendErrorMessage(GetString("You do not have permission to perform this action."));
args.Player.Disable(GetString("Spreading lava without holding a lava bucket"), DisableFlags.WriteToLogAndConsole);
args.Player.Disable(reason, DisableFlags.WriteToLogAndConsole);
args.Player.SendTileSquareCentered(tileX, tileY, 1);
args.Handled = true;
}
if (TShock.ItemBans.DataModel.ItemIsBanned(EnglishLanguage.GetItemNameById(selectedItemType), args.Player))
{
Reject(GetString("Using banned {0} to manipulate liquid", Lang.GetItemNameValue(selectedItemType)));
return;
}
if (!wasThereABombNearby && type == LiquidType.Lava && TShock.ItemBans.DataModel.ItemIsBanned("Lava Bucket", args.Player))
switch (type)
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnLiquidSet rejected lava bucket from {0}", args.Player.Name));
args.Player.SendErrorMessage(GetString("You do not have permission to perform this action."));
args.Player.Disable(GetString("Using banned lava bucket without permissions"), DisableFlags.WriteToLogAndConsole);
args.Player.SendTileSquareCentered(tileX, tileY, 1);
args.Handled = true;
return;
case LiquidType.Water:
if (TShock.ItemBans.DataModel.ItemIsBanned(EnglishLanguage.GetItemNameById(ItemID.WaterBucket), args.Player))
{
Reject(GetString("Using banned water bucket without permissions"));
return;
}
break;
case LiquidType.Lava:
if (TShock.ItemBans.DataModel.ItemIsBanned(EnglishLanguage.GetItemNameById(ItemID.LavaBucket), args.Player))
{
Reject(GetString("Using banned lava bucket without permissions"));
return;
}
break;
case LiquidType.Honey:
if (TShock.ItemBans.DataModel.ItemIsBanned(EnglishLanguage.GetItemNameById(ItemID.HoneyBucket), args.Player))
{
Reject(GetString("Using banned honey bucket without permissions"));
return;
}
break;
case LiquidType.Shimmer:
if (TShock.ItemBans.DataModel.ItemIsBanned(EnglishLanguage.GetItemNameById(ItemID.BottomlessShimmerBucket), args.Player))
{
Reject(GetString("Using banned shimmering water bucket without permissions"));
return;
}
break;
default:
Reject(GetString("Manipulating unknown liquid type"));
return;
}
if (!wasThereABombNearby && type == LiquidType.Water && !(bucket == 1 || bucket == 0 || bucket == 4 || bucket == 9))
switch (selectedItemType)
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnLiquidSet rejected bucket check 2 from {0}", args.Player.Name));
args.Player.SendErrorMessage(GetString("You do not have permission to perform this action."));
args.Player.Disable(GetString("Spreading water without holding a water bucket"), DisableFlags.WriteToLogAndConsole);
args.Player.SendTileSquareCentered(tileX, tileY, 1);
args.Handled = true;
return;
}
if (!wasThereABombNearby && type == LiquidType.Water && TShock.ItemBans.DataModel.ItemIsBanned("Water Bucket", args.Player))
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnLiquidSet rejected bucket check 3 from {0}", args.Player.Name));
args.Player.SendErrorMessage(GetString("You do not have permission to perform this action."));
args.Player.Disable(GetString("Using banned water bucket without permissions"), DisableFlags.WriteToLogAndConsole);
args.Player.SendTileSquareCentered(tileX, tileY, 1);
args.Handled = true;
return;
}
if (!wasThereABombNearby && type == LiquidType.Honey && !(bucket == 3 || bucket == 0 || bucket == 7 || bucket == 9))
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnLiquidSet rejected bucket check 4 from {0}", args.Player.Name));
args.Player.SendErrorMessage(GetString("You do not have permission to perform this action."));
args.Player.Disable(GetString("Spreading honey without holding a honey bucket"), DisableFlags.WriteToLogAndConsole);
args.Player.SendTileSquareCentered(tileX, tileY, 1);
args.Handled = true;
return;
}
if (!wasThereABombNearby && type == LiquidType.Honey && TShock.ItemBans.DataModel.ItemIsBanned("Honey Bucket", args.Player))
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnLiquidSet rejected bucket check 5 from {0}", args.Player.Name));
args.Player.SendErrorMessage(GetString("You do not have permission to perform this action."));
args.Player.Disable(GetString("Using banned honey bucket without permissions"), DisableFlags.WriteToLogAndConsole);
args.Player.SendTileSquareCentered(tileX, tileY, 1);
args.Handled = true;
return;
}
if (!wasThereABombNearby && type == LiquidType.Shimmer && !(bucket == 8 || bucket == 9))
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnLiquidSet rejected bucket check 6 from {0}", args.Player.Name));
args.Player.SendErrorMessage(GetString("You do not have permission to perform this action."));
args.Player.Disable(GetString("Spreading shimmer without holding a shimmer bucket"), DisableFlags.WriteToLogAndConsole);
args.Player.SendTileSquareCentered(tileX, tileY, 1);
args.Handled = true;
return;
}
if (!wasThereABombNearby && type == LiquidType.Shimmer &&
TShock.ItemBans.DataModel.ItemIsBanned("Bottomless Shimmer Bucket", args.Player))
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnLiquidSet rejected bucket check 7 from {0}", args.Player.Name));
args.Player.SendErrorMessage(GetString("You do not have permission to perform this action."));
args.Player.Disable(GetString("Using banned bottomless shimmer bucket without permissions"), DisableFlags.WriteToLogAndConsole);
args.Player.SendTileSquareCentered(tileX, tileY, 1);
args.Handled = true;
return;
case ItemID.WaterBucket:
case ItemID.BottomlessBucket:
if (type != LiquidType.Water)
{
Reject(GetString("Using {0} on non-water", Lang.GetItemNameValue(selectedItemType)));
return;
}
break;
case ItemID.HoneyBucket:
case ItemID.HoneyAbsorbantSponge:
case ItemID.BottomlessHoneyBucket:
if (type != LiquidType.Honey)
{
Reject(GetString("Using {0} on non-honey", Lang.GetItemNameValue(selectedItemType)));
return;
}
break;
case ItemID.LavaAbsorbantSponge:
case ItemID.BottomlessLavaBucket:
case ItemID.LavaBucket:
if (type != LiquidType.Lava)
{
Reject(GetString("Using {0} on non-lava", Lang.GetItemNameValue(selectedItemType)));
return;
}
break;
case ItemID.BottomlessShimmerBucket:
if (type != LiquidType.Shimmer)
{
Reject(GetString("Using {0} on non-shimmer", Lang.GetItemNameValue(selectedItemType)));
return;
}
break;
case ItemID.SuperAbsorbantSponge:
if (type != LiquidType.Water && type != LiquidType.Shimmer)
{
Reject(GetString("Using {0} on non-water or shimmer", Lang.GetItemNameValue(selectedItemType)));
return;
}
break;
case ItemID.EmptyBucket:
case ItemID.UltraAbsorbantSponge:
break;
default:
Reject(GetString("Using {0} to manipulate unknown liquid {1}", Lang.GetItemNameValue(selectedItemType), type));
return;
}
}
@ -1867,41 +1861,57 @@ namespace TShockAPI
int type = args.Type;
int time = args.Time;
void Reject(bool shouldResync = true)
{
args.Handled = true;
if (shouldResync)
args.Player.SendData(PacketTypes.PlayerBuff, number: id);
}
if (id >= Main.maxPlayers)
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnPlayerBuff rejected player cap from {0}", args.Player.Name));
args.Handled = true;
TShock.Log.ConsoleDebug(GetString(
"Bouncer / OnPlayerBuff rejected {0} ({1}) applying buff {2} to {3} for {4} ticks: target ID out of bounds",
args.Player.Name, args.Player.Index, type, id, time));
Reject(false);
return;
}
if (TShock.Players[id] == null)
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnPlayerBuff rejected null check from {0}", args.Player.Name));
args.Handled = true;
TShock.Log.ConsoleDebug(GetString(
"Bouncer / OnPlayerBuff rejected {0} ({1}) applying buff {2} to {3} for {4} ticks: target is null", args.Player.Name,
args.Player.Index, type, id, time));
Reject(false);
return;
}
if (type >= Terraria.ID.BuffID.Count)
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnPlayerBuff rejected invalid buff type {0}", args.Player.Name));
args.Player.SendData(PacketTypes.PlayerBuff, "", id);
args.Handled = true;
TShock.Log.ConsoleDebug(GetString(
"Bouncer / OnPlayerBuff rejected {0} ({1}) applying buff {2} to {3} for {4} ticks: invalid buff type", args.Player.Name,
args.Player.Index, type, id, time));
Reject(false);
return;
}
if (args.Player.IsBeingDisabled())
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnPlayerBuff rejected disabled from {0}", args.Player.Name));
args.Player.SendData(PacketTypes.PlayerBuff, "", id);
args.Handled = true;
TShock.Log.ConsoleDebug(GetString(
"Bouncer / OnPlayerBuff rejected {0} ({1}) applying buff {2} to {3} for {4} ticks: sender is being disabled",
args.Player.Name, args.Player.Index, type, id, time));
Reject();
return;
}
if (args.Player.IsBouncerThrottled())
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnPlayerBuff rejected throttled from {0}", args.Player.Name));
args.Player.SendData(PacketTypes.PlayerBuff, "", id);
args.Handled = true;
TShock.Log.ConsoleDebug(GetString(
"Bouncer / OnPlayerBuff rejected {0} ({1}) applying buff {2} to {3} for {4} ticks: sender is being throttled",
args.Player.Name, args.Player.Index, type, id, time));
Reject();
return;
}
@ -1910,41 +1920,46 @@ namespace TShockAPI
if (!args.Player.IsInRange(targetPlayer.TileX, targetPlayer.TileY, 50))
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnPlayerBuff rejected range check from {0}", args.Player.Name));
args.Player.SendData(PacketTypes.PlayerBuff, "", id);
args.Handled = true;
TShock.Log.ConsoleDebug(GetString(
"Bouncer / OnPlayerBuff rejected {0} ({1}) applying buff {2} to {3} for {4} ticks: sender is not in range of target",
args.Player.Name, args.Player.Index, type, id, time));
Reject();
return;
}
if (buffLimit == null)
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnPlayerBuff rejected non-whitelisted buff {0}", args.Player.Name));
args.Player.SendData(PacketTypes.PlayerBuff, "", id);
args.Handled = true;
TShock.Log.ConsoleDebug(GetString(
"Bouncer / OnPlayerBuff rejected {0} ({1}) applying buff {2} to {3} for {4} ticks: buff is not whitelisted",
args.Player.Name, args.Player.Index, type, id, time));
Reject();
return;
}
if (buffLimit.CanOnlyBeAppliedToSender && id != args.Player.Index)
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnPlayerBuff rejected applied to non-sender from {0}", args.Player.Name));
args.Player.SendData(PacketTypes.PlayerBuff, "", id);
args.Handled = true;
TShock.Log.ConsoleDebug(GetString(
"Bouncer / OnPlayerBuff rejected {0} ({1}) applying buff {2} to {3} for {4} ticks: buff cannot be applied to non-senders",
args.Player.Name, args.Player.Index, type, id, time));
Reject();
return;
}
if (!buffLimit.CanBeAddedWithoutHostile && !targetPlayer.TPlayer.hostile)
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnPlayerBuff rejected hostile/pvp from {0}", args.Player.Name));
args.Player.SendData(PacketTypes.PlayerBuff, "", id);
args.Handled = true;
TShock.Log.ConsoleDebug(GetString(
"Bouncer / OnPlayerBuff rejected {0} ({1}) applying buff {2} to {3} for {4} ticks: buff cannot be applied without pvp",
args.Player.Name, args.Player.Index, type, id, time));
Reject();
return;
}
if (time <= 0 || time > buffLimit.MaxTicks)
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnPlayerBuff rejected time too long from {0}", args.Player.Name));
args.Player.SendData(PacketTypes.PlayerBuff, "", id);
args.Handled = true;
TShock.Log.ConsoleDebug(GetString(
"Bouncer / OnPlayerBuff rejected {0} ({1}) applying buff {2} to {3} for {4} ticks: buff cannot be applied for that long",
args.Player.Name, args.Player.Index, type, id, time));
Reject();
return;
}
}

View file

@ -55,6 +55,10 @@ namespace TShockAPI.Configuration
[Description("Allows stacks in chests to go beyond the stack limit during world loading.")]
public bool IgnoreChestStacksOnLoad = false;
/// <summary>Allows changing of the default world tile provider.</summary>
[Description("Allows changing of the default world tile provider.")]
public string WorldTileProvider = "default";
#endregion
@ -109,8 +113,8 @@ namespace TShockAPI.Configuration
[Description("Enables never ending invasion events. You still need to start the event, such as with the /invade command.")]
public bool InfiniteInvasion;
/// <summary>Sets the PvP mode. Valid types are: "normal", "always", "disabled".</summary>
[Description("Sets the PvP mode. Valid types are: \"normal\", \"always\" and \"disabled\".")]
/// <summary>Sets the PvP mode. Valid types are: "normal", "always", "pvpwithnoteam", "disabled".</summary>
[Description("Sets the PvP mode. Valid types are: \"normal\", \"always\", \"pvpwithnoteam\" and \"disabled\".")]
public string PvPMode = "normal";
/// <summary>Prevents tiles from being placed within SpawnProtectionRadius of the default spawn.</summary>

View file

@ -856,6 +856,14 @@ namespace TShockAPI
/// </summary>
public int RespawnTimer { get; set; }
/// <summary>
/// Number Of Deaths PVE
/// </summary>
public int NumberOfDeathsPVE { get; set; }
/// <summary>
/// Number Of Deaths PVP
/// </summary>
public int NumberOfDeathsPVP { get; set; }
/// <summary>
/// Context of where the player is spawning from.
/// </summary>
public PlayerSpawnContext SpawnContext { get; set; }
@ -864,7 +872,7 @@ namespace TShockAPI
/// PlayerSpawn - When a player spawns
/// </summary>
public static HandlerList<SpawnEventArgs> PlayerSpawn = new HandlerList<SpawnEventArgs>();
private static bool OnPlayerSpawn(TSPlayer player, MemoryStream data, byte pid, int spawnX, int spawnY, int respawnTimer, PlayerSpawnContext spawnContext)
private static bool OnPlayerSpawn(TSPlayer player, MemoryStream data, byte pid, int spawnX, int spawnY, int respawnTimer, int numberOfDeathsPVE, int numberOfDeathsPVP, PlayerSpawnContext spawnContext)
{
if (PlayerSpawn == null)
return false;
@ -877,6 +885,8 @@ namespace TShockAPI
SpawnX = spawnX,
SpawnY = spawnY,
RespawnTimer = respawnTimer,
NumberOfDeathsPVE = numberOfDeathsPVE,
NumberOfDeathsPVP = numberOfDeathsPVP,
SpawnContext = spawnContext
};
PlayerSpawn.Invoke(null, args);
@ -1033,12 +1043,16 @@ namespace TShockAPI
/// 0 = Old One's Army, 1 = Granite, 2 = Marble, 3 = Hive, 4 = Gem Cave, 5 = Lihzhard Temple, 6 = Graveyard
/// </summary>
public BitsByte Zone4 { get; set; }
/// <summary>
/// 0 = The Aether
/// </summary>
public BitsByte Zone5 { get; set; }
}
/// <summary>
/// PlayerZone - When the player sends it's zone/biome information to the server
/// </summary>
public static HandlerList<PlayerZoneEventArgs> PlayerZone = new HandlerList<PlayerZoneEventArgs>();
private static bool OnPlayerZone(TSPlayer player, MemoryStream data, byte plr, BitsByte zone1, BitsByte zone2, BitsByte zone3, BitsByte zone4)
private static bool OnPlayerZone(TSPlayer player, MemoryStream data, byte plr, BitsByte zone1, BitsByte zone2, BitsByte zone3, BitsByte zone4, BitsByte zone5)
{
if (PlayerZone == null)
return false;
@ -1051,7 +1065,8 @@ namespace TShockAPI
Zone1 = zone1,
Zone2 = zone2,
Zone3 = zone3,
Zone4 = zone4
Zone4 = zone4,
Zone5 = zone5
};
PlayerZone.Invoke(null, args);
return args.Handled;
@ -1519,12 +1534,16 @@ namespace TShockAPI
/// Type
/// </summary>
public byte type { get; set; }
/// <summary>
/// Paint Coat Tile
/// </summary>
public byte coatTile { get; set; }
}
/// <summary>
/// NPCStrike - Called when an NPC is attacked
/// </summary>
public static HandlerList<PaintTileEventArgs> PaintTile = new HandlerList<PaintTileEventArgs>();
private static bool OnPaintTile(TSPlayer player, MemoryStream data, Int32 x, Int32 y, byte t)
private static bool OnPaintTile(TSPlayer player, MemoryStream data, Int32 x, Int32 y, byte t, byte ct)
{
if (PaintTile == null)
return false;
@ -1535,7 +1554,8 @@ namespace TShockAPI
Data = data,
X = x,
Y = y,
type = t
type = t,
coatTile = ct
};
PaintTile.Invoke(null, args);
return args.Handled;
@ -1558,12 +1578,16 @@ namespace TShockAPI
/// Type
/// </summary>
public byte type { get; set; }
/// <summary>
/// Paint Coat Wall
/// </summary>
public byte coatWall { get; set; }
}
/// <summary>
/// Called When a wall is painted
/// </summary>
public static HandlerList<PaintWallEventArgs> PaintWall = new HandlerList<PaintWallEventArgs>();
private static bool OnPaintWall(TSPlayer player, MemoryStream data, Int32 x, Int32 y, byte t)
private static bool OnPaintWall(TSPlayer player, MemoryStream data, Int32 x, Int32 y, byte t, byte cw)
{
if (PaintWall == null)
return false;
@ -1574,7 +1598,8 @@ namespace TShockAPI
Data = data,
X = x,
Y = y,
type = t
type = t,
coatWall = cw
};
PaintWall.Invoke(null, args);
return args.Handled;
@ -1734,12 +1759,15 @@ namespace TShockAPI
/// <summary>Alternate variation of the object placed.</summary>
public byte Alternate { get; set; }
/// <summary>Related to Rubblemaker.</summary>
public sbyte Random { get; set; }
/// <summary>The direction the object was placed.</summary>
public bool Direction { get; set; }
}
/// <summary>Fired when an object is placed in the world.</summary>
public static HandlerList<PlaceObjectEventArgs> PlaceObject = new HandlerList<PlaceObjectEventArgs>();
private static bool OnPlaceObject(TSPlayer player, MemoryStream data, short x, short y, short type, short style, byte alternate, bool direction)
private static bool OnPlaceObject(TSPlayer player, MemoryStream data, short x, short y, short type, short style, byte alternate, sbyte random, bool direction)
{
if (PlaceObject == null)
return false;
@ -1753,6 +1781,7 @@ namespace TShockAPI
Type = type,
Style = style,
Alternate = alternate,
Random = random,
Direction = direction
};
@ -1980,6 +2009,10 @@ namespace TShockAPI
/// Is the damage critical?
/// </summary>
public bool Critical { get; set; }
/// <summary>
/// Cooldown Counter
/// </summary>
public sbyte CooldownCounter { get; set; }
/// <summary>The reason the player took damage and/or died.</summary>
public PlayerDeathReason PlayerDeathReason { get; set; }
}
@ -1987,7 +2020,7 @@ namespace TShockAPI
/// PlayerDamage - Called when a player is damaged
/// </summary>
public static HandlerList<PlayerDamageEventArgs> PlayerDamage = new HandlerList<PlayerDamageEventArgs>();
private static bool OnPlayerDamage(TSPlayer player, MemoryStream data, byte id, byte dir, short dmg, bool pvp, bool crit, PlayerDeathReason playerDeathReason)
private static bool OnPlayerDamage(TSPlayer player, MemoryStream data, byte id, byte dir, short dmg, bool pvp, bool crit, sbyte cooldownCounter, PlayerDeathReason playerDeathReason)
{
if (PlayerDamage == null)
return false;
@ -2001,6 +2034,7 @@ namespace TShockAPI
Damage = dmg,
PVP = pvp,
Critical = crit,
CooldownCounter = cooldownCounter,
PlayerDeathReason = playerDeathReason,
};
PlayerDamage.Invoke(null, args);
@ -2687,9 +2721,11 @@ namespace TShockAPI
short spawnx = args.Data.ReadInt16();
short spawny = args.Data.ReadInt16();
int respawnTimer = args.Data.ReadInt32();
short numberOfDeathsPVE = args.Data.ReadInt16();
short numberOfDeathsPVP = args.Data.ReadInt16();
PlayerSpawnContext context = (PlayerSpawnContext)args.Data.ReadByte();
if (OnPlayerSpawn(args.Player, args.Data, player, spawnx, spawny, respawnTimer, context))
if (OnPlayerSpawn(args.Player, args.Data, player, spawnx, spawny, respawnTimer, numberOfDeathsPVE, numberOfDeathsPVP, context))
return true;
if ((Main.ServerSideCharacter) && (spawnx == -1 && spawny == -1)) //this means they want to spawn to vanilla spawn
@ -2912,17 +2948,19 @@ namespace TShockAPI
Vector2 vel = args.Data.ReadVector2();
byte owner = args.Data.ReadInt8();
short type = args.Data.ReadInt16();
NewProjectileData bits = new NewProjectileData((BitsByte)args.Data.ReadByte());
BitsByte bitsByte = (BitsByte)args.Data.ReadByte();
BitsByte bitsByte2 = (BitsByte)(bitsByte[2] ? args.Data.ReadByte() : 0);
float[] ai = new float[Projectile.maxAI];
for (int i = 0; i < Projectile.maxAI; ++i)
ai[i] = !bits.AI[i] ? 0.0f : args.Data.ReadSingle();
ushort bannerId = bits.HasBannerIdToRespondTo ? args.Data.ReadUInt16() : (ushort)0;
short dmg = bits.HasDamage ? args.Data.ReadInt16() : (short)0;
float knockback = bits.HasKnockback ? args.Data.ReadSingle() : 0.0f;
short origDmg = bits.HasOriginalDamage ? args.Data.ReadInt16() : (short)0;
short projUUID = bits.HasUUUID ? args.Data.ReadInt16() : (short)-1;
if (projUUID >= 1000)
projUUID = -1;
for (int i = 0; i < Projectile.maxAI; ++i) ai[i] = 0f;
ai[0] = bitsByte[0] ? args.Data.ReadSingle() : 0f;
ai[1] = bitsByte[1] ? args.Data.ReadSingle() : 0f;
ushort bannerId = (ushort)(bitsByte[3] ? args.Data.ReadUInt16() : 0);
short dmg = (short)(bitsByte[4] ? args.Data.ReadInt16() : 0);
float knockback = bitsByte[5] ? args.Data.ReadSingle() : 0f;
short origDmg = (short)(bitsByte[6] ? args.Data.ReadInt16() : 0);
short projUUID = (short)(bitsByte[7] ? args.Data.ReadInt16() : -1);
if (projUUID >= 1000) projUUID = -1;
ai[2] = (bitsByte2[0] ? args.Data.ReadSingle() : 0f);
var index = TShock.Utils.SearchProjectile(ident, owner);
@ -3054,7 +3092,7 @@ namespace TShockAPI
}
string pvpMode = TShock.Config.Settings.PvPMode.ToLowerInvariant();
if (pvpMode == "disabled" || pvpMode == "always" || (DateTime.UtcNow - args.Player.LastPvPTeamChange).TotalSeconds < 5)
if (pvpMode == "disabled" || pvpMode == "always" || pvpMode == "pvpwithnoteam" || (DateTime.UtcNow - args.Player.LastPvPTeamChange).TotalSeconds < 5)
{
TShock.Log.ConsoleDebug(GetString("GetDataHandlers / HandleTogglePvp rejected fastswitch {0}", args.Player.Name));
args.Player.SendData(PacketTypes.TogglePvp, "", id);
@ -3150,8 +3188,9 @@ namespace TShockAPI
BitsByte zone2 = args.Data.ReadInt8();
BitsByte zone3 = args.Data.ReadInt8();
BitsByte zone4 = args.Data.ReadInt8();
BitsByte zone5 = args.Data.ReadInt8();
if (OnPlayerZone(args.Player, args.Data, plr, zone1, zone2, zone3, zone4))
if (OnPlayerZone(args.Player, args.Data, plr, zone1, zone2, zone3, zone4, zone5))
return true;
return false;
@ -3310,7 +3349,8 @@ namespace TShockAPI
if (id != args.Player.Index)
return true;
if ((DateTime.UtcNow - args.Player.LastPvPTeamChange).TotalSeconds < 5)
string pvpMode = TShock.Config.Settings.PvPMode.ToLowerInvariant();
if (pvpMode == "pvpwithnoteam" || (DateTime.UtcNow - args.Player.LastPvPTeamChange).TotalSeconds < 5)
{
args.Player.SendData(PacketTypes.PlayerTeam, "", id);
TShock.Log.ConsoleDebug(GetString("GetDataHandlers / HandlePlayerTeam rejected team fastswitch {0}", args.Player.Name));
@ -3616,13 +3656,14 @@ namespace TShockAPI
var x = args.Data.ReadInt16();
var y = args.Data.ReadInt16();
var t = args.Data.ReadInt8();
var ct = args.Data.ReadInt8();//PaintCoatTile
if (x < 0 || y < 0 || x >= Main.maxTilesX || y >= Main.maxTilesY || t > Main.numTileColors)
{
TShock.Log.ConsoleDebug(GetString("GetDataHandlers / HandlePaintTile rejected range check {0}", args.Player.Name));
return true;
}
if (OnPaintTile(args.Player, args.Data, x, y, t))
if (OnPaintTile(args.Player, args.Data, x, y, t, ct))
{
return true;
}
@ -3663,13 +3704,14 @@ namespace TShockAPI
var x = args.Data.ReadInt16();
var y = args.Data.ReadInt16();
var t = args.Data.ReadInt8();
var cw = args.Data.ReadInt8();//PaintCoatWall
if (x < 0 || y < 0 || x >= Main.maxTilesX || y >= Main.maxTilesY || t > Main.numTileColors)
{
TShock.Log.ConsoleDebug(GetString("GetDataHandlers / HandlePaintWall rejected range check {0}", args.Player.Name));
return true;
}
if (OnPaintWall(args.Player, args.Data, x, y, t))
if (OnPaintWall(args.Player, args.Data, x, y, t, cw))
{
return true;
}
@ -3928,9 +3970,10 @@ namespace TShockAPI
short type = args.Data.ReadInt16();
short style = args.Data.ReadInt16();
byte alternate = args.Data.ReadInt8();
sbyte random = (sbyte)args.Data.ReadInt8();
bool direction = args.Data.ReadBoolean();
if (OnPlaceObject(args.Player, args.Data, x, y, type, style, alternate, direction))
if (OnPlaceObject(args.Player, args.Data, x, y, type, style, alternate, random, direction))
return true;
return false;
@ -4068,7 +4111,7 @@ namespace TShockAPI
private static bool HandleNpcTeleportPortal(GetDataHandlerArgs args)
{
var npcIndex = args.Data.ReadByte();
var npcIndex = args.Data.ReadUInt16();
var portalColorIndex = args.Data.ReadInt16();
var newPosition = new Vector2(args.Data.ReadSingle(), args.Data.ReadSingle());
var velocity = new Vector2(args.Data.ReadSingle(), args.Data.ReadSingle());
@ -4162,8 +4205,9 @@ namespace TShockAPI
var bits = (BitsByte)(args.Data.ReadByte());
var crit = bits[0];
var pvp = bits[1];
var cooldownCounter = (sbyte)args.Data.ReadInt8();
if (OnPlayerDamage(args.Player, args.Data, id, direction, dmg, pvp, crit, playerDeathReason))
if (OnPlayerDamage(args.Player, args.Data, id, direction, dmg, pvp, crit, cooldownCounter, playerDeathReason))
return true;
return false;

View file

@ -33,6 +33,8 @@ namespace TShockAPI.Net
public short TileX { get; set; }
public short TileY { get; set; }
public int RespawnTimer { get; set; }
public short NumberOfDeathsPVE { get; set; }
public short NumberOfDeathsPVP { get; set; }
public PlayerSpawnContext PlayerSpawnContext { get; set; }
public override void Pack(Stream stream)
@ -41,6 +43,8 @@ namespace TShockAPI.Net
stream.WriteInt16(TileX);
stream.WriteInt16(TileY);
stream.WriteInt32(RespawnTimer);
stream.WriteInt16(NumberOfDeathsPVE);
stream.WriteInt16(NumberOfDeathsPVP);
stream.WriteByte((byte) PlayerSpawnContext);
}
}

View file

@ -1394,7 +1394,9 @@ namespace TShockAPI
/// <param name="tiley">The Y coordinate.</param>
/// <param name="context">The PlayerSpawnContext.</param>
/// <param name="respawnTimer">The respawn timer, will be Player.respawnTimer if parameter is null.</param>
public void Spawn(int tilex, int tiley, PlayerSpawnContext context, int? respawnTimer = null)
/// <param name="numberOfDeathsPVE">The number of deaths PVE, will be TPlayer.numberOfDeathsPVE if parameter is null.</param>
/// <param name="numberOfDeathsPVP">The number of deaths PVP, will be TPlayer.numberOfDeathsPVP if parameter is null.</param>
public void Spawn(int tilex, int tiley, PlayerSpawnContext context, int? respawnTimer = null, short? numberOfDeathsPVE = null, short? numberOfDeathsPVP = null)
{
using (var ms = new MemoryStream())
{
@ -1404,6 +1406,8 @@ namespace TShockAPI
TileX = (short)tilex,
TileY = (short)tiley,
RespawnTimer = respawnTimer ?? TShock.Players[Index].RespawnTimer * 60,
NumberOfDeathsPVE = numberOfDeathsPVE ?? (short)TPlayer.numberOfDeathsPVE,
NumberOfDeathsPVP = numberOfDeathsPVP ?? (short)TPlayer.numberOfDeathsPVP,
PlayerSpawnContext = context,
};
msg.PackFull(ms);

View file

@ -45,6 +45,10 @@ using TShockAPI.Localization;
using TShockAPI.Configuration;
using Terraria.GameContent.Creative;
using System.Runtime.InteropServices;
using MonoMod.Cil;
using Terraria.Achievements;
using Terraria.Initializers;
using Terraria.UI.Chat;
using TShockAPI.Modules;
namespace TShockAPI
@ -383,6 +387,19 @@ namespace TShockAPI
if (Config.Settings.EnableGeoIP && File.Exists(geoippath))
Geo = new GeoIPCountry(geoippath);
// check if a custom tile provider is to be used
switch(Config.Settings.WorldTileProvider?.ToLower())
{
case "heaptile":
Log.ConsoleInfo(GetString($"Using {nameof(HeapTile)} for tile implementation"), TraceLevel.Info);
Main.tile = new TileProvider();
break;
case "constileation":
Log.ConsoleInfo(GetString($"Using {nameof(ConstileationProvider)} for tile implementation"), TraceLevel.Info);
Main.tile = new ConstileationProvider();
break;
}
Log.ConsoleInfo(GetString("TShock {0} ({1}) now running.", Version, VersionCodename));
ServerApi.Hooks.GamePostInitialize.Register(this, OnPostInit);
@ -416,6 +433,33 @@ namespace TShockAPI
EnglishLanguage.Initialize();
// The AchievementTagHandler expects Main.Achievements to be non-null, which is not normally the case on dedicated servers.
// When trying to parse an achievement chat tag, it will instead throw.
// The tag is parsed when calling ChatManager.ParseMessage, which is used in TShock when writing chat messages to the
// console. Our OnChat handler uses Utils.Broadcast, which will send the message to all connected clients, write the message
// to the console and the log. Due to the order of execution, the message ends up being sent to all connected clients, but
// throws whilst trying to write to the console, and never gets written to the log.
// To solve the issue, we make achievements available on the server, allowing the tag handler to work as expected, and
// even allowing the localization of achievement names to appear in the console.
if (Game != null)
{
// Initialize the AchievementManager, which is normally only done on clients.
Game._achievements = new AchievementManager();
IL.Terraria.Initializers.AchievementInitializer.Load += OnAchievementInitializerLoad;
// Actually call AchievementInitializer.Load, which is also normally only done on clients.
AchievementInitializer.Load();
}
else
{
// If we don't have a Game instance, then we'll just remove the achievement tag handler entirely. This will cause the
// raw tag to just be used instead (and not be localized), but still avoid all the issues outlined above.
ChatManager._handlers.Remove("a", out _);
ChatManager._handlers.Remove("achievement", out _);
}
ModuleManager.Initialise(new object[] { this });
if (Config.Settings.RestApiEnabled)
@ -452,6 +496,13 @@ namespace TShockAPI
}
}
private static void OnAchievementInitializerLoad(ILContext il)
{
// Modify AchievementInitializer.Load to remove the Main.netMode == 2 check (occupies the first 4 IL instructions)
for (var i = 0; i < 4; i++)
il.Body.Instructions.RemoveAt(0);
}
protected void CrashReporter_HeapshotRequesting(object sender, EventArgs e)
{
foreach (TSPlayer player in TShock.Players)
@ -473,6 +524,8 @@ namespace TShockAPI
}
SaveManager.Instance.Dispose();
IL.Terraria.Initializers.AchievementInitializer.Load -= OnAchievementInitializerLoad;
ModuleManager.Dispose();
ServerApi.Hooks.GamePostInitialize.Deregister(this, OnPostInit);
@ -1663,7 +1716,7 @@ namespace TShockAPI
player.SendFileTextAsMessage(FileTools.MotdPath);
string pvpMode = Config.Settings.PvPMode.ToLowerInvariant();
if (pvpMode == "always")
if (pvpMode == "always" || pvpMode == "pvpwithnoteam")
{
player.TPlayer.hostile = true;
player.SendData(PacketTypes.TogglePvp, "", player.Index);

View file

@ -290,9 +290,15 @@ namespace TShockAPI
if (currentName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
return new List<Item> { GetItemById(i) };
if (currentName.StartsWith(name, StringComparison.InvariantCultureIgnoreCase))
{
startswith.Add(i);
else if (currentName.Contains(name, StringComparison.InvariantCultureIgnoreCase))
continue;
}
if (currentName.Contains(name, StringComparison.InvariantCultureIgnoreCase))
{
contains.Add(i);
continue;
}
}
currentName = EnglishLanguage.GetItemNameById(i);
if (!string.IsNullOrEmpty(currentName))
@ -300,9 +306,15 @@ namespace TShockAPI
if (currentName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
return new List<Item> { GetItemById(i) };
if (currentName.StartsWith(name, StringComparison.InvariantCultureIgnoreCase))
{
startswith.Add(i);
else if (currentName.Contains(name, StringComparison.InvariantCultureIgnoreCase))
continue;
}
if (currentName.Contains(name, StringComparison.InvariantCultureIgnoreCase))
{
contains.Add(i);
continue;
}
}
}
@ -377,9 +389,15 @@ namespace TShockAPI
if (currentName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
return new List<NPC> { GetNPCById(i) };
if (currentName.StartsWith(name, StringComparison.InvariantCultureIgnoreCase))
{
startswith.Add(i);
else if (currentName.Contains(name, StringComparison.InvariantCultureIgnoreCase))
continue;
}
if (currentName.Contains(name, StringComparison.InvariantCultureIgnoreCase))
{
contains.Add(i);
continue;
}
}
currentName = EnglishLanguage.GetNpcNameById(i);
if (!string.IsNullOrEmpty(currentName))
@ -387,9 +405,15 @@ namespace TShockAPI
if (currentName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
return new List<NPC> { GetNPCById(i) };
if (currentName.StartsWith(name, StringComparison.InvariantCultureIgnoreCase))
{
startswith.Add(i);
else if (currentName.Contains(name, StringComparison.InvariantCultureIgnoreCase))
continue;
}
if (currentName.Contains(name, StringComparison.InvariantCultureIgnoreCase))
{
contains.Add(i);
continue;
}
}
}
@ -435,9 +459,15 @@ namespace TShockAPI
if (currentName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
return new List<int> { i };
if (currentName.StartsWith(name, StringComparison.InvariantCultureIgnoreCase))
{
startswith.Add(i);
else if (currentName.Contains(name, StringComparison.InvariantCultureIgnoreCase))
continue;
}
if (currentName.Contains(name, StringComparison.InvariantCultureIgnoreCase))
{
contains.Add(i);
continue;
}
}
currentName = EnglishLanguage.GetBuffNameById(i);
if (!string.IsNullOrWhiteSpace(currentName))
@ -445,9 +475,15 @@ namespace TShockAPI
if (currentName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
return new List<int> { i };
if (currentName.StartsWith(name, StringComparison.InvariantCultureIgnoreCase))
{
startswith.Add(i);
else if (currentName.Contains(name, StringComparison.InvariantCultureIgnoreCase))
continue;
}
if (currentName.Contains(name, StringComparison.InvariantCultureIgnoreCase))
{
contains.Add(i);
continue;
}
}
}
@ -483,9 +519,15 @@ namespace TShockAPI
if (currentName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
return new List<int> { i };
if (currentName.StartsWith(name, StringComparison.InvariantCultureIgnoreCase))
{
startswith.Add(i);
else if (currentName.Contains(name, StringComparison.InvariantCultureIgnoreCase))
continue;
}
if (currentName.Contains(name, StringComparison.InvariantCultureIgnoreCase))
{
contains.Add(i);
continue;
}
}
currentName = EnglishLanguage.GetPrefixById(i);
if (!string.IsNullOrWhiteSpace(currentName))
@ -493,9 +535,15 @@ namespace TShockAPI
if (currentName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
return new List<int> { i };
if (currentName.StartsWith(name, StringComparison.InvariantCultureIgnoreCase))
{
startswith.Add(i);
else if (currentName.Contains(name, StringComparison.InvariantCultureIgnoreCase))
continue;
}
if (currentName.Contains(name, StringComparison.InvariantCultureIgnoreCase))
{
contains.Add(i);
continue;
}
}
}

128
TShockInstaller/Program.cs Normal file
View file

@ -0,0 +1,128 @@
using System.Diagnostics;
using System.IO.Compression;
using System.Runtime.InteropServices;
using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Tar;
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine($"TShock Installer {typeof(Program).Assembly.GetName().Version}.");
// reference: https://github.com/dotnet/install-scripts/blob/main/src/dotnet-install.sh
// ./dotnet-install.sh -verbose -version 6.0.11 --runtime dotnet
Console.WriteLine("Determining dotnet runtime url...");
var arch = RuntimeInformation.ProcessArchitecture switch
{
Architecture.X64 => "x64",
Architecture.Arm64 => "arm64",
_ => null
};
if (arch is null)
{
Console.WriteLine($"{RuntimeInformation.ProcessArchitecture} is not yet supported via this installer.");
return;
}
string? url = null;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
url = $"https://dotnetcli.azureedge.net/dotnet/Runtime/6.0.11/dotnet-runtime-6.0.11-osx-{arch}.tar.gz";
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
url = $"https://dotnetcli.azureedge.net/dotnet/Runtime/6.0.11/dotnet-runtime-6.0.11-win-{arch}.zip";
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
url = $"https://dotnetcli.azureedge.net/dotnet/Runtime/6.0.11/dotnet-runtime-6.0.11-linux-{arch}.tar.gz";
if(url is null)
{
Console.WriteLine("Unable to determine .net runtime to install. " +
"Refer to https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-install-script " +
"and install using --install-dir dotnet, so that the dotnet folder is beside TShock.Server[.exe]");
return;
}
Console.WriteLine("Using url: " + url);
var filename = url.Split('/').Last();
var is_targz = filename.EndsWith(".tar.gz");
var download_info = new FileInfo(filename);
if (!download_info.Exists) // todo hash check
{
Console.WriteLine($"Downloading: {filename}...");
using var client = new HttpClient();
using var resp = await client.GetStreamAsync(url);
using var fs = new FileStream(filename, FileMode.Create);
await resp.CopyToAsync(fs);
}
else
{
Console.WriteLine("Using existing download on disk: " + filename);
}
var dotnet_path = Path.Combine("dotnet", "dotnet" + (is_targz ? "" : ".exe"));
var tshock_path = "TShock.Server" + (is_targz ? "" : ".exe");
if (!File.Exists(dotnet_path))
{
try
{
Console.WriteLine("Extracting to ./dotnet/");
if (is_targz)
{
using var srm_dotnet_file = File.OpenRead(filename);
using var srm_gzip = new GZipInputStream(srm_dotnet_file);
using var tar_archive = TarArchive.CreateInputTarArchive(srm_gzip, System.Text.Encoding.UTF8);
tar_archive.ExtractContents("dotnet");
[DllImport("libc", SetLastError = true)]
static extern int chmod(string pathname, int mode);
chmod(dotnet_path, 755);
}
else
{
ZipFile.ExtractToDirectory(filename, "dotnet");
}
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine($"Failed to extract {filename}. The archive will be removed. Restart the installer to begin the download again.");
Console.Error.WriteLine(ex);
if (File.Exists(filename))
File.Delete(filename);
return;
}
}
else
{
Console.WriteLine($"Extract skipped, existing found at: {dotnet_path}");
}
var dotnet_root = System.IO.Path.GetFullPath("dotnet");
Console.WriteLine($"To be able to run {tshock_path} yourself set the environment variable DOTNET_ROOT={dotnet_root}");
Environment.SetEnvironmentVariable("DOTNET_ROOT", dotnet_root);
Console.WriteLine($"Extracted, launching: {tshock_path}");
var proc = new Process();
Console.CancelKeyPress += (sender, e) =>
{
e.Cancel = !proc.HasExited;
};
proc.StartInfo = new()
{
FileName = tshock_path,
};
foreach (var arg in args)
proc.StartInfo.ArgumentList.Add(arg);
proc.Start();
await proc.WaitForExitAsync();

View file

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>5.0.0</Version>
<PublishTrimmed>true</PublishTrimmed>
<AssemblyName>TShock.Installer</AssemblyName>
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SharpZipLib" Version="1.4.1" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,30 @@
using Microsoft.Xna.Framework;
using NUnit.Framework;
using Terraria.UI.Chat;
namespace TShockLauncher.Tests;
public class ChatTests
{
/// <summary>
/// Ensures that the <see cref="Terraria.GameContent.UI.Chat.AchievementTagHandler"/> does not cause exceptions when used on the server.
/// </summary>
///
/// <remarks>The behaviour of TShock regarding the achievement tag handler changes depending on if TShock has
/// a <see cref="Terraria.Main"/> instance or not. Therefore, we do not check the correctness of the parsed message, but only if it
/// throws an exception.
/// </remarks>
[TestCase]
public void TestChatAchievementTagHandler()
{
Assert.That(() =>
{
ChatManager.ParseMessage("No achievement tags", Color.White);
ChatManager.ParseMessage("One achievement tag: [a:KILL_THE_SUN]", Color.White);
ChatManager.ParseMessage("One achievement tag, using the longer variant: [achievement:KILL_THE_SUN]", Color.White);
ChatManager.ParseMessage("Multiple achievement tags: [a:KILL_THE_SUN] and [a:TOPPED_OFF]", Color.White);
ChatManager.ParseMessage("One achievement tag, referring to a non-existent achievement: [a:_THIS_WILL_NEVER_EXIST_]", Color.White);
ChatManager.ParseMessage("Both valid and invalid achievement tags: [a:KILL_THE_SUN] and [a:_THIS_WILL_NEVER_EXIST_]", Color.White);
}, Throws.Nothing);
}
}

View file

@ -1,6 +1,4 @@
using NUnit.Framework;
using Terraria;
using Terraria.Localization;
using TShockAPI;
using TShockAPI.DB;
@ -8,21 +6,6 @@ namespace TShockLauncher.Tests;
public class GroupTests
{
/// <summary>
/// This will be called automatically by nunit before other tests in this class.
/// It serves to initialise the bare minimum variables needed for TShock to be testable without booting up an actual server.
/// </summary>
[SetUp]
public static void SetupTShock()
{
Program.SavePath = ""; // 1.4.4.2 staticness introduced this where by default it is null, and any touch to Terraria.Main will use it and cause a crash.
LanguageManager.Instance.SetLanguage(GameCulture.DefaultCulture); // TShockAPI.Localization will fail without ActiveCulture set
Lang.InitializeLegacyLocalization(); // TShockAPI.Localization will fail without preparing NPC names etc
var ts = new TShock(null); // prepares configs etc
ts.Initialize(); // used to prepare tshocks own static variables, such as the TShock.DB instance
}
/// <summary>
/// This tests to ensure the group commands work.
/// </summary>

View file

@ -0,0 +1,28 @@
using NUnit.Framework;
using Terraria;
using Terraria.Initializers;
using Terraria.Localization;
using TShockAPI;
namespace TShockLauncher.Tests;
[SetUpFixture]
public class TestSetup
{
/// <summary>
/// This will be called automatically by NUnit before the first test.
/// It serves to initialise the bare minimum variables needed for TShock to be testable without booting up an actual server.
/// </summary>
[OneTimeSetUp]
public static void SetupTShock()
{
ChatInitializer.Load();
Program.SavePath = ""; // 1.4.4.2 staticness introduced this where by default it is null, and any touch to Terraria.Main will use it and cause a crash.
LanguageManager.Instance.SetLanguage(GameCulture.DefaultCulture); // TShockAPI.Localization will fail without ActiveCulture set
Lang.InitializeLegacyLocalization(); // TShockAPI.Localization will fail without preparing NPC names etc
var ts = new TShock(null); // prepares configs etc
ts.Initialize(); // used to prepare tshocks own static variables, such as the TShock.DB instance
}
}

1
TShockLauncher/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/packages

View file

@ -22,11 +22,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
* - to copy/move around TShockAPI.dll (the TShock plugin to TSAPI)
* - to publish TShock releases.
* - move dependencies to a ./bin folder
*
*
* The assembly name of this launcher (TShock.exe) was decided on by a community poll.
*/
using System.Reflection;
using TShockPluginManager;
if (args.Length > 0 && args[0].ToLower() == "plugins")
{
var items = args.ToList();
items.RemoveAt(0);
await NugetCLI.Main(items);
return;
}
else
{
Start();
}
Dictionary<string, Assembly> _cache = new Dictionary<string, Assembly>();

View file

@ -14,6 +14,7 @@
<ItemGroup>
<ProjectReference Include="..\TerrariaServerAPI\TerrariaServerAPI\TerrariaServerAPI.csproj" ExcludeFromSingleFile="true" />
<ProjectReference Include="..\TShockAPI\TShockAPI.csproj" ExcludeFromSingleFile="true" ReferenceOutputAssembly="false" /> <!-- allow api to rebuilt with this project, so ServerPlugins are refreshed -->
<ProjectReference Include="..\TShockPluginManager\TShockPluginManager.csproj"/>
<Reference Include="HttpServer" ExcludeFromSingleFile="true">
<HintPath>..\prebuilts\HttpServer.dll</HintPath>
</Reference>
@ -96,4 +97,4 @@
</ItemGroup>
<Move SourceFiles="@(MoveBinaries)" DestinationFolder="$(PublishDir)bin" ContinueOnError="true" />
</Target>
</Project>
</Project>

View file

@ -0,0 +1,114 @@
/*
TShock, a server mod for Terraria
Copyright (C) 2022 Janet Blackquill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Text;
namespace TShockPluginManager
{
static class CLIHelpers
{
static public bool YesNo()
{
System.Console.Write("[y/n] ");
bool accept;
bool confirm = false;
do
{
ConsoleKey response = Console.ReadKey(true).Key;
(accept, confirm) = response switch {
ConsoleKey.Y => (true, true),
ConsoleKey.N => (false, true),
_ => (false, false)
};
} while (!confirm);
if (accept)
System.Console.WriteLine("yes");
else
System.Console.WriteLine("no");
return accept;
}
public enum Answers {
Yes,
No,
Explain
}
static public Answers YesNoExplain()
{
System.Console.Write("[y/n/e] ");
Answers ans;
bool confirm = false;
do
{
ConsoleKey response = Console.ReadKey(true).Key;
(ans, confirm) = response switch {
ConsoleKey.Y => (Answers.Yes, true),
ConsoleKey.N => (Answers.No, true),
ConsoleKey.E => (Answers.Explain, true),
_ => (Answers.Explain, false)
};
} while (!confirm);
if (ans == Answers.Yes)
System.Console.WriteLine("yes");
else if (ans == Answers.No)
System.Console.WriteLine("no");
else
System.Console.WriteLine("explain");
return ans;
}
static private string[] ColorNames = Enum.GetNames(typeof(ConsoleColor));
static public void Write(string text)
{
var initial = Console.ForegroundColor;
var buffer = new StringBuilder();
var chars = text.ToCharArray().ToList();
while (chars.Count > 0)
{
var ch = chars.First();
if (ch == '<')
{
var possibleColor = new string(chars.Skip(1).TakeWhile(c => c != '>').ToArray());
Func<string, bool> predicate = x => string.Equals(x, possibleColor, StringComparison.CurrentCultureIgnoreCase);
if (!ColorNames.Any(predicate))
goto breakFromIf;
var color = ColorNames.First(predicate);
if (buffer.Length > 0)
{
Console.Write(buffer.ToString());
buffer.Clear();
}
Console.ForegroundColor = Enum.Parse<ConsoleColor>(color);
chars = chars.Skip(2 + possibleColor.Length).ToList();
continue;
}
breakFromIf:
buffer.Append(ch);
chars.RemoveAt(0);
}
if (buffer.Length > 0)
Console.Write(buffer.ToString());
Console.ForegroundColor = initial;
}
static public void WriteLine(string text)
{
Write(text + "\n");
}
}
}

View file

@ -0,0 +1,11 @@
using GetText;
namespace TShockPluginManager
{
static class I18n
{
static string TranslationsDirectory => Path.Combine(AppContext.BaseDirectory, "i18n");
// we share the same translations catalog as TShockAPI
public static Catalog C = new Catalog("TShockAPI", TranslationsDirectory);
}
}

View file

@ -0,0 +1,328 @@
/*
TShock, a server mod for Terraria
Copyright (C) 2022 Janet Blackquill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Reflection;
using System.Runtime.InteropServices;
using NuGet.Common;
using NuGet.Configuration;
using NuGet.Frameworks;
using NuGet.Packaging;
using NuGet.Packaging.Core;
using NuGet.Packaging.Signing;
using NuGet.Protocol.Core.Types;
using NuGet.Resolver;
using NuGet.Versioning;
namespace TShockPluginManager
{
public class Nugetter
{
// this object can figure out the right framework folders to use from a set of packages
private FrameworkReducer FrameworkReducer;
// the package framework we want to install
private NuGetFramework NuGetFramework;
// nuget settings
private ISettings Settings;
// this is responsible for bookkeeping the folders that nuget touches
private NuGetPathContext PathContext;
// this is responsible for managing the package sources
private PackageSourceProvider PackageSourceProvider;
// this is responsible for managing the repositories of packages from all of the package sources
private SourceRepositoryProvider SourceRepositoryProvider;
// this can tell us the paths of local packages
private PackagePathResolver PackagePathResolver;
// this is possible for bookkeeping the extraction state of packages
private PackageExtractionContext PackageExtractionContext;
public Nugetter()
{
FrameworkReducer = new FrameworkReducer();
NuGetFramework = NuGetFramework.ParseFolder("net6.0");
Settings = NuGet.Configuration.Settings.LoadDefaultSettings(root: null);
PathContext = NuGetPathContext.Create(Settings);
PackageSourceProvider = new PackageSourceProvider(Settings);
SourceRepositoryProvider = new SourceRepositoryProvider(PackageSourceProvider, Repository.Provider.GetCoreV3());
PackagePathResolver = new PackagePathResolver(Path.GetFullPath("packages"));
PackageExtractionContext = new PackageExtractionContext(
PackageSaveMode.Defaultv3,
XmlDocFileSaveMode.Skip,
ClientPolicyContext.GetClientPolicy(Settings, NullLogger.Instance),
NullLogger.Instance);
}
async Task GetPackageDependencies(
PackageIdentity package,
NuGetFramework framework,
SourceCacheContext cacheContext,
ILogger logger,
IEnumerable<SourceRepository> repositories,
ISet<SourcePackageDependencyInfo> availablePackages)
{
// if we've already gotten dependencies for this package, don't
if (availablePackages.Contains(package)) return;
foreach (var sourceRepository in repositories)
{
// make sure the source repository can actually tell us about dependencies
var dependencyInfoResource = await sourceRepository.GetResourceAsync<DependencyInfoResource>();
// get the try and dependencies
// (the above function returns a nullable value, but doesn't properly indicate it as such)
#pragma warning disable CS8602
var dependencyInfo = await dependencyInfoResource?.ResolvePackage(
package, framework, cacheContext, logger, CancellationToken.None);
#pragma warning restore CS8602
// oop, we don't have the ability to get dependency info from this repository, or
// it wasn't found. let's try the next source repository!
if (dependencyInfo == null) continue;
availablePackages.Add(dependencyInfo);
foreach (var dependency in dependencyInfo.Dependencies)
{
// make sure we get the dependencies of the dependencies of the dependencies ... as well
await GetPackageDependencies(
new PackageIdentity(dependency.Id, dependency.VersionRange.MinVersion),
framework, cacheContext, logger, repositories, availablePackages);
}
}
}
/// <returns>all the packages representing dependencies bundled with TShock.Server</returns>
public async Task<IEnumerable<SourcePackageDependencyInfo>> GetAllBuiltinDependencies()
{
// this is a convenient approximation of what dependencies will be included with TShock.Server
// and really only needs to be updated if new third-party dependencies are added
var knownBundles = new[] {
new PackageIdentity("GetText.NET", NuGetVersion.Parse("1.6.6")),
new PackageIdentity("OTAPI.Upcoming", NuGetVersion.Parse("3.1.8-alpha")),
new PackageIdentity("TSAPI", NuGetVersion.Parse("5.0.0-beta")),
new PackageIdentity("TShock", NuGetVersion.Parse("5.0.0-beta")),
};
return await GetAllDependenciesFor(knownBundles);
}
/// <returns>all the dependencies for the provided package identities</returns>
public async Task<IEnumerable<SourcePackageDependencyInfo>> GetAllDependenciesFor(IEnumerable<PackageIdentity> targets)
{
using var cacheContext = new SourceCacheContext();
// get all of the possible packages in our dependency tree
var possiblePackages = new HashSet<SourcePackageDependencyInfo>(PackageIdentityComparer.Default);
foreach (var target in targets)
{
await GetPackageDependencies(
target,
NuGetFramework,
cacheContext,
NullLogger.Instance,
SourceRepositoryProvider.GetRepositories(),
possiblePackages
);
}
var resolverContext = new PackageResolverContext(
// select minimum possible versions
DependencyBehavior.Lowest,
// these are the packages the user wanted
targets.Select(x => x.Id),
// we don't hard-require anything
Enumerable.Empty<string>(),
// we don't have a lockfile
Enumerable.Empty<PackageReference>(),
// we don't have fancy versioning
Enumerable.Empty<PackageIdentity>(),
// these are the packages that we figured out are in the dependency tree from nuget
possiblePackages,
// all the package sources
SourceRepositoryProvider.GetRepositories().Select(s => s.PackageSource),
NullLogger.Instance
);
var resolver = new PackageResolver();
var packagesToInstall =
// get the resolved versioning info from the resolver
resolver.Resolve(resolverContext, CancellationToken.None)
// and use that to select the specific packages to install from the possible packages
.Select(p => possiblePackages.Single(x => PackageIdentityComparer.Default.Equals(x, p)));
return packagesToInstall;
}
/// <returns>whether or not subPath is a subpath of basePath</returns>
public static bool IsSubPathOf(string subPath, string basePath)
{
var rel = Path.GetRelativePath(basePath, subPath);
return rel != "."
&& rel != ".."
&& !rel.StartsWith("../")
&& !rel.StartsWith(@"..\")
&& !Path.IsPathRooted(rel);
}
/// <returns>items required for end-user running of a package</returns>
public IEnumerable<FrameworkSpecificGroup> ItemsToInstall(PackageReaderBase packageReader)
{
var libItems = packageReader.GetLibItems();
var libnearest = FrameworkReducer.GetNearest(NuGetFramework, libItems.Select(x => x.TargetFramework));
libItems = libItems.Where(x => x.TargetFramework.Equals(libnearest));
var frameworkItems = packageReader.GetFrameworkItems();
var fwnearest = FrameworkReducer.GetNearest(NuGetFramework, frameworkItems.Select(x => x.TargetFramework));
frameworkItems = frameworkItems.Where(x => x.TargetFramework.Equals(fwnearest));
return libItems.Concat(frameworkItems);
}
/// <returns>path to package folder and metadata reader</returns>
public async Task<(string, PackageReaderBase)> GetOrDownloadPackage(SourcePackageDependencyInfo pkg)
{
using var cacheContext = new SourceCacheContext();
PackageReaderBase packageReader;
string pkgPath;
// already installed?
if (PackagePathResolver.GetInstalledPath(pkg) is string path)
{
// we're gaming
packageReader = new PackageFolderReader(path);
pkgPath = path;
}
else
{
// gotta download it...
var downloadResource = await pkg.Source.GetResourceAsync<DownloadResource>(CancellationToken.None);
Console.WriteLine($"Downloading {pkg.Id}...");
var downloadResult = await downloadResource.GetDownloadResourceResultAsync(
pkg,
new PackageDownloadContext(cacheContext),
SettingsUtility.GetGlobalPackagesFolder(Settings),
NullLogger.Instance, CancellationToken.None);
packageReader = downloadResult.PackageReader;
Console.WriteLine($"Extracting {pkg.Id}...");
// and extract the package
await PackageExtractor.ExtractPackageAsync(
downloadResult.PackageSource,
downloadResult.PackageStream,
PackagePathResolver,
PackageExtractionContext,
CancellationToken.None);
if (PackagePathResolver.GetInstalledPath(pkg) is string loc)
{
pkgPath = loc;
}
else
{
pkgPath = null;
// die somehow
}
}
return (pkgPath, packageReader);
}
/// <returns>resolved packages to be installed for what the user requested</returns>
public async Task<IEnumerable<SourcePackageDependencyInfo>> GetPackagesToInstallFor(PackageIdentity[] userRequest)
{
using var cacheContext = new SourceCacheContext();
return (await GetAllDependenciesFor(userRequest)).OrderBy(v => v.Id);
}
/// <summary>installs a locally downloaded package</summary>
public void InstallPackage(SourcePackageDependencyInfo pkg, string pkgPath, PackageReaderBase packageReader)
{
// objects to help us detect if packages already come with the .NET distribution
string[] runtimeAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll");
var paResolver = new PathAssemblyResolver(runtimeAssemblies);
using var mlc = new MetadataLoadContext(paResolver);
// packages can declare themselves as plugin via the TShockPlugin package type
var isPlugin = packageReader.NuspecReader.GetPackageTypes().Any(v => v.Name == "TShockPlugin");
Console.WriteLine($"Installing {pkg.Id}...");
foreach (var item in ItemsToInstall(packageReader))
{
var files = item.Items;
if (item.Items.Count() == 0)
continue;
// the common ancestor directory of all files in the package.
// if a package has the following files:
// - /home/orwell/packages/FooBar/hi.dll
// - /home/orwell/packages/FooBar/en-US/hi.resources.dll
// - /home/orwell/packages/FooBar/de-DE/hi.resources.dll
// - /home/orwell/packages/FooBar/ar-AR/hi.resources.dll
// this will be /home/orwell/packages/FooBar
var rootmostPath = files
.Select(x => Path.Join(pkgPath, x))
.Aggregate(Path.GetDirectoryName(Path.Join(pkgPath, files.First())), (acc, x) =>
IsSubPathOf(acc!, Path.GetDirectoryName(x)!) ?
Path.GetDirectoryName(x) :
acc);
foreach (var file in files)
{
// the absolute path of the package on the filesystem
var filePath = Path.Join(pkgPath, file);
// the path of the package relative to the package root
var packageRelativeFilePath = filePath.Substring(rootmostPath!.Length);
bool alreadyExists;
// if it's a .dll, we try to detect if we already have the assemblies
// (e.g. because we bundle them in TShock.Server or the .NET runtime comes)
// with them
if (file.EndsWith(".dll"))
{
var asms = AppDomain.CurrentDomain.GetAssemblies();
var asm = mlc.LoadFromAssemblyPath(filePath);
alreadyExists = asms.Any(a => a.GetName().Name == asm.GetName().Name);
}
else alreadyExists = false;
// if it already exists, skip. but only if it's not an explicitly requested plugin.
if (alreadyExists && !isPlugin)
continue;
var relativeFolder = Path.GetDirectoryName(packageRelativeFilePath);
var targetFolder = Path.Join(isPlugin ? "./ServerPlugins" : "./bin", relativeFolder);
Directory.CreateDirectory(targetFolder);
File.Copy(filePath, Path.Join(targetFolder, Path.GetFileName(filePath)), true);
}
}
}
/// <summary>downloads and installs the given packages</summary>
public async Task DownloadAndInstall(PackageIdentity[] userRequest)
{
var packagesToInstall = await GetAllDependenciesFor(userRequest);
var builtins = await GetAllBuiltinDependencies();
foreach (var pkg in packagesToInstall)
{
var bundled = builtins!.Where(x => x.Id == pkg.Id).FirstOrDefault();
if (bundled != null)
continue;
(string pkgPath, PackageReaderBase packageReader) = await GetOrDownloadPackage(pkg);
InstallPackage(pkg, pkgPath, packageReader);
}
}
}
}

View file

@ -0,0 +1,198 @@
/*
TShock, a server mod for Terraria
Copyright (C) 2022 Janet Blackquill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
global using static TShockPluginManager.I18n;
using System.CommandLine;
using System.Text.Json;
using System.Text.Json.Serialization;
using NuGet.Packaging.Core;
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;
namespace TShockPluginManager
{
public static class NugetCLI
{
static public async Task<int> Main(List<string> args)
{
RootCommand root = new RootCommand(
description: C.GetString("Manage plugins and their requirements")
);
Command cmdSync = new Command(
name: "sync",
description: C.GetString("Install the plugins as specified in the plugins.json")
);
cmdSync.SetHandler(Sync);
root.Add(cmdSync);
return await root.InvokeAsync(args.ToArray());
}
class SyncManifest
{
[JsonPropertyName("packages")]
public Dictionary<string, NuGetVersion> Packages { get; set; } = new();
public PackageIdentity[] GetPackageIdentities() =>
Packages.Select((kvp) => new PackageIdentity(kvp.Key, kvp.Value))
.OrderBy(kvp => kvp.Id)
.ToArray();
}
public class NuGetVersionConverter : JsonConverter<NuGetVersion>
{
public override NuGetVersion? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return NuGetVersion.Parse(reader.GetString()!);
}
public override void Write(Utf8JsonWriter writer, NuGetVersion value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToNormalizedString());
}
}
static async Task Sync()
{
var opts = new JsonSerializerOptions
{
ReadCommentHandling = JsonCommentHandling.Skip,
Converters =
{
new NuGetVersionConverter()
}
};
SyncManifest manifest;
try
{
string txt = await File.ReadAllTextAsync("packages.json");
manifest = JsonSerializer.Deserialize<SyncManifest>(txt, opts)!;
}
catch (System.IO.FileNotFoundException)
{
CLIHelpers.WriteLine(C.GetString("You're trying to sync, but you don't have a packages.json file."));
CLIHelpers.WriteLine(C.GetString("Without a list of plugins to install, no plugins can be installed."));
return;
}
catch (System.Text.Json.JsonException e)
{
CLIHelpers.WriteLine(C.GetString("There was an issue reading the packages.json."));
CLIHelpers.WriteLine($"{e.Message}");
return;
}
foreach (var item in manifest.GetPackageIdentities())
{
CLIHelpers.WriteLine($"<green>{item.Id}<black> [{item.Version}]");
}
var numWanted = manifest.GetPackageIdentities().Count();
CLIHelpers.WriteLine(C.GetPluralString("This is the plugin you requested to install.", "These are the plugins you requested to install", numWanted));
CLIHelpers.WriteLine(C.GetString("Connect to the internet to figure out what to download?"));
if (!CLIHelpers.YesNo())
return;
CLIHelpers.WriteLine(C.GetString("One moment..."));
var nugetter = new Nugetter();
PackageIdentity[] userRequests;
IEnumerable<SourcePackageDependencyInfo> packagesToInstall;
IEnumerable<SourcePackageDependencyInfo> builtinDependencies;
IEnumerable<SourcePackageDependencyInfo> directlyRequestedPackages;
IEnumerable<SourcePackageDependencyInfo> indirectlyRequiredPackages;
try
{
userRequests = manifest.GetPackageIdentities();
packagesToInstall = await nugetter.GetPackagesToInstallFor(manifest.GetPackageIdentities());
builtinDependencies = await nugetter.GetAllBuiltinDependencies();
directlyRequestedPackages = packagesToInstall.Where(x => userRequests.Any(y => x.Id == y.Id));
indirectlyRequiredPackages = packagesToInstall.Where(x => !userRequests.Any(y => x.Id == y.Id));
}
catch (NuGet.Resolver.NuGetResolverInputException e)
{
CLIHelpers.WriteLine(C.GetString("There was an issue figuring out what to download."));
CLIHelpers.WriteLine($"{e.Message}");
return;
}
catch (NuGet.Resolver.NuGetResolverConstraintException e)
{
CLIHelpers.WriteLine(C.GetString("The versions of plugins you requested aren't compatible with eachother."));
CLIHelpers.WriteLine(C.GetString("Read the message below to find out more."));
CLIHelpers.WriteLine($"{e.Message}");
return;
}
CLIHelpers.WriteLine(C.GetPluralString("=== Requested Plugin ===", "=== Requested Plugins ===", directlyRequestedPackages.Count()));
foreach (var item in directlyRequestedPackages)
DumpOne(item, builtinDependencies);
CLIHelpers.WriteLine(C.GetPluralString("=== Dependency ===", "=== Dependencies ===", indirectlyRequiredPackages.Count()));
foreach (var item in indirectlyRequiredPackages)
DumpOne(item, builtinDependencies);
CLIHelpers.WriteLine(C.GetString("Download and install the given packages?"));
CLIHelpers.WriteLine(C.GetString("Make sure that you trust the plugins you're installing."));
CLIHelpers.WriteLine(C.GetString("If you want to know which plugins need which dependencies, press E."));
bool ok = false;
do
{
switch (CLIHelpers.YesNoExplain())
{
case CLIHelpers.Answers.Yes:
ok = true;
break;
case CLIHelpers.Answers.No:
return;
case CLIHelpers.Answers.Explain:
foreach (var pkg in directlyRequestedPackages)
{
DumpGraph(pkg, packagesToInstall, builtinDependencies, 0);
}
CLIHelpers.WriteLine(C.GetString("Download and install the given packages?"));
CLIHelpers.WriteLine(C.GetString("If you'd like to see which plugins need which dependencies again, press E."));
break;
}
} while (!ok);
await nugetter.DownloadAndInstall(userRequests);
CLIHelpers.WriteLine(C.GetString("All done! :)"));
}
static public void DumpOne(SourcePackageDependencyInfo pkg, IEnumerable<SourcePackageDependencyInfo> builtins)
{
if (builtins.Any(x => x.Id == pkg.Id))
return;
var initial = Console.ForegroundColor;
CLIHelpers.WriteLine(C.GetString($"<green>{pkg.Id}<black> from <blue>{pkg.Source.PackageSource.Name} <black>[{pkg.Source.PackageSource.Source}]"));
Console.ForegroundColor = initial;
}
static public void DumpGraph(SourcePackageDependencyInfo from, IEnumerable<SourcePackageDependencyInfo> data, IEnumerable<SourcePackageDependencyInfo> builtins, int level)
{
var indent = new String('\t', level);
Console.Write(indent);
CLIHelpers.WriteLine(C.GetString($"<green>{from.Id} <black>from <blue>{from.Source.PackageSource.Name} <black>[{from.Source.PackageSource.Source}]"));
foreach (var dep in from.Dependencies)
{
if (!builtins.Any(x => x.Id == dep.Id))
{
DumpGraph(data.Single(x => x.Id == dep.Id), data, builtins, level + 1);
}
}
}
}
}

View file

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NuGet.Packaging" Version="6.3.1" />
<PackageReference Include="NuGet.Protocol" Version="6.3.1" />
<PackageReference Include="NuGet.Resolver" Version="6.3.1" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="6.0.0" />
<PackageReference Include="GetText.NET" Version="1.7.14" />
</ItemGroup>
</Project>

View file

@ -78,7 +78,32 @@ Use past tense when adding new entries; sign your name off when you add or chang
* If there is no section called "Upcoming changes" below this line, please add one with `## Upcoming changes` as the first line, and then a bulleted item directly after with the first change. -->
## Upcoming changes
* An additional option `pvpwithnoteam` is added at `PvPMode` to enable PVP with no team.(@CelestialAnarchy, #2617, @ATFGK)
* Corrected and updated deserialization of the following packets (@ATFGK):
* `ProjectileNew`: Read the third `AI` value.
* Before this change, it was previously possible for the projectile damage limit to falsely trigger, such as when using the Terra Balde and Fire Gauntlet together.
* `PlayerSpawn`: Read the `NumberOfDeathsPVE` and `NumberOfDeathsPVP` values.
* Before this change, the `PlayerSpawnContext` was always read incorrectly, due to the values above being placed in the middle of the existing structure.
* `NpcTeleportPortal`: Read the NPC index as a `ushort` instead of a `byte`.
* `PlaceObject`: Read the `Random` value.
* Before this change, the `Direction` was always read incorrectly, due to the value above being placed in the middle of the existing structure.
* `Zones`: Read the `zone5` value.
* `PaintTile` and `PaintWall`: Read the `coatTile` and `coatWall` values.
* `PlayerHurtV2`: Read the `cooldownCounter` value.
* Updated `SpawnMsg` to include the `NumberOfDeathsPVE` and `NumberOfDeathsPVP`, and allow them to be optionally used in `TSPlayer.Spawn`. (@ATFGK)
* Added `WorldTileProvider` to the tshock config with values `default`, `constileation` or `heaptile`. This allows tile providers to be changed in environments where CLI args cannot be altered. See the documentation website for more info about these providers. (@SignatureBeef)
* Updated the Utils.FindByIdOrName to follow same logic. Now fuzzy match fallback to `StartsWith` and then `Contains`. (@sgkoishi)
* Added `ShadowCandle` and `BrainOfConfusionBuff` (BoC dodge buff) to the `PlayerAddBuffWhitelist` (@drunderscore)
* Improved rejection message and code duplication in `OnPlayerBuff`. (@drunderscore)
* This will make it so Bouncer rejections regarding `PlayerAddBuff` will now always include the sender index, buff type, receiver index, and time in ticks, allowing much faster triage of buff whitelist issues.
* Allowed Digging Molecart and bomb fish to break tiles and place tracks. (@sgkoishi)
* Added built-in package management capabilities for plugins. (@pontaoski)
* Fixed Super Sponge unable to absorb shimmer. (@sgkoishi, #2833)
* Increased whitelisted duration of the Mighty Wind (`WindPushed`) buff (from sandstorms). (@drunderscore)
* Allowed the Hellfire (`OnFire3`) buff. (@drunderscore)
* Allowed Digging Molecart and bomb fish to break tiles and place tracks (@sgkoishi)
* Initialized achievements and the `AchievementManager` on the server. This ensures that players cannot cause exceptions to be thrown, chat messages are always logged, and allows achievement names to be localized in the console. Also added a test case for this. (@drunderscore)
* Allowed multiple test cases to be in TShock's test suite. (@drunderscore)
* Fixed unable to use Purification/Evil Powder in jungle. (@sgkoishi)
## TShock 5.1.3

13
docs/packages-help.txt Normal file
View file

@ -0,0 +1,13 @@
Description:
Manage plugins and their requirements
Usage:
TShock.Server [command] [options]
Options:
--version Show version information
-?, -h, --help Show help and usage information
Commands:
sync Install the plugins as specified in the plugins.json

32
docs/packages.md Normal file
View file

@ -0,0 +1,32 @@
TShock supports downloading and installing plugins from NuGet repositories.
This allows it to automatically download the plugin as well as all of the extra things that the plugin needs.
For developers, this makes distributing plugins easier.
This functionality is accessible via the TShock.Server executable used to run the server normally.
Under Linux:
```
./TShock.Server plugins
```
Under Windows (cmd.exe):
```
TShock.Server plugins
```
The documentation for the commands is included in the help functionality.
A copy of the help output in English can be found in [packages-help.txt](packages-help.txt).
This file primarily exists to document the `packages.json`.
The file format is currently simple, including only a single object, containing a key `packages` that has a map of package IDs to their versions.
An example `packages.json` is shown below:
```
{
"packages": {
"Commandy.20.10.22.Test": "0.0.1"
}
}
```
The name of the plugin is specified as the key, with the version as the value.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff