Add character & inventory manager

This commit is contained in:
Naruse
2026-04-21 14:39:26 +08:00
parent c98fa7efa6
commit 7a8cab5723
16 changed files with 558 additions and 53 deletions

View File

@@ -0,0 +1,54 @@
using MikuSB.Data;
using MikuSB.Data.Excel;
using MikuSB.Database;
using MikuSB.Database.Character;
using MikuSB.Enums.Item;
using MikuSB.GameServer.Game.Player;
using MikuSB.Util.Extensions;
namespace MikuSB.GameServer.Game.Character;
public class CharacterManager(PlayerInstance player) : BasePlayerManager(player)
{
public CharacterData CharacterData { get; } = DatabaseHelper.GetInstanceOrCreateNew<CharacterData>(player.Uid);
public async ValueTask<CardExcel?> AddCharacter(ItemTypeEnum genre, uint detail, uint particular, uint level = 1)
{
var characterId = GameResourceTemplateId.FromGdpl((uint)genre,detail,particular,level);
if (CharacterData.Characters.Any(a => a.TemplateId == characterId)) return null;
var CharacterExcel = GameData.CardData.Values.FirstOrDefault(x => x.Genre == (int)genre && x.Detail == detail && x.Particular == particular);
if (CharacterExcel == null) return null;
var character = new CharacterInfo
{
Guid = CharacterData.NextCharacterGuid++,
TemplateId = characterId,
Level = level,
Break = CharacterExcel.InitBreak,
Timestamp = Extensions.GetUnixSec(),
};
var weaponInfo = await Player.InventoryManager!.AddWeaponItem((ItemTypeEnum)CharacterExcel.DefaultWeaponGPDL[0], CharacterExcel.DefaultWeaponGPDL[1], CharacterExcel.DefaultWeaponGPDL[2], (uint)CharacterExcel.DefaultWeaponGPDL[3]);
if (weaponInfo != null) character.WeaponUniqueId = weaponInfo.UniqueId;
var skinInfo = await Player.InventoryManager!.AddSkinItem(ItemTypeEnum.TYPE_CARD_SKIN,detail,particular,level);
if (skinInfo != null)
{
character.SkinId = skinInfo.UniqueId;
character.UnlockedSkin.Add(skinInfo.UniqueId);
}
CharacterData.Characters.Add(character);
return CharacterExcel;
}
public CharacterInfo? GetCharacter(ulong TemplateId)
{
return CharacterData.Characters.Find(Character => Character.TemplateId == TemplateId);
}
public CharacterInfo? GetCharacterGDPL(ItemTypeEnum genre, int detail, int particular)
{
var templateId = GameResourceTemplateId.FromGdpl((uint)genre,(uint)detail,(uint)particular,1);
return CharacterData.Characters.Find(Character => Character.TemplateId == templateId);
}
}

View File

