Compare commits

..

8 Commits
v2.6 ... v2.9

Author SHA1 Message Date
Naruse
5aa5ef92d0 Update version.txt 2026-05-13 19:59:33 +08:00
Naruse
c34ad5eb1e unlock more furniture 2026-05-13 19:58:53 +08:00
Naruse
8a597e24b6 add system change furniture 2026-05-13 19:42:10 +08:00
Naruse
9763f1f8d9 auto create account if not exist 2026-05-13 19:08:02 +08:00
Kei-Luna
933ba097f9 Modified to send all inventory data via RspLogin upon login. 2026-05-13 19:31:01 +09:00
Kei-Luna
c10d380e11 Added a process to save data to the database when the giveall command is executed. 2026-05-13 19:29:49 +09:00
Kei-Luna
6c5d546026 Compress large packets 2026-05-13 19:09:41 +09:00
Kei-Luna
9e518edb8e README 2026-05-13 14:48:47 +09:00
12 changed files with 109 additions and 27 deletions

View File

@@ -0,0 +1,18 @@
namespace MikuSB.Data.Excel;
[ResourceEntity("house/FurniturePos.json")]
public class HouseFurniturePosExcel : ExcelResource
{
public uint AreaId { get; set; }
public uint GroupId { get; set; }
public override uint GetId()
{
return (AreaId << 48) | (GroupId << 32);
}
public override void Loaded()
{
GameData.HouseFurniturePosData.TryAdd(GetId(), this);
}
}

View File

@@ -31,6 +31,7 @@ public static class GameData
public static Dictionary<uint, WeaponPartsExcel> WeaponPartsData { get; private set; } = []; public static Dictionary<uint, WeaponPartsExcel> WeaponPartsData { get; private set; } = [];
public static Dictionary<uint, GuideExcel> GuideData { get; private set; } = []; public static Dictionary<uint, GuideExcel> GuideData { get; private set; } = [];
public static Dictionary<uint, DormGiftExcel> DormGiftData { get; private set; } = []; public static Dictionary<uint, DormGiftExcel> DormGiftData { get; private set; } = [];
public static Dictionary<uint, HouseFurniturePosExcel> HouseFurniturePosData { get; private set; } = [];
} }
public static class GameResourceTemplateId public static class GameResourceTemplateId

View File

@@ -1,4 +1,5 @@
using MikuSB.Data; using MikuSB.Data;
using MikuSB.Database;
using MikuSB.Database.Inventory; using MikuSB.Database.Inventory;
using MikuSB.Enums.Item; using MikuSB.Enums.Item;
using MikuSB.Enums.Player; using MikuSB.Enums.Player;
@@ -42,6 +43,7 @@ public class CommandGiveAll : ICommands
weapons.Add(weapon); weapons.Add(weapon);
} }
if (weapons.Count > 0) await player.SendPacket(new PacketNtfCallScript(weapons)); if (weapons.Count > 0) await player.SendPacket(new PacketNtfCallScript(weapons));
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems", await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems",
I18NManager.Translate("Word.Weapon"), weapons.Count.ToString())); I18NManager.Translate("Word.Weapon"), weapons.Count.ToString()));
} }
@@ -77,6 +79,7 @@ public class CommandGiveAll : ICommands
supportCards.Add(supportCard); supportCards.Add(supportCard);
} }
if (supportCards.Count > 0) await player.SendPacket(new PacketNtfCallScript(supportCards)); if (supportCards.Count > 0) await player.SendPacket(new PacketNtfCallScript(supportCards));
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems", await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems",
I18NManager.Translate("Word.SupportCard"), supportCards.Count.ToString())); I18NManager.Translate("Word.SupportCard"), supportCards.Count.ToString()));
} }
@@ -111,6 +114,7 @@ public class CommandGiveAll : ICommands
weaponSkins.Add(weaponSkin); weaponSkins.Add(weaponSkin);
} }
if (weaponSkins.Count > 0) await player.SendPacket(new PacketNtfCallScript(weaponSkins)); if (weaponSkins.Count > 0) await player.SendPacket(new PacketNtfCallScript(weaponSkins));
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems", await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems",
I18NManager.Translate("Word.WeaponSkin"), weaponSkins.Count.ToString())); I18NManager.Translate("Word.WeaponSkin"), weaponSkins.Count.ToString()));
} }
@@ -147,6 +151,7 @@ public class CommandGiveAll : ICommands
profileItems.Add(profile); profileItems.Add(profile);
} }
if (profileItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(profileItems)); if (profileItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(profileItems));
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems", await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems",
I18NManager.Translate("Word.Profile"), profileItems.Count.ToString())); I18NManager.Translate("Word.Profile"), profileItems.Count.ToString()));
} }
@@ -183,6 +188,7 @@ public class CommandGiveAll : ICommands
skinPartItems.Add(skinPart); skinPartItems.Add(skinPart);
} }
if (skinPartItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(skinPartItems)); if (skinPartItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(skinPartItems));
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems", await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems",
I18NManager.Translate("Word.SkinPart"), skinPartItems.Count.ToString())); I18NManager.Translate("Word.SkinPart"), skinPartItems.Count.ToString()));
} }
@@ -219,6 +225,7 @@ public class CommandGiveAll : ICommands
callItems.Add(callItem); callItems.Add(callItem);
} }
if (callItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(callItems)); if (callItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(callItems));
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems", await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems",
I18NManager.Translate("Word.CallItem"), callItems.Count.ToString())); I18NManager.Translate("Word.CallItem"), callItems.Count.ToString()));
} }
@@ -255,6 +262,7 @@ public class CommandGiveAll : ICommands
weaponPartItems.Add(weaponPart); weaponPartItems.Add(weaponPart);
} }
if (weaponPartItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(weaponPartItems)); if (weaponPartItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(weaponPartItems));
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems", await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems",
I18NManager.Translate("Word.WeaponPart"), weaponPartItems.Count.ToString())); I18NManager.Translate("Word.WeaponPart"), weaponPartItems.Count.ToString()));
} }
@@ -291,6 +299,7 @@ public class CommandGiveAll : ICommands
skinItems.Add(skin); skinItems.Add(skin);
} }
if (skinItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(skinItems)); if (skinItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(skinItems));
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems", await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems",
I18NManager.Translate("Word.Skin"), skinItems.Count.ToString())); I18NManager.Translate("Word.Skin"), skinItems.Count.ToString()));
} }
@@ -327,6 +336,7 @@ public class CommandGiveAll : ICommands
furnitureItems.Add(furniture); furnitureItems.Add(furniture);
} }
if (furnitureItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(furnitureItems)); if (furnitureItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(furnitureItems));
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems", await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems",
I18NManager.Translate("Word.Furniture"), furnitureItems.Count.ToString())); I18NManager.Translate("Word.Furniture"), furnitureItems.Count.ToString()));
} }

