Skip to content

Commit 24992ee

Browse files
authoredMar 3, 2025··
Merge pull request #815 from UnrealMultiple/cai_dev
添加CaiBotLite插件以及CaiBot的一些修改
2 parents 08cdec2 + f77968d commit 24992ee

22 files changed

+1537
-304
lines changed
 

‎Plugin.sln

+20
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlayerSpeed", "src\PlayerSp
252252
EndProject
253253
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModifyWeapons", "src\ModifyWeapons\ModifyWeapons.csproj", "{76988FD0-FDEF-427F-856E-DB5F0BF8FCC5}"
254254
EndProject
255+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TShockConfigMultiLang", "src\TShockConfigMultiLang\TShockConfigMultiLang.csproj", "{573C495D-9693-4B23-AC88-E8829E38D3B7}"
256+
EndProject
255257
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoClear", "src\AutoClear\AutoClear.csproj", "{1F782958-F002-498A-A928-D095F1BCD2CD}"
256258
EndProject
257259
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomMonster", "src\CustomMonster\CustomMonster.csproj", "{24596D5B-31DA-4E1A-9748-095942500946}"
@@ -270,6 +272,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SurvivalCrisis", "src\Survi
270272
EndProject
271273
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReverseWorld", "src\ReverseWorld\ReverseWorld.csproj", "{1E7334CF-E0C9-493D-8D6D-4E95BCB33E79}"
272274
EndProject
275+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CaiBotLite", "src\CaiBotLite\CaiBotLite.csproj", "{8747AAC3-FF12-429A-BBFC-4EB7FD51FF17}"
276+
EndProject
273277
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteWall", "src\NoteWall\NoteWall.csproj", "{4CD02D78-581C-441D-A492-36278A6F9A54}"
274278
EndProject
275279
Global
@@ -1256,6 +1260,14 @@ Global
12561260
{76988FD0-FDEF-427F-856E-DB5F0BF8FCC5}.Release|Any CPU.Build.0 = Release|Any CPU
12571261
{76988FD0-FDEF-427F-856E-DB5F0BF8FCC5}.Release|x64.ActiveCfg = Release|Any CPU
12581262
{76988FD0-FDEF-427F-856E-DB5F0BF8FCC5}.Release|x64.Build.0 = Release|Any CPU
1263+
{573C495D-9693-4B23-AC88-E8829E38D3B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
1264+
{573C495D-9693-4B23-AC88-E8829E38D3B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
1265+
{573C495D-9693-4B23-AC88-E8829E38D3B7}.Debug|x64.ActiveCfg = Debug|Any CPU
1266+
{573C495D-9693-4B23-AC88-E8829E38D3B7}.Debug|x64.Build.0 = Debug|Any CPU
1267+
{573C495D-9693-4B23-AC88-E8829E38D3B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
1268+
{573C495D-9693-4B23-AC88-E8829E38D3B7}.Release|Any CPU.Build.0 = Release|Any CPU
1269+
{573C495D-9693-4B23-AC88-E8829E38D3B7}.Release|x64.ActiveCfg = Release|Any CPU
1270+
{573C495D-9693-4B23-AC88-E8829E38D3B7}.Release|x64.Build.0 = Release|Any CPU
12591271
{1F782958-F002-498A-A928-D095F1BCD2CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
12601272
{1F782958-F002-498A-A928-D095F1BCD2CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
12611273
{1F782958-F002-498A-A928-D095F1BCD2CD}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -1328,6 +1340,14 @@ Global
13281340
{1E7334CF-E0C9-493D-8D6D-4E95BCB33E79}.Release|Any CPU.Build.0 = Release|Any CPU
13291341
{1E7334CF-E0C9-493D-8D6D-4E95BCB33E79}.Release|x64.ActiveCfg = Release|Any CPU
13301342
{1E7334CF-E0C9-493D-8D6D-4E95BCB33E79}.Release|x64.Build.0 = Release|Any CPU
1343+
{8747AAC3-FF12-429A-BBFC-4EB7FD51FF17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
1344+
{8747AAC3-FF12-429A-BBFC-4EB7FD51FF17}.Debug|Any CPU.Build.0 = Debug|Any CPU
1345+
{8747AAC3-FF12-429A-BBFC-4EB7FD51FF17}.Debug|x64.ActiveCfg = Debug|Any CPU
1346+
{8747AAC3-FF12-429A-BBFC-4EB7FD51FF17}.Debug|x64.Build.0 = Debug|Any CPU
1347+
{8747AAC3-FF12-429A-BBFC-4EB7FD51FF17}.Release|Any CPU.ActiveCfg = Release|Any CPU
1348+
{8747AAC3-FF12-429A-BBFC-4EB7FD51FF17}.Release|Any CPU.Build.0 = Release|Any CPU
1349+
{8747AAC3-FF12-429A-BBFC-4EB7FD51FF17}.Release|x64.ActiveCfg = Release|Any CPU
1350+
{8747AAC3-FF12-429A-BBFC-4EB7FD51FF17}.Release|x64.Build.0 = Release|Any CPU
13311351
{4CD02D78-581C-441D-A492-36278A6F9A54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
13321352
{4CD02D78-581C-441D-A492-36278A6F9A54}.Debug|Any CPU.Build.0 = Debug|Any CPU
13331353
{4CD02D78-581C-441D-A492-36278A6F9A54}.Debug|x64.ActiveCfg = Debug|Any CPU

‎src/CaiBot/CaiBot.csproj

+4-7
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@
33
<Import Project="..\..\template.targets"/>
44

55
<ItemGroup>
6-
<None Remove="SixLabors.ImageSharp.dll"/>
6+
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
77
</ItemGroup>
88

99
<ItemGroup>
10-
<EmbeddedResource Include="SixLabors.ImageSharp.dll"/>
11-
</ItemGroup>
12-
<ItemGroup>
13-
<Reference Include="SixLabors.ImageSharp">
14-
<HintPath>SixLabors.ImageSharp.dll</HintPath>
15-
</Reference>
10+
<EmbeddedResource Include="$(NuGetPackageRoot)sixlabors.imagesharp\3.1.6\lib\$(TargetFramework)\SixLabors.ImageSharp.dll">
11+
<Link>SixLabors.ImageSharp.dll</Link>
12+
</EmbeddedResource>
1613
</ItemGroup>
1714
<ItemGroup>
1815
<ProjectReference Include="..\Economics.RPG\Economics.RPG.csproj"/>

‎src/CaiBot/CaiBotApi.cs

+213-213
Large diffs are not rendered by default.

‎src/CaiBot/CaiBotPlayer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public CaiBotPlayer()
1616
{
1717
this.Group = new SuperAdminGroup();
1818
this.AwaitingResponse = new Dictionary<string, Action<object>>();
19-
this.Account = new UserAccount { Name = "CaiBot" };
19+
this.Account = new UserAccount { Name = "CaiBot",ID = -1 };
2020
}
2121

2222
public override void SendMessage(string msg, Color color)

‎src/CaiBot/Login.cs

+7-3
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ internal static bool MessageBuffer_InvokeGetData(Hooks.MessageBuffer.orig_Invoke
5151
}
5252
}
5353

54-
RestObject re = new () { { "type", "whitelistV2" }, { "name", player.Name }, { "uuid", uuid }, { "ip", player.IP } };
55-
5654
if (!CaiBotApi.IsWebsocketConnected)
5755
{
5856
if (CaiBotApi.WhiteListCaches.TryGetValue(player.Name, out var whiteListCache2)) //从缓存处读取白名单
@@ -76,7 +74,13 @@ internal static bool MessageBuffer_InvokeGetData(Hooks.MessageBuffer.orig_Invoke
7674
}
7775
else
7876
{
79-
_ = CaiBotApi.SendDateAsync(re.ToJson());
77+
78+
PacketWriter packetWriter = new ();
79+
packetWriter.SetType("whitelistV2")
80+
.Write("name", player.Name)
81+
.Write("uuid", uuid)
82+
.Write("ip", player.IP)
83+
.Send();
8084
}
8185

8286
}

‎src/CaiBot/MapGenerator.cs

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ private static MapTile NewWorldMapIndexer(Func<WorldMap, int, int, MapTile> orig
3636

3737
private static void LightWholeMap()
3838
{
39+
Main.Map = new WorldMap(Main.maxTilesX, Main.maxTilesY) { _tiles = new MapTile[Main.maxTilesX, Main.maxTilesY] };
3940
for (var x = 0; x < Main.maxTilesX; x++)
4041
{
4142
for (var y = 0; y < Main.maxTilesY; y++)

‎src/CaiBot/PacketWriter.cs

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using Newtonsoft.Json;
2+
using System.Net.WebSockets;
3+
using System.Text;
4+
using TShockAPI;
5+
6+
namespace CaiBot;
7+
8+
[Serializable]
9+
public class PacketWriter : Dictionary<string, object>
10+
{
11+
public static bool Debug;
12+
public static bool IsLiteMessage;
13+
public static ClientWebSocket WebSocket = null!;
14+
private readonly long _groupId;
15+
private readonly string _groupOpenId;
16+
private readonly string _msgId;
17+
private readonly long _at;
18+
19+
public static void Init(bool isLiteMessage,bool debug = false)
20+
{
21+
IsLiteMessage = isLiteMessage;
22+
Debug = debug;
23+
}
24+
25+
26+
public PacketWriter()
27+
{
28+
this._groupId = 0L;
29+
this._at = 0L;
30+
this._groupOpenId = "";
31+
this._msgId = "";
32+
}
33+
34+
public PacketWriter(long groupId,long at = 0L)
35+
{
36+
this._groupId = groupId;
37+
this._at = at;
38+
this._groupOpenId = "";
39+
this._msgId = "";
40+
}
41+
42+
public PacketWriter(string groupOpenId, string msgId)
43+
{
44+
this._groupId = 0L;
45+
this._at = 0L;
46+
this._groupOpenId = groupOpenId;
47+
this._msgId = msgId;
48+
}
49+
50+
51+
52+
public PacketWriter Write(string key, object value)
53+
{
54+
this.Add(key, value);
55+
return this;
56+
}
57+
public PacketWriter SetType(string type)
58+
{
59+
this.Add("type",type);
60+
return this;
61+
}
62+
public void Send()
63+
{
64+
var botPrefix = IsLiteMessage ? "CaiBotLite" : "CaiBot";
65+
try
66+
{
67+
if (IsLiteMessage)
68+
{
69+
if (this._groupOpenId != "")
70+
{
71+
this.Add("group", this._groupOpenId);
72+
}
73+
if (this._msgId != "")
74+
{
75+
this.Add("msg_id", this._msgId);
76+
}
77+
}
78+
else
79+
{
80+
if (this._groupId != 0)
81+
{
82+
this.Add("group", this._groupId);
83+
}
84+
if (this._at != 0L)
85+
{
86+
this.Add("at", this._at);
87+
}
88+
}
89+
var message = JsonConvert.SerializeObject(this, Formatting.None);
90+
if (Debug)
91+
{
92+
TShock.Log.ConsoleInfo($"[{botPrefix}]发送BOT数据包:{message}");
93+
}
94+
95+
var messageBytes = Encoding.UTF8.GetBytes(message);
96+
_ = WebSocket.SendAsync(new ArraySegment<byte>(messageBytes), WebSocketMessageType.Text, true,
97+
CancellationToken.None);
98+
}
99+
catch (Exception e)
100+
{
101+
TShock.Log.ConsoleInfo($"[{botPrefix}]发送数据包时发生错误:{e}");
102+
}
103+
}
104+
105+
public new object this[string key]
106+
{
107+
get => this.TryGetValue(key, out var obj);
108+
set
109+
{
110+
if (!this.ContainsKey(key))
111+
{
112+
this.Add(key, value);
113+
}
114+
else
115+
{
116+
base[key] = value;
117+
}
118+
}
119+
}
120+
}

‎src/CaiBot/Plugin.cs

+53-65
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,16 @@ namespace CaiBot;
1818
[ApiVersion(2, 1)]
1919
public class Plugin : TerrariaPlugin
2020
{
21-
public static readonly Version VersionNum = new (2025, 1, 29, 1); //日期+版本号(0,1,2...)
21+
public static readonly Version VersionNum = new (2025, 3, 2, 1); //日期+版本号(0,1,2...)
2222
internal static int InitCode = -1;
2323
public static bool LocalMode => Config.Settings.BotApi != "api.terraria.ink:22334";
2424
public static bool DebugMode;
2525
private static bool _stopWebsocket;
26-
internal static ClientWebSocket WebSocket = new ();
27-
private static readonly Task WebSocketTask = Task.CompletedTask;
28-
private static readonly CancellationTokenSource TokenSource = new ();
29-
26+
internal static ClientWebSocket WebSocket
27+
{
28+
get => PacketWriter.WebSocket;
29+
set => PacketWriter.WebSocket = value;
30+
}
3031
public Plugin(Main game) : base(game)
3132
{
3233
}
@@ -40,25 +41,26 @@ public Plugin(Main game) : base(game)
4041
public override void Initialize()
4142
{
4243
AppDomain.CurrentDomain.AssemblyResolve += this.CurrentDomain_AssemblyResolve;
43-
MapGenerator.Init();
44-
EconomicSupport.Init();
4544
Commands.ChatCommands.Add(new Command("caibot.admin", this.CaiBotCommand, "caibot"));
4645
Config.Settings.Read();
4746
Config.Settings.Write();
4847
DebugMode = Program.LaunchParameters.ContainsKey("-caidebug");
48+
ServerApi.Hooks.NetGetData.Register(this, Login.OnGetData, int.MaxValue);
49+
ServerApi.Hooks.ServerChat.Register(this, OnChat, int.MaxValue);
50+
ServerApi.Hooks.GamePostInitialize.Register(this, GenBindCode);
4951
BanManager.OnBanPostAdd += this.OnBanInsert;
5052
Hooks.MessageBuffer.InvokeGetData += Login.MessageBuffer_InvokeGetData;
51-
ServerApi.Hooks.NetGetData.Register(this, Login.OnGetData, int.MaxValue);
52-
ServerApi.Hooks.ServerChat.Register(this, this.OnChat, int.MaxValue);
53-
PlayerHooks.PlayerPostLogin += this.PlayerHooksOnPlayerPostLogin;
53+
PlayerHooks.PlayerPostLogin += PlayerHooksOnPlayerPostLogin;
5454
PlayerHooks.PlayerLogout += this.PlayerHooksOnPlayerLogout;
5555
GeneralHooks.ReloadEvent += this.GeneralHooksOnReloadEvent;
56-
ServerApi.Hooks.GamePostInitialize.Register(this, GenBindCode);
57-
Task.Run(StartCaiApi, TokenSource.Token);
58-
Task.Run(StartHeartBeat, TokenSource.Token);
56+
MapGenerator.Init();
57+
EconomicSupport.Init();
58+
PacketWriter.Init(false, DebugMode);
59+
Task.Factory.StartNew(StartCaiApi, TaskCreationOptions.LongRunning);
60+
Task.Factory.StartNew(StartHeartBeat, TaskCreationOptions.LongRunning);
5961
if (LocalMode)
6062
{
61-
TShock.Log.ConsoleWarn($"[CaiAPI]CaiBot插件正在以本地模式运行, 当前API地址: {Config.Settings.BotApi}");
63+
TShock.Log.ConsoleWarn($"[CaiBot]CaiBot插件正在以本地模式运行, 当前API地址: {Config.Settings.BotApi}");
6264
}
6365
}
6466

@@ -68,22 +70,17 @@ protected override void Dispose(bool disposing)
6870
{
6971
var asm = Assembly.GetExecutingAssembly();
7072
Commands.ChatCommands.RemoveAll(c => c.CommandDelegate.Method.DeclaringType?.Assembly == asm);
71-
MapGenerator.Dispose();
7273
AppDomain.CurrentDomain.AssemblyResolve -= this.CurrentDomain_AssemblyResolve;
73-
Hooks.MessageBuffer.InvokeGetData -= Login.MessageBuffer_InvokeGetData;
74-
BanManager.OnBanPostAdd -= this.OnBanInsert;
7574
ServerApi.Hooks.NetGetData.Deregister(this, Login.OnGetData);
7675
ServerApi.Hooks.GamePostInitialize.Deregister(this, GenBindCode);
77-
ServerApi.Hooks.ServerChat.Deregister(this, this.OnChat);
78-
PlayerHooks.PlayerPostLogin -= this.PlayerHooksOnPlayerPostLogin;
76+
ServerApi.Hooks.ServerChat.Deregister(this, OnChat);
77+
Hooks.MessageBuffer.InvokeGetData -= Login.MessageBuffer_InvokeGetData;
78+
BanManager.OnBanPostAdd -= this.OnBanInsert;
79+
PlayerHooks.PlayerPostLogin -= PlayerHooksOnPlayerPostLogin;
7980
PlayerHooks.PlayerLogout -= this.PlayerHooksOnPlayerLogout;
8081
_stopWebsocket = true;
8182
WebSocket.Dispose();
82-
if (!WebSocketTask.IsCompleted)
83-
{
84-
TokenSource.Cancel();
85-
TokenSource.Dispose();
86-
}
83+
MapGenerator.Dispose();
8784
}
8885

8986
base.Dispose(disposing);
@@ -93,7 +90,6 @@ private void GeneralHooksOnReloadEvent(ReloadEventArgs e)
9390
{
9491
Config.Settings.Read();
9592
e.Player.SendSuccessMessage("[CaiBot]配置文件已重载 :)");
96-
WebSocket.Dispose();
9793
}
9894

9995
private static async Task? StartHeartBeat()
@@ -105,8 +101,8 @@ private void GeneralHooksOnReloadEvent(ReloadEventArgs e)
105101
{
106102
if (WebSocket.State == WebSocketState.Open)
107103
{
108-
Dictionary<string, string> heartBeat = new () { { "type", "HeartBeat" } };
109-
await CaiBotApi.SendDateAsync(JsonConvert.SerializeObject(heartBeat));
104+
var packetWriter = new PacketWriter();
105+
packetWriter.SetType("HeartBeat").Send();
110106
}
111107
}
112108
catch
@@ -140,7 +136,7 @@ private void GeneralHooksOnReloadEvent(ReloadEventArgs e)
140136
var token = json["token"]!.ToString();
141137
Config.Settings.Token = token;
142138
Config.Settings.Write();
143-
TShock.Log.ConsoleInfo("[CaiAPI]被动绑定成功!");
139+
TShock.Log.ConsoleInfo("[CaiBot]被动绑定成功!");
144140
}
145141

146142

@@ -154,15 +150,15 @@ private void GeneralHooksOnReloadEvent(ReloadEventArgs e)
154150
var receivedData = Encoding.UTF8.GetString(buffer, 0, result.Count);
155151
if (DebugMode)
156152
{
157-
TShock.Log.ConsoleInfo($"[CaiAPI]收到BOT数据包: {receivedData}");
153+
TShock.Log.ConsoleInfo($"[CaiBot]收到BOT数据包: {receivedData}");
158154
}
159155

160156
_ = CaiBotApi.HandleMessageAsync(receivedData);
161157
}
162158
}
163159
catch (Exception ex)
164160
{
165-
TShock.Log.ConsoleInfo("[CaiAPI]CaiBot断开连接...");
161+
TShock.Log.ConsoleInfo("[CaiBot]CaiBot断开连接...");
166162
if (DebugMode)
167163
{
168164
TShock.Log.ConsoleError(ex.ToString());
@@ -177,7 +173,7 @@ private void GeneralHooksOnReloadEvent(ReloadEventArgs e)
177173
}
178174
}
179175

180-
private void OnChat(ServerChatEventArgs args)
176+
private static void OnChat(ServerChatEventArgs args)
181177
{
182178
var plr = TShock.Players[args.Who];
183179

@@ -190,31 +186,28 @@ private void OnChat(ServerChatEventArgs args)
190186
{
191187
return;
192188
}
193-
194-
var result = new RestObject
195-
{
196-
{ "type", "chat" },
197-
{ "chat", string.Format(Config.Settings.ServerChatFormat, plr.Name, args.Text, plr.Group.Name, plr.Group.Prefix, EconomicSupport.IsSupported("GetLevelName") ? EconomicSupport.GetLevelName(plr.Account.Name).Replace("职业:", "") : "不支持") }, //[Server]玩家名:内容" 额外 {2}:玩家组名 {3}:玩家聊天前缀 {4}:Ec职业名
198-
{ "group", Config.Settings.GroupNumber }
199-
};
200-
_ = CaiBotApi.SendDateAsync(JsonConvert.SerializeObject(result));
189+
190+
PacketWriter packetWriter = new ();
191+
192+
packetWriter.SetType("chat") //[Server]玩家名:内容" 额外 {2}:玩家组名 {3}:玩家聊天前缀 {4}:Ec职业名
193+
.Write("chat", string.Format(Config.Settings.ServerChatFormat, plr.Name,args.Text, plr.Group.Name, plr.Group.Prefix,
194+
EconomicSupport.IsSupported("GetLevelName") ? EconomicSupport.GetLevelName(plr.Account.Name).Replace("职业:", "") : "不支持") )
195+
.Send();
201196
}
202197

203-
private void PlayerHooksOnPlayerPostLogin(PlayerPostLoginEventArgs e)
198+
private static void PlayerHooksOnPlayerPostLogin(PlayerPostLoginEventArgs e)
204199
{
205200
var plr = e.Player;
206201
if (!Config.Settings.SyncChatFromServer || string.IsNullOrEmpty(Config.Settings.JoinServerFormat) || plr == null)
207202
{
208203
return;
209204
}
210-
211-
var result = new RestObject
212-
{
213-
{ "type", "chat" },
214-
{ "chat", string.Format(Config.Settings.JoinServerFormat, plr.Name, plr.Group.Name, plr.Group.Prefix, EconomicSupport.IsSupported("GetLevelName") ? EconomicSupport.GetLevelName(plr.Account.Name).Replace("职业:", "") : "不支持") }, //[Server]玩家名:内容" 额外 {2}:玩家组名 {3}:玩家聊天前缀 {4}:Ec职业名
215-
{ "group", Config.Settings.GroupNumber }
216-
};
217-
_ = CaiBotApi.SendDateAsync(JsonConvert.SerializeObject(result));
205+
206+
PacketWriter packetWriter = new ();
207+
208+
packetWriter.SetType("chat") //[Server]玩家名:内容" 额外 {2}:玩家组名 {3}:玩家聊天前缀 {4}:Ec职业名
209+
.Write("chat", string.Format(Config.Settings.JoinServerFormat, plr.Name, plr.Group.Name, plr.Group.Prefix, EconomicSupport.IsSupported("GetLevelName") ? EconomicSupport.GetLevelName(plr.Account.Name).Replace("职业:", "") : "不支持"))
210+
.Send();
218211
}
219212

220213
private void PlayerHooksOnPlayerLogout(PlayerLogoutEventArgs e)
@@ -224,14 +217,10 @@ private void PlayerHooksOnPlayerLogout(PlayerLogoutEventArgs e)
224217
{
225218
return;
226219
}
227-
228-
var result = new RestObject
229-
{
230-
{ "type", "chat" },
231-
{ "chat", string.Format(Config.Settings.ExitServerFormat, plr.Name, plr.Group.Name, plr.Group.Prefix, EconomicSupport.IsSupported("GetLevelName") ? EconomicSupport.GetLevelName(plr.Account.Name).Replace("职业:", "") : "不支持") }, //[Server]玩家名:内容" 额外 {2}:玩家组名 {3}:玩家聊天前缀 {4}:Ec职业名
232-
{ "group", Config.Settings.GroupNumber }
233-
};
234-
_ = CaiBotApi.SendDateAsync(JsonConvert.SerializeObject(result));
220+
PacketWriter packetWriter = new ();
221+
packetWriter.SetType("chat") //[Server]玩家名:内容" 额外 {2}:玩家组名 {3}:玩家聊天前缀 {4}:Ec职业名
222+
.Write("chat", string.Format(Config.Settings.ExitServerFormat, plr.Name, plr.Group.Name, plr.Group.Prefix, EconomicSupport.IsSupported("GetLevelName") ? EconomicSupport.GetLevelName(plr.Account.Name).Replace("职业:", "") : "不支持"))
223+
.Send();
235224
}
236225

237226
private void CaiBotCommand(CommandArgs args)
@@ -299,6 +288,7 @@ void ShowHelpText()
299288
case "调试":
300289
case "debug":
301290
DebugMode = !DebugMode;
291+
PacketWriter.Debug = DebugMode;
302292
plr.SendInfoMessage($"[CaiBot]调试模式已{(DebugMode ? "开启" : "关闭")}!");
303293
break;
304294
case "验证码":
@@ -343,15 +333,13 @@ private void OnBanInsert(object? sender, BanEventArgs e)
343333

344334
var name = e.Ban.Identifier.Replace(Identifier.Name.Prefix, "").Replace(Identifier.Account.Prefix, "");
345335
var expireTime = e.Ban.GetPrettyExpirationString();
346-
var result = new RestObject
347-
{
348-
{ "type", "post_ban_add" },
349-
{ "name", name },
350-
{ "reason", e.Ban.Reason },
351-
{ "admin", e.Ban.BanningUser },
352-
{ "expire_time", expireTime == "Never" ? "永久封禁" : expireTime }
353-
};
354-
_ = CaiBotApi.SendDateAsync(JsonConvert.SerializeObject(result));
336+
PacketWriter packetWriter = new ();
337+
packetWriter.SetType("post_ban_add")
338+
.Write("name", name)
339+
.Write("reason", e.Ban.Reason)
340+
.Write("admin", e.Ban.BanningUser)
341+
.Write("expire_time", expireTime == "Never" ? "永久封禁" : expireTime)
342+
.Send();
355343
}
356344

357345

‎src/CaiBot/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
## 更新日志
5555

5656
```
57+
v2025.02.06.1 添加数据包处理错误, 修复地图尺寸导致点亮地图失败
5758
v2025.01.29.1 修复`上传地图`文件名错误导致上传失败
5859
v2025.01.27.1 添加更多进度查询信息
5960
v2025.01.24.1 升级绘图库,修复`上传小地图`,允许用户自定义Api地址,

‎src/CaiBot/SixLabors.ImageSharp.dll

-2 MB
Binary file not shown.

‎src/CaiBot/Utils.cs

+10-15
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using Terraria;
55
using Terraria.GameContent.Events;
66
using Terraria.ID;
7-
using Terraria.IO;
87

98
namespace CaiBot;
109

@@ -251,14 +250,12 @@ internal static List<int> GetActiveBuffs(IDbConnection connection, int userId, s
251250

252251
connection.Open();
253252

254-
using (var reader = command.ExecuteReader())
253+
using var reader = command.ExecuteReader();
254+
if (reader.Read())
255255
{
256-
if (reader.Read())
257-
{
258-
var activeBuffsString = reader.GetString(0);
259-
var activeBuffsList = activeBuffsString.Split(',').Select(int.Parse).ToList();
260-
return activeBuffsList;
261-
}
256+
var activeBuffsString = reader.GetString(0);
257+
var activeBuffsList = activeBuffsString.Split(',').Select(int.Parse).ToList();
258+
return activeBuffsList;
262259
}
263260
}
264261
catch
@@ -275,14 +272,12 @@ internal static List<int> GetActiveBuffs(IDbConnection connection, int userId, s
275272

276273
connection.Open();
277274

278-
using (var reader = command.ExecuteReader())
275+
using var reader = command.ExecuteReader();
276+
if (reader.Read())
279277
{
280-
if (reader.Read())
281-
{
282-
var activeBuffsString = reader.GetString(0);
283-
var activeBuffsList = activeBuffsString.Split(',').Select(int.Parse).ToList();
284-
return activeBuffsList;
285-
}
278+
var activeBuffsString = reader.GetString(0);
279+
var activeBuffsList = activeBuffsString.Split(',').Select(int.Parse).ToList();
280+
return activeBuffsList;
286281
}
287282
}
288283
catch

‎src/CaiBotLite/CaiBotApi.cs

+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
using Microsoft.Xna.Framework;
2+
using Newtonsoft.Json.Linq;
3+
using SixLabors.ImageSharp.Formats.Png;
4+
using System.Net.WebSockets;
5+
using System.Runtime.InteropServices;
6+
using System.Text;
7+
using Terraria;
8+
using TerrariaApi.Server;
9+
using TShockAPI;
10+
using CaiBot;
11+
using Utils = CaiBot.Utils;
12+
13+
namespace CaiBotLite;
14+
15+
internal static class CaiBotApi
16+
{
17+
internal static bool IsWebsocketConnected =>
18+
Plugin.WebSocket.State == WebSocketState.Open;
19+
20+
internal static readonly Dictionary<string,(DateTime,int)> WhiteListCaches = new();
21+
22+
internal static async Task HandleMessageAsync(string receivedData)
23+
{
24+
try
25+
{
26+
var jsonObject = JObject.Parse(receivedData);
27+
var type = (string) jsonObject["type"]!;
28+
29+
var group = "";
30+
var msgId = "";
31+
if (jsonObject.ContainsKey("group"))
32+
{
33+
group = jsonObject["group"]!.ToObject<string>()!;
34+
}
35+
36+
if (jsonObject.ContainsKey("msg_id"))
37+
{
38+
msgId = jsonObject["msg_id"]!.ToObject<string>()!;
39+
}
40+
41+
var packetWriter = new PacketWriter(group, msgId);
42+
switch (type)
43+
{
44+
case "delserver":
45+
TShock.Log.ConsoleInfo("[CaiBotLite]BOT发送解绑命令...");
46+
Config.Settings.Token = string.Empty;
47+
Config.Settings.Write();
48+
Plugin.GenBindCode(EventArgs.Empty);
49+
Plugin.WebSocket.Dispose();
50+
break;
51+
case "hello":
52+
TShock.Log.ConsoleInfo("[CaiBotLite]CaiBOT连接成功...");
53+
//发送服务器信息
54+
packetWriter.SetType("hello")
55+
.Write("tshock_version", TShock.VersionNum.ToString())
56+
.Write("plugin_version", Plugin.VersionNum)
57+
.Write("terraria_version", Main.versionNumber)
58+
.Write("cai_whitelist", Config.Settings.WhiteList)
59+
.Write("os", RuntimeInformation.RuntimeIdentifier)
60+
.Write("world", TShock.Config.Settings.UseServerName ? TShock.Config.Settings.ServerName : Main.worldName)
61+
.Send();
62+
break;
63+
case "cmd":
64+
var cmd = (string) jsonObject["cmd"]!;
65+
CaiBotPlayer tr = new ();
66+
Commands.HandleCommand(tr, cmd);
67+
TShock.Utils.SendLogs($"[CaiBot] `{(string) jsonObject["at"]!}`来自群`{(string) jsonObject["group"]!}`执行了: {(string) jsonObject["cmd"]!}", Color.PaleVioletRed);
68+
69+
packetWriter.SetType("cmd")
70+
.Write("result", string.Join('\n', tr.GetCommandOutput()))
71+
.Send();
72+
break;
73+
case "online":
74+
var onlineResult = new StringBuilder();
75+
if (TShock.Utils.GetActivePlayerCount() == 0)
76+
{
77+
onlineResult.Append("没有玩家在线捏...");
78+
}
79+
else
80+
{
81+
onlineResult.AppendLine($"在线玩家({TShock.Utils.GetActivePlayerCount()}/{TShock.Config.Settings.MaxSlots})");
82+
onlineResult.Append(string.Join(',', TShock.Players.Where(x => x is { Active: true }).Select(x => x.Name)));
83+
}
84+
85+
var onlineProcessList = Utils.GetOnlineProcessList();
86+
var onlineProcess = !onlineProcessList.Any() ? "已毕业" : onlineProcessList.ElementAt(0) + "前";
87+
88+
packetWriter.SetType("online")
89+
.Write("result", onlineResult.ToString()) // “怎么有种我是男的的感觉” -- 张芷睿大人 (24.12.22)
90+
.Write("worldname", string.IsNullOrEmpty(Main.worldName) ? "地图还没加载捏~" : Main.worldName)
91+
.Write("process", onlineProcess)
92+
.Send();
93+
break;
94+
case "process":
95+
packetWriter.SetType("process")
96+
.Write("process", Utils.GetProcessList())
97+
.Write("kill_counts", Utils.GetKillCountList())
98+
.Write("worldname", Main.worldName)
99+
.Write("drunk_world", Main.drunkWorld)
100+
.Write("zenith_world", Main.zenithWorld)
101+
.Write("world_icon", Utils.GetWorldIconName())
102+
.Send();
103+
break;
104+
case "whitelist":
105+
var name = (string) jsonObject["name"]!;
106+
var code = (int) jsonObject["code"]!;
107+
108+
WhiteListCaches[name] = (DateTime.Now, code);
109+
110+
if (Login.CheckWhite(name, code))
111+
{
112+
var plr = TShock.Players.FirstOrDefault(x => x.Name == name);
113+
if (plr != null)
114+
{
115+
Login.HandleLogin(plr);
116+
}
117+
}
118+
119+
break;
120+
case "selfkick":
121+
name = (string) jsonObject["name"]!;
122+
var playerList2 = TSPlayer.FindByNameOrID("tsn:" + name);
123+
if (playerList2.Count == 0)
124+
{
125+
return;
126+
}
127+
128+
playerList2[0].Kick("在群中使用自踢命令.", true, saveSSI: true);
129+
break;
130+
case "lookbag":
131+
name = (string) jsonObject["name"]!;
132+
var playerList3 = TSPlayer.FindByNameOrID("tsn:" + name);
133+
if (playerList3.Count != 0)
134+
{
135+
var plr = playerList3[0].TPlayer;
136+
var lookOnlineResult = LookBag.LookOnline(plr);
137+
packetWriter.SetType("lookbag")
138+
.Write("name", lookOnlineResult.Name)
139+
.Write("exist", 1)
140+
.Write("life", $"{lookOnlineResult.Health}/{lookOnlineResult.MaxHealth}")
141+
.Write("mana", $"{lookOnlineResult.Mana}/{lookOnlineResult.MaxMana}")
142+
.Write("quests_completed", lookOnlineResult.QuestsCompleted)
143+
.Write("inventory", lookOnlineResult.ItemList)
144+
.Write("buffs", lookOnlineResult.Buffs)
145+
.Write("enhances", lookOnlineResult.Enhances)
146+
.Write("economic", EconomicData.GetEconomicData(lookOnlineResult.Name))
147+
.Send();
148+
}
149+
else
150+
{
151+
var acc = TShock.UserAccounts.GetUserAccountByName(name);
152+
if (acc == null)
153+
{
154+
packetWriter.SetType("lookbag")
155+
.Write("exist", 0)
156+
.Send();
157+
return;
158+
}
159+
160+
var data = TShock.CharacterDB.GetPlayerData(new TSPlayer(-1), acc.ID);
161+
if (data == null)
162+
{
163+
packetWriter.SetType("lookbag")
164+
.Write("exist", 0)
165+
.Send();
166+
return;
167+
}
168+
169+
var lookOnlineResult = LookBag.LookOffline(acc, data);
170+
packetWriter.SetType("lookbag")
171+
.Write("name", lookOnlineResult.Name )
172+
.Write("exist", 1 )
173+
.Write("life", $"{lookOnlineResult.Health}/{lookOnlineResult.MaxHealth}")
174+
.Write("mana", $"{lookOnlineResult.Mana}/{lookOnlineResult.MaxMana}")
175+
.Write("quests_completed", lookOnlineResult.QuestsCompleted)
176+
.Write("inventory", lookOnlineResult.ItemList)
177+
.Write("buffs", lookOnlineResult.Buffs)
178+
.Write("enhances", lookOnlineResult.Enhances )
179+
.Write("economic", EconomicData.GetEconomicData(lookOnlineResult.Name))
180+
.Send();
181+
}
182+
break;
183+
case "mappng":
184+
var bitmap = MapGenerator.CreateMapImg();
185+
using (MemoryStream ms = new ())
186+
{
187+
await bitmap.SaveAsync(ms, new PngEncoder());
188+
var imageBytes = ms.ToArray();
189+
var base64 = Convert.ToBase64String(imageBytes);
190+
packetWriter.SetType("mappngV2")
191+
.Write("result", Utils.CompressBase64(base64))
192+
.Send();
193+
}
194+
break;
195+
case "mapfile":
196+
var mapfile = MapGenerator.CreateMapFile();
197+
packetWriter.SetType("mapfileV2")
198+
.Write("name", mapfile.Item2)
199+
.Write("base64", Utils.CompressBase64(mapfile.Item1) )
200+
.Send();
201+
202+
break;
203+
case "worldfile":
204+
packetWriter.SetType("worldfileV2")
205+
.Write("name", Path.GetFileName(Main.worldPathName))
206+
.Write("base64", Utils.CompressBase64(Utils.FileToBase64String(Main.worldPathName)) )
207+
.Send();
208+
209+
break;
210+
case "pluginlist":
211+
var pluginList = ServerApi.Plugins.Select(p => new PluginInfo(p.Plugin.Name, p.Plugin.Description, p.Plugin.Author, p.Plugin.Version)).ToList();
212+
packetWriter.SetType("pluginlist")
213+
.Write("plugins", pluginList)
214+
.Send();
215+
break;
216+
}
217+
}
218+
catch (Exception ex)
219+
{
220+
TShock.Log.ConsoleError("[CaiBotLite] 处理BOT数据包时出错:\n"+ex+$"\n源数据包: {receivedData}");
221+
}
222+
}
223+
224+
225+
}

‎src/CaiBotLite/CaiBotLite.csproj

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<Import Project="..\..\template.targets"/>
4+
5+
<ItemGroup>
6+
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
7+
</ItemGroup>
8+
9+
<ItemGroup>
10+
<EmbeddedResource Include="$(NuGetPackageRoot)sixlabors.imagesharp\3.1.6\lib\$(TargetFramework)\SixLabors.ImageSharp.dll">
11+
<Link>SixLabors.ImageSharp.dll</Link>
12+
</EmbeddedResource>
13+
</ItemGroup>
14+
15+
<ItemGroup>
16+
<ProjectReference Include="..\Economics.RPG\Economics.RPG.csproj"/>
17+
<ProjectReference Include="..\Economics.Skill\Economics.Skill.csproj"/>
18+
<ProjectReference Include="..\EconomicsAPI\EconomicsAPI.csproj"/>
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<Compile Include="..\CaiBot\CaiBotPlayer.cs">
23+
<Link>CaiBotPlayer.cs</Link>
24+
</Compile>
25+
<Compile Include="..\CaiBot\EconomicData.cs">
26+
<Link>EconomicData.cs</Link>
27+
</Compile>
28+
<Compile Include="..\CaiBot\EconomicSupport.cs">
29+
<Link>EconomicSupport.cs</Link>
30+
</Compile>
31+
<Compile Include="..\CaiBot\LookBag.cs">
32+
<Link>LookBag.cs</Link>
33+
</Compile>
34+
<Compile Include="..\CaiBot\MapGenerator.cs">
35+
<Link>MapGenerator.cs</Link>
36+
</Compile>
37+
<Compile Include="..\CaiBot\PacketWriter.cs">
38+
<Link>PacketWriter.cs</Link>
39+
</Compile>
40+
<Compile Include="..\CaiBot\PluginInfo.cs">
41+
<Link>PluginInfo.cs</Link>
42+
</Compile>
43+
<Compile Include="..\CaiBot\Utils.cs">
44+
<Link>Utils.cs</Link>
45+
</Compile>
46+
</ItemGroup>
47+
48+
</Project>

‎src/CaiBotLite/Config.cs

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using Newtonsoft.Json;
2+
3+
namespace CaiBotLite;
4+
5+
public class Config
6+
{
7+
private const string ConfigPath = "tshock/CaiBotLite.json";
8+
public static Config Settings = new ();
9+
10+
[JsonProperty("白名单开关", Order = 1)]
11+
public bool WhiteList = true;
12+
13+
[JsonProperty("密钥", Order = 2)]
14+
public string Token = "";
15+
16+
[JsonProperty("白名单拦截提示的群号", Order = 3)]
17+
public long GroupNumber;
18+
19+
20+
21+
22+
/// <summary>
23+
/// 将配置文件写入硬盘
24+
/// </summary>
25+
internal void Write()
26+
{
27+
using FileStream fileStream = new (ConfigPath, FileMode.Create, FileAccess.Write, FileShare.Write);
28+
using StreamWriter streamWriter = new (fileStream);
29+
streamWriter.Write(JsonConvert.SerializeObject(this, JsonSettings));
30+
}
31+
32+
/// <summary>
33+
/// 从硬盘读取配置文件
34+
/// </summary>
35+
internal void Read()
36+
{
37+
Config result;
38+
if (!File.Exists(ConfigPath))
39+
{
40+
result = new Config();
41+
result.Write();
42+
}
43+
else
44+
{
45+
using FileStream fileStream = new (ConfigPath, FileMode.Open, FileAccess.Read, FileShare.Read);
46+
using StreamReader streamReader = new (fileStream);
47+
result = JsonConvert.DeserializeObject<Config>(streamReader.ReadToEnd(), JsonSettings)!;
48+
}
49+
50+
Settings = result;
51+
}
52+
53+
private static readonly JsonSerializerSettings JsonSettings = new () { Formatting = Formatting.Indented, ObjectCreationHandling = ObjectCreationHandling.Replace };
54+
}

‎src/CaiBotLite/Login.cs

+336
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
using NuGet.Protocol;
2+
using On.OTAPI;
3+
using Rests;
4+
using Terraria;
5+
using TerrariaApi.Server;
6+
using TShockAPI;
7+
using TShockAPI.DB;
8+
using TShockAPI.Hooks;
9+
using PacketWriter = CaiBot.PacketWriter;
10+
11+
namespace CaiBotLite;
12+
13+
internal static class Login
14+
{
15+
internal static bool MessageBuffer_InvokeGetData(Hooks.MessageBuffer.orig_InvokeGetData orig,
16+
MessageBuffer instance, ref byte packetId, ref int readOffset, ref int start, ref int length,
17+
ref int messageType, int maxPackets)
18+
{
19+
if (!Config.Settings.WhiteList)
20+
{
21+
return orig(instance, ref packetId, ref readOffset, ref start, ref length, ref messageType, maxPackets);
22+
}
23+
24+
try
25+
{
26+
if (packetId == (byte) PacketTypes.ClientUUID)
27+
{
28+
var player = TShock.Players[instance.whoAmI];
29+
30+
instance.ResetReader();
31+
instance.reader.BaseStream.Position = readOffset;
32+
33+
var uuid = instance.reader.ReadString();
34+
35+
if (string.IsNullOrEmpty(player.Name))
36+
{
37+
player.Kick("[Cai白名单]玩家名获取失败!");
38+
return false;
39+
}
40+
41+
if (CaiBotApi.WhiteListCaches.TryGetValue(player.Name, out var whiteListCache))
42+
{
43+
if (DateTime.Now - whiteListCache.Item1 <= TimeSpan.FromSeconds(10))
44+
{
45+
if (!CheckWhite(player.Name,whiteListCache.Item2))
46+
{
47+
return false;
48+
}
49+
HandleLogin(player);
50+
51+
return orig(instance, ref packetId, ref readOffset, ref start, ref length, ref messageType, maxPackets);
52+
}
53+
}
54+
55+
56+
57+
if (!CaiBotApi.IsWebsocketConnected)
58+
{
59+
if (CaiBotApi.WhiteListCaches.TryGetValue(player.Name, out var whiteListCache2)) //从缓存处读取白名单
60+
{
61+
TShock.Log.ConsoleWarn("[CaiBot]正在使用白名单缓存验证玩家...");
62+
if (!CheckWhite(player.Name, whiteListCache2.Item2))
63+
{
64+
return false;
65+
}
66+
HandleLogin(player);
67+
}
68+
else
69+
{
70+
TShock.Log.ConsoleError("[CaiBot]机器人处于未连接状态, 玩家无法加入。\n" +
71+
"如果你不想使用Cai白名单,可以在tshock/CaiBot.json中将其关闭。");
72+
player.Disconnect("[CaiBot]机器人处于未连接状态, 玩家无法加入。");
73+
return false;
74+
}
75+
76+
77+
}
78+
else
79+
{
80+
var packetWriter = new PacketWriter();
81+
packetWriter.SetType("whitelistV2")
82+
.Write("name", player.Name)
83+
.Write("uuid", uuid )
84+
.Write("ip", player.IP)
85+
.Send();
86+
}
87+
88+
}
89+
}
90+
catch (Exception ex)
91+
{
92+
TShock.Log.ConsoleError(ex.ToString());
93+
}
94+
95+
return orig(instance, ref packetId, ref readOffset, ref start, ref length, ref messageType, maxPackets);
96+
}
97+
98+
internal static void OnGetData(GetDataEventArgs args)
99+
{
100+
if (!Config.Settings.WhiteList)
101+
{
102+
return;
103+
}
104+
105+
var type = args.MsgID;
106+
107+
var player = TShock.Players[args.Msg.whoAmI];
108+
if (player == null || !player.ConnectionAlive)
109+
{
110+
args.Handled = true;
111+
return;
112+
}
113+
114+
if ((player.State < 10 || player.Dead) && (int) type > 12 && (int) type != 16 && (int) type != 42 &&
115+
(int) type != 50 &&
116+
(int) type != 38 && (int) type != 21 && (int) type != 22)
117+
{
118+
args.Handled = true;
119+
return;
120+
}
121+
122+
try
123+
{
124+
if (type == PacketTypes.ContinueConnecting2)
125+
{
126+
player.DataWhenJoined = new PlayerData(player);
127+
player.DataWhenJoined.CopyCharacter(player);
128+
args.Handled = true;
129+
}
130+
}
131+
catch (Exception e)
132+
{
133+
TShock.Log.ConsoleError(e.ToString());
134+
}
135+
}
136+
137+
internal static bool CheckWhite(string name, int code)
138+
{
139+
var playerList = TSPlayer.FindByNameOrID("tsn:" + name);
140+
141+
var groupID = Config.Settings.GroupNumber.ToString();
142+
if (Config.Settings.GroupNumber == 0)
143+
{
144+
groupID = "未设置";
145+
}
146+
if (playerList.Count == 0)
147+
{
148+
return false;
149+
}
150+
151+
var plr = playerList[0];
152+
if (string.IsNullOrEmpty(name))
153+
{
154+
TShock.Log.ConsoleInfo($"[Cai白名单]玩家[{name}](IP: {plr.IP})版本可能过低...");
155+
plr.Disconnect("你的游戏版本可能过低,\n" +
156+
"请使用Terraria1.4.4+游玩");
157+
return false;
158+
}
159+
160+
try
161+
{
162+
switch (code)
163+
{
164+
case 200:
165+
{
166+
TShock.Log.ConsoleInfo($"[Cai白名单]玩家[{name}](IP: {plr.IP})已通过白名单验证...");
167+
break;
168+
}
169+
case 404:
170+
{
171+
TShock.Log.ConsoleInfo($"[Cai白名单]玩家[{name}](IP: {plr.IP})没有添加白名单...");
172+
plr.SilentKickInProgress = true;
173+
plr.Disconnect($"[Cai白名单]没有添加白名单!\n" +
174+
$"请在群[{groupID}]内发送'/添加白名单 角色名字'");
175+
return false;
176+
}
177+
case 403:
178+
{
179+
TShock.Log.ConsoleInfo($"[Cai白名单]玩家[{name}](IP: {plr.IP})被屏蔽,处于CaiBot云黑名单中...");
180+
plr.SilentKickInProgress = true;
181+
plr.Disconnect("[Cai白名单]你已被服务器屏蔽,\n" +
182+
"你处于本群黑名单中!");
183+
return false;
184+
}
185+
// case 401:
186+
// {
187+
// TShock.Log.ConsoleInfo($"[Cai白名单]玩家[{name}](IP: {plr.IP})不在本群内...");
188+
// plr.SilentKickInProgress = true;
189+
// plr.Disconnect($"[Cai白名单]你不在服务器群内!\n" +
190+
// $"请加入服务器群: {number}");
191+
// return false;
192+
// }
193+
case 405:
194+
{
195+
TShock.Log.ConsoleInfo($"[Cai白名单]玩家[{name}](IP: {plr.IP})使用未授权的设备...");
196+
plr.SilentKickInProgress = true;
197+
plr.Disconnect($"[Cai白名单]在群[{groupID}]内发送'/登录',\n" +
198+
$"以批准此设备登录");
199+
200+
return false;
201+
}
202+
}
203+
}
204+
catch (Exception ex)
205+
{
206+
TShock.Log.ConsoleInfo($"[Cai白名单]玩家[{name}](IP: {plr.IP})验证白名单时出现错误...\n" +
207+
$"{ex}");
208+
plr.SilentKickInProgress = true;
209+
plr.Disconnect($"[Cai白名单]服务器发生错误无法处理该请求!请尝试重新加入游戏或者联系服务器群[{groupID}]管理员");
210+
return false;
211+
}
212+
213+
return true;
214+
}
215+
216+
internal static bool HandleLogin(TSPlayer player)
217+
{
218+
var password = Guid.NewGuid().ToString();
219+
var account = TShock.UserAccounts.GetUserAccountByName(player.Name);
220+
if (account != null)
221+
{
222+
player.RequiresPassword = false;
223+
player.PlayerData = TShock.CharacterDB.GetPlayerData(player, account.ID);
224+
225+
if (player.State == 1)
226+
{
227+
player.State = 2;
228+
}
229+
230+
NetMessage.SendData((int) PacketTypes.WorldInfo, player.Index);
231+
232+
var group = TShock.Groups.GetGroupByName(account.Group);
233+
234+
player.Group = group;
235+
player.tempGroup = null;
236+
player.Account = account;
237+
player.IsLoggedIn = true;
238+
player.IsDisabledForSSC = false;
239+
240+
if (Main.ServerSideCharacter)
241+
{
242+
if (player.HasPermission(Permissions.bypassssc))
243+
{
244+
player.PlayerData.CopyCharacter(player);
245+
TShock.CharacterDB.InsertPlayerData(player);
246+
}
247+
248+
player.PlayerData.RestoreCharacter(player);
249+
}
250+
251+
player.LoginFailsBySsi = false;
252+
253+
if (player.HasPermission(Permissions.ignorestackhackdetection))
254+
{
255+
player.IsDisabledForStackDetection = false;
256+
}
257+
258+
if (player.HasPermission(Permissions.usebanneditem))
259+
{
260+
player.IsDisabledForBannedWearable = false;
261+
}
262+
263+
player.SendSuccessMessage($"[CaiBot]已经验证{account.Name}登录完毕。");
264+
TShock.Log.ConsoleInfo(player.Name + "成功验证登录。");
265+
TShock.UserAccounts.SetUserAccountUUID(account, player.UUID);
266+
PlayerHooks.OnPlayerPostLogin(player);
267+
return true;
268+
}
269+
270+
if (player.Name != TSServerPlayer.AccountName)
271+
{
272+
account = new UserAccount { Name = player.Name, Group = TShock.Config.Settings.DefaultRegistrationGroupName, UUID = player.UUID };
273+
try
274+
{
275+
account.CreateBCryptHash(password);
276+
}
277+
catch (ArgumentOutOfRangeException)
278+
{
279+
return true;
280+
}
281+
282+
player.SendSuccessMessage("[CaiBot]账户{0}注册成功。", account.Name);
283+
TShock.UserAccounts.AddUserAccount(account);
284+
TShock.Log.ConsoleInfo("玩家{0}注册了新账户:{1}", player.Name, account.Name);
285+
player.PlayerData = TShock.CharacterDB.GetPlayerData(player, account.ID);
286+
287+
if (player.State == 1)
288+
{
289+
player.State = 2;
290+
}
291+
292+
NetMessage.SendData((int) PacketTypes.WorldInfo, player.Index);
293+
294+
var group = TShock.Groups.GetGroupByName(account.Group);
295+
296+
player.Group = group;
297+
player.tempGroup = null;
298+
player.Account = account;
299+
player.IsLoggedIn = true;
300+
player.IsDisabledForSSC = false;
301+
302+
if (Main.ServerSideCharacter)
303+
{
304+
if (player.HasPermission(Permissions.bypassssc))
305+
{
306+
player.PlayerData.CopyCharacter(player);
307+
TShock.CharacterDB.InsertPlayerData(player);
308+
}
309+
310+
player.PlayerData.RestoreCharacter(player);
311+
}
312+
313+
player.LoginFailsBySsi = false;
314+
315+
if (player.HasPermission(Permissions.ignorestackhackdetection))
316+
{
317+
player.IsDisabledForStackDetection = false;
318+
}
319+
320+
if (player.HasPermission(Permissions.usebanneditem))
321+
{
322+
player.IsDisabledForBannedWearable = false;
323+
}
324+
325+
player.SendSuccessMessage($"[CaiBot]已经验证{account.Name}登录完毕.");
326+
TShock.Log.ConsoleInfo(player.Name + "成功验证登录.");
327+
TShock.UserAccounts.SetUserAccountUUID(account, player.UUID);
328+
PlayerHooks.OnPlayerPostLogin(player);
329+
return true;
330+
}
331+
332+
player.SilentKickInProgress = true;
333+
player.Disconnect("[CaiBot]此名字不可用!");
334+
return true;
335+
}
336+
}

‎src/CaiBotLite/Plugin.cs

+291
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
using Newtonsoft.Json.Linq;
2+
using On.OTAPI;
3+
using CaiBot;
4+
using System.Net;
5+
using System.Net.WebSockets;
6+
using System.Reflection;
7+
using System.Text;
8+
using Terraria;
9+
using TerrariaApi.Server;
10+
using TShockAPI;
11+
using TShockAPI.Hooks;
12+
using PacketWriter = CaiBot.PacketWriter;
13+
using Program = Terraria.Program;
14+
15+
namespace CaiBotLite;
16+
17+
[ApiVersion(21)]
18+
公共 class Plugin : TerrariaPlugin
19+
{
20+
公共 static readonly Version VersionNum = new (2025331); //日期+版本号(0,1,2...)
21+
internal static int InitCode = -1;
22+
公共 static bool DebugMode;
23+
private static bool _stopWebsocket;
24+
internal static ClientWebSocket WebSocket
25+
{
26+
get => PacketWriter.WebSocket;
27+
set => PacketWriter.WebSocket = value;
28+
}
29+
30+
公共 Plugin(Main game) : base(game)
31+
{
32+
}
33+
34+
公共 override string Author => "Cai,羽学,西江";
35+
公共 override string Description => "CaiBot官方机器人的适配插件";
36+
公共 override string Name => "CaiBotLitePlugin";
37+
公共 override Version Version => VersionNum;
38+
39+
40+
公共 override void Initialize()
41+
{
42+
DebugMode = Program.LaunchParameters.ContainsKey("-caidebug");
43+
AppDomain.CurrentDomain.AssemblyResolve += this.CurrentDomain_AssemblyResolve;
44+
Commands.ChatCommands.Add(new Command("caibotlite.admin"this.CaiBotCommand, "caibotlite"));
45+
Config.Settings.Read();
46+
Config.Settings.Write();
47+
ServerApi.Hooks.NetGetData.Register(this, Login.OnGetData, int.MaxValue);
48+
ServerApi.Hooks.GamePostInitialize.Register(this, GenBindCode);
49+
Hooks.MessageBuffer.InvokeGetData += Login.MessageBuffer_InvokeGetData;
50+
GeneralHooks.ReloadEvent += GeneralHooksOnReloadEvent;
51+
MapGenerator.Init();
52+
EconomicSupport.Init();
53+
PacketWriter.Init(true, DebugMode);
54+
Task.Factory.StartNew(StartCaiApi, TaskCreationOptions.LongRunning);
55+
Task.Factory.StartNew(StartHeartBeat, TaskCreationOptions.LongRunning);
56+
}
57+
58+
protected override void Dispose(bool disposing)
59+
{
60+
if (disposing)
61+
{
62+
var asm = Assembly.GetExecutingAssembly();
63+
Commands.ChatCommands.RemoveAll(c => c.CommandDelegate.Method.DeclaringType?.Assembly == asm);
64+
ServerApi.Hooks.NetGetData.Deregister(this, Login.OnGetData);
65+
ServerApi.Hooks.GamePostInitialize.Deregister(this, GenBindCode);
66+
AppDomain.CurrentDomain.AssemblyResolve -= this.CurrentDomain_AssemblyResolve;
67+
Hooks.MessageBuffer.InvokeGetData -= Login.MessageBuffer_InvokeGetData;
68+
MapGenerator.Dispose();
69+
_stopWebsocket = true;
70+
WebSocket.Dispose();
71+
}
72+
73+
base.Dispose(disposing);
74+
}
75+
76+
private static void GeneralHooksOnReloadEvent(ReloadEventArgs e)
77+
{
78+
Config.Settings.Read();
79+
e.Player.SendSuccessMessage("[CaiBotLite]配置文件已重载 :)");
80+
}
81+
82+
private static async Task? StartHeartBeat()
83+
{
84+
while (!_stopWebsocket)
85+
{
86+
await Task.Delay(TimeSpan.FromSeconds(60));
87+
try
88+
{
89+
if (WebSocket.State == WebSocketState.Open)
90+
{
91+
var packetWriter = new PacketWriter();
92+
packetWriter.SetType("HeartBeat")
93+
.Send();
94+
}
95+
}
96+
catch
97+
{
98+
TShock.Log.ConsoleInfo("[CaiBotLite]心跳包发送失败!");
99+
}
100+
}
101+
}
102+
103+
private static async Task? StartCaiApi()
104+
{
105+
while (!_stopWebsocket)
106+
{
107+
try
108+
{
109+
WebSocket = new ClientWebSocket();
110+
while (string.IsNullOrEmpty(Config.Settings.Token))
111+
{
112+
await Task.Delay(TimeSpan.FromSeconds(10));
113+
HttpClient client = new ();
114+
client.Timeout = TimeSpan.FromSeconds(5.0);
115+
var response = client.GetAsync($"https://api.terraria.ink:22338/bot/get_token?code={InitCode}")
116+
.Result;
117+
if (response.StatusCode != HttpStatusCode.OK || Config.Settings.Token != "")
118+
{
119+
continue;
120+
}
121+
122+
var responseBody = await response.Content.ReadAsStringAsync();
123+
var json = JObject.Parse(responseBody);
124+
var token = json["token"]!.ToString();
125+
Config.Settings.Token = token;
126+
Config.Settings.Write();
127+
TShock.Log.ConsoleInfo("[CaiBotLite]被动绑定成功!");
128+
}
129+
130+
131+
await WebSocket.ConnectAsync(new Uri($"wss://api.terraria.ink:22338/bot/" + Config.Settings.Token), CancellationToken.None);
132+
133+
134+
while (true)
135+
{
136+
var buffer = new byte[1024];
137+
var result = await WebSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
138+
var receivedData = Encoding.UTF8.GetString(buffer, 0, result.Count);
139+
if (DebugMode)
140+
{
141+
TShock.Log.ConsoleInfo($"[CaiBotLite]收到BOT数据包: {receivedData}");
142+
}
143+
144+
_ = CaiBotApi.HandleMessageAsync(receivedData);
145+
}
146+
}
147+
catch (Exception ex)
148+
{
149+
TShock.Log.ConsoleInfo("[CaiBotLite]CaiBot断开连接...");
150+
if (DebugMode)
151+
{
152+
TShock.Log.ConsoleError(ex.ToString());
153+
}
154+
else
155+
{
156+
TShock.Log.ConsoleError("链接失败原因: " + ex.Message);
157+
}
158+
}
159+
160+
await Task.Delay(5000);
161+
}
162+
}
163+
164+
165+
166+
private void CaiBotCommand(CommandArgs args)
167+
{
168+
var plr = args.Player;
169+
170+
void ShowHelpText()
171+
{
172+
if (!PaginationTools.TryParsePageNumber(args.Parameters, 1, plr, out var pageNumber))
173+
{
174+
return;
175+
}
176+
177+
List<string> lines = new ()
178+
{
179+
"/caibot debug CaiBot调试开关",
180+
"/caibot code 生成并且展示验证码",
181+
"/caibot info 显示CaiBot的一些信息",
182+
"/caibot unbind 主动解除绑定",
183+
"/caibot test Cai保留用于测试的命令,乱用可能会爆掉"
184+
};
185+
186+
PaginationTools.SendPage(
187+
plr, pageNumber, lines,
188+
new PaginationTools.Settings { HeaderFormat = GetString("帮助 ({0}/{1}):"), FooterFormat = GetString("输入 {0}caibot help {{0}} 查看更多").SFormat(Commands.Specifier) }
189+
);
190+
}
191+
192+
if (args.Parameters.Count == 0)
193+
{
194+
ShowHelpText();
195+
return;
196+
}
197+
198+
199+
switch (args.Parameters[0].ToLowerInvariant())
200+
{
201+
case "test":
202+
Console.WriteLine("你怎么知道Cai喜欢留一个测试命令?");
203+
break;
204+
// 帮助
205+
case "help":
206+
ShowHelpText();
207+
return;
208+
209+
default:
210+
ShowHelpText();
211+
break;
212+
213+
case "信息":
214+
case "info":
215+
plr.SendInfoMessage($"[CaiBot信息]\n" +
216+
$"插件版本: v{VersionNum}\n" +
217+
$"WebSocket状态: {WebSocket.State}\n" +
218+
$"绑定QQ群: {(Config.Settings.GroupNumber == 0L ? "未绑定|未连接" : Config.Settings.GroupNumber)}\n" +
219+
$"绑定状态: {Config.Settings.Token != ""}\n" +
220+
$"Debug模式: {DebugMode}\n" +
221+
$"Economic API支持: {EconomicSupport.GetCoinsSupport}\n" +
222+
$"Economic RPG支持: {EconomicSupport.GetLevelNameSupport}\n" +
223+
$"Economic Skill支持: {EconomicSupport.GetSkillSupport}\n"
224+
);
225+
break;
226+
case "调试":
227+
case "debug":
228+
DebugMode = !DebugMode;
229+
plr.SendInfoMessage($"[CaiBotLite]调试模式已{(DebugMode ? "开启" : "关闭")}!");
230+
break;
231+
case "验证码":
232+
case "code":
233+
if (!string.IsNullOrEmpty(Config.Settings.Token))
234+
{
235+
plr.SendInfoMessage("[CaiBotLite]服务器已绑定无法生成验证码!");
236+
return;
237+
}
238+
239+
GenBindCode(EventArgs.Empty);
240+
plr.SendInfoMessage("[CaiBotLite]验证码已生成,请在后台查看喵~");
241+
break;
242+
243+
case "解绑":
244+
case "unbind":
245+
if (string.IsNullOrEmpty(Config.Settings.Token))
246+
{
247+
plr.SendInfoMessage("[CaiBotLite]服务器没有绑定任何群哦!");
248+
return;
249+
}
250+
Config.Settings.Token = string.Empty;
251+
Config.Settings.Write();
252+
WebSocket.Dispose();
253+
GenBindCode(EventArgs.Empty);
254+
plr.SendInfoMessage("[CaiBotLite]验证码已生成,请在后台查看喵~");
255+
break;
256+
}
257+
}
258+
259+
260+
261+
public static void GenBindCode(EventArgs args)
262+
{
263+
if (!string.IsNullOrEmpty(Config.Settings.Token))
264+
{
265+
return;
266+
}
267+
268+
InitCode = new Random().Next(10000000, 99999999);
269+
TShock.Log.ConsoleError($"[CaiBotLite]您的服务器绑定码为: {InitCode}");
270+
}
271+
272+
273+
#region 加载前置
274+
275+
private Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs args)
276+
{
277+
var resourceName =
278+
$"{Assembly.GetExecutingAssembly().GetName().Name}.{new AssemblyName(args.Name).Name}.dll";
279+
using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName);
280+
if (stream == null)
281+
{
282+
return null;
283+
}
284+
285+
var assemblyData = new byte[stream.Length];
286+
_ = stream.Read(assemblyData, 0, assemblyData.Length);
287+
return Assembly.Load(assemblyData);
288+
}
289+
290+
#endregion
291+
}

‎src/CaiBotLite/README.md

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# CaiBotPlugin 官方机器人适配插件
2+
3+
- 作者: Cai
4+
- 仓库: 此仓库
5+
- 此插件为CaiBotLite适配插件
6+
7+
8+
## 指令
9+
10+
| 语法 | 权限 | 说明 |
11+
|-----------------|:------------:|:------------:|
12+
| /caibot debug | caibot.admin | 调试模式开关 |
13+
| /caibot code | caibot.admin | 生成验证码 |
14+
| /caibot info | caibot.admin | 显示CaiBot状态信息 |
15+
| /caibot unbind | caibot.admin | 主动解除群绑定 |
16+
17+
## 配置
18+
19+
> 配置文件位置:tshock/CaiBotLite.json
20+
21+
```json5
22+
{
23+
"白名单开关": true, //Cai白名单的开关
24+
"密钥": "1145141919810", //由系统自动配置
25+
"白名单拦截提示的群号": 991556763 //为0时不显示群号
26+
}
27+
```
28+
29+
## 更新日志
30+
31+
```
32+
v2025.01.28.1 建立项目
33+
```
34+
35+
## 反馈
36+
37+
- 优先发issued -> 共同维护的插件库:https://github.com/UnrealMultiple/TShockPlugin
38+
- 次优先:TShock官方群:816771079
39+
- 大概率看不到但是也可以:国内社区trhub.cn ,bbstr.net , tr.monika.love
40+

‎src/CaiBotLite/i18n/en-US.po

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
msgid ""
2+
msgstr ""
3+
"Project-Id-Version: tshock-chinese-plugin\n"
4+
"POT-Creation-Date: 2025-01-24 05:21:50+0000\n"
5+
"PO-Revision-Date: 2025-01-25 01:03\n"
6+
"Last-Translator: \n"
7+
"Language-Team: English\n"
8+
"MIME-Version: 1.0\n"
9+
"Content-Type: text/plain; charset=UTF-8\n"
10+
"Content-Transfer-Encoding: 8bit\n"
11+
"X-Generator: GetText.NET Extractor\n"
12+
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
13+
"X-Crowdin-Project: tshock-chinese-plugin\n"
14+
"X-Crowdin-Project-ID: 751499\n"
15+
"X-Crowdin-Language: en\n"
16+
"X-Crowdin-File: /master/src/CaiBot/i18n/template.pot\n"
17+
"X-Crowdin-File-ID: 1120\n"
18+
"Language: en_US\n"
19+
20+
#: ../../Plugin.cs:259
21+
msgid "帮助 ({0}/{1}):"
22+
msgstr "Help ({0}/{1}):"
23+
24+
#: ../../Plugin.cs:259
25+
msgid "输入 {0}caibot help {{0}} 查看更多"
26+
msgstr "Enter {0}caibot help {{0}} to see more"
27+

‎src/CaiBotLite/i18n/es-ES.po

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
msgid ""
2+
msgstr ""
3+
"Project-Id-Version: tshock-chinese-plugin\n"
4+
"POT-Creation-Date: 2025-01-24 05:21:50+0000\n"
5+
"PO-Revision-Date: 2025-01-25 01:03\n"
6+
"Last-Translator: \n"
7+
"Language-Team: Spanish\n"
8+
"MIME-Version: 1.0\n"
9+
"Content-Type: text/plain; charset=UTF-8\n"
10+
"Content-Transfer-Encoding: 8bit\n"
11+
"X-Generator: GetText.NET Extractor\n"
12+
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
13+
"X-Crowdin-Project: tshock-chinese-plugin\n"
14+
"X-Crowdin-Project-ID: 751499\n"
15+
"X-Crowdin-Language: es-ES\n"
16+
"X-Crowdin-File: /master/src/CaiBot/i18n/template.pot\n"
17+
"X-Crowdin-File-ID: 1120\n"
18+
"Language: es_ES\n"
19+
20+
#: ../../Plugin.cs:259
21+
msgid "帮助 ({0}/{1}):"
22+
msgstr ""
23+
24+
#: ../../Plugin.cs:259
25+
msgid "输入 {0}caibot help {{0}} 查看更多"
26+
msgstr ""
27+

‎src/CaiBotLite/i18n/ru-RU.po

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
msgid ""
2+
msgstr ""
3+
"Project-Id-Version: tshock-chinese-plugin\n"
4+
"POT-Creation-Date: 2025-01-24 05:21:50+0000\n"
5+
"PO-Revision-Date: 2025-01-25 01:03\n"
6+
"Last-Translator: \n"
7+
"Language-Team: Russian\n"
8+
"MIME-Version: 1.0\n"
9+
"Content-Type: text/plain; charset=UTF-8\n"
10+
"Content-Transfer-Encoding: 8bit\n"
11+
"X-Generator: GetText.NET Extractor\n"
12+
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
13+
"X-Crowdin-Project: tshock-chinese-plugin\n"
14+
"X-Crowdin-Project-ID: 751499\n"
15+
"X-Crowdin-Language: ru\n"
16+
"X-Crowdin-File: /master/src/CaiBot/i18n/template.pot\n"
17+
"X-Crowdin-File-ID: 1120\n"
18+
"Language: ru_RU\n"
19+
20+
#: ../../Plugin.cs:259
21+
msgid "帮助 ({0}/{1}):"
22+
msgstr ""
23+
24+
#: ../../Plugin.cs:259
25+
msgid "输入 {0}caibot help {{0}} 查看更多"
26+
msgstr ""
27+

‎src/CaiBotLite/i18n/template.pot

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
msgid ""
2+
msgstr ""
3+
"Project-Id-Version: CaiBot\n"
4+
"POT-Creation-Date: 2025-01-24 05:21:50+0000\n"
5+
"PO-Revision-Date: 2025-01-24 05:21:50+0000\n"
6+
"Last-Translator: \n"
7+
"Language-Team: \n"
8+
"MIME-Version: 1.0\n"
9+
"Content-Type: text/plain; charset=utf-8\n"
10+
"Content-Transfer-Encoding: 8bit\n"
11+
"X-Generator: GetText.NET Extractor\n"
12+
13+
#: ../../Plugin.cs:259
14+
msgid "帮助 ({0}/{1}):"
15+
msgstr ""
16+
17+
#: ../../Plugin.cs:259
18+
msgid "输入 {0}caibot help {{0}} 查看更多"
19+
msgstr ""
20+

‎src/CaiBotLite/manifest.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"Dependencies": [],
3+
"README.en-US": {
4+
"Description": "CaiBot adapter plugin (Only support QQ)"
5+
},
6+
"README.es-ES": {
7+
"Description": "Plugin adaptador CaiBot (Only support QQ)"
8+
},
9+
"README": {
10+
"Description": "CaiBot 官方机器人适配插件"
11+
}
12+
}

0 commit comments

Comments
 (0)
Please sign in to comment.