@@ -0,0 +1,82 @@
using MikuSB.Data;
using MikuSB.Database;
using MikuSB.Database.Inventory;
using MikuSB.Enums.Item;
using MikuSB.GameServer.Game.Player;
namespace MikuSB.GameServer.Game.Inventory;
public class InventoryManager(PlayerInstance player) : BasePlayerManager(player)
{
public InventoryData InventoryData { get; } = DatabaseHelper.GetInstanceOrCreateNew<InventoryData>(player.Uid);
public async ValueTask<GameWeaponInfo?> AddWeaponItem(ItemTypeEnum genre, uint detail, uint particular, uint level = 1)
{
if (genre != ItemTypeEnum.TYPE_WEAPON) return null;
var weaponData = GameData.WeaponData.Values.FirstOrDefault(x => x.Genre == (int)genre && x.Detail == detail && x.Particular == particular);
if (weaponData == null) return null;
var templateId = GameResourceTemplateId.FromGdpl((uint)genre,detail,particular,level);
var weaponInfo = new GameWeaponInfo
{
TemplateId = templateId,
UniqueId = InventoryData.NextUniqueUid++,
Level = level,
ItemCount = 1
};
InventoryData.Weapons[weaponInfo.UniqueId] = weaponInfo;
return weaponInfo;
}
public GameWeaponInfo? GetWeaponItem(uint uniqueId)
{
return InventoryData.Weapons.GetValueOrDefault(uniqueId);
}
public GameWeaponInfo? GetWeaponItemByTemplateId(ulong templateId)
{
return InventoryData.Weapons.Values.FirstOrDefault(x => x.TemplateId == templateId);
}
public GameWeaponInfo? GetWeaponItemGDPL(ItemTypeEnum genre, int detail, int particular)
{
var templateId = GameResourceTemplateId.FromGdpl((uint)genre, (uint)detail, (uint)particular, 1);
return InventoryData.Weapons.Values.FirstOrDefault(x => x.TemplateId == templateId);
}
public async ValueTask<GameSkinInfo?> AddSkinItem(ItemTypeEnum genre, uint detail, uint particular, uint level = 1)
{
if (genre != ItemTypeEnum.TYPE_CARD_SKIN) return null;
var skinData = GameData.CardSkinData.Values.FirstOrDefault(x => x.Genre == (int)genre && x.Detail == detail && x.Particular == particular);
if (skinData == null) return null;
var templateId = GameResourceTemplateId.FromGdpl((uint)genre,detail,particular,level);
var skinInfo = new GameSkinInfo
{
TemplateId = templateId,
UniqueId = InventoryData.NextUniqueUid++,
Level = level,
ItemCount = 1
};
InventoryData.Skins[skinInfo.UniqueId] = skinInfo;
return skinInfo;
}
public GameSkinInfo? GetSkinItem(uint uniqueId)
{
return InventoryData.Skins.GetValueOrDefault(uniqueId);
}
public GameSkinInfo? GetSkinItemByTemplateId(ulong templateId)
{
return InventoryData.Skins.Values.FirstOrDefault(x => x.TemplateId == templateId);
}
public GameSkinInfo? GetSkinItemGDPL(ItemTypeEnum genre, int detail, int particular)
{
var templateId = GameResourceTemplateId.FromGdpl((uint)genre, (uint)detail, (uint)particular, 1);
return InventoryData.Skins.Values.FirstOrDefault(x => x.TemplateId == templateId);
}
}

View File

