diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 4d075e0..21f12f6 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -19,22 +19,17 @@ jobs: - name: Setup Dotnet uses: actions/setup-dotnet@v4 - - name: Setup NuGet - uses: NuGet/setup-nuget@v2 - - - name: Restore NuGet packages - run: nuget restore YongAnFrame.sln - - name: Restore dependencies run: dotnet restore - name: Build - run: dotnet build --no-restore + run: dotnet build --configuration Release - name: Generate NuGet packages - run: nuget pack + run: dotnet pack - name: Upload NuGet package uses: actions/upload-artifact@v4 with: + name: nupkg path: YongAnFrame.*.nupkg diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 36f6bdd..915d168 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,5 +1,5 @@ # Simple workflow for deploying static content to GitHub Pages -name: Build and Publish Docfx Pages +name: Build and Publish Master Docfx Pages on: # Runs on pushes targeting the default branch @@ -34,12 +34,6 @@ jobs: - name: Dotnet Setup uses: actions/setup-dotnet@v4 - - name: Setup NuGet - uses: NuGet/setup-nuget@v2 - - - name: Restore NuGet packages - run: nuget restore YongAnFrame.sln - - run: dotnet tool update -g docfx - run: docfx docs/docfx.json diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index b911f62..ee5ae43 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -18,24 +18,19 @@ jobs: - name: Setup Dotnet uses: actions/setup-dotnet@v4 - - - name: Setup NuGet - uses: NuGet/setup-nuget@v2 - - - name: Restore NuGet packages - run: nuget restore YongAnFrame.sln - name: Restore dependencies run: dotnet restore - name: Build - run: dotnet build --no-restore + run: dotnet build --configuration Release - name: Generate NuGet packages - run: nuget pack + run: dotnet pack - name: Upload NuGet package uses: actions/upload-artifact@v4 with: + name: nupkg path: YongAnFrame.*.nupkg diff --git a/.gitignore b/.gitignore index 9c2122d..9491a2f 100644 --- a/.gitignore +++ b/.gitignore @@ -360,5 +360,4 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd -/nuget.exe +FodyWeavers.xsd \ No newline at end of file diff --git a/Commands/ExpCommand.cs b/Commands/ExpCommand.cs index d6329f4..419ec5a 100644 --- a/Commands/ExpCommand.cs +++ b/Commands/ExpCommand.cs @@ -2,19 +2,36 @@ using Exiled.API.Features; using Exiled.Permissions.Extensions; using System; -using System.Linq; +using YongAnFrame.Extensions; namespace YongAnFrame.Commands { + /// + /// 玩家经验指令 + /// [CommandHandler(typeof(RemoteAdminCommandHandler))] public class ExpCommand : ICommand { - public string Command => "pexperience"; - - public string[] Aliases => ["pexp"]; - + /// + /// 主要指令名 + /// + public string Command => "PlayerExp"; + /// + /// 次要指令名 + /// + public string[] Aliases => ["pexp","pe"]; + /// + /// 指令描述 + /// public string Description => "用于经验的设置"; + /// + /// 指令逻辑 + /// + /// 指令集 + /// 发送者 + /// 原因 + /// 是否运行成功 public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) { response = "NO"; diff --git a/Commands/MessageCommand.cs b/Commands/MessageCommand.cs index 5b1ec40..1d50525 100644 --- a/Commands/MessageCommand.cs +++ b/Commands/MessageCommand.cs @@ -3,7 +3,11 @@ using Exiled.Permissions.Extensions; using System; using System.Collections.Generic; -using YongAnFrame.Players; +using System.Runtime.ConstrainedExecution; +using YongAnFrame.Extensions; +using YongAnFrame.Features.Players; +using YongAnFrame.Features.UI.Enums; +using YongAnFrame.Features.UI.Texts; namespace YongAnFrame.Commands { @@ -13,12 +17,13 @@ namespace YongAnFrame.Commands [CommandHandler(typeof(RemoteAdminCommandHandler))] public sealed class MessageCommand : ICommand { + /// public string Command => "message"; - - public string[] Aliases => ["mes", "msg"]; - + /// + public string[] Aliases => ["m", "msg"]; + /// public string Description => "用于发送消息"; - + /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) { List choicePlayer = []; @@ -52,11 +57,7 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s if (int.TryParse(idString, out int id)) { - FramePlayer yPlayer = FramePlayer.Get(id); - if (yPlayer != null) - { - choicePlayer.Add(yPlayer); - } + choicePlayer.Add(FramePlayer.Get(id)); } break; } @@ -79,7 +80,7 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s foreach (FramePlayer yPlayer in choicePlayer) { - yPlayer.HintManager.MessageTexts.Add(new HintManager.Text($"[管理员发送]{arguments.Array[2]}", duration)); + yPlayer.UI.MessageList.Add(new MessageText($"{arguments.Array[2]}", duration, MessageType.Admin)); } response = "已成功运行"; return true; diff --git a/Commands/PlayerCommand.cs b/Commands/PlayerCommand.cs index 1823024..ff86f0b 100644 --- a/Commands/PlayerCommand.cs +++ b/Commands/PlayerCommand.cs @@ -1,33 +1,39 @@ using CommandSystem; using Exiled.API.Features; using System; -using YongAnFrame.Players; +using YongAnFrame.Extensions; +using YongAnFrame.Features.Players; namespace YongAnFrame.Commands { + /// + /// 框架玩家指令 + /// [CommandHandler(typeof(ClientCommandHandler))] public class PlayerCommand : ICommand { - public string Command => "hPlayer"; - - public string[] Aliases => ["hPlay", "hp", "h"]; - + /// + public string Command => "FramePlayer"; + /// + public string[] Aliases => ["player", "fp" ,"p"]; + /// public string Description => "用于管理自己的YongAnFrame用户"; - + /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) { response = "NULL"; if (arguments.Count >= 1 && Player.TryGet(sender, out Player player)) { - FramePlayer fPlayer = FramePlayer.Get(player); + //FramePlayer fPlayer = player.ToFPlayer(); switch (arguments.Array[1]) { case "BDNT": - fPlayer.HintManager.Clean(); - fPlayer.ExPlayer.ShowHint($"{YongAnFramePlugin.Instance.Translation.BypassDoNotTrack.Split('\n')}", 10000f); + // 等待重置 + //fPlayer.HintManager.Clean(); + //fPlayer.ExPlayer.ShowHint($"{YongAnFramePlugin.Instance.Translation.BypassDoNotTrack.Split('\n')}", 10000f); return true; case "INFO": - + return true; } } diff --git a/Commands/SkillCommand.cs b/Commands/SkillCommand.cs index c7d95a1..a45f45e 100644 --- a/Commands/SkillCommand.cs +++ b/Commands/SkillCommand.cs @@ -1,37 +1,57 @@ using CommandSystem; using Exiled.API.Features; using System; -using YongAnFrame.Players; -using YongAnFrame.Roles; -using YongAnFrame.Roles.Properties; +using YongAnFrame.Extensions; +using YongAnFrame.Features.Players; +using YongAnFrame.Features.Roles; +using YongAnFrame.Features.UI.Enums; +using YongAnFrame.Features.UI.Texts; namespace YongAnFrame.Commands { /// - /// 未完成请勿乱用 + /// 技能指令 /// [CommandHandler(typeof(ClientCommandHandler))] public sealed class SkillsCommand : ICommand { + /// public string Command => "skills"; - + /// public string[] Aliases => ["sk"]; - + /// public string Description => "skills"; - + /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) { response = "NO"; if (arguments.Count >= 1 && int.TryParse(arguments.Array[1], out int num) && Player.TryGet(sender, out Player player)) { - FramePlayer fPlayer = FramePlayer.Get(player); - - if (fPlayer.CustomRolePlus != null && fPlayer.CustomRolePlus.Check(fPlayer, out CustomRolePlusProperties data)) + FramePlayer fPlayer = player.ToFPlayer(); + + if (fPlayer.CustomRolePlus is not null && fPlayer.CustomRolePlus.Check(fPlayer, out CustomRolePlusData data)) { - SkillManager skillManager = data.SkillManagers[num]; - skillManager.Run(); - fPlayer.HintManager.MessageTexts.Add(new HintManager.Text($"技能[{skillManager.SkillProperties.Name}:{fPlayer.CustomRolePlus.GetType().GUID.ToString() + 10000}]已经发动,持续时间:{skillManager.SkillProperties.ActiveMaxTime}", skillManager.SkillProperties.ActiveMaxTime)); + if (data.Skills == null) + { + response = "角色没有技能"; + return false; + } + + Skill skill = data.Skills[num]; + if (skill.IsActive) + { + fPlayer.UI.MessageList.Add(new MessageText("技能正在持续", 5, MessageType.System)); + } + else if (skill.IsBurial) + { + fPlayer.UI.MessageList.Add(new MessageText($"技能正在冷却(CD:{skill.BurialRemainingTime})", 5, MessageType.System)); + } + else + { + skill.Run(); + } + response = "OK"; return true; } diff --git a/Components/CapacityList.cs b/Components/CapacityList.cs index ef1dff3..af58cc3 100644 --- a/Components/CapacityList.cs +++ b/Components/CapacityList.cs @@ -4,25 +4,32 @@ namespace YongAnFrame.Components { - public class CapacityList(int capacity) : ICollection, IEnumerable, IEnumerable + /// + /// 容量列表 + /// + /// 存储 + /// 容量 + /// 修改委托 + public class CapacityList(int capacity, Action? modify = null) : IList, ICollection, IEnumerable, IEnumerable { private readonly List list = new(capacity); + private readonly Action? modify = modify; - public int Capacity { get; set; } = capacity; - + /// + /// 获取容量 + /// + public int Capacity { get; } = capacity; + /// public int Count => list.Count; - + /// public bool IsReadOnly => false; + /// public T this[int index] { get { - if (Count > index) - { - return list[index]; - } - return default; + return list[index]; } set { @@ -30,6 +37,7 @@ public T this[int index] } } + /// public void Add(T item) { if (Capacity > list.Count) @@ -41,46 +49,51 @@ public void Add(T item) list.RemoveAt(0); list.Add(item); } + modify?.Invoke(); } + /// public bool Remove(T item) { - return list.Remove(item); + bool v = list.Remove(item); + modify?.Invoke(); + return v; } - public IEnumerator GetEnumerator() - { - return list.GetEnumerator(); - } + /// + IEnumerator IEnumerable.GetEnumerator() => list.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() - { - return list.GetEnumerator(); - } + /// + public IEnumerator GetEnumerator() => list.GetEnumerator(); + /// public void Clear() { list.Clear(); + modify?.Invoke(); } - public bool Contains(T item) - { - return list.Contains(item); - } + /// + public bool Contains(T item) => list.Contains(item); - public void CopyTo(T[] array, int arrayIndex) - { - list.CopyTo(array, arrayIndex); - } + /// + public void CopyTo(T[] array, int arrayIndex) => list.CopyTo(array, arrayIndex); - public int IndexOf(T item) - { - return list.IndexOf(item); - } + /// + public int IndexOf(T item) => list.IndexOf(item); + /// public void RemoveAt(int index) { list.RemoveAt(index); + modify?.Invoke(); + } + + /// + public void Insert(int index, T item) + { + list.Insert(index, item); + modify?.Invoke(); } } } diff --git a/Config.cs b/Config.cs index bee8590..295a27b 100644 --- a/Config.cs +++ b/Config.cs @@ -1,10 +1,12 @@ using Exiled.API.Interfaces; +using System.Collections.Generic; using System.ComponentModel; +using YongAnFrame.Features.Roles; namespace YongAnFrame { /// - /// 插件的配置 + /// 的配置 /// public sealed class Config : IConfig { @@ -13,10 +15,18 @@ public sealed class Config : IConfig /// public bool Debug { get; set; } /// - /// 全局的经验加成 + /// 获取或设置全局的经验加成 /// [Description("全局的经验加成")] public float GlobalExpMultiplier { get; set; } = 1; - + + /// + /// 获取或设置禁用自定义角色生成 + /// + /// + /// 只能用于继承的类 + /// + [Description("禁用自定义角色生成(只能用于继承CustomRolePlus的类)")] + public List DisableCustomRolePlus { get; set; } = ["114514", "1"]; } } diff --git a/Events/EventArgs/FramePlayer/FramePlayerCreatedEventArgs.cs b/Events/EventArgs/FramePlayer/FramePlayerCreatedEventArgs.cs index 1132d5f..1f9275f 100644 --- a/Events/EventArgs/FramePlayer/FramePlayerCreatedEventArgs.cs +++ b/Events/EventArgs/FramePlayer/FramePlayerCreatedEventArgs.cs @@ -5,8 +5,11 @@ namespace YongAnFrame.Events.EventArgs.FramePlayer /// /// FramePlayer被创建时的事件数据 /// - public sealed class FramePlayerCreatedEventArgs(Players.FramePlayer fPlayer) : IExiledEvent + public sealed class FramePlayerCreatedEventArgs(Features.Players.FramePlayer fPlayer) : IExiledEvent { - public Players.FramePlayer FPlayer { get; } = fPlayer; + /// + /// 获取框架玩家 + /// + public Features.Players.FramePlayer FPlayer { get; } = fPlayer; } } diff --git a/Events/EventArgs/FramePlayer/FramePlayerInvalidingEventArgs.cs b/Events/EventArgs/FramePlayer/FramePlayerInvalidingEventArgs.cs index bff6f88..c626bdf 100644 --- a/Events/EventArgs/FramePlayer/FramePlayerInvalidingEventArgs.cs +++ b/Events/EventArgs/FramePlayer/FramePlayerInvalidingEventArgs.cs @@ -5,8 +5,11 @@ namespace YongAnFrame.Events.EventArgs.FramePlayer /// /// FramePlayer被无效时的事件数据 /// - public sealed class FramePlayerInvalidatingEventArgs(Players.FramePlayer fPlayer) : IExiledEvent + public sealed class FramePlayerInvalidatingEventArgs(Features.Players.FramePlayer fPlayer) : IExiledEvent { - public Players.FramePlayer FPlayer { get; } = fPlayer; + /// + /// 获取框架玩家 + /// + public Features.Players.FramePlayer FPlayer { get; } = fPlayer; } } diff --git a/Events/Handlers/FramePlayer.cs b/Events/Handlers/FramePlayer.cs index 80845a7..3a721d0 100644 --- a/Events/Handlers/FramePlayer.cs +++ b/Events/Handlers/FramePlayer.cs @@ -3,32 +3,36 @@ namespace YongAnFrame.Events.Handlers { + /// + /// 框架玩家事件处理器 + /// public sealed class FramePlayer { /// - /// FramePlayer被创建时的事件 + /// 被创建时事件 /// public static Event FramePlayerCreated { get; set; } = new Event(); /// - /// FramePlayer被无效时的事件 + /// 被无效时事件 /// public static Event FramePlayerInvalidating { get; set; } = new Event(); /// - /// FramePlayer提示刷新前的事件 + /// 提示刷新前事件 /// public static Event FramePlayerHintUpdate { get; set; } = new Event(); - - public static void OnFramePlayerCreated(FramePlayerCreatedEventArgs args) - { - FramePlayerCreated.InvokeSafely(args); - } - public static void OnFramerHintUpdate() - { - FramePlayerHintUpdate.InvokeSafely(); - } - public static void OnFramePlayerInvalidating(FramePlayerInvalidatingEventArgs args) - { - FramePlayerInvalidating.InvokeSafely(args); - } + /// + /// 触发被创建时事件 + /// + /// 事件数据 + public static void OnFramePlayerCreated(FramePlayerCreatedEventArgs args) => FramePlayerCreated.InvokeSafely(args); + /// + /// 提示刷新前事件 + /// + public static void OnFramerHintUpdate() => FramePlayerHintUpdate.InvokeSafely(); + /// + /// 提示刷新前事件 + /// + /// 事件数据 + public static void OnFramePlayerInvalidating(FramePlayerInvalidatingEventArgs args) => FramePlayerInvalidating.InvokeSafely(args); } } diff --git a/YongAnTool.cs b/Extensions/YongAnExtension.cs similarity index 57% rename from YongAnTool.cs rename to Extensions/YongAnExtension.cs index afcbd38..d50b238 100644 --- a/YongAnTool.cs +++ b/Extensions/YongAnExtension.cs @@ -1,17 +1,13 @@ using Exiled.API.Features; -using Respawning; using System; -using System.Collections.Generic; -using System.Reflection; -using YongAnFrame.Players; -using YongAnFrame.Roles.Enums; +using YongAnFrame.Features.Players; -namespace YongAnFrame +namespace YongAnFrame.Extensions { /// - /// 扩展方法工具类 + /// 扩展方法通用工具类 /// - public static class YongAnTool + public static class YongAnExtension { /// /// 作为种子取随机数 @@ -20,19 +16,13 @@ public static class YongAnTool /// 最小值 /// 最大值 /// - public static int StrictNext(this Random r, int min, int max) - { - return new Random(BitConverter.ToInt32(Guid.NewGuid().ToByteArray(), 0)).Next(min, max); - } + public static int StrictNext(this Random r, int min, int max) => new Random(BitConverter.ToInt32(Guid.NewGuid().ToByteArray(), 0)).Next(min, max); /// /// 转换为 /// /// /// - public static FramePlayer ToFPlayer(this Player p) - { - return FramePlayer.Get(p); - } + public static FramePlayer ToFPlayer(this Player? p) => FramePlayer.Get(p); } } diff --git a/Features/MusicManager.cs b/Features/MusicManager.cs new file mode 100644 index 0000000..92ae08e --- /dev/null +++ b/Features/MusicManager.cs @@ -0,0 +1,210 @@ + +using AudioApi; +using AudioApi.EventArgs.Voice; +using Exiled.API.Features; +using Exiled.API.Features.Components; +using Mirror; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using YongAnFrame.Features.Players; +using static YongAnFrame.Features.TrackEvent; + +namespace YongAnFrame.Features +{ + /// + /// 音乐管理器 + /// + public static class MusicManager + { + private static uint num = 0; + /// + /// 获取放音频的玩家(NPC) + /// + public static Dictionary MusicNpc { get; } = []; + private static readonly Dictionary trackEventDic = []; + + static MusicManager() + { + VoicePlayerBase.OnTrackLoaded += TrackLoaded; + VoicePlayerBase.OnFinishedTrack += FinishedTrack; + } + + private static void TrackLoaded(TrackLoadedEventArgs args) + { + if (trackEventDic.TryGetValue(args.VoicePlayerBase, out TrackEvent trackEvent)) + { + trackEvent.PlayMusicAction?.Invoke(args); + } + } + + private static void FinishedTrack(TrackFinishedEventArgs args) + { + if (trackEventDic.TryGetValue(args.VoicePlayerBase, out TrackEvent trackEvent)) + { + trackEvent.StopMusicAction?.Invoke(args); + } + + } + + private static ReferenceHub CreateMusicNpc(string name) + { + var newNpc = UnityEngine.Object.Instantiate(NetworkManager.singleton.playerPrefab); + ReferenceHub hubNpc = newNpc.GetComponent(); + NetworkServer.AddPlayerForConnection(new FakeConnection(0), newNpc); + hubNpc.nicknameSync.Network_myNickSync = name; + hubNpc.authManager.NetworkSyncedUserId = null; + MusicNpc.Add($"{num++}:{name}", hubNpc); + return hubNpc; + } + + private static void KillMusicNpc(VoicePlayerBase playerBase) + { + if (playerBase is null) return; + ReferenceHub npc = playerBase.Owner; + MusicNpc.Remove(npc.nicknameSync.Network_myNickSync); + CustomNetworkManager.TypedSingleton.OnServerDisconnect(npc.connectionToClient); + Player.Dictionary.Remove(npc.gameObject); + UnityEngine.Object.Destroy(npc.gameObject); + } + + /// + /// 立刻停止播放音频 + /// + /// voicePlayerBase + public static void Stop(VoicePlayerBase playerBase) + { + if (playerBase is null) return; + playerBase.Stoptrack(true); + KillMusicNpc(playerBase); + } + /// + /// 向所有玩家播放音频 + /// + /// 音频文件 + /// NPC名称 + /// + public static VoicePlayerBase Play(string musicName, string npcName) => Play(musicName, npcName, -1); + /// + /// 向玩家()播放音频 + /// + /// 音频文件/Url + /// NPC名称 + /// 传播距离检测源头玩家(可null,null时是NPC) + /// + public static VoicePlayerBase Play(string musicName, string npcName, FramePlayer source) => Play(musicName, npcName, source, 0); + /// + /// NPC在米内向玩家播放音频 + /// + /// 音频文件/Url + /// NPC名称 + /// 传播距离(-1时是全部玩家,0时是源头玩家) + /// + public static VoicePlayerBase Play(string musicName, string npcName, float distance) => Play(musicName, npcName, null, distance); + /// + /// 在米内向玩家播放音频 + /// + /// 音频文件/Url + /// NPC名称 + /// 传播距离检测源头玩家(可null,null时是NPC) + /// 传播距离(-1时是全部玩家,0时是源头玩家) + /// + public static VoicePlayerBase Play(string musicName, string npcName, FramePlayer? source, float distance) => Play(musicName, npcName, null, source, distance, null, 80, false); + /// + /// 播放音频 + /// + /// 音频文件/Url + /// NPC名称 + /// 播放事件(可null) + /// 传播距离检测源头玩家(可null,null时是NPC) + /// 传播距离(-1时是全部玩家,0时是源头玩家) + /// 额外可接收音频的玩家(可null) + /// 音量大小 + /// 是否循环 + /// + public static VoicePlayerBase Play(string musicName, string npcName, TrackEvent? trackEvent, FramePlayer? source, float distance, FramePlayer[]? extraPlay, float volume = 80, bool isLoop = false) + { + VoicePlayerBase? voicePlayerBase = null; + ReferenceHub npc = CreateMusicNpc(npcName); + voicePlayerBase = VoicePlayerBase.Get(npc); + + try + { + if (trackEvent is not null) + { + if (trackEventDic.ContainsKey(voicePlayerBase)) + { + trackEventDic[voicePlayerBase] = (TrackEvent)trackEvent; + } + else + { + trackEventDic.Add(voicePlayerBase, (TrackEvent)trackEvent); + } + } + + if (distance != -1) + { + if (source is not null) + { + if (distance == 0) + { + voicePlayerBase.BroadcastTo.Add(npc.PlayerId); + } + else + { + voicePlayerBase.BroadcastTo = [.. FramePlayer.List.Where(p => Vector3.Distance(p.ExPlayer!.Position, source.ExPlayer!.Position) <= distance).Select((s) => s.ExPlayer!.Id)]; + } + } + + if (extraPlay is not null) + { + foreach (var player in extraPlay) + { + if (!voicePlayerBase.BroadcastTo.Contains(player.ExPlayer.Id)) + { + voicePlayerBase.BroadcastTo.Add(player.ExPlayer.Id); + } + } + } + } + + voicePlayerBase.CurrentPlay = $"{PathManager.Music}/{musicName}.ogg"; + voicePlayerBase.Volume = volume; + voicePlayerBase.Loop = isLoop; + voicePlayerBase.Play(-1); + } + catch (Exception) + { + Stop(voicePlayerBase); + } + return voicePlayerBase; + } + } + /// + /// 音轨事件 + /// + /// 播放音频委托 + /// 停止音频委托 + public readonly struct TrackEvent(PlayMusic? playMusic, StopMusic? stopMusic) + { + /// + /// 播放音频 + /// + /// + public delegate void PlayMusic(TrackLoadedEventArgs args); + /// + /// 停止音频 + /// + /// + public delegate void StopMusic(TrackFinishedEventArgs args); + /// + /// 获取播放音频委托 + /// + public PlayMusic? PlayMusicAction { get; } = playMusic; + /// + /// 获取停止音频委托 + /// + public StopMusic? StopMusicAction { get; } = stopMusic; + } +} \ No newline at end of file diff --git a/Features/PathManager.cs b/Features/PathManager.cs new file mode 100644 index 0000000..ed0d828 --- /dev/null +++ b/Features/PathManager.cs @@ -0,0 +1,35 @@ +using Exiled.API.Features; +using System.IO; + +namespace YongAnFrame.Features +{ + /// + /// IO路径管理器 + /// + public static class PathManager + { + /// + /// 获取音频路径 + /// + public static string Music => $"{Paths.Exiled}/YongAnFrame/{Server.Port}/Music"; + /// + /// 获取日志路径 + /// + public static string Log => $"{Paths.Exiled}/YongAnFrame/{Server.Port}/Log"; + + /// + /// 检查路径是否存在 + /// + public static void CheckPath() + { + if (!Directory.Exists(Music)) + { + Directory.CreateDirectory(Music); + } + if (!Directory.Exists(Log)) + { + Directory.CreateDirectory(Log); + } + } + } +} diff --git a/Features/PlayerUI.cs b/Features/PlayerUI.cs new file mode 100644 index 0000000..0ae1bfb --- /dev/null +++ b/Features/PlayerUI.cs @@ -0,0 +1,180 @@ +using HintServiceMeow.Core.Enum; +using HintServiceMeow.Core.Models.Hints; +using HintServiceMeow.Core.Utilities; +using MEC; +using System.Collections.Generic; +using System.Text; +using YongAnFrame.Components; +using YongAnFrame.Features.Players; +using YongAnFrame.Features.Roles; +using YongAnFrame.Features.UI.Texts; + +namespace YongAnFrame.Features +{ + /// + /// 的UI + /// + public class PlayerUI + { + /// + /// 获取拥有该实例的 + /// + public FramePlayer FPlayer { get; } + /// + /// 获取的HintServiceMeow核心 + /// + public PlayerDisplay PlayerDisplay { get; private set; } + /// + /// 获取消息数据列表 + /// + public CapacityList MessageList { get; } + /// + /// 获取聊天数据列表 + /// + public CapacityList ChatList { get; } + + private readonly CoroutineHandle coroutine; + #region Hint + private readonly Hint versionHint = new() + { + Text = "YongAnFrame 1.0.0-beta6+002", + FontSize = 20, + Alignment = HintAlignment.Center, + YCoordinateAlign = HintVerticalAlign.Top, + YCoordinate = 0 + }; + private readonly Hint customRoleHint = new() + { + FontSize = 20, + Alignment = HintAlignment.Center, + YCoordinateAlign = HintVerticalAlign.Bottom, + YCoordinate = 1080 + }; + private readonly Hint chatHint = new() + { + FontSize = 20, + Alignment = HintAlignment.Right, + YCoordinate = 400 + }; + private readonly Hint messageHint = new() + { + FontSize = 20, + Alignment = HintAlignment.Left, + YCoordinate = 400 + }; + #endregion + + private IEnumerator Timer() + { + while (true) + { + + + for (int i = 0; i < MessageList.Count; i++) + { + MessageText message = MessageList[i]; + if (message.Duration-- <= 0) + { + MessageList.Remove(message); + i--; + } + UpdateMessageUI(); + } + + bool isUpdate = false; + + for (int i = 0; i < ChatList.Count; i++) + { + ChatText chat = ChatList[i]; + if (chat.Duration-- <= 0) + { + ChatList.Remove(chat); + i--; + isUpdate = true; + } + } + if (isUpdate) UpdateChatUI(); + + yield return Timing.WaitForSeconds(1f); + } + } + + /// + /// 更新全部UI + /// + public void UpdateUI() + { + UpdateCustomRoleUI(); + UpdateMessageUI(); + UpdateChatUI(); + } + + /// + /// 更新自定义角色UI + /// + public void UpdateCustomRoleUI() + { + if (FPlayer.CustomRolePlus is null) + { + customRoleHint.Text = null; + return; + } + StringBuilder builder = new($"{FPlayer.CustomRolePlus.Name}\n\r{FPlayer.CustomRolePlus.Description}"); + Skill[]? Skills = FPlayer.CustomRolePlus.BaseData[FPlayer].Skills; + if (Skills != null) + { + foreach (var skill in Skills) + { + builder.AppendLine($"{skill.Name}({skill.UseItem}):{skill.Description}(激活:{skill.ActiveMaxTime}|冷却:{skill.BurialMaxTime})"); + } + } + customRoleHint.Text = builder.ToString(); + } + + /// + /// 更新消息UI + /// + public void UpdateMessageUI() + { + messageHint.Text = string.Join("\n\r", MessageList); + } + /// + /// 更新聊天UI + /// + public void UpdateChatUI() + { + chatHint.Text = string.Join("\n\r", ChatList); + } + /// + /// 清除全部UI + /// + public void Clean() + { + Timing.KillCoroutines(coroutine); + PlayerDisplay.ClearHint(); + } + /// + /// 构造方法 + /// + /// + public PlayerUI(FramePlayer fPlayer) + { + FPlayer = fPlayer; + MessageList = new(7, UpdateMessageUI); + ChatList = new(7, UpdateChatUI); + coroutine = Timing.RunCoroutine(Timer()); + PlayerDisplay = PlayerDisplay.Get(fPlayer); + PlayerDisplay.AddHint(customRoleHint); + PlayerDisplay.AddHint(chatHint); + PlayerDisplay.AddHint(messageHint); + PlayerDisplay.AddHint(versionHint); + } + /// + /// 解构方法 + /// + ~PlayerUI() + { + Clean(); + } + } +} diff --git a/Features/Players/CustomPlayer.cs b/Features/Players/CustomPlayer.cs new file mode 100644 index 0000000..627ba95 --- /dev/null +++ b/Features/Players/CustomPlayer.cs @@ -0,0 +1,59 @@ +using Exiled.API.Features; +using YongAnFrame.Features.Players.Interfaces; +using YongAnFrame.Features.Roles; + +namespace YongAnFrame.Features.Players +{ + /// + /// 自定义玩家 + /// + /// + /// 如果有自定义玩家需要挂载到的需求,请继承从而自动处理挂载逻辑 + ///
+ /// 但是目前暂无实质性功能 + ///
+ /// 框架玩家 + public abstract class CustomPlayer(FramePlayer player) + { + /// + /// 获取拥有该实例的 + /// + public FramePlayer FramePlayer { get; private set; } = player; + /// + public Player ExPlayer => FramePlayer.ExPlayer; + /// + /// 获取是否无效 + /// + public bool IsInvalid => FramePlayer is null; + /// + public CustomRolePlus? CustomRolePlus => FramePlayer.CustomRolePlus; + /// + public PlayerUI UI => FramePlayer.UI; + /// + public ICustomAlgorithm CustomAlgorithm { get => FramePlayer.CustomAlgorithm; set => FramePlayer.CustomAlgorithm = value; } + /// + public ulong Level { get => FramePlayer.Level; set => FramePlayer.Level = value; } + /// + public ulong Exp { get => FramePlayer.Exp; set => FramePlayer.Exp = value; } + /// + public float ExpMultiplier { get => FramePlayer.ExpMultiplier; set => FramePlayer.ExpMultiplier = value; } + /// + public bool IsBDNT { get => FramePlayer.IsBDNT; set => FramePlayer.IsBDNT = value; } + /// + public PlayerTitle? UsingTitles { get => FramePlayer.UsingTitles; set => FramePlayer.UsingTitles = value; } + /// + public PlayerTitle? UsingRankTitles { get => FramePlayer.UsingRankTitles; set => FramePlayer.UsingRankTitles = value; } + /// + public void AddExp(ulong exp, string name = "未知原因") => FramePlayer.AddExp(exp, name); + /// + public void UpdateShowInfo() => FramePlayer.UpdateShowInfo(); + /// + public abstract void Invalid(); + + /// + /// 隐性转换 + /// + /// 自定义玩家 + public static implicit operator FramePlayer(CustomPlayer yPlayer) => yPlayer.FramePlayer; + } +} diff --git a/Players/FramePlayer.cs b/Features/Players/FramePlayer.cs similarity index 68% rename from Players/FramePlayer.cs rename to Features/Players/FramePlayer.cs index d07f347..c3d800f 100644 --- a/Players/FramePlayer.cs +++ b/Features/Players/FramePlayer.cs @@ -1,53 +1,73 @@ using Exiled.API.Features; +using Exiled.CustomRoles; using Exiled.CustomRoles.API; +using Exiled.CustomRoles.API.Features; using Exiled.Events.EventArgs.Player; using Exiled.Events.Features; using MEC; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using YongAnFrame.Events.EventArgs.FramePlayer; -using YongAnFrame.Roles; -using static YongAnFrame.Players.HintManager; +using YongAnFrame.Extensions; +using YongAnFrame.Features.Players.Interfaces; +using YongAnFrame.Features.Roles; +using YongAnFrame.Features.UI.Enums; +using YongAnFrame.Features.UI.Texts; -namespace YongAnFrame.Players +namespace YongAnFrame.Features.Players { + /// + /// 永安框架的玩家类 + /// public sealed class FramePlayer : ICustomAlgorithm { - private PlayerTitle usingTitles = null; - private PlayerTitle usingRankTitles = null; + private PlayerTitle? usingTitles = null; + private PlayerTitle? usingRankTitles = null; private static readonly Dictionary dictionary = []; + private Player? exPlayer; /// - /// 获取拥有该实例的 + /// 获取该实例拥有的 /// - public Player ExPlayer { get; private set; } + /// + /// 在运行事件后实例无效,再调用可能会引发异常
+ /// 玩家退出后必须不再引用,否则会造成数字ID重复的问题 + ///
+ public Player ExPlayer + { + get + { + if (exPlayer is null) + { + throw new InvalidCastException("FramePlayer实例已无效"); + } + return exPlayer; + } + } /// /// 获取有效的框架玩家列表 /// public static IReadOnlyCollection List => [.. dictionary.Values]; /// - /// 获取的玩家是否有效 - /// - public bool IsInvalid { get => ExPlayer == null; } - /// /// 获取玩家拥有的自定义角色 /// - public CustomRolePlus CustomRolePlus + public CustomRolePlus? CustomRolePlus { get { - if (ExPlayer.GetCustomRoles().Count != 0) + ReadOnlyCollection customRoleList = ExPlayer.GetCustomRoles(); + if (customRoleList.Count != 0 && customRoleList[0] is CustomRolePlus custom) { - return (CustomRolePlus)ExPlayer.GetCustomRoles()[0]; + return custom; } return null; } } /// - /// 获取或设置玩家的提示系统管理器 + /// 获取玩家的UI /// - public HintManager HintManager { get; private set; } - + public PlayerUI UI { get; private set; } /// /// 获取或设置玩家正在使用的主要自定义算法 /// @@ -62,6 +82,10 @@ public CustomRolePlus CustomRolePlus ///
public ulong Exp { get; set; } /// + /// 获取全局的经验加成 + /// + public float GlobalExpMultiplier => YongAnFramePlugin.Instance.Config.GlobalExpMultiplier; + /// /// 获取或设置玩家的经验倍率 /// public float ExpMultiplier { get; set; } @@ -72,17 +96,27 @@ public CustomRolePlus CustomRolePlus /// /// 获取或设置玩家正在使用的名称称号 /// - public PlayerTitle UsingTitles { get => usingTitles; set { if (value != null && !value.IsRank) { usingTitles = value; } } } + public PlayerTitle? UsingTitles + { + get => usingTitles; + set + { + if (value is not null && !value.IsRank) + { + usingTitles = value; + } + } + } /// /// 获取或设置玩家正在使用的地位称号 /// - public PlayerTitle UsingRankTitles + public PlayerTitle? UsingRankTitles { get => usingRankTitles; set { - if (value != null && value.IsRank) + if (value is not null && value.IsRank) { usingRankTitles = value; } @@ -93,7 +127,7 @@ public PlayerTitle UsingRankTitles /// /// 获取或设置玩家的地位名称。 /// - public string RankName + public string? RankName { get => ExPlayer.RankName; set @@ -107,9 +141,9 @@ public string RankName /// /// 获取或设置玩家的地位颜色。 /// - public string RankColor + public string? RankColor { - get => ExPlayer.RankColor; + get => ExPlayer.RankColor; set { if (RankColor != value) @@ -135,23 +169,25 @@ public string CustomName #endregion #region Static + /// + /// 注册全局事件 + /// public static void SubscribeStaticEvents() { Exiled.Events.Handlers.Player.Verified += new CustomEventHandler(OnStaticVerified); - //Exiled.Events.Handlers.Server.WaitingForPlayers += new CustomEventHandler(OnStaticWaitingForPlayers); Exiled.Events.Handlers.Player.Destroying += new CustomEventHandler(OnStaticDestroying); } - + /// + /// 注销全局事件 + /// public static void UnsubscribeStaticEvents() { Exiled.Events.Handlers.Player.Verified += new CustomEventHandler(OnStaticVerified); - //Exiled.Events.Handlers.Server.WaitingForPlayers += new CustomEventHandler(OnStaticWaitingForPlayers); Exiled.Events.Handlers.Player.Destroying += new CustomEventHandler(OnStaticDestroying); } private static void OnStaticVerified(VerifiedEventArgs args) { - if (args.Player.IsNPC) return; new FramePlayer(args.Player); } private static void OnStaticDestroying(DestroyingEventArgs args) @@ -159,10 +195,6 @@ private static void OnStaticDestroying(DestroyingEventArgs args) FramePlayer fPlayer = args.Player.ToFPlayer(); fPlayer.Invalid(); } - //private static void OnStaticWaitingForPlayers() - //{ - // dictionary.Clear(); - //} #endregion @@ -172,9 +204,9 @@ private static void OnStaticDestroying(DestroyingEventArgs args) /// Exiled玩家 internal FramePlayer(Player player) { - ExPlayer = player; + exPlayer = player; dictionary.Add(ExPlayer.Id, this); - HintManager = new HintManager(this); + UI = new(this); CustomAlgorithm = this; Events.Handlers.FramePlayer.OnFramePlayerCreated(new FramePlayerCreatedEventArgs(this)); UpdateShowInfo(); @@ -187,12 +219,11 @@ internal FramePlayer(Player player) /// 原因 public void AddExp(ulong exp, string name = "未知原因") { - float globalExpMultiplier = YongAnFramePlugin.Instance.Config.GlobalExpMultiplier; - float expMultiplier = ExpMultiplier * globalExpMultiplier; + float expMultiplier = ExpMultiplier * GlobalExpMultiplier; ulong addExp = (ulong)(exp * expMultiplier); Exp += addExp; - HintManager.MessageTexts.Add(new Text($"{name},获得{exp}+{addExp - exp}经验({expMultiplier}倍经验)", 5)); + UI.MessageList.Add(new MessageText($"{name},获得{exp}+{addExp - exp}经验({expMultiplier}倍经验)", 5, MessageType.System)); ulong needExp = CustomAlgorithm.GetNeedUpLevel(Level); ulong oldLevel = Level; @@ -206,7 +237,7 @@ public void AddExp(ulong exp, string name = "未知原因") if (oldLevel < Level) { UpdateShowInfo(); - HintManager.MessageTexts.Add(new Text($"恭喜你从{oldLevel}级到达{Level}级,距离下一级需要{Exp}/{needExp}经验", 8)); + UI.MessageList.Add(new MessageText($"恭喜你从{oldLevel}级到达{Level}级,距离下一级需要{Exp}/{needExp}经验", 8, MessageType.System)); } } @@ -220,12 +251,10 @@ public void AddExp(ulong exp, string name = "未知原因") /// public void UpdateShowInfo() { - if (ExPlayer.IsNPC) return; - - if (ExPlayer.GlobalBadge != null) + if (ExPlayer.GlobalBadge is not null) { CustomName = $"[LV:{Level}][全球徽章]{ExPlayer.Nickname}"; - if (CustomRolePlus != null) + if (CustomRolePlus is not null) { RankName = $"*{ExPlayer.GlobalBadge.Value.Text}* {CustomRolePlus.Name}"; } @@ -237,18 +266,18 @@ public void UpdateShowInfo() return; } - string rankColor = null; - string rankName = null; + string? rankColor = null; + string? rankName = null; - if (CustomRolePlus != null) + if (CustomRolePlus is not null) { rankName = CustomRolePlus.Name; rankColor = CustomRolePlus.NameColor; } - if (usingTitles != null) + if (usingTitles is not null) { - if (usingTitles.DynamicCommand != null) + if (usingTitles.DynamicCommand is not null) { Timing.KillCoroutines(coroutines[1]); coroutines[1] = Timing.RunCoroutine(DynamicTitlesShow()); @@ -267,16 +296,16 @@ public void UpdateShowInfo() ExPlayer.CustomName = $"[LV:{Level}]{ExPlayer.Nickname}"; } - if (usingRankTitles != null) + if (usingRankTitles is not null) { - if (usingRankTitles.DynamicCommand != null) + if (usingRankTitles.DynamicCommand is not null) { Timing.KillCoroutines(coroutines[0]); coroutines[0] = Timing.RunCoroutine(DynamicRankTitlesShow()); } else { - if (CustomRolePlus != null) + if (CustomRolePlus is not null) { rankName = $"{CustomRolePlus.Name} *{usingRankTitles.Name}*"; } @@ -300,9 +329,13 @@ private IEnumerator DynamicRankTitlesShow() { while (true) { + if (usingRankTitles is null || usingRankTitles.DynamicCommand is null) + { + yield break; + } foreach (var command in usingRankTitles.DynamicCommand) { - if (CustomRolePlus != null) + if (CustomRolePlus is not null) { RankName = $"{CustomRolePlus.Name} *{command[0]}*"; } @@ -310,7 +343,7 @@ private IEnumerator DynamicRankTitlesShow() { RankName = $"{command[0]}"; } - if (usingRankTitles == null) + if (usingRankTitles is null) { RankColor = command[1]; } @@ -322,10 +355,14 @@ private IEnumerator DynamicTitlesShow() { while (true) { + if (usingTitles is null || usingTitles.DynamicCommand is null) + { + yield break; + } foreach (var command in usingTitles.DynamicCommand) { CustomName = $"[LV:{Level}][{command[0]}]{ExPlayer.Nickname}"; - if (usingRankTitles == null) + if (usingRankTitles is null) { RankColor = command[1]; } @@ -336,33 +373,32 @@ private IEnumerator DynamicTitlesShow() #endregion /// - public ulong GetNeedUpLevel(ulong level) - { - return (ulong)(100 + Math.Floor(level / 10f) * 100); - } + public ulong GetNeedUpLevel(ulong level) => (ulong)(100 + Math.Floor(level / 10f) * 100); /// /// 获取框架玩家 /// /// Exiled玩家 /// 框架玩家 - public static FramePlayer Get(Player player) + public static FramePlayer Get(Player? player) { - if (dictionary.TryGetValue(player.Id, out FramePlayer yPlayer)) + if (player is null) { - return yPlayer; + throw new InvalidCastException("Player实例无效"); } - return null; + if (!dictionary.TryGetValue(player.Id, out FramePlayer yPlayer)) + { + throw new InvalidCastException("FramePlayer实例无效"); + } + return yPlayer; } + /// /// 获取框架玩家 /// /// 玩家数字ID /// 框架玩家 - public static FramePlayer Get(int numId) - { - return Get(Player.Get(numId)); - } + public static FramePlayer Get(int numId) => Get(Player.Get(numId)); /// /// 调用后该实例会立刻无效
@@ -374,13 +410,14 @@ public void Invalid() Events.Handlers.FramePlayer.OnFramePlayerInvalidating(new FramePlayerInvalidatingEventArgs(this)); CustomRolePlus?.RemoveRole(this); dictionary.Remove(ExPlayer.Id); - HintManager?.Clean(); - ExPlayer = null; + UI.Clean(); + exPlayer = null; } - public static implicit operator Player(FramePlayer yPlayer) - { - return yPlayer.ExPlayer; - } + /// + /// 隐性转换 + /// + /// 框架玩家 + public static implicit operator Player(FramePlayer yPlayer) => yPlayer.ExPlayer; } } diff --git a/Players/ICustomAlgorithm.cs b/Features/Players/Interfaces/ICustomAlgorithm.cs similarity index 86% rename from Players/ICustomAlgorithm.cs rename to Features/Players/Interfaces/ICustomAlgorithm.cs index 9e7190b..38ab96f 100644 --- a/Players/ICustomAlgorithm.cs +++ b/Features/Players/Interfaces/ICustomAlgorithm.cs @@ -1,4 +1,4 @@ -namespace YongAnFrame.Players +namespace YongAnFrame.Features.Players.Interfaces { /// /// 自定义算法接口 diff --git a/Features/Players/PlayerTitle.cs b/Features/Players/PlayerTitle.cs new file mode 100644 index 0000000..652cee5 --- /dev/null +++ b/Features/Players/PlayerTitle.cs @@ -0,0 +1,99 @@ +using Exiled.API.Features; +using System; +using System.Collections.Generic; + +namespace YongAnFrame.Features.Players +{ + /// + /// 的称号 + /// + public sealed class PlayerTitle + { + private static readonly Dictionary dictionary = []; + /// + /// 获取有效的玩家称号列表 + /// + public static IReadOnlyCollection List => [.. dictionary.Values]; + /// + /// 获取或设置加载称号委托 + /// + public static Func? LoadFunc { get; set; } + /// + /// 获取或设置称号的ID + /// + public uint Id { get; set; } + /// + /// 获取或设置称号的名称 + /// + public string Name { get; set; } + /// + /// 获取或设置称号的颜色 + /// + public string Color { get; set; } + /// + /// 获取或设置称号是否为Rank + /// + public bool IsRank { get; set; } + /// + /// 获取称号的动态指令集 + /// + public List? DynamicCommand { get; private set; } + + /// + /// 构造方法 + /// + /// ID + /// 名称 + /// 颜色 + /// 是否为Rank + /// 动态指令集 + public PlayerTitle(uint id, string name, string color, bool isRank, string? dynamicCommandString = null) + { + Id = id; + Name = name; + Color = color; + IsRank = isRank; + SetDynamicCommand(dynamicCommandString); + } + + /// + /// 设置称号的动态指令集 + /// + /// + public void SetDynamicCommand(string? dynamicCommandString) + { + List? dynamicCommands = null; + if (!string.IsNullOrEmpty(dynamicCommandString)) + { + dynamicCommands = []; + foreach (string dCommand in dynamicCommandString!.Split(';')) + { + dynamicCommands.Add(dCommand.Split(',')); + } + } + DynamicCommand = dynamicCommands; + } + + /// + /// 获取称号 + /// + /// 称号ID + /// 获取的称号 + public static PlayerTitle? Get(uint id) + { + if (LoadFunc is null) + { + Log.Error("称号功能无法在框架内获取,请设置PlayerTitle.LoadFunc属性或写个缓存"); + return null; + } + + if (dictionary.TryGetValue(id, out PlayerTitle? title)) + { + return title; + } + title = LoadFunc.Invoke(id); + if (title != null) dictionary.Add(id, title); + return title; + } + } +} diff --git a/Roles/CustomRolePlus.cs b/Features/Roles/CustomRolePlus.cs similarity index 66% rename from Roles/CustomRolePlus.cs rename to Features/Roles/CustomRolePlus.cs index a1b809b..8001a56 100644 --- a/Roles/CustomRolePlus.cs +++ b/Features/Roles/CustomRolePlus.cs @@ -10,16 +10,20 @@ using PlayerRoles; using System; using System.Collections.Generic; -using System.Reflection; -using System.Runtime.InteropServices; -using YongAnFrame.Players; -using YongAnFrame.Roles.Enums; -using YongAnFrame.Roles.Interfaces; -using YongAnFrame.Roles.Properties; +using System.Diagnostics.CodeAnalysis; +using YongAnFrame.Extensions; +using YongAnFrame.Features.Players; +using YongAnFrame.Features.Roles.Enums; +using YongAnFrame.Features.Roles.Interfaces; +using YongAnFrame.Features.Roles.Properties; +using YongAnFrame.Features.UI.Enums; +using YongAnFrame.Features.UI.Texts; -namespace YongAnFrame.Roles +namespace YongAnFrame.Features.Roles { - [Guid("913613e0-c6e7-4511-a079-bacc7bc0089c")] + /// + /// 高级自定义角色 + /// public abstract class CustomRolePlus : CustomRole { /// @@ -27,6 +31,13 @@ public abstract class CustomRolePlus : CustomRole /// public override bool IgnoreSpawnSystem { get; set; } = false; /// + /// 获取禁用的自定义角色生成 + /// + /// + /// 只能用于继承的类 + /// + public List DisableCustomRolePlusList => YongAnFramePlugin.Instance.Config.DisableCustomRolePlus; + /// /// 获取或设置自定义角色的生成属性 /// public new virtual Properties.SpawnProperties SpawnProperties { get; set; } = new Properties.SpawnProperties(); @@ -34,11 +45,11 @@ public abstract class CustomRolePlus : CustomRole /// 获取或设置自定义角色是否开启生成 /// public bool IsStartSpawn { get; set; } = true; - internal Dictionary BaseData { get; } = []; + internal Dictionary BaseData { get; } = []; /// - /// 获取或设置自定义角色的更多属性 + /// 获取或设置自定义角色的基础属性 /// - public virtual MoreProperties MoreProperties { get; set; } = new MoreProperties(); + public virtual BaseProperties BaseProperties { get; set; } = new BaseProperties(); /// /// 获取或设置自定义角色的名字颜色 /// @@ -54,36 +65,37 @@ public abstract class CustomRolePlus : CustomRole #region Static + /// + /// 获取或设置全局刷新次数 + /// public static int RespawnWave { get; private set; } = 0; + /// + /// 注册全局事件 + /// public static void SubscribeStaticEvents() { Exiled.Events.Handlers.Server.RoundStarted += new CustomEventHandler(OnStaticRoundStarted); Exiled.Events.Handlers.Server.RespawningTeam += new CustomEventHandler(OnStaticRespawningTeam); } + /// + /// 注销全局事件 + /// public static void UnsubscribeStaticEvents() { Exiled.Events.Handlers.Server.RoundStarted -= new CustomEventHandler(OnStaticRoundStarted); Exiled.Events.Handlers.Server.RespawningTeam -= new CustomEventHandler(OnStaticRespawningTeam); } - private static void OnStaticRoundStarted() - { - RespawnWave = 0; - } - private static void OnStaticRespawningTeam(RespawningTeamEventArgs args) - { - RespawnWave++; - } + private static void OnStaticRoundStarted() => RespawnWave = 0; + private static void OnStaticRespawningTeam(RespawningTeamEventArgs args) => RespawnWave++; + #endregion /// - /// 获取这个角色所有自定义角色的属性 + /// 获取这个角色的所有数据 /// /// 获取的值 - public virtual CustomRolePlusProperties[] GetAllProperties() - { - return [.. BaseData.Values]; - } + public virtual CustomRolePlusData[] GetAllData() => [.. BaseData.Values]; /// /// 检查玩家是否拥有该角色 @@ -91,26 +103,17 @@ public virtual CustomRolePlusProperties[] GetAllProperties() /// 框架玩家 /// 返回的数据 /// - public virtual bool Check(FramePlayer player, out CustomRolePlusProperties data) - { - return BaseData.TryGetValue(player, out data); - } + public virtual bool Check(FramePlayer player, out CustomRolePlusData data) => BaseData.TryGetValue(player, out data); /// /// 检查玩家是否拥有该角色 /// /// 框架玩家 - public virtual bool Check(FramePlayer player) - { - return player.CustomRolePlus == this; - } + public virtual bool Check(FramePlayer player) => player.CustomRolePlus == this; /// /// 给玩家添加这个角色 /// /// EX玩家 - public override void AddRole(Player player) - { - AddRole(player.ToFPlayer()); - } + public override void AddRole(Player player) => AddRole(player.ToFPlayer()); /// /// 给玩家添加这个角色 /// @@ -119,40 +122,46 @@ public virtual void AddRole(FramePlayer fPlayer) { if (Check(fPlayer)) return; - Log.Debug($"已添加{fPlayer.ExPlayer.Nickname}的{Name}({Id})角色"); - base.AddRole(fPlayer.ExPlayer); AddRoleData(fPlayer); - if (MoreProperties.BaseMovementSpeedMultiplier < 1f) + if (BaseProperties.BaseMovementSpeedMultiplier < 1f) { - fPlayer.ExPlayer.EnableEffect(Exiled.API.Enums.EffectType.Disabled); - fPlayer.ExPlayer.ChangeEffectIntensity(Exiled.API.Enums.EffectType.Disabled, 1); + fPlayer.ExPlayer.EnableEffect(Exiled.API.Enums.EffectType.Slowness); + fPlayer.ExPlayer.ChangeEffectIntensity(Exiled.API.Enums.EffectType.Slowness, (byte)((1f - BaseProperties.BaseMovementSpeedMultiplier) * 100)); } - if (MoreProperties.BaseMovementSpeedMultiplier > 1f) + if (BaseProperties.BaseMovementSpeedMultiplier > 1f) { fPlayer.ExPlayer.EnableEffect(Exiled.API.Enums.EffectType.MovementBoost); - fPlayer.ExPlayer.ChangeEffectIntensity(Exiled.API.Enums.EffectType.MovementBoost, (byte)((MoreProperties.BaseMovementSpeedMultiplier - 1f) * 100)); + fPlayer.ExPlayer.ChangeEffectIntensity(Exiled.API.Enums.EffectType.MovementBoost, (byte)((BaseProperties.BaseMovementSpeedMultiplier - 1f) * 100)); } if (!string.IsNullOrEmpty(SpawnProperties.Info)) Cassie.MessageTranslated($""/*ADMINISTER TEAM DESIGNATED {CASSIEDeathName} HASENTERED*/, SpawnProperties.Info, true, true, true); - if (!string.IsNullOrEmpty(SpawnProperties.MusicFileName)) + if (!string.IsNullOrEmpty(SpawnProperties.MusicNameName)) { - MusicManager.Instance.Play(SpawnProperties.MusicFileName, $"{Name}"); + MusicManager.Play(SpawnProperties.MusicNameName!, $"{Name}"); } + fPlayer.UpdateShowInfo(); + fPlayer.UI.UpdateCustomRoleUI(); + + Log.Info($"已为{fPlayer.ExPlayer.Nickname}添加{Name}({Id})角色"); } + /// + /// 给玩家添加这个角色的数据 + /// + /// 框架玩家 protected virtual void AddRoleData(FramePlayer fPlayer) { - CustomRolePlusProperties properties = new(); - BaseData.Add(fPlayer, properties); + CustomRolePlusData data = new(); + BaseData.Add(fPlayer, data); if (this is ISkill skill) { - properties.SkillManagers = new SkillManager[skill.SkillProperties.Length]; + data.Skills = new Skill[skill.SkillProperties.Length]; for (int i = 0; i < skill.SkillProperties.Length; i++) { - properties.SkillManagers[i] = new(fPlayer, skill, (byte)i); + data.Skills[i] = new(fPlayer, skill.SkillProperties[i]); } } } @@ -162,11 +171,11 @@ protected virtual void AddRoleData(FramePlayer fPlayer) /// EX玩家 public override void RemoveRole(Player player) { - FramePlayer fPlayer = player.ToFPlayer(); - if (fPlayer != null) + try { RemoveRole(player.ToFPlayer()); } + catch (InvalidCastException){ } } /// /// 给玩家移除这个角色 @@ -174,19 +183,21 @@ public override void RemoveRole(Player player) /// 框架玩家 public virtual void RemoveRole(FramePlayer fPlayer) { - if (!Check(fPlayer)) return; - Log.Debug($"已删除{fPlayer.ExPlayer.Nickname}的{Name}({Id})角色"); - if (Check(fPlayer, out CustomRolePlusProperties data) && !data.IsDeathHandling) + if (Check(fPlayer, out CustomRolePlusData data)) { - Cassie.MessageTranslated($"Died", $"{Name}游玩二游被榨干而死(非常正常死亡)"); + if (!data.IsDeathHandling) + { + Cassie.MessageTranslated($"Died", $"{Name}游玩二游被榨干而死(非常正常死亡)"); + } + base.RemoveRole(fPlayer.ExPlayer); + BaseData.Remove(fPlayer); + fPlayer.UpdateShowInfo(); + Log.Info($"已为{fPlayer.ExPlayer.Nickname}删除{Name}({Id})角色"); } - base.RemoveRole(fPlayer.ExPlayer); - BaseData.Remove(fPlayer); - fPlayer.ExPlayer.ShowHint($"", 0.1f); - fPlayer.UpdateShowInfo(); + } - #region TrySpawn + #region Spawn private uint limitCount = 0; private uint spawnCount = 0; @@ -194,15 +205,10 @@ public virtual void RemoveRole(FramePlayer fPlayer) /// 尝试给这个玩家生成这个角色 /// /// 框架玩家 - /// 是否重置limitCount - /// - public virtual bool TrySpawn(FramePlayer fPlayer, bool chanceRef = false) + /// 是否成功 + public virtual bool TrySpawn(FramePlayer fPlayer) { - if (chanceRef) - { - limitCount = 0; - } - if (fPlayer.CustomRolePlus == null && spawnCount < SpawnProperties.MaxCount && Server.PlayerCount >= SpawnProperties.MinPlayer && SpawnChanceNum <= SpawnProperties.Chance && SpawnProperties.Limit > limitCount) + if (fPlayer.CustomRolePlus is null && ((OldRole != RoleTypeId.None && fPlayer.ExPlayer.Role.Type == OldRole) || (OldRole == RoleTypeId.None && fPlayer.ExPlayer.Role.Type == Role)) && spawnCount < SpawnProperties.MaxCount && Server.PlayerCount >= SpawnProperties.MinPlayer && spawnChanceNum <= SpawnProperties.Chance && SpawnProperties.Limit > limitCount) { limitCount++; spawnCount++; @@ -211,13 +217,6 @@ public virtual bool TrySpawn(FramePlayer fPlayer, bool chanceRef = false) } return false; } - - [Obsolete("旧算法遗留方法,不再进行兼容性维护")] - public virtual bool TrySpawn(List noCustomRole, bool chanceRef = false) - { - if (noCustomRole == null || noCustomRole.Count == 0) { return false; } - return TrySpawn(noCustomRole[Loader.Random.StrictNext(0, noCustomRole.Count)]); - } #endregion #region Events @@ -234,18 +233,22 @@ private void OnRestartingRound() // TrySpawn(NoCustomRole.FindAll((p) => OldRole == RoleTypeId.None && Role == p.ExPlayer.Role.Type || p.ExPlayer.Role.Type == OldRole)); // } //} - public int SpawnChanceNum { get; private set; } = Loader.Random.StrictNext(1, 101); - private void OnStaticRestartingRound() - { - SpawnChanceNum = Loader.Random.StrictNext(1, 101); - } + private int spawnChanceNum = Loader.Random.StrictNext(1, 101); + + private void OnStaticRestartingRound() => spawnChanceNum = Loader.Random.StrictNext(1, 101); - private void OnSpawning(SpawningEventArgs args) + private void OnSpawned(SpawnedEventArgs args) { FramePlayer fPlayer = args.Player.ToFPlayer(); - if (IsStartSpawn && (OldRole != RoleTypeId.None && args.Player.Role.Type == OldRole) || (OldRole == RoleTypeId.None && args.Player.Role.Type == Role)) + + if (fPlayer.ExPlayer.GetCustomRoles().Count > 0) + { + return; + } + + if (IsStartSpawn && !DisableCustomRolePlusList.Contains(Id.ToString())) { switch (SpawnProperties.RefreshTeam) { @@ -265,26 +268,25 @@ private void OnSpawning(SpawningEventArgs args) private void OnDroppingItem(DroppingItemEventArgs args) { FramePlayer fPlayer = args.Player.ToFPlayer(); - if (Check(fPlayer, out CustomRolePlusProperties data)) + if (Check(fPlayer, out CustomRolePlusData data)) { - if (data.SkillManagers != null) + if (data.Skills is not null) { - foreach (var skillsManager in data.SkillManagers) + foreach (var skill in data.Skills) { - if (args.Item.Type == skillsManager.SkillProperties.UseItem) + if (args.Item.Type == skill.UseItem) { - if (skillsManager.IsActive) + if (skill.IsActive) { - fPlayer.HintManager.MessageTexts.Add(new HintManager.Text("技能正在持续", 5)); + fPlayer.UI.MessageList.Add(new MessageText("技能正在持续", 5, MessageType.System)); } - else if (skillsManager.IsBurial) + else if (skill.IsBurial) { - fPlayer.HintManager.MessageTexts.Add(new HintManager.Text($"技能正在冷却(CD:{skillsManager.BurialRemainingTime})", 5)); + fPlayer.UI.MessageList.Add(new MessageText($"技能正在冷却(CD:{skill.BurialRemainingTime})", 5, MessageType.System)); } else { - skillsManager.Run(); - fPlayer.HintManager.MessageTexts.Add(new HintManager.Text($"技能[{skillsManager.SkillProperties.Name}]已经发动,持续时间:{skillsManager.SkillProperties.ActiveMaxTime}", skillsManager.SkillProperties.ActiveMaxTime)); + skill.Run(); } args.IsAllowed = false; } @@ -294,24 +296,24 @@ private void OnDroppingItem(DroppingItemEventArgs args) } private void OnHurting(HurtingEventArgs args) { - if (args.Attacker != null && args.Player != null) + if (args.Attacker is not null && args.Player is not null) { if (Check(args.Player)) { - args.Amount *= MoreProperties.DamageResistanceMultiplier; + args.Amount *= BaseProperties.DamageResistanceMultiplier; } else if (Check(args.Attacker)) { DamageHandler damageHandler = args.DamageHandler; - float damage = damageHandler.Damage * MoreProperties.AttackDamageMultiplier; - if (MoreProperties.IsAttackIgnoresArmor) + float damage = damageHandler.Damage * BaseProperties.AttackDamageMultiplier; + if (BaseProperties.IsAttackIgnoresArmor) { if (damageHandler is FirearmDamageHandler firearmDamageHandler) { damage += ((Exiled.API.Features.Roles.HumanRole)damageHandler.Target.Role).GetArmorEfficacy(firearmDamageHandler.Hitbox); } } - if (MoreProperties.IsAttackIgnoresAhp) + if (BaseProperties.IsAttackIgnoresAhp) { damage += damageHandler.AbsorbedAhpDamage; } @@ -331,13 +333,12 @@ private void OnHurting(HurtingEventArgs args) } } } - private void OnDying(DyingEventArgs args) { FramePlayer fPlayer = args.Player.ToFPlayer(); - if (Check(fPlayer, out CustomRolePlusProperties data)) + if (Check(fPlayer, out CustomRolePlusData data)) { - if (args.Attacker == null) + if (args.Attacker is null) { Cassie.MessageTranslated($"Died", $"{Name}被充满恶意的游戏环境草飞了"); data.IsDeathHandling = true; @@ -364,11 +365,13 @@ private void OnDying(DyingEventArgs args) data.IsDeathHandling = true; } } - + /// + /// 注册事件 + /// protected override void SubscribeEvents() { //Exiled.Events.Handlers.Server.RoundStarted += new CustomEventHandler(OnRoundStarted); - Exiled.Events.Handlers.Player.Spawning += new CustomEventHandler(OnSpawning); + Exiled.Events.Handlers.Player.Spawned += new CustomEventHandler(OnSpawned); Exiled.Events.Handlers.Player.Hurting += new CustomEventHandler(OnHurting); Exiled.Events.Handlers.Server.RestartingRound += new CustomEventHandler(OnRestartingRound); Exiled.Events.Handlers.Player.DroppingItem += new CustomEventHandler(OnDroppingItem); @@ -380,15 +383,17 @@ protected override void SubscribeEvents() { Inventory.Add(ItemType.Coin.ToString()); } - } + /// + /// 注销事件 + /// protected override void UnsubscribeEvents() { //Exiled.Events.Handlers.Server.RoundStarted -= new CustomEventHandler(OnRoundStarted); Exiled.Events.Handlers.Player.Hurting -= new CustomEventHandler(OnHurting); Exiled.Events.Handlers.Server.RestartingRound -= new CustomEventHandler(OnRestartingRound); Exiled.Events.Handlers.Player.DroppingItem -= new CustomEventHandler(OnDroppingItem); - Exiled.Events.Handlers.Player.Spawning -= new CustomEventHandler(OnSpawning); + Exiled.Events.Handlers.Player.Spawned -= new CustomEventHandler(OnSpawned); Exiled.Events.Handlers.Player.Dying -= new CustomEventHandler(OnDying); Exiled.Events.Handlers.Server.RestartingRound -= new CustomEventHandler(OnStaticRestartingRound); base.UnsubscribeEvents(); @@ -400,24 +405,31 @@ protected override void UnsubscribeEvents() } #endregion + /// + /// 不要覆写 + /// + /// EX玩家 protected override void ShowMessage(Player player) { } } - [Guid("913613e0-c6e7-4511-a079-bacc7bc9000c")] - public abstract class CustomRolePlus : CustomRolePlus where T : CustomRolePlusProperties, new() + /// + /// 带有自定义数据的高级自定义角色 + /// + /// 自定义数据 + public abstract class CustomRolePlus : CustomRolePlus where T : CustomRolePlusData, new() { /// /// 检查玩家是否拥有该角色 /// /// 框架玩家 /// 返回的数据 - /// - public virtual bool Check(FramePlayer player, out T data) + /// 是否拥有该角色 + public virtual bool Check(FramePlayer player, out T? data) { - if (BaseData.TryGetValue(player, out CustomRolePlusProperties baseData)) + if (BaseData.TryGetValue(player, out CustomRolePlusData baseData)) { data = (T)baseData; return true; @@ -426,16 +438,17 @@ public virtual bool Check(FramePlayer player, out T data) return false; } + /// protected override void AddRoleData(FramePlayer fPlayer) { T properties = new(); BaseData.Add(fPlayer, properties); if (this is ISkill skill) { - properties.SkillManagers = new SkillManager[skill.SkillProperties.Length]; + properties.Skills = new Skill[skill.SkillProperties.Length]; for (int i = 0; i < skill.SkillProperties.Length; i++) { - properties.SkillManagers[i] = new(fPlayer, skill, (byte)i); + properties.Skills[i] = new(fPlayer, skill.SkillProperties[i]); } } } diff --git a/Features/Roles/CustomRolePlusData.cs b/Features/Roles/CustomRolePlusData.cs new file mode 100644 index 0000000..9e5fbf6 --- /dev/null +++ b/Features/Roles/CustomRolePlusData.cs @@ -0,0 +1,17 @@ +namespace YongAnFrame.Features.Roles +{ + /// + /// 的数据 + /// + public class CustomRolePlusData + { + /// + /// 获取或设置技能 + /// + public Skill[]? Skills { get; internal set; } + /// + /// 获取或设置是否正常死亡 + /// + public bool IsDeathHandling { get; set; } + } +} diff --git a/Features/Roles/Enums/RefreshTeamType.cs b/Features/Roles/Enums/RefreshTeamType.cs new file mode 100644 index 0000000..88e34c9 --- /dev/null +++ b/Features/Roles/Enums/RefreshTeamType.cs @@ -0,0 +1,21 @@ +namespace YongAnFrame.Features.Roles.Enums +{ + /// + /// 的刷新队伍类型 + /// + public enum RefreshTeamType + { + /// + /// 初始生成 + /// + Start = 0, + /// + /// 九尾狐 + /// + MTF = 1, + /// + /// 混沌分裂者 + /// + CI = 2, + } +} diff --git a/Features/Roles/Interfaces/ISkill.cs b/Features/Roles/Interfaces/ISkill.cs new file mode 100644 index 0000000..71bc6e6 --- /dev/null +++ b/Features/Roles/Interfaces/ISkill.cs @@ -0,0 +1,18 @@ +using YongAnFrame.Features.Roles.Properties; + +namespace YongAnFrame.Features.Roles.Interfaces +{ + /// + /// 技能接口 + /// + /// + /// 所有有技能的自定义角色必须实现此接口 + /// + public interface ISkill + { + /// + /// 获取技能属性 + /// + SkillProperties[] SkillProperties { get; } + } +} diff --git a/Roles/Properties/MoreProperties.cs b/Features/Roles/Properties/BaseProperties.cs similarity index 81% rename from Roles/Properties/MoreProperties.cs rename to Features/Roles/Properties/BaseProperties.cs index ed3ff56..c7fc4dc 100644 --- a/Roles/Properties/MoreProperties.cs +++ b/Features/Roles/Properties/BaseProperties.cs @@ -1,6 +1,9 @@ -namespace YongAnFrame.Roles.Properties +namespace YongAnFrame.Features.Roles.Properties { - public struct MoreProperties + /// + /// 的基础属性 + /// + public struct BaseProperties() { /// /// 获取或设置伤害加成倍数 @@ -22,9 +25,5 @@ public struct MoreProperties /// 获取或设置基础移动速度倍数 /// public float BaseMovementSpeedMultiplier { get; set; } = 1; - - public MoreProperties() - { - } } } diff --git a/Features/Roles/Properties/SkillProperties.cs b/Features/Roles/Properties/SkillProperties.cs new file mode 100644 index 0000000..25e5eaa --- /dev/null +++ b/Features/Roles/Properties/SkillProperties.cs @@ -0,0 +1,60 @@ +using static YongAnFrame.Features.Roles.Skill; + +namespace YongAnFrame.Features.Roles.Properties +{ + /// + /// 的原始技能属性 + /// + /// + /// 你无法修改结构体里的任何对象,如果想修改对象请从对象修改,因为要保留技能的原始属性,从而保证可以恢复到原始属性 + /// + /// 名称 + /// 发动描述 + /// 介绍 + /// 最大作用时间 + /// 最大冷却时间 + /// 激活开始委托 + /// 激活结束委托 + /// 冷却结束委托 + /// 绑定物品 + public readonly struct SkillProperties(string name, string statement, string description, float activeMaxTime, float burialMaxTime, + ActiveStart? activeStart = null, ActiveEnd? activeEnd = null, BurialEnd? burialEnd = null, ItemType useItem = ItemType.Coin) + { + /// + /// 获取名称 + /// + public string Name { get; } = name; + /// + /// 获取绑定物品 + /// + public ItemType UseItem { get; } = useItem; + /// + /// 获取发动描述 + /// + public string Statement { get; } = statement; + /// + /// 获取介绍 + /// + public string Description { get; } = description; + /// + /// 获取最大作用时间 + /// + public float ActiveMaxTime { get; } = activeMaxTime; + /// + /// 获取最大冷却时间 + /// + public float BurialMaxTime { get; } = burialMaxTime; + /// + /// 获取激活开始委托 + /// + public ActiveStart? ActiveStartAction { get; } = activeStart; + /// + /// 获取激活结束委托 + /// + public ActiveEnd? ActiveEndAction { get; } = activeEnd; + /// + /// 获取冷却结束委托 + /// + public BurialEnd? BurialEndAction { get; } = burialEnd; + } +} diff --git a/Roles/Properties/SpawnProperties.cs b/Features/Roles/Properties/SpawnProperties.cs similarity index 72% rename from Roles/Properties/SpawnProperties.cs rename to Features/Roles/Properties/SpawnProperties.cs index 2467f94..632ea3a 100644 --- a/Roles/Properties/SpawnProperties.cs +++ b/Features/Roles/Properties/SpawnProperties.cs @@ -1,13 +1,12 @@ -using YongAnFrame.Roles.Enums; +using YongAnFrame.Features.Roles.Enums; -namespace YongAnFrame.Roles.Properties +namespace YongAnFrame.Features.Roles.Properties { - public struct SpawnProperties + /// + /// 的生成属性 + /// + public struct SpawnProperties() { - public SpawnProperties() - { - } - /// /// 获取或设置每次生成的最多数量 /// @@ -23,7 +22,7 @@ public SpawnProperties() /// /// 获取或设置生成时播放音频文件 /// - public string MusicFileName { get; set; } = null; + public string? MusicNameName { get; set; } = null; /// /// 获取或设置生成时跟随的队伍 /// @@ -31,17 +30,17 @@ public SpawnProperties() /// /// 暂时弃用 /// - public string Info { get; set; } = null; + public string? Info { get; set; } = null; /// - /// 获取或设置生成的数量限制 + /// 获取或设置生成数量限制 /// public uint Limit { get; set; } = 1; /// - /// 获取或设置每次生成的概率 + /// 获取或设置生成概率 /// public float Chance { get; set; } = 100; /// - /// 获取或设置的刷新波次 + /// 获取或设置刷新波次 /// /// /// 只适用于除 以外的所有内容 diff --git a/Features/Roles/Skill.cs b/Features/Roles/Skill.cs new file mode 100644 index 0000000..7edc26f --- /dev/null +++ b/Features/Roles/Skill.cs @@ -0,0 +1,152 @@ +using MEC; +using System.Collections.Generic; +using YongAnFrame.Features.Players; +using YongAnFrame.Features.Roles.Properties; +using YongAnFrame.Features.UI.Enums; +using YongAnFrame.Features.UI.Texts; + +namespace YongAnFrame.Features.Roles +{ + /// + /// 的技能 + /// + /// 框架玩家 + /// 技能原始属性 + public class Skill(FramePlayer fPlayer, SkillProperties properties) + { + /// + /// 激活开始 + /// + /// 播放音乐文件名称 + public delegate string? ActiveStart(FramePlayer fPlayer); + /// + /// 激活结束 + /// + /// 播放音乐文件名称 + public delegate string? ActiveEnd(FramePlayer fPlayer); + /// + /// 冷却结束 + /// + /// 播放音乐文件名称 + public delegate string? BurialEnd(FramePlayer fPlayer); + /// + /// 获取原始属性 + /// + public SkillProperties Properties { get; } = properties; + /// + /// 获取名称 + /// + public string Name { get; set; } = properties.Name; + /// + /// 获取绑定物品 + /// + public ItemType UseItem => Properties.UseItem; + /// + /// 获取发动描述 + /// + public string? Statement { get; set; } = properties.Statement; + /// + /// 获取介绍 + /// + public string Description { get; set; } = properties.Description; + /// + /// 获取最大作用时间 + /// + public float ActiveMaxTime { get; set; } = properties.ActiveMaxTime; + /// + /// 获取最大冷却时间 + /// + public float BurialMaxTime { get; set; } = properties.BurialMaxTime; + /// + /// 获取激活开始委托 + /// + public ActiveStart? ActiveStartAction { get; set; } = properties.ActiveStartAction; + /// + /// 获取激活结束委托 + /// + public ActiveEnd? ActiveEndAction { get; set; } = properties.ActiveEndAction; + /// + /// 获取冷却结束委托 + /// + public BurialEnd? BurialEndAction { get; set; } = properties.BurialEndAction; + /// + /// 获取是否激活 + /// + public bool IsActive { get => ActiveRemainingTime > 0; } + /// + /// 获取是否冷却 + /// + public bool IsBurial { get => BurialRemainingTime > 0; } + /// + /// 获取行动时间 + /// + public float ActiveRemainingTime { get; private set; } + /// + /// 获取冷却时间 + /// + public float BurialRemainingTime { get; private set; } + + private CoroutineHandle coroutineHandle; + + + /// + /// 使用技能 + /// + /// + /// 有计时任务会直接覆盖 + /// + public void Run() + { + if (coroutineHandle.IsValid) + { + Timing.KillCoroutines(coroutineHandle); + } + + ActiveRemainingTime = ActiveMaxTime; + BurialRemainingTime = BurialMaxTime; + + coroutineHandle = Timing.RunCoroutine(Timer()); + fPlayer.UI.MessageList.Add(new MessageText($"{(string.IsNullOrEmpty(Statement) ? $"技能[{Name}]已经发动" : $"{Name}:{Statement}")}(持续时间:{ActiveMaxTime})", ActiveMaxTime, MessageType.System)); + } + + /// + /// 还原技能 + /// + public void Restore() + { + Name = Properties.Name; + Statement = Properties.Statement; + Description = Properties.Description; + ActiveMaxTime = Properties.ActiveMaxTime; + BurialMaxTime = Properties.BurialMaxTime; + ActiveStartAction = Properties.ActiveStartAction; + ActiveEndAction = Properties.ActiveEndAction; + BurialEndAction = Properties.BurialEndAction; + if (coroutineHandle.IsValid) + { + Timing.KillCoroutines(coroutineHandle); + } + fPlayer.UI.MessageList.Add(new MessageText($"技能[{Name}]被其他人影响,技能信息全部重置", 10, MessageType.System)); + } + + private IEnumerator Timer() + { + string? musicName = ActiveStartAction?.Invoke(fPlayer); + if (musicName is not null) MusicManager.Play(musicName, $"技能发动语音", fPlayer, 10); + while (IsActive) + { + ActiveRemainingTime--; + yield return Timing.WaitForSeconds(1f); + } + musicName = ActiveEndAction?.Invoke(fPlayer); + if (musicName is not null) MusicManager.Play(musicName, $"技能结束语音", fPlayer, 10); + while (IsBurial) + { + BurialRemainingTime--; + yield return Timing.WaitForSeconds(1f); + } + musicName = BurialEndAction?.Invoke(fPlayer); + if (musicName is not null) MusicManager.Play(musicName, $"技能准备好语音", fPlayer, 10); + } + } +} diff --git a/Features/UI/Enums/ChatType.cs b/Features/UI/Enums/ChatType.cs new file mode 100644 index 0000000..9078c46 --- /dev/null +++ b/Features/UI/Enums/ChatType.cs @@ -0,0 +1,27 @@ +using YongAnFrame.Features.UI.Texts; + +namespace YongAnFrame.Features.UI.Enums +{ + /// + /// 的类型 + /// + public enum ChatType : byte + { + /// + /// 未知 + /// + Unknown = 0, + /// + /// 全部 + /// + All = 1, + /// + /// 队伍 + /// + Team = 2, + /// + /// 私聊 + /// + Private = 3, + } +} diff --git a/Features/UI/Enums/MessageType.cs b/Features/UI/Enums/MessageType.cs new file mode 100644 index 0000000..73c03ad --- /dev/null +++ b/Features/UI/Enums/MessageType.cs @@ -0,0 +1,35 @@ +using YongAnFrame.Features.UI.Texts; + +namespace YongAnFrame.Features.UI.Enums +{ + /// + /// 的类型 + /// + public enum MessageType : byte + { + /// + /// 未知 + /// + Unknown = 0, + /// + /// 管理员 + /// + Admin = 1, + /// + /// 反馈 + /// + Feedback = 2, + /// + /// 系统 + /// + System = 3, + /// + /// 安全 + /// + Safety = 4, + /// + /// 异常 + /// + Abnormal = 5 + } +} diff --git a/Features/UI/Texts/ChatText.cs b/Features/UI/Texts/ChatText.cs new file mode 100644 index 0000000..055ec92 --- /dev/null +++ b/Features/UI/Texts/ChatText.cs @@ -0,0 +1,52 @@ +using YongAnFrame.Features.Players; +using YongAnFrame.Features.UI.Enums; + +namespace YongAnFrame.Features.UI.Texts +{ + /// + /// 的消息文本 + /// + /// 内容 + /// 时效 + /// 聊天类型 + /// 发送者(null时是匿名) + public class ChatText(string text, float duration, ChatType type = ChatType.Unknown, FramePlayer? player = null) : Text(text, duration) + { + /// + /// 获取聊天类型 + /// + public ChatType Type { get; } = type; + /// + public override string ToString() + { + string text = "Error"; + switch (Type) + { + case ChatType.Unknown: + text = $"[未知]|[{(player is null ? $"匿名" : $"{player.ExPlayer.Nickname}({player.ExPlayer.Role.Team})")}]:{Content}"; + break; + case ChatType.All: + text = $"[全部]|[{(player is null ? $"匿名" : $"{player.ExPlayer.Nickname}({player.ExPlayer.Role.Team})")}]:{Content}"; + break; + case ChatType.Team: + text = $"[队伍]|[{(player is null ? $"匿名" : $"{player.ExPlayer.Nickname}({player.ExPlayer.Role.Team})")}]:{Content}"; + break; + case ChatType.Private: + text = $"[私聊]|[{(player is null ? $"匿名" : $"{player.ExPlayer.Nickname}({player.ExPlayer.Role.Team})")}]:{Content}"; + break; + } + return text; + } + + /// + /// 隐性转换 + /// + /// 准备转换的对象 + public static implicit operator string(ChatText text) => text.ToString(); + /// + /// 隐性转换 + /// + /// 准备转换的对象 + public static implicit operator ChatText(string text) => new(text, 60); + } +} diff --git a/Features/UI/Texts/MessageText.cs b/Features/UI/Texts/MessageText.cs new file mode 100644 index 0000000..fab6a79 --- /dev/null +++ b/Features/UI/Texts/MessageText.cs @@ -0,0 +1,56 @@ +using YongAnFrame.Features.UI.Enums; + +namespace YongAnFrame.Features.UI.Texts +{ + /// + /// 的消息文本 + /// + /// 内容 + /// 时效 + /// 信息类型 + public class MessageText(string text, float duration, MessageType type = MessageType.Unknown) : Text(text, duration) + { + /// + /// 获取信息类型 + /// + public MessageType Type { get; } = type; + + /// + public override string ToString() + { + string text = "Error"; + switch (Type) + { + case MessageType.Unknown: + text = $"[{Duration}][未知] {text}"; + break; + case MessageType.Admin: + text = $"[{Duration}][管理员] {Content}"; + break; + case MessageType.Feedback: + text = $"[{Duration}][玩家反馈] {Content}"; + break; + case MessageType.System: + text = $"[{Duration}][系统] {Content}"; + break; + case MessageType.Safety: + text = $"[{Duration}][安全] {Content}"; + break; + case MessageType.Abnormal: + text = $"[{Duration}][异常] {Content}"; + break; + } + return text; + } + /// + /// 隐性转换 + /// + /// 准备转换的对象 + public static implicit operator string(MessageText text) => text.ToString(); + /// + /// 隐性转换 + /// + /// 准备转换的对象 + public static implicit operator MessageText(string text) => new(text, -1); + } +} diff --git a/Features/UI/Texts/Text.cs b/Features/UI/Texts/Text.cs new file mode 100644 index 0000000..ad18f9f --- /dev/null +++ b/Features/UI/Texts/Text.cs @@ -0,0 +1,31 @@ +namespace YongAnFrame.Features.UI.Texts +{ + /// + /// 的基础文本 + /// + /// 内容 + /// 时效 + public class Text(string text, float duration) + { + /// + /// 获取内容 + /// + public string Content { get; } = text; + /// + /// 获取时效 + /// + public float Duration { get; internal set; } = duration; + /// + public override string ToString() => Content; + /// + /// 隐性转换 + /// + /// 准备转换的对象 + public static implicit operator string(Text text) => text.ToString(); + /// + /// 隐性转换 + /// + /// 准备转换的对象 + public static implicit operator Text(string text) => new(text, 60); + } +} diff --git a/Patch/AddLogPatch.cs b/Patch/AddLogPatch.cs new file mode 100644 index 0000000..6231603 --- /dev/null +++ b/Patch/AddLogPatch.cs @@ -0,0 +1,65 @@ +using HarmonyLib; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using YongAnFrame.Features; + +namespace YongAnFrame.Patch +{ + /// + /// 在添加的补丁 + /// + [HarmonyPatch(typeof(ServerConsole), nameof(ServerConsole.AddLog))] + public static class AddLogPatch + { +#pragma warning disable IDE0060 // 删除未使用的参数 + private static void Prefix(string q, ConsoleColor color = ConsoleColor.Gray, bool hideFromOutputs = false) +#pragma warning restore IDE0060 // 删除未使用的参数 + { + SaveLog(q, new StackTrace()); + } + + private static readonly Queue logQueue = new(); + private static readonly Task logTask = new(async () => + { + while (true) + { + while (logQueue.Count != 0) + { + InfoData infoData = logQueue.Dequeue(); + using StreamWriter writer = new($"{PathManager.Log}/{DateTime.Now:yyyy-MM-dd}.log", true, Encoding.UTF8); + writer.WriteLine(infoData); + } + await Task.Delay(1000); + } + }); + + /// + /// 启动日志任务 + /// + public static void StartTask() + { + if (logTask.Status == TaskStatus.Created) + { + logTask.Start(); + } + } + + private static void SaveLog(string log, StackTrace trace) + { + logQueue.Enqueue(new InfoData(log, trace)); + } + + private readonly struct InfoData(string content, StackTrace trace) + { + public string Content { get; } = content; + public StackTrace StackTrace { get; } = trace; + public override readonly string ToString() => $"{Content}::{StackTrace}"; + public static implicit operator string(InfoData data) => data.ToString(); + } + } +} diff --git a/Players/CustomPlayer.cs b/Players/CustomPlayer.cs deleted file mode 100644 index 9b60650..0000000 --- a/Players/CustomPlayer.cs +++ /dev/null @@ -1,43 +0,0 @@ -using YongAnFrame.Roles; - -namespace YongAnFrame.Players -{ - public abstract class CustomPlayer(FramePlayer player) - { - /// - /// 获取拥有该实例的 - /// - public FramePlayer FramePlayer { get; private set; } = player; - /// - public bool IsInvalid => FramePlayer == null; - /// - public CustomRolePlus CustomRolePlus => FramePlayer.CustomRolePlus; - /// - public HintManager HintManager => FramePlayer.HintManager; - /// - public ICustomAlgorithm CustomAlgorithm { get => FramePlayer.CustomAlgorithm; set => FramePlayer.CustomAlgorithm = value; } - /// - public ulong Level { get => FramePlayer.Level; set => FramePlayer.Level = value; } - /// - public ulong Exp { get => FramePlayer.Exp; set => FramePlayer.Exp = value; } - /// - public float ExpMultiplier { get => FramePlayer.ExpMultiplier; set => FramePlayer.ExpMultiplier = value; } - /// - public bool IsBDNT { get => FramePlayer.IsBDNT; set => FramePlayer.IsBDNT = value; } - /// - public PlayerTitle UsingTitles { get => FramePlayer.UsingTitles; set => FramePlayer.UsingTitles = value; } - /// - public PlayerTitle UsingRankTitles { get => FramePlayer.UsingRankTitles; set => FramePlayer.UsingRankTitles = value; } - - /// - public void AddExp(ulong exp, string name = "未知原因") => FramePlayer.AddExp(exp, name); - - public void UpdateShowInfoList() => FramePlayer.UpdateShowInfo(); - - /// - public virtual void Invalid() - { - FramePlayer = null; - } - } -} diff --git a/Players/HintManager.cs b/Players/HintManager.cs deleted file mode 100644 index a7eb021..0000000 --- a/Players/HintManager.cs +++ /dev/null @@ -1,132 +0,0 @@ -using MEC; -using System.Collections.Generic; -using YongAnFrame.Components; - -namespace YongAnFrame.Players -{ - /// - /// 提示系统管理器 - /// - public sealed class HintManager - { - private readonly FramePlayer fPlayer; - private readonly CoroutineHandle coroutine; - - public Text[] CustomText = new Text[20]; - public CapacityList MessageTexts { get; } = new(7); - public CapacityList ChatTexts { get; } = new(6); - public HintManager(FramePlayer player) - { - fPlayer = player; - coroutine = Timing.RunCoroutine(Update()); - } - - private IEnumerator Update() - { - while (true) - { - CustomText = new Text[20]; - Events.Handlers.FramePlayer.OnFramerHintUpdate(); - string[] text = new string[36]; - - int used = 0; - text[used] = $"YongAnFrame 1.0.0-Beta5"; - - if (fPlayer.ExPlayer.DoNotTrack && !fPlayer.IsBDNT) - { - text[used] = "[注意]已开启DoNotTrack(DNT),游戏数据不会被保存,想保存数据请控制台输入pl BDNT查看详情"; - } - - used = 1; - text[used] = ""; - - for (int i = 0; i < ChatTexts.Capacity; i++) - { - Text chatText = ChatTexts[i]; - if (chatText != null) - { - text[used] += chatText; - chatText.Duration--; - - if (chatText.Duration <= 0) - { - ChatTexts.Remove(chatText); - i--; - } - } - else - { - text[used] += Text.Empty; - } - used++; - } - text[used] = ""; - - foreach (Text data in CustomText) - { - text[used] += data ?? Text.Empty; - used++; - } - - for (int i = 0; i < MessageTexts.Capacity; i++) - { - Text messageText = MessageTexts[i]; - if (messageText != null) - { - text[used] = $"[{messageText.Duration}]{messageText}"; - - messageText.Duration--; - if (messageText.Duration <= 0) - { - MessageTexts.Remove(messageText); - i--; - } - } - else - { - text[used] += Text.Empty; - } - used++; - } - text[34] = ""; - - if (fPlayer.CustomRolePlus != null) - { - text[34] += $"{fPlayer.CustomRolePlus.Name}"; - text[35] = fPlayer.CustomRolePlus.Description; - } - fPlayer.ExPlayer.ShowHint($"{string.Join("\n", text)}\n\n\n\n\n\n\n\n\n\n\n\n\n\n", 2f); - yield return Timing.WaitForSeconds(1f); - } - } - - /// - /// 立刻停用这个提示系统管理器 - /// - public void Clean() - { - Timing.KillCoroutines(coroutine); - } - - public class Text(string text, float duration) - { - public string Content { get; private set; } = text; - public float Duration { get; internal set; } = duration; - - public static string Empty => "占"; - - public override string ToString() - { - return Content; - } - public static implicit operator string(Text text) - { - return text.ToString(); - } - public static implicit operator Text(string text) - { - return new Text(text, -1); - } - } - } -} diff --git a/Players/PlayerTitle.cs b/Players/PlayerTitle.cs deleted file mode 100644 index 46f0e42..0000000 --- a/Players/PlayerTitle.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Collections.Generic; - -namespace YongAnFrame.Players -{ - /// - /// 玩家称号 - /// - public sealed class PlayerTitle - { - /// - /// 获取或设置称号的ID - /// - public uint Id { get; set; } - /// - /// 获取或设置称号的名称 - /// - public string Name { get; set; } - /// - /// 获取或设置称号的颜色 - /// - public string Color { get; set; } - /// - /// 获取或设置称号是否为Rank - /// - public bool IsRank { get; set; } - /// - /// 获取称号的动态指令集 - /// - public List DynamicCommand { get; private set; } - - public PlayerTitle(uint id, string name, string color, bool isRank, string dynamicCommandString) - { - Id = id; - Name = name; - Color = color; - IsRank = isRank; - SetDynamicCommand(dynamicCommandString); - } - - /// - /// 设置称号的动态指令集 - /// - /// - public void SetDynamicCommand(string dynamicCommandString) - { - List dynamicCommands = null; - if (!string.IsNullOrEmpty(dynamicCommandString)) - { - dynamicCommands = []; - foreach (string dCommand in dynamicCommandString.Split(';')) - { - dynamicCommands.Add(dCommand.Split(',')); - } - } - DynamicCommand = dynamicCommands; - } - } -} diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs deleted file mode 100644 index ca90715..0000000 --- a/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// 有关程序集的一般信息由以下 -// 控制。更改这些特性值可修改 -// 与程序集关联的信息。 -[assembly: AssemblyTitle("YongAnFrame")] -[assembly: AssemblyDescription("基于EXILED写的框架")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("YongAn404")] -[assembly: AssemblyProduct("YongAnFrame")] -[assembly: AssemblyCopyright("Copyright © YongAn404")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("zh-cn")] - -// 将 ComVisible 设置为 false 会使此程序集中的类型 -//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型 -//请将此类型的 ComVisible 特性设置为 true。 -[assembly: ComVisible(false)] - -// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID -[assembly: Guid("913613e0-c6e7-4511-a079-bacc7bc9089c")] - -// 程序集的版本信息由下列四个值组成: -// -// 主版本 -// 次版本 -// 生成号 -// 修订号 -// -//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值 -//通过使用 "*",如下所示: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.13")] -[assembly: AssemblyFileVersion("1.0.0.13")] diff --git a/README.md b/README.md index b3e98b3..051e850 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ # YongAnFrame [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FSCP-SL-Plugin-YongAnTeam%2FYongAnFrame.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FSCP-SL-Plugin-YongAnTeam%2FYongAnFrame?ref=badge_shield) -基于[EXILED](https://github.com/ExMod-Team/EXILED)写的框架,为国内服务器常见的插件需求简化开发 +基于[EXILED](https://github.com/ExSLMod-Team/EXILED)写的框架,为国内服务器常见的插件需求简化开发 ## YongAnFrame是什么类型的插件/框架? -**YongAnFrame** 是一个集成了命令系统、整合了自定义角色及技能系统、称号系统、提示系统的免费框架,使用 **[EXILED](https://github.com/ExMod-Team/EXILED)** 开发。\ +**YongAnFrame** 是一个集成了命令系统、整合了自定义角色及技能系统、称号系统、UI系统等的免费框架,使用 **[EXILED](https://github.com/ExMod-Team/EXILED)** 开发。\ 您可以通过安装该插件并使用该框架开发插件,简化您的开发流程。 ## 功能 - 称号系统:提供动态指令运行集 - DNT检测和BDNT(Bypass DNT)请求:使信息获取和保存符合VSR规则 -- 提示系统:提供可方便调用显示提示,拒绝冲突 +- UI系统:使用[HSM](https://github.com/MeowServer/HintServiceMeow)预制一些比较常用的UI - 自定义角色:提供对[EXILED](https://github.com/ExMod-Team/EXILED)的自定义角色升级,部分兼容[EXILED](https://github.com/ExMod-Team/EXILED)自定义角色 - 等级系统:提供整合统一的等级API,支持自定义算法 - 自定义算法:提供一个可替换主要算法的属性(该功能不支持多元化,只能采用一个主要算法) -- 音频API:提供方便更好调用SCPSLAudioApi +- 音频API:提供更方便调用SCPSLAudioApi ## 为服务端安装框架 如图,下载 **Release下的最新压缩包** 全部解压并合并到**EXILED目录**,如C:\Users\Administrator\Appdata\Roaming\ ,安装后请启动一次服务端,并在EXILED\Config内根据提示调整配置文件. diff --git a/Roles/Enums/RefreshTeamType.cs b/Roles/Enums/RefreshTeamType.cs deleted file mode 100644 index c7b307f..0000000 --- a/Roles/Enums/RefreshTeamType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace YongAnFrame.Roles.Enums -{ - public enum RefreshTeamType - { - Start = 0, - MTF = 1, - CI = 2, - } -} diff --git a/Roles/Interfaces/ISkill.cs b/Roles/Interfaces/ISkill.cs deleted file mode 100644 index c700fb8..0000000 --- a/Roles/Interfaces/ISkill.cs +++ /dev/null @@ -1,12 +0,0 @@ -using YongAnFrame.Roles.Properties; - -namespace YongAnFrame.Roles.Interfaces -{ - public interface ISkill - { - /// - /// 技能属性 - /// - SkillProperties[] SkillProperties { get; } - } -} diff --git a/Roles/Interfaces/ISkillActiveEnd.cs b/Roles/Interfaces/ISkillActiveEnd.cs deleted file mode 100644 index b7870e4..0000000 --- a/Roles/Interfaces/ISkillActiveEnd.cs +++ /dev/null @@ -1,14 +0,0 @@ -using YongAnFrame.Players; - -namespace YongAnFrame.Roles.Interfaces -{ - public interface ISkillActiveEnd : ISkill - { - /// - /// 行动结束 - /// - /// - /// 方法的音乐文件名称 - string ActiveEnd(FramePlayer yPlayer, byte id); - } -} diff --git a/Roles/Interfaces/ISkillActiveStart.cs b/Roles/Interfaces/ISkillActiveStart.cs deleted file mode 100644 index c009897..0000000 --- a/Roles/Interfaces/ISkillActiveStart.cs +++ /dev/null @@ -1,14 +0,0 @@ -using YongAnFrame.Players; - -namespace YongAnFrame.Roles.Interfaces -{ - public interface ISkillActiveStart : ISkill - { - /// - /// 行动开始 - /// - /// - /// 方法的音乐文件名称 - string ActiveStart(FramePlayer yPlayer, byte id); - } -} diff --git a/Roles/Interfaces/ISkillBurialEnd.cs b/Roles/Interfaces/ISkillBurialEnd.cs deleted file mode 100644 index 96af3da..0000000 --- a/Roles/Interfaces/ISkillBurialEnd.cs +++ /dev/null @@ -1,14 +0,0 @@ -using YongAnFrame.Players; - -namespace YongAnFrame.Roles.Interfaces -{ - public interface ISkillBurialEnd : ISkill - { - /// - /// 冷却结束 - /// - /// - /// 方法的音乐文件名称 - string BurialEnd(FramePlayer yPlayer, byte id); - } -} diff --git a/Roles/MusicManager.cs b/Roles/MusicManager.cs deleted file mode 100644 index f1fd2cd..0000000 --- a/Roles/MusicManager.cs +++ /dev/null @@ -1,248 +0,0 @@ -using Exiled.API.Features; -using Exiled.API.Features.Components; -using Mirror; -using SCPSLAudioApi.AudioCore; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using UnityEngine; -using YongAnFrame.Players; -using static SCPSLAudioApi.AudioCore.AudioPlayerBase; - -namespace YongAnFrame.Roles -{ - /// - /// 一个通用的音频控制器 - /// - public sealed class MusicManager - { - private static readonly MusicManager instance = new(); - - private uint num = 0; - /// - /// 获取单例 - /// - public static MusicManager Instance => instance; - /// - /// 获取或设置放音频的玩家(NPC) - /// - public Dictionary MusicNpc { get; } = []; - private MusicManager() { } - - internal void Init() - { - OnFinishedTrack += TrackFinished; - Log.Info("MusicManager----------OK"); - } - - private void TrackFinished(AudioPlayerBase playerBase, string track, bool directPlay, ref int nextQueuePos) - { - Stop(playerBase); - } - - private ReferenceHub CreateMusicNpc(string name) - { - var newNpc = UnityEngine.Object.Instantiate(NetworkManager.singleton.playerPrefab); - ReferenceHub hubNpc = newNpc.GetComponent(); - NetworkServer.AddPlayerForConnection(new FakeConnection(0), newNpc); - hubNpc.nicknameSync.Network_myNickSync = name; - hubNpc.authManager.NetworkSyncedUserId = null; - MusicNpc.Add($"{num++}:{name}", hubNpc); - return hubNpc; - } - - /// - /// 立刻停止播放音频 - /// - /// AudioPlayerBase - public void Stop(AudioPlayerBase playerBase) - { - if (playerBase == null) return; - ReferenceHub npc = playerBase.Owner; - playerBase.Stoptrack(true); - MusicNpc.Remove(npc.nicknameSync.Network_myNickSync); - CustomNetworkManager.TypedSingleton.OnServerDisconnect(npc.connectionToClient); - Player.Dictionary.Remove(npc.gameObject); - UnityEngine.Object.Destroy(npc.gameObject); - } - /// - /// 向所有玩家播放音频 - /// - /// 音频文件 - /// NPC名称 - /// - public AudioPlayerBase Play(string musicFile, string npcName) - { - return Play(musicFile, npcName, -1); - } - /// - /// 向一名玩家播放音频 - /// - /// 音频文件 - /// NPC名称 - /// 传播距离检测源头玩家(可null,null时是NPC) - /// - public AudioPlayerBase Play(string musicFile, string npcName, FramePlayer source) - { - return Play(musicFile, npcName, source, 0); - } - /// - /// NPC向玩家播放音频 - /// - /// 音频文件 - /// NPC名称 - /// 传播距离(-1时是全部玩家,0时是源头玩家) - /// - public AudioPlayerBase Play(string musicFile, string npcName,float distance) - { - return Play(musicFile, npcName, null, distance); - } - /// - /// 在多少米内向玩家播放音频 - /// - /// 音频文件 - /// NPC名称 - /// 传播距离检测源头玩家(可null,null时是NPC) - /// 传播距离(-1时是全部玩家,0时是源头玩家) - /// - public AudioPlayerBase Play(string musicFile, string npcName, FramePlayer source, float distance) - { - return Play(musicFile, npcName, null, source, distance, null, false, 80, false); - } - /// - /// 播放音频 - /// - /// 音频文件 - /// NPC名称 - /// 播放事件(可null) - /// 传播距离检测源头玩家(可null,null时是NPC) - /// 传播距离(-1时是全部玩家,0时是源头玩家) - /// 额外可接收音频的玩家(可null) - /// [弃用]是否覆盖播放 - /// 音量大小 - /// 是否循环 - /// - public AudioPlayerBase Play(string musicFile, string npcName, TrackEvent? trackEvent, FramePlayer source, float distance, FramePlayer[] extraPlay, bool isSole = false, float volume = 80, bool isLoop = false) - { - AudioPlayerBase audioPlayerBase = null; - try - { - if (trackEvent.HasValue) - { - OnTrackLoaded += trackEvent.Value.TrackLoaded; - } - - ReferenceHub npc = CreateMusicNpc(npcName); - audioPlayerBase = Get(npc); - - if (distance != -1) - { - if (source != null) - { - if (distance == 0) - { - audioPlayerBase.BroadcastTo.Add(npc.PlayerId); - } - else - { - audioPlayerBase.BroadcastTo = FramePlayer.List.Where(p => Vector3.Distance(p.ExPlayer.Position, source.ExPlayer.Position) <= distance).Select((s) => s.ExPlayer.Id).ToList(); - } - } - - if (extraPlay != null) - { - foreach (var player in extraPlay) - { - if (!audioPlayerBase.BroadcastTo.Contains(player.ExPlayer.Id)) - { - audioPlayerBase.BroadcastTo.Add(player.ExPlayer.Id); - } - } - } - } - - audioPlayerBase.CurrentPlay = $"{Paths.Plugins}/{Server.Port}/YongAnPluginData/{musicFile}.ogg"; - audioPlayerBase.Volume = volume; - audioPlayerBase.Loop = isLoop; - audioPlayerBase.Play(-1); - } - catch (Exception) - { - Stop(audioPlayerBase); - } - return audioPlayerBase; - } - - /// - /// 播放音频(Url) - /// - /// 音频文件 - /// NPC名称 - /// 播放事件(可null) - /// 传播距离检测源头玩家(可null,null时是NPC) - /// 传播距离(-1时是全部玩家,0时是源头玩家) - /// 额外可接收音频的玩家(可null) - /// [弃用]是否覆盖播放 - /// 音量大小 - /// 是否循环 - /// - public AudioPlayerBase PlayUrl(string musicUrl, string npcName, TrackEvent? trackEvent, FramePlayer source, float distance, FramePlayer[] extraPlay, bool isSole = false, float volume = 80, bool isLoop = false) - { - AudioPlayerBase audioPlayerBase = null; - try - { - if (trackEvent.HasValue) - { - OnTrackLoaded += trackEvent.Value.TrackLoaded; - } - - ReferenceHub npc = CreateMusicNpc(npcName); - audioPlayerBase = Get(npc); - - if (distance != -1) - { - if (source != null) - { - if (distance == 0) - { - audioPlayerBase.BroadcastTo.Add(npc.PlayerId); - } - else - { - audioPlayerBase.BroadcastTo = FramePlayer.List.Where(p => Vector3.Distance(p.ExPlayer.Position, source.ExPlayer.Position) <= distance).Select((s) => s.ExPlayer.Id).ToList(); - } - } - - if (extraPlay != null) - { - foreach (var player in extraPlay) - { - if (!audioPlayerBase.BroadcastTo.Contains(player.ExPlayer.Id)) - { - audioPlayerBase.BroadcastTo.Add(player.ExPlayer.Id); - } - } - } - } - - audioPlayerBase.CurrentPlay = musicUrl; - audioPlayerBase.Volume = volume; - audioPlayerBase.Loop = isLoop; - audioPlayerBase.AllowUrl = true; - audioPlayerBase.Play(-1); - } - catch (Exception) - { - Stop(audioPlayerBase); - } - return audioPlayerBase; - } - } - public readonly struct TrackEvent(TrackLoaded trackLoaded) - { - public TrackLoaded TrackLoaded { get; } = trackLoaded; - } -} \ No newline at end of file diff --git a/Roles/Properties/CustomRolePlusProperties.cs b/Roles/Properties/CustomRolePlusProperties.cs deleted file mode 100644 index d616143..0000000 --- a/Roles/Properties/CustomRolePlusProperties.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace YongAnFrame.Roles.Properties -{ - /// - /// 自定义角色玩家数据 - /// - public class CustomRolePlusProperties - { - /// - /// 获取或设置自定义角色的技能管理器 - /// - public SkillManager[] SkillManagers { get; set; } - /// - /// 获取或设置自定义角色是否正常死亡 - /// - public bool IsDeathHandling { get; set; } - } -} diff --git a/Roles/Properties/SkillProperties.cs b/Roles/Properties/SkillProperties.cs deleted file mode 100644 index 0d51d2d..0000000 --- a/Roles/Properties/SkillProperties.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace YongAnFrame.Roles.Properties -{ - public readonly struct SkillProperties(string name, string statement, string description, float activeMaxTime, float burialMaxTime, ItemType useItem = ItemType.Coin) - { - /// - /// 获取技能的名称 - /// - public string Name { get; } = name; - /// - /// 获取技能的绑定物品 - /// - public ItemType UseItem { get; } = useItem; - /// - /// 获取技能的发动描述 - /// - public string Statement { get; } = statement; - /// - /// 获取技能的介绍 - /// - public string Description { get; } = description; - /// - /// 获取技能的最大作用时间 - /// - public float ActiveMaxTime { get; } = activeMaxTime; - /// - /// 获取技能的最大冷却时间 - /// - public float BurialMaxTime { get; } = burialMaxTime; - } -} diff --git a/Roles/SkillManager.cs b/Roles/SkillManager.cs deleted file mode 100644 index 48049f3..0000000 --- a/Roles/SkillManager.cs +++ /dev/null @@ -1,119 +0,0 @@ -using MEC; -using System.Collections.Generic; -using YongAnFrame.Players; -using YongAnFrame.Roles.Interfaces; -using YongAnFrame.Roles.Properties; -using static YongAnFrame.Roles.MusicManager; - -namespace YongAnFrame.Roles -{ - /// - /// 技能控制器 - /// - /// - /// - /// - public sealed class SkillManager(FramePlayer fPlayer, ISkill skill, byte id) - { - /// - /// 获取或设置技能的ID - /// - public byte Id { get; } = id; - private ISkillActiveStart SkillActiveStart - { - get - { - if (skill is ISkillActiveStart skillActiveStart) - { - return skillActiveStart; - } - return null; - } - } - private ISkillActiveEnd SkillActiveEnd - { - get - { - if (skill is ISkillActiveEnd skillActiveEnd) - { - return skillActiveEnd; - } - return null; - } - } - private ISkillBurialEnd SkillBurialEnd - { - get - { - if (skill is ISkillBurialEnd skillBurialEnd) - { - return skillBurialEnd; - } - return null; - } - } - /// - /// 获取技能的属性 - /// - public SkillProperties SkillProperties { get => skill.SkillProperties[Id]; } - - /// - /// 获取或设置技能是否行动 - /// - public bool IsActive { get => ActiveRemainingTime > 0; } - /// - /// 获取或设置技能是否冷却 - /// - public bool IsBurial { get => BurialRemainingTime > 0; } - /// - /// 获取或设置技能的行动时间 - /// - public float ActiveRemainingTime { get; private set; } - /// - /// 获取或设置技能的冷却时间 - /// - public float BurialRemainingTime { get; private set; } - - private CoroutineHandle coroutineHandle; - - - /// - /// 使用技能 - /// - /// - /// 有计时任务会直接覆盖 - /// - public void Run() - { - if (coroutineHandle != null) - { - Timing.KillCoroutines(coroutineHandle); - } - - ActiveRemainingTime = SkillProperties.ActiveMaxTime; - BurialRemainingTime = SkillProperties.BurialMaxTime; - - coroutineHandle = Timing.RunCoroutine(Timer()); - } - - private IEnumerator Timer() - { - string musicFileName = SkillActiveStart?.ActiveStart(fPlayer, Id); - if (musicFileName != null) Instance.Play(musicFileName, $"技能发动语音", fPlayer, 10); - while (IsActive) - { - ActiveRemainingTime--; - yield return Timing.WaitForSeconds(1f); - } - musicFileName = SkillActiveEnd?.ActiveEnd(fPlayer, Id); - if (musicFileName != null) Instance.Play(musicFileName, $"技能结束语音", fPlayer, 10); - while (IsBurial) - { - BurialRemainingTime--; - yield return Timing.WaitForSeconds(1f); - } - musicFileName = SkillBurialEnd?.BurialEnd(fPlayer, Id); - if (musicFileName != null) Instance.Play(musicFileName, $"技能准备好语音", fPlayer, 10); - } - } -} diff --git a/Translation.cs b/Translation.cs index 6eb1ac0..762aa4d 100644 --- a/Translation.cs +++ b/Translation.cs @@ -4,14 +4,14 @@ namespace YongAnFrame { /// - /// 插件的翻译 + /// 的翻译 /// public sealed class Translation : ITranslation { /// - /// BDNT(Bypass Do Not Track)协议文本 + /// 获取或设置BDNT(Bypass Do Not Track)协议文本 /// [Description("BDNT(Bypass Do Not Track)协议文本")] - public string BypassDoNotTrack { get; set; } = "BDNT(Bypass Do Not Track)协议\n根据VSR(Verified Server Rules) 8.11,所以开启DNT(Do Not Track)的玩家不会进行非服务器安全性的游戏数据收集或保存。\n根据VSR 8.11.5,所以只有签署BDNT的玩家才会对DNT相关的规则不适用。\n根据VSR 8.11.5.3,所以欲签署BDNT的玩家有知晓收集或保存数据内容的权利。\n|||如果你看不懂BDNT协议的条例请不要签署|||\n1.你将会被收集SteamID用来保存等级和称号数据,这个条例收集的数据是公开展示的,任何人都可以访问!\n2.签署玩家依然有请求删除收集或保存数据的权利,请求之后你依然有24小时可以撤销请求(注意!删除数据是不可逆的)!"; + public string BypassDoNotTrack { get; set; } = "BDNT(Bypass Do Not Track)协议\n根据VSR(Verified Server Rules) 8.11,所以开启DNT(Do Not Track)的玩家不会进行非服务器安全性的游戏数据收集或保存。\n根据VSR 8.11.5,所以只有签署BDNT的玩家才会对DNT相关的规则不适用。\n根据VSR 8.11.5.3,所以需要签署BDNT的玩家有知晓收集或保存数据内容的权利。\n|||如果你看不懂BDNT协议的条例请不要签署|||\n1.你将会被收集SteamID用来保存等级和称号数据,这个条例收集的数据是公开展示的,任何人都可以访问!\n2.签署玩家依然有请求删除收集或保存数据的权利,请求之后你依然有24小时可以撤销请求(注意!删除数据是不可逆的)!"; } } diff --git a/YongAnFrame.csproj b/YongAnFrame.csproj index ef6366c..beeb5cf 100644 --- a/YongAnFrame.csproj +++ b/YongAnFrame.csproj @@ -1,145 +1,80 @@ - - - + - Debug - AnyCPU - {B036C1E0-4D95-487C-BCA3-32AF84D820EA} - Library - Properties - YongAnFrame - YongAnFrame + enable latest - v4.8 - 512 - true - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\YongAnFrame.xml - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - embedded + net48 + x64 + True + True + zh-Hans + False + 1.0.0-beta6 + YongAn404 + $(Authors) + Copyright © YongAn404 + $(AssemblyName) + 1.0.0.0 + 1.0.0.0 + disable + True + LGPL-3.0-only + README.md + git + EXILED;SCPSL;SCP:SL + https://github.com/YongAn404/YongAnFrame + https://github.com/YongAn404/YongAnFrame + + + + + + + + + + + + + + + + + + + True + \ + + + True + lib\$(TargetFramework)\HintServiceMeow-Exiled.dll + + + + + + + + - lib\net48\Assembly-CSharp-firstpass.dll - - - packages\ExMod.Exiled.9.0.0-beta.1\lib\net48\Assembly-CSharp-Publicized.dll - False + lib\$(TargetFramework)\Assembly-CSharp-firstpass.dll False - - packages\ExMod.Exiled.9.0.0-beta.1\lib\net48\CommandSystem.Core.dll - - - packages\ExMod.Exiled.9.0.0-beta.1\lib\net48\Exiled.API.dll - - - packages\ExMod.Exiled.9.0.0-beta.1\lib\net48\Exiled.CreditTags.dll - - - packages\ExMod.Exiled.9.0.0-beta.1\lib\net48\Exiled.CustomItems.dll - - - packages\ExMod.Exiled.9.0.0-beta.1\lib\net48\Exiled.CustomRoles.dll - - - packages\ExMod.Exiled.9.0.0-beta.1\lib\net48\Exiled.Events.dll - - - packages\ExMod.Exiled.9.0.0-beta.1\lib\net48\Exiled.Loader.dll - - - packages\ExMod.Exiled.9.0.0-beta.1\lib\net48\Exiled.Permissions.dll + + lib\$(TargetFramework)\HintServiceMeow-Exiled.dll - lib\net48\Mirror.dll - - - packages\ExMod.Exiled.9.0.0-beta.1\lib\net48\NorthwoodLib.dll - - - packages\ExMod.Exiled.9.0.0-beta.1\lib\net48\PluginAPI.dll - - - False - lib\net48\SCPSLAudioApi.dll + lib\$(TargetFramework)\Mirror.dll + False - - - - - - - - - lib\net48\UnityEngine.CoreModule.dll + lib\$(TargetFramework)\UnityEngine.CoreModule.dll + False - lib\net48\UnityEngine.PhysicsModule.dll - - - packages\ExMod.Exiled.9.0.0-beta.1\lib\net48\YamlDotNet.dll + lib\$(TargetFramework)\UnityEngine.PhysicsModule.dll + False - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/YongAnFrame.nuspec b/YongAnFrame.nuspec deleted file mode 100644 index 5586d46..0000000 --- a/YongAnFrame.nuspec +++ /dev/null @@ -1,22 +0,0 @@ - - - - $id$ - 1.0.0-beta5 - $title$ - $author$ - true - LGPL-3.0-only - - docs\README.md - https://github.com/SCP-SL-Plugin-YongAnTeam/YongAnFrame - $description$ - $version$ - $copyright$ - EXILED SCPSL SCP:SL - - - - - - \ No newline at end of file diff --git a/YongAnFramePlugin.cs b/YongAnFramePlugin.cs index 4201c13..0fb00fb 100644 --- a/YongAnFramePlugin.cs +++ b/YongAnFramePlugin.cs @@ -1,10 +1,11 @@ using Exiled.API.Enums; using Exiled.API.Features; -using Exiled.API.Interfaces; -using Exiled.Events.Commands.Reload; -using SCPSLAudioApi; -using YongAnFrame.Players; -using YongAnFrame.Roles; +using HarmonyLib; +using System; +using YongAnFrame.Features; +using YongAnFrame.Features.Players; +using YongAnFrame.Features.Roles; +using YongAnFrame.Patch; namespace YongAnFrame { @@ -13,26 +14,39 @@ namespace YongAnFrame /// public sealed class YongAnFramePlugin : Plugin { - private static YongAnFramePlugin instance; + private static YongAnFramePlugin? instance; /// /// 获取单例 /// - public static YongAnFramePlugin Instance => instance; - /// - public override PluginPriority Priority => PluginPriority.First; + public static YongAnFramePlugin Instance + { + get + { + if (instance is null) + { + throw new InvalidCastException("YongAnFramePlugin实例无效"); + } + return instance; + } + } + /// + /// 获取实例 + /// + public Harmony Harmony { get; } = new Harmony("YongAnFrame.Harmony"); + /// - public override bool IgnoreRequiredVersionCheck => true; + public override PluginPriority Priority => PluginPriority.Higher; /// public override void OnEnabled() { instance = this; Log.Info("\r\n __ __ ______ __ __ ______ ______ __ __ \r\n/\\ \\_\\ \\ /\\ __ \\ /\\ \"-.\\ \\ /\\ ___\\ /\\ __ \\ /\\ \"-.\\ \\ \r\n\\ \\____ \\ \\ \\ \\/\\ \\ \\ \\ \\-. \\ \\ \\ \\__ \\ \\ \\ __ \\ \\ \\ \\-. \\ \r\n \\/\\_____\\ \\ \\_____\\ \\ \\_\\\\\"\\_\\ \\ \\_____\\ \\ \\_\\ \\_\\ \\ \\_\\\\\"\\_\\ \r\n \\/_____/ \\/_____/ \\/_/ \\/_/ \\/_____/ \\/_/\\/_/ \\/_/ \\/_/ \r\n \r\n ______ ______ ______ __ __ ______ \r\n/\\ ___\\ /\\ == \\ /\\ __ \\ /\\ \"-./ \\ /\\ ___\\ \r\n\\ \\ __\\ \\ \\ __< \\ \\ __ \\ \\ \\ \\-./\\ \\ \\ \\ __\\ \r\n \\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\ \\_\\ \\ \\_____\\ \r\n \\/_/ \\/_/ /_/ \\/_/\\/_/ \\/_/ \\/_/ \\/_____/ \r\n \r\n "); - Log.Info("============System============"); + PathManager.CheckPath(); FramePlayer.SubscribeStaticEvents(); - MusicManager.Instance.Init(); CustomRolePlus.SubscribeStaticEvents(); - Startup.SetupDependencies(); + AddLogPatch.StartTask(); + Harmony.PatchAll(); base.OnEnabled(); } @@ -42,6 +56,7 @@ public override void OnDisabled() instance = null; FramePlayer.UnsubscribeStaticEvents(); CustomRolePlus.UnsubscribeStaticEvents(); + Harmony.UnpatchAll(); base.OnDisabled(); } } diff --git a/docs/docfx.json b/docs/docfx.json index a4a8c88..3a4b1f4 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -34,6 +34,7 @@ "files": [ "api/**.yml", "api/index.md", + "articles/*.md", "toc.yml", "*.md" ] diff --git a/docs/toc.yml b/docs/toc.yml index 3ff9579..5104f91 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -1,5 +1,7 @@ +- name: Home + href: index.html - name: Articles - href: https://github.com/SCP-SL-Plugin-YongAnTeam/YongAnFrame/wiki/%5BCN%5D%E5%BC%80%E5%8F%91%E6%95%99%E7%A8%8B + href: articles/index.html - name: API Documentation href: api/YongAnFrame.html - name: GitHub diff --git a/lib/net48/Assembly-CSharp-firstpass.dll b/lib/net48/Assembly-CSharp-firstpass.dll index 9383cfa..040e8b6 100644 Binary files a/lib/net48/Assembly-CSharp-firstpass.dll and b/lib/net48/Assembly-CSharp-firstpass.dll differ diff --git a/lib/net48/HintServiceMeow-Exiled.dll b/lib/net48/HintServiceMeow-Exiled.dll new file mode 100644 index 0000000..cb7623d Binary files /dev/null and b/lib/net48/HintServiceMeow-Exiled.dll differ diff --git a/lib/net48/Mirror.dll b/lib/net48/Mirror.dll index bf8dc56..d469c7a 100644 Binary files a/lib/net48/Mirror.dll and b/lib/net48/Mirror.dll differ diff --git a/lib/net48/SCPSLAudioApi.dll b/lib/net48/SCPSLAudioApi.dll deleted file mode 100644 index ce14bf2..0000000 Binary files a/lib/net48/SCPSLAudioApi.dll and /dev/null differ diff --git a/lib/net48/UnityEngine.CoreModule.dll b/lib/net48/UnityEngine.CoreModule.dll index 4c01b22..c2f8e2a 100644 Binary files a/lib/net48/UnityEngine.CoreModule.dll and b/lib/net48/UnityEngine.CoreModule.dll differ diff --git a/lib/net48/UnityEngine.PhysicsModule.dll b/lib/net48/UnityEngine.PhysicsModule.dll index 14c31ba..4c918bd 100644 Binary files a/lib/net48/UnityEngine.PhysicsModule.dll and b/lib/net48/UnityEngine.PhysicsModule.dll differ diff --git a/packages.config b/packages.config deleted file mode 100644 index e06816f..0000000 --- a/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file