View File

@@ -214,6 +214,10 @@ public class PlayerInstance(PlayerGameData data)
}; };
foreach (var chara in CharacterManager.CharacterData.Characters) proto.Items.Add(chara.ToProto()); foreach (var chara in CharacterManager.CharacterData.Characters) proto.Items.Add(chara.ToProto());
foreach (var item in InventoryManager.InventoryData.Items.Values) proto.Items.Add(item.ToProto());
foreach (var skin in InventoryManager.InventoryData.Skins.Values) proto.Items.Add(skin.ToProto());
foreach (var weapon in InventoryManager.InventoryData.Weapons.Values) proto.Items.Add(weapon.ToProto());
foreach (var card in InventoryManager.InventoryData.SupportCards.Values) proto.Items.Add(card.ToProto());
foreach (var x in Data.Attrs) foreach (var x in Data.Attrs)
{ {
uint gid = x.Gid; uint gid = x.Gid;
@@ -226,9 +230,7 @@ public class PlayerInstance(PlayerGameData data)
continue; continue;
} }
//ToDo proto.Attrs[ToPackedAttrKey(gid, sid)] = val;
//Temporary fix for login issues(need to handle LoginRsp properly with zlib.)
//proto.Attrs[ToPackedAttrKey(gid, sid)] = val;
proto.Attrs[ToShiftedAttrKey(gid, sid)] = val; proto.Attrs[ToShiftedAttrKey(gid, sid)] = val;
} }
@@ -325,20 +327,49 @@ public class PlayerInstance(PlayerGameData data)
private static IEnumerable<(uint Gid, uint Sid, uint Value)> BuildGirlFurnitureAttrs() private static IEnumerable<(uint Gid, uint Sid, uint Value)> BuildGirlFurnitureAttrs()
{ {
// Unlock some furniture slots for every girl
// Each furniture attr int stores 10 slots using 3 bits per slot
// Value below means slot 0..9 = 1
const uint furnitureUnlockedValue = 153391689; const uint furnitureUnlockedValue = 153391689;
var groupFurnitureByArea = new Dictionary<uint, uint>();
foreach (var pos in GameData.HouseFurniturePosData.Values)
{
var areaId = pos.AreaId;
var groupId = pos.GroupId;
uint selectedIndex = 1;
var shift = (groupId - 1) * 3;
if (!groupFurnitureByArea.TryGetValue(areaId, out var packed)) packed = 0;
packed |= (selectedIndex << (int)shift);
groupFurnitureByArea[areaId] = packed;
}
for (uint girlId = 0; girlId <= 50; girlId++) for (uint girlId = 0; girlId <= 50; girlId++)
{ {
// FurnitureStart..FurnitureEnd = 10..19 var baseSid = girlId * 50;
for (uint offset = 10; offset <= 19; offset++) for (uint offset = 10; offset <= 19; offset++)
{ yield return (101, baseSid + offset, furnitureUnlockedValue);
uint sid = (girlId * 50) + offset;
yield return (101, sid, furnitureUnlockedValue); if (groupFurnitureByArea.TryGetValue(girlId, out var groupValue))
} yield return (101, baseSid + 20, groupValue);
} }
// Massage room furniture
// 10010..10019
for (uint sid = 10010; sid <= 10019; sid++)
yield return (101, sid, furnitureUnlockedValue);
// Massage room group state
yield return (101, 10020, 1);
// Hot spring furniture
// 15001..15010
for (uint sid = 15001; sid <= 15010; sid++)
yield return (101, sid, furnitureUnlockedValue);
// Beach furniture
// 17101..17110
for (uint sid = 17101; sid <= 17110; sid++)
yield return (101, sid, furnitureUnlockedValue);
for (uint sid = 30000; sid < 31000; sid++)
yield return (101, sid, furnitureUnlockedValue);
} }
private static IEnumerable<(uint Gid, uint Sid, uint Value)> BuildLobbyBootstrapAttrs() private static IEnumerable<(uint Gid, uint Sid, uint Value)> BuildLobbyBootstrapAttrs()

