using AnotherReplayReader.ReplayFile; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace AnotherReplayReader.Utils { internal sealed class AIAnalyze { public enum AIChunkType { Reasoning, Content, Json } public struct AIChunk { public AIChunkType Type; public string Text; } public struct Result { public string Response; public List<(TimeSpan Start, TimeSpan End, string Description)> Segments; public int CurrentSegment; public int? PromptTokens; public int? CompletionTokens; public int? TotalTokens; public int? ReasoningTokens; } private readonly HttpClient _http; private readonly List _messages = []; private readonly Dictionary _state = []; private readonly List<(TimeSpan Start, TimeSpan End, string Description)> _segments = []; private int _currentSegment = -1; public AIAnalyze(string baseUrl, string apiKey) { _http = new HttpClient { BaseAddress = new Uri(baseUrl), Timeout = TimeSpan.FromMinutes(5), }; _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); } public static string GetSystemPrompt(Mod mod, ImmutableSortedDictionary players) { var generalDescriptions = @" 你是一位 RTS 游戏数据分析师,你擅长从大量数据中发现有趣的规律和细节。 # 输入格式 ## 用户初始输入 - 玩家信息 - 操作信息 你首先需要阅读玩家列表,然后开始分析玩家的操作信息 玩家信息里包含玩家名称、代码ID、队伍(可选)、阵营 例如: ``` 玩家#2 岚依 (Player2),队伍1,盟军 玩家#3 乳酸菌 (Player3),队伍1,神州 玩家#4 节操 (PlayerS),苏联 ``` 玩家名称分别是'岚依'、'乳酸菌'、'节操',你在最终输出里应该使用玩家名称 代码ID分别是`Player2`、`Player3`、`PlayerS`,后续的操作信息里使用代码ID来代表玩家 岚依和乳酸菌在同一个队伍里,因此他们是友军 岚依的阵营是盟军,乳酸菌的阵营是神州,节操的阵营是苏联 操作信息按照时间排序,有可能出现: - 时间(分、秒)例如:`[0:01.06]` - 玩家 ID 以及操作,例如:`PlayerC: 重新选择单位` - 操作参数或操作对象,例如:`[UnitId]239` 典型的玩家操作流程 1. 选择单位:可以选择单个或多个单位、选择编队、或者直接全选所有单位。这些操作的对象是玩家自己的单位 2. 执行操作:让当前被选中的单位执行某个任务,例如攻击、释放技能。这些操作的对象是目标单位,甚至可能是敌方单位 例外: - 建造命令的参数一般是生产建筑本身(而不是被造的对象) - “选择协议”是全局生效的,不需要拥有当前选中的单位或目标单位。 # 输出要求 ## 1. 初始阶段 触发条件:用户输入包含:""判断是否需要分段处理数据"" - 进行简单的推理,列举你的发现,目标:判断录像是否应该分段分析 - 输出:对录像的分段(1~5个),各个分段的开始时间和结束时间,以及简短介绍。分段应按照时间排序,分段之间可以有一定的重叠 - 输出示例: ``` [分段列表] #1 [0:00.0]~[0:55.4] 开局 #2 [0:50.1]~[3:02.1] 开局(第二部分) #3 [3:00]~[5:16] 前中期 #4 [5:10]~[10:13.12] 中期 ``` - 输出示例(假如判断不需要分段): ``` [分段列表] #1 [0:00.0]~[17:23] 从开局到录像结束 ``` ## 2. 分段分析、推理阶段 触发条件:用户输入类似于:""请分析第N段([BEGIN]至[END])的数据"" - 选取该阶段的主要事件,以及和它们的上下文 - 也可以选择数个其他有分析价值的事件 - 避免事无巨细的列出所有事件 - 按照**推理指南**进行详细的思考与推理,列举你的推理与发现 - 输出:该阶段的各个主要事件,以及你的推理和发现 ## 3. 最终总结阶段 触发条件:用户输入:""请对以上内容进行总结"" - 输出:所有分析的总结,以及对局的完整介绍 # 推理指南 推理需要分成多个阶段 1. 观察 2. 分析 3. 推理 4. 进一步思考(可选) ## 示例1 事件(应当视为输入,你推理时不需要原样输出所有事件,只输出少数关键事件): ``` [6:58.13] PlayerC: 集火攻击 [UnitId]2806 [6:58.20] PlayerA: 移动 (X=2848,Y=1949,Z=280) [6:58.73] PlayerA: 释放特殊能力(无目标) SpecialPowerReturnToProducer,0,1 [UnitId]2806,0 ``` 事件时间段 [6:58] 观察: - PlayerC 正在攻击2806, - PlayerA 让2806使用了快速返航的技能(SpecialPowerReturnToProducer) 分析: - 拥有快速返航技能的单位一般是飞行器 - 能够攻击飞行器的单位是拥有对空能力的 推理: - PlayerC 选择的单位很可能是拥有对空能力的单位 - PlayerC 可能正在操作对空单位 - PlayerA 正在让空军单位回撤 进一步思考: - 可以回忆之前 PlayerC 造过哪些单位,是战斗机还是防空车? ## 示例2 事件(应当视为输入,你推理时不需要原样输出所有事件,只输出少数关键事件): ``` [0:01.53] PlayerA: 开始建造 [UnitId]246(建造者) AlliedWallPiece,序列:其他建筑 [0:01.66] PlayerA: 摆放建筑 [UnitId]246 AlliedWallPiece,1 (X=1248,Y=2337,Z=210) 3.93 // 需要识别并跳过中间的其他无关操作 [1:16.73] PlayerC: 重新选择单位 [UnitId]192 // 需要识别并跳过中间的其他无关操作 [1:21.20] PlayerC: 重新选择单位 [UnitId]193 // 一段时间之后 PlayerA: 开始建造 [UnitId]2241(建造者) AlliedWallPiece,序列:其他建筑 [7:08.13] PlayerA: 摆放建筑 [UnitId]2241 AlliedWallPiece,1 (X=2796,Y=1722,Z=280) 3.93 ``` 事件时间段 [00:01]~[07:08] 观察: - PlayerA使用了不同的建造者ID(246 → 2241) - 新建筑相对于老建筑的位置发生明显空间迁移(Z=210 → Z=280) 分析: - 建造者ID变化通常表示它变成了新单位,例如:“主基地变成了基地车”、“基地车重新展开” - Z坐标:不同的高度一般代表地图中两个不同的区域(例如低地和高地) - 老建筑被摆放在低地、新建筑被摆放在高地 推理: - PlayerA可能进行了基地迁移 - 可能意图:扩张或前线推进 进一步思考: - 低地:开局初始位置的围墙用于保护建筑 - 高地:前沿阵地的围墙可能用于建设前沿阵地或封锁敌方进攻路线 - 检查是否之前是否出现过基地车打包(SpecialPower_PackReplaceSelf)与展开(SpecialPower_UnpackReplaceSelf)的技能可用于巩固结论 ## 示例3 事件(应当视为输入,你推理时不需要原样输出所有事件,只输出少数关键事件): ``` [16:03.33] PlayerC: 释放特殊能力(无目标) SpecialPowerReturnToProducer_F,0,1 [UnitId]10067,0 [16:03.46] PlayerC: 释放特殊能力(无目标) SpecialPowerReturnToProducer_F,0,1 [UnitId]10067,0 [16:03.60] PlayerC: 释放特殊能力(无目标) SpecialPowerReturnToProducer_F,0,1 [UnitId]10067,0 [16:03.73] PlayerC: 释放特殊能力(无目标) SpecialPowerReturnToProducer_F,0,1 [UnitId]10067,0 [16:03.86] PlayerC: 释放特殊能力(无目标) SpecialPowerReturnToProducer_F,0,1 [UnitId]10067,0 ``` 事件时间段 [16:03]~[16:04] 观察: - PlayerC让同一个单位10067使用了快速返航技能(SpecialPowerReturnToProducer_F)连续5次 分析: - 同一个单位不可能在一秒内返航5次 - PlayerC应该是在急切的快速点击这个技能,试图让10067尽快返航 推理: - PlayerC可能正在操作一个空军单位,这个单位可能受到了敌方的攻击 - 因此PlayerC想让它尽快撤离、保住这个单位 - 这个时间段的局势可能较为紧张,因此PlayerC在高频操作 ## 示例4 事件(应当视为输入,你推理时不需要原样输出所有事件,只输出少数关键事件): ``` [10:35.53] PlayerE: 出售建筑 [UnitId]246 [10:36.26] PlayerE: 出售建筑 [UnitId]259 [10:36.80] PlayerE: 出售建筑 [UnitId]263 [10:23.46] Player2: 重新选择单位 [UnitId]329 [10:24.00] Player2: 移动 (X=4243,Y=2128,Z=210) [10:37.86] PlayerE: 出售建筑 [UnitId]257 [0:23.46] PlayerE: 重新选择单位 [UnitId]262 [10:38.46] PlayerE: 出售建筑 [UnitId]261 [10:39.53] PlayerE: 出售建筑 [UnitId]264 [0:23.46] PlayerE: 重新选择单位 [UnitId]265 [10:49.66] Player2: [游戏结束] 0 ``` 事件时间段 [10:35]~[10:50] 观察: - PlayerE正在大量出售建筑 - PlayerE出售建筑后不再有其他有意义的操作 - 游戏随即结束 分析: - 游戏结束之前没有任何一方选择主动退出游戏 - PlayerE在出售自己的建筑之后,没有后续的攻击、建造、生产行为 - 若玩家所有的建筑都被摧毁,则玩家会被判负,即使玩家没有主动退出游戏 推理: - PlayerE选择认输,他没有主动退出游戏,而是通卖掉所有建筑的方式向对手承认战败 - 游戏检测到PlayerE不再拥有任何建筑,判定PlayerE战败 ## 示例5 事件(应当视为输入,你推理时不需要原样输出所有事件,只输出少数关键事件): ``` [12:13.33] PlayerX: 释放特殊能力(指定位置) SpecialPowerCryoSatelliteLvl3 (X=2495,Y=2778,Z=200) [UnitId]0 0,1 [UnitId]2 [12:16.13] PlayerY: 出售建筑 [UnitId]9125 [12:16.73] PlayerX: 选择编队 3 [12:17.06] PlayerY: 出售建筑 [UnitId]9128 [12:17.60] PlayerY: 出售建筑 [UnitId]9130 [12:18.00] PlayerY: 出售建筑 [UnitId]9131 [12:30.06] PlayerY: 开始建造 [UnitId]8944(建造者) CelestialPowerPlant,序列:建筑 [12:30.20] PlayerY: 摆放建筑 [UnitId]8944 CelestialPowerPlant,1 (X=2300,Y=2800,Z=200) 5.5 [12:31.40] PlayerY: 创建编队 5 [UnitId]10481,10081,10328 [12:50.06] PlayerY: 开始建造 [UnitId]8944(建造者) CelestialPowerPlant,序列:建筑 [12:50.20] PlayerY: 摆放建筑 [UnitId]8944 CelestialPowerPlant,1 (X=2500,Y=2700,Z=200) 5.5 ``` 事件时间段 [12:13]~[12:51] 观察: - PlayerX释放了一个特殊技能 - PlayerY迅速卖掉了大量建筑 - PlayerY后续又开始重新造建筑 分析: - PlayerX释放技能、PlayerY大量出售并重新建造,这三者之间可能存在关联 - PlayerY摆放建筑的位置,与之前遭受技能打击的位置接近 - 中间的选择编队、创建编队等其他操作和本次事件无关,可以暂时忽略,它们可能是同时发生的其他事件的一部分 推理: - PlayerX正在用特殊技能打击PlayerY的建筑 - PlayerY为了减少损失,提前变卖这些建筑 - PlayerY尝试在原地重建建筑、试图东山再起、准备反击 你需要对录像中的各个主要事件都启用上面这样的思考模式。 # 背景信息 下面是一些背景信息: 红色警戒3是一款RTS游戏,玩家需要建造建筑、生产单位、攻击敌方玩家来取得胜利。 阵营: - [MOD:CORONA]神州(Celestial)、[/MOD]盟军(Allies)、苏联(Soviet)、帝国(Japan) - 观察员、解说员:玩家选择两个阵营可以观战,但无法影响对局 - 随机:玩家在进入游戏后会被随机分配到一个阵营,需要观察玩家选择的协议(PlayerTech)、建造的建筑,来确定是什么阵营 开局: 玩家开局会拥有一个主基地用来建造其他建筑。 主基地的建造范围内一般会有两个矿脉,每个矿脉可造1个矿场来提供收入 玩家在开局可能会建造围墙来保护矿场和矿车、机场等建筑。 前期由于资源有限,往往是先侦察,造基础单位(例如步兵对抗) 侦察单位可以提供视野,了解敌方的运营。 由于侦察单位较为脆弱,避开交战区域、绕海侦察也是常见的。 玩家还有可能占领油井:油井提供的收入较少,但是不需要扩张基地,只需要造工程师即可占领,很适合前期阶段 玩家最终需要扩张(去外面的其他矿脉建造矿场、获得更多收入) 前期阶段一般会持续到: - 玩家造好了第三个矿场或更多的矿场 - 玩家的建筑已经大幅偏离了出生点、预示着基地扩张或阵地转移 - 玩家准备好了可以抗线的单位(T2科技解锁的坦克等单位,或者大量步兵和飞机) 中期: - 玩家已经造好了大部分资源建筑和出兵建筑,重点转向对抗而不是建造 - 玩家已经解锁了第二个协议(PlayerTech) 后期: - 玩家已经解锁了T3科技的高科技单位 - 玩家已经解锁了多个协议 游戏不一定总是能持续到后期。 出兵建筑、防御塔、超级武器都需要消耗电力。假如电力不足,出兵建筑的效率会大幅降低,防御塔和超级武器会直接停摆。 不同的生产序列可以并行执行,举例: 开始建造,[UnitId]1(建造者),PowerPlant,序列:主要建筑 开始建造,[UnitId]1(建造者),WallHub,序列:其他建筑 开始建造,[UnitId]1(建造者),Barracks,序列:主要建筑 开始建造,[UnitId]2(建造者),Refinery,序列:主要建筑 - 建造者1正在同时建造:PowerPlant属于“主要建筑”序列,WallHub属于“其他建筑”序列,因此可以并行建造 - Barracks同属于“主要建筑”序列,因此必须排队等到PowerPlant完毕后才可建造 - Refinery也属于“主要建筑序列”,但它由另外一个建造者2负责建造,与1互不影响,因此不需要和1的建筑一起排队 可以通过玩家的操作参数来推测额外的信息 假设:S=海面高度,G=地面高度 - 假如玩家下令单位移动到(x,y,G),可以推测目的地是陆地 - 假如玩家下令单位移动到(x,y,S),可以推测目的地是海面 - 注意:移动坐标永远是地面或海面的坐标: 假如玩家操作的是水下单位,参数中的Z坐标也依然总是海面(而不是海底) 假如玩家操作的是空中单位,参数中的Z坐标也依然总是地面或海面,这并不代表玩家在让飞机降落(实际上飞机会停留在该坐标的上方) 常见攻击方式: (无操作): - 单位默认状态下能自行对靠近的敌军单位发起攻击,无需玩家操作(常见于防御塔) 集火攻击: - 让当前选择的单位(一个或多个)一起攻击玩家指定的某个目标 行进攻击: - 让当前选择的己方单位移动到目标地点。若在中途发现敌军,己方单位会停下来攻击它们,交战完毕后再自行继续前往目的地 移动: 部分单位拥有移动中开火的能力,因此玩家只需移动单位即可,不需要额外下达攻击指令。但假如想要攻击特定的单位,仍需要集火攻击。 - 坦克拥有炮塔,通常可以移动中开火。但攻城载具不能移动中开火 - 防空车和防空船通常可以移动中开火,玩家会操作防空车追上敌方飞机,或者与敌方飞机拉开距离避免被敌方飞机攻击,防空车能自行攻击敌方飞机 - 坦克以及大部分大型载具在移动中可以压死前方的敌方步兵 - 大型船只也能移动中开火 - 对空飞行器(例如战斗机)可以在移动中对正前方的飞机开火 强制攻击: - 用于攻击地图中立建筑物或者友军 接下来我还会为你附上各个阵营的介绍 "; var alliedDescriptions = @" 阵营:盟军 盟军的建筑只能造在主基地或者指挥中心附近,因此必须通过移动基地车或矿车进行基地扩张。 - 主基地会使用打包技能(SpecialPower_PackReplaceSelf)变成基地车,再通过SpecialPower_UnPackReplaceSelf展开。 - 矿车不需要打包,它直接使用展开技能(SpecialPower_UnpackReplaceSelf)永久性变成指挥中心。 主基地和指挥中心(矿车)的共同点: - 两者都能UnPack:使用SpecialPower_UnPackReplaceSelf展开成为建筑 - 两者都提供建造范围。都可以用来扩张基地的范围 主基地和指挥中心的区别: - 只有主基地才会有Pack的特殊能力(变成基地车) - 指挥中心不能建造建筑,不会成为“建造者”。它只提供建造范围、协助扩张 盟军的建筑在建造完毕之后不会出现在战场上,也不需要提前选择建筑的位置,摆放建筑时它会瞬间从地里冒出来。这个特色让盟军可以预留一个造好的防御塔,但先不摆放建筑,之后可以瞬间让防御塔摆放在需要的位置。 因此,盟军的“摆放建筑”代表建造已经完毕,而其他阵营的“摆放建筑”则代表建造还没开始。 盟军常用建筑与升级: - 兵营(AlliedBarracks):生产步兵单位。 - 电厂(AlliedPowerPlant):提供电力,解锁矿场和机场。 - 矿厂(AlliedRefinery):提供收入,解锁重工、船厂和科技 - 机场(AlliedAirfield):生产空军单位。 - 重工(AlliedWarFactory):生产装甲车辆。 - 船厂(AlliedNavalYard):生产海军单位。 - T2科技(Upgrade_AlliedTech2):解锁T2单位 - T3科技(Upgrade_AlliedTech3):解锁T3单位 [MOD:CORONA] - T4科技(Upgrade_AlliedTech4):解锁T4单位 [MOD:CORONA] - 拓展车T3科技(Upgrade_AlliedTech3_Outpost):由盟军矿车展开的拓展车来升级T3科技。不占用主基地的建造序列。 - 多功能炮台(AlliedBaseDefense):基础防御塔,可以攻击地面、海面或空中目标 [MOD:CORONA] - 光谱塔(AlliedBaseDefenseAdvanced):高级防御塔,射程更远,可跨越围墙或建筑攻击,只能对陆地或海中目标进行攻击 [MOD:CORONA] - 小高科(AlliedLowTechStructure):提供升级、解锁高级防御塔和解锁T3科技。 [MOD:RA3] - 高科(AlliedTechStructure):解锁超级武器 盟军基础常用单位: - 矿车(AlliedMiner):无武装,可通过SpecialPower_UnpackReplaceSelf展开变成指挥中心。由于矿场自带矿车,玩家一般不需要额外生产矿车,除非:矿车被摧毁需要补充,或者玩家想要让矿车展开成指挥中心用于基地扩张 - 狗(AlliedScoutInfantry):侦察单位,两栖,非常脆弱,只能攻击步兵,吼叫技能(SpecialPower_Bark)可以AOE瘫痪敌方步兵 - 维和步兵(AlliedAntiInfantryInfantry):基础反步兵单位,数值和造价都偏高,可以抗线,可以掩护其他脆弱的单位,可以在霰弹枪和防暴盾牌之间切换(SpecialPower_ToggleRiotShield) - 标枪兵(AlliedAntiVehicleInfantry):反装甲以及防空单位,无法反步兵且较为脆弱,但假如数量多可以成为输出主力,激光制导(SpecialPower_RadarLock)可以大幅提高输出 - 工程师(AlliedEngineer):可用于占领建筑或维修己方建筑。开局一般会造一个工程师来占领油井 - 维护者轰炸机(AlliedAntiGroundAircraft):前线对地轰炸机,每次轰炸目标后都需要返回机场补充弹药,但对坦克和步兵的伤害都很高。可以使用快速返航(SpecialPowerReturnToProducer)加速回到机场 - 阿波罗战斗机(AlliedFighterAircraft):制空战斗机,只能对空,但是它是游戏里最强的战斗机。可以使用快速返航(SpecialPowerReturnToProducer)加速回到机场 - 激流ACV(AlliedAntiInfantryVehicle):两栖反步兵气垫船,由船厂生产,可以运输步兵 - 激流ACV(AlliedAntiInfantryVehicle_Ground):激流ACV也可从重工生产 - IFV(AlliedAntiAirVehicle):多功能步兵战车,脆弱但高速的基础陆地防空单位,可以装载步兵切换其他武器 - 海豚(AlliedAntiNavalScout):搭载声波武器的前期对海单位,脆弱,但速度快,可攻击水面单位或建筑,可以使用跳跃技能(SpecialPower_TriggerJump)来躲避攻击 - 水翼船(AlliedAntiAirShip):是游戏里最强的[MOD:CORONA]前期[/MOD]水面防空单位,默认使用防空机枪,但可以在干扰器和防空机枪之间切换(SpecialPower_ToggleWeaponScrambler),切换为干扰器之后,水翼船可以禁止敌方目标开火 - 基地车(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)把敌方火力都吸引到自己身上,从而对友军提供掩护。突袭驱逐舰甚至可以上岸的两栖单位,在岸上同样可以吸收敌方火力,掩护陆军和空军 盟军T3常用单位,需要T3升级(Upgrade_AlliedTech3): - 雅典娜炮(AlliedAntiStructureVehicle):盟军远距离对地攻城单位,引导太空卫星激光攻击固定或低速目标,还可以开启巨大的护盾(SpecialPower_ToggleShieldSphere)来掩护附近的友军 - 幻影坦克(AlliedAntiVehicleVehicleTech3):使用光谱武器的先进攻击坦克,伤害很高,但射程很低。[MOD:RA3]因此出场率不如雅典娜炮[/MOD][MOD:CORONA]可以使用隐形技能(SpecialPower_AlliedAntiVehicleVehicleTech3AssassinStateDisguise)潜入到敌方腹地进行袭击,敌方必须造侦察单位来探测隐形才能反制[/MOD] [MOD:RA3] - 世纪轰炸机(AlliedBomberAircraft):擅长攻击建筑等大型目标的战略轰炸机,可用来轰炸敌方基地。攻击后需要返回机场补充弹药。可以运输步兵并使用SpecialPower_EjectPassengersUntargeted让步兵跳伞。 [MOD:CORONA] - 世纪轰炸机(AlliedAntiStructureBomberAircraft):擅长攻击建筑、军舰等大型目标的战略轰炸机。攻击后需要返回机场补充弹药。可以使用技能SpecialPowerReturnToProducer来快速返航。 [MOD:RA3] - 航空母舰(AlliedAntiStructureShip):盟军远距离对地攻城单位,可以释放无人机攻击海面或地面目标 [MOD:CORONA] - 冷冻军团(AlliedCryoLegionnaire):高级支援步兵,可以大幅降低敌方单位的速度让敌方难以冲击阵地或逃跑 [MOD:CORONA] - 波塞冬巡洋舰(AlliedAntiNavyShipTech3):盟军的大型制海巡洋舰 - 谭雅(AlliedCommandoTech1):盟军英雄步兵单位,擅长反步兵和反建筑的无双女士兵。她的时空腰带(SpecialPower_TimeBelt)能让谭雅回溯到一段时间之前的血量和坐标位置。她可以高效炸毁建筑,因此玩家会设法把谭雅进入(或者直接用载具运输到)敌方基地。每个玩家同时只能有一位谭雅 [MOD:CORONA] 盟军T4常用单位,需要T4升级(Upgrade_AlliedTech4): [MOD:CORONA] - 航空母舰(AlliedAntiStructureShip):盟军远距离对地攻城单位,可以释放无人机攻击海面或地面目标 - 先锋炮艇机(AlliedGunshipAircraft):持久的空对地火力,不需要回到机场补充弹药 盟军常用开局: - 常规兵营开局(较为泛用),以下是几种兵营开的变种: A. 兵营、电站、矿场x2、[卖掉兵营 避免摆下机场后电力不足]、机场(卖掉兵营导致步兵较少,但机场更快) B. 兵营、电站、矿场x2、电厂、机场;(先造第二个电厂,这样就不需要卖兵营来省电了,能一直续步兵,但机场更慢) [MOD:CORONA] C. 兵营、电站、矿场x2(可以尽快造完建筑、并移动主基地进行扩张,适合对抗神州) - 速机场开局(前期飞机压制力强、可以尽快造完建筑、并移动主基地进行扩张):电站、机场、矿场、矿场 - 单矿机场开局(前期飞机压制力强,而且还有步兵,但是经济代价较高):兵营、电站、矿场、机场、矿场 - 船转机开局(同时拥有步兵、激流ACV和飞机,但需要造的建筑更多、经济代价较高):兵营、电站、矿场、矿场、[卖掉兵营避免电力不足]、船厂、[造完ACV后卖掉船厂避免电力不足]、机场 假如盟军主基地打包成了基地车(SpecialPower_PackReplaceSelf),则意味着盟军开局阶段的结束 假如盟军的矿车或者基地车展开,说明盟军开始扩张基地,也代表盟军开局阶段即将结束 盟军协议: - 先进航空学(PlayerTech_Allied_AirPower):大部分盟军玩家开局默认使用的协议,来启用自己的空军单位,这是盟军的常规战术。选择该协议不代表盟军立刻会使用空军,请留意玩家实际上的出兵。 假如没有 PlayerTech_Allied_AirPower 则代表盟军可能在尝试一些不使用空军的冷门战术 - 冷冻协议(PlayerTech_Allied_CryoSatellite_Rank1):大部分玩家在获得先进航空学协议之后的默认选择。解锁协议技能SpecialPowerCryoSatelliteLvl1,可以冰冻战场上的一小块区域。假如不及时逃离这片区域,被冻住的目标会被其他单位一击秒杀。 后续可以解锁更高级别的冷冻协议以及协议技能。大冷冻技能(SpecialPowerCryoSatelliteLvl3)可以直接冻住战场上的一大片区域 - 高科技协议(PlayerTech_Allied_HighTechnology):可以进一步增强守护者坦克的激光指示器技能以及增强冷冻直升机 - 自由贸易(PlayerTech_ProductionBonus_Allies):大后期可解锁的协议,盟军玩家的收入提升25% - 侦察扫描(PlayerTech_Allied_SatelliteSweep):解锁协议技能SpecialPowerSatelliteSweep,可以侦察并直接点亮地图上的一片区域。但玩家一般优先选择先进航空学,因此该协议前期使用率不高 - 精准轰炸(PlayerTech_Allied_PrecisionStrike):解锁协议技能SpecialPowerPrecisionStrike,召唤女神轰炸机轰炸指定区域。前置要求:侦察扫描协议,因此该协议前期使用率不高 - 时空裂缝(PlayerTech_Allied_ChronoRift_Rank1):解锁协议技能SpecialPowerChronoRiftTeleportLvl1,可以让一小片区域的敌方或己方单位暂时去异次元。前置要求:侦察扫描、精准轰炸,因此该协议前期使用率不高 后续可以解锁更高级别的时空裂缝协议,范围更大,控场效果更强 盟军超级武器: - 超时空传送仪(AlliedSuperWeapon) 超级武器 每次至少需要3分钟准备 利用超时空科技(SpecialPowerChronosphereObjectSelect, SpecialPowerChronosphereObjectSpawn),把己方或敌方部队送往合适的目的地,或者传到不合适的目的地来秒杀单位(例如把坦克传到水底,或把海军传到岸上),不可传送建筑 - 质子撞击炮(AlliedSuperWeaponAdvanced) 终极武器 每次至少需要6分钟准备(SpecialPowerParticleCannon),对一大片地面目标造成伤害 "; var celestialDescriptions = @" 阵营:神州 神州只能拥有一个主基地。神州的建筑不受建造范围的限制,即使不移动主基地,也可以把建筑摆在任何一个地方。 建造时,神州需要先摆放建筑,随后主基地会自动产生一个飞行核心朝目标位置飞去。飞行核心抵达目标位置后,自动变成建筑。 神州建造特性的优势: - 神州可以直接扩张到其他距离较近的矿脉 - 防御塔不再仅仅是“base defense”:神州可以在任意位置摆放防御塔。神州可以往前线、甚至敌方家里摆放防御塔,然后敌方必须额外造防空单位来拦截飞过来的防御塔核心。 神州建造特性的劣势: - 必须等待核心飞到目的地才可以继续建造。因此远距离建造会大幅降低效率。 - 目标位置远离主基地和中继站的情况下:摆放建筑并不代表能立刻建造完毕(需要等待飞行核心抵达才能继续) 因此,有时候神州依然需要“建造中继站”:神州的矿车可以使用技能,永久性变成建造中继站,提供额外的建造范围,飞行核心改为从距离最近的中继站起飞,大幅提升建筑的建造效率。 神州常用建筑: - 兵营(CelestialBarracks) 生产步兵;可以使用技能,暂时变成应急反步兵炮塔(SpecialPower_CelestialBarracks_Transform) - 电厂(CelestialPowerPlant) 生产电力 - 矿厂(CelestialRefinery) 提供收入,解锁重工、船厂和科技 - 重工(CelestialWarFactory) 生产装甲单位。 - 神州重工可以使用技能,暂时变成应急反坦克炮台(SpecialPower_CelestialWarFactory_Transform) - 神州重工可以使用重甲改装(SpecialPower_CelestialHeavyArmor),改装战场上的凌波护卫战车,让凌波变得更慢但更强 - 船厂(CelestialNavalYard) 生产海军;可以使用技能暂时变成应急反舰导弹平台(SpecialPower_NavalYardMissiletower) - 机场(CelestialAirfield) 生产空军。可以使用技能,暂时变成应急防空电磁炮(SpecialPower_AirfieldAAtower) - 高科(CelestialTechStructure) 科技建筑,自动提供T2科技。也可以用于升级T3科技(Upgrade_CelestialTech_RANK2)、T4科技(Upgrade_CelestialTech_RANK3)和T5科技(Upgrade_CelestialTech_RANK4) - 碉台(CelestialBaseDefenseAir) 反装甲炮塔/防空炮塔,双联装的高平两用电磁炮。 - 浑天塔(CelestialBaseDefenseAdvanced) 先进基地防御,只能对地。这座高塔能同时对多个目标发射光束,对目标进行减速并削弱他们的护甲。 - 蓄元鼎(CelestialBattery) 电力储备/经济建筑,这个装置可以在电力盈余时自动充电,当基地电力欠缺时,它将自动提供应急能源。此建筑也可以出售周围电厂的电力来换取资金(SpecialPower_CelestialElectricitySale) 神州常用升级: - T3科技(Upgrade_CelestialTech_RANK2):解锁T3单位 - T4科技(Upgrade_CelestialTech_RANK3):解锁T4单位 **注意**:神州的 Upgrade_CelestialTech_RANK2 是 T3,不是 T2; 神州基础常用单位: - 矿车(CelestialMiner) 无武装,必要时可以拓展前线基地(SpecialPower_UnpackReplaceSelf) - 天眼哨机(CelestialScoutDrone) 由兵营生产的飞行侦察无人机,无法对敌方目标造成伤害。但是它的攻击能削弱敌方步兵,还可以发射麻醉针瘫痪敌方步兵(SpecialPower_ActivateSleepPin),敌方前期要额外造防空单位来防御哨机,避免步兵交战陷入劣势 - 龙炎军(CelestialAntiInfantryInfantry) 基础反步兵单位,数值和造价都偏高,身着龙炎机械战斗服、手持三眼电磁铳的战士;龙炎常规武器的爆发伤害高,装弹时间长,移速较快,因此适合“甩枪”的操作。有经验的玩家会让龙炎反复前进和撤退,让龙炎进行拉扯,在前期步兵战斗中取得优势。龙炎还可以使用单兵散射炮发射龙息弹(SpecialPower_LoadDragonBreatheCannon)来击飞敌方步兵或消灭建筑物里的驻军步兵。 - 铁卫(CelestialAntiVehicleInfantry) 反装甲/防空步兵,能用高速穿甲弹击穿厚重的坦克装甲,亦能在架设护盾后发射破墙榴弹 (SpecialPower_ToggleShield) - 凌波护卫战车(CelestialAntiInfantryVehicle_B) 轻型反步兵运兵战车,配备机炮的两栖步战车,可以运输步兵(SpecialPower_GatherPassenger) - 磁弩(CelestialAntiAirShip) 轻型两栖防空车,由重工生产,可以使用技能(SpecialPower_ToggleHeavyEMCannonWeapon)把防空速射炮换成对地的磁轨炮,变成轻型的两栖反载具单位,或者使用相同技能切换回防空速射炮 - 磁弩(CelestialAntiAirShip_Water) 磁弩也可从船厂生产 - 乌篷猎船(CelestialAntiNavyShipTech1) 反舰快艇/猎潜艇,这些不起眼的轻型小船装备了能发射聚焦冲击波的武器,能对军舰和潜艇造成破坏,猎船可以放置声纳浮标让敌方潜艇无处遁形(SpecialPower_CelestialSonarBuoy) - 凤凰战机(CelestialFighterAircraft) 制空战斗机,可以使用技能快速回到机场(SpecialPowerReturnToProducer_F) - 毕方支援机(CelestialSupportAircraft) 支援直升机,搭载高能激光器的直升机,本身伤害较低,但能使敌方车辆装甲熔融,降低敌方目标护甲。使用技能可以在激光器主武器和电磁支援之间切换(SpecialPower_ToggleCelestialSupportAircraftBuffWeapon),切换为电磁支援后可以增加友军输出 神州T2常用单位,需要神州高科(CelestialTechStructure): - 岚影刺(CelestialInfiltrationInfantry) 渗透部队/狙击手,她可以从远处暗杀敌方步兵,也可以化妆成敌方步兵(SpecialPower_CelestialDisguise),让敌方无法发现 - 朱雀(CelestialAttackerAircraft) 对地攻击机,搭载中型离子炮的反装甲攻击机,还可以喷火反步兵或者削弱敌方装甲(SpecialPower_ActivateFire) - 麒麟(CelestialAntiVehicleVehicleTech1) 反装甲主战坦克,神州陆军的新一代中流砥柱。可以使用技能偏转敌方来袭的炮火和导弹(SpecialPower_ToggleRangeUpdateCelestial) - 青锋导弹车(CelestialLongRangeMissileVehicle_B) 远程反装甲,装备反坦克导弹的轻型载具,足以应对装甲目标,还可以发射烟雾弹削弱敌方单位的射程(SpecialPower_TriggerSmokeBombMissile) - 计蒙驱逐舰(CelestialAlmightlyShip) 制海战舰,多用途驱逐舰,用舰炮和导弹对付海面、潜水或地面目标。可以使用阻止敌方单位使用技能(SpecialPower_CelestialShipScrambler) 神州T3常用单位,需要T3升级(Upgrade_CelestialTech_RANK2): - 天罡(CelestialAntiInfantryInfantryAdvanced) 先进反步兵/反飞行器的机甲步兵,配备外骨骼和迅雷转轮机关铳的精英步兵,能快速收割敌方步兵、击落敌方飞行器。还可以瘫痪牵制敌方载具(SpecialPower_ActivateEMPThunder) - 祝融(CelestialAntiVehicleVehicleTech3) 先进反装甲重型坦克,搭载了转轮式装弹的大型离子炮,借由主炮的余热可持续提高武器射速,使用技能可以大幅提高移速(SpecialPower_RapidCooling) - 白虎(CelestialAntiStructureVehicle) 远距离对地攻城单位,可以发射高能等离子体团攻击远处的目标,也可以产生临时量子压制力场(SpecialPower_CelestialQuantumBreak)减速并削弱力场内的敌方目标 - 重明(CelestialInterceptorAircraft) 擅长拦截敌方重型空军的截击机,能发射炽热等离子束武器,可以使用技能快速回到机场(SpecialPowerReturnToProducer_F) - 金乌(CelestialBomberAircraft) 重型轰炸机,发射导弹攻击低速目标、建筑和敌方军舰。可以使用技能快速回到机场(SpecialPowerReturnToProducer_F) - 玄冥(CelestialAntiNavyShipTech3) 先进制海战舰,装备多种先进武器的巨型战舰,可以使用技能扫描远距离的海上目标并发射导弹(SpecialPower_CANSTier3HuntingMissile) 神州T4常用单位,需要T3升级(Upgrade_CelestialTech_RANK3): - 摇光巡天炮(CelestialAdvanceAircraftTech4) 实验级飞行重轰炸,常态下无武装,但是可以展开到亚轨道高空。使用技能在常态和亚轨道状态之间切换(SpecialPower_CAAT4_Transform)。在亚轨道彻底展开的摇光巡天炮,能对一切目标发动穿透力极强的聚变射流攻击! - 玄武(CelestialAntiStructureShip) 神州远距离对地轰炸,导弹攻击潜艇,额外装备有远近皆宜的对舰武器,还可以使用技能发射核弹(SpecialPower_CelestialShipMissle_01) - 破军金甲(CelestialAntiVehicleVehicleTech4) 实验级反装甲机器人 巨大的战斗机甲,凭借无与伦比的厚重装甲冲进敌方坦克集群,并挥动能量巨剑将目标劈成两半,是敌方装甲部队的噩梦,可以使用技能越过障碍或直接降落到敌方部队中间(SpecialPower_CelestialArmybreakerLeap) 神州常用开局: - 常规兵营开局:兵营,电厂,矿场,矿场,矿场;依靠神州强大的前期步兵进行压制,直接完成第三个矿场的扩张,然后再造其他建筑。神州的天眼哨机和碉台飞行核心可能会迫使对手出防空步兵(而不是出反步兵的基础步兵),可能让神州基础步兵获得短暂的数量优势 - 二矿重工开局:兵营,电厂,矿场,矿场,重工;假如前线步兵压力较大,也可以提前造重工,用凌波护卫战车辅助步兵,也可以让磁弩防空车切换成对地武器,并从陆地或海上进攻 - 二矿机场开局:兵营,电厂,矿场,矿场,机场;适合对帝国的天狗机甲等单位进行空军压制 神州协议: - 百夫长(PlayerTech_Celestial_CenturionUpgrade):解锁协议技能SpecialPower_CelestialCenturionUpgrade,可强化一个步兵。 - 压制力场(PlayerTech_Celestial_EMSuppressField_Lv1):百夫长的后续协议。解锁协议技能SpecialPower_Celestial_EMSuppressField_Lv1,可以让一小片区域内的敌方单位大幅减速。 后续可以解锁更高级别的压制力场。大型压制力场(SpecialPower_Celestial_EMSuppressField_Lv3)可以让一大片区域内的敌方单位大幅减速。 - 空投仓(PlayerTech_Celestial_SpaceReinforce):压制力场的后续协议。解锁协议技能SpecialPower_CelestialSpaceReinforce,可从太空往地图上的任意陆地区域投送龙炎军和破甲铁卫 - 电能纳贡(PlayerTech_Celestial_PowerSealOff):解锁协议技能SpecialPower_CelestialPowerSealOff。只能对敌方电厂释放,让敌方电力暂时减少、己方电力暂时增加 - 天火塔(PlayerTech_Celestial_EMTurretDrop):电能纳贡的后续协议。解锁协议技能SpecialPowerCelestialEMTurretDrop,可从太空往地图上的任意陆地区域投送一个对地的激光防御塔 - 雷铸天兵(PlayerTech_Celestial_lightningTroopUpgrade_Lv1):天火塔的后续协议。解锁协议技能SpecialPower_CelestiallightningTroopUpgrade_Lv1,产生一道闪电,可以为己方部队充能并大幅强化己方部队,也可以用于对敌方部队造成伤害 神州超级武器: - 日晷阵列(CelestialSuperWeapon) 超级武器 每次至少需要3分钟准备,可以释放止戈力场(SpecialPowerPause01),止戈力场内的敌我双方均不能开火,可用于阻挡敌方特殊能力、协议、或紧急救援己方单位 - 浴日神坛(CelestialSuperWeaponAdvanced) 终极武器 每次至少需要6分钟准备,向指定区域发射日冕风暴(SpecialPowerCelestialCannon),杀伤区域内的所有目标 "; var infinityIsleDescription = @" 当前交战的地图是:无限岛 这张地图是对称的,只有一条陆地进攻路线,也是主要的陆地交战区域,中央高地。 中央高地是长条状的,只有两个出入口,位于中央高地的两端,通向两位玩家的出生点。 玩家可以在高地战场正面交锋,也可以选择绕海,或者使用空军(不受地形限制)。 低地被中央高地分割成两部分,低地都是三面环海(还有一面是高地),两栖单位可以从低地上岸或下水。 中央高地也有靠海的地方,但两栖单位无法跨越悬崖从高地直接入海,需要从低地绕道。(额外矿区) 每个矿区可以摆放一个矿厂。 # 地图参数 海面高度:Z=200 低地高度:Z=210 高地高度:Z=280 ## 出生点1(陆地)(X=1390,Y=1590,Z=210) - 矿区(X=1230,Y=1975,Z=210) - 矿区(X=1760,Y=1485,Z=210) ### 高地油井(X=2280,Y=2770,Z=280) ### 扩张方向 - 矿区,海面,远离中央区域(X=870,Y=870,Z=200) - 矿区,中央高地(X=1740,Y=2760,Z=280),在它附近有:高地通往出生点1的唯一陆地路线。 - 额外矿区,海面,中央高地悬崖外面的海矿(X=2710,Y=2865,Z=200) ## 出生点2(陆地)(X=3980,Y=2190,Z=210) - 矿区(X=4030,Y=1760,Z=210) - 矿区(X=3540,Y=2290,Z=210) ### 高地油井(X=2980,Y=1050,Z=280) ### 扩张方向 - 矿区,海面,远离中央区域(X=4390,Y=2910,Z=280) - 矿区,中央高地(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 "; static string Process(string text, Mod mod) { text = text.Trim(); var processed = new StringBuilder(); bool skipNextLine = false; foreach (var line in text.Replace("\r", "").Split('\n')) { if (skipNextLine) { skipNextLine = false; continue; } const string prefix = "[MOD:"; const string endTag = "[/MOD]"; if (line.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { if (!line.StartsWith($"[MOD:{mod.ModName.ToUpperInvariant()}]", StringComparison.OrdinalIgnoreCase)) { skipNextLine = true; } continue; } // also support inline replace: content[MOD:CORONA]content[/MOD]content var l = line; while (true) { var startIndex = l.IndexOf(prefix, StringComparison.OrdinalIgnoreCase); if (startIndex == -1) { break; } // then there is mod name, ']', content, and end tag // we need: // - substring before prefix // - mod name for checking // - content between ']' and [MOD:...] // - substring after end tag var endIndex = l.IndexOf(endTag, startIndex + prefix.Length, StringComparison.OrdinalIgnoreCase); if (endIndex == -1) { throw new FormatException(); } var modNameStartIndex = startIndex + prefix.Length; var modNameEndIndex = l.IndexOf(']', modNameStartIndex); if (modNameEndIndex == -1 || modNameEndIndex > endIndex) { throw new FormatException(); } var modName = l.Substring(modNameStartIndex, modNameEndIndex - modNameStartIndex); var contentStartIndex = modNameEndIndex + 1; var content = l.Substring(contentStartIndex, endIndex - contentStartIndex); if (modName.Equals(mod.ModName, StringComparison.OrdinalIgnoreCase)) { l = l.Substring(0, startIndex) + content + l.Substring(endIndex + endTag.Length); } else { l = l.Substring(0, startIndex) + l.Substring(endIndex + endTag.Length); } } processed.Append(l); processed.Append('\n'); } return processed.ToString(); } var sb = new StringBuilder(); sb.AppendLine(generalDescriptions); sb.AppendLine(); var factions = players .Select(kv => ModData.GetFaction(mod, kv.Value.FactionId).Name) .Distinct() .ToArray(); var factionDescriptions = new Dictionary { { "盟军", Process(alliedDescriptions, mod) }, { "神州", Process(celestialDescriptions, mod) }, }; foreach (var faction in factions) { if (factionDescriptions.TryGetValue(faction, out var description)) { sb.AppendLine(description); sb.AppendLine(); } } return sb.ToString().Replace("\r", ""); } public static string BuildUserPrompt(Mod mod, ImmutableSortedDictionary players, string commandData) { var playerNamesForAI = PlayerNamesForAI(mod, players); var playerData = players.Select(kv => { var id = kv.Key; var player = kv.Value; var playerNameForAI = playerNamesForAI[id]; var prefix = player.IsComputer ? "电脑" : "玩家"; var name = player.IsComputer ? $"[{player.PlayerName}AI]" : player.PlayerName; var factionName = ModData.GetFaction(mod, player.FactionId).Name; if (player.Team >= 0) { return $"{prefix}#{id} {name} ({playerNameForAI}),{factionName},队伍{player.Team}"; } return $"{prefix}#{id} {name} ({playerNameForAI}),{factionName}"; }); var sb = new StringBuilder(); sb.AppendLine("请你阅读并分析以下数据,首先进行初步分析,然后判断是否需要分段处理数据"); sb.AppendLine("玩家列表:"); sb.AppendLine(string.Join("\n", playerData)); sb.AppendLine("数据:"); sb.AppendLine(commandData); return sb.ToString().Replace("\r", ""); } public static (int BytesCount, int EstimatedTokenCount) EstimateTokenCount(string text) { var bytesCount = Encoding.UTF8.GetByteCount(text); var estimatedTokenCount = (int)Math.Ceiling(bytesCount / 2.2); // 2.2 is tested return (bytesCount, estimatedTokenCount); } public static ImmutableSortedDictionary PlayerNamesForAI(Mod mod, ImmutableSortedDictionary players) { var computers = players.Where(kv => kv.Value.IsComputer).ToImmutableSortedDictionary(); var humanPlayers = players.Where(kv => !kv.Value.IsComputer).ToImmutableSortedDictionary(); var result = new Dictionary(); foreach (var kv in players) { var id = kv.Key; var player = kv.Value; var prefix = player.IsComputer ? "AI_" : "Player"; var faction = ModData.GetFaction(mod, player.FactionId); // check if player faction id is only appeared once, if so use faction name as player name var checkSource = player.IsComputer ? computers : humanPlayers; if (checkSource.Count(kv2 => kv2.Value.FactionId == player.FactionId) == 1) { var letter = faction.Name switch { "盟军" => "A", "苏联" => "S", "帝国" => "E", "神州" => "C", "随机" => "R", _ when faction.Kind is FactionKind.Observer => "O", _ => id.ToString(), }; result[id] = $"{prefix}{letter}"; } else { result[id] = faction.Kind is FactionKind.Observer ? $"{prefix}O{id}" : $"{prefix}{id}"; } } return result.ToImmutableSortedDictionary(); } public async Task AnalyzeAsync( string model, string instruction, string text, Dictionary extraParams, Action onChunk, CancellationToken cancellationToken) { foreach (var kv in extraParams) { _state[kv.Key] = kv.Value; } _state["model"] = model; _state["messages"] = _messages; _state["stream"] = true; _state["stream_options"] = new { include_usage = true }; _messages.Clear(); _messages.AddRange( [ new { role = "system", content = instruction }, new { role = "user", content = text } ]); var result = await DoRequest(onChunk, cancellationToken); var splitted = result.Response.Split('\n').ToList(); var titleIndex = splitted.FindIndex(l => l.Contains("[分段列表]")); if (titleIndex == -1) { throw new Exception("AI分析失败"); } _segments.Clear(); _currentSegment = 0; // regex match two timespan in "[0:00.0]~[0:55.4]" var timeSpanRegex = new Regex(@"\[([^]]+)\]~\[([^]]+)\]"); for (var i = titleIndex + 1; i < splitted.Count; ++i) { var line = splitted[i]; var match = timeSpanRegex.Match(line); if (match.Success) { var startTimeText = match.Groups[1].Value; var endTimeText = match.Groups[2].Value; 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; } } result.Segments = _segments; result.CurrentSegment = _currentSegment; return result; } public async Task ContinueAnalyzeAsync( Action onChunk, 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", content = instruction }); var result = await DoRequest(onChunk, cancellationToken); ++_currentSegment; result.Segments = _segments; result.CurrentSegment = _currentSegment; return result; } private async Task DoRequest(Action onChunk, CancellationToken cancellationToken) { var inputJson = JsonSerializer.Serialize(_state); using var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); request.Content = new StringContent(inputJson, Encoding.UTF8, "application/json"); using var response = await _http.SendAsync( request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); response.EnsureSuccessStatusCode(); using var stream = await response.Content.ReadAsStreamAsync(); using var reader = new StreamReader(stream); var fullBuilder = new StringBuilder(); var result = new Result(); while (!reader.EndOfStream) { var line = await reader.ReadLineAsync(); if (string.IsNullOrWhiteSpace(line)) { continue; } if (!line.StartsWith("data: ")) { continue; } var data = line.Substring(6); if (data == "[DONE]") { break; } onChunk?.Invoke(new AIChunk { Type = AIChunkType.Json, Text = data }); using var doc = JsonDocument.Parse(data); if (doc.RootElement.TryGetProperty("usage", out var usage) && usage.ValueKind == JsonValueKind.Object) { static int? GetIntegerProperty(JsonElement @object, string field) { if (@object.TryGetProperty(field, out var value) && value.ValueKind is JsonValueKind.Number) { return value.GetInt32(); } return null; } result.PromptTokens = GetIntegerProperty(usage, "prompt_tokens"); result.TotalTokens = GetIntegerProperty(usage, "total_tokens"); result.CompletionTokens = GetIntegerProperty(usage, "completion_tokens"); result.ReasoningTokens = GetIntegerProperty(usage, "reasoning_tokens"); } if (!doc.RootElement.TryGetProperty("choices", out var choices) || choices.ValueKind != JsonValueKind.Array || choices.GetArrayLength() == 0) { continue; } var delta = choices[0].GetProperty("delta"); // ===== content ===== if (delta.TryGetProperty("content", out var content)) { var text = content.GetString(); if (!string.IsNullOrEmpty(text)) { fullBuilder.Append(text); onChunk?.Invoke(new AIChunk { Type = AIChunkType.Content, Text = text }); } } // ===== reasoning (optional, DeepSeek / some models) ===== if (delta.TryGetProperty("reasoning_content", out var reasoning)) { var text = reasoning.GetString(); if (!string.IsNullOrEmpty(text)) { onChunk?.Invoke(new AIChunk { Type = AIChunkType.Reasoning, Text = text }); } } } var resultText = fullBuilder.ToString(); _messages.Add(new { role = "assistant", content = resultText }); result.Response = resultText; return result; } private static TimeSpan ParseAITimeSpan(string input) { if (string.IsNullOrWhiteSpace(input)) { throw new FormatException("Empty input"); } input = input.Trim(); var parts = input.Split(':'); if (parts.Length == 0) { throw new FormatException("Invalid format"); } // ----------------------------- // 1. 解析最后一段:seconds + fraction // ----------------------------- if (!float.TryParse(parts.Last(), out float floatSeconds)) { throw new FormatException("Invalid seconds"); } int seconds = (int)floatSeconds; int milliseconds = (int)Math.Round((floatSeconds - seconds) * 1000); // ----------------------------- // 2. 累加前面的部分(从右往左) // ----------------------------- long totalSeconds = seconds; long multiplier = 60; // 每层递进:秒->分->时->天... for (int i = parts.Length - 2; i >= 0; i--) { if (!long.TryParse(parts[i], out long value)) throw new FormatException($"Invalid number: {parts[i]}"); totalSeconds += value * multiplier; multiplier *= 60; } var result = TimeSpan.FromSeconds(totalSeconds) + TimeSpan.FromMilliseconds(milliseconds); return result; } } }