diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e9c50e --- /dev/null +++ b/.gitignore @@ -0,0 +1,401 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +# Custom +/out diff --git a/Demo/Demo.csproj b/Demo/Demo.csproj new file mode 100644 index 0000000..63a964a --- /dev/null +++ b/Demo/Demo.csproj @@ -0,0 +1,6 @@ + + + + + + diff --git a/Demo/StatusTxtMgrDemo.cs b/Demo/StatusTxtMgrDemo.cs new file mode 100644 index 0000000..ed9dad0 --- /dev/null +++ b/Demo/StatusTxtMgrDemo.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Terraria; +using TerrariaApi.Server; + +namespace Demo +{ + [ApiVersion(2, 1)] + public class StatusTxtMgrDemo : TerrariaPlugin + { + #region Plugin Infos + public override string Name => "Status Text Manager Demo"; + public override Version Version => new Version("1.0.0"); + public override string Author => "LaoSparrow (Team CNS)"; + public override string Description => "Status Text Manager Demo"; + #endregion + + #region Initialize / Dispose + public StatusTxtMgrDemo(Main game) : base(game) + { + Order = 1; + } + + public override void Initialize() + { + /* + * 参数 被调用的间隔. + * 会在以下几种情况被调用: + * AccumulateTickCount % UpdateInterval == 0 + * "AccumulateTickCount" 每秒平均60次 + * + * 当 == 5 + * + * 0 1 2 3 4 5 6 7 ... + * * - - - - * - - ... + * ^ ^ + * + * 会在第 0, 5 ... Tick 被调用 + * + * + * 例子: + * + * ``` StatusTxtMgr.Hooks.StatusTextUpdate.Register(OnStatusTextUpdate, --> 15 <--); ``` + * 每 0.25秒 调用一次 ( 15 / 60 = 0.25 ) + * + * ``` StatusTxtMgr.Hooks.StatusTextUpdate.Register(OnStatusTextUpdate, --> 180 <--); ``` + * 每 3秒 调用一次 ( 180 / 60 = 3 ) + */ + + StatusTxtMgr.StatusTxtMgr.Hooks.StatusTextUpdate.Register(OnStatusTextUpdate, 120); // update every 2s + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + StatusTxtMgr.StatusTxtMgr.Hooks.StatusTextUpdate.Deregister(OnStatusTextUpdate); + } + base.Dispose(disposing); + } + #endregion + + #region Hooks + private ulong callCount = 0; + private void OnStatusTextUpdate(StatusTxtMgr.StatusTextUpdateEventArgs args) + { + // 示例 + var sb = args.statusTextBuilder; + sb.AppendLine("Hello World"); + sb.AppendLine("Hello Player " + args.tsplayer.Name); + sb.AppendLine("Call Count: " + ++callCount); + } + #endregion + } +} diff --git a/Demo2/Demo2.csproj b/Demo2/Demo2.csproj new file mode 100644 index 0000000..63a964a --- /dev/null +++ b/Demo2/Demo2.csproj @@ -0,0 +1,6 @@ + + + + + + diff --git a/Demo2/StatusTxtMgrDemo2.cs b/Demo2/StatusTxtMgrDemo2.cs new file mode 100644 index 0000000..4b47108 --- /dev/null +++ b/Demo2/StatusTxtMgrDemo2.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Terraria; +using TerrariaApi.Server; + +namespace Demo +{ + [ApiVersion(2, 1)] + public class StatusTxtMgrDemo2 : TerrariaPlugin + { + #region Plugin Infos + public override string Name => "Status Text Manager Demo2"; + public override Version Version => new Version("1.0.0"); + public override string Author => "LaoSparrow (Team CNS)"; + public override string Description => "Status Text Manager Demo2"; + #endregion + + #region Initialize / Dispose + public StatusTxtMgrDemo2(Main game) : base(game) + { + Order = 1; + } + + public override void Initialize() + { + StatusTxtMgr.StatusTxtMgr.Hooks.StatusTextUpdate.Register(OnStatusTextUpdate, 30); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + StatusTxtMgr.StatusTxtMgr.Hooks.StatusTextUpdate.Deregister(OnStatusTextUpdate); + } + base.Dispose(disposing); + } + #endregion + + #region Hooks + private ulong callCount = 0; + private void OnStatusTextUpdate(StatusTxtMgr.StatusTextUpdateEventArgs args) + { + var sb = args.statusTextBuilder; + sb.AppendLine("Hello World2"); + sb.AppendLine("Hello Player " + args.tsplayer.Name); + sb.AppendLine("Call Count: " + ++callCount); + } + #endregion + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..c3df295 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# StatusTxtMgr Demo + +示例在 `Demo` 项目中 diff --git a/StatusTxtMgr.sln b/StatusTxtMgr.sln new file mode 100644 index 0000000..90481e2 --- /dev/null +++ b/StatusTxtMgr.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StatusTxtMgr", "StatusTxtMgr\StatusTxtMgr.csproj", "{926F4C18-1AE0-4907-B813-AF607BE56292}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo", "Demo\Demo.csproj", "{5F1E128A-7B8E-4D24-8EF3-C2CE323CF37E}" + ProjectSection(ProjectDependencies) = postProject + {926F4C18-1AE0-4907-B813-AF607BE56292} = {926F4C18-1AE0-4907-B813-AF607BE56292} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo2", "Demo2\Demo2.csproj", "{32EECD79-5D8F-4D55-8C10-99B96A44BF99}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {926F4C18-1AE0-4907-B813-AF607BE56292}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {926F4C18-1AE0-4907-B813-AF607BE56292}.Debug|Any CPU.Build.0 = Debug|Any CPU + {926F4C18-1AE0-4907-B813-AF607BE56292}.Release|Any CPU.ActiveCfg = Release|Any CPU + {926F4C18-1AE0-4907-B813-AF607BE56292}.Release|Any CPU.Build.0 = Release|Any CPU + {5F1E128A-7B8E-4D24-8EF3-C2CE323CF37E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F1E128A-7B8E-4D24-8EF3-C2CE323CF37E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F1E128A-7B8E-4D24-8EF3-C2CE323CF37E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F1E128A-7B8E-4D24-8EF3-C2CE323CF37E}.Release|Any CPU.Build.0 = Release|Any CPU + {32EECD79-5D8F-4D55-8C10-99B96A44BF99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32EECD79-5D8F-4D55-8C10-99B96A44BF99}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32EECD79-5D8F-4D55-8C10-99B96A44BF99}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32EECD79-5D8F-4D55-8C10-99B96A44BF99}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CF683196-9739-4880-A931-8467BE490B23} + EndGlobalSection +EndGlobal diff --git a/StatusTxtMgr/STMSettings.cs b/StatusTxtMgr/STMSettings.cs new file mode 100644 index 0000000..2e8ae98 --- /dev/null +++ b/StatusTxtMgr/STMSettings.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using StatusTxtMgr.SettingsModel; + +namespace StatusTxtMgr +{ + public class STMSettings + { + [JsonConverter(typeof(StringEnumConverter))] + public Utils.LogLevel LogLevel = Utils.LogLevel.INFO; + public List StatusTextSettings = new(); + } +} diff --git a/StatusTxtMgr/SettingsModel/HandlerInfoOverride.cs b/StatusTxtMgr/SettingsModel/HandlerInfoOverride.cs new file mode 100644 index 0000000..b429dc2 --- /dev/null +++ b/StatusTxtMgr/SettingsModel/HandlerInfoOverride.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace StatusTxtMgr.SettingsModel +{ + public class HandlerInfoOverride : IStatusTextSetting + { + [JsonProperty] + public static string TypeName => "handler_info_override"; + public string PluginName { get; set; } + public bool Enabled { get; set; } + public ulong UpdateInterval { get; set; } + + public void ProcessHandlers(List handlers, List processedHandlers, int settingsIdx) + { + var handlerMatched = handlers.Find(h => h.AssemblyName == PluginName); + if (handlerMatched == null) + return; + + handlers.Remove(handlerMatched); + if (Enabled) + { + if (UpdateInterval > 0) + handlerMatched.UpdateInterval = UpdateInterval; + processedHandlers.Add(handlerMatched); + } + } + } +} diff --git a/StatusTxtMgr/SettingsModel/IStatusTextSetting.cs b/StatusTxtMgr/SettingsModel/IStatusTextSetting.cs new file mode 100644 index 0000000..3df3612 --- /dev/null +++ b/StatusTxtMgr/SettingsModel/IStatusTextSetting.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; +using StatusTxtMgr.Utils.Attrs; +using StatusTxtMgr.Utils.JsonConverters; + +namespace StatusTxtMgr.SettingsModel +{ + [JsonConverter(typeof(InterfaceConcreteConverter))] + [Implements(typeof(StaticText), typeof(HandlerInfoOverride))] + public interface IStatusTextSetting + { + // 实际上还需要加一个 + // public static string TypeName => "handler_info_override"; + // 供 Converter 使用 + + /// + /// 令 Handler 处理 Handler List + /// + /// 由插件而来未经处理的 Handlers + /// 最终交由 HandlerList 依序调用的 Handlers + /// 实例在 Setting 中的 index + void ProcessHandlers(List handlers, List processedHandlers, int settingsIdx); + } +} diff --git a/StatusTxtMgr/SettingsModel/StaticText.cs b/StatusTxtMgr/SettingsModel/StaticText.cs new file mode 100644 index 0000000..11a1418 --- /dev/null +++ b/StatusTxtMgr/SettingsModel/StaticText.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; +using TShockAPI; + +namespace StatusTxtMgr.SettingsModel +{ + public class StaticText : IStatusTextSetting, IStatusTextUpdateHandler + { + [JsonProperty] + public static string TypeName => "static_text"; + public string Text { get; set; } + + public void ProcessHandlers(List handlers, List processedHandlers, int settingsIdx) => + processedHandlers.Add(this); + + + public bool Invoke(TSPlayer tsplr, bool forceUpdate = false) => forceUpdate; + + public string GetPlrST(TSPlayer tsplr) => Text; + } +} diff --git a/StatusTxtMgr/StatusTextUpdateHandler.cs b/StatusTxtMgr/StatusTextUpdateHandler.cs new file mode 100644 index 0000000..9b9ed3d --- /dev/null +++ b/StatusTxtMgr/StatusTextUpdateHandler.cs @@ -0,0 +1,62 @@ +using StatusTxtMgr.Utils; +using System.Reflection; +using System.Text; +using Terraria; +using TShockAPI; + +namespace StatusTxtMgr +{ + public interface IStatusTextUpdateHandler + { + bool Invoke(TSPlayer tsplr, bool forceUpdate = false); + string GetPlrST(TSPlayer tsplr); + } + + + public delegate void StatusTextUpdateDelegate(StatusTextUpdateEventArgs args); + + public class StatusTextUpdateEventArgs + { + public TSPlayer tsplayer { get; set; } + public StringBuilder statusTextBuilder { get; set; } + } + + public class StatusTextUpdateHandlerItem : IStatusTextUpdateHandler + { + public StatusTextUpdateDelegate UpdateDelegate; + public ulong UpdateInterval = 60; + public string AssemblyName; + + private StringBuilder?[] plrSBs = new StringBuilder[Main.maxPlayers]; + + public StatusTextUpdateHandlerItem(StatusTextUpdateDelegate updateDelegate, ulong updateInterval = 60) + { + UpdateDelegate = updateDelegate ?? throw new ArgumentNullException(nameof(updateDelegate)); + UpdateInterval = updateInterval > 0 ? updateInterval : throw new ArgumentException("cannot be 0", nameof(updateInterval)); + AssemblyName = updateDelegate.Method.DeclaringType?.Assembly.GetName().Name ?? ""; + } + + public bool Invoke(TSPlayer tsplr, bool forceUpdate = false) + { + try + { + // 检查对应玩家是否需要更新 Status Text + if (forceUpdate || (Utils.Common.TickCount + (ulong)tsplr.Index) % UpdateInterval == 0) + { + var updateDelegate = UpdateDelegate; + var args = new StatusTextUpdateEventArgs() { tsplayer = tsplr, statusTextBuilder = plrSBs.AcquirePlrSB(tsplr) }; + updateDelegate(args); + return true; + } + } + catch (Exception ex) + { + Logger.Warn($"Exception occur while invoking delegate of '{AssemblyName}' in StatusTextUpdateHandler.Invoke, Ex: {ex}"); + } + return false; + } + + // 获取当前 Handler 对对应玩家的 Status Text + public string GetPlrST(TSPlayer tsplr) => plrSBs[tsplr.Index]?.ToString() ?? ""; + } +} diff --git a/StatusTxtMgr/StatusTextUpdateHandlerList.cs b/StatusTxtMgr/StatusTextUpdateHandlerList.cs new file mode 100644 index 0000000..571b3d2 --- /dev/null +++ b/StatusTxtMgr/StatusTextUpdateHandlerList.cs @@ -0,0 +1,88 @@ +using StatusTxtMgr.Utils; +using System.Text; +using TShockAPI; + +namespace StatusTxtMgr +{ + // 从旧版 Tshock HandlerList 抄的,建议光速重写 + public class StatusTextUpdateHandlerList + { + private List Handlers { get; set; } = new(); + private List ProcessedHandlers { get; set; } = new(); + private object HandlersLock = new object(); + + public StatusTextUpdateHandlerList() + { + + } + + public void Register(StatusTextUpdateDelegate handler, ulong updateInterval = 60) + { + Register(new StatusTextUpdateHandlerItem(handler, updateInterval)); + } + + public void Register(StatusTextUpdateHandlerItem handlerItem) + { + lock (HandlersLock) + { + Handlers.Add(handlerItem); + LoadSettings(); + } + } + + public void Deregister(StatusTextUpdateDelegate handler) + { + lock (HandlersLock) + { + Handlers.RemoveAll(hi => hi.UpdateDelegate == handler); + LoadSettings(); + } + } + + public bool Invoke(TSPlayer tsplr, StringBuilder sb, bool forceUpdate = false) + { + try + { + List list; + lock (HandlersLock) + { + list = new List(ProcessedHandlers); + } + var isUpdateRequired = list.Aggregate(false, (current, hi) => hi.Invoke(tsplr, forceUpdate) || current); + // 轮询 Handlers 对应玩家是否需要更新 Status Text + if (isUpdateRequired) + { + // 将更新后的 Status Text 组合起来 + foreach (var hi in list) + { + sb.Append(hi.GetPlrST(tsplr)); + } + } + return isUpdateRequired; // 对应玩家是否需要更新 Status Text + } + catch (Exception ex) + { + Logger.Warn("Exception occur in StatusTextUpdateHandlerList.Invoke, Ex: " + ex); + return false; + } + } + + public void LoadSettings() + { + lock (HandlersLock) + { + // 依次加载所有 Setting Handlers + var handlers = new List(Handlers); + ProcessedHandlers.Clear(); + var idx = 0; + foreach (var sts in StatusTxtMgr.Settings.StatusTextSettings) + { + sts.ProcessHandlers(handlers, ProcessedHandlers, idx); + idx++; + } + // 将所有未被 Setting Handlers '认领' 的 Plugin Handlers 加入到 Processed Handlers 中 + ProcessedHandlers.AddRange(handlers); + } + } + } +} diff --git a/StatusTxtMgr/StatusTxtMgr.cs b/StatusTxtMgr/StatusTxtMgr.cs new file mode 100644 index 0000000..30dbaac --- /dev/null +++ b/StatusTxtMgr/StatusTxtMgr.cs @@ -0,0 +1,192 @@ +using StatusTxtMgr.Utils; +using System.Text; +using Terraria; +using TerrariaApi.Server; +using TShockAPI; +using TShockAPI.Configuration; +using TShockAPI.Hooks; + +namespace StatusTxtMgr +{ + [ApiVersion(2, 1)] + public class StatusTxtMgr : TerrariaPlugin + { + #region Plugin Infos + public override string Name => "Status Text Manager"; + public override Version Version => new("1.0.0"); + public override string Author => "LaoSparrow (Team CNS)"; + public override string Description => "Manage status text of different plugins"; + #endregion + + #region Fields + // Config + private static string ConfigFilePath = Path.Combine(TShock.SavePath, "StatusTxtMgr.json"); + private static ConfigFile Config = new(); + internal static STMSettings Settings => Config.Settings; + + // Exported Hooks + public static ApiShortcut Hooks = new(); + private static readonly StatusTextUpdateHandlerList handlerList = new(); + + // States + private readonly bool[] isPlrNeedInit = new bool[Main.maxPlayers]; + private readonly bool[] isPlrSTVisible = new bool[Main.maxPlayers]; + #endregion + + #region Initialize / Dispose + public StatusTxtMgr(Main game) : base(game) + { + Order = 1; + } + + public override void Initialize() + { + ServerApi.Hooks.GameInitialize.Register(this, OnGameInitialize); + ServerApi.Hooks.GamePostUpdate.Register(this, OnGamePostUpdate); + ServerApi.Hooks.ServerJoin.Register(this, OnServerJoin); + GeneralHooks.ReloadEvent += OnReload; + + Commands.ChatCommands.Add(new Command(cmdst, "statustext", "st")); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + ServerApi.Hooks.GameInitialize.Deregister(this, OnGameInitialize); + ServerApi.Hooks.GamePostUpdate.Deregister(this, OnGamePostUpdate); + ServerApi.Hooks.ServerJoin.Deregister(this, OnServerJoin); + GeneralHooks.ReloadEvent -= OnReload; + } + base.Dispose(disposing); + } + #endregion + + #region Cmds + private void cmdst(CommandArgs args) + { + switch (args.Parameters.Count) + { + case 0: + isPlrSTVisible[args.Player.Index] = !isPlrSTVisible[args.Player.Index]; + if (isPlrSTVisible[args.Player.Index]) + { + isPlrNeedInit[args.Player.Index] = true; + args.Player.SendSuccessMessage("已开启模板显示"); + } + else + { + isPlrNeedInit[args.Player.Index] = false; + args.Player.SendData(PacketTypes.Status, "", 0, 0x1f); + args.Player.SendSuccessMessage("已关闭模板显示"); + } + break; + + case 1: + switch (args.Parameters[0]) + { + case "on": + case "show": + if (!isPlrSTVisible[args.Player.Index]) + { + isPlrSTVisible[args.Player.Index] = true; + isPlrNeedInit[args.Player.Index] = true; + } + args.Player.SendSuccessMessage("已开启模板显示"); + break; + + case "off": + case "hide": + if (isPlrSTVisible[args.Player.Index]) + { + isPlrSTVisible[args.Player.Index] = false; + isPlrNeedInit[args.Player.Index] = false; + args.Player.SendData(PacketTypes.Status, "", 0, 0x1f); + } + args.Player.SendSuccessMessage("已关闭模板显示"); + break; + + case "help": + default: + args.Player.SendInfoMessage("用法:/st "); + break; + } + break; + } + } + #endregion + + #region Hooks + private void OnGameInitialize(EventArgs args) + { + // Config Loading + if (!Directory.Exists(TShock.SavePath)) + Directory.CreateDirectory(TShock.SavePath); + LoadConfig(); + } + + private void OnReload(ReloadEventArgs args) + { + LoadConfig(); + } + + private void LoadConfig() + { + try + { + Config.Read(ConfigFilePath, out var incompleteSettings); + if (incompleteSettings) + Config.Write(ConfigFilePath); + handlerList.LoadSettings(); + } + catch (Exception ex) + { + Logger.Warn("Failed to load config, Ex: " + ex); + } + } + + private void OnGamePostUpdate(EventArgs args) + { + try + { + foreach (var tsplr in Utils.Common.PlayersOnline) + { + if (!isPlrSTVisible[tsplr.Index]) + continue; + + StringBuilder sb = Utils.StringBuilderCache.Acquire(); + if (handlerList.Invoke(tsplr, sb, isPlrNeedInit[tsplr.Index])) + { + tsplr.SendData(PacketTypes.Status, Utils.StringBuilderCache.GetStringAndRelease(sb), 0, 0x1f); + // 0x1f -> HideStatusTextPercent + } + else + { + Utils.StringBuilderCache.Release(sb); + } + isPlrNeedInit[tsplr.Index] = false; + } + + Utils.Common.CountTick(); + } + catch (Exception ex) + { + Logger.Warn("Exception occur in OnGamePostUpdate, Ex: " + ex); + } + } + + private void OnServerJoin(JoinEventArgs args) + { + isPlrSTVisible[args.Who] = true; + isPlrNeedInit[args.Who] = true; + } + #endregion + + #region Api Shortcut + public class ApiShortcut + { + public StatusTextUpdateHandlerList StatusTextUpdate => handlerList; + } + #endregion + } +} diff --git a/StatusTxtMgr/StatusTxtMgr.csproj b/StatusTxtMgr/StatusTxtMgr.csproj new file mode 100644 index 0000000..577afe3 --- /dev/null +++ b/StatusTxtMgr/StatusTxtMgr.csproj @@ -0,0 +1,3 @@ + + + diff --git a/StatusTxtMgr/Utils/Attrs/ImplementsAttribute.cs b/StatusTxtMgr/Utils/Attrs/ImplementsAttribute.cs new file mode 100644 index 0000000..58eddfc --- /dev/null +++ b/StatusTxtMgr/Utils/Attrs/ImplementsAttribute.cs @@ -0,0 +1,12 @@ +namespace StatusTxtMgr.Utils.Attrs +{ + public class ImplementsAttribute : Attribute + { + public Type[] ImplementsTypes; + + public ImplementsAttribute(params Type[] implementsTypes) + { + ImplementsTypes = implementsTypes; + } + } +} diff --git a/StatusTxtMgr/Utils/Common.cs b/StatusTxtMgr/Utils/Common.cs new file mode 100644 index 0000000..da71362 --- /dev/null +++ b/StatusTxtMgr/Utils/Common.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Terraria; +using TShockAPI; + +namespace StatusTxtMgr.Utils +{ + internal static class Common + { + public static IEnumerable PlayersOnline => from p in TShock.Players where p is { Active: true } select p; + + public static StringBuilder AcquirePlrSB(this StringBuilder?[] sbs, TSPlayer tsplr) + { + var plrId = tsplr.Index; + var sb = sbs[plrId]; + if (sb == null) + { + sb = new StringBuilder(); + sbs[plrId] = sb; + return sb; + } + return sb.Clear(); + } + + public static ulong TickCount = 0; + public static void CountTick() => TickCount++; + public static void ClearTickCount() => TickCount = 0; + } +} diff --git a/StatusTxtMgr/Utils/JsonConverters/InterfaceConcreteConverter.cs b/StatusTxtMgr/Utils/JsonConverters/InterfaceConcreteConverter.cs new file mode 100644 index 0000000..28fea60 --- /dev/null +++ b/StatusTxtMgr/Utils/JsonConverters/InterfaceConcreteConverter.cs @@ -0,0 +1,59 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StatusTxtMgr.Utils.Attrs; +using System.Reflection; + +namespace StatusTxtMgr.Utils.JsonConverters +{ + // 评价:一坨使 + // 懒得解释了 + public class InterfaceConcreteConverter : Newtonsoft.Json.JsonConverter + { + public override bool CanRead => true; + public override bool CanWrite => false; + public override bool CanConvert(Type objectType) => objectType.IsInterface; + + + public InterfaceConcreteConverter() + { + + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + try + { + var jsonObj = JObject.Load(reader); + object target = null; + JToken jsonTypeName; + if (jsonObj.TryGetValue("TypeName", out jsonTypeName) && jsonTypeName is JValue) + { + foreach (Type t in objectType.GetCustomAttribute()?.ImplementsTypes) + { + var propInfo = t.GetProperty("TypeName", BindingFlags.Public | BindingFlags.Static); + if (propInfo == null || propInfo.PropertyType != typeof(string)) + continue; + if ((string)propInfo.GetValue(null) == jsonTypeName.Value()) + { + target = Activator.CreateInstance(t); + break; + } + } + } + if (target == null) + throw new Exception("Could not find a corresponding concrete class"); + serializer.Populate(jsonObj.CreateReader(), target); + return target; + } + catch (Exception ex) + { + throw new Exception("Failed to convert", ex); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} diff --git a/StatusTxtMgr/Utils/Log.cs b/StatusTxtMgr/Utils/Log.cs new file mode 100644 index 0000000..e35f284 --- /dev/null +++ b/StatusTxtMgr/Utils/Log.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StatusTxtMgr.Utils +{ + public enum LogLevel + { + NONE, + ERROR, + WARNING, + INFO, + DEBUG, + } + + internal static class Logger + { + private static readonly Dictionary level2Color = new Dictionary() + { + { LogLevel.NONE, ConsoleColor.White }, + { LogLevel.ERROR, ConsoleColor.Red }, + { LogLevel.WARNING, ConsoleColor.Yellow }, + { LogLevel.INFO, ConsoleColor.White }, + { LogLevel.DEBUG, ConsoleColor.Green } + }; + + private static readonly object consoleWriteLock = new object(); + + public static void Err(string msg) => Log(msg, LogLevel.ERROR); + public static void Warn(string msg) => Log(msg, LogLevel.WARNING); + public static void Info(string msg) => Log(msg, LogLevel.INFO); + public static void Debug(string msg) => Log(msg, LogLevel.DEBUG); + + public static void Log(string msg, LogLevel level = LogLevel.DEBUG) + { + if (level > StatusTxtMgr.Settings.LogLevel) + return; + lock (consoleWriteLock) + { + Console.ForegroundColor = level2Color[level]; + Console.WriteLine($"[StatusTxtMgr] [{level:G}] {msg}"); + Console.ResetColor(); + } + } + } +} diff --git a/StatusTxtMgr/Utils/StringBuilderCache.cs b/StatusTxtMgr/Utils/StringBuilderCache.cs new file mode 100644 index 0000000..c4c5679 --- /dev/null +++ b/StatusTxtMgr/Utils/StringBuilderCache.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StatusTxtMgr.Utils +{ + + // 从 System.Text.StringBuilderCache 抄的,不做解释 + internal static class StringBuilderCache + { + internal const int MAX_BUILDER_SIZE = 360; + + [ThreadStatic] + private static StringBuilder CachedInstance; + + public static StringBuilder Acquire(int capacity = 16) + { + if (capacity <= 360) + { + StringBuilder cachedInstance = CachedInstance; + if (cachedInstance != null && capacity <= cachedInstance.Capacity) + { + CachedInstance = null; + cachedInstance.Clear(); + return cachedInstance; + } + } + + return new StringBuilder(capacity); + } + + public static void Release(StringBuilder sb) + { + if (sb.Capacity <= 360) + { + CachedInstance = sb; + } + } + + public static string GetStringAndRelease(StringBuilder sb) + { + string result = sb.ToString(); + Release(sb); + return result; + } + } +} diff --git a/template.targets b/template.targets new file mode 100644 index 0000000..2cd4acd --- /dev/null +++ b/template.targets @@ -0,0 +1,18 @@ + + + net6.0 + enable + enable + + + + ..\out\$(Configuration) + false + + + + + contentFiles + + +