View File

@@ -87,7 +87,6 @@ public class HandlerReqLogin : Handler
await connection.Player.OnHeartBeat(); await connection.Player.OnHeartBeat();
await connection.SendPacket(new PacketNtfUpdateFriend(connection.Player!)); await connection.SendPacket(new PacketNtfUpdateFriend(connection.Player!));
ApplySavedGirlSkinTypes(connection.Player!); ApplySavedGirlSkinTypes(connection.Player!);
await connection.SendPacket(new PacketNtfCallScript(connection.Player!.InventoryManager.InventoryData));
await SendGirlSkinTypeOnLogin(connection); await SendGirlSkinTypeOnLogin(connection);
} }

View File

@@ -22,7 +22,7 @@ public class PacketRspLogin : BasePacket
}; };
var bytes = Google.Protobuf.MessageExtensions.ToByteArray(proto); var bytes = Google.Protobuf.MessageExtensions.ToByteArray(proto);
Logger.Info($"RspLogin proto size: {bytes.Length} bytes (limit: 65535)"); Logger.Info($"RspLogin proto size: {bytes.Length} bytes");
SetData(bytes); SetData(bytes);
} }

View File

@@ -42,7 +42,8 @@ dotnet build
``` ```
2. Set `GamePath` in `Config.json` to the path of your game executable. 2. Set `GamePath` in `Config.json` to the path of your game executable.
3. Start the server and run the `game` command. 3. Start the server and run the `game` command.
4. Enjoy. 4. Create an account in the server console.
5. Enjoy.
## Feature List ## Feature List

View File

@@ -42,7 +42,8 @@ dotnet build
``` ```
2. Config.json の`GamePath`にあなたのゲームの実行ファイルのパスを書き込みます 2. Config.json の`GamePath`にあなたのゲームの実行ファイルのパスを書き込みます
3. サーバーを起動し`game`コマンドを入力します 3. サーバーを起動し`game`コマンドを入力します
4. 楽しむ 4. サーバーコンソールでアカウントを作成する
5. 楽しむ
## 機能一覧 ## 機能一覧

View File

@@ -270,12 +270,17 @@ public class RouteController : ControllerBase
var finalEmail = email ?? form_email ?? await GetJsonBodyValue("email"); var finalEmail = email ?? form_email ?? await GetJsonBodyValue("email");
if (!string.IsNullOrWhiteSpace(finalEmail)) if (!string.IsNullOrWhiteSpace(finalEmail))
{ {
var accountByEmail = AccountData.GetAccountByEmail(finalEmail); var username = finalEmail.Split('@')[0];
if (accountByEmail == null) var accountData = AccountData.GetAccountByUserName(username);
return BuildLoginFailedResponse("Account not found."); if (accountData == null)
{
if (!ConfigManager.Config.ServerOption.AutoCreateUser) return BuildLoginFailedResponse("Account not found.");
AccountData.CreateAccount(username, 0, "123456");
accountData = AccountData.GetAccountByUserName(username)!;
}
var finalUidValue = accountByEmail.Uid.ToString(); var finalUidValue = accountData.Uid.ToString();
var finalTokenValue = accountByEmail.GenerateComboToken(); var finalTokenValue = accountData.GenerateComboToken();
object emailLoginRsp = new object emailLoginRsp = new
{ {
@@ -286,14 +291,14 @@ public class RouteController : ControllerBase
isFirstLogin = false, isFirstLogin = false,
isNeedKoreaSciAuth = false, isNeedKoreaSciAuth = false,
ksOpenId = $"ks_{finalUidValue}", ksOpenId = $"ks_{finalUidValue}",
nickname = accountByEmail.Username, nickname = accountData.Username,
passportId = finalUidValue, passportId = finalUidValue,
playerFillAgeUrl = "", playerFillAgeUrl = "",
status = 0, status = 0,
thirdPartyUid = "", thirdPartyUid = "",
token = finalTokenValue, token = finalTokenValue,
type = "guest", type = "guest",
uid = accountByEmail.Uid uid = accountData.Uid
}, },
msg = "操作成功" msg = "操作成功"
}; };

