Files
MikuSB/GameServer/Game/Player/PlayerInstance.cs
2026-04-28 16:12:27 +09:00

348 lines
11 KiB
C#

using Google.Protobuf;
using MikuSB.Data;
using MikuSB.Database;
using MikuSB.Database.Account;
using MikuSB.Database.Inventory;
using MikuSB.Database.Player;
using MikuSB.Enums.Item;
using MikuSB.GameServer.Command;
using MikuSB.GameServer.Game.Character;
using MikuSB.GameServer.Game.Inventory;
using MikuSB.GameServer.Game.Lineup;
using MikuSB.GameServer.Server;
using MikuSB.Proto;
using MikuSB.TcpSharp;
using MikuSB.Util;
using MikuSB.Util.Extensions;
namespace MikuSB.GameServer.Game.Player;
public class PlayerInstance(PlayerGameData data)
{
#region Property
public Connection? Connection { get; set; }
public static readonly List<PlayerInstance> _playerInstances = [];
public int Uid { get; set; }
public bool Initialized { get; set; }
public bool IsNewPlayer { get; set; }
#endregion
#region Data & Manager
public PlayerGameData Data { get; set; } = data;
public CharacterManager CharacterManager { get; set; } = null!;
public InventoryManager InventoryManager { get; set; } = null!;
public LineupManager LineupManager { get; set; } = null!;
#endregion
#region Initializers
public PlayerInstance(int uid) : this(new PlayerGameData { Uid = uid })
{
// new player
IsNewPlayer = true;
Data.Name = PlayerGameData.NormalizeDisplayName(AccountData.GetAccountByUid(uid)?.Username);
DatabaseHelper.CreateInstance(Data);
var t = Task.Run(async () =>
{
await InitialPlayerManager();
foreach (var skinCard in GameData.CardSkinData.Values)
{
await InventoryManager.AddSkinItem((ItemTypeEnum)skinCard.Genre, skinCard.Detail, skinCard.Particular, skinCard.Level, false);
}
foreach (var ar in GameData.ArItemData.Values)
{
await InventoryManager.AddArItem((ItemTypeEnum)ar.Genre, ar.Detail, ar.Particular, ar.Level, false);
}
foreach (var manifest in GameData.ManifestationData.Values)
{
await InventoryManager.AddManifestationItem((ItemTypeEnum)manifest.Genre, manifest.Detail, manifest.Particular, manifest.Level, false);
}
foreach (var card in GameData.CardData.Values)
{
await CharacterManager.AddCharacter((ItemTypeEnum)card.Genre, card.Detail, card.Particular, card.Level, sendPacket:false);
}
foreach (var sc in GameData.SupportCardData)
{
await InventoryManager.AddSupportCardItem(sc.Detail, sc.Particular, sc.Level, sendPacket: false);
}
foreach (var supplies in GameData.AllSuppliesData)
{
await InventoryManager.AddSuppliesItem(supplies, 90000, false);
}
var selected = CharacterManager.CharacterData.Characters
.OrderBy(_ => Guid.NewGuid())
.Take(3)
.Select(x => x.Guid)
.ToList();
await LineupManager.UpdateLineup(1, selected[0], selected[1], selected[2],false);
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);
LineupManager = new LineupManager(this);
CharacterManager = new CharacterManager(this);
await Task.CompletedTask;
}
public T InitializeDatabase<T>() where T : BaseDatabaseDataHelper, new()
{
var instance = DatabaseHelper.GetInstanceOrCreateNew<T>(Uid);
return instance!;
}
#endregion
#region Network
public async ValueTask OnEnterGame()
{
if (!Initialized) await InitialPlayerManager();
Data.EnsureDisplayName();
await CharacterManager.RepairCharacterWeapons();
await EnsureSupplies();
}
public IEnumerable<BaseGameItemInfo> GetSupplyItems() =>
InventoryManager.InventoryData.Items.Values.Where(x => (x.TemplateId & 0xFFFF) == 5);
private async ValueTask EnsureSupplies()
{
foreach (var supplies in GameData.AllSuppliesData)
{
await InventoryManager.AddSuppliesItem(supplies, 90000, false);
}
}
public async ValueTask OnLogin()
{
_playerInstances.Add(this);
await Task.CompletedTask;
}
public static PlayerInstance? GetPlayerInstanceByUid(long uid)
=> _playerInstances.FirstOrDefault(player => player.Uid == uid);
public void OnLogoutAsync()
{
_playerInstances.Remove(this);
}
public async ValueTask SendPacket(BasePacket packet)
{
if (Connection?.IsOnline == true) await Connection.SendPacket(packet);
}
public async ValueTask SendPacket(int cmdId, IMessage msg)
{
if (Connection?.IsOnline == true) await Connection.SendPacket(cmdId,msg);
}
#endregion
#region Actions
public async ValueTask OnHeartBeat()
{
DatabaseHelper.ToSaveUidList.SafeAdd(Uid);
await Task.CompletedTask;
}
public async ValueTask ReceiveMessage(uint sendUid, uint recvUid, string? message = null, uint? emojiId = null)
{
var data = new ChatMsg
{
Type = ChatType.Friend,
Sender = sendUid,
Recver = recvUid,
Emoji = emojiId ?? 0,
Text = message ?? "",
Profile = Data.ToProfileProto(),
TimeStamp = (uint)Extensions.GetUnixMs()
};
await SendPacket(CmdIds.NtfFriendChat, data);
if (recvUid == ConfigManager.Config.ServerOption.ServerProfile.Uid)
{
if (message != null)
{
if (message.StartsWith("/")) message = message[1..].Trim();
CommandExecutor.ExecuteCommand(new PlayerCommandSender(this), message);
}
}
}
#endregion
#region Serialization
public PlayerProfile ToServerFriendProto()
{
var server = ConfigManager.Config.ServerOption.ServerProfile;
var proto = new PlayerProfile
{
Pid = (uint)server.Uid,
Account = server.Name,
Name = server.Name,
Sex = server.Gender,
Level = (uint)server.Level,
Sign = server.Signature
};
return proto;
}
public Proto.Player ToPlayerProto()
{
var displayName = PlayerGameData.NormalizeDisplayName(Data.Name);
var proto = new Proto.Player
{
Pid = (ulong)Data.Uid,
Account = displayName,
Provider = displayName,
Name = displayName,
Level = Data.Level,
Sex = Data.Gender,
Vigor = Data.Vigor,
Solutions = { LineupManager.LineupData.LineupInfo.Values.Select(x => x.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;
}
proto.ShowItems.AddRange(Data.ShowItems);
return proto;
}
public void SetDisplayName(string? name)
{
Data.Name = PlayerGameData.NormalizeDisplayName(name);
}
public void SetShowItem(int index, ulong itemId)
{
if (index <= 0)
return;
while (Data.ShowItems.Count < index)
Data.ShowItems.Add(0);
Data.ShowItems[index - 1] = itemId;
}
public uint ToPackedAttrKey(uint gid, uint sid)
{
if (gid == 0)
return sid;
return (gid * 10000) + sid;
}
public 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);
// Cash.GetMoneyCount uses group 1 with sid = moneyId * 2 + 1 for most currencies.
// Fill a wide currency id range so every in-game currency starts effectively unlimited.
for (uint moneyId = 1; moneyId <= 200; moneyId++)
yield return (1, moneyId * 2 + 1, 999_999_999);
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.
// Unlock every level defined in level.json so all chapters are accessible from the start.
foreach (var levelId in GameData.ChapterLevelData.Keys)
{
yield return (21, levelId, 7);
yield return (22, levelId, 1_700_000_000);
}
// Main Scene 0 mean default scene
yield return (132, 1, 0);
}
#endregion
}