diff --git a/ApmWindow.xaml.cs b/ApmWindow.xaml.cs index c14a727..7402b1b 100644 --- a/ApmWindow.xaml.cs +++ b/ApmWindow.xaml.cs @@ -243,7 +243,7 @@ namespace AnotherReplayReader var plotter = await _plotter; var dump = new EventDump(); dump.LoadStringHashes(); - dump.SetDumpData(_replay.Mod, plotter); + dump.SetDumpData(plotter); await dump.ShowPlainText(); } } diff --git a/EventDump.xaml.cs b/EventDump.xaml.cs index a7854a6..74d8018 100644 --- a/EventDump.xaml.cs +++ b/EventDump.xaml.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics; using System.IO; using System.Linq; using System.Text; @@ -15,7 +14,6 @@ using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; -using System.Windows.Markup; using System.Windows.Threading; using System.Xml.Linq; @@ -33,27 +31,24 @@ namespace AnotherReplayReader VeryCompactedForAI, } - private class Model( - Mod mod, - ImmutableSortedDictionary players, - ImmutableArray<(TimeSpan, ImmutableArray)> commands, - CompactLevel level + private record Model( + Replay? Replay, + ImmutableSortedDictionary Players, + ImmutableArray<(TimeSpan, ImmutableArray)> Commands, + CompactLevel Level ) { - public Mod Mod { get; } = mod; - public ImmutableSortedDictionary Players { get; } = players; - public ImmutableArray<(TimeSpan, ImmutableArray)> Commands { get; } = commands; - public CompactLevel Level { get; } = level; - public ImmutableSortedDictionary? PlayersNamesForAI { get; } = level <= CompactLevel.NoCompact + public Mod Mod => Replay?.Mod ?? new("RA3"); + public ImmutableSortedDictionary? PlayersNamesForAI { get; } = Replay is null || Level <= CompactLevel.NoCompact ? null - : AIAnalyze.PlayerNamesForAI(mod, players); - + : AIAnalyze.PlayerNamesForAI(Replay.Mod, Players); public bool IsDefault => Players.IsEmpty && Commands.IsEmpty; public Model() : this( - new("RA3"), + null, ImmutableSortedDictionary.Empty, - ImmutableArray<(TimeSpan, ImmutableArray)>.Empty, CompactLevel.NoCompact + ImmutableArray<(TimeSpan, ImmutableArray)>.Empty, + CompactLevel.NoCompact ) { } @@ -81,6 +76,7 @@ namespace AnotherReplayReader private readonly CancellationTokenSource _cancellation = new(); private Model _model = new(); private string? _cached; + private AIAnalyze.TimeIndexedPrefixSums? _cachedPrefixSums; private DateTimeOffset _aiStartTime; public EventDump() @@ -140,10 +136,10 @@ namespace AnotherReplayReader x => x.Attribute("Text")!.Value); } - internal void SetDumpData(Mod mod, ApmPlotter plotter) + internal void SetDumpData(ApmPlotter plotter) { var level = (CompactLevel)_compactLevelComboBox.SelectedIndex; - _model = new Model(mod, plotter.PlayersMap, plotter.Commands, level); + _model = new Model(plotter.Replay, plotter.PlayersMap, plotter.Commands, level); } public async Task ShowPlainText() @@ -171,16 +167,22 @@ namespace AnotherReplayReader _textBox.Text = "正在加载,请稍候"; _tokenUsageLabel.Content = ""; Show(); - var text = await Task.Run(() => GeneratePlainText(_model)); + var (text, prefixSums) = await Task.Run(() => + { + var text = GeneratePlainText(_model, out var prefixSums); + return (text, prefixSums); + }); var (bytesCount, estimatedTokenCount) = AIAnalyze.EstimateTokenCount(text); _textBox.Text = text; _cached = text; + _cachedPrefixSums = prefixSums; // display KB and K tokens in _tokenUsageLabel _tokenUsageLabel.Content = $"大小: {bytesCount / 1024.0:0.00} KiB,估计Token数: {estimatedTokenCount / 1000.0:0.00} K"; } - private static string GeneratePlainText(Model model) + private static string GeneratePlainText(Model model, out AIAnalyze.TimeIndexedPrefixSums prefixSums) { + prefixSums = new([], []); var sb = new StringBuilder(); for (int chunkIndex = 0; chunkIndex < model.Commands.Length; ++chunkIndex) { @@ -192,6 +194,7 @@ namespace AnotherReplayReader { continue; } + prefixSums.Add(time, filtered.Count); sb.AppendLine($"[{TimeStampToString(time, model.Level)}]"); foreach (var command in filtered) { @@ -227,7 +230,7 @@ namespace AnotherReplayReader 0x205 or 0x206 when i == 3 => $"序列:{ProductionQueueTypeToString((int)value)}", 0x207 when i == 1 && j == 1 => $"序列:{ProductionQueueTypeToString((int)value)}", 0x207 or 0x208 or 0x209 when i == 0 => $"{text}(建造者)", - 0x517 or 0x518 when i == 0 => $"{text}(出兵建筑)", + 0x205 or 0x206 when i == 0 => $"{text}(出兵建筑)", 0x252 => $"{model.PlayerNameByGameSlotIndex((int)command.Data[0].Value)}已主动退出游戏", 0x22E when i == 0 => j == 0 ? StanceToString((int)value) : null, _ => text, @@ -246,6 +249,10 @@ namespace AnotherReplayReader { return true; } + if (level == CompactLevel.NoCompact) + { + return true; + } if (ApmPlotter.IsUnknown(commandId)) { return false; @@ -418,7 +425,7 @@ namespace AnotherReplayReader private async void OnCompactLevelComboBoxSelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) { var selectedLevel = (CompactLevel)_compactLevelComboBox.SelectedIndex; - _model = new Model(_model.Mod, _model.Players, _model.Commands, selectedLevel); + _model = new Model(_model.Replay, _model.Players, _model.Commands, selectedLevel); await ShowPlainText(); } @@ -438,6 +445,9 @@ namespace AnotherReplayReader _tokensDetailsTextBlock.Text = ""; _extraStatusTextBlock.Text = ""; + _aiTextBox.Clear(); + _aiReasoningTextBox.Clear(); + var pending = new ConcurrentQueue(); var emaSpeedCalculator = new AIAnalyzeUI.EmaSpeed(); var totalCharacters = 0; @@ -491,9 +501,13 @@ namespace AnotherReplayReader timer.Start(); try { - await LaunchAIAnalyze(cached, pending.Enqueue); + await LaunchAIAnalyze(cached, _cachedPrefixSums, pending.Enqueue); TimerUpdateStatus(this, EventArgs.Empty); } + catch (OperationCanceledException) + { + MessageBox.Show(this, "AI分析已取消"); + } catch (Exception ex) { MessageBox.Show(this, $"AI分析失败: {ex}"); @@ -505,8 +519,13 @@ namespace AnotherReplayReader } } - private async Task LaunchAIAnalyze(string replayData, Action newContent) + private async Task LaunchAIAnalyze( + string replayData, + AIAnalyze.TimeIndexedPrefixSums eventCounts, + Action newContent + ) { + AIAnalyzeUI.Debug.Clear(); void AddExtraContent(string message) { var delta = new AIAnalyze.AIChunk @@ -519,10 +538,15 @@ namespace AnotherReplayReader newContent(new(Delta: delta, TimeStamp: null, IsExtra: true)); } - // var analyzer = new AIAnalyze("https://integrate.api.nvidia.com/v1/", "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"); var analyzer = new AIAnalyze("https://integrate.api.nvidia.com/v1/", "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"); + // var analyzer = new AIAnalyze("https://api.deepseek.com", "sk-a6ffa8e74bfc419bbb4722b5d4c79907"); var deepSeekExtraParams = new Dictionary { + ["model"] = "deepseek-ai/deepseek-v4-flash", // nvidia + // ["model"] = "nvidia/nemotron-3-super-120b-a12b", "nvidia/nemotron-3-nano-30b-a3b" // slow + // ["model"] = "nvidia/nemotron-3-nano-30b-a3b", // not smark + // ["model"] = "deepseek-v4-flash", // deepseek official + ["temperature"] = 0.75, ["top_p"] = 0.95, ["max_tokens"] = 16384, @@ -534,8 +558,9 @@ namespace AnotherReplayReader ["reasoning_effort"] = "high", ["thinking"] = new { type = "enabled" } }; - var systemPrompt = AIAnalyze.GetSystemPrompt(_model.Mod, _model.Players); - var userPrompt = AIAnalyze.BuildUserPrompt(_model.Mod, _model.Players, replayData); + var systemPrompt = AIAnalyze.GetSystemPrompt(_model.Replay ?? throw new InvalidOperationException(), _model.Players); + var userPrompt = AIAnalyze.BuildUserPrompt(_model.Mod, _model.Players, replayData, out var userPromptPrefix); + AddExtraContent($"--- // 输入\r\n{userPromptPrefix}[输入:操作信息流水账]\r\n---\r\n"); #region info var (systemPromptSize, systemPromptTokenCount) = AIAnalyze.EstimateTokenCount(systemPrompt); var (userPromptSize, userPromptTokenCount) = AIAnalyze.EstimateTokenCount(userPrompt); @@ -549,7 +574,7 @@ namespace AnotherReplayReader _extraStatusTextBlock.Text = "AI正在了解录像……"; _aiStartTime = DateTimeOffset.UtcNow; var firstReader = AIAnalyzeUI.BuildAIChunkReader(newContent); - var result = await Task.Run(() => analyzer.AnalyzeAsync("deepseek-ai/deepseek-v4-flash", + var result = await Task.Run(() => analyzer.AnalyzeAsync( systemPrompt, userPrompt, deepSeekExtraParams, @@ -557,19 +582,43 @@ namespace AnotherReplayReader _cancellation.Token )); _tokensDetailsTextBlock.Text = $"Token: {result.TotalTokens}(输入{result.PromptTokens})"; + + AddExtraContent("\r\n--- // 分段大小\r\n"); + foreach (var (Start, End, Description) in result.Segments) + { + var count = eventCounts.Query(Start, End); + AddExtraContent($"[{Start:mm\\:ss\\.ff} - {End:mm\\:ss\\.ff}],事件数: {count}\r\n"); + } + AddExtraContent("\r\n--- // 开始分段分析\r\n"); while (result.CurrentSegment < result.Segments.Count) { + var (Start, End, Description) = result.Segments[result.CurrentSegment]; var currentSegmentName = $"{result.CurrentSegment + 1}"; _extraStatusTextBlock.Text = $"AI正在分析录像{currentSegmentName}/{result.Segments.Count}"; var segmentReader = AIAnalyzeUI.BuildAIChunkReader(newContent); + var eventCount = eventCounts.Query(Start, End); + var segmentUserPrompt = AIAnalyze.BuildSegmentUserPrompt(result.Segments, result.CurrentSegment, eventCount); + AddExtraContent($"--- // 输入\r\n{segmentUserPrompt}\r\n---\r\n"); result = await Task.Run(() => analyzer.ContinueAnalyzeAsync( segmentReader, + segmentUserPrompt, _cancellation.Token )); _tokensDetailsTextBlock.Text = $"Token: {result.TotalTokens}(输入{result.PromptTokens})"; AddExtraContent($"\r\n--- // 第{currentSegmentName}段已分析完毕\r\n"); } + var totalEvents = eventCounts.GetTotal(); + var finalUserPrompt = AIAnalyze.BuildFinalUserPrompt(totalEvents); + AddExtraContent($"--- // 输入\r\n{finalUserPrompt}\r\n---\r\n"); + _extraStatusTextBlock.Text = $"AI正在分析录像总结"; + result = await Task.Run(() => analyzer.FinishAnalyzeAsync( + AIAnalyzeUI.BuildAIChunkReader(newContent), + finalUserPrompt, + _cancellation.Token + )); + _tokensDetailsTextBlock.Text = $"Token: {result.TotalTokens}(输入{result.PromptTokens})"; + AddExtraContent($"\r\n--- // 录像分析完毕\r\n"); } } } diff --git a/Utils/AIAnalyze.cs b/Utils/AIAnalyze.cs index e264c55..14dc1f9 100644 --- a/Utils/AIAnalyze.cs +++ b/Utils/AIAnalyze.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.Tracing; using System.IO; using System.Linq; using System.Net.Http; @@ -11,6 +12,7 @@ using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using System.Windows; namespace AnotherReplayReader.Utils { @@ -29,10 +31,12 @@ namespace AnotherReplayReader.Utils public string Text; } + public record Segment(TimeSpan Start, TimeSpan End, string Description); + public struct Result { public string Response; - public List<(TimeSpan Start, TimeSpan End, string Description)> Segments; + public List Segments; public int CurrentSegment; public int? PromptTokens; public int? CompletionTokens; @@ -40,10 +44,93 @@ namespace AnotherReplayReader.Utils public int? ReasoningTokens; } + public record TimeIndexedPrefixSums(List Times, List PrefixSums) + { + public void Add(TimeSpan time, int value) + { + if (Times.Count > 0 && time < Times.Last()) + { + throw new ArgumentException("Time must be added in non-decreasing order."); + } + Times.Add(time); + PrefixSums.Add((PrefixSums.LastOrDefault()) + value); + } + + public int Query(TimeSpan start, TimeSpan end) + { + var times = Times; + var prefix = PrefixSums; + + int startIndex = LowerBound(times, start); + int endIndex = UpperBound(times, end); + + if (startIndex >= times.Count || endIndex < 0 || startIndex > endIndex) + { + return 0; + } + + int result = prefix[endIndex]; + + if (startIndex > 0) + { + result -= prefix[startIndex - 1]; + } + + return result; + } + + public int GetTotal() + { + return PrefixSums.LastOrDefault(); + } + + public static int LowerBound(List arr, TimeSpan target) + { + int left = 0, right = arr.Count; + + while (left < right) + { + int mid = left + (right - left) / 2; + + if (arr[mid] < target) + { + left = mid + 1; + } + else + { + right = mid; + } + } + + return left; + } + + public static int UpperBound(List arr, TimeSpan target) + { + int left = 0, right = arr.Count; + + while (left < right) + { + int mid = left + (right - left) / 2; + + if (arr[mid] <= target) + { + left = mid + 1; + } + else + { + right = mid; + } + } + + return left - 1; + } + } + private readonly HttpClient _http; private readonly List _messages = []; private readonly Dictionary _state = []; - private readonly List<(TimeSpan Start, TimeSpan End, string Description)> _segments = []; + private readonly List _segments = []; private int _currentSegment = -1; public AIAnalyze(string baseUrl, string apiKey) @@ -58,11 +145,12 @@ namespace AnotherReplayReader.Utils new AuthenticationHeaderValue("Bearer", apiKey); } - public static string GetSystemPrompt(Mod mod, ImmutableSortedDictionary players) + public static string GetSystemPrompt(Replay replay, ImmutableSortedDictionary players) { var generalDescriptions = @" 你是一位 RTS 游戏数据分析师,你擅长从大量数据中发现有趣的规律和细节。 +用户则是一位玩家,用户会向你提供玩家操作记录,你要对其进行分析。 # 输入格式 ## 用户初始输入 @@ -96,8 +184,8 @@ namespace AnotherReplayReader.Utils # 输出要求 ## 1. 初始阶段 触发条件:用户输入包含:""判断是否需要分段处理数据"" -- 进行简单的推理,列举你的发现,目标:判断录像是否应该分段分析 -- 输出:对录像的分段(1~5个),各个分段的开始时间和结束时间,以及简短介绍。分段应按照时间排序,分段之间可以有一定的重叠 +- 进行简单的推理,列举你的发现,目标:判断玩家操作记录是否应该分段分析 +- 输出:对玩家操作记录的分段,各个分段的开始时间和结束时间,以及简短介绍。分段应按照时间排序,分段之间可以有一定的重叠 - 输出示例: ``` [分段列表] @@ -109,20 +197,23 @@ namespace AnotherReplayReader.Utils - 输出示例(假如判断不需要分段): ``` [分段列表] -#1 [0:00.0]~[17:23] 从开局到录像结束 +#1 [0:00.0]~[17:23] 从开局到玩家操作记录结束 ``` ## 2. 分段分析、推理阶段 触发条件:用户输入类似于:""请分析第N段([BEGIN]至[END])的数据"" - 选取该阶段的主要事件,以及和它们的上下文 - 也可以选择数个其他有分析价值的事件 -- 避免事无巨细的列出所有事件 +- 推理思考时:不要直接列出所有操作信息,可以先只列出一部分,然后按需向前以及向后“延申” +- 值得列出的、值得反复确认的运营类操作信息:开始建造、摆放建筑、出售建筑 +- **不是**运营类操作信息:重新选择单位、创建编队、选择编队、移动、攻击等。 +- 当你在思考时:你可以首先从数量较少的运营类操作信息开始,然后找到可能与其相关的其他操作信息,综合进行推理。不要直接按照时间线列出所有操作信息。 - 按照**推理指南**进行详细的思考与推理,列举你的推理与发现 - 输出:该阶段的各个主要事件,以及你的推理和发现 ## 3. 最终总结阶段 -触发条件:用户输入:""请对以上内容进行总结"" -- 输出:所有分析的总结,以及对局的完整介绍 +触发条件:用户输入包含:""请对以上内容进行总结"" +- 输出:所有分析的总结,以及这次对局的完整介绍 # 推理指南 推理需要分成多个阶段 @@ -132,7 +223,7 @@ namespace AnotherReplayReader.Utils 4. 进一步思考(可选) ## 示例1 -事件(应当视为输入,你推理时不需要原样输出所有事件,只输出少数关键事件): +操作信息(应当视为输入,你推理时不需要原样输出所有操作,只输出少数关键事件): ``` [6:58.13] PlayerC: 集火攻击 @@ -152,7 +243,7 @@ PlayerA: 释放特殊能力(无目标) - PlayerC 正在攻击2806, - PlayerA 让2806使用了快速返航的技能(SpecialPowerReturnToProducer) 分析: -- 拥有快速返航技能的单位一般是飞行器 +- 拥有快速返航技能的单位一般是固定翼飞行器 - 能够攻击飞行器的单位是拥有对空能力的 推理: - PlayerC 选择的单位很可能是拥有对空能力的单位 @@ -163,7 +254,7 @@ PlayerA: 释放特殊能力(无目标) ## 示例2 -事件(应当视为输入,你推理时不需要原样输出所有事件,只输出少数关键事件): +操作信息(应当视为输入,你推理时不需要原样输出所有操作,只输出少数关键事件): ``` [0:01.53] PlayerA: 开始建造 @@ -218,7 +309,7 @@ PlayerA: 摆放建筑 ## 示例3 -事件(应当视为输入,你推理时不需要原样输出所有事件,只输出少数关键事件): +操作信息(应当视为输入,你推理时不需要原样输出所有操作,只输出少数关键事件): ``` [16:03.33] PlayerC: 释放特殊能力(无目标) @@ -252,13 +343,13 @@ PlayerC: 释放特殊能力(无目标) - 同一个单位不可能在一秒内返航5次 - PlayerC应该是在急切的快速点击这个技能,试图让10067尽快返航 推理: -- PlayerC可能正在操作一个空军单位,这个单位可能受到了敌方的攻击 +- PlayerC可能正在操作一个固定翼飞机(例如战斗机),这个单位可能受到了敌方的攻击 - 因此PlayerC想让它尽快撤离、保住这个单位 - 这个时间段的局势可能较为紧张,因此PlayerC在高频操作 ## 示例4 -事件(应当视为输入,你推理时不需要原样输出所有事件,只输出少数关键事件): +操作信息(应当视为输入,你推理时不需要原样输出所有操作,只输出少数关键事件): ``` [10:35.53] PlayerE: 出售建筑 @@ -319,7 +410,7 @@ Player2: [游戏结束] ## 示例5 -事件(应当视为输入,你推理时不需要原样输出所有事件,只输出少数关键事件): +操作信息(应当视为输入,你推理时不需要原样输出所有操作,只输出少数关键事件): ``` [12:13.33] PlayerX: 释放特殊能力(指定位置) @@ -393,7 +484,73 @@ PlayerY: 摆放建筑 - PlayerY尝试在原地重建建筑、试图东山再起、准备反击 -你需要对录像中的各个主要事件都启用上面这样的思考模式。 +## 示例6: +操作信息(应当视为输入,你推理时不需要原样输出所有操作,只输出少数关键事件): +``` +[7:18.73] +PlayerA: 开始维修建筑 + [UnitId]2241 + +[7:19.13] +PlayerA: 开始建造 + [UnitId]2241(建造者) + AlliedWallPiece,序列:其他建筑 + +[7:19.33] +PlayerA: 开始建造 + [UnitId]2241(建造者) + AlliedWallPiece,序列:其他建筑 + +[8:30.60] +PlayerA: 释放特殊能力(无目标) + SpecialPower_PackReplaceSelf,0,1 + [UnitId]2241,0 + +[8:30.73] +PlayerA: 选择相同单位 + False,False + [UnitId]4518 + +[8:31.06] +PlayerA: 队形操作 + (X=2493,Y=2374,Z=280) + 2.59 + 4 + False,False + +[8:31.13] +PlayerA: 开始出兵 + [UnitId]291(出兵建筑) + AlliedEngineer + 序列:步兵 + +[8:43.73] +PlayerA: 开始出兵 + [UnitId]1958(出兵建筑) + AlliedMCV + 序列:载具 +``` +事件时间段[7:18]~[8:44] +观察: +- PlayerA开始维修主基地 +- PlayerA建造围墙 +- 约一分钟后,PlayerA主基地打包成基地车 +- PlayerA开始出工程师(AlliedEngineer) +- PlayerA开始建造基地车(AlliedMCV) +分析: +- PlayerA的主基地受到伤害,因此开始维修,但接下来一分钟没有其他操作 +- 工程师进驻己方建筑可以进行维修,而且效率远高于普通维修 +- PlayerA最终开始生产基地车 +推理: +- 主基地凭借自身的高血量,承受了超过一分钟的攻击 +- PlayerA选择出工程师,说明主基地在长时间承受攻击后,已经非常危险,急需更高效率的维修 +- PlayerA试图操作基地车令其移动 +- 基地车可能没有活下来,PlayerA开始建造第二辆基地车 +进一步思考: +- PlayerA应该是做好了主基地被摧毁的准备,因此开始提前准备第二辆基地车 + + +你需要对玩家操作记录中的各个主要事件都启用上面这样的思考模式。 # 背景信息 @@ -425,7 +582,12 @@ PlayerY: 摆放建筑 - 玩家已经解锁了多个协议 游戏不一定总是能持续到后期。 -出兵建筑、防御塔、超级武器都需要消耗电力。假如电力不足,出兵建筑的效率会大幅降低,防御塔和超级武器会直接停摆。 +消耗电力的建筑: +出兵建筑、矿场、防御塔、超级武器都会消耗电力。 +出兵建筑包括兵营、重工、船厂、机场。 +假如电力不足,防御塔和超级武器会直接停摆,出兵建筑的效率会大幅降低。 +因此,有时候存在:出售兵营/防御塔等建筑,以避免电力不足的情况。 + 不同的生产序列可以并行执行,举例: 开始建造,[UnitId]1(建造者),PowerPlant,序列:主要建筑 @@ -467,6 +629,16 @@ PlayerY: 摆放建筑 - 用于攻击地图中立建筑物或者友军 +建筑一般既可以摆放在陆地上,也可以摆放在海上,除非特殊注明。 +- 兵营和重工只能摆放在陆地上。 +- 船厂只能摆放在海上。 +其他建筑既可以摆放在陆地上,也可以摆放在海上。(例如机场、防御塔) + +步兵、载具只能在陆地上移动,无法下水,除非特殊注明。 +- 被标为两栖的单位可以在海上移动 +船只、军舰,只能在海上移动,无法上岸,除非特殊注明。 +- 被标为两栖的单位可以在陆地上移动 + 接下来我还会为你附上各个阵营的介绍 "; @@ -484,12 +656,12 @@ PlayerY: 摆放建筑 盟军的建筑在建造完毕之后不会出现在战场上,也不需要提前选择建筑的位置,摆放建筑时它会瞬间从地里冒出来。这个特色让盟军可以预留一个造好的防御塔,但先不摆放建筑,之后可以瞬间让防御塔摆放在需要的位置。 因此,盟军的“摆放建筑”代表建造已经完毕,而其他阵营的“摆放建筑”则代表建造还没开始。 盟军常用建筑与升级: -- 兵营(AlliedBarracks):生产步兵单位。 +- 兵营(AlliedBarracks):生产步兵单位。只能摆放在陆地上。 - 电厂(AlliedPowerPlant):提供电力,解锁矿场和机场。 -- 矿厂(AlliedRefinery):提供收入,解锁重工、船厂和科技 +- 矿厂(AlliedRefinery):提供收入,解锁重工、船厂和科技。 - 机场(AlliedAirfield):生产空军单位。 -- 重工(AlliedWarFactory):生产装甲车辆。 -- 船厂(AlliedNavalYard):生产海军单位。 +- 重工(AlliedWarFactory):生产装甲车辆。只能摆放在陆地上。 +- 船厂(AlliedNavalYard):生产海军单位。只能摆放在海上。 - T2科技(Upgrade_AlliedTech2):解锁T2单位 - T3科技(Upgrade_AlliedTech3):解锁T3单位 [MOD:CORONA] @@ -501,46 +673,47 @@ PlayerY: 摆放建筑 - 光谱塔(AlliedBaseDefenseAdvanced):高级防御塔,射程更远,可跨越围墙或建筑攻击,只能对陆地或海中目标进行攻击 [MOD:CORONA] - 小高科(AlliedLowTechStructure):提供升级、解锁高级防御塔和解锁T3科技。 -[MOD:RA3] +[MOD:NO:CORONA] - 高科(AlliedTechStructure):解锁超级武器 盟军基础常用单位: -- 矿车(AlliedMiner):无武装,可通过SpecialPower_UnpackReplaceSelf展开变成指挥中心。由于矿场自带矿车,玩家一般不需要额外生产矿车,除非:矿车被摧毁需要补充,或者玩家想要让矿车展开成指挥中心用于基地扩张 -- 狗(AlliedScoutInfantry):侦察单位,两栖,非常脆弱,只能攻击步兵,吼叫技能(SpecialPower_Bark)可以AOE瘫痪敌方步兵 +- 矿车(AlliedMiner):无武装,两栖,可通过SpecialPower_UnpackReplaceSelf在陆地或水上展开变成指挥中心。由于矿场自带矿车,玩家一般不需要额外生产矿车,除非:矿车被摧毁需要补充,或者玩家想要让矿车展开成指挥中心用于基地扩张 +- 狗(AlliedScoutInfantry):侦察单位,两栖,非常脆弱,只能攻击步兵,吼叫技能(SpecialPower_Bark)可以AOE瘫痪敌方步兵。由于两栖特性,玩家可能利用它去绕海侦察。绕海侦察不一定会导致战斗,因为狗无法攻击载具和建筑而且非常脆弱,但它能够提供视野和侦察信息。 - 维和步兵(AlliedAntiInfantryInfantry):基础反步兵单位,数值和造价都偏高,可以抗线,可以掩护其他脆弱的单位,可以在霰弹枪和防暴盾牌之间切换(SpecialPower_ToggleRiotShield) - 标枪兵(AlliedAntiVehicleInfantry):反装甲以及防空单位,无法反步兵且较为脆弱,但假如数量多可以成为输出主力,激光制导(SpecialPower_RadarLock)可以大幅提高输出 -- 工程师(AlliedEngineer):可用于占领建筑或维修己方建筑。开局一般会造一个工程师来占领油井 +- 工程师(AlliedEngineer):两栖,可用于占领建筑或维修己方建筑。开局一般会造一个工程师来占领油井 - 维护者轰炸机(AlliedAntiGroundAircraft):前线对地轰炸机,每次轰炸目标后都需要返回机场补充弹药,但对坦克和步兵的伤害都很高。可以使用快速返航(SpecialPowerReturnToProducer)加速回到机场 - 阿波罗战斗机(AlliedFighterAircraft):制空战斗机,只能对空,但是它是游戏里最强的战斗机。可以使用快速返航(SpecialPowerReturnToProducer)加速回到机场 -- 激流ACV(AlliedAntiInfantryVehicle):两栖反步兵气垫船,由船厂生产,可以运输步兵 +- 激流ACV(AlliedAntiInfantryVehicle):两栖,反步兵气垫船,由船厂生产,可以运输步兵 - 激流ACV(AlliedAntiInfantryVehicle_Ground):激流ACV也可从重工生产 - IFV(AlliedAntiAirVehicle):多功能步兵战车,脆弱但高速的基础陆地防空单位,可以装载步兵切换其他武器 - 海豚(AlliedAntiNavalScout):搭载声波武器的前期对海单位,脆弱,但速度快,可攻击水面单位或建筑,可以使用跳跃技能(SpecialPower_TriggerJump)来躲避攻击 - 水翼船(AlliedAntiAirShip):是游戏里最强的[MOD:CORONA]前期[/MOD]水面防空单位,默认使用防空机枪,但可以在干扰器和防空机枪之间切换(SpecialPower_ToggleWeaponScrambler),切换为干扰器之后,水翼船可以禁止敌方目标开火 -- 基地车(AlliedMCV):昂贵且耗时的两栖无武装车辆,可以展开为主基地。一般不会额外造基地车、只会使用开局本来就有的唯一一个主基地。 +- 基地车(AlliedMCV):两栖,昂贵且耗时的无武装车辆,但血量很高。可以在陆地或水上展开为主基地。一般不会额外造基地车、只会使用开局本来就有的唯一一个主基地。 盟军T2常用单位,需要T2升级(Upgrade_AlliedTech2): - 守护者坦克(AlliedAntiVehicleVehicleTech1):盟军反装甲坦克,切换为激光指示器(SpecialPower_ToggleTargetPainter)来提高友军的输出。由于激光指示器无法叠加,而且盟军步兵和飞机的输出很高,所以守护者的出场率偏低,一般造一两个。但假如数量够多也有奇效。 [MOD:CORONA] - 光棱坦克(AlliedPrismTank):擅长反步兵和反轻型装甲。移速较慢。假如数量很多,也可以凭借射程优势与坦克对抗。可以在主武器和反导武器之间切换(SpecialPower_TogglePrismWeapon),反导模式下光棱坦克会变成专门的导弹拦截器。 -- 冷冻直升机(AlliedSupportAircraft):[MOD:RA3]盟军最[/MOD]强大的支援直升机,被它攻击的目标不会受到伤害,但会被冻住。冰冻状态下的单位无法开火或移动,而且可被其他单位一击秒杀。冷冻直升机可以对单个敌军或友军目标使用缩小光束(SpecialPower_ShrinkRay),缩小的单位各项属性都被大幅削弱,但速度增加。 -- 突袭驱逐舰(AlliedAntiNavyShipTech1):坚固的船厂T2单位,只能使用输出较低的舰炮[MOD:CORONA],但可用高伤害的深水炸弹攻击潜艇[/MOD]。它可以用黑洞装甲(SpecialPower_ToggleMagneticArmor)把敌方火力都吸引到自己身上,从而对友军提供掩护。突袭驱逐舰甚至可以上岸的两栖单位,在岸上同样可以吸收敌方火力,掩护陆军和空军 +- 冷冻直升机(AlliedSupportAircraft):[MOD:NO:CORONA]盟军最[/MOD]强大的支援直升机,被它攻击的目标不会受到伤害,但会被冻住。冰冻状态下的单位无法开火或移动,而且可被其他单位一击秒杀。冷冻直升机可以对单个敌军或友军目标使用缩小光束(SpecialPower_ShrinkRay),缩小的单位各项属性都被大幅削弱,但速度增加。 +- 突袭驱逐舰(AlliedAntiNavyShipTech1):两栖,坚固的船厂T2单位,只能使用输出较低的舰炮[MOD:CORONA],但可用高伤害的深水炸弹攻击潜艇[/MOD]。它可以用黑洞装甲(SpecialPower_ToggleMagneticArmor)把敌方火力都吸引到自己身上,从而对友军提供掩护。突袭驱逐舰在陆地上移动,它在岸上同样可以吸收敌方火力,掩护陆军和空军 盟军T3常用单位,需要T3升级(Upgrade_AlliedTech3): - 雅典娜炮(AlliedAntiStructureVehicle):盟军远距离对地攻城单位,引导太空卫星激光攻击固定或低速目标,还可以开启巨大的护盾(SpecialPower_ToggleShieldSphere)来掩护附近的友军 -- 幻影坦克(AlliedAntiVehicleVehicleTech3):使用光谱武器的先进攻击坦克,伤害很高,但射程很低。[MOD:RA3]因此出场率不如雅典娜炮[/MOD][MOD:CORONA]可以使用隐形技能(SpecialPower_AlliedAntiVehicleVehicleTech3AssassinStateDisguise)潜入到敌方腹地进行袭击,敌方必须造侦察单位来探测隐形才能反制[/MOD] -[MOD:RA3] +- 幻影坦克(AlliedAntiVehicleVehicleTech3):使用光谱武器的先进攻击坦克,伤害很高,但射程很低。[MOD:NO:CORONA]因此出场率不如雅典娜炮[/MOD][MOD:CORONA]可以使用隐形技能(SpecialPower_AlliedAntiVehicleVehicleTech3AssassinStateDisguise)潜入到敌方腹地进行袭击,敌方必须造侦察单位来探测隐形才能反制[/MOD] +[MOD:NO:CORONA] - 世纪轰炸机(AlliedBomberAircraft):擅长攻击建筑等大型目标的战略轰炸机,可用来轰炸敌方基地。攻击后需要返回机场补充弹药。可以运输步兵并使用SpecialPower_EjectPassengersUntargeted让步兵跳伞。 [MOD:CORONA] - 世纪轰炸机(AlliedAntiStructureBomberAircraft):擅长攻击建筑、军舰等大型目标的战略轰炸机。攻击后需要返回机场补充弹药。可以使用技能SpecialPowerReturnToProducer来快速返航。 -[MOD:RA3] +[MOD:NO:CORONA] - 航空母舰(AlliedAntiStructureShip):盟军远距离对地攻城单位,可以释放无人机攻击海面或地面目标 [MOD:CORONA] -- 冷冻军团(AlliedCryoLegionnaire):高级支援步兵,可以大幅降低敌方单位的速度让敌方难以冲击阵地或逃跑 +- 冷冻军团(AlliedCryoLegionnaire):两栖,高级支援步兵,可以大幅降低敌方单位的速度让敌方难以冲击阵地或逃跑 [MOD:CORONA] - 波塞冬巡洋舰(AlliedAntiNavyShipTech3):盟军的大型制海巡洋舰 -- 谭雅(AlliedCommandoTech1):盟军英雄步兵单位,擅长反步兵和反建筑的无双女士兵。她的时空腰带(SpecialPower_TimeBelt)能让谭雅回溯到一段时间之前的血量和坐标位置。她可以高效炸毁建筑,因此玩家会设法把谭雅进入(或者直接用载具运输到)敌方基地。每个玩家同时只能有一位谭雅 +- 谭雅(AlliedCommandoTech1):两栖,盟军英雄步兵单位,擅长反步兵和反建筑的无双女士兵。她的时空腰带(SpecialPower_TimeBelt)能让谭雅回溯到一段时间之前的血量和坐标位置。她可以高效炸毁建筑,因此玩家会设法把谭雅进入(或者直接用载具运输到)敌方基地。每个玩家同时只能有一位谭雅 [MOD:CORONA] 盟军T4常用单位,需要T4升级(Upgrade_AlliedTech4): [MOD:CORONA] - 航空母舰(AlliedAntiStructureShip):盟军远距离对地攻城单位,可以释放无人机攻击海面或地面目标 +[MOD:CORONA] - 先锋炮艇机(AlliedGunshipAircraft):持久的空对地火力,不需要回到机场补充弹药 盟军常用开局: - 常规兵营开局(较为泛用),以下是几种兵营开的变种: @@ -598,24 +771,24 @@ PlayerY: 摆放建筑 - T4科技(Upgrade_CelestialTech_RANK3):解锁T4单位 **注意**:神州的 Upgrade_CelestialTech_RANK2 是 T3,不是 T2; 神州基础常用单位: -- 矿车(CelestialMiner) 无武装,必要时可以拓展前线基地(SpecialPower_UnpackReplaceSelf) +- 矿车(CelestialMiner) 无武装,两栖,必要时可以拓展前线基地(SpecialPower_UnpackReplaceSelf) - 天眼哨机(CelestialScoutDrone) 由兵营生产的飞行侦察无人机,无法对敌方目标造成伤害。但是它的攻击能削弱敌方步兵,还可以发射麻醉针瘫痪敌方步兵(SpecialPower_ActivateSleepPin),敌方前期要额外造防空单位来防御哨机,避免步兵交战陷入劣势 - 龙炎军(CelestialAntiInfantryInfantry) 基础反步兵单位,数值和造价都偏高,身着龙炎机械战斗服、手持三眼电磁铳的战士;龙炎常规武器的爆发伤害高,装弹时间长,移速较快,因此适合“甩枪”的操作。有经验的玩家会让龙炎反复前进和撤退,让龙炎进行拉扯,在前期步兵战斗中取得优势。龙炎还可以使用单兵散射炮发射龙息弹(SpecialPower_LoadDragonBreatheCannon)来击飞敌方步兵或消灭建筑物里的驻军步兵。 - 铁卫(CelestialAntiVehicleInfantry) 反装甲/防空步兵,能用高速穿甲弹击穿厚重的坦克装甲,亦能在架设护盾后发射破墙榴弹 (SpecialPower_ToggleShield) -- 凌波护卫战车(CelestialAntiInfantryVehicle_B) 轻型反步兵运兵战车,配备机炮的两栖步战车,可以运输步兵(SpecialPower_GatherPassenger) -- 磁弩(CelestialAntiAirShip) 轻型两栖防空车,由重工生产,可以使用技能(SpecialPower_ToggleHeavyEMCannonWeapon)把防空速射炮换成对地的磁轨炮,变成轻型的两栖反载具单位,或者使用相同技能切换回防空速射炮 +- 凌波护卫战车(CelestialAntiInfantryVehicle_B) 轻型反步兵运兵战车,两栖,配备机炮的两栖步战车,可以运输步兵(SpecialPower_GatherPassenger) +- 磁弩(CelestialAntiAirShip) 轻型防空车,两栖,由重工生产,可以使用技能(SpecialPower_ToggleHeavyEMCannonWeapon)把防空速射炮换成对地的磁轨炮,变成轻型的两栖反载具单位,或者使用相同技能切换回防空速射炮 - 磁弩(CelestialAntiAirShip_Water) 磁弩也可从船厂生产 - 乌篷猎船(CelestialAntiNavyShipTech1) 反舰快艇/猎潜艇,这些不起眼的轻型小船装备了能发射聚焦冲击波的武器,能对军舰和潜艇造成破坏,猎船可以放置声纳浮标让敌方潜艇无处遁形(SpecialPower_CelestialSonarBuoy) - 凤凰战机(CelestialFighterAircraft) 制空战斗机,可以使用技能快速回到机场(SpecialPowerReturnToProducer_F) - 毕方支援机(CelestialSupportAircraft) 支援直升机,搭载高能激光器的直升机,本身伤害较低,但能使敌方车辆装甲熔融,降低敌方目标护甲。使用技能可以在激光器主武器和电磁支援之间切换(SpecialPower_ToggleCelestialSupportAircraftBuffWeapon),切换为电磁支援后可以增加友军输出 神州T2常用单位,需要神州高科(CelestialTechStructure): -- 岚影刺(CelestialInfiltrationInfantry) 渗透部队/狙击手,她可以从远处暗杀敌方步兵,也可以化妆成敌方步兵(SpecialPower_CelestialDisguise),让敌方无法发现 +- 岚影刺(CelestialInfiltrationInfantry) 渗透部队/狙击手,两栖,她可以从远处暗杀敌方步兵,也可以化妆成敌方步兵(SpecialPower_CelestialDisguise),让敌方无法发现 - 朱雀(CelestialAttackerAircraft) 对地攻击机,搭载中型离子炮的反装甲攻击机,还可以喷火反步兵或者削弱敌方装甲(SpecialPower_ActivateFire) - 麒麟(CelestialAntiVehicleVehicleTech1) 反装甲主战坦克,神州陆军的新一代中流砥柱。可以使用技能偏转敌方来袭的炮火和导弹(SpecialPower_ToggleRangeUpdateCelestial) - 青锋导弹车(CelestialLongRangeMissileVehicle_B) 远程反装甲,装备反坦克导弹的轻型载具,足以应对装甲目标,还可以发射烟雾弹削弱敌方单位的射程(SpecialPower_TriggerSmokeBombMissile) - 计蒙驱逐舰(CelestialAlmightlyShip) 制海战舰,多用途驱逐舰,用舰炮和导弹对付海面、潜水或地面目标。可以使用阻止敌方单位使用技能(SpecialPower_CelestialShipScrambler) 神州T3常用单位,需要T3升级(Upgrade_CelestialTech_RANK2): -- 天罡(CelestialAntiInfantryInfantryAdvanced) 先进反步兵/反飞行器的机甲步兵,配备外骨骼和迅雷转轮机关铳的精英步兵,能快速收割敌方步兵、击落敌方飞行器。还可以瘫痪牵制敌方载具(SpecialPower_ActivateEMPThunder) +- 天罡(CelestialAntiInfantryInfantryAdvanced) 高级步兵,先进反步兵/反飞行器,配备外骨骼和迅雷转轮机关铳的精英步兵,能快速收割敌方步兵、击落敌方飞行器。还可以瘫痪牵制敌方载具(SpecialPower_ActivateEMPThunder) - 祝融(CelestialAntiVehicleVehicleTech3) 先进反装甲重型坦克,搭载了转轮式装弹的大型离子炮,借由主炮的余热可持续提高武器射速,使用技能可以大幅提高移速(SpecialPower_RapidCooling) - 白虎(CelestialAntiStructureVehicle) 远距离对地攻城单位,可以发射高能等离子体团攻击远处的目标,也可以产生临时量子压制力场(SpecialPower_CelestialQuantumBreak)减速并削弱力场内的敌方目标 - 重明(CelestialInterceptorAircraft) 擅长拦截敌方重型空军的截击机,能发射炽热等离子束武器,可以使用技能快速回到机场(SpecialPowerReturnToProducer_F) @@ -642,7 +815,7 @@ PlayerY: 摆放建筑 - 浴日神坛(CelestialSuperWeaponAdvanced) 终极武器 每次至少需要6分钟准备,向指定区域发射日冕风暴(SpecialPowerCelestialCannon),杀伤区域内的所有目标 "; - var infinityIsleDescription = @" + var infinityIsleVanilla = @" 当前交战的地图是:无限岛 这张地图是对称的,只有一条陆地进攻路线,也是主要的陆地交战区域,中央高地。 中央高地是长条状的,只有两个出入口,位于中央高地的两端,通向两位玩家的出生点。 @@ -674,11 +847,49 @@ PlayerY: 摆放建筑 - 矿区,中央高地(X=3250,Y=1060,Z=280),在它附近有:高地通往出生点2的唯一陆地路线。 - 额外矿区,海面,中央高地悬崖外面的海矿(X=2650,Y=850,Z=280) -[MOD:RA3] 地图中央点:(X=2650,Y=1900,Z=280) 地图边界(X):0~5300 地图边界(Y):0~3800 "; + var infinityIsleCorona = @" +当前交战的地图是:无限岛 +这张地图是对称的,只有一条陆地进攻路线,也是主要的陆地交战区域,中央高地。 +中央高地是长条状的,只有两个出入口,位于中央高地的两端,通向两位玩家的出生点。 +玩家可以在高地战场正面交锋,也可以选择绕海,或者使用空军(不受地形限制)。 +低地被中央高地分割成两部分,低地都是三面环海(还有一面是高地),两栖单位可以从低地上岸或下水。 +中央高地也有靠海的地方,但两栖单位无法跨越悬崖从高地直接入海,需要从低地绕道。(额外矿区) +每个矿区可以摆放一个矿厂。 + +# 地图参数 +海面高度:Z=200 +低地高度:Z=210 +高地高度:Z=280 + +## 出生点1(陆地)(X=1420,Y=1920,Z=210) +- 矿区(X=1322,Y=2178,Z=210) +- 矿区(X=1513,Y=1648,Z=210) +### 高地油井(X=2350,Y=2980,Z=280)[UnitId]228 +### 扩张方向 +- 矿区,海面,远离中央区域(X=940,Y=1096,Z=200) +- 矿区,中央高地(X=1810,Y=2984,Z=280),在它附近有:高地通往出生点1的唯一陆地路线。 +- 额外矿区,海面,中央高地悬崖外面的海矿(X=2828,Y=3090,Z=200) + +## 出生点2(陆地)(X=4125,Y=2270,Z=210) +- 矿区(X=4101,Y=1987,Z=210) +- 矿区(x=4016,Y=2496,Z=210) +### 高地油井(X=3070,Y=1280,Z=280)[UnitId]229 +### 扩张方向 +- 矿区,海面,远离中央区域(X=4504,Y=3132,Z=200) +- 矿区,中央高地(X=3613,Y=1266,Z=280),在它附近有:高地通往出生点2的唯一陆地路线。 +- 额外矿区,海面,中央高地悬崖外面的海矿(X=2743,Y=1054,Z=200) + +地图中央点:(X=2745,Y=2125,Z=280) +地图边界(X):0~5490 +地图边界(Y):0~4250 + +中立建筑:[UnitId]的范围从10~227的单位都属于地图物件或中立建筑 +"; + static string Process(string text, Mod mod) { @@ -693,10 +904,21 @@ PlayerY: 摆放建筑 continue; } const string prefix = "[MOD:"; + const string noPrefix = "[MOD:NO:"; const string endTag = "[/MOD]"; if (line.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { - if (!line.StartsWith($"[MOD:{mod.ModName.ToUpperInvariant()}]", StringComparison.OrdinalIgnoreCase)) + // check exclude + if (line.StartsWith(noPrefix, StringComparison.OrdinalIgnoreCase)) + { + if (line.StartsWith($"[MOD:NO:{mod.ModName}]", StringComparison.OrdinalIgnoreCase)) + { + skipNextLine = true; + } + continue; + } + + if (!line.StartsWith($"[MOD:{mod.ModName}]", StringComparison.OrdinalIgnoreCase)) { skipNextLine = true; } @@ -731,6 +953,21 @@ PlayerY: 摆放建筑 var modName = l.Substring(modNameStartIndex, modNameEndIndex - modNameStartIndex); var contentStartIndex = modNameEndIndex + 1; var content = l.Substring(contentStartIndex, endIndex - contentStartIndex); + // exclude first + if (modName.StartsWith("NO:", StringComparison.OrdinalIgnoreCase)) + { + var deniedModName = modName.Substring(3); + if (deniedModName.Equals(mod.ModName, StringComparison.OrdinalIgnoreCase)) + { + l = l.Substring(0, startIndex) + l.Substring(endIndex + endTag.Length); + } + else + { + l = l.Substring(0, startIndex) + content + l.Substring(endIndex + endTag.Length); + } + continue; + } + if (modName.Equals(mod.ModName, StringComparison.OrdinalIgnoreCase)) { l = l.Substring(0, startIndex) + content + l.Substring(endIndex + endTag.Length); @@ -747,9 +984,10 @@ PlayerY: 摆放建筑 } var sb = new StringBuilder(); - sb.AppendLine(generalDescriptions); + sb.AppendLine(generalDescriptions.Trim()); sb.AppendLine(); + var mod = replay.Mod; var factions = players .Select(kv => ModData.GetFaction(mod, kv.Value.FactionId).Name) .Distinct() @@ -766,11 +1004,35 @@ PlayerY: 摆放建筑 sb.AppendLine(description); sb.AppendLine(); } + else + { + MessageBox.Show($"缺乏阵营{faction}的介绍,AI难以分析"); + } + } + var isCorona = mod.ModName.Equals("corona", StringComparison.OrdinalIgnoreCase); + var mapDescriptions = new Dictionary + { + ["map_mp_2_rao1"] = isCorona ? infinityIsleCorona : infinityIsleVanilla + }; + var mapPath = Path.GetFileNameWithoutExtension(replay.MapPath); + if (mapDescriptions.TryGetValue(mapPath, out var mapDescription)) + { + sb.AppendLine(); + sb.AppendLine(mapDescription.Trim()); + } + else + { + MessageBox.Show($"没有地图{mapPath}的介绍,AI难以分析"); } return sb.ToString().Replace("\r", ""); } - public static string BuildUserPrompt(Mod mod, ImmutableSortedDictionary players, string commandData) + public static string BuildUserPrompt( + Mod mod, + ImmutableSortedDictionary players, + string commandData, + out string prefix + ) { var playerNamesForAI = PlayerNamesForAI(mod, players); var playerData = players.Select(kv => @@ -792,10 +1054,52 @@ PlayerY: 摆放建筑 sb.AppendLine("玩家列表:"); sb.AppendLine(string.Join("\n", playerData)); sb.AppendLine("数据:"); + prefix = sb.ToString(); sb.AppendLine(commandData); return sb.ToString().Replace("\r", ""); } + public static string BuildSegmentUserPrompt(List segments, int currentSegmentIndex, int eventCount) + { + if (currentSegmentIndex < 0 || currentSegmentIndex >= segments.Count) + { + throw new InvalidOperationException("Current segment index is out of range."); + } + var segment = segments[currentSegmentIndex]; + var beginText = currentSegmentIndex <= 0 + ? "游戏开始" + : segment.Start.ToString(); + var endText = currentSegmentIndex >= segments.Count - 1 + ? "游戏结束" + : segment.End.ToString(); + var instruction = @$" +请分析第{currentSegmentIndex + 1}段({beginText}至{endText})的数据。 +本阶段一共有{eventCount}条操作信息,数量很大,因此**不要**在思考推理时直接把{eventCount}条操作信息都列出来。 +也不要把每一条操作信息都视作一个单独事件。 +你需要列出{beginText}至{endText}的主要事件、以及其他有分析价值的事件。 +请按照按照[观察]、[分析]、[推理]、[进一步思考(可选)]的步骤,对各个事件进行分析和推理。 + +假如当前阶段存在一些较为重要的单位、而且能够推测出它们可能是什么单位,则可以列出单位的UnitId以及你对单位的推测 +"; + return instruction.Trim().Replace("\r", ""); + } + + public static string BuildFinalUserPrompt(int totalEventCount) + { + var instruction = $@" +请对以上内容进行总结。 +基于: +- 你对于各阶段的推理分析 +- 由我在对话刚开始时提供的原始数据(约{totalEventCount}个操作信息)。 +请对各阶段的推理分析进行汇总。 +判断是否需要对之前的分析进行补充,例如检查之前是否遗漏了某些重要的操作信息? +判断是否需要对之前的分析进行修正。 +最终应该输出: +- 一个完整的对局介绍(解说),其中需要包含玩家们在各阶段经历的所有主要事件,也需要包含你的分析和推理。 +"; + return instruction.Trim().Replace("\r", ""); + } + public static (int BytesCount, int EstimatedTokenCount) EstimateTokenCount(string text) { var bytesCount = Encoding.UTF8.GetByteCount(text); @@ -842,7 +1146,6 @@ PlayerY: 摆放建筑 } public async Task AnalyzeAsync( - string model, string instruction, string text, Dictionary extraParams, @@ -853,7 +1156,6 @@ PlayerY: 摆放建筑 { _state[kv.Key] = kv.Value; } - _state["model"] = model; _state["messages"] = _messages; _state["stream"] = true; _state["stream_options"] = new @@ -899,11 +1201,7 @@ PlayerY: 摆放建筑 var start = ParseAITimeSpan(startTimeText); var end = ParseAITimeSpan(endTimeText); var description = line.Substring(match.Index + match.Length).Trim(); - _segments.Add((start, end, description)); - } - if (_segments.Count >= 5) - { - break; + _segments.Add(new(start, end, description)); } } @@ -914,27 +1212,14 @@ PlayerY: 摆放建筑 public async Task ContinueAnalyzeAsync( Action onChunk, + string instruction, CancellationToken cancellationToken) { if (_currentSegment < 0 || _currentSegment >= _segments.Count) { throw new InvalidOperationException("Current segment index is out of range."); } - var segment = _segments[_currentSegment]; - var beginText = _currentSegment <= 0 - ? "游戏开始" - : segment.Start.ToString(); - var endText = _currentSegment >= _segments.Count - 1 - ? "游戏结束" - : segment.End.ToString(); - var instruction = @$" -请分析第{_currentSegment + 1}段({beginText}至{endText})的数据。 -你需要列出{beginText}至{endText}的主要事件、以及其他有分析价值的事件,每个事件都应该附上时间戳。 -请按照按照[观察]、[分析]、[推理]、[进一步思考(可选)]的步骤,对各个事件进行分析和推理。 - -假如当前阶段存在一些较为重要的单位,假如能推测出它们可能是什么单位,则可以列出单位的UnitId以及你对单位的推测 -"; - instruction = instruction.Trim().Replace("\r", ""); + _messages.Add(new { role = "user", @@ -950,6 +1235,30 @@ PlayerY: 摆放建筑 return result; } + public async Task FinishAnalyzeAsync( + Action onChunk, + string instruction, + CancellationToken cancellationToken) + { + if (_currentSegment != _segments.Count) + { + throw new InvalidOperationException("Current segment index is out of range."); + } + + _messages.Add(new + { + role = "user", + content = instruction + }); + + var result = await DoRequest(onChunk, cancellationToken); + + + result.Segments = _segments; + result.CurrentSegment = _currentSegment; + return result; + } + private async Task DoRequest(Action onChunk, CancellationToken cancellationToken) { var inputJson = JsonSerializer.Serialize(_state); diff --git a/Utils/AIAnalyzeUI.cs b/Utils/AIAnalyzeUI.cs index 3bd383e..467a597 100644 --- a/Utils/AIAnalyzeUI.cs +++ b/Utils/AIAnalyzeUI.cs @@ -8,6 +8,8 @@ namespace AnotherReplayReader.Utils { internal static class AIAnalyzeUI { + public static List Debug { get; } = []; + public record AIAnalyzeProgressData(AIAnalyze.AIChunk Delta, DateTimeOffset? TimeStamp, bool IsExtra); public class EmaSpeed @@ -74,10 +76,13 @@ namespace AnotherReplayReader.Utils public static Action BuildAIChunkReader(Action newContent) { + var sb = new StringBuilder(); + Debug.Add(sb); void OnAIChunk(AIAnalyze.AIChunk c) { if (c.Type == AIAnalyze.AIChunkType.Json) { + sb.AppendLine(c.Text); return; } newContent(new(Delta: c, TimeStamp: DateTimeOffset.UtcNow, IsExtra: false));