using TerrariaApi.Server;
using Terraria;
using TShockAPI.Hooks;
using TShockAPI;
using Microsoft.Xna.Framework;
using System.Text;
namespace ProgressionGates;
[ApiVersion(2, 1)]
public class ProgressionGates : TerrariaPlugin
{
public override string Name => "Progression Gates";
public override Version Version => new Version(1, 0, 0);
public override string Author => "Discipile";
public override string Description => "Allows the gatekeeping of bosses, worldevents, and invasions until a specific time.";
internal static Dictionary<Engagements, EngagementGate> Gates;
public ProgressionGates(Main game) : base(game)
{
}
public override void Initialize()
{
GeneralHooks.ReloadEvent += OnReload;
ServerApi.Hooks.GameUpdate.Register(this, OnUpdate);
ServerApi.Hooks.NpcSpawn.Register(this, OnSpawnNPC);
Commands.ChatCommands.Add(new Command("pgates.statecheck", StateCheck, "pstate", "state", "progressionstate", "serverstate"));
Commands.ChatCommands.Add(new Command("pgates.manage", ProgressionManage, "pgate", "pmanage", "progressiongatesmanage"));
Config.Load();
}
private void OnReload(ReloadEventArgs args)
{
Config.Load();
}
private void OnUpdate(EventArgs args)
{
Engagements enagement = Engagements.UnusedOrError;
bool SendWorldInfo = false;
if (Main.slimeRain)
{
enagement = Engagements.SlimeRain;
if (!Gates[enagement].AllowedToSpawn)
{
Main.StopSlimeRain();
TSPlayer.All.SendMessage($"Event \'{enagement}\' is currently disabled.", Color.MediumVioletRed);
SendWorldInfo = true;
}
}
if (Main.bloodMoon)
{
enagement = Engagements.BloodMoon;
if (!Gates[enagement].AllowedToSpawn)
{
TSPlayer.Server.SetBloodMoon(false);
TSPlayer.All.SendMessage($"Event \'{enagement}\' is currently disabled.", Color.MediumVioletRed);
SendWorldInfo = true;
}
}
if (Main.snowMoon)
{
enagement = Engagements.FrostMoon;
if (!Gates[enagement].AllowedToSpawn)
{
TSPlayer.Server.SetFrostMoon(false);
TSPlayer.All.SendMessage($"Event \'{enagement}\' is currently disabled.", Color.MediumVioletRed);
SendWorldInfo = true;
}
}
if (Main.pumpkinMoon) {
enagement = Engagements.PumpkinMoon;
if (!Gates[enagement].AllowedToSpawn)
{
TSPlayer.Server.SetPumpkinMoon(false);
TSPlayer.All.SendMessage($"Event \'{enagement}\' is currently disabled.", Color.MediumVioletRed);
SendWorldInfo = true;
}
}
if (Main.eclipse)
{
enagement = Engagements.SolarEclipse;
if (!Gates[enagement].AllowedToSpawn)
{
TSPlayer.Server.SetEclipse(false);
TSPlayer.All.SendMessage($"Event \'{enagement}\' is currently disabled.", Color.MediumVioletRed);
SendWorldInfo = true;
}
}
if (NPC.MoonLordCountdown> 0)
{
enagement = Engagements.MoonLord;
if (!Gates[enagement].AllowedToSpawn)
{
NPC.MoonLordCountdown = 0;
NPC.LunarApocalypseIsUp = false;
NPC.TowerActiveNebula = false;
NPC.TowerActiveSolar = false;
NPC.TowerActiveStardust = false;
NPC.TowerActiveVortex = false;
TSPlayer.All.SendData(PacketTypes.MoonLordCountdown);
TSPlayer.All.SendMessage($"Event \'{enagement}\' is currently disabled.", Color.MediumVioletRed);
SendWorldInfo = true;
}
}
if (Terraria.GameContent.Events.DD2Event.Ongoing)
{
enagement = Engagements.OldOnesArmy;
if (!Gates[enagement].AllowedToSpawn)
{
Terraria.GameContent.Events.DD2Event.StopInvasion();
TSPlayer.All.SendMessage($"Event \'{enagement}\' is currently disabled.", Color.MediumVioletRed);
SendWorldInfo = true;
}
}
if (Main.invasionType > 0)
{
switch (Main.invasionType)
{
case 1:
enagement = Engagements.GoblinArmy;
break;
case 2:
enagement = Engagements.FrostLegion;
break;
case 3:
enagement = Engagements.PirateInvasion;
break;
case 4:
enagement = Engagements.PumpkinMoon;
break;
case 5:
enagement = Engagements.FrostMoon;
break;
case 6:
enagement = Engagements.SolarEclipse;
break;
case 7:
enagement = Engagements.MartianMadness;
break;
default:
return;
}
if (!Gates[enagement].AllowedToSpawn)
{
Main.invasionType = 0;
Main.invasionSize = 0;
TSPlayer.All.SendMessage($"Event \'{enagement}\' is currently disabled.", Color.MediumVioletRed);
SendWorldInfo = true;
}
}
if (SendWorldInfo)
{
TSPlayer.All.SendData(PacketTypes.WorldInfo);
}
}
private void OnSpawnNPC(NpcSpawnEventArgs args)
{
NPC npc = Main.npc[args.NpcId];
Engagements type = EngagementsFunctions.FromID(npc.type);
if (type is Engagements.UnusedOrError)
return;
EngagementGate engagement = Gates[type];
if (!engagement.AllowedToSpawn)
{
npc.active = false;
npc.type = 0;
TSPlayer.All.SendData(PacketTypes.NpcUpdate, "", npc.whoAmI);
TSPlayer.All.SendMessage($"{npc.FullName} is currently disabled.", Color.MediumVioletRed);
}
}
private void StateCheck(CommandArgs args)
{
if (args.Parameters.Count == 1)
{
if (args.Parameters[0].ToLower() == "why")
{
/* if you're seeing this; feel free to change at your own will; my staff and I just got tired of having to justify it ever so often :P */
args.Player.SendInfoMessage("Progression is gated on our Survival Server in order to slow down the rate of progression so that players with: school, jobs, family, etc., have enough time to enjoy all of the content without feeling pressured to play or without having to miss certain pieces of content. If you don't like the idea of this or believe it is too slow, that's okay — you do not have to play here; the server is not designed to appeal to this preference. Yes, we are well aware you can complete a regularly Terraria playthrough in less than a week's time, but the difference is you control the pace at which you do your own playthrough, whereas this server is publicly available 24/7, and thus, state changes can occur without you present, resulting in you potentially missing content, etc. — things we explicitly want to avoid. We also believe giving the server more time to evolve helps build a stronger community and a more interesting server / server dynamic as opposed to a server that resets every week, clearing history and builds along with it.");
return;
}
else
{
args.Player.SendErrorMessage("Usage: /state [why]. /state why explains why we have progression gated on this server.");
return;
}
}
const string spacer = " | ";
string red = "[c/ff0000:";
string red_endl = "]";
string green = "[c/00ff00:";
string green_endl = "]";
bool L = args.Player.IsLoggedIn;
if (!L)
{
red = string.Empty;
red_endl = string.Empty;
green = string.Empty;
green_endl = string.Empty;
}
StringBuilder S = new StringBuilder();
void AppendEvent(Engagements eventType, string name, string icon)
{
if (Gates[eventType].AllowedToSpawn)
{
S.Append($"{(L ? $"[i:{icon}]" : string.Empty)}{green}{name}: Unlocked.{green_endl}");
}
else
{
S.Append($"{(L ? $"[i:{icon}]" : string.Empty)}{red}{name}: Locked");
if (Gates[eventType].UnlockTime != DateTime.MaxValue)
{
S.Append($" (Unlock Date: {Gates[eventType].UnlockTime})");
}
S.Append($".{red_endl}");
}
S.Append(spacer);
}
AppendEvent(Engagements.BloodMoon, "Blood Moon", "4271");
AppendEvent(Engagements.SlimeRain, "Slime Rain", "23");
AppendEvent(Engagements.KingSlime, "King Slime", "2489");
AppendEvent(Engagements.EyeOfCthulhu, "Eye of Cthulhu", "1360");
AppendEvent(Engagements.GoblinArmy, "Goblin Army", "361");
S.Append("\n");
AppendEvent(Engagements.EaterOfWorlds, "Eater of Worlds", "1361");
AppendEvent(Engagements.BrainOfCthulhu, "Brain of Cthulhu", "1362");
AppendEvent(Engagements.OldOnesArmy, "Old One's Army", "3828");
AppendEvent(Engagements.QueenBee, "Queen Bee", "1364");
AppendEvent(Engagements.Deerclops, "Deerclops", "5108");
AppendEvent(Engagements.Skeletron, "Skeletron", "1363");
S.Append("\n");
AppendEvent(Engagements.WallOfFlesh, "Wall of Flesh", "1365");
AppendEvent(Engagements.FrostLegion, "Frost Legion", "602");
AppendEvent(Engagements.PirateInvasion, "Pirate Invasion", "1315");
AppendEvent(Engagements.QueenSlime, "Queen Slime", "4958");
S.Append("\n");
AppendEvent(Engagements.Destroyer, "The Destroyer", "1366");
AppendEvent(Engagements.Twins, "The Twins", "2106");
AppendEvent(Engagements.SkeletronPrime, "Skeletron Prime", "1367");
AppendEvent(Engagements.SolarEclipse, "Solar Eclipse", "2767");
S.Append("\n");
AppendEvent(Engagements.Plantera, "Plantera", "1370");
AppendEvent(Engagements.PumpkinMoon, "Pumpkin Moon", "1844");
AppendEvent(Engagements.FrostMoon, "Frost Moon", "1958");
AppendEvent(Engagements.DukeFishron, "Duke Fishron", "2589");
AppendEvent(Engagements.EmpressOfLight, "Empress of Light", "4783");
S.Append("\n");
AppendEvent(Engagements.Golem, "Golem", "1371");
AppendEvent(Engagements.MartianMadness, "Martian Madness", "3358");
AppendEvent(Engagements.LunaticCultist, "Lunatic Cultist", "3357");
AppendEvent(Engagements.LunarPillars, "Lunar Pillars", "3572");
AppendEvent(Engagements.MoonLord, "Moon Lord", "3595");
args.Player.SendInfoMessage($"[i:367]World Progression State[i:1513] (Locked = unable to encounter at this moment, but will be unlocked in the future; Unlocked = free to encounter right now).");
args.Player.SendInfoMessage(S.ToString());
}
private void ProgressionManage(CommandArgs args)
{
const string usage = "Usage: /pgate <option>. Options: lock, unlock, check, list, [r]eset[o]verride.";
const string invalidEngagement = "Invalid engagement. Type /pgate list for a list of valid Engagement types.";
if (args.Parameters.Count < 1)
{
args.Player.SendErrorMessage(usage);
return;
}
try
{
switch (args.Parameters[0].ToLower())
{
case "list":
string list = string.Join(", ", Enum.GetNames(typeof(Engagements)).Where(e => e != Enum.GetName(Engagements.UnusedOrError)));
args.Player.SendInfoMessage($"Engagements: {list}");
break;
case "lock":
if (args.Parameters.Count < 2)
{
args.Player.SendErrorMessage("Usage: /pgate lock <engagement> [override/DateTime]. If override is used, the change is temporary. If DateTime is used, it sets the unlock time.");
return;
}
if (!Enum.TryParse(args.Parameters[1], true, out Engagements engagement) || engagement == Engagements.UnusedOrError)
{
args.Player.SendErrorMessage(invalidEngagement);
return;
}
if (args.Parameters.Count == 2)
{
Gates[engagement].SetUnlockTime(DateTime.MaxValue);
Config.SaveConfig();
args.Player.SendSuccessMessage($"[{engagement}] is now locked (Unlock time set to {Gates[engagement].UnlockTime}).");
return;
}
if (args.Parameters.Count >= 3)
{
if (args.Parameters[2].ToLower() == "true" || args.Parameters[2].ToLower() == "override")
{
Gates[engagement].OverrideAllowance(false);
args.Player.SendSuccessMessage($"[{engagement}] is now locked for the remainder of the session.");
return;
}
else if (DateTime.TryParse(args.Parameters[2], out DateTime customTime))
{
Gates[engagement].SetUnlockTime(customTime);
Config.SaveConfig();
args.Player.SendSuccessMessage($"[{engagement}] is now locked until {customTime}.");
return;
}
else
{
args.Player.SendErrorMessage("Invalid parameter. Must be \"true\", \"override\", or a valid DateTime (e.g., \"2025-04-01 12:30:00\").");
return;
}
}
break;
case "unlock":
if (args.Parameters.Count < 2)
{
args.Player.SendErrorMessage("Usage: /pgate unlock <engagement> [override/DateTime]. If override is used, the change is temporary. If DateTime is used, it sets the unlock time.");
return;
}
if (!Enum.TryParse(args.Parameters[1], true, out Engagements engagement2) || engagement2 == Engagements.UnusedOrError)
{
args.Player.SendErrorMessage(invalidEngagement);
return;
}
if (args.Parameters.Count == 2)
{
Gates[engagement2].SetUnlockTime(DateTime.MinValue);
Config.SaveConfig();
args.Player.SendSuccessMessage($"[{engagement2}] is now unlocked (Unlock time set to {Gates[engagement2].UnlockTime}).");
return;
}
if (args.Parameters.Count >= 3)
{
if (args.Parameters[2].ToLower() == "true" || args.Parameters[2].ToLower() == "override")
{
Gates[engagement2].OverrideAllowance(true);
args.Player.SendSuccessMessage($"[{engagement2}] is now unlocked for the remainder of the session.");
return;
}
else if (DateTime.TryParse(args.Parameters[2], out DateTime customTime))
{
Gates[engagement2].SetUnlockTime(customTime);
Config.SaveConfig();
args.Player.SendSuccessMessage($"[{engagement2}] will be unlocked at {customTime}.");
return;
}
else
{
args.Player.SendErrorMessage("Invalid parameter. Must be \"true\", \"override\", or a valid DateTime (e.g., \"2025-04-01 12:30:00\").");
return;
}
}
break;
case "check":
if (args.Parameters.Count < 2)
{
args.Player.SendErrorMessage("Usage: /pgate check <engagement>");
return;
}
if (Enum.TryParse(args.Parameters[1], true, out Engagements check) && check != Engagements.UnusedOrError)
{
if (Gates[check].AllowedToSpawn)
{
args.Player.SendSuccessMessage($"[{check}] is allowed (Unlock Time: {Gates[check].UnlockTime}) [Override : {(Gates[check].GetOverrideStatus() == null ? "N/A" : Gates[check].GetOverrideStatus())}].");
}
else
{
args.Player.SendErrorMessage($"[{check}] is not allowed (Unlock Time: {Gates[check].UnlockTime}) [Override : {(Gates[check].GetOverrideStatus() == null ? "N/A" : Gates[check].GetOverrideStatus())}].");
}
}
else if (args.Parameters[1].ToLower() == "all")
{
foreach (var gate in Gates)
{
if (gate.Value.AllowedToSpawn)
{
args.Player.SendSuccessMessage($"[{gate.Key}] is allowed (Unlock Time: {gate.Value.UnlockTime}) [Override : {(gate.Value.GetOverrideStatus() == null ? "N/A" : gate.Value.GetOverrideStatus())}].");
}
else
{
args.Player.SendErrorMessage($"[{gate.Key}] is not allowed (Unlock Time: {gate.Value.UnlockTime}) [Override : {(gate.Value.GetOverrideStatus() == null ? "N/A" : gate.Value.GetOverrideStatus())}].");
}
}
}
else
{
args.Player.SendErrorMessage(invalidEngagement);
}
break;
case "resetoverride":
case "ro":
if (args.Parameters.Count < 2)
{
args.Player.SendErrorMessage("Usage: /pgate resetoverride <engagement>");
return;
}
if (Enum.TryParse(args.Parameters[1], true, out Engagements reset))
{
Gates[reset].OverrideAllowance(null);
args.Player.SendSuccessMessage($"[{reset}] override has been reset.");
}
else
{
args.Player.SendErrorMessage(invalidEngagement);
}
break;
default:
args.Player.SendErrorMessage(usage);
break;
}
}
catch (Exception ex)
{
args.Player.SendErrorMessage($"An error occurred: {ex.GetType()}: {ex.Message}");
TShock.Log.ConsoleError(ex.ToString());
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
GeneralHooks.ReloadEvent -= OnReload;
ServerApi.Hooks.GameUpdate.Deregister(this, OnUpdate);
ServerApi.Hooks.NpcSpawn.Deregister(this, OnSpawnNPC);
}
base.Dispose(disposing);
}
}