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("**"); + } +} 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()) { } +} 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); +} 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..e54a006cf --- /dev/null +++ b/Modules/GameEventHistory/Events/GameEndEvent.cs @@ -0,0 +1,15 @@ +using System.Text; + +namespace TownOfHost.Modules.GameEventHistory.Events; + +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(WinsText); + } +} 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())); + } +} 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); +} 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/Modules/Utils.cs b/Modules/Utils.cs index ed378467b..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/tukasa0001/TownOfHost/main/Resources/TabIcon_MainSettings.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/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/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..e118c83f5 100644 --- a/Patches/OutroPatch.cs +++ b/Patches/OutroPatch.cs @@ -10,6 +10,9 @@ using TownOfHost.Roles.Core; using TownOfHost.Templates; using static TownOfHost.Translator; +using TownOfHost.Modules.GameEventHistory; +using TownOfHost.Modules.GameEventHistory.Events; +using TownOfHost.Modules.Webhook; namespace TownOfHost { @@ -121,6 +124,7 @@ class SetEverythingUpPatch public static void Postfix(EndGameManager __instance) { if (!Main.playerVersion.ContainsKey(0)) return; + //####################################### // ==勝利陣営表示== //####################################### @@ -193,6 +197,8 @@ public static void Postfix(EndGameManager __instance) } LastWinsText = WinnerText.text.RemoveHtmlTags(); + EventHistory.CurrentInstance?.AddEvent(new GameEndEvent(LastWinsText)); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //####################################### @@ -249,22 +255,46 @@ 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) + var resultMessageBuilder = new WebhookMessageBuilder() + { + UserName = "試合結果", + }; + if (Main.SendResultToDiscord.Value) { - resultMessage += Utils.ColorIdToDiscordEmoji(Palette.PlayerColors.IndexOf(Main.PlayerColors[id]), !PlayerState.GetByPlayerId(id).IsDead) + ":star:" + EndGamePatch.SummaryText[id].RemoveHtmlTags() + "\n"; + resultMessageBuilder.ContentBuilder.AppendLine("### 各プレイヤーの最終結果"); + foreach (var id in Main.winnerList) + { + 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) + { + 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(); + } } - foreach (var id in cloneRoles) + if (Main.SendHistoryToDiscord.Value) + { + resultMessageBuilder.ContentBuilder.AppendLine("### 記録"); + EventHistory.CurrentInstance.AppendDiscordString(resultMessageBuilder.ContentBuilder); + } + + if (resultMessageBuilder.ContentBuilder.Length > 0) { - resultMessage += Utils.ColorIdToDiscordEmoji(Palette.PlayerColors.IndexOf(Main.PlayerColors[id]), !PlayerState.GetByPlayerId(id).IsDead) + "\u3000" + EndGamePatch.SummaryText[id].RemoveHtmlTags() + "\n"; + WebhookManager.Instance.StartSend(resultMessageBuilder); } - Utils.SendWebhook(resultMessage, GetString("LastResult")); } } 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))] 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","憑依開始時に生存者にフォーカス","","","","","" diff --git a/main.cs b/main.cs index c50322183..b616be7de 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)] @@ -54,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"; @@ -78,6 +79,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 +145,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); @@ -239,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 {