View File

@@ -12,6 +12,7 @@ public class BasePacket
public long Timestamp { get; set; } public long Timestamp { get; set; }
public IMessage? Message { get; set; } public IMessage? Message { get; set; }
public PacketFraming Framing { get; set; } public PacketFraming Framing { get; set; }
public int UncompressedBodySize { get; set; }
public BasePacket(ushort cmdId) public BasePacket(ushort cmdId)
{ {

View File

@@ -2,6 +2,7 @@
using MikuSB.Enums.Packet; using MikuSB.Enums.Packet;
using MikuSB.Util; using MikuSB.Util;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.IO.Compression;
using System.Net.Sockets; using System.Net.Sockets;
namespace MikuSB.TcpSharp namespace MikuSB.TcpSharp
@@ -63,11 +64,11 @@ namespace MikuSB.TcpSharp
} }
} }
public byte[] Encode(ushort packetId, byte[] payload, PacketFraming framing = PacketFraming.FourByteLittleEndianLength) public byte[] Encode(ushort packetId, byte[] payload, PacketFraming framing = PacketFraming.FourByteLittleEndianLength, int uncompressedSize = 0)
{ {
return framing switch return framing switch
{ {
PacketFraming.TwoByteBigEndianLength => EncodeTwoByteFrame(packetId, payload), PacketFraming.TwoByteBigEndianLength => EncodeTwoByteFrame(packetId, payload, uncompressedSize),
PacketFraming.FourByteLittleEndianLength => EncodeFourByteFrame(packetId, payload), PacketFraming.FourByteLittleEndianLength => EncodeFourByteFrame(packetId, payload),
_ => EncodeFourByteFrame(packetId, payload) _ => EncodeFourByteFrame(packetId, payload)
}; };
@@ -220,8 +221,20 @@ namespace MikuSB.TcpSharp
return payload; return payload;
} }
private byte[] EncodeTwoByteFrame(ushort packetId, byte[] payload) private const int CompressionThreshold = 60000;
private static byte[] ZlibCompress(byte[] data)
{ {
using var ms = new MemoryStream();
using (var zlib = new ZLibStream(ms, CompressionLevel.Optimal, leaveOpen: true))
zlib.Write(data, 0, data.Length);
return ms.ToArray();
}
private byte[] EncodeTwoByteFrame(ushort packetId, byte[] payload, int uncompressedSize = 0)
{
if (payload.Length > CompressionThreshold)
payload = ZlibCompress(payload);
var wrappedPayload = WrapPayload(payload); var wrappedPayload = WrapPayload(payload);
var buffer = new byte[HeaderSize4Byte + wrappedPayload.Length]; var buffer = new byte[HeaderSize4Byte + wrappedPayload.Length];
@@ -246,7 +259,9 @@ namespace MikuSB.TcpSharp
const int wrapperHeaderSize = 35; const int wrapperHeaderSize = 35;
var wrapped = new byte[wrapperHeaderSize + payload.Length]; var wrapped = new byte[wrapperHeaderSize + payload.Length];
BinaryPrimitives.WriteUInt16LittleEndian(wrapped.AsSpan(6, 2), (ushort)payload.Length); BinaryPrimitives.WriteUInt16LittleEndian(wrapped.AsSpan(6, 2), (ushort)payload.Length);
wrapped[11] = 1; if (payload.Length >= 2 && payload[0] == 0x78 &&
(payload[1] == 0x01 || payload[1] == 0x5E || payload[1] == 0x9C || payload[1] == 0xDA))
wrapped[10] = 2;
payload.CopyTo(wrapped.AsSpan(wrapperHeaderSize)); payload.CopyTo(wrapped.AsSpan(wrapperHeaderSize));
return wrapped; return wrapped;

View File

@@ -1 +1 @@
v=2.6 v=2.9