Files
AnotherReplayReader/Utils/AIAnalyze.cs
2026-06-21 14:42:04 +02:00

1116 lines
58 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<object> _messages = [];
private readonly Dictionary<string, object> _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<int, Player> 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使用了不同的建造者ID246 → 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)
地图边界X0~5300
地图边界Y0~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<string, string>
{
{ "盟军", 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<int, Player> 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<int, string> PlayerNamesForAI(Mod mod, ImmutableSortedDictionary<int, Player> players)
{
var computers = players.Where(kv => kv.Value.IsComputer).ToImmutableSortedDictionary();
var humanPlayers = players.Where(kv => !kv.Value.IsComputer).ToImmutableSortedDictionary();
var result = new Dictionary<int, string>();
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<Result> AnalyzeAsync(
string model,
string instruction,
string text,
Dictionary<string, object> extraParams,
Action<AIChunk> 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<Result> ContinueAnalyzeAsync(
Action<AIChunk> 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<Result> DoRequest(Action<AIChunk> 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;
}
}
}