Compare commits

..

4 Commits

Author SHA1 Message Date
Kei-Luna
846139347a Implement Weapon_Evolution 2026-04-29 09:58:35 +09:00
Kei-Luna
d1102b444c Implement Weapon_Break 2026-04-29 09:40:36 +09:00
Kei-Luna
3611624073 Update version.txt 2026-04-29 09:01:38 +09:00
Kei-Luna
c61ac08dd3 Fixed a critical issue with in-game chat. 2026-04-29 09:01:20 +09:00
9 changed files with 245 additions and 6 deletions

View File

@@ -0,0 +1,34 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("break.json")]
public class BreakExcel : ExcelResource
{
[JsonProperty("ID")] public int Id { get; set; }
[JsonProperty("Items1")] public List<List<int>> Items1 { get; set; } = [];
[JsonProperty("Items2")] public List<List<int>> Items2 { get; set; } = [];
[JsonProperty("Items3")] public List<List<int>> Items3 { get; set; } = [];
[JsonProperty("Items4")] public List<List<int>> Items4 { get; set; } = [];
[JsonProperty("Items5")] public List<List<int>> Items5 { get; set; } = [];
[JsonProperty("Items6")] public List<List<int>> Items6 { get; set; } = [];
public List<List<int>> GetItems(uint breakLevel) => breakLevel switch
{
1 => Items1,
2 => Items2,
3 => Items3,
4 => Items4,
5 => Items5,
6 => Items6,
_ => []
};
public override uint GetId() => (uint)Id;
public override void Loaded()
{
GameData.BreakData[Id] = this;
}
}

View File

@@ -16,6 +16,7 @@ public static class GameData
public static Dictionary<uint, ArItemExcel> ArItemData { get; private set; } = [];
public static Dictionary<uint, ManifestationExcel> ManifestationData { get; private set; } = [];
public static Dictionary<uint, Rogue3DDifficultExcel> Rogue3DDifficultData { get; private set; } = [];
public static Dictionary<int, BreakExcel> BreakData { get; private set; } = [];
public static Dictionary<uint, SpineExcel> SpineData { get; private set; } = [];
public static Dictionary<uint, NodeConditionExcel> NodeConditionData { get; private set; } = [];
public static List<SupportCardExcel> SupportCardData { get; private set; } = [];

View File

@@ -56,6 +56,7 @@ public abstract class GrowableItemInfo : BaseGameItemInfo
public new uint Level { get; set; }
public new uint Exp { get; set; }
public uint Break { get; set; }
public uint Evolue { get; set; }
public uint EquipAvatarId { get; set; }
}
@@ -73,7 +74,8 @@ public class GameWeaponInfo : GrowableItemInfo
{
Level = Level,
Exp = Exp,
Break = Break
Break = Break,
Evolue = Evolue
}
};
return proto;

View File

@@ -38,9 +38,9 @@ public class PlayerCommandSender(PlayerInstance player) : ICommandSender
Type = ChatType.Friend,
Sender = (uint)ConfigManager.Config.ServerOption.ServerProfile.Uid,
Recver = (uint)Player.Uid,
Text = msg,
Text = ChatMessageHelper.NormalizeForClient(msg),
Profile = Player.ToServerFriendProto(),
TimeStamp = (uint)Extensions.GetUnixMs()
TimeStamp = ChatMessageHelper.BuildClientTimestamp()
};
await Player.SendPacket(CmdIds.NtfFriendChat, data);
}

View File

@@ -0,0 +1,27 @@
using System.Text.RegularExpressions;
namespace MikuSB.GameServer.Game.Player;
public static partial class ChatMessageHelper
{
[GeneratedRegex(@"\s+")]
private static partial Regex MultiWhitespaceRegex();
public static uint BuildClientTimestamp()
{
return (uint)MikuSB.Util.Extensions.Extensions.GetUnixSec();
}
public static string NormalizeForClient(string? text)
{
if (string.IsNullOrWhiteSpace(text))
return string.Empty;
var normalized = text
.Replace("\r\n", " ")
.Replace('\r', ' ')
.Replace('\n', ' ');
return MultiWhitespaceRegex().Replace(normalized, " ").Trim();
}
}

View File

@@ -160,9 +160,9 @@ public class PlayerInstance(PlayerGameData data)
Sender = sendUid,
Recver = recvUid,
Emoji = emojiId ?? 0,
Text = message ?? "",
Text = ChatMessageHelper.NormalizeForClient(message),
Profile = Data.ToProfileProto(),
TimeStamp = (uint)Extensions.GetUnixMs()
TimeStamp = ChatMessageHelper.BuildClientTimestamp()
};
await SendPacket(CmdIds.NtfFriendChat, data);

View File