@@ -1,7 +1,12 @@
using MikuSB.Database;
using MikuSB.Data;
using MikuSB.Database;
using MikuSB.Database.Account;
using MikuSB.Database.Player;
using MikuSB.Enums.Item;
using MikuSB.GameServer.Game.Character;
using MikuSB.GameServer.Game.Inventory;
using MikuSB.GameServer.Server;
using MikuSB.Proto;
using MikuSB.TcpSharp;
using MikuSB.Util.Extensions;
@@ -22,6 +27,8 @@ public class PlayerInstance(PlayerGameData data)
#region Data & Manager
public PlayerGameData Data { get; set; } = data;
public CharacterManager CharacterManager { get; set; } = null!;
public InventoryManager InventoryManager { get; set; } = null!;
#endregion
@@ -37,16 +44,47 @@ public class PlayerInstance(PlayerGameData data)
var t = Task.Run(async () =>
{
await InitialPlayerManager();
foreach (var card in GameData.CardData.Values) await CharacterManager.AddCharacter((ItemTypeEnum)card.Genre, card.Detail, card.Particular, card.Level);
var bootstrapAttrs = BuildLobbyBootstrapAttrs();
var existingAttrs = Data.Attrs
.ToDictionary(x => (x.Gid, x.Sid));
var seenAttrs = new HashSet<(uint Gid, uint Sid)>();
foreach (var (gid, sid, value) in bootstrapAttrs)
{
if (!seenAttrs.Add((gid, sid)))
continue;
if (existingAttrs.TryGetValue((gid, sid), out var attr))
{
if (attr.Val < value)
attr.Val = value;
continue;
}
var newAttr = new PlayerAttr
{
Gid = gid,
Sid = sid,
Val = value
};
Data.Attrs.Add(newAttr);
existingAttrs[(gid, sid)] = newAttr;
}
});
t.Wait();
Initialized = true;
}
private async ValueTask InitialPlayerManager()
{
Uid = Data.Uid;
Data.LastActiveTime = Extensions.GetUnixSec();
InventoryManager = new InventoryManager(this);
CharacterManager = new CharacterManager(this);
await Task.CompletedTask;
}
@@ -94,5 +132,113 @@ public class PlayerInstance(PlayerGameData data)
#region Serialization
public Proto.Player ToPlayerProto()
{
var proto = new Proto.Player
{
Pid = (ulong)Data.Uid,
Account = Data.Name,
Provider = Data.Name,
Name = Data.Name,
Level = Data.Level,
Sex = Data.Gender,
Solutions =
{
new Lineup // TODO Lineup Manager
{
Index = 1,
Name = "Default",
Member1 = 1,
Member2 = 2,
Member3 = 3
}
},
};
foreach(var weapon in InventoryManager.InventoryData.Weapons.Values) proto.Items.Add(weapon.ToProto());
foreach (var skin in InventoryManager.InventoryData.Skins.Values) proto.Items.Add(skin.ToProto());
foreach (var chara in CharacterManager.CharacterData.Characters) proto.Items.Add(chara.ToProto());
foreach (var x in Data.Attrs)
{
uint gid = x.Gid;
uint sid = x.Sid;
uint val = x.Val;
if (gid == 0)
{
proto.Attrs[sid] = val;
continue;
}
proto.Attrs[ToPackedAttrKey(gid, sid)] = val;
proto.Attrs[ToShiftedAttrKey(gid, sid)] = val;
}
return proto;
}
private static uint ToPackedAttrKey(uint gid, uint sid)
{
if (gid == 0)
return sid;
return (gid * 10000) + sid;
}
private static uint ToShiftedAttrKey(uint gid, uint sid)
{
if (gid == 0)
return sid;
return (gid << 16) | sid;
}
private static IEnumerable<(uint Gid, uint Sid, uint Value)> BuildLobbyBootstrapAttrs()
{
// GuideLogic uses group 4. Value 999 is safely above every configured step count,
// so the client treats these guides as already completed.
yield return (4, 0, 5);
yield return (11, 1, 1);
yield return (57, 0, 1);
yield return (99, 3, 30);
yield return (110, 1, 1);
yield return (178, 1, 1_700_000_000);
yield return (187, 1, 2);
for (uint guideId = 1; guideId <= 150; guideId++)
yield return (4, guideId, 999);
for (uint guideId = 10_000; guideId <= 10_300; guideId++)
yield return (4, guideId, 999);
for (uint guideId = 11_000; guideId <= 11_300; guideId++)
yield return (4, guideId, 999);
for (uint guideId = 12_000; guideId <= 12_100; guideId++)
yield return (4, guideId, 999);
for (uint guideId = 22_000; guideId <= 22_100; guideId++)
yield return (4, guideId, 999);
// Additional guide ids referenced directly by the Lua scripts and observed client logs.
foreach (var guideId in new uint[] { 10_031, 10_041, 10_061, 10_081, 10_101, 10_224, 11_006, 11_202, 11_210, 22_002 })
yield return (4, guideId, 999);
// Launch.GPASSID = 22 stores pass counts. ChapterLevel.GID = 21 stores star flags.
// Completing the prologue/early chapter range prevents function conditions from
// treating the account as a fresh tutorial player.
for (uint levelId = 10_000; levelId <= 10_160; levelId++)
{
yield return (21, levelId, 7);
yield return (22, levelId, 1);
}
foreach (var levelId in new uint[] { 10_121, 10_122, 10_123, 50_000, 50_151 })
{
yield return (21, levelId, 7);
yield return (22, levelId, 1);
}
}
#endregion
}