From 3e4991e509b33edbe354eed743e2f91267d2aa2b Mon Sep 17 00:00:00 2001 From: Hyz-sui <86903430+Hyz-sui@users.noreply.github.com> Date: Tue, 2 Apr 2024 22:46:23 +0900 Subject: [PATCH 01/13] feature: client configuration SendHistoryToDiscord --- Patches/ClientOptionsPatch.cs | 5 +++++ main.cs | 2 ++ 2 files changed, 7 insertions(+) diff --git a/Patches/ClientOptionsPatch.cs b/Patches/ClientOptionsPatch.cs index a8a78018d..199f9180e 100644 --- a/Patches/ClientOptionsPatch.cs +++ b/Patches/ClientOptionsPatch.cs @@ -13,6 +13,7 @@ public static class OptionsMenuBehaviourStartPatch private static ClientActionItem UnloadMod; private static ClientActionItem DumpLog; private static ClientActionItem SendResultToDiscord; + private static ClientActionItem SendHistoryToDiscord; private static ClientActionItem ShowLobbySummary; private static ClientActionItem CopyGameCodeOnCreateLobby; private static ClientActionItem HauntMenuFocusCrewmate; @@ -36,6 +37,10 @@ public static void Postfix(OptionsMenuBehaviour __instance) { SendResultToDiscord = ClientOptionItem.Create("DiscordResult", Main.SendResultToDiscord, __instance); } + if (SendHistoryToDiscord == null || SendHistoryToDiscord.ToggleButton == null) + { + SendHistoryToDiscord = ClientOptionItem.Create("DiscordHistory", Main.SendHistoryToDiscord, __instance); + } if (ShowLobbySummary == null || ShowLobbySummary.ToggleButton == null) { ShowLobbySummary = ClientOptionItem.Create("ShowLobbySummary", Main.ShowLobbySummary, __instance, () => diff --git a/main.cs b/main.cs index c50322183..bf1f1e62a 100644 --- a/main.cs +++ b/main.cs @@ -78,6 +78,7 @@ public class Main : BasePlugin public static ConfigEntry ForceJapanese { get; private set; } public static ConfigEntry JapaneseRoleName { get; private set; } public static ConfigEntry SendResultToDiscord { get; private set; } + public static ConfigEntry SendHistoryToDiscord { get; private set; } public static ConfigEntry ShowLobbySummary { get; private set; } public static ConfigEntry CopyGameCodeOnCreateLobby { get; private set; } public static ConfigEntry HauntMenuFocusCrewmate { get; private set; } @@ -143,6 +144,7 @@ public override void Load() ForceJapanese = Config.Bind("Client Options", "Force Japanese", false); JapaneseRoleName = Config.Bind("Client Options", "Japanese Role Name", true); SendResultToDiscord = Config.Bind("Client Options", "Send Game Result To Discord", false); + SendHistoryToDiscord = Config.Bind("Client Options", "Send Game History To Discord", false); ShowLobbySummary = Config.Bind("Client Options", "Show Lobby Summary", true); CopyGameCodeOnCreateLobby = Config.Bind("Client Options", "Copy Game Code On Create Lobby", true); HauntMenuFocusCrewmate = Config.Bind("Client Options", "Haunt Menu Focuses Crewmate", true); From 9e561ab69b8b5836668e31c21941e90d223c9cb1 Mon Sep 17 00:00:00 2001 From: Hyz-sui <86903430+Hyz-sui@users.noreply.github.com> Date: Tue, 2 Apr 2024 22:48:24 +0900 Subject: [PATCH 02/13] feature: IHistoryEvent --- Modules/GameEventHistory/IHistoryEvent.cs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Modules/GameEventHistory/IHistoryEvent.cs diff --git a/Modules/GameEventHistory/IHistoryEvent.cs b/Modules/GameEventHistory/IHistoryEvent.cs new file mode 100644 index 000000000..1cce51ca2 --- /dev/null +++ b/Modules/GameEventHistory/IHistoryEvent.cs @@ -0,0 +1,8 @@ +using System.Text; + +namespace TownOfHost.Modules.GameEventHistory; + +public interface IHistoryEvent +{ + public void AppendDiscordString(StringBuilder builder); +} From 94e3c263188ba39ba40a44280aeb5623fb387f3c Mon Sep 17 00:00:00 2001 From: Hyz-sui <86903430+Hyz-sui@users.noreply.github.com> Date: Tue, 2 Apr 2024 22:50:13 +0900 Subject: [PATCH 03/13] feature: abstract Event --- Modules/GameEventHistory/Event.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 Modules/GameEventHistory/Event.cs diff --git a/Modules/GameEventHistory/Event.cs b/Modules/GameEventHistory/Event.cs new file mode 100644 index 000000000..d986fb1a4 --- /dev/null +++ b/Modules/GameEventHistory/Event.cs @@ -0,0 +1,24 @@ +using System; +using System.Text; + +namespace TownOfHost.Modules.GameEventHistory; + +public abstract class Event : IHistoryEvent +{ + public DateTime UtcTime { get; } + public abstract string Bullet { get; } + protected Event() + { + UtcTime = DateTime.UtcNow; + } + + public abstract void AppendDiscordString(StringBuilder builder); + + protected void AppendPlayerWithEmoji(StringBuilder builder, EventCommittedPlayer player, bool isAlive) + { + builder.Append(Utils.ColorIdToDiscordEmoji(player.ColorId, isAlive)); + builder.Append(" **"); + builder.Append(player.Name); + builder.Append("**"); + } +} From 4c4840f7d48d8b984280f15a0a496d08be5d863b Mon Sep 17 00:00:00 2001 From: Hyz-sui <86903430+Hyz-sui@users.noreply.github.com> Date: Tue, 2 Apr 2024 22:57:25 +0900 Subject: [PATCH 04/13] feature: EventCommittedPlayer represents a committer for the event --- Modules/GameEventHistory/EventCommittedPlayer.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 Modules/GameEventHistory/EventCommittedPlayer.cs diff --git a/Modules/GameEventHistory/EventCommittedPlayer.cs b/Modules/GameEventHistory/EventCommittedPlayer.cs new file mode 100644 index 000000000..014dcc471 --- /dev/null +++ b/Modules/GameEventHistory/EventCommittedPlayer.cs @@ -0,0 +1,13 @@ +using TownOfHost.Roles.Core; + +namespace TownOfHost.Modules.GameEventHistory; + +public readonly struct EventCommittedPlayer(string name, byte playerId, int colorId, CustomRoles roleId) +{ + public string Name { get; init; } = name; + public byte PlayerId { get; init; } = playerId; + public int ColorId { get; init; } = colorId; + public CustomRoles RoleId { get; init; } = roleId; + + public EventCommittedPlayer(PlayerControl playerControl) : this(playerControl.GetRealName(), playerControl.PlayerId, playerControl.Data.DefaultOutfit.ColorId, playerControl.GetCustomRole()) { } +} From 60ba58b12616475b6cc5f35a75b8d92fb5861b56 Mon Sep 17 00:00:00 2001 From: Hyz-sui <86903430+Hyz-sui@users.noreply.github.com> Date: Tue, 2 Apr 2024 22:59:50 +0900 Subject: [PATCH 05/13] feature: EventHistory represents the history of a game --- Modules/GameEventHistory/EventHistory.cs | 46 ++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 Modules/GameEventHistory/EventHistory.cs diff --git a/Modules/GameEventHistory/EventHistory.cs b/Modules/GameEventHistory/EventHistory.cs new file mode 100644 index 000000000..cd02a94ce --- /dev/null +++ b/Modules/GameEventHistory/EventHistory.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Text; +using TownOfHost.Attributes; + +namespace TownOfHost.Modules.GameEventHistory; + +public sealed class EventHistory : IHistoryEvent +{ + public static EventHistory CurrentInstance { get; private set; } + + [GameModuleInitializer] + public static void NewGame() + { + CurrentInstance = new(); + } + + private readonly List events = []; + + public void AddEvent(Event @event) + { + events.Add(@event); + } + public void AppendDiscordString(StringBuilder builder) + { + foreach (var @event in events) + { + builder.Append(@event.Bullet); + builder.Append(' '); + builder.Append(" "); + @event.AppendDiscordString(builder); + builder.AppendLine(); + } + } + public string ToDiscordString() + { + var builder = new StringBuilder(); + AppendDiscordString(builder); + return builder.ToString(); + } + + private readonly static DateTime Epoch = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); +} From 75e9327eebb30711c25ef04fc9e3549a22f22f55 Mon Sep 17 00:00:00 2001 From: Hyz-sui <86903430+Hyz-sui@users.noreply.github.com> Date: Tue, 2 Apr 2024 23:03:40 +0900 Subject: [PATCH 06/13] feature: create event stuff --- .../Events/CrewTaskFinishEvent.cs | 15 +++++++ .../GameEventHistory/Events/GameEndEvent.cs | 14 +++++++ .../GameEventHistory/Events/GameStartEvent.cs | 13 ++++++ .../Events/MeetingCallEvent.cs | 39 ++++++++++++++++++ .../Events/MeetingEndEvent.cs | 27 +++++++++++++ .../GameEventHistory/Events/MurderEvent.cs | 40 +++++++++++++++++++ .../GameEventHistory/Events/RevengeEvent.cs | 18 +++++++++ .../Events/RoleChangeEvent.cs | 21 ++++++++++ 8 files changed, 187 insertions(+) create mode 100644 Modules/GameEventHistory/Events/CrewTaskFinishEvent.cs create mode 100644 Modules/GameEventHistory/Events/GameEndEvent.cs create mode 100644 Modules/GameEventHistory/Events/GameStartEvent.cs create mode 100644 Modules/GameEventHistory/Events/MeetingCallEvent.cs create mode 100644 Modules/GameEventHistory/Events/MeetingEndEvent.cs create mode 100644 Modules/GameEventHistory/Events/MurderEvent.cs create mode 100644 Modules/GameEventHistory/Events/RevengeEvent.cs create mode 100644 Modules/GameEventHistory/Events/RoleChangeEvent.cs diff --git a/Modules/GameEventHistory/Events/CrewTaskFinishEvent.cs b/Modules/GameEventHistory/Events/CrewTaskFinishEvent.cs new file mode 100644 index 000000000..c72ad65df --- /dev/null +++ b/Modules/GameEventHistory/Events/CrewTaskFinishEvent.cs @@ -0,0 +1,15 @@ +using System.Text; + +namespace TownOfHost.Modules.GameEventHistory.Events; + +public sealed class CrewTaskFinishEvent(EventCommittedPlayer player) : Event +{ + public override string Bullet { get; } = ":blue_circle:"; + public EventCommittedPlayer Player { get; } = player; + + public override void AppendDiscordString(StringBuilder builder) + { + builder.Append("**タスク完了:** "); + AppendPlayerWithEmoji(builder, Player, true); + } +} diff --git a/Modules/GameEventHistory/Events/GameEndEvent.cs b/Modules/GameEventHistory/Events/GameEndEvent.cs new file mode 100644 index 000000000..70487461c --- /dev/null +++ b/Modules/GameEventHistory/Events/GameEndEvent.cs @@ -0,0 +1,14 @@ +using System.Text; + +namespace TownOfHost.Modules.GameEventHistory.Events; + +public sealed class GameEndEvent() : Event +{ + public override string Bullet { get; } = ":green_circle:"; + + public override void AppendDiscordString(StringBuilder builder) + { + builder.Append("**ゲーム終了:** "); + builder.Append(SetEverythingUpPatch.LastWinsText); + } +} diff --git a/Modules/GameEventHistory/Events/GameStartEvent.cs b/Modules/GameEventHistory/Events/GameStartEvent.cs new file mode 100644 index 000000000..fe3b04118 --- /dev/null +++ b/Modules/GameEventHistory/Events/GameStartEvent.cs @@ -0,0 +1,13 @@ +using System.Text; + +namespace TownOfHost.Modules.GameEventHistory.Events; + +public sealed class GameStartEvent : Event +{ + public override string Bullet { get; } = ":green_circle:"; + + public override void AppendDiscordString(StringBuilder builder) + { + builder.Append("**ゲーム開始**"); + } +} diff --git a/Modules/GameEventHistory/Events/MeetingCallEvent.cs b/Modules/GameEventHistory/Events/MeetingCallEvent.cs new file mode 100644 index 000000000..4e3b6f744 --- /dev/null +++ b/Modules/GameEventHistory/Events/MeetingCallEvent.cs @@ -0,0 +1,39 @@ +using System.Text; + +namespace TownOfHost.Modules.GameEventHistory.Events; + +public sealed class MeetingCallEvent(EventCommittedPlayer reporter) : Event +{ + public override string Bullet { get; } = ":green_circle:"; + public EventCommittedPlayer Reporter { get; } = reporter; + public EventCommittedPlayer? Dead { get; } = null; + + public MeetingCallEvent(EventCommittedPlayer reporter, EventCommittedPlayer dead) : this(reporter) + { + Dead = dead; + } + + public override void AppendDiscordString(StringBuilder builder) + { + if (Dead.HasValue) + { + AppendDiscordReport(builder); + } + else + { + AppendDiscordEmergency(builder); + } + } + private void AppendDiscordReport(StringBuilder builder) + { + builder.Append("**通報:** "); + AppendPlayerWithEmoji(builder, Reporter, true); + builder.Append(" → "); + AppendPlayerWithEmoji(builder, Dead.Value, false); + } + private void AppendDiscordEmergency(StringBuilder builder) + { + builder.Append("**緊急会議:** "); + AppendPlayerWithEmoji(builder, Reporter, true); + } +} diff --git a/Modules/GameEventHistory/Events/MeetingEndEvent.cs b/Modules/GameEventHistory/Events/MeetingEndEvent.cs new file mode 100644 index 000000000..af25d2ec2 --- /dev/null +++ b/Modules/GameEventHistory/Events/MeetingEndEvent.cs @@ -0,0 +1,27 @@ +using System.Text; + +namespace TownOfHost.Modules.GameEventHistory.Events; + +public sealed class MeetingEndEvent() : Event +{ + public override string Bullet { get; } = ":green_circle:"; + public EventCommittedPlayer? Exiled { get; } = null; + + public MeetingEndEvent(EventCommittedPlayer exiled) : this() + { + Exiled = exiled; + } + + public override void AppendDiscordString(StringBuilder builder) + { + builder.Append("**会議結果:** 追放者 "); + if (Exiled.HasValue) + { + AppendPlayerWithEmoji(builder, Exiled.Value, false); + } + else + { + builder.Append("なし"); + } + } +} diff --git a/Modules/GameEventHistory/Events/MurderEvent.cs b/Modules/GameEventHistory/Events/MurderEvent.cs new file mode 100644 index 000000000..cd954e1df --- /dev/null +++ b/Modules/GameEventHistory/Events/MurderEvent.cs @@ -0,0 +1,40 @@ +using System.Text; +using TownOfHost.Roles.Core; + +namespace TownOfHost.Modules.GameEventHistory.Events; + +public sealed class MurderEvent(EventCommittedPlayer killer, EventCommittedPlayer victim, SystemTypes room) : Event +{ + public override string Bullet { get; } = killer.RoleId is CustomRoles.Sheriff ? ":yellow_square:" : ":red_square:"; + public EventCommittedPlayer Killer { get; } = killer; + public EventCommittedPlayer Victim { get; } = victim; + public SystemTypes Room { get; } = room; + + public override void AppendDiscordString(StringBuilder builder) + { + if (Killer.PlayerId == Victim.PlayerId) + { + AppendDiscordSuicide(builder); + } + else + { + AppendDiscordMurder(builder); + } + } + private void AppendDiscordSuicide(StringBuilder builder) + { + builder.Append("**自爆:** "); + AppendPlayerWithEmoji(builder, Killer, false); + builder.Append(" @"); + builder.Append(DestroyableSingleton.Instance.GetString(Room)); + } + private void AppendDiscordMurder(StringBuilder builder) + { + builder.Append("**キル:** "); + AppendPlayerWithEmoji(builder, Killer, true); + builder.Append(" → "); + AppendPlayerWithEmoji(builder, Victim, false); + builder.Append(" @"); + builder.Append(DestroyableSingleton.Instance.GetString(Room)); + } +} diff --git a/Modules/GameEventHistory/Events/RevengeEvent.cs b/Modules/GameEventHistory/Events/RevengeEvent.cs new file mode 100644 index 000000000..59fcaaf6e --- /dev/null +++ b/Modules/GameEventHistory/Events/RevengeEvent.cs @@ -0,0 +1,18 @@ +using System.Text; + +namespace TownOfHost.Modules.GameEventHistory.Events; + +public sealed class RevengeEvent(EventCommittedPlayer cat, EventCommittedPlayer victim) : Event +{ + public override string Bullet { get; } = ":red_circle:"; + public EventCommittedPlayer Cat { get; } = cat; + public EventCommittedPlayer Victim { get; } = victim; + + public override void AppendDiscordString(StringBuilder builder) + { + builder.Append("**道連れ:** "); + AppendPlayerWithEmoji(builder, Cat, true); + builder.Append(" → "); + AppendPlayerWithEmoji(builder, Victim, false); + } +} diff --git a/Modules/GameEventHistory/Events/RoleChangeEvent.cs b/Modules/GameEventHistory/Events/RoleChangeEvent.cs new file mode 100644 index 000000000..fdca69517 --- /dev/null +++ b/Modules/GameEventHistory/Events/RoleChangeEvent.cs @@ -0,0 +1,21 @@ +using System.Text; +using TownOfHost.Roles.Core; + +namespace TownOfHost.Modules.GameEventHistory.Events; + +public sealed class RoleChangeEvent(EventCommittedPlayer player, CustomRoles to) : Event +{ + public override string Bullet { get; } = ":green_circle:"; + public EventCommittedPlayer Player { get; } = player; + public CustomRoles To { get; } = to; + + public override void AppendDiscordString(StringBuilder builder) + { + builder.Append("**ロール変更:** "); + AppendPlayerWithEmoji(builder, Player, true); + builder.Append(" "); + builder.Append(Translator.GetRoleString(Player.RoleId.ToString())); + builder.Append(" → "); + builder.Append(Translator.GetRoleString(To.ToString())); + } +} From eb1db2fd164938c9abe2ce9bf7a2d86680de7216 Mon Sep 17 00:00:00 2001 From: Hyz-sui <86903430+Hyz-sui@users.noreply.github.com> Date: Tue, 2 Apr 2024 23:05:23 +0900 Subject: [PATCH 07/13] feature: trigger events --- Modules/GameState.cs | 7 ++++++- Modules/MeetingVoteManager.cs | 5 +++++ Patches/MeetingHudPatch.cs | 5 +++++ Patches/OutroPatch.cs | 5 +++++ Patches/PlayerContorolPatch.cs | 12 ++++++++++++ Patches/onGameStartedPatch.cs | 4 ++++ 6 files changed, 37 insertions(+), 1 deletion(-) diff --git a/Modules/GameState.cs b/Modules/GameState.cs index 62af54de6..f1fb032a2 100644 --- a/Modules/GameState.cs +++ b/Modules/GameState.cs @@ -9,6 +9,8 @@ using TownOfHost.Attributes; using TownOfHost.Roles.Core; +using TownOfHost.Modules.GameEventHistory; +using TownOfHost.Modules.GameEventHistory.Events; namespace TownOfHost { @@ -98,9 +100,12 @@ public void RemoveSubRole(CustomRoles role) } public void ChangeMainRole(CustomRoles role) { + var player = Utils.GetPlayerById(PlayerId); + + EventHistory.CurrentInstance?.AddEvent(new RoleChangeEvent(new(player), role)); + this.PreviousRoles.Add(this.MainRole); this.SetMainRole(role); - var player = Utils.GetPlayerById(PlayerId); player.GetRoleClass()?.Dispose(); CustomRoleManager.CreateInstance(role, player); } diff --git a/Modules/MeetingVoteManager.cs b/Modules/MeetingVoteManager.cs index 06e701ac1..a1438b392 100644 --- a/Modules/MeetingVoteManager.cs +++ b/Modules/MeetingVoteManager.cs @@ -2,6 +2,8 @@ using System.Linq; using System.Collections.Generic; using TownOfHost.Roles.Core; +using TownOfHost.Modules.GameEventHistory; +using TownOfHost.Modules.GameEventHistory.Events; namespace TownOfHost.Modules; @@ -112,6 +114,9 @@ public void CheckAndEndMeeting() public void EndMeeting(bool applyVoteMode = true) { var result = CountVotes(applyVoteMode); + + EventHistory.CurrentInstance?.AddEvent(result.Exiled == null ? new MeetingEndEvent() : new MeetingEndEvent(new(result.Exiled.Object))); + var logName = result.Exiled == null ? (result.IsTie ? "同数" : "スキップ") : result.Exiled.Object.GetNameWithRole(); logger.Info($"追放者: {logName} で会議を終了します"); diff --git a/Patches/MeetingHudPatch.cs b/Patches/MeetingHudPatch.cs index ad3392289..09f6383d2 100644 --- a/Patches/MeetingHudPatch.cs +++ b/Patches/MeetingHudPatch.cs @@ -11,6 +11,8 @@ using TownOfHost.Roles.Neutral; using TownOfHost.Roles.Core.Interfaces; using static TownOfHost.Translator; +using TownOfHost.Modules.GameEventHistory; +using TownOfHost.Modules.GameEventHistory.Events; namespace TownOfHost; @@ -227,6 +229,9 @@ private static void RevengeOnExile(byte playerId, CustomDeathReason deathReason) if (player == null) return; var target = PickRevengeTarget(player, deathReason); if (target == null) return; + + EventHistory.CurrentInstance?.AddEvent(new RevengeEvent(new(player), new(target))); + TryAddAfterMeetingDeathPlayers(CustomDeathReason.Revenge, target.PlayerId); target.SetRealKiller(player); Logger.Info($"{player.GetNameWithRole()}の道連れ先:{target.GetNameWithRole()}", "RevengeOnExile"); diff --git a/Patches/OutroPatch.cs b/Patches/OutroPatch.cs index cc8692371..dd656f379 100644 --- a/Patches/OutroPatch.cs +++ b/Patches/OutroPatch.cs @@ -10,6 +10,8 @@ using TownOfHost.Roles.Core; using TownOfHost.Templates; using static TownOfHost.Translator; +using TownOfHost.Modules.GameEventHistory; +using TownOfHost.Modules.GameEventHistory.Events; namespace TownOfHost { @@ -121,6 +123,9 @@ class SetEverythingUpPatch public static void Postfix(EndGameManager __instance) { if (!Main.playerVersion.ContainsKey(0)) return; + + EventHistory.CurrentInstance?.AddEvent(new GameEndEvent()); + //####################################### // ==勝利陣営表示== //####################################### diff --git a/Patches/PlayerContorolPatch.cs b/Patches/PlayerContorolPatch.cs index a9597840e..086f2e421 100644 --- a/Patches/PlayerContorolPatch.cs +++ b/Patches/PlayerContorolPatch.cs @@ -12,6 +12,8 @@ using TownOfHost.Roles.Core; using TownOfHost.Roles.Core.Interfaces; using TownOfHost.Roles.AddOns.Crewmate; +using TownOfHost.Modules.GameEventHistory; +using TownOfHost.Modules.GameEventHistory.Events; namespace TownOfHost { @@ -151,6 +153,8 @@ public static void Prefix(PlayerControl __instance, [HarmonyArgument(0)] PlayerC if (isSucceeded) { + EventHistory.CurrentInstance?.AddEvent(new MurderEvent(new(__instance), new(target), target.GetPlainShipRoom().RoomId)); + if (target.shapeshifting) { //シェイプシフトアニメーション中 @@ -371,6 +375,8 @@ public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] GameDat //以下、ボタンが押されることが確定したものとする。 //============================================= + EventHistory.CurrentInstance?.AddEvent(target == null ? new MeetingCallEvent(new(__instance)) : new MeetingCallEvent(new(__instance), new(target.Object))); + foreach (var role in CustomRoleManager.AllActiveRoles.Values) { role.OnReportDeadBody(__instance, target); @@ -689,6 +695,12 @@ public static bool Prefix(PlayerControl __instance) //属性クラスの扱いを決定するまで仮置き ret &= Workhorse.OnCompleteTask(pc); Utils.NotifyRoles(); + + if (taskState.IsTaskFinished && Utils.HasTasks(__instance.Data)) + { + EventHistory.CurrentInstance?.AddEvent(new CrewTaskFinishEvent(new(__instance))); + } + return ret; } public static void Postfix() diff --git a/Patches/onGameStartedPatch.cs b/Patches/onGameStartedPatch.cs index 56a14c6c7..6acf78df8 100644 --- a/Patches/onGameStartedPatch.cs +++ b/Patches/onGameStartedPatch.cs @@ -11,6 +11,8 @@ using TownOfHost.Roles.Core; using TownOfHost.Roles.AddOns.Common; using static TownOfHost.Translator; +using TownOfHost.Modules.GameEventHistory; +using TownOfHost.Modules.GameEventHistory.Events; namespace TownOfHost { @@ -104,6 +106,8 @@ public static void Postfix(AmongUsClient __instance) MeetingStates.MeetingCalled = false; MeetingStates.FirstMeeting = true; GameStates.AlreadyDied = false; + + EventHistory.CurrentInstance?.AddEvent(new GameStartEvent()); } } [HarmonyPatch(typeof(RoleManager), nameof(RoleManager.SelectRoles))] From 30efd998ad228cb30138af4e6b2ab60ce6510fa4 Mon Sep 17 00:00:00 2001 From: Hyz-sui <86903430+Hyz-sui@users.noreply.github.com> Date: Tue, 2 Apr 2024 23:05:56 +0900 Subject: [PATCH 08/13] feature: send history --- Patches/OutroPatch.cs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/Patches/OutroPatch.cs b/Patches/OutroPatch.cs index dd656f379..665948b63 100644 --- a/Patches/OutroPatch.cs +++ b/Patches/OutroPatch.cs @@ -254,22 +254,30 @@ public static void Postfix(EndGameManager __instance) // ==Discordに結果を送信== //####################################### - if (PlayerControl.LocalPlayer.PlayerId == 0 && Main.SendResultToDiscord.Value) + if (PlayerControl.LocalPlayer.PlayerId == 0) { if (CustomWinnerHolder.WinnerTeam == CustomWinner.Draw) Logger.Info("廃村のため試合結果の送信をキャンセル", "Webhook"); else { - var resultMessage = ""; - foreach (var id in Main.winnerList) + if (Main.SendResultToDiscord.Value) { - resultMessage += Utils.ColorIdToDiscordEmoji(Palette.PlayerColors.IndexOf(Main.PlayerColors[id]), !PlayerState.GetByPlayerId(id).IsDead) + ":star:" + EndGamePatch.SummaryText[id].RemoveHtmlTags() + "\n"; + var resultMessage = ""; + foreach (var id in Main.winnerList) + { + resultMessage += Utils.ColorIdToDiscordEmoji(Palette.PlayerColors.IndexOf(Main.PlayerColors[id]), !PlayerState.GetByPlayerId(id).IsDead) + ":star:" + EndGamePatch.SummaryText[id].RemoveHtmlTags() + "\n"; + } + foreach (var id in cloneRoles) + { + resultMessage += Utils.ColorIdToDiscordEmoji(Palette.PlayerColors.IndexOf(Main.PlayerColors[id]), !PlayerState.GetByPlayerId(id).IsDead) + "\u3000" + EndGamePatch.SummaryText[id].RemoveHtmlTags() + "\n"; + } + Utils.SendWebhook(resultMessage, GetString("LastResult")); } - foreach (var id in cloneRoles) + if (Main.SendHistoryToDiscord.Value) { - resultMessage += Utils.ColorIdToDiscordEmoji(Palette.PlayerColors.IndexOf(Main.PlayerColors[id]), !PlayerState.GetByPlayerId(id).IsDead) + "\u3000" + EndGamePatch.SummaryText[id].RemoveHtmlTags() + "\n"; + var historyMessage = EventHistory.CurrentInstance.ToDiscordString(); + Utils.SendWebhook(historyMessage, "ゲーム記録"); } - Utils.SendWebhook(resultMessage, GetString("LastResult")); } } From 573dc90c122092271205fb0f4098c7cf2e5069af Mon Sep 17 00:00:00 2001 From: Hyz-sui <86903430+Hyz-sui@users.noreply.github.com> Date: Tue, 2 Apr 2024 23:19:28 +0900 Subject: [PATCH 09/13] feature: use original avatar --- Modules/Utils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Utils.cs b/Modules/Utils.cs index ed378467b..4e3a6f30f 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -1151,7 +1151,7 @@ public static void SendWebhook(string text, string userName = "Town Of Host") { { "content", text }, { "username", userName }, - { "avatar_url", "https://raw.githubusercontent.com/tukasa0001/TownOfHost/main/Resources/TabIcon_MainSettings.png" } + { "avatar_url", "https://raw.githubusercontent.com/Hyz-sui/TownOfHost-H/images-H/Images/discord-avatar.png" } }; using StreamReader sr = new("WebhookUrl.txt", Encoding.UTF8); string webhookUrl = sr.ReadLine(); From dad48f7b2ea9204a1b0ab4f490820765b8895b18 Mon Sep 17 00:00:00 2001 From: Hyz-sui <86903430+Hyz-sui@users.noreply.github.com> Date: Wed, 3 Apr 2024 00:07:42 +0900 Subject: [PATCH 10/13] feature: string --- Resources/string.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/Resources/string.csv b/Resources/string.csv index 7767a1a67..6500dcffd 100644 --- a/Resources/string.csv +++ b/Resources/string.csv @@ -13,6 +13,7 @@ "EvilHackerInheritAbility","InheritAbility","死亡時、生存インポスターに能力を引き継ぐ","","","","","" "EvilHackerSkipUnoccupiedRooms","EvilHackerSkipUnoccupiedRooms","アドミン情報で誰もいない部屋を省略する","","","","","" "DiscordResult","Discord Result","Discordに試合結果を送信","","","","","" +"DiscordHistory","Discord History","Discordにゲーム記録を送信","","","","","" "ShowLobbySummary","Show Lobby Summary","ロビーで前の試合の結果を表示","","","","","" "CopyCode","Copy Code","部屋建て時にコードを自動でコピー","","","","","" "HauntFocusCrew","Haunt Focus crew","憑依開始時に生存者にフォーカス","","","","","" From 31f21b58922d2bd11bec3e3a5fe2c4fb479fa605 Mon Sep 17 00:00:00 2001 From: Hyz-sui <86903430+Hyz-sui@users.noreply.github.com> Date: Mon, 8 Apr 2024 20:39:08 +0900 Subject: [PATCH 11/13] fix: gameend winstext --- Modules/GameEventHistory/Events/GameEndEvent.cs | 5 +++-- Patches/OutroPatch.cs | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Modules/GameEventHistory/Events/GameEndEvent.cs b/Modules/GameEventHistory/Events/GameEndEvent.cs index 70487461c..e54a006cf 100644 --- a/Modules/GameEventHistory/Events/GameEndEvent.cs +++ b/Modules/GameEventHistory/Events/GameEndEvent.cs @@ -2,13 +2,14 @@ namespace TownOfHost.Modules.GameEventHistory.Events; -public sealed class GameEndEvent() : Event +public sealed class GameEndEvent(string winsText) : Event { public override string Bullet { get; } = ":green_circle:"; + public string WinsText { get; } = winsText; public override void AppendDiscordString(StringBuilder builder) { builder.Append("**ゲーム終了:** "); - builder.Append(SetEverythingUpPatch.LastWinsText); + builder.Append(WinsText); } } diff --git a/Patches/OutroPatch.cs b/Patches/OutroPatch.cs index 665948b63..ac4ae2987 100644 --- a/Patches/OutroPatch.cs +++ b/Patches/OutroPatch.cs @@ -124,8 +124,6 @@ public static void Postfix(EndGameManager __instance) { if (!Main.playerVersion.ContainsKey(0)) return; - EventHistory.CurrentInstance?.AddEvent(new GameEndEvent()); - //####################################### // ==勝利陣営表示== //####################################### @@ -198,6 +196,8 @@ public static void Postfix(EndGameManager __instance) } LastWinsText = WinnerText.text.RemoveHtmlTags(); + EventHistory.CurrentInstance?.AddEvent(new GameEndEvent(LastWinsText)); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //####################################### From 1c0862956f7194defdc07c3a3ee79885049f8565 Mon Sep 17 00:00:00 2001 From: Hyz-sui <86903430+Hyz-sui@users.noreply.github.com> Date: Tue, 9 Apr 2024 21:44:58 +0900 Subject: [PATCH 12/13] =?UTF-8?q?=E3=82=A6=E3=82=A7=E3=83=96=E3=83=95?= =?UTF-8?q?=E3=83=83=E3=82=AF=E3=81=AE=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF?= =?UTF-8?q?=E3=82=BF=20(#52)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feature: webhook request wrapper * feature: webhook builder * feature: manager * migrate to new webhook impl * remove legacy methods --- Modules/Utils.cs | 44 -------- Modules/Webhook/WebhookManager.cs | 127 +++++++++++++++++++++++ Modules/Webhook/WebhookMessageBuilder.cs | 13 +++ Modules/Webhook/WebhookRequest.cs | 11 ++ Patches/OutroPatch.cs | 29 ++++-- main.cs | 9 +- 6 files changed, 181 insertions(+), 52 deletions(-) create mode 100644 Modules/Webhook/WebhookManager.cs create mode 100644 Modules/Webhook/WebhookMessageBuilder.cs create mode 100644 Modules/Webhook/WebhookRequest.cs diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 4e3a6f30f..4c3018da3 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -1130,50 +1130,6 @@ public static void FlashColor(Color color, float duration = 1f) obj.GetComponent().color = new(color.r, color.g, color.b, Mathf.Clamp01((-2f * Mathf.Abs(t - 0.5f) + 1) * color.a)); //アルファ値を0→目標→0に変化させる }))); } - public static void MakeWebhookUrlFile() - { - Logger.Info("WebhookUrl.txtを作成", "Webhook"); - try - { - File.WriteAllText("WebhookUrl.txt", "この文章を削除してウェブフックのURLを記述/Remove this text and enter Webhook url"); - } - catch (Exception ex) - { - Logger.Error(ex.ToString(), "Webhook"); - } - } - public static void SendWebhook(string text, string userName = "Town Of Host") - { - if (!File.Exists("WebhookUrl.txt")) - MakeWebhookUrlFile(); - HttpClient client = new(); - Dictionary message = new() - { - { "content", text }, - { "username", userName }, - { "avatar_url", "https://raw.githubusercontent.com/Hyz-sui/TownOfHost-H/images-H/Images/discord-avatar.png" } - }; - using StreamReader sr = new("WebhookUrl.txt", Encoding.UTF8); - string webhookUrl = sr.ReadLine(); - if (!Regex.IsMatch(webhookUrl, "^(https://(ptb.|canary.)?discord(app)?.com/api/webhooks/)")) // ptbとcanaryとappはあってもなくてもいい - { - Logger.Info("WebhookUrl.txtの内容がdiscordのウェブフックurlではなかったためウェブフックの送信をキャンセル", "Webhook"); - return; - } - try - { - TaskAwaiter awaiter = client.PostAsync(webhookUrl, new FormUrlEncodedContent(message)).GetAwaiter(); - var response = awaiter.GetResult(); - Logger.Info("ウェブフックを送信しました", "Webhook"); - if (!response.IsSuccessStatusCode) - Logger.Warn("応答が異常です", "Webhook"); - Logger.Info($"{(int)response.StatusCode} {response.ReasonPhrase}", "Webhook"); // 正常な応答: 204 No Content - } - catch (Exception ex) - { - Logger.Error(ex.ToString(), "Webhook"); - } - } public static string ColorIdToDiscordEmoji(int colorId, bool alive) { if (alive) diff --git a/Modules/Webhook/WebhookManager.cs b/Modules/Webhook/WebhookManager.cs new file mode 100644 index 000000000..b7e8485cb --- /dev/null +++ b/Modules/Webhook/WebhookManager.cs @@ -0,0 +1,127 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace TownOfHost.Modules.Webhook; + +public sealed class WebhookManager : IDisposable +{ + public static WebhookManager Instance { get; } = new(); + + // see https://discord.com/developers/docs/resources/webhook#execute-webhook + private HttpClient httpClient = new() + { + Timeout = TimeSpan.FromSeconds(4), + }; + + private static readonly ILogHandler logger = Logger.Handler(nameof(WebhookManager)); + private bool disposedValue; + + public void StartSend(WebhookMessageBuilder builder) + { + if (!TryReadUrl(out var url)) + { + logger.Warn("URL設定が正しくありません"); + return; + } + var message = builder.ContentBuilder.ToString(); + var content = new WebhookRequest(message, builder.UserName, builder.AvatarUrl); + var sendTask = SendAsync(content, url); + sendTask.ContinueWith(task => + { + if (task.Exception is { } aggregateException) + { + logger.Warn("送信中に例外が発生しました"); + logger.Exception(aggregateException.InnerException); + } + }); + } + private bool TryReadUrl(out string url) + { + if (CreateConfigFileIfNecessary()) + { + url = null; + return false; + } + using var stream = WebhookUrlFile.OpenRead(); + using var reader = new StreamReader(stream, Encoding.UTF8); + var text = reader.ReadLine(); + if (ValidateUrl(text)) + { + url = text; + return true; + } + else + { + url = null; + return false; + } + } + public bool CreateConfigFileIfNecessary() + { + if (WebhookUrlFile.Exists) + { + return false; + } + using var stream = WebhookUrlFile.Create(); + using var writer = new StreamWriter(stream, Encoding.UTF8); + writer.WriteLine("この文章をすべて削除してウェブフックのURLを入力し,上書き保存してください"); + return true; + } + private bool ValidateUrl(string url) + { + if (string.IsNullOrWhiteSpace(url)) + { + return false; + } + return webhookUrlRegex.IsMatch(url); + } + public async Task SendAsync(WebhookRequest webhookRequest, string url, CancellationToken cancellationToken = default) + { + try + { + var response = await httpClient.PostAsJsonAsync(url, webhookRequest, cancellationToken); + logger.Info($"{(int)response.StatusCode} {response.ReasonPhrase}"); + if (!response.IsSuccessStatusCode) + { + logger.Warn("送信に失敗"); + } + } + catch (TaskCanceledException taskCanceledException) + { + logger.Warn("送信はキャンセルされました"); + logger.Exception(taskCanceledException); + } + } + + private void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + httpClient.Dispose(); + } + disposedValue = true; + } + } + public void Dispose() + { + // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public FileInfo WebhookUrlFile { get; } = +#if DEBUG + new("DebugWebhookUrl.txt"); +#else + new("WebhookUrl.txt"); +#endif + private readonly Regex webhookUrlRegex = new("^(https://(ptb.|canary.)?discord(app)?.com/api/webhooks/)"); +} diff --git a/Modules/Webhook/WebhookMessageBuilder.cs b/Modules/Webhook/WebhookMessageBuilder.cs new file mode 100644 index 000000000..b3ee5bfa9 --- /dev/null +++ b/Modules/Webhook/WebhookMessageBuilder.cs @@ -0,0 +1,13 @@ +using System.Text; + +namespace TownOfHost.Modules.Webhook; + +public sealed class WebhookMessageBuilder +{ + public StringBuilder ContentBuilder { get; } = new(); + public string UserName { get; init; } = DefaultUserName; + public string AvatarUrl { get; init; } = DefaultAvatarUrl; + + private const string DefaultUserName = "TownOfHost-H"; + private const string DefaultAvatarUrl = "https://raw.githubusercontent.com/Hyz-sui/TownOfHost-H/images-H/Images/discord-avatar.png"; +} diff --git a/Modules/Webhook/WebhookRequest.cs b/Modules/Webhook/WebhookRequest.cs new file mode 100644 index 000000000..29ddaff1f --- /dev/null +++ b/Modules/Webhook/WebhookRequest.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace TownOfHost.Modules.Webhook; + +public readonly record struct WebhookRequest( + [property: JsonPropertyName("content")] + string Content, + [property: JsonPropertyName("username")] + string UserName, + [property: JsonPropertyName("avatar_url")] + string AvatarUrl); diff --git a/Patches/OutroPatch.cs b/Patches/OutroPatch.cs index ac4ae2987..e118c83f5 100644 --- a/Patches/OutroPatch.cs +++ b/Patches/OutroPatch.cs @@ -12,6 +12,7 @@ using static TownOfHost.Translator; using TownOfHost.Modules.GameEventHistory; using TownOfHost.Modules.GameEventHistory.Events; +using TownOfHost.Modules.Webhook; namespace TownOfHost { @@ -257,26 +258,42 @@ public static void Postfix(EndGameManager __instance) if (PlayerControl.LocalPlayer.PlayerId == 0) { if (CustomWinnerHolder.WinnerTeam == CustomWinner.Draw) + { Logger.Info("廃村のため試合結果の送信をキャンセル", "Webhook"); + } else { + var resultMessageBuilder = new WebhookMessageBuilder() + { + UserName = "試合結果", + }; if (Main.SendResultToDiscord.Value) { - var resultMessage = ""; + resultMessageBuilder.ContentBuilder.AppendLine("### 各プレイヤーの最終結果"); foreach (var id in Main.winnerList) { - resultMessage += Utils.ColorIdToDiscordEmoji(Palette.PlayerColors.IndexOf(Main.PlayerColors[id]), !PlayerState.GetByPlayerId(id).IsDead) + ":star:" + EndGamePatch.SummaryText[id].RemoveHtmlTags() + "\n"; + resultMessageBuilder.ContentBuilder.Append(Utils.ColorIdToDiscordEmoji(Palette.PlayerColors.IndexOf(Main.PlayerColors[id]), !PlayerState.GetByPlayerId(id).IsDead)); + resultMessageBuilder.ContentBuilder.Append(":star:"); + resultMessageBuilder.ContentBuilder.Append(EndGamePatch.SummaryText[id].RemoveHtmlTags()); + resultMessageBuilder.ContentBuilder.AppendLine(); } foreach (var id in cloneRoles) { - resultMessage += Utils.ColorIdToDiscordEmoji(Palette.PlayerColors.IndexOf(Main.PlayerColors[id]), !PlayerState.GetByPlayerId(id).IsDead) + "\u3000" + EndGamePatch.SummaryText[id].RemoveHtmlTags() + "\n"; + resultMessageBuilder.ContentBuilder.Append(Utils.ColorIdToDiscordEmoji(Palette.PlayerColors.IndexOf(Main.PlayerColors[id]), !PlayerState.GetByPlayerId(id).IsDead)); + resultMessageBuilder.ContentBuilder.Append('\u3000'); + resultMessageBuilder.ContentBuilder.Append(EndGamePatch.SummaryText[id].RemoveHtmlTags()); + resultMessageBuilder.ContentBuilder.AppendLine(); } - Utils.SendWebhook(resultMessage, GetString("LastResult")); } if (Main.SendHistoryToDiscord.Value) { - var historyMessage = EventHistory.CurrentInstance.ToDiscordString(); - Utils.SendWebhook(historyMessage, "ゲーム記録"); + resultMessageBuilder.ContentBuilder.AppendLine("### 記録"); + EventHistory.CurrentInstance.AppendDiscordString(resultMessageBuilder.ContentBuilder); + } + + if (resultMessageBuilder.ContentBuilder.Length > 0) + { + WebhookManager.Instance.StartSend(resultMessageBuilder); } } } diff --git a/main.cs b/main.cs index bf1f1e62a..86a005dbb 100644 --- a/main.cs +++ b/main.cs @@ -16,6 +16,7 @@ using TownOfHost.Attributes; using TownOfHost.Modules; using TownOfHost.Roles.Core; +using TownOfHost.Modules.Webhook; [assembly: AssemblyFileVersionAttribute(TownOfHost.Main.PluginVersion)] [assembly: AssemblyInformationalVersionAttribute(TownOfHost.Main.PluginVersion)] @@ -241,13 +242,17 @@ public override void Load() ClassInjector.RegisterTypeInIl2Cpp(); - if (!File.Exists("WebhookUrl.txt")) - Utils.MakeWebhookUrlFile(); + WebhookManager.Instance.CreateConfigFileIfNecessary(); SystemEnvironment.SetEnvironmentVariables(); Harmony.PatchAll(); } + public override bool Unload() + { + WebhookManager.Instance.Dispose(); + return false; + } } public enum CustomDeathReason { From 1e6ed1a2fb761dfee2f0dbf696d518181eb9bf02 Mon Sep 17 00:00:00 2001 From: Hyz-sui <86903430+Hyz-sui@users.noreply.github.com> Date: Wed, 10 Apr 2024 18:10:10 +0900 Subject: [PATCH 13/13] version 2024.3.5.3 --- main.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.cs b/main.cs index 86a005dbb..b616be7de 100644 --- a/main.cs +++ b/main.cs @@ -55,7 +55,7 @@ public class Main : BasePlugin // ========== //Sorry for many Japanese comments. - public static readonly string ForkVersion = "2024.3.5.2"; + public static readonly string ForkVersion = "2024.3.5.3"; public static readonly Version ParsedForkVersion = Version.Parse(ForkVersion); public const string PluginGuid = "com.emptybottle.townofhost";