@@ -0,0 +1,98 @@
using MikuSB.Data;
using MikuSB.Database;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Weapon;
// s2c: function(sErr) — send "null" on success (json.decode("null") = nil = falsy in Lua)
[CallGSApi("Weapon_Break")]
public class Weapon_Break : ICallGSHandler
{
private const uint MaxBreak = 6;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<WeaponBreakParam>(param);
if (req == null || req.WeaponId == 0)
{
await CallGSRouter.SendScript(connection, "Weapon_Break", "\"error.BadParam\"");
return;
}
var weapon = player.InventoryManager.InventoryData.Weapons.GetValueOrDefault((uint)req.WeaponId);
if (weapon == null)
{
await CallGSRouter.SendScript(connection, "Weapon_Break", "\"error.BadParam\"");
return;
}
if (weapon.Break >= MaxBreak)
{
await CallGSRouter.SendScript(connection, "Weapon_Break", "\"tip.already_max_break\"");
return;
}
var nextBreak = weapon.Break + 1;
// Look up break cost from WeaponExcel → BreakExcel
var weaponExcel = GameData.WeaponData.Values.FirstOrDefault(x =>
GameResourceTemplateId.FromGdpl(x.Genre, x.Detail, x.Particular, x.Level) == weapon.TemplateId);
var requestedMaterials = new Dictionary<ulong, uint>();
if (weaponExcel != null && GameData.BreakData.TryGetValue(weaponExcel.BreakMatID, out var breakExcel))
{
foreach (var row in breakExcel.GetItems(nextBreak))
{
if (row.Count < 5) continue;
var tid = GameResourceTemplateId.FromGdpl(
(uint)row[0], (uint)row[1], (uint)row[2], (uint)row[3]);
requestedMaterials[tid] = requestedMaterials.GetValueOrDefault(tid) + (uint)row[4];
}
}
// Validate materials
foreach (var (tid, count) in requestedMaterials)
{
var item = player.InventoryManager.InventoryData.Items.Values.FirstOrDefault(x => x.TemplateId == tid);
if (item == null || item.ItemCount < count)
{
await CallGSRouter.SendScript(connection, "Weapon_Break", "\"tip.not_material_for_break\"");
return;
}
}
// Consume materials
var syncItems = new List<Item>();
foreach (var (tid, count) in requestedMaterials)
{
var item = player.InventoryManager.InventoryData.Items.Values.First(x => x.TemplateId == tid);
item.ItemCount -= count;
var proto = item.ToProto();
if (item.ItemCount == 0)
{
player.InventoryManager.InventoryData.Items.Remove(item.UniqueId);
proto.Count = 0;
}
syncItems.Add(proto);
}
weapon.Break = nextBreak;
syncItems.Add(weapon.ToProto());
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
var sync = new NtfSyncPlayer();
sync.Items.AddRange(syncItems);
await CallGSRouter.SendScript(connection, "Weapon_Break", "null", sync);
}
}
internal sealed class WeaponBreakParam
{
[JsonPropertyName("Id")]
public int WeaponId { get; set; }
}

View File

@@ -0,0 +1,77 @@
using MikuSB.Database;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Weapon;
// s2c: function(sErr) — send "null" on success
// Id = target weapon UniqueId
// nItemId = material item UniqueId (weapon or supply item to consume)
[CallGSApi("Weapon_Evolution")]
public class Weapon_Evolution : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<WeaponEvolutionParam>(param);
if (req == null || req.WeaponId == 0 || req.MaterialId == 0)
{
await CallGSRouter.SendScript(connection, "Weapon_Evolution", "\"error.BadParam\"");
return;
}
var weapon = player.InventoryManager.InventoryData.Weapons.GetValueOrDefault((uint)req.WeaponId);
if (weapon == null)
{
await CallGSRouter.SendScript(connection, "Weapon_Evolution", "\"error.BadParam\"");
return;
}
var syncItems = new List<Item>();
// Material can be a weapon or a regular item
if (player.InventoryManager.InventoryData.Weapons.TryGetValue((uint)req.MaterialId, out var matWeapon))
{
player.InventoryManager.InventoryData.Weapons.Remove((uint)req.MaterialId);
var removed = matWeapon.ToProto();
removed.Count = 0;
syncItems.Add(removed);
}
else if (player.InventoryManager.InventoryData.Items.TryGetValue((uint)req.MaterialId, out var matItem))
{
matItem.ItemCount--;
var proto = matItem.ToProto();
if (matItem.ItemCount == 0)
{
player.InventoryManager.InventoryData.Items.Remove(matItem.UniqueId);
proto.Count = 0;
}
syncItems.Add(proto);
}
else
{
await CallGSRouter.SendScript(connection, "Weapon_Evolution", "\"tip.not_material\"");
return;
}
weapon.Evolue++;
syncItems.Add(weapon.ToProto());
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
var sync = new NtfSyncPlayer();
sync.Items.AddRange(syncItems);
await CallGSRouter.SendScript(connection, "Weapon_Evolution", "null", sync);
}
}
internal sealed class WeaponEvolutionParam
{
[JsonPropertyName("Id")]
public int WeaponId { get; set; }
[JsonPropertyName("nItemId")]
public int MaterialId { get; set; }
}

View File

@@ -1 +1 @@
v=1.2
v=1.4