Compare commits

..

74 Commits

Author SHA1 Message Date
Kei-Luna
a555dd2930 Update version.txt 2026-05-13 08:50:25 +09:00
Kei-Luna
85605a786c todo 2026-05-13 08:50:08 +09:00
Kei-Luna
f3d6ff3873 README 2026-05-13 08:15:40 +09:00
Kei-Luna
41df375e21 Changed to use a separate account for each email address. 2026-05-13 08:15:33 +09:00
Kei-Luna
5332d5fe1a Change certificate generation 2026-05-13 07:39:42 +09:00
Kei-Luna
da61f1e929 Small fix 2026-05-13 07:22:46 +09:00
Kei-Luna
d9b16fb55d MikuSB.Loader 2026-05-13 07:19:45 +09:00
Kei-Luna
e92b214161 Completely offline (Thank you cs8425) 2026-05-13 06:36:26 +09:00
Naruse
6740b8ecf7 unlock some furniture (not all items implemented yet)
special thanks to undownding for the logic
2026-05-12 16:03:38 +08:00
Naruse
233419eba3 Update LanguageCHS.cs 2026-05-11 19:31:02 +08:00
Naruse
a7a68418db add girl skin command 2026-05-11 19:29:32 +08:00
Naruse
f053359c41 add manifestation command 2026-05-11 19:21:54 +08:00
Naruse
0cf633a0e1 auto add girl to room & favor to lvl 500 2026-05-11 19:16:19 +08:00
Naruse
e4397f10ee add tutorial guide 2026-05-11 17:11:47 +08:00
Kei-Luna
bdb4ee3d51 Implement saving settings 2026-05-09 20:03:28 +09:00
Kei-Luna
7507d10072 Updater fixes due to changes in resource folders 2026-05-09 17:39:17 +09:00
Kei-Luna
8a7a919d04 Changed the Resources folder structure to match the official one. 2026-05-09 17:30:25 +09:00
Kei-Luna
2d1079a80d I rewrote House_Request based on the code provided by undownding. 2026-05-05 07:35:34 +09:00
Kei-Luna
677e4027b9 Update version.txt 2026-05-03 13:50:50 +09:00
Kei-Luna
4dc28ae423 more House_Func 2026-05-03 13:42:09 +09:00
Kei-Luna
1fce4ca2e6 Update version.txt 2026-05-03 09:55:55 +09:00
Kei-Luna
93c59e5730 Arcade Game can play 2026-05-03 09:54:46 +09:00
Kei-Luna
f7814b0537 Adds a dailybuff to Paradox. 2026-05-02 09:46:47 +09:00
Kei-Luna
2fdda3157f Unlock all of Paradox's talents 2026-05-02 09:13:04 +09:00
Kei-Luna
f07c5f77fd Unlock all difficulty levels of Paradox 2026-05-02 08:42:57 +09:00
Naruse
2883ac3d41 add weapon parts 2026-04-30 17:18:23 +08:00
Kei-Luna
49f4fdfda5 Update ConfigContainer.cs 2026-04-30 15:46:03 +09:00
Kei-Luna
bcc5716f97 Fix for the function to save to the database. 2026-04-30 15:46:00 +09:00
Kei-Luna
4bd66621bf Update ProxyServer.cs 2026-04-30 10:02:08 +09:00
Kei-Luna
14a611bf8e readme 2026-04-30 06:08:53 +09:00
Kei-Luna
283358f1ec Update version.txt 2026-04-30 06:01:05 +09:00
Kei-Luna
5d8b3f2bad Merge pull request #6 from cs8425/work-for-linux
fix some bug, upgrade to dotnet 10 and add docs for linux
2026-04-30 05:43:01 +09:00
cs8425
a992bc86c5 Merge remote-tracking branch 'origin/main' into work-for-linux 2026-04-29 22:38:36 +08:00
cs8425
d97eae6f5b update README_linux.md 2026-04-29 22:38:13 +08:00
cs8425
04121eb357 try workflow build for linux 2026-04-29 22:34:14 +08:00
cs8425
391879a483 bump github workflow to dotnet 10 2026-04-29 20:28:11 +08:00
cs8425
8e3323dd94 fix pem format not export when first create CA cert 2026-04-29 19:34:25 +08:00
cs8425
7e83d2ccd2 fix racing between AutoSave and process exit 2026-04-29 19:30:57 +08:00
Naruse
408113963b Merge branch 'main' of https://github.com/MikuLeaks/MikuSB 2026-04-29 16:00:34 +08:00
Naruse
fcf0bf0843 add call item 2026-04-29 16:00:10 +08:00
Kei-Luna
2bf7554c53 Unlock GM menu 2026-04-29 16:57:11 +09:00
Naruse
50121d619b add skin part 2026-04-29 15:42:25 +08:00
Naruse
e8bb77e90e logistic index 2026-04-29 13:04:17 +08:00
Naruse
5f173ce8d2 add badge 2026-04-29 12:41:45 +08:00
Naruse
9b8fa5d7c8 add profile item 2026-04-29 11:02:11 +08:00
Naruse
1969c1ec89 use enum for item slots 2026-04-29 09:46:21 +08:00
Naruse
e32319aa50 fix black screen after clicking love icon 2026-04-29 09:22:35 +08:00
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
Kei-Luna
720f56c708 fix 2026-04-29 06:59:22 +09:00
Kei-Luna
5fa42bdb23 Merge pull request #2 from ahasasjeb/main 2026-04-29 06:34:49 +09:00
cs8425
5f3fc0c746 add docs for running on Linux 2026-04-29 02:53:08 +08:00
cs8425
2fe795d29a also export CA cert in pem format 2026-04-29 02:52:48 +08:00
Naruse
5af23a8113 unlock more stage for operation 2026-04-29 00:47:21 +08:00
Naruse
ee79486748 v1.2 2026-04-29 00:46:44 +08:00
Naruse
0cf0e3beb4 fix weapon break limit 2026-04-29 00:36:17 +08:00
Naruse
8d6e0d7638 add weapon skin & command for it 2026-04-28 23:48:14 +08:00
cs8425
b256fe7b01 fix proxy log and proxy https connection always return 404 2026-04-28 23:16:13 +08:00
Naruse
5f0de1a9f0 add support card giveall command
NOTE: need to delete old database because im moving support card to different column
2026-04-28 23:08:23 +08:00
cs8425
e93dadafe8 fix infinite loop for auto SaveDatabase 2026-04-28 22:49:58 +08:00
Naruse
069ee6aa2a add girl neuronic command 2026-04-28 21:45:32 +08:00
cs8425
34b93ad55d fix infinite loop when pressed ctrl+c 2026-04-28 21:09:14 +08:00
cs8425
3eca16a661 upgrade to dotnet 10 2026-04-28 20:19:17 +08:00
Kei-Luna
64f977173f Update README.md 2026-04-28 18:03:34 +09:00
Kei-Luna
0e62fcc9b2 v 1.1 2026-04-28 16:29:57 +09:00
Kei-Luna
f4ad74e00d Keep username changes 2026-04-28 16:12:27 +09:00
Kei-Luna
ccdfbee828 Implemented a level-up for SupporterCards. 2026-04-28 16:12:02 +09:00
Kei-Luna
5d0f587fb9 Support cards can now be equipped. 2026-04-28 15:37:44 +09:00
Kei-Luna
94f972ff82 add all support card 2026-04-28 15:23:42 +09:00
lvjia
2f8d0c6afc Fix debug command i18n keys 2026-04-28 14:05:32 +08:00
lvjia
f6204134a9 /debug Multiple languages
Co-authored-by: Copilot <copilot@github.com>
2026-04-28 13:09:34 +08:00
lvjia
57ce0e183b Rewrite girl skin type handling 2026-04-28 12:47:05 +08:00
121 changed files with 5044 additions and 862 deletions

View File

@@ -9,6 +9,9 @@ on:
permissions: permissions:
contents: write contents: write
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
jobs: jobs:
build-release: build-release:
runs-on: windows-latest runs-on: windows-latest
@@ -18,7 +21,7 @@ jobs:
- uses: actions/setup-dotnet@v4 - uses: actions/setup-dotnet@v4
with: with:
dotnet-version: 9.0.x dotnet-version: 10.0.x
- name: Read version - name: Read version
id: version id: version
@@ -54,3 +57,55 @@ jobs:
files: | files: |
artifacts/MikuSB-win-x64.zip artifacts/MikuSB-win-x64.zip
artifacts/MikuSB-win-x64.zip.sha256 artifacts/MikuSB-win-x64.zip.sha256
build-release-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Publish server
run: |
dotnet publish ./MikuSB/MikuSB.csproj -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true --property:PublishDir=../artifacts/dist
- name: Read version
id: version
shell: bash
run: |
line=$(head -n 1 version.txt | xargs)
VERSION=${line#v=}
SHORT_HASH="$(git rev-parse --short=7 HEAD)"
if [ -z "$VERSION" ]; then
echo "version.txt is empty."
VERSION=${SHORT_HASH}
fi
if [[ "${{ env.BRANCH_NAME }}" == "main" ]]; then
echo "tag=v${VERSION}" >> $GITHUB_OUTPUT
else
SAFE_NAME=$(echo "${{ env.BRANCH_NAME }}" | tr '/' '-')
echo "tag=${SAFE_NAME}-v${VERSION}-${SHORT_HASH}" >> $GITHUB_OUTPUT
fi
- name: Assemble release package
run: |
packageDir="./artifacts/dist/"
zipFile="MikuSB-linux-x64.zip"
cp ./version.txt ${packageDir}
pushd ${packageDir}
zip -r ../${zipFile} .
popd
sha256sum ./artifacts/${zipFile} > ./artifacts/${zipFile}.sha256
- name: Create release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.tag }}
name: ${{ steps.version.outputs.tag }}
generate_release_notes: true
files: |
./artifacts/MikuSB-linux-x64.zip
./artifacts/MikuSB-linux-x64.zip.sha256

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<CETCompat>false</CETCompat> <CETCompat>false</CETCompat>
@@ -21,7 +21,6 @@
<PackageReference Include="SQLitePCLRaw.provider.e_sqlite3" Version="2.1.10" /> <PackageReference Include="SQLitePCLRaw.provider.e_sqlite3" Version="2.1.10" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.172" /> <PackageReference Include="SqlSugarCore" Version="5.1.4.172" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
<PackageReference Include="System.IO.Pipelines" Version="9.0.0" />
<PackageReference Include="System.Management" Version="9.0.0" /> <PackageReference Include="System.Management" Version="9.0.0" />
</ItemGroup> </ItemGroup>

View File

@@ -9,6 +9,7 @@ public class ConfigContainer
public PathConfig Path { get; set; } = new(); public PathConfig Path { get; set; } = new();
public ServerOption ServerOption { get; set; } = new(); public ServerOption ServerOption { get; set; } = new();
public ProxyOptions Proxy { get; set; } = new(); public ProxyOptions Proxy { get; set; } = new();
public LoaderOptions Loader { get; set; } = new();
} }
public class HttpServerConfig public class HttpServerConfig
@@ -60,6 +61,7 @@ public class ServerOption
public string FallbackLanguage { get; set; } = "EN"; public string FallbackLanguage { get; set; } = "EN";
public string[] DefaultPermissions { get; set; } = ["Admin"]; public string[] DefaultPermissions { get; set; } = ["Admin"];
public ServerProfile ServerProfile { get; set; } = new(); public ServerProfile ServerProfile { get; set; } = new();
public bool EnableGmMenu { get; set; } = false;
public bool AutoCreateUser { get; set; } = true; public bool AutoCreateUser { get; set; } = true;
public bool SavePersonalDebugFile { get; set; } = false; public bool SavePersonalDebugFile { get; set; } = false;
public bool AutoSendResponseWhenNoHandler { get; set; } = true; public bool AutoSendResponseWhenNoHandler { get; set; } = true;
@@ -85,10 +87,13 @@ public class ServerProfile
public class ProxyOptions public class ProxyOptions
{ {
public bool Enabled { get; set; } = true; public bool Enabled { get; set; } = true;
public int Port { get; set; } = 8888; public int Port { get; set; } = 18888;
public int ServerHttpPort { get; set; } = 21500; }
public bool InstallRootCertificate { get; set; } = true;
public bool ManageSystemProxy { get; set; } = true; public class LoaderOptions
public bool RestoreSystemProxyOnStop { get; set; } = true; {
public string ProxyOverride { get; set; } = "localhost;127.*;10.*;192.168.*;<local>"; public string GamePath { get; set; } = "";
public string[] PatchPaths { get; set; } = [@"Patch\MikuSB-Patch.dll"];
public string[] Arguments { get; set; } = ["-FeatureLevelES31", "-channelid=seasun", "-NoSplash"];
public bool SetAllProxy { get; set; } = true;
} }

View File

@@ -0,0 +1,24 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Config;
public class StringToUIntConverter : JsonConverter<uint>
{
public override uint ReadJson(JsonReader reader, Type objectType, uint existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.Value == null)
return 0;
var value = reader.Value.ToString();
if (string.IsNullOrWhiteSpace(value))
return 0;
return uint.TryParse(value, out var result) ? result : 0;
}
public override void WriteJson(JsonWriter writer, uint value, JsonSerializer serializer)
{
writer.WriteValue(value);
}
}

View File

@@ -1,6 +1,6 @@
namespace MikuSB.Data.Excel; namespace MikuSB.Data.Excel;
[ResourceEntity("ar_item.json")] [ResourceEntity("item/templates/ar_item.json")]
public class ArItemExcel : ExcelResource public class ArItemExcel : ExcelResource
{ {
public uint Genre { get; set; } public uint Genre { get; set; }

View File

@@ -0,0 +1,34 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("item/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

@@ -1,6 +1,6 @@
namespace MikuSB.Data.Excel; namespace MikuSB.Data.Excel;
[ResourceEntity("break_level_limit.json")] [ResourceEntity("item/break_level_limit.json")]
public class BreakLevelLimitExcel : ExcelResource public class BreakLevelLimitExcel : ExcelResource
{ {
public int ID { get; set; } public int ID { get; set; }

View File

@@ -0,0 +1,24 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace MikuSB.Data.Excel;
[ResourceEntity("item/templates/call_item.json")]
public class CallItemExcel : ExcelResource
{
public uint Genre { get; set; }
public uint Detail { get; set; }
public uint Particular { get; set; }
public uint Level { get; set; }
public string I18n { get; set; } = "";
public override uint GetId()
{
return (uint)I18n.GetHashCode();
}
public override void Loaded()
{
GameData.CallItemData.Add(GetId(), this);
}
}

View File

@@ -2,7 +2,7 @@
namespace MikuSB.Data.Excel; namespace MikuSB.Data.Excel;
[ResourceEntity("card.json")] [ResourceEntity("item/templates/card.json")]
public class CardExcel : ExcelResource public class CardExcel : ExcelResource
{ {
public uint Genre { get; set; } public uint Genre { get; set; }

View File

@@ -2,7 +2,7 @@
namespace MikuSB.Data.Excel; namespace MikuSB.Data.Excel;
[ResourceEntity("card_skin.json")] [ResourceEntity("item/templates/card_skin.json")]
public class CardSkinExcel : ExcelResource public class CardSkinExcel : ExcelResource
{ {
public uint Genre { get; set; } public uint Genre { get; set; }

View File

@@ -0,0 +1,27 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("item/templates/card_skin_parts.json")]
public class CardSkinPartsExcel : ExcelResource
{
public uint Genre { get; set; }
public uint Detail { get; set; }
public uint Particular { get; set; }
public uint Level { get; set; }
public uint Icon { get; set; }
public int AppearID { get; set; }
[JsonProperty("profile")] public List<List<int>> CardSkinID { get; set; } = [];
public string I18n { get; set; } = "";
[JsonIgnore] public ulong TemplateId { get; set; }
public override uint GetId()
{
return (uint)I18n.GetHashCode();
}
public override void Loaded()
{
TemplateId = GameResourceTemplateId.FromGdpl(Genre, Detail, Particular, Level);
GameData.CardSkinPartsData.Add(Icon, this);
}
}

View File

@@ -1,6 +1,6 @@
namespace MikuSB.Data.Excel; namespace MikuSB.Data.Excel;
[ResourceEntity("level.json")] [ResourceEntity("chapter/level.json")]
public class ChapterLevelExcel : ExcelResource public class ChapterLevelExcel : ExcelResource
{ {
public uint ID { get; set; } public uint ID { get; set; }

View File

@@ -0,0 +1,14 @@
namespace MikuSB.Data.Excel;
[ResourceEntity("daily/level.json")]
public class DailyLevelExcel : ExcelResource
{
public uint ID { get; set; }
public override uint GetId() => ID;
public override void Loaded()
{
GameData.DailyLevelData.Add(ID, this);
}
}

View File

@@ -0,0 +1,21 @@
namespace MikuSB.Data.Excel;
[ResourceEntity("item/templates/dorm_gift.json")]
public class DormGiftExcel : ExcelResource
{
public uint Genre { get; set; }
public uint Detail { get; set; }
public uint Particular { get; set; }
public uint Level { get; set; }
public string I18n { get; set; } = "";
public override uint GetId()
{
return (Genre << 24) | (Detail << 16) | (Particular << 8) | Level;
}
public override void Loaded()
{
GameData.DormGiftData.Add(GetId(), this);
}
}

View File

@@ -0,0 +1,21 @@
using MikuSB.Data.Config;
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("guide/guide.json")]
public class GuideExcel : ExcelResource
{
public uint ID { get; set; }
[JsonConverter(typeof(StringToUIntConverter))] public uint Group { get; set; }
public override uint GetId()
{
return (ID << 48) | (Group << 32);
}
public override void Loaded()
{
GameData.GuideData.TryAdd(GetId(), this);
}
}

View File

@@ -2,7 +2,7 @@
namespace MikuSB.Data.Excel; namespace MikuSB.Data.Excel;
[ResourceEntity("manifestation.json")] [ResourceEntity("item/templates/manifestation.json")]
public class ManifestationExcel : ExcelResource public class ManifestationExcel : ExcelResource
{ {
public uint Genre { get; set; } public uint Genre { get; set; }

View File

@@ -1,9 +1,9 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace MikuSB.Data.Excel; namespace MikuSB.Data.Excel;
// nodecondition.json: NodeConditionId → NodeXCost per sub-node (1-9) // nodecondition.json: NodeConditionId → NodeXCost per sub-node (1-9)
[ResourceEntity("nodecondition.json")] [ResourceEntity("item/skill/nodecondition.json")]
public class NodeConditionExcel : ExcelResource public class NodeConditionExcel : ExcelResource
{ {
[JsonProperty("ID")] public uint Id { get; set; } [JsonProperty("ID")] public uint Id { get; set; }

View File

@@ -0,0 +1,22 @@
namespace MikuSB.Data.Excel;
[ResourceEntity("item/templates/profile.json")]
public class ProfileExcel : ExcelResource
{
public uint Genre { get; set; }
public uint Detail { get; set; }
public uint Particular { get; set; }
public uint Level { get; set; }
public string I18n { get; set; } = "";
public string LuaType { get; set; } = "";
public override uint GetId()
{
return (uint)I18n.GetHashCode();
}
public override void Loaded()
{
GameData.ProfileData.Add(GetId(), this);
}
}

View File

@@ -1,8 +1,8 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace MikuSB.Data.Excel; namespace MikuSB.Data.Excel;
[ResourceEntity("recycle.json")] [ResourceEntity("item/recycle.json")]
public class RecycleExcel : ExcelResource public class RecycleExcel : ExcelResource
{ {
public int ID { get; set; } public int ID { get; set; }

View File

@@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("dlc/rogue3d/dailybuff.json")]
public class Rogue3DDailyBuffExcel : ExcelResource
{
[JsonProperty("ID")] public uint Id { get; set; }
[JsonProperty("GroupID")] public uint GroupId { get; set; }
[JsonProperty("ScoreBuffID")] public uint ScoreBuffId { get; set; }
public override uint GetId() => Id;
public override void Loaded()
{
GameData.Rogue3DDailyBuffData[Id] = this;
}
}

View File

@@ -1,8 +1,8 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace MikuSB.Data.Excel; namespace MikuSB.Data.Excel;
[ResourceEntity("server_01_difficult.json")] [ResourceEntity("dlc/rogue3d/server_01_difficult.json")]
public class Rogue3DDifficultExcel : ExcelResource public class Rogue3DDifficultExcel : ExcelResource
{ {
[JsonProperty("DifficultID")] public uint DifficultId { get; set; } [JsonProperty("DifficultID")] public uint DifficultId { get; set; }

View File

@@ -0,0 +1,19 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("dlc/rogue3d/server_10_season.json")]
public class Rogue3DSeasonExcel : ExcelResource
{
[JsonProperty("SeasonID")] public uint SeasonId { get; set; }
[JsonProperty("Type")] public int Type { get; set; }
[JsonProperty("OpenTime")] public string OpenTime { get; set; } = "";
[JsonProperty("CloseTime")] public string CloseTime { get; set; } = "";
public override uint GetId() => SeasonId;
public override void Loaded()
{
GameData.Rogue3DSeasonData[SeasonId] = this;
}
}

View File

@@ -0,0 +1,31 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("dlc/rogue3d/server_03_talent.json")]
public class Rogue3DTalentExcel : ExcelResource
{
[JsonProperty("TalentID")] public uint TalentId { get; set; }
[JsonProperty("UnlockCondition")] private object? UnlockConditionRaw { get; set; }
[JsonIgnore] public uint UnlockCondition { get; private set; }
public override uint GetId() => TalentId;
public override void Loaded()
{
UnlockCondition = ParseUnlockCondition(UnlockConditionRaw);
GameData.Rogue3DTalentData[TalentId] = this;
}
private static uint ParseUnlockCondition(object? raw)
{
return raw switch
{
null => 0,
long value when value > 0 => (uint)value,
int value when value > 0 => (uint)value,
string text when uint.TryParse(text, out var value) => value,
_ => 0
};
}
}

View File

@@ -1,9 +1,9 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace MikuSB.Data.Excel; namespace MikuSB.Data.Excel;
// spine.json: SpineId → Node{i}Req (nodecondition ID per master node index) // spine.json: SpineId → Node{i}Req (nodecondition ID per master node index)
[ResourceEntity("spine.json")] [ResourceEntity("item/skill/spine.json")]
public class SpineExcel : ExcelResource public class SpineExcel : ExcelResource
{ {
[JsonProperty("ID")] public uint Id { get; set; } [JsonProperty("ID")] public uint Id { get; set; }

View File

@@ -1,9 +1,9 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace MikuSB.Data.Excel; namespace MikuSB.Data.Excel;
[ResourceEntity("suplies.json")] [ResourceEntity("item/templates/suplies.json")]
public class SuppliesExcel : ExcelResource public class SuppliesExcel : ExcelResource
{ {
public uint Genre { get; set; } public uint Genre { get; set; }

View File

@@ -0,0 +1,32 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("item/templates/support_card.json")]
public class SupportCardExcel : ExcelResource
{
public uint Genre { get; set; }
public uint Detail { get; set; }
public uint Particular { get; set; }
public uint Level { get; set; }
public uint Icon { get; set; }
public uint ProvideExp { get; set; }
[JsonProperty("LevelLimitID")] public int LevelLimitId { get; set; }
public uint MaxLevel => LevelLimitId switch
{
1007 => 10,
1008 => 13,
1009 => 16,
_ => 10
};
public ulong TemplateId => GameResourceTemplateId.FromGdpl(Genre, Detail, Particular, Level);
public override uint GetId() => Icon;
public override void Loaded()
{
GameData.SupportCardData.Add(this);
}
}

View File

@@ -1,9 +1,9 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace MikuSB.Data.Excel; namespace MikuSB.Data.Excel;
[ResourceEntity("upgrade_exp.json")] [ResourceEntity("item/upgrade_exp.json")]
public class UpgradeExpExcel : ExcelResource public class UpgradeExpExcel : ExcelResource
{ {
public int Lv { get; set; } public int Lv { get; set; }

View File

@@ -1,9 +1,9 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace MikuSB.Data.Excel; namespace MikuSB.Data.Excel;
[ResourceEntity("weapon.json")] [ResourceEntity("item/templates/weapon.json")]
public class WeaponExcel : ExcelResource public class WeaponExcel : ExcelResource
{ {
public uint Genre { get; set; } public uint Genre { get; set; }

View File

@@ -0,0 +1,24 @@
using Newtonsoft.Json;
namespace MikuSB.Data.Excel;
[ResourceEntity("item/templates/weapon_parts.json")]
public class WeaponPartsExcel : ExcelResource
{
public uint Genre { get; set; }
public uint Detail { get; set; }
public uint Particular { get; set; }
public uint Level { get; set; }
public uint Icon { get; set; }
public int AppearID { get; set; }
public string I18n { get; set; } = "";
public override uint GetId()
{
return (uint)I18n.GetHashCode();
}
public override void Loaded()
{
GameData.WeaponPartsData.Add(GetId(), this);
}
}

View File

@@ -0,0 +1,24 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace MikuSB.Data.Excel;
[ResourceEntity("item/templates/weapon_skin.json")]
public class WeaponSkinExcel : ExcelResource
{
public uint Genre { get; set; }
public uint Detail { get; set; }
public uint Particular { get; set; }
public uint Level { get; set; }
public string I18n { get; set; } = "";
public override uint GetId()
{
return (uint)I18n.GetHashCode();
}
public override void Loaded()
{
GameData.WeaponSkinData.Add(GetId(), this);
}
}

View File

@@ -16,8 +16,21 @@ public static class GameData
public static Dictionary<uint, ArItemExcel> ArItemData { get; private set; } = []; public static Dictionary<uint, ArItemExcel> ArItemData { get; private set; } = [];
public static Dictionary<uint, ManifestationExcel> ManifestationData { get; private set; } = []; public static Dictionary<uint, ManifestationExcel> ManifestationData { get; private set; } = [];
public static Dictionary<uint, Rogue3DDifficultExcel> Rogue3DDifficultData { get; private set; } = []; public static Dictionary<uint, Rogue3DDifficultExcel> Rogue3DDifficultData { get; private set; } = [];
public static Dictionary<uint, Rogue3DSeasonExcel> Rogue3DSeasonData { get; private set; } = [];
public static Dictionary<uint, Rogue3DTalentExcel> Rogue3DTalentData { get; private set; } = [];
public static Dictionary<uint, Rogue3DDailyBuffExcel> Rogue3DDailyBuffData { 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, SpineExcel> SpineData { get; private set; } = [];
public static Dictionary<uint, NodeConditionExcel> NodeConditionData { get; private set; } = []; public static Dictionary<uint, NodeConditionExcel> NodeConditionData { get; private set; } = [];
public static List<SupportCardExcel> SupportCardData { get; private set; } = [];
public static Dictionary<uint, WeaponSkinExcel> WeaponSkinData { get; private set; } = [];
public static Dictionary<uint, DailyLevelExcel> DailyLevelData { get; private set; } = [];
public static Dictionary<uint, ProfileExcel> ProfileData { get; private set; } = [];
public static Dictionary<uint, CardSkinPartsExcel> CardSkinPartsData { get; private set; } = [];
public static Dictionary<uint, CallItemExcel> CallItemData { 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, DormGiftExcel> DormGiftData { get; private set; } = [];
} }
public static class GameResourceTemplateId public static class GameResourceTemplateId

View File

@@ -46,7 +46,7 @@ public class ResourceManager
foreach (var fileName in attribute.FileName) foreach (var fileName in attribute.FileName)
try try
{ {
var path = ConfigManager.Config.Path.ResourcePath + "/ExcelOutput/" + fileName; var path = ConfigManager.Config.Path.ResourcePath + "/" + fileName;
var file = new FileInfo(path); var file = new FileInfo(path);
if (!file.Exists) if (!file.Exists)
{ {

View File

@@ -26,11 +26,14 @@ public class AccountData : BaseDatabaseDataHelper
AccountData? result = null; AccountData? result = null;
DatabaseHelper.GetAllInstance<AccountData>()?.ForEach(account => DatabaseHelper.GetAllInstance<AccountData>()?.ForEach(account =>
{ {
if (account.Username == username) result = account; if (string.Equals(account.Username, username, StringComparison.OrdinalIgnoreCase)) result = account;
}); });
return result; return result;
} }
public static AccountData? GetAccountByEmail(string email)
=> GetAccountByUserName(email);
public static AccountData? GetAccountByUid(int uid, bool force = false) public static AccountData? GetAccountByUid(int uid, bool force = false)
{ {
var result = DatabaseHelper.GetInstance<AccountData>(uid, force); var result = DatabaseHelper.GetInstance<AccountData>(uid, force);
@@ -61,8 +64,15 @@ public class AccountData : BaseDatabaseDataHelper
#region Account #region Account
public static void CreateAccount(string username, int uid, string password) public static AccountData CreateAccount(string username, int uid, string password)
{ {
if (string.IsNullOrWhiteSpace(username))
throw new ArgumentException("Username cannot be empty.", nameof(username));
if (GetAccountByUserName(username) != null)
throw new InvalidOperationException($"Account '{username}' already exists.");
if (uid != 0 && GetAccountByUid(uid) != null)
throw new InvalidOperationException($"UID '{uid}' is already in use.");
var newUid = uid; var newUid = uid;
if (uid == 0) if (uid == 0)
{ {
@@ -84,6 +94,7 @@ public class AccountData : BaseDatabaseDataHelper
SetPassword(account, password); SetPassword(account, password);
DatabaseHelper.CreateInstance(account); DatabaseHelper.CreateInstance(account);
return account;
} }
public static void DeleteAccount(int uid) public static void DeleteAccount(int uid)
@@ -178,4 +189,4 @@ public class AccountData : BaseDatabaseDataHelper
} }
#endregion #endregion
} }

View File

@@ -23,11 +23,14 @@ public class CharacterInfo
public int Trust { get; set; } public int Trust { get; set; }
public uint WeaponUniqueId { get; set; } public uint WeaponUniqueId { get; set; }
public uint SkinId { get; set; } public uint SkinId { get; set; }
public uint WeaponSkinId { get; set; }
public uint SupportTeamIndex { get; set; } = 1;
public ItemFlagEnum Flag { get; set; } = ItemFlagEnum.FLAG_READED; public ItemFlagEnum Flag { get; set; } = ItemFlagEnum.FLAG_READED;
public uint Expiration { get; set; } public uint Expiration { get; set; }
[SugarColumn(IsJson = true)] public List<uint> UnlockedSkin { get; set; } = []; [SugarColumn(IsJson = true)] public List<uint> UnlockedSkin { get; set; } = [];
[SugarColumn(IsJson = true)] public List<uint> Spines { get; set; } = []; [SugarColumn(IsJson = true)] public List<uint> Spines { get; set; } = [];
[SugarColumn(IsJson = true)] public List<uint> Affixs { get; set; } = []; [SugarColumn(IsJson = true)] public List<uint> Affixs { get; set; } = [];
[SugarColumn(IsJson = true)] public Dictionary<uint, uint> SupportSlots { get; set; } = []; // Key = EqSlot (= support card Detail), Value = support card UniqueId
public long Timestamp { get; set; } public long Timestamp { get; set; }
public uint Count { get; set; } = 1; public uint Count { get; set; } = 1;
@@ -53,8 +56,12 @@ public class CharacterInfo
proto.Enhance.Spines.AddRange(Spines.Select(x => (ulong)x)); proto.Enhance.Spines.AddRange(Spines.Select(x => (ulong)x));
proto.Enhance.Affixs.AddRange(Affixs); proto.Enhance.Affixs.AddRange(Affixs);
proto.Slots[4] = WeaponUniqueId; proto.Slots[(uint)ItemCardSlotTypeEnum.SLOT_WEAPON] = WeaponUniqueId;
proto.Slots[5] = SkinId; proto.Slots[(uint)ItemCardSlotTypeEnum.SLOT_SKIN] = SkinId;
proto.Slots[(uint)ItemCardSlotTypeEnum.SLOT_WEAPON_SKIN] = WeaponSkinId;
proto.Slots[(uint)ItemCardSlotTypeEnum.SLOT_SUPPORTERINDEX] = SupportTeamIndex;
foreach (var (slot, uid) in SupportSlots)
proto.Slots[slot] = uid;
return proto; return proto;
} }

View File

@@ -14,7 +14,10 @@ public class DatabaseHelper
public static readonly ConcurrentDictionary<int, List<BaseDatabaseDataHelper>> UidInstanceMap = []; public static readonly ConcurrentDictionary<int, List<BaseDatabaseDataHelper>> UidInstanceMap = [];
public static readonly List<int> ToSaveUidList = []; public static readonly List<int> ToSaveUidList = [];
public static long LastSaveTick = DateTime.UtcNow.Ticks; public static long LastSaveTick = DateTime.UtcNow.Ticks;
public static Thread? SaveThread;
private static int _saving = 0;
private static Task? _saveTask;
private static CancellationTokenSource? _cts;
public static bool LoadAccount; public static bool LoadAccount;
public static bool LoadAllData; public static bool LoadAllData;
@@ -77,15 +80,11 @@ public class DatabaseHelper
while (!res.IsCompleted) while (!res.IsCompleted)
{ {
Thread.Sleep(100);
} }
LastSaveTick = DateTime.UtcNow.Ticks; _cts = new CancellationTokenSource();
_saveTask = RunAutoSave(_cts.Token);
SaveThread = new Thread(() =>
{
while (true) CalcSaveDatabase();
});
SaveThread.Start();
LoadAllData = true; LoadAllData = true;
} }
@@ -248,6 +247,35 @@ public class DatabaseHelper
ToSaveUidList.RemoveAll(x => x == key); ToSaveUidList.RemoveAll(x => x == key);
} }
public static void Stop()
{
_cts?.Cancel();
}
public static async Task WaitAsync()
{
if (_saveTask != null)
await _saveTask;
}
private static async Task RunAutoSave(CancellationToken token)
{
LastSaveTick = DateTime.UtcNow.Ticks;
try
{
while (!token.IsCancellationRequested)
{
CalcSaveDatabase();
await Task.Delay(100, token);
}
}
catch (OperationCanceledException)
{
// exit normally
// Console.WriteLine($"RunAutoSave exit! - OperationCanceledException");
}
}
// Auto save per 5 min // Auto save per 5 min
public static void CalcSaveDatabase() public static void CalcSaveDatabase()
{ {
@@ -257,6 +285,10 @@ public class DatabaseHelper
public static void SaveDatabase() public static void SaveDatabase()
{ {
// ensure only one SaveDatabase() runnig
if (Interlocked.Exchange(ref _saving, 1) == 1)
return;
try try
{ {
var prev = DateTime.Now; var prev = DateTime.Now;
@@ -281,11 +313,18 @@ public class DatabaseHelper
Math.Round(t, 2).ToString(CultureInfo.InvariantCulture))); Math.Round(t, 2).ToString(CultureInfo.InvariantCulture)));
ToSaveUidList.Clear(); ToSaveUidList.Clear();
// Thread.Sleep(5000); // for test if saving process taking too long
} }
catch (Exception e) catch (Exception e)
{ {
logger.Error("An error occurred while saving the database", e); logger.Error("An error occurred while saving the database", e);
} }
finally
{
// release lock
Volatile.Write(ref _saving, 0);
}
LastSaveTick = DateTime.UtcNow.Ticks; LastSaveTick = DateTime.UtcNow.Ticks;
} }

View File

@@ -17,6 +17,12 @@ public class InventoryData : BaseDatabaseDataHelper
[SugarColumn(IsJson = true)] [SugarColumn(IsJson = true)]
public Dictionary<uint, GameSkinInfo> Skins { get; set; } = []; // Key: UniqueId public Dictionary<uint, GameSkinInfo> Skins { get; set; } = []; // Key: UniqueId
[SugarColumn(IsJson = true)]
public Dictionary<uint, GameSupportCardInfo> SupportCards { get; set; } = []; // Key: UniqueId
[SugarColumn(IsJson = true)]
public Dictionary<uint, uint> SkinTypesBySkinId { get; set; } = []; // Key: nSkinId, Value: client nType
} }
public class BaseGameItemInfo public class BaseGameItemInfo
@@ -26,30 +32,37 @@ public class BaseGameItemInfo
public uint ItemCount { get; set; } public uint ItemCount { get; set; }
public ItemTypeEnum ItemType { get; set; } public ItemTypeEnum ItemType { get; set; }
public ItemFlagEnum Flag { get; set; } = ItemFlagEnum.FLAG_READED; public ItemFlagEnum Flag { get; set; } = ItemFlagEnum.FLAG_READED;
public uint Level { get; set; }
public uint Exp { get; set; }
public virtual Item ToProto() public virtual Item ToProto()
{ {
return new Item var proto = new Item
{ {
Id = UniqueId, Id = UniqueId,
Template = TemplateId, Template = TemplateId,
Count = ItemCount, Count = ItemCount,
Flag = (uint)Flag Flag = (uint)Flag
}; };
if (Level > 0 || Exp > 0)
proto.Enhance = new Enhance { Level = Level, Exp = Exp };
return proto;
} }
} }
public abstract class GrowableItemInfo : BaseGameItemInfo public abstract class GrowableItemInfo : BaseGameItemInfo
{ {
public bool IsLocked { get; set; } public bool IsLocked { get; set; }
public uint Level { get; set; } public new uint Level { get; set; }
public uint Exp { get; set; } public new uint Exp { get; set; }
public uint Break { get; set; } public uint Break { get; set; }
public uint Evolue { get; set; }
public uint EquipAvatarId { get; set; } public uint EquipAvatarId { get; set; }
} }
public class GameWeaponInfo : GrowableItemInfo public class GameWeaponInfo : GrowableItemInfo
{ {
[SugarColumn(IsJson = true)] public Dictionary<uint, ulong> PartSlots { get; set; } = [];
public override Item ToProto() public override Item ToProto()
{ {
var proto = new Item var proto = new Item
@@ -62,13 +75,17 @@ public class GameWeaponInfo : GrowableItemInfo
{ {
Level = Level, Level = Level,
Exp = Exp, Exp = Exp,
Break = Break Break = Break,
Evolue = Evolue
} }
}; };
foreach (var (slot, uid) in PartSlots) proto.Slots[slot] = uid;
return proto; return proto;
} }
}public class GameSkinInfo : BaseGameItemInfo }
public class GameSkinInfo : BaseGameItemInfo
{ {
[SugarColumn(IsJson = true)] public Dictionary<uint, ulong> PartSlots { get; set; } = [];
public uint SkinType { get; set; } public uint SkinType { get; set; }
public override Item ToProto() public override Item ToProto()
{ {
@@ -79,7 +96,31 @@ public class GameWeaponInfo : GrowableItemInfo
Count = ItemCount, Count = ItemCount,
Flag = (uint)Flag, Flag = (uint)Flag,
}; };
proto.Slots[11] = SkinType; proto.Slots[(uint)ItemSkinSlotTypeEnum.SLOT_CARD_SKIL_TYPE] = Math.Min(SkinType, 1);
foreach (var (slot, uid) in PartSlots) proto.Slots[slot] = uid;
return proto; return proto;
} }
} }
public class GameSupportCardInfo : BaseGameItemInfo
{
public uint AffixId { get; set; }
public override Item ToProto()
{
var proto = new Item
{
Id = UniqueId,
Template = TemplateId,
Count = ItemCount,
Flag = (uint)Flag,
Enhance = new Enhance
{
Level = Level,
Exp = Exp
}
};
proto.Slots[(uint)ItemSupportCardSlotTypeEnum.SLOT_AFFIXINDEX] = AffixId;
return proto;
}
}

View File

@@ -7,6 +7,8 @@ namespace MikuSB.Database.Player;
[SugarTable("Player")] [SugarTable("Player")]
public class PlayerGameData : BaseDatabaseDataHelper public class PlayerGameData : BaseDatabaseDataHelper
{ {
public const string DefaultDisplayName = "Miku";
public string? Name { get; set; } = ""; public string? Name { get; set; } = "";
public string? Signature { get; set; } = "MikuPS"; public string? Signature { get; set; } = "MikuPS";
public uint Level { get; set; } = 100; public uint Level { get; set; } = 100;
@@ -25,13 +27,30 @@ public class PlayerGameData : BaseDatabaseDataHelper
return result; return result;
} }
public static string NormalizeDisplayName(string? name)
{
var normalized = name?.Trim();
return string.IsNullOrWhiteSpace(normalized) ? DefaultDisplayName : normalized;
}
public bool EnsureDisplayName()
{
var normalized = NormalizeDisplayName(Name);
if (string.Equals(Name, normalized, StringComparison.Ordinal))
return false;
Name = normalized;
return true;
}
public PlayerProfile ToProfileProto() public PlayerProfile ToProfileProto()
{ {
var displayName = NormalizeDisplayName(Name);
var proto = new PlayerProfile var proto = new PlayerProfile
{ {
Pid = (uint)Uid, Pid = (uint)Uid,
Account = Name, Account = displayName,
Name = Name, Name = displayName,
Level = Level, Level = Level,
Sex = Gender, Sex = Gender,
Sign = Signature, Sign = Signature,

View File

@@ -1,13 +0,0 @@
namespace MikuSB.Enums.Item;
public enum ItemFlagEnum
{
FLAG_USE = 1,// 使用中
FLAG_LOCK = 2,// 锁定中
FLAG_READED = 4,// 道具已查看
FLAG_LEAVE = 8,// 角色大招后离场
FLAG_WEAPON_DEFAULT = 16,// 武器显示原始样式
FLAG_WEAPON_AUDIO = 32,// 武器消音器音效
FLAG_ROLE_LIKE = 64,// 心选角色
}

View File

@@ -25,4 +25,56 @@ public enum ItemTypeEnum
TYPE_MAIN_SCENE = 21, //主界面场景道具 TYPE_MAIN_SCENE = 21, //主界面场景道具
TYPE_AR = 24, //AR道具 TYPE_AR = 24, //AR道具
TYPE_CALL = 25, //电话陪伴道具 TYPE_CALL = 25, //电话陪伴道具
}
public enum ItemCardSlotTypeEnum
{
SLOT_SUPPORTERCARD1 = 1, // 后勤卡
SLOT_SUPPORTERCARD2 = 2, // 后勤卡
SLOT_SUPPORTERCARD3 = 3, // 后勤卡
SLOT_WEAPON = 4, // 武器
SLOT_SKIN = 5, // 时装
SLOT_WEAPON_SKIN = 6, // 武器时装
SLOT_SUPPORTERINDEX = 7, // 当前使用的后勤组
SLOT_SUPPORTERCARD4 = 8, // 后勤卡
SLOT_SUPPORTERCARD5 = 9, // 后勤卡
SLOT_SUPPORTERCARD6 = 10, // 后勤卡
SLOT_SUPPORTERCARD7 = 11, // 后勤卡
SLOT_SUPPORTERCARD8 = 12, // 后勤卡
SLOT_SUPPORTERCARD9 = 13, // 后勤卡
}
public enum ItemSkinPartSlotTypeEnum
{
SLOT_SkinPartSlot1 = 1,
SLOT_SkinPartSlot2 = 2,
SLOT_SkinPartSlot3 = 3,
SLOT_SkinPartSlot4 = 4,
SLOT_SkinPartSlot5 = 5,
SLOT_SkinPartSlot6 = 6,
SLOT_SkinPartSlot7 = 7,
SLOT_SkinPartSlot8 = 8,
SLOT_SkinPartSlot9 = 9,
SLOT_SkinPartSlot10 = 10,
}
public enum ItemSkinSlotTypeEnum
{
SLOT_CARD_SKIL_TYPE = 11
}
public enum ItemSupportCardSlotTypeEnum
{
SLOT_AFFIXINDEX = 1 // 可洗练的初始词缀索引
}
public enum ItemFlagEnum
{
FLAG_USE = 1, // 使用中
FLAG_LOCK = 2, // 锁定中
FLAG_READED = 4, // 道具已查看
FLAG_LEAVE = 8, // 角色大招后离场
FLAG_WEAPON_DEFAULT = 16, // 武器显示原始样式
FLAG_WEAPON_AUDIO = 32, // 武器消音器音效
FLAG_ROLE_LIKE = 64, // 心选角色
} }

View File

@@ -0,0 +1,24 @@
namespace MikuSB.Enums.Player;
public enum ProfileShowItemTypeEnum
{
SHOWITEM_CARD1 = 1, //第1个展示卡
SHOWITEM_CARD2 = 2, //第2个展示卡
SHOWITEM_CARD3 = 3, //第3个展示卡
SHOWITEM_GIRL = 4, //看板娘
SHOWITEM_FACE = 5, //头像
SHOWITEM_FRAME = 6, //头像框
SHOWITEM_CARD4 = 7, //第4个展示卡
SHOWITEM_CARD5 = 8, //第5个展示卡
SHOWITEM_SKIN1 = 9, //第1个展示皮肤
SHOWITEM_SKIN2 = 10, //第2个展示皮肤
SHOWITEM_SKIN3 = 11, //第3个展示皮肤
SHOWITEM_SKIN4 = 12, //第4个展示皮肤
SHOWITEM_SKIN5 = 13, //第5个展示皮肤
SHOWITEM_BADGE1 = 14, //第一个展示勋章
SHOWITEM_BADGE2 = 15, //第二个展示勋章
SHOWITEM_BADGE3 = 16, //第三个展示勋章
SHOWITEM_COVER = 17, //展示封面
SHOWITEM_NAMECARD = 18, //展示名片
SHOWITEM_BUBBLE = 19, //聊天气泡
}

View File

@@ -35,6 +35,15 @@ public class ServerTextCHS
/// </summary> /// </summary>
public class WordTextCHS public class WordTextCHS
{ {
public string Furniture => "家具";
public string Skin => "皮肤";
public string WeaponPart => "武器部件";
public string CallItem => "召唤道具";
public string SkinPart => "皮肤部件";
public string Profile => "个人资料";
public string WeaponSkin => "武器皮肤";
public string SupportCard => "支援卡";
public string Weapon => "武器";
public string Rank => "星魂"; public string Rank => "星魂";
public string Avatar => "角色"; public string Avatar => "角色";
public string Material => "材料"; public string Material => "材料";
@@ -119,8 +128,11 @@ public class CommandTextCHS
{ {
public NoticeTextCHS Notice { get; } = new(); public NoticeTextCHS Notice { get; } = new();
public HelpTextCHS Help { get; } = new(); public HelpTextCHS Help { get; } = new();
public AccountTextCHS Account { get; } = new();
public GirlTextCHS Girl { get; } = new(); public GirlTextCHS Girl { get; } = new();
public GiveAllTextCHS GiveAll { get; } = new(); public GiveAllTextCHS GiveAll { get; } = new();
public DebugTextCHS Debug { get; } = new();
public GameCommandTextCHS Game { get; } = new();
} }
#endregion #endregion
@@ -207,6 +219,14 @@ public class HelpTextCHS
public string CommandAlias => "命令别名: "; public string CommandAlias => "命令别名: ";
} }
public class AccountTextCHS
{
public string Desc => "管理 SDK 登录使用的账号映射";
public string Usage => "用法: /account create <邮箱> <UID>";
public string Created => "已创建账号映射: {0} -> UID {1}";
public string CreateFailed => "创建账号映射失败: {0}";
}
/// <summary> /// <summary>
/// path: Game.Command.Girl /// path: Game.Command.Girl
/// </summary> /// </summary>
@@ -218,11 +238,15 @@ public class GirlTextCHS
public string Usage => public string Usage =>
"用法: /girl add <detail/-1> -p<particular> -l<level> -s<star>\n" + "用法: /girl add <detail/-1> -p<particular> -l<level> -s<star>\n" +
"用法: /girl level <guid/-1> <level>"; "用法: /girl level <guid/-1> <level>\n" +
"用法: /girl neuronic <guid/-1> <level>\n" +
"用法: /girl break <guid/-1> <level>";
public string NotFound => "角色不存在!"; public string NotFound => "角色不存在!";
public string Added => "已为玩家添加 {0} 个角色!"; public string Added => "已为玩家添加 {0} 个角色!";
public string UpdateLevel => "已将 {1} 个角色等级设置为 {0}"; public string UpdateLevel => "已将 {1} 个角色等级设置为 {0}";
public string UpdateNeuronicLevel => "已将 {1} 个角色的神经元等级设置为 {0}";
public string UpdateBreakLevel => "已将 {1} 个角色的天启/武格等级设置为 {0}";
} }
/// <summary> /// <summary>
@@ -230,13 +254,42 @@ public class GirlTextCHS
/// </summary> /// </summary>
public class GiveAllTextCHS public class GiveAllTextCHS
{ {
public string Desc => "给玩家所有物品\n" + public string Desc => "给玩家所有物品\n" +
"备注: -1 表全部"; "注意:-1 表全部";
public string Usage => "用法: /giveall weapon <detail/-1> -p<particular> -l<level>"; public string Usage => "用法/giveall weapon <detail/-1> -p<特定> -l<等級>\n" +
public string WeaponNotFound => "找不到武器!"; "用法:/giveall weaponskin <detail/-1> -p<特定>\n" +
public string WeaponAdded => "已添加 {0} 把武器给玩家!"; "用法:/giveall card <detail/-1> -p<特定> -l<等級>" +
"用法:/giveall profile <detail/-1> -g<类型> -p<特定> -l<等级>" +
"用法:/giveall skinpart <detail/-1> -g<類型> -p<特定> -l<等級>" +
"用法:/giveall weaponpart <detail/-1> -g<類型> -p<特定> -l<等級>" +
"用法:/giveall call <detail/-1> -g<類型> -p<特定> -l<等級>";
public string NotFound => "未找到 {0}";
public string GiveAllItems => "已向玩家添加 {0} 个 {1}";
}
/// <summary>
/// path: Game.Command.Debug
/// </summary>
public class DebugTextCHS
{
public string Desc => "调试包输出开关";
public string Usage => "用法: /debug [on|off|simple|detail|file]";
public string Enabled => "已启用调试包输出。";
public string Disabled => "已禁用调试包输出。";
public string SimpleEnabled => "已启用简单调试包输出。";
public string DetailEnabled => "已启用详细调试包输出。";
public string FileEnabled => "个人调试文件输出已启用。";
public string FileDisabled => "个人调试文件输出已禁用。";
}
public class GameCommandTextCHS
{
public string Desc => "使用补丁注入启动已配置的游戏";
public string Usage => "用法: /game [额外游戏参数]";
public string Started => "游戏已启动。PID: {0}";
public string Failed => "游戏启动失败: {0}";
} }
#endregion #endregion
#endregion #endregion

View File

@@ -35,6 +35,15 @@ public class ServerTextCHT
/// </summary> /// </summary>
public class WordTextCHT public class WordTextCHT
{ {
public string Furniture => "傢俱";
public string Skin => "皮膚";
public string WeaponPart => "武器部件";
public string CallItem => "召喚道具";
public string SkinPart => "外觀部件";
public string Profile => "個人資料";
public string WeaponSkin => "武器外觀";
public string SupportCard => "支援卡";
public string Weapon => "武器";
public string Rank => "星魂"; public string Rank => "星魂";
public string Avatar => "角色"; public string Avatar => "角色";
public string Material => "材料"; public string Material => "材料";
@@ -119,8 +128,11 @@ public class CommandTextCHT
{ {
public NoticeTextCHT Notice { get; } = new(); public NoticeTextCHT Notice { get; } = new();
public HelpTextCHT Help { get; } = new(); public HelpTextCHT Help { get; } = new();
public AccountTextCHT Account { get; } = new();
public GirlTextCHT Girl { get; } = new(); public GirlTextCHT Girl { get; } = new();
public GiveAllTextCHT GiveAll { get; } = new(); public GiveAllTextCHT GiveAll { get; } = new();
public DebugTextCHT Debug { get; } = new();
public GameCommandTextCHT Game { get; } = new();
} }
#endregion #endregion
@@ -207,6 +219,14 @@ public class HelpTextCHT
public string CommandAlias => "命令別名: "; public string CommandAlias => "命令別名: ";
} }
public class AccountTextCHT
{
public string Desc => "管理 SDK 登入使用的帳號映射";
public string Usage => "用法: /account create <郵箱> <UID>";
public string Created => "已建立帳號映射: {0} -> UID {1}";
public string CreateFailed => "建立帳號映射失敗: {0}";
}
/// <summary> /// <summary>
/// path: Game.Command.Girl /// path: Game.Command.Girl
/// </summary> /// </summary>
@@ -218,11 +238,15 @@ public class GirlTextCHT
public string Usage => public string Usage =>
"用法: /girl add <detail/-1> -p<particular> -l<level> -s<star>\n" + "用法: /girl add <detail/-1> -p<particular> -l<level> -s<star>\n" +
"用法: /girl level <guid/-1> <level>"; "用法: /girl level <guid/-1> <level>\n" +
"用法: /girl neuronic <guid/-1> <level>\n" +
"用法: /girl break <guid/-1> <level>";
public string NotFound => "角色不存在!"; public string NotFound => "角色不存在!";
public string Added => "已為玩家新增 {0} 個角色!"; public string Added => "已為玩家新增 {0} 個角色!";
public string UpdateLevel => "已將 {1} 個角色等級設為 {0}"; public string UpdateLevel => "已將 {1} 個角色等級設為 {0}";
public string UpdateNeuronicLevel => "已將 {1} 個角色的神經元等級設置為 {0}";
public string UpdateBreakLevel => "已将 {1} 个角色的天啟/武格等级设置为 {0}";
} }
/// <summary> /// <summary>
@@ -230,13 +254,42 @@ public class GirlTextCHT
/// </summary> /// </summary>
public class GiveAllTextCHT public class GiveAllTextCHT
{ {
public string Desc => "給玩家所有物品\n" + public string Desc => "給玩家所有物品\n" +
"備註: -1 表全部"; "注意:-1 表全部";
public string Usage => "用法: /giveall weapon <detail/-1> -p<particular> -l<level>"; public string Usage => "用法/giveall weapon <detail/-1> -p<特定> -l<等級>\n" +
public string WeaponNotFound => "找不到武器!"; "用法:/giveall weaponskin <detail/-1> -p<特定>\n" +
public string WeaponAdded => "已添加 {0} 把武器給玩家!"; "用法:/giveall card <detail/-1> -p<特定> -l<等級>" +
"用法:/giveall profile <detail/-1> -g<類型> -p<特定> -l<等級>" +
"用法:/giveall skinpart <detail/-1> -g<類型> -p<特定> -l<等級>" +
"用法:/giveall weaponpart <detail/-1> -g<類型> -p<特定> -l<等級>" +
"用法:/giveall call <detail/-1> -g<類型> -p<特定> -l<等級>";
public string NotFound => "未找到 {0}";
public string GiveAllItems => "已向玩家添加 {0} 個 {1}";
}
/// <summary>
/// path: Game.Command.Debug
/// </summary>
public class DebugTextCHT
{
public string Desc => "切換調試封包輸出";
public string Usage => "用法: /debug [on|off|simple|detail|file]";
public string Enabled => "已啟用調試封包輸出。";
public string Disabled => "已停用調試封包輸出。";
public string SimpleEnabled => "已啟用簡易調試封包輸出。";
public string DetailEnabled => "已啟用詳細調試封包輸出。";
public string FileEnabled => "個人調試檔案輸出已啟用。";
public string FileDisabled => "個人調試檔案輸出已停用。";
}
public class GameCommandTextCHT
{
public string Desc => "使用補丁注入啟動已配置的遊戲";
public string Usage => "用法: /game [額外遊戲參數]";
public string Started => "遊戲已啟動。PID: {0}";
public string Failed => "遊戲啟動失敗: {0}";
} }
#endregion #endregion
#endregion #endregion

View File

@@ -35,10 +35,16 @@ public class ServerTextEN
/// </summary> /// </summary>
public class WordTextEN public class WordTextEN
{ {
public string Star => "Star"; public string Furniture => "Furniture";
public string Skin => "Skin";
public string WeaponPart => "Weapon Part";
public string CallItem => "Call Item";
public string SkinPart => "Skin Part";
public string Profile => "Profile";
public string WeaponSkin => "Weapon Skin";
public string Valk => "Valkyrie"; public string Valk => "Valkyrie";
public string Material => "Material"; public string Material => "Material";
public string Stigmata => "Stigmata"; public string SupportCard => "Support Card";
public string Weapon => "Weapon"; public string Weapon => "Weapon";
public string Banner => "Gacha"; public string Banner => "Gacha";
public string Activity => "Activity"; public string Activity => "Activity";
@@ -81,8 +87,11 @@ public class CommandTextEN
{ {
public NoticeTextEN Notice { get; } = new(); public NoticeTextEN Notice { get; } = new();
public HelpTextEN Help { get; } = new(); public HelpTextEN Help { get; } = new();
public AccountTextEN Account { get; } = new();
public GirlTextEN Girl { get; } = new(); public GirlTextEN Girl { get; } = new();
public GiveAllTextEN GiveAll { get; } = new(); public GiveAllTextEN GiveAll { get; } = new();
public DebugTextEN Debug { get; } = new();
public GameCommandTextEN Game { get; } = new();
} }
#endregion #endregion
@@ -176,6 +185,14 @@ public class HelpTextEN
public string CommandAlias => "Command Alias"; public string CommandAlias => "Command Alias";
} }
public class AccountTextEN
{
public string Desc => "Manage account mappings for SDK logins";
public string Usage => "Usage: /account create <email> <uid>";
public string Created => "Created account mapping: {0} -> UID {1}";
public string CreateFailed => "Failed to create account mapping: {0}";
}
/// <summary> /// <summary>
/// path: Game.Command.Girl /// path: Game.Command.Girl
/// </summary> /// </summary>
@@ -187,11 +204,15 @@ public class GirlTextEN
public string Usage => public string Usage =>
"Usage: /girl add <detail/-1> -p<particular> -l<level> -s<star>\n" + "Usage: /girl add <detail/-1> -p<particular> -l<level> -s<star>\n" +
"Usage: /girl level <guid/-1> <level>"; "Usage: /girl level <guid/-1> <level>\n" +
"Usage: /girl neuronic <guid/-1> <level>\n" +
"Usage: /girl break <guid/-1> <level>";
public string NotFound => "Character not found!"; public string NotFound => "Character not found!";
public string Added => "Granted {0} character(s) to player!"; public string Added => "Granted {0} character(s) to player!";
public string UpdateLevel => "Set {1} character(s) to level {0}!"; public string UpdateLevel => "Set {1} character(s) to level {0}!";
public string UpdateNeuronicLevel => "Set {1} character(s) Neuronic to level {0}!";
public string UpdateBreakLevel => "Set {1} character(s) Manifestation to level {0}!";
} }
/// <summary> /// <summary>
@@ -201,11 +222,40 @@ public class GiveAllTextEN
{ {
public string Desc => "Give all items to player\n"+ public string Desc => "Give all items to player\n"+
"Note: -1 means all"; "Note: -1 means all";
public string Usage => "Usage: /giveall weapon <detail/-1> -p<particular> -l<level>"; public string Usage => "Usage: /giveall weapon <detail/-1> -p<particular> -l<level>\n" +
public string WeaponNotFound => "Weapon not found!"; "Usage: /giveall weaponskin <detail/-1> -p<particular>\n" +
public string WeaponAdded => "Added {0} weapon(s) to player!"; "Usage: /giveall card <detail/-1> -p<particular> -l<level>" +
"Usage: /giveall profile <detail/-1> -g<genre> -p<particular> -l<level>" +
"Usage: /giveall skinpart <detail/-1> -g<genre> -p<particular> -l<level>" +
"Usage: /giveall weaponpart <detail/-1> -g<genre> -p<particular> -l<level>" +
"Usage: /giveall call <detail/-1> -g<genre> -p<particular> -l<level>";
public string NotFound => "{0} not found!";
public string GiveAllItems => "Added {0} {1} to player!";
}
/// <summary>
/// path: Game.Command.Debug
/// </summary>
public class DebugTextEN
{
public string Desc => "Toggle debug packet output";
public string Usage => "Usage: /debug [on|off|simple|detail|file]";
public string Enabled => "Debug packet output enabled.";
public string Disabled => "Debug packet output disabled.";
public string SimpleEnabled => "Simple debug packet output enabled.";
public string DetailEnabled => "Detailed debug packet output enabled.";
public string FileEnabled => "Personal debug file output enabled.";
public string FileDisabled => "Personal debug file output disabled.";
}
public class GameCommandTextEN
{
public string Desc => "Launch the configured game with patch injection";
public string Usage => "Usage: /game [extra game args]";
public string Started => "Game launched. PID: {0}";
public string Failed => "Failed to launch game: {0}";
} }
#endregion #endregion
#endregion #endregion

View File

@@ -53,7 +53,7 @@ public class IConsole
Console.WriteLine(); Console.WriteLine();
Input = []; Input = [];
CursorIndex = 0; CursorIndex = 0;
if (InputHistory.Count >= HistoryMaxCount) if (InputHistory.Count >= HistoryMaxCount)
InputHistory.RemoveAt(0); InputHistory.RemoveAt(0);
InputHistory.Add(input); InputHistory.Add(input);
HistoryIndex = InputHistory.Count; HistoryIndex = InputHistory.Count;
@@ -80,7 +80,7 @@ public class IConsole
public static void HandleUpArrow() public static void HandleUpArrow()
{ {
if (InputHistory.Count == 0) return; if (InputHistory.Count == 0) return;
if (HistoryIndex > 0) if (HistoryIndex > 0)
{ {
HistoryIndex--; HistoryIndex--;
@@ -94,7 +94,7 @@ public class IConsole
public static void HandleDownArrow() public static void HandleDownArrow()
{ {
if (HistoryIndex >= InputHistory.Count) return; if (HistoryIndex >= InputHistory.Count) return;
HistoryIndex++; HistoryIndex++;
if (HistoryIndex >= InputHistory.Count) if (HistoryIndex >= InputHistory.Count)
{ {
@@ -102,7 +102,7 @@ public class IConsole
Input = []; Input = [];
CursorIndex = 0; CursorIndex = 0;
} }
else else
{ {
var history = InputHistory[HistoryIndex]; var history = InputHistory[HistoryIndex];
Input = [.. history]; Input = [.. history];
@@ -114,7 +114,7 @@ public class IConsole
public static void HandleLeftArrow() public static void HandleLeftArrow()
{ {
if (CursorIndex <= 0) return; if (CursorIndex <= 0) return;
var (left, _) = Console.GetCursorPosition(); var (left, _) = Console.GetCursorPosition();
CursorIndex--; CursorIndex--;
Console.SetCursorPosition(left - GetWidth(Input[CursorIndex].ToString()), Console.CursorTop); Console.SetCursorPosition(left - GetWidth(Input[CursorIndex].ToString()), Console.CursorTop);
@@ -123,7 +123,7 @@ public class IConsole
public static void HandleRightArrow() public static void HandleRightArrow()
{ {
if (CursorIndex >= Input.Count) return; if (CursorIndex >= Input.Count) return;
var (left, _) = Console.GetCursorPosition(); var (left, _) = Console.GetCursorPosition();
CursorIndex++; CursorIndex++;
Console.SetCursorPosition(left + GetWidth(Input[CursorIndex - 1].ToString()), Console.CursorTop); Console.SetCursorPosition(left + GetWidth(Input[CursorIndex - 1].ToString()), Console.CursorTop);
@@ -148,13 +148,29 @@ public class IConsole
#endregion #endregion
public static string ListenConsole() public static async Task ListenConsole(CancellationToken exitToken)
{ {
while (true) while (!exitToken.IsCancellationRequested)
{ {
ConsoleKeyInfo keyInfo; ConsoleKeyInfo keyInfo;
try { keyInfo = Console.ReadKey(true); } try
catch (InvalidOperationException) { continue; } {
if (!Console.KeyAvailable)
{
await Task.Delay(10, exitToken);
continue;
}
keyInfo = Console.ReadKey(true);
}
catch (OperationCanceledException)
{
break;
}
catch (InvalidOperationException)
{
await Task.Delay(50, exitToken);
continue;
}
switch (keyInfo.Key) switch (keyInfo.Key)
{ {

View File

@@ -38,5 +38,21 @@
"DebugMessage": true, "DebugMessage": true,
"DebugDetailMessage": true, "DebugDetailMessage": true,
"DebugNoHandlerPacket": true "DebugNoHandlerPacket": true
},
"Proxy": {
"Enabled": true,
"Port": 18888
},
"Loader": {
"GamePath": "",
"PatchPaths": [
"Patch\\MikuSB-Patch.dll"
],
"Arguments": [
"-FeatureLevelES31",
"-channelid=seasun",
"-NoSplash"
],
"SetAllProxy": true
} }
} }

View File

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

View File

@@ -0,0 +1,37 @@
using MikuSB.Database.Account;
using MikuSB.Enums.Player;
using MikuSB.Internationalization;
namespace MikuSB.GameServer.Command.Commands;
[CommandInfo("account", "Game.Command.Account.Desc", "Game.Command.Account.Usage", [], [PermEnum.Admin, PermEnum.Support])]
public class CommandAccount : ICommands
{
[CommandMethod("create")]
public async ValueTask Create(CommandArg arg)
{
if (!await arg.CheckArgCnt(2))
return;
var email = arg.Args[0].Trim();
if (!int.TryParse(arg.Args[1], out var uid) || uid <= 0)
{
await arg.SendMsg(I18NManager.Translate("Game.Command.Notice.InvalidArguments"));
return;
}
try
{
var account = AccountData.CreateAccount(email, uid, "");
await arg.SendMsg(I18NManager.Translate("Game.Command.Account.Created", account.Username, account.Uid.ToString()));
}
catch (InvalidOperationException ex)
{
await arg.SendMsg(I18NManager.Translate("Game.Command.Account.CreateFailed", ex.Message));
}
catch (ArgumentException ex)
{
await arg.SendMsg(I18NManager.Translate("Game.Command.Account.CreateFailed", ex.Message));
}
}
}

View File

@@ -0,0 +1,69 @@
using MikuSB.Configuration;
using MikuSB.Enums.Player;
using MikuSB.Util;
using MikuSB.Internationalization;
namespace MikuSB.GameServer.Command.Commands;
[CommandInfo("debug", "Game.Command.Debug.Desc", "Game.Command.Debug.Usage", ["dbg"], [PermEnum.Admin, PermEnum.Support])]
public class CommandDebug : ICommands
{
private static readonly Logger Logger = new("CommandManager");
[CommandDefault]
public async ValueTask ToggleDebug(CommandArg arg)
{
var option = arg.Args.FirstOrDefault()?.ToLowerInvariant() ?? "on";
var serverOption = ConfigManager.Config.ServerOption;
var message = option switch
{
"on" => EnableDebug(serverOption),
"off" => DisableDebug(serverOption),
"simple" => EnableSimpleDebug(serverOption),
"detail" => EnableDetailDebug(serverOption),
"file" => ToggleDebugFile(serverOption),
_ => I18NManager.Translate("Game.Command.Debug.Usage")
};
Logger.Info(message);
await arg.SendMsg(message);
}
private static string EnableDebug(ServerOption serverOption)
{
serverOption.EnableDebug = true;
serverOption.DebugMessage = true;
serverOption.DebugDetailMessage = true;
return I18NManager.Translate("Game.Command.Debug.Enabled");
}
private static string DisableDebug(ServerOption serverOption)
{
serverOption.EnableDebug = false;
return I18NManager.Translate("Game.Command.Debug.Disabled");
}
private static string EnableSimpleDebug(ServerOption serverOption)
{
serverOption.EnableDebug = true;
serverOption.DebugMessage = true;
serverOption.DebugDetailMessage = false;
return I18NManager.Translate("Game.Command.Debug.SimpleEnabled");
}
private static string EnableDetailDebug(ServerOption serverOption)
{
serverOption.EnableDebug = true;
serverOption.DebugMessage = true;
serverOption.DebugDetailMessage = true;
return I18NManager.Translate("Game.Command.Debug.DetailEnabled");
}
private static string ToggleDebugFile(ServerOption serverOption)
{
serverOption.SavePersonalDebugFile = !serverOption.SavePersonalDebugFile;
return serverOption.SavePersonalDebugFile
? I18NManager.Translate("Game.Command.Debug.FileEnabled")
: I18NManager.Translate("Game.Command.Debug.FileDisabled");
}
}

View File

@@ -0,0 +1,29 @@
using MikuSB.Enums.Player;
using MikuSB.Internationalization;
using MikuSB.Loader;
using MikuSB.Util;
namespace MikuSB.GameServer.Command.Commands;
[CommandInfo("game", "Game.Command.Game.Desc", "Game.Command.Game.Usage", [], [PermEnum.Admin, PermEnum.Support])]
public class CommandGame : ICommands
{
private static readonly Logger Logger = new("CommandManager");
[CommandDefault]
public async ValueTask Launch(CommandArg arg)
{
try
{
var pid = GameLaunchService.Launch(arg.Args.ToArray());
var message = I18NManager.Translate("Game.Command.Game.Started", pid.ToString());
Logger.Info(message);
await arg.SendMsg(message);
}
catch (Exception ex)
{
Logger.Error("Failed to launch game.", ex);
await arg.SendMsg(I18NManager.Translate("Game.Command.Game.Failed", ex.Message));
}
}
}

View File

@@ -84,4 +84,86 @@ public class CommandGirl : ICommands
level.ToString(), level.ToString(),
girls.Count.ToString())); girls.Count.ToString()));
} }
[CommandMethod("neuronic")]
public async ValueTask UpdateNeuronicLevel(CommandArg arg)
{
if (!await arg.CheckOnlineTarget()) return;
if (!await arg.CheckArgCnt(2)) return;
var guid = arg.GetInt(0);
var level = Math.Clamp(arg.GetInt(1), 0, 6);
var player = arg.Target!.Player!;
List<CharacterInfo> girls = [];
List<uint> spines = new List<uint>();
for (int i = 0; i < 6; i++)
spines.Add(i < level ? 511u : 0u);
uint proLevel = (uint)(spines.Count(x => x == 511) / 2);
if (guid == -1)
{
foreach (var girl in player.CharacterManager.CharacterData.Characters)
{
girl.Spines = spines;
girl.ProLevel = proLevel;
girls.Add(girl);
}
}
else
{
var girl = player.CharacterManager.GetCharacterByGUID((uint)guid);
if (girl == null)
{
await arg.SendMsg(I18NManager.Translate("Game.Command.Girl.NotFound"));
return;
}
girl.Spines = spines;
girl.ProLevel = proLevel;
girls.Add(girl);
}
if (girls.Count > 0) await player.SendPacket(new PacketNtfCallScript(girls));
await arg.SendMsg(I18NManager.Translate("Game.Command.Girl.UpdateNeuronicLevel",
level.ToString(),
girls.Count.ToString()));
}
[CommandMethod("break")]
public async ValueTask UpdateBreakLevel(CommandArg arg)
{
if (!await arg.CheckOnlineTarget()) return;
if (!await arg.CheckArgCnt(2)) return;
var guid = arg.GetInt(0);
var level = Math.Clamp(arg.GetInt(1), 0, 45);
var player = arg.Target!.Player!;
List<CharacterInfo> girls = [];
if (guid == -1)
{
foreach (var girl in player.CharacterManager.CharacterData.Characters)
{
girl.Break = (uint)level;
girls.Add(girl);
}
}
else
{
var girl = player.CharacterManager.GetCharacterByGUID((uint)guid);
if (girl == null)
{
await arg.SendMsg(I18NManager.Translate("Game.Command.Girl.NotFound"));
return;
}
girl.Break = (uint)level;
girls.Add(girl);
}
if (girls.Count > 0) await player.SendPacket(new PacketNtfCallScript(girls));
await arg.SendMsg(I18NManager.Translate("Game.Command.Girl.UpdateBreakLevel",
level.ToString(),
girls.Count.ToString()));
}
} }

View File

@@ -18,7 +18,7 @@ public class CommandGiveAll : ICommands
if (await arg.GetOption('l') is not int level) return; if (await arg.GetOption('l') is not int level) return;
var detail = arg.GetInt(0); var detail = arg.GetInt(0);
level = Math.Clamp(level, 1, 80); level = Math.Clamp(level, 1, 90);
var player = arg.Target!.Player!; var player = arg.Target!.Player!;
List<GameWeaponInfo> weapons = []; List<GameWeaponInfo> weapons = [];
if (detail == -1) if (detail == -1)
@@ -36,12 +36,298 @@ public class CommandGiveAll : ICommands
var weapon = await player.InventoryManager!.AddWeaponItem(ItemTypeEnum.TYPE_WEAPON, (uint)detail,(uint)particular,1,(uint)level,false); var weapon = await player.InventoryManager!.AddWeaponItem(ItemTypeEnum.TYPE_WEAPON, (uint)detail,(uint)particular,1,(uint)level,false);
if (weapon == null) if (weapon == null)
{ {
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.WeaponNotFound")); await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.NotFound", I18NManager.Translate("Word.Weapon")));
return; return;
} }
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));
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.WeaponAdded", weapons.Count.ToString())); await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems",
I18NManager.Translate("Word.Weapon"), weapons.Count.ToString()));
}
[CommandMethod("card")]
public async ValueTask GiveAllSupportCard(CommandArg arg)
{
if (!await arg.CheckOnlineTarget()) return;
if (await arg.GetOption('p') is not int particular) return;
if (await arg.GetOption('l') is not int level) return;
var detail = arg.GetInt(0);
var player = arg.Target!.Player!;
List<GameSupportCardInfo> supportCards = [];
if (detail == -1)
{
// add all
foreach (var config in GameData.SupportCardData)
{
var supportCard = await player.InventoryManager!
.AddSupportCardItem(config.Detail, config.Particular, config.Level, (uint)level, false);
if (supportCard != null) supportCards.Add(supportCard);
}
}
else
{
var supportCard = await player.InventoryManager!.AddSupportCardItem((uint)detail, (uint)particular, 1, (uint)level, false);
if (supportCard == null)
{
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.NotFound", I18NManager.Translate("Word.SupportCard")));
return;
}
supportCards.Add(supportCard);
}
if (supportCards.Count > 0) await player.SendPacket(new PacketNtfCallScript(supportCards));
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems",
I18NManager.Translate("Word.SupportCard"), supportCards.Count.ToString()));
}
[CommandMethod("weaponskin")]
public async ValueTask GiveAllWeaponSkin(CommandArg arg)
{
if (!await arg.CheckOnlineTarget()) return;
if (await arg.GetOption('p') is not int particular) return;
var detail = arg.GetInt(0);
var player = arg.Target!.Player!;
List<BaseGameItemInfo> weaponSkins = [];
if (detail == -1)
{
// add all
foreach (var config in GameData.WeaponSkinData.Values)
{
var weaponSkin = await player.InventoryManager!
.AddWeaponSkinItem((ItemTypeEnum)config.Genre, config.Detail, config.Particular, config.Level, false);
if (weaponSkin != null) weaponSkins.Add(weaponSkin);
}
}
else
{
var weaponSkin = await player.InventoryManager!.AddWeaponSkinItem(ItemTypeEnum.TYPE_WEAPON, (uint)detail, (uint)particular, 1, false);
if (weaponSkin == null)
{
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.NotFound", I18NManager.Translate("Word.WeaponSkin")));
return;
}
weaponSkins.Add(weaponSkin);
}
if (weaponSkins.Count > 0) await player.SendPacket(new PacketNtfCallScript(weaponSkins));
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems",
I18NManager.Translate("Word.WeaponSkin"), weaponSkins.Count.ToString()));
}
[CommandMethod("profile")]
public async ValueTask GiveAllProfile(CommandArg arg)
{
if (!await arg.CheckOnlineTarget()) return;
if (await arg.GetOption('p') is not int particular) return;
if (await arg.GetOption('l') is not int level) return;
if (await arg.GetOption('g') is not int genre) return;
var detail = arg.GetInt(0);
var player = arg.Target!.Player!;
List<BaseGameItemInfo> profileItems = [];
if (detail == -1)
{
// add all
foreach (var config in GameData.ProfileData.Values)
{
var profile = await player.InventoryManager!
.AddProfileItem((ItemTypeEnum)config.Genre, config.Detail, config.Particular, config.Level, false);
if (profile != null) profileItems.Add(profile);
}
}
else
{
var profile = await player.InventoryManager!.AddProfileItem((ItemTypeEnum)genre, (uint)detail, (uint)particular, (uint)level, false);
if (profile == null)
{
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.NotFound", I18NManager.Translate("Word.Profile")));
return;
}
profileItems.Add(profile);
}
if (profileItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(profileItems));
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems",
I18NManager.Translate("Word.Profile"), profileItems.Count.ToString()));
}
[CommandMethod("skinpart")]
public async ValueTask GiveAllSkinPart(CommandArg arg)
{
if (!await arg.CheckOnlineTarget()) return;
if (await arg.GetOption('p') is not int particular) return;
if (await arg.GetOption('l') is not int level) return;
if (await arg.GetOption('g') is not int genre) return;
var detail = arg.GetInt(0);
var player = arg.Target!.Player!;
List<BaseGameItemInfo> skinPartItems = [];
if (detail == -1)
{
// add all
foreach (var config in GameData.CardSkinPartsData.Values)
{
var skinPart = await player.InventoryManager!
.AddSkinPartItem((ItemTypeEnum)config.Genre, config.Detail, config.Particular, config.Level, false);
if (skinPart != null) skinPartItems.Add(skinPart);
}
}
else
{
var skinPart = await player.InventoryManager!.AddSkinPartItem((ItemTypeEnum)genre, (uint)detail, (uint)particular, (uint)level, false);
if (skinPart == null)
{
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.NotFound", I18NManager.Translate("Word.SkinPart")));
return;
}
skinPartItems.Add(skinPart);
}
if (skinPartItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(skinPartItems));
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems",
I18NManager.Translate("Word.SkinPart"), skinPartItems.Count.ToString()));
}
[CommandMethod("call")]
public async ValueTask GiveAllCallItem(CommandArg arg)
{
if (!await arg.CheckOnlineTarget()) return;
if (await arg.GetOption('p') is not int particular) return;
if (await arg.GetOption('l') is not int level) return;
if (await arg.GetOption('g') is not int genre) return;
var detail = arg.GetInt(0);
var player = arg.Target!.Player!;
List<BaseGameItemInfo> callItems = [];
if (detail == -1)
{
// add all
foreach (var config in GameData.CallItemData.Values)
{
var callItem = await player.InventoryManager!
.AddCallItem((ItemTypeEnum)config.Genre, config.Detail, config.Particular, config.Level, false);
if (callItem != null) callItems.Add(callItem);
}
}
else
{
var callItem = await player.InventoryManager!.AddCallItem((ItemTypeEnum)genre, (uint)detail, (uint)particular, (uint)level, false);
if (callItem == null)
{
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.NotFound", I18NManager.Translate("Word.CallItem")));
return;
}
callItems.Add(callItem);
}
if (callItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(callItems));
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems",
I18NManager.Translate("Word.CallItem"), callItems.Count.ToString()));
}
[CommandMethod("weaponpart")]
public async ValueTask GiveAllWeaponPart(CommandArg arg)
{
if (!await arg.CheckOnlineTarget()) return;
if (await arg.GetOption('p') is not int particular) return;
if (await arg.GetOption('l') is not int level) return;
if (await arg.GetOption('g') is not int genre) return;
var detail = arg.GetInt(0);
var player = arg.Target!.Player!;
List<BaseGameItemInfo> weaponPartItems = [];
if (detail == -1)
{
// add all
foreach (var config in GameData.WeaponPartsData.Values)
{
var weaponPart = await player.InventoryManager!
.AddWeaponPartItem((ItemTypeEnum)config.Genre, config.Detail, config.Particular, config.Level, false);
if (weaponPart != null) weaponPartItems.Add(weaponPart);
}
}
else
{
var weaponPart = await player.InventoryManager!.AddWeaponPartItem((ItemTypeEnum)genre, (uint)detail, (uint)particular, (uint)level, false);
if (weaponPart == null)
{
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.NotFound", I18NManager.Translate("Word.WeaponPart")));
return;
}
weaponPartItems.Add(weaponPart);
}
if (weaponPartItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(weaponPartItems));
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems",
I18NManager.Translate("Word.WeaponPart"), weaponPartItems.Count.ToString()));
}
[CommandMethod("skin")]
public async ValueTask GiveAllSkin(CommandArg arg)
{
if (!await arg.CheckOnlineTarget()) return;
if (await arg.GetOption('p') is not int particular) return;
if (await arg.GetOption('l') is not int level) return;
if (await arg.GetOption('g') is not int genre) return;
var detail = arg.GetInt(0);
var player = arg.Target!.Player!;
List<GameSkinInfo> skinItems = [];
if (detail == -1)
{
// add all
foreach (var config in GameData.CardSkinData.Values)
{
var skin = await player.InventoryManager!
.AddSkinItem((ItemTypeEnum)config.Genre, config.Detail, config.Particular, config.Level, false);
if (skin != null) skinItems.Add(skin);
}
}
else
{
var skin = await player.InventoryManager!.AddSkinItem((ItemTypeEnum)genre, (uint)detail, (uint)particular, (uint)level, false);
if (skin == null)
{
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.NotFound", I18NManager.Translate("Word.Skin")));
return;
}
skinItems.Add(skin);
}
if (skinItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(skinItems));
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems",
I18NManager.Translate("Word.Skin"), skinItems.Count.ToString()));
}
[CommandMethod("furniture")]
public async ValueTask GiveAllHouseFurniture(CommandArg arg)
{
if (!await arg.CheckOnlineTarget()) return;
if (await arg.GetOption('p') is not int particular) return;
if (await arg.GetOption('l') is not int level) return;
if (await arg.GetOption('g') is not int genre) return;
var detail = arg.GetInt(0);
var player = arg.Target!.Player!;
List<BaseGameItemInfo> furnitureItems = [];
if (detail == -1)
{
// add all
foreach (var config in GameData.DormGiftData.Values)
{
var furniture = await player.InventoryManager!
.AddHouseFurnitureItem((ItemTypeEnum)config.Genre, config.Detail, config.Particular, config.Level, false);
if (furniture != null) furnitureItems.Add(furniture);
}
}
else
{
var furniture = await player.InventoryManager!.AddHouseFurnitureItem((ItemTypeEnum)genre, (uint)detail, (uint)particular, (uint)level, false);
if (furniture == null)
{
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.NotFound", I18NManager.Translate("Word.Furniture")));
return;
}
furnitureItems.Add(furniture);
}
if (furnitureItems.Count > 0) await player.SendPacket(new PacketNtfCallScript(furnitureItems));
await arg.SendMsg(I18NManager.Translate("Game.Command.GiveAll.GiveAllItems",
I18NManager.Translate("Word.Furniture"), furnitureItems.Count.ToString()));
} }
} }

View File

@@ -27,11 +27,11 @@ public class CharacterManager(PlayerInstance player) : BasePlayerManager(player)
Timestamp = Extensions.GetUnixSec() Timestamp = Extensions.GetUnixSec()
}; };
var weaponInfo = await Player.InventoryManager!.AddWeaponItem((ItemTypeEnum)CharacterExcel.DefaultWeaponGPDL[0], CharacterExcel.DefaultWeaponGPDL[1], CharacterExcel.DefaultWeaponGPDL[2], CharacterExcel.DefaultWeaponGPDL[3],sendPacket:false); var weaponInfo = await Player.InventoryManager!.AddWeaponItem((ItemTypeEnum)CharacterExcel.DefaultWeaponGPDL[0], CharacterExcel.DefaultWeaponGPDL[1], CharacterExcel.DefaultWeaponGPDL[2], CharacterExcel.DefaultWeaponGPDL[3],sendPacket: true);
if (weaponInfo != null) character.WeaponUniqueId = weaponInfo.UniqueId; if (weaponInfo != null) character.WeaponUniqueId = weaponInfo.UniqueId;
var skinInfo = Player.InventoryManager!.GetSkinItemGDPL(ItemTypeEnum.TYPE_CARD_SKIN, detail, particular, level) var skinInfo = Player.InventoryManager!.GetSkinItemGDPL(ItemTypeEnum.TYPE_CARD_SKIN, detail, particular, level)
?? await Player.InventoryManager!.AddSkinItem(ItemTypeEnum.TYPE_CARD_SKIN, detail, particular, level, false); ?? await Player.InventoryManager!.AddSkinItem(ItemTypeEnum.TYPE_CARD_SKIN, detail, particular, level, true);
if (skinInfo != null) character.SkinId = skinInfo.UniqueId; if (skinInfo != null) character.SkinId = skinInfo.UniqueId;
if (sendPacket) await Player.SendPacket(new PacketNtfCallScript([character])); if (sendPacket) await Player.SendPacket(new PacketNtfCallScript([character]));

View File

@@ -24,8 +24,8 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player)
TemplateId = templateId, TemplateId = templateId,
UniqueId = InventoryData.NextUniqueUid++, UniqueId = InventoryData.NextUniqueUid++,
Level = weaponLevel, Level = weaponLevel,
Break = weaponData.InitBreak, Break = GetWeaponBreak(weaponLevel),
ItemType = ItemTypeEnum.TYPE_WEAPON, ItemType = genre,
ItemCount = 1 ItemCount = 1
}; };
InventoryData.Weapons[weaponInfo.UniqueId] = weaponInfo; InventoryData.Weapons[weaponInfo.UniqueId] = weaponInfo;
@@ -35,6 +35,17 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player)
return weaponInfo; return weaponInfo;
} }
private static uint GetWeaponBreak(uint level)
{
if (level <= 20) return 1;
if (level <= 40) return 2;
if (level <= 60) return 3;
if (level <= 70) return 4;
if (level <= 80) return 5;
if (level <= 90) return 6;
return 7;
}
public GameWeaponInfo? GetWeaponItem(uint uniqueId) public GameWeaponInfo? GetWeaponItem(uint uniqueId)
{ {
return InventoryData.Weapons.GetValueOrDefault(uniqueId); return InventoryData.Weapons.GetValueOrDefault(uniqueId);
@@ -56,13 +67,13 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player)
if (genre != ItemTypeEnum.TYPE_CARD_SKIN) return null; 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 && x.Level == level); var skinData = GameData.CardSkinData.Values.FirstOrDefault(x => x.Genre == (int)genre && x.Detail == detail && x.Particular == particular && x.Level == level);
if (skinData == null) return null; if (skinData == null) return null;
var templateId = GameResourceTemplateId.FromGdpl((uint)genre,detail,particular,level); var templateId = GameResourceTemplateId.FromGdpl((uint)genre,detail,particular,level);
if (InventoryData.Items.Values.Any(x => x.TemplateId == templateId)) return null;
var skinInfo = new GameSkinInfo var skinInfo = new GameSkinInfo
{ {
TemplateId = templateId, TemplateId = templateId,
UniqueId = InventoryData.NextUniqueUid++, UniqueId = InventoryData.NextUniqueUid++,
ItemType = ItemTypeEnum.TYPE_CARD_SKIN, ItemType = genre,
ItemCount = 1 ItemCount = 1
}; };
InventoryData.Skins[skinInfo.UniqueId] = skinInfo; InventoryData.Skins[skinInfo.UniqueId] = skinInfo;
@@ -100,7 +111,7 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player)
{ {
TemplateId = templateId, TemplateId = templateId,
UniqueId = InventoryData.NextUniqueUid++, UniqueId = InventoryData.NextUniqueUid++,
ItemType = ItemTypeEnum.TYPE_AR, ItemType = genre,
ItemCount = 1 ItemCount = 1
}; };
InventoryData.Items[arInfo.UniqueId] = arInfo; InventoryData.Items[arInfo.UniqueId] = arInfo;
@@ -110,6 +121,44 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player)
return arInfo; return arInfo;
} }
public async ValueTask<GameSupportCardInfo?> AddSupportCardItem(uint detail, uint particular, uint level = 1, uint cardLevel = 1, bool sendPacket = true)
{
const ItemTypeEnum genre = ItemTypeEnum.TYPE_SUPPORT;
var spCard = GameData.SupportCardData.FirstOrDefault(x => x.Genre == (int)genre && x.Detail == detail && x.Particular == particular && x.Level == level);
if (spCard == null) return null;
var templateId = GameResourceTemplateId.FromGdpl((uint)genre, detail, particular, level);
cardLevel = Math.Clamp(cardLevel, 1, spCard.MaxLevel);
var info = new GameSupportCardInfo
{
TemplateId = templateId,
UniqueId = InventoryData.NextUniqueUid++,
ItemType = genre,
ItemCount = 1,
Level = cardLevel,
};
InventoryData.SupportCards[info.UniqueId] = info;
if (sendPacket) await Player.SendPacket(new PacketNtfCallScript([info]));
return info;
}
public GameSupportCardInfo? GetSupportCardItem(uint uniqueId)
{
return InventoryData.SupportCards.GetValueOrDefault(uniqueId);
}
public GameSupportCardInfo? GetSupportCardByTemplateId(ulong templateId)
{
return InventoryData.SupportCards.Values.FirstOrDefault(x => x.TemplateId == templateId);
}
public GameSupportCardInfo? GetSupportCardItemGDPL(ItemTypeEnum genre, uint detail, uint particular, uint level)
{
var templateId = GameResourceTemplateId.FromGdpl((uint)genre, detail, particular, level);
return InventoryData.SupportCards.Values.FirstOrDefault(x => x.TemplateId == templateId);
}
public async ValueTask<BaseGameItemInfo?> AddManifestationItem(ItemTypeEnum genre, uint detail, uint particular, uint level = 1, bool sendPacket = true) public async ValueTask<BaseGameItemInfo?> AddManifestationItem(ItemTypeEnum genre, uint detail, uint particular, uint level = 1, bool sendPacket = true)
{ {
if (genre != ItemTypeEnum.TYPE_MANIFESTATION) return null; if (genre != ItemTypeEnum.TYPE_MANIFESTATION) return null;
@@ -122,7 +171,7 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player)
{ {
TemplateId = templateId, TemplateId = templateId,
UniqueId = InventoryData.NextUniqueUid++, UniqueId = InventoryData.NextUniqueUid++,
ItemType = ItemTypeEnum.TYPE_MANIFESTATION, ItemType = genre,
ItemCount = 1 ItemCount = 1
}; };
InventoryData.Items[manifestInfo.UniqueId] = manifestInfo; InventoryData.Items[manifestInfo.UniqueId] = manifestInfo;
@@ -178,4 +227,131 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player)
return itemInfo; return itemInfo;
} }
public async ValueTask<BaseGameItemInfo?> AddWeaponSkinItem(ItemTypeEnum genre, uint detail, uint particular, uint level = 1, bool sendPacket = true)
{
if (genre != ItemTypeEnum.TYPE_WEAPON_SKIN) return null;
var skinData = GameData.WeaponSkinData.Values.FirstOrDefault(x => x.Genre == (int)genre && x.Detail == detail && x.Particular == particular && x.Level == level);
if (skinData == null) return null;
var templateId = GameResourceTemplateId.FromGdpl((uint)genre, detail, particular, level);
if (InventoryData.Items.Values.Any(x => x.TemplateId == templateId)) return null;
var skinInfo = new BaseGameItemInfo
{
TemplateId = templateId,
UniqueId = InventoryData.NextUniqueUid++,
ItemType = genre,
ItemCount = 1
};
InventoryData.Items[skinInfo.UniqueId] = skinInfo;
if (sendPacket) await Player.SendPacket(new PacketNtfCallScript([skinInfo]));
return skinInfo;
}
public async ValueTask<BaseGameItemInfo?> AddProfileItem(ItemTypeEnum genre, uint detail, uint particular, uint level = 1, bool sendPacket = true)
{
if (genre < ItemTypeEnum.TYPE_PROFILE || genre > ItemTypeEnum.TYPE_ANALYST) return null;
var profileData = GameData.ProfileData.Values.FirstOrDefault(x => x.Genre == (int)genre && x.Detail == detail && x.Particular == particular && x.Level == level);
if (profileData == null) return null;
var templateId = GameResourceTemplateId.FromGdpl((uint)genre, detail, particular, level);
if (InventoryData.Items.Values.Any(x => x.TemplateId == templateId)) return null;
var profileInfo = new BaseGameItemInfo
{
TemplateId = templateId,
UniqueId = InventoryData.NextUniqueUid++,
ItemType = genre,
ItemCount = 1
};
InventoryData.Items[profileInfo.UniqueId] = profileInfo;
if (sendPacket) await Player.SendPacket(new PacketNtfCallScript([profileInfo]));
return profileInfo;
}
public async ValueTask<BaseGameItemInfo?> AddSkinPartItem(ItemTypeEnum genre, uint detail, uint particular, uint level = 1, bool sendPacket = true)
{
if (genre != ItemTypeEnum.TYPE_CARD_SKIN_PART) return null;
var skinPartData = GameData.CardSkinPartsData.Values.FirstOrDefault(x => x.Genre == (int)genre && x.Detail == detail && x.Particular == particular && x.Level == level);
if (skinPartData == null) return null;
var templateId = GameResourceTemplateId.FromGdpl((uint)genre, detail, particular, level);
if (InventoryData.Items.Values.Any(x => x.TemplateId == templateId)) return null;
var skinPartInfo = new BaseGameItemInfo
{
TemplateId = templateId,
UniqueId = InventoryData.NextUniqueUid++,
ItemType = genre,
ItemCount = 1
};
InventoryData.Items[skinPartInfo.UniqueId] = skinPartInfo;
if (sendPacket) await Player.SendPacket(new PacketNtfCallScript([skinPartInfo]));
return skinPartInfo;
}
public async ValueTask<BaseGameItemInfo?> AddCallItem(ItemTypeEnum genre, uint detail, uint particular, uint level = 1, bool sendPacket = true)
{
if (genre != ItemTypeEnum.TYPE_CALL) return null;
var callData = GameData.CallItemData.Values.FirstOrDefault(x => x.Genre == (int)genre && x.Detail == detail && x.Particular == particular && x.Level == level);
if (callData == null) return null;
var templateId = GameResourceTemplateId.FromGdpl((uint)genre, detail, particular, level);
if (InventoryData.Items.Values.Any(x => x.TemplateId == templateId)) return null;
var callInfo = new BaseGameItemInfo
{
TemplateId = templateId,
UniqueId = InventoryData.NextUniqueUid++,
ItemType = genre,
ItemCount = 1
};
InventoryData.Items[callInfo.UniqueId] = callInfo;
if (sendPacket) await Player.SendPacket(new PacketNtfCallScript([callInfo]));
return callInfo;
}
public async ValueTask<BaseGameItemInfo?> AddWeaponPartItem(ItemTypeEnum genre, uint detail, uint particular, uint level = 1, bool sendPacket = true)
{
if (genre != ItemTypeEnum.TYPE_WEAPON_PART) return null;
var weaponPartData = GameData.WeaponPartsData.Values.FirstOrDefault(x => x.Genre == (int)genre && x.Detail == detail && x.Particular == particular && x.Level == level);
if (weaponPartData == null) return null;
var templateId = GameResourceTemplateId.FromGdpl((uint)genre, detail, particular, level);
if (InventoryData.Items.Values.Any(x => x.TemplateId == templateId)) return null;
var weaponPartInfo = new BaseGameItemInfo
{
TemplateId = templateId,
UniqueId = InventoryData.NextUniqueUid++,
ItemType = genre,
ItemCount = 1
};
InventoryData.Items[weaponPartInfo.UniqueId] = weaponPartInfo;
if (sendPacket) await Player.SendPacket(new PacketNtfCallScript([weaponPartInfo]));
return weaponPartInfo;
}
public async ValueTask<BaseGameItemInfo?> AddHouseFurnitureItem(ItemTypeEnum genre, uint detail, uint particular, uint level = 1, bool sendPacket = true)
{
if (genre != ItemTypeEnum.TYPE_HOUSE) return null;
var houseFurnitureData = GameData.DormGiftData.Values.FirstOrDefault(x => x.Genre == (int)genre && x.Detail == detail && x.Particular == particular && x.Level == level);
if (houseFurnitureData == null) return null;
var templateId = GameResourceTemplateId.FromGdpl((uint)genre, detail, particular, level);
if (InventoryData.Items.Values.Any(x => x.TemplateId == templateId)) return null;
var furnitureInfo = new BaseGameItemInfo
{
TemplateId = templateId,
UniqueId = InventoryData.NextUniqueUid++,
ItemType = genre,
ItemCount = 1
};
InventoryData.Items[furnitureInfo.UniqueId] = furnitureInfo;
if (sendPacket) await Player.SendPacket(new PacketNtfCallScript([furnitureInfo]));
return furnitureInfo;
}
} }

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

@@ -1,4 +1,4 @@
using Google.Protobuf; using Google.Protobuf;
using MikuSB.Data; using MikuSB.Data;
using MikuSB.Database; using MikuSB.Database;
using MikuSB.Database.Account; using MikuSB.Database.Account;
@@ -43,7 +43,7 @@ public class PlayerInstance(PlayerGameData data)
{ {
// new player // new player
IsNewPlayer = true; IsNewPlayer = true;
Data.Name = AccountData.GetAccountByUid(uid)?.Username; Data.Name = PlayerGameData.NormalizeDisplayName(AccountData.GetAccountByUid(uid)?.Username);
DatabaseHelper.CreateInstance(Data); DatabaseHelper.CreateInstance(Data);
@@ -79,34 +79,6 @@ public class PlayerInstance(PlayerGameData data)
await LineupManager.UpdateLineup(1, selected[0], selected[1], selected[2],false); 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(); t.Wait();
@@ -134,6 +106,7 @@ public class PlayerInstance(PlayerGameData data)
public async ValueTask OnEnterGame() public async ValueTask OnEnterGame()
{ {
if (!Initialized) await InitialPlayerManager(); if (!Initialized) await InitialPlayerManager();
Data.EnsureDisplayName();
await CharacterManager.RepairCharacterWeapons(); await CharacterManager.RepairCharacterWeapons();
await EnsureSupplies(); await EnsureSupplies();
} }
@@ -187,9 +160,9 @@ public class PlayerInstance(PlayerGameData data)
Sender = sendUid, Sender = sendUid,
Recver = recvUid, Recver = recvUid,
Emoji = emojiId ?? 0, Emoji = emojiId ?? 0,
Text = message ?? "", Text = ChatMessageHelper.NormalizeForClient(message),
Profile = Data.ToProfileProto(), Profile = Data.ToProfileProto(),
TimeStamp = (uint)Extensions.GetUnixMs() TimeStamp = ChatMessageHelper.BuildClientTimestamp()
}; };
await SendPacket(CmdIds.NtfFriendChat, data); await SendPacket(CmdIds.NtfFriendChat, data);
@@ -225,16 +198,19 @@ public class PlayerInstance(PlayerGameData data)
public Proto.Player ToPlayerProto() public Proto.Player ToPlayerProto()
{ {
BuildPlayerAttr();
var displayName = PlayerGameData.NormalizeDisplayName(Data.Name);
var proto = new Proto.Player var proto = new Proto.Player
{ {
Pid = (ulong)Data.Uid, Pid = (ulong)Data.Uid,
Account = Data.Name, Account = displayName,
Provider = Data.Name, Provider = displayName,
Name = Data.Name, Name = displayName,
Level = Data.Level, Level = Data.Level,
Sex = Data.Gender, Sex = Data.Gender,
Vigor = Data.Vigor, Vigor = Data.Vigor,
Solutions = { LineupManager.LineupData.LineupInfo.Values.Select(x => x.ToProto()) }, Solutions = { LineupManager.LineupData.LineupInfo.Values.Select(x => x.ToProto()) },
Badges = { InventoryManager.InventoryData.Items.Values.Where(x => x.ItemType == ItemTypeEnum.TYPE_BADGE).Select(x => (ulong)x.UniqueId) }
}; };
foreach (var chara in CharacterManager.CharacterData.Characters) proto.Items.Add(chara.ToProto()); foreach (var chara in CharacterManager.CharacterData.Characters) proto.Items.Add(chara.ToProto());
@@ -250,15 +226,27 @@ public class PlayerInstance(PlayerGameData data)
continue; continue;
} }
proto.Attrs[ToPackedAttrKey(gid, sid)] = val; //ToDo
//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;
} }
foreach (var x in Data.StrAttrs)
{
proto.StrAttrs[ToShiftedAttrKey(x.Gid, x.Sid)] = x.Val;
}
proto.ShowItems.AddRange(Data.ShowItems); proto.ShowItems.AddRange(Data.ShowItems);
return proto; return proto;
} }
public void SetDisplayName(string? name)
{
Data.Name = PlayerGameData.NormalizeDisplayName(name);
}
public void SetShowItem(int index, ulong itemId) public void SetShowItem(int index, ulong itemId)
{ {
if (index <= 0) if (index <= 0)
@@ -270,6 +258,22 @@ public class PlayerInstance(PlayerGameData data)
Data.ShowItems[index - 1] = itemId; Data.ShowItems[index - 1] = itemId;
} }
public void SetStrAttr(uint gid, uint sid, string value)
{
var attr = Data.StrAttrs.FirstOrDefault(x => x.Gid == gid && x.Sid == sid);
if (attr == null)
{
attr = new PlayerStrAttr
{
Gid = gid,
Sid = sid
};
Data.StrAttrs.Add(attr);
}
attr.Val = value;
}
public uint ToPackedAttrKey(uint gid, uint sid) public uint ToPackedAttrKey(uint gid, uint sid)
{ {
if (gid == 0) if (gid == 0)
@@ -286,6 +290,57 @@ public class PlayerInstance(PlayerGameData data)
return (gid << 16) | sid; return (gid << 16) | sid;
} }
public void BuildPlayerAttr(bool additional = false)
{
var bootstrapAttrs = BuildLobbyBootstrapAttrs().ToList();
if (additional) bootstrapAttrs.AddRange(BuildGirlFurnitureAttrs());
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;
}
}
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;
for (uint girlId = 0; girlId <= 50; girlId++)
{
// FurnitureStart..FurnitureEnd = 10..19
for (uint offset = 10; offset <= 19; offset++)
{
uint sid = (girlId * 50) + offset;
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()
{ {
// GuideLogic uses group 4. Value 999 is safely above every configured step count, // GuideLogic uses group 4. Value 999 is safely above every configured step count,
@@ -330,6 +385,20 @@ public class PlayerInstance(PlayerGameData data)
yield return (22, levelId, 1_700_000_000); yield return (22, levelId, 1_700_000_000);
} }
foreach (var levelId in GameData.DailyLevelData.Keys)
{
yield return (21, levelId, 7);
yield return (22, levelId, 1_700_000_000);
}
foreach (var guide in GameData.GuideData.Values)
{
yield return (4, guide.ID, 999);
}
for (uint favor = 0; favor <= 50; favor++)
yield return (101, favor * 50, 500);
// Main Scene 0 mean default scene // Main Scene 0 mean default scene
yield return (132, 1, 0); yield return (132, 1, 0);
} }

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<CETCompat>false</CETCompat> <CETCompat>false</CETCompat>
@@ -20,9 +20,9 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" /> <ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\MikuSB.Loader\MikuSB.Loader.csproj" />
<ProjectReference Include="..\TcpSharp\TcpSharp.csproj" /> <ProjectReference Include="..\TcpSharp\TcpSharp.csproj" />
<ProjectReference Include="..\Proto\Proto.csproj" /> <ProjectReference Include="..\Proto\Proto.csproj" />
<PackageReference Include="System.IO.Pipelines" Version="9.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,26 @@
using System.Text.Json;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Daily;
[CallGSApi("Daily_SetSelectSuit")]
public class Daily_SetSelectSuit : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<GirlWeaponSkinParam>(param);
if (req == null)
{
await CallGSRouter.SendScript(connection, "Daily_SetSelectSuit", "{}");
return;
}
var rsp = $"{{\"SuitId\":{req.Suit}}}";
await CallGSRouter.SendScript(connection, "Daily_SetSelectSuit", rsp);
}
}
internal sealed class GirlWeaponSkinParam
{
public uint Type { get; set; }
public uint Suit { get; set; }
}

View File

@@ -12,11 +12,19 @@ public class EnterGirlRoom : ICallGSHandler
var req = JsonSerializer.Deserialize<EnterGirlRoomParam>(param); var req = JsonSerializer.Deserialize<EnterGirlRoomParam>(param);
var response = new JsonObject var response = new JsonObject
{ {
["nCardId"] = req?.CardId ?? 1, ["nCardId"] = 0,
["nSkinId"] = req?.SkinId ?? 0, ["nSkinId"] = 0,
["bOpen"] = true ["bOpen"] = false
}; };
if (req == null)
{
await CallGSRouter.SendScript(connection, "EnterGirlRoom", response.ToJsonString());
return;
}
response["nCardId"] = req.CardId;
response["nSkinId"] = req.SkinId;
response["bOpen"] = true;
await CallGSRouter.SendScript(connection, "EnterGirlRoom", response.ToJsonString()); await CallGSRouter.SendScript(connection, "EnterGirlRoom", response.ToJsonString());
} }
} }
@@ -26,6 +34,6 @@ internal sealed class EnterGirlRoomParam
[JsonPropertyName("nSkinId")] [JsonPropertyName("nSkinId")]
public int SkinId { get; set; } public int SkinId { get; set; }
[JsonPropertyName("nCardID")] [JsonPropertyName("nCardId")]
public uint CardId { get; set; } public uint CardId { get; set; }
} }

View File

@@ -0,0 +1,52 @@
using MikuSB.Data;
using MikuSB.Database.Inventory;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Girl;
[CallGSApi("GirlSkinParts_Update")]
public class GirlSkinParts_Update : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<GirlSkinPartsUpdateParam>(param);
if (req == null)
{
await CallGSRouter.SendScript(connection, "GirlSkinParts_Update", "{\"sErr\":\"error.BadParam\"}");
return;
}
var player = connection.Player!;
var data = new List<GameSkinInfo>();
foreach(var partId in req.PartsId)
{
var partData = player.InventoryManager.GetNormalItem(partId);
if (partData == null) continue;
var partExcel = GameData.CardSkinPartsData.Values.FirstOrDefault(x => x.TemplateId == partData.TemplateId);
if (partExcel == null) continue;
var skinData = player.InventoryManager.GetSkinItem(req.SkinId);
if (skinData == null) continue;
skinData.PartSlots[partExcel.Detail] = partData.UniqueId;
data.Add(skinData);
}
var sync = new NtfSyncPlayer
{
Items = { data.Select(x => x.ToProto()) }
};
await CallGSRouter.SendScript(connection, "GirlSkinParts_Update", "{}", sync);
}
}
internal sealed class GirlSkinPartsUpdateParam
{
[JsonPropertyName("tbPartsID")]
public List<uint> PartsId { get; set; } = [];
[JsonPropertyName("nSkinId")]
public uint SkinId { get; set; }
}

View File

@@ -1,4 +1,9 @@
using MikuSB.Proto; using MikuSB.Data;
using MikuSB.Database;
using MikuSB.Database.Inventory;
using MikuSB.Enums.Item;
using MikuSB.GameServer.Game.Player;
using MikuSB.Proto;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@@ -11,9 +16,10 @@ public class GirlSkin_ChangeSkinType : ICallGSHandler
public async Task Handle(Connection connection, string param, ushort seqNo) public async Task Handle(Connection connection, string param, ushort seqNo)
{ {
var req = JsonSerializer.Deserialize<ChangeSkinTypeParam>(param); var req = JsonSerializer.Deserialize<ChangeSkinTypeParam>(param);
var skinType = ClampClientSkinType(req?.Type ?? 0);
var response = new JsonObject var response = new JsonObject
{ {
["nType"] = req?.Type ?? 1, ["nType"] = skinType,
["nSkinId"] = req?.SkinId ["nSkinId"] = req?.SkinId
}; };
if (req == null) if (req == null)
@@ -21,16 +27,22 @@ public class GirlSkin_ChangeSkinType : ICallGSHandler
await CallGSRouter.SendScript(connection, "GirlSkin_ChangeSkinType", response.ToJsonString()); await CallGSRouter.SendScript(connection, "GirlSkin_ChangeSkinType", response.ToJsonString());
return; return;
} }
var player = connection.Player!; var player = connection.Player!;
var skinData = player.InventoryManager.GetSkinItem(req.SkinId); var skinData = GetOrCreateSkinItem(player, req.SkinId);
if (skinData != null)
skinData.SkinType = skinType;
player.InventoryManager.InventoryData.SkinTypesBySkinId ??= [];
player.InventoryManager.InventoryData.SkinTypesBySkinId[req.SkinId] = skinType;
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
if (skinData == null) if (skinData == null)
{ {
await CallGSRouter.SendScript(connection, "GirlSkin_ChangeSkinType", response.ToJsonString()); await CallGSRouter.SendScript(connection, "GirlSkin_ChangeSkinType", response.ToJsonString());
return; return;
} }
skinData.SkinType = req.Type;
var sync = new NtfSyncPlayer var sync = new NtfSyncPlayer
{ {
Items = { skinData.ToProto() } Items = { skinData.ToProto() }
@@ -38,6 +50,42 @@ public class GirlSkin_ChangeSkinType : ICallGSHandler
await CallGSRouter.SendScript(connection, "GirlSkin_ChangeSkinType", response.ToJsonString(), sync); await CallGSRouter.SendScript(connection, "GirlSkin_ChangeSkinType", response.ToJsonString(), sync);
} }
internal static uint ClampClientSkinType(uint skinType)
{
return Math.Min(skinType, 1);
}
internal static GameSkinInfo? GetOrCreateSkinItem(PlayerInstance player, uint skinId)
{
var inventoryData = player.InventoryManager.InventoryData;
if (inventoryData.Skins.TryGetValue(skinId, out var skinInfo))
return skinInfo;
if (!GameData.CardSkinData.TryGetValue(skinId, out var skinData))
return null;
var templateId = GameResourceTemplateId.FromGdpl(skinData.Genre, skinData.Detail, skinData.Particular, skinData.Level);
skinInfo = player.InventoryManager.GetSkinItemByTemplateId(templateId);
if (skinInfo != null)
{
inventoryData.Skins.Remove(skinInfo.UniqueId);
skinInfo.UniqueId = skinId;
}
else
{
skinInfo = new GameSkinInfo
{
UniqueId = skinId,
TemplateId = templateId,
ItemType = ItemTypeEnum.TYPE_CARD_SKIN,
ItemCount = 1
};
}
inventoryData.Skins[skinId] = skinInfo;
return skinInfo;
}
} }
internal sealed class ChangeSkinTypeParam internal sealed class ChangeSkinTypeParam

View File

@@ -0,0 +1,47 @@
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Girl;
[CallGSApi("GirlWeaponSkin_Change")]
public class GirlWeaponSkin_Change : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<GirlWeaponSkinParam>(param);
if (req == null)
{
await CallGSRouter.SendScript(connection, "GirlWeaponSkin_Change", "{\"err\":\"error.BadParam\"}");
return;
}
var player = connection.Player!;
var cardData = player.CharacterManager.GetCharacterByGUID(req.CardId);
if (cardData == null) return;
var skinData = player.InventoryManager.GetNormalItem(req.SkinId);
if (skinData == null)
{
await CallGSRouter.SendScript(connection, "GirlWeaponSkin_Change", "{\"err\":\"error.BadParam\"}");
return;
}
cardData.WeaponSkinId = req.SkinId;
var sync = new NtfSyncPlayer
{
Items = { cardData.ToProto() }
};
await CallGSRouter.SendScript(connection, "GirlWeaponSkin_Change", "null", sync);
}
}
internal sealed class GirlWeaponSkinParam
{
[JsonPropertyName("nCardId")]
public uint CardId { get; set; }
[JsonPropertyName("nSkinId")]
public uint SkinId { get; set; }
}

View File

@@ -0,0 +1,37 @@
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Girl;
[CallGSApi("RoleCard_SetSupporterTeamIndex")]
public class RoleCard_SetSupporterTeamIndex : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<SetSupporterTeamIndexParam>(param);
if (req == null)
{
await CallGSRouter.SendScript(connection, "RoleCard_SetSupporterTeamIndex", "{\"err\":\"error.BadParam\"}");
return;
}
var player = connection.Player!;
var cardData = player.CharacterManager.GetCharacterByGUID(req.CardId);
if (cardData == null) return;
cardData.SupportTeamIndex = req.Index;
var sync = new NtfSyncPlayer
{
Items = { cardData.ToProto() }
};
await CallGSRouter.SendScript(connection, "RoleCard_SetSupporterTeamIndex", "null", sync);
}
}
internal sealed class SetSupporterTeamIndexParam
{
[JsonPropertyName("Id")]
public uint CardId { get; set; }
public uint Index { get; set; }
}

View File

@@ -0,0 +1,237 @@
using MikuSB.Proto;
using System.Text.Json.Nodes;
namespace MikuSB.GameServer.Server.CallGS.Handlers.House;
[HouseFunc("ArcadeGameEnterMainUI")]
public class ArcadeGameEnterMainUI : IHouseFuncHandler
{
private static readonly int[] ArcadeUnlockedGirlList = [1, 5, 13, 23, 10];
public async Task Handle(Connection connection, string param)
{
var unlockGirls = new JsonArray();
foreach (var girlId in ArcadeUnlockedGirlList)
unlockGirls.Add(girlId);
var rsp = new JsonObject
{
["tbUnlockGirlList"] = unlockGirls,
["FuncName"] = "ArcadeGameEnterMainUI"
};
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Success(rsp));
}
}
[HouseFunc("ArcadeGameEnter")]
public class ArcadeGameEnter : IHouseFuncHandler
{
private static readonly Random Random = new();
public async Task Handle(Connection connection, string param)
{
var rsp = new JsonObject
{
["FuncName"] = "ArcadeGameEnter",
["nSeed"] = Random.Next(1, 1_000_000_000)
};
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Success(rsp));
}
}
[HouseFunc("ArcadeGameSettlement")]
public class ArcadeGameSettlement : IHouseFuncHandler
{
private const uint HouseArcadeInfoStart = 18000;
private const uint ArcadeAttrExpOffset = 2;
private const uint ArcadeAttrEndlessScoreOffset = 4;
private const uint ArcadeAttrGirlEndlessModeStateOffset = 5;
private const uint ArcadeAttrGirlNormalModeStateOffset = 10;
private const uint ArcadeAttrGirlNormalModeStateEndOffset = 35;
private const uint ArcadeAttrConditionValStartOffset = 36;
private const uint ArcadeAttrConditionValEndOffset = 55;
private const uint ArcadeAttrPropUseStartOffset = 56;
private const uint ArcadeAttrPropUseEndOffset = 250;
private const int ArcadeGameModeNormal = 1;
private const int ArcadeGameModeEndless = 2;
public async Task Handle(Connection connection, string param)
{
var root = HouseJson.ParseObject(param);
if (root == null) return;
var player = connection.Player!;
var sync = new NtfSyncPlayer();
var modeType = HouseJson.NumField(root, "nModeType");
var finishRound = Math.Max(0, HouseJson.NumField(root, "nFinishRound"));
var maxScore = Math.Max(0, HouseJson.NumField(root, "nMaxScore"));
if (root["tbConditionVal"] is JsonArray conditions)
{
foreach (var conditionNode in conditions)
{
if (conditionNode is not JsonArray condition || condition.Count < 2) continue;
var id = HouseJson.ToInt(condition[0]);
var value = Math.Max(0, HouseJson.ToInt(condition[1]));
if (id < 0) continue;
var sid = HouseArcadeInfoStart + ArcadeAttrConditionValStartOffset + (uint)id;
if (sid > HouseArcadeInfoStart + ArcadeAttrConditionValEndOffset) continue;
var prev = HouseAttr.Read(player, sid);
if ((uint)value > prev)
await HouseAttr.SetAsync(connection, sid, (uint)value, sync);
}
}
if (root["tbPropUse"] is JsonArray propUses)
{
var slotStart = HouseArcadeInfoStart + ArcadeAttrPropUseStartOffset;
var slotEnd = HouseArcadeInfoStart + ArcadeAttrPropUseEndOffset;
var slotState = new Dictionary<uint, ArcadePropUseSlot>();
var emptySlots = new Queue<uint>();
for (uint sid = slotStart; sid <= slotEnd; sid++)
{
var value = HouseAttr.Read(player, sid);
if (value == 0)
{
emptySlots.Enqueue(sid);
continue;
}
slotState[sid] = new ArcadePropUseSlot
{
Type = (byte)(value & 0xff),
Id = (byte)((value >> 8) & 0xff),
Count = (ushort)((value >> 16) & 0xffff)
};
}
foreach (var propUseNode in propUses)
{
if (propUseNode is not JsonArray propUse || propUse.Count < 3) continue;
var type = HouseJson.ToInt(propUse[0]) & 0xff;
var id = HouseJson.ToInt(propUse[1]) & 0xff;
var count = HouseJson.ToInt(propUse[2]);
if (count <= 0) continue;
uint? foundSid = null;
foreach (var (sid, slot) in slotState)
{
if (slot.Type == type && slot.Id == id)
{
foundSid = sid;
break;
}
}
if (foundSid != null)
{
var slot = slotState[foundSid.Value];
slot.Count = (ushort)Math.Min(0xffff, slot.Count + count);
slotState[foundSid.Value] = slot;
await HouseAttr.SetAsync(connection, foundSid.Value, HouseAttr.PackArcadePropUse(slot.Type, slot.Id, slot.Count), sync);
}
else if (emptySlots.Count > 0)
{
var sid = emptySlots.Dequeue();
var packedCount = (ushort)Math.Min(0xffff, count);
slotState[sid] = new ArcadePropUseSlot { Type = type, Id = id, Count = packedCount };
await HouseAttr.SetAsync(connection, sid, HouseAttr.PackArcadePropUse(type, id, packedCount), sync);
}
}
}
var girlList = root["tbGirlDataList"] as JsonArray;
var nAddExp = 0;
if (modeType == ArcadeGameModeNormal)
{
if (girlList != null)
{
foreach (var girlNode in girlList)
{
var girlId = HouseJson.ToInt(girlNode);
if (girlId <= 0) continue;
var sid = HouseArcadeInfoStart + ArcadeAttrGirlNormalModeStateOffset + (uint)girlId;
if (sid > HouseArcadeInfoStart + ArcadeAttrGirlNormalModeStateEndOffset) continue;
var prev = HouseAttr.Read(player, sid);
await HouseAttr.SetAsync(connection, sid, prev + (uint)finishRound, sync);
}
}
nAddExp = finishRound * 10;
}
else if (modeType == ArcadeGameModeEndless)
{
var scoreSid = HouseArcadeInfoStart + ArcadeAttrEndlessScoreOffset;
var prevScore = HouseAttr.Read(player, scoreSid);
if ((uint)maxScore > prevScore)
await HouseAttr.SetAsync(connection, scoreSid, (uint)maxScore, sync);
var bitsSid = HouseArcadeInfoStart + ArcadeAttrGirlEndlessModeStateOffset;
var bits = HouseAttr.Read(player, bitsSid);
var originalBits = bits;
if (girlList != null)
{
foreach (var girlNode in girlList)
{
var girlId = HouseJson.ToInt(girlNode);
if (girlId is > 0 and < 31)
bits |= 1u << girlId;
}
}
if (bits != originalBits)
await HouseAttr.SetAsync(connection, bitsSid, bits, sync);
nAddExp = maxScore / 10;
}
if (nAddExp > 0)
{
var expSid = HouseArcadeInfoStart + ArcadeAttrExpOffset;
var prevExp = HouseAttr.Read(player, expSid);
await HouseAttr.SetAsync(connection, expSid, prevExp + (uint)nAddExp, sync);
}
var rsp = new JsonObject
{
["nAddExp"] = nAddExp,
["FuncName"] = "ArcadeGameSettlement"
};
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Success(rsp), sync);
}
private struct ArcadePropUseSlot
{
public int Type { get; set; }
public int Id { get; set; }
public ushort Count { get; set; }
}
}
[HouseFunc("ArcadeGameLogSettlement")]
public class ArcadeGameLogSettlement : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var rsp = new JsonObject { ["FuncName"] = "ArcadeGameLogSettlement" };
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Success(rsp));
}
}
[HouseFunc("ArcadeGameGetLevelReward")]
public class ArcadeGameGetLevelReward : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var rsp = new JsonObject { ["FuncName"] = "ArcadeGameGetLevelReward" };
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Success(rsp));
}
}
[HouseFunc("ArcadeGameGetAchReward")]
public class ArcadeGameGetAchReward : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var rsp = new JsonObject { ["FuncName"] = "ArcadeGameGetAchReward" };
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Success(rsp));
}
}

View File

@@ -0,0 +1,119 @@
using MikuSB.Proto;
using System.Text.Json.Nodes;
namespace MikuSB.GameServer.Server.CallGS.Handlers.House;
[HouseFunc("GirlRegister")]
public class GirlRegister : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var root = HouseJson.ParseObject(param);
if (root == null) return;
var girlId = HouseJson.NumField(root, "GirlId");
var floorId = HouseJson.NumField(root, "FloorId");
var sync = new NtfSyncPlayer();
if (girlId > 0)
{
var bedroomSid = HouseAttr.GetNextBedroomSid(connection.Player!, (uint)floorId);
await HouseAttr.SetAsync(connection, HouseAttr.GirlRoomNumSid(girlId), HouseAttr.BedroomRegisteredNoRoom, sync);
if (bedroomSid != 0) await HouseAttr.SetAsync(connection, bedroomSid, (uint)girlId, sync);
}
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Synthesize(root), sync);
}
}
[HouseFunc("SetBedroomGirlId")]
public class SetBedroomGirlId : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var root = HouseJson.ParseObject(param);
if (root == null) return;
var bedroomId = HouseJson.NumField(root, "BedroomId");
var girlId = HouseJson.NumField(root, "GirlId");
var sync = new NtfSyncPlayer();
if (bedroomId > 0 && girlId > 0)
await HouseAttr.MoveGirlIntoRoomAsync(connection, girlId, bedroomId, sync);
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Synthesize(root), sync);
}
}
[HouseFunc("GirlRoomChange")]
public class GirlRoomChange : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var root = HouseJson.ParseObject(param);
if (root == null) return;
var girlId = HouseJson.NumField(root, "GirlId");
var roomId = HouseJson.NumField(root, "RoomId");
var oldRoomId = girlId > 0 ? (int)HouseAttr.Read(connection.Player!, HouseAttr.GirlRoomNumSid(girlId)) : 0;
var sync = new NtfSyncPlayer();
if (girlId > 0 && roomId > 0)
await HouseAttr.MoveGirlIntoRoomAsync(connection, girlId, roomId, sync);
var rsp = new JsonObject
{
["FuncName"] = "GirlRoomChangeSuccess",
["GirlId"] = girlId,
["OldRoomId"] = oldRoomId,
["NewRoomId"] = roomId
};
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Success(rsp), sync);
}
}
[HouseFunc("GirlLeaveRoom")]
public class GirlLeaveRoom : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var root = HouseJson.ParseObject(param);
if (root == null) return;
var bedroomId = HouseJson.NumField(root, "BedroomId");
var girlId = HouseJson.NumField(root, "GirlId");
var sync = new NtfSyncPlayer();
if (bedroomId > 0 && girlId > 0)
{
await HouseAttr.SetAsync(connection, HouseAttr.BedroomSlotSid(bedroomId), 0, sync);
await HouseAttr.SetAsync(connection, HouseAttr.GirlRoomNumSid(girlId), HouseAttr.BedroomRegisteredNoRoom, sync);
}
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Synthesize(root), sync);
}
}
[HouseFunc("ExchangeRoomGirl")]
public class ExchangeRoomGirl : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var root = HouseJson.ParseObject(param);
if (root == null) return;
var roomId1 = HouseJson.NumField(root, "RoomId1");
var roomId2 = HouseJson.NumField(root, "RoomId2");
var sync = new NtfSyncPlayer();
if (roomId1 > 0 && roomId2 > 0 && roomId1 != roomId2)
{
var slot1 = HouseAttr.BedroomSlotSid(roomId1);
var slot2 = HouseAttr.BedroomSlotSid(roomId2);
var girl1 = HouseAttr.Read(connection.Player!, slot1);
var girl2 = HouseAttr.Read(connection.Player!, slot2);
await HouseAttr.SetAsync(connection, slot1, girl2, sync);
await HouseAttr.SetAsync(connection, slot2, girl1, sync);
if (girl1 > 0) await HouseAttr.SetAsync(connection, HouseAttr.GirlRoomNumSid((int)girl1), (uint)roomId2, sync);
if (girl2 > 0) await HouseAttr.SetAsync(connection, HouseAttr.GirlRoomNumSid((int)girl2), (uint)roomId1, sync);
}
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Synthesize(root), sync);
}
}

View File

@@ -0,0 +1,29 @@
using MikuSB.Proto;
namespace MikuSB.GameServer.Server.CallGS.Handlers.House;
[HouseFunc("SetGroupFurIndex")]
public class SetGroupFurIndex : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var root = HouseJson.ParseObject(param);
if (root == null) return;
var areaId = HouseJson.NumField(root, "AreaId");
var groupId = HouseJson.NumField(root, "GroupId");
var index = HouseJson.NumField(root, "Index");
var sync = new NtfSyncPlayer();
if (areaId > 0 && groupId is >= 1 and <= 10)
{
var sid = (uint)(areaId * 50 + 20);
var prev = HouseAttr.Read(connection.Player!, sid);
var shift = (groupId - 1) * 3;
var mask = ~(0b111u << shift);
var next = (prev & mask) | (((uint)index & 0b111u) << shift);
await HouseAttr.SetAsync(connection, sid, next, sync, sendImmediate: true);
}
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Synthesize(root), sync);
}
}

View File

@@ -0,0 +1,60 @@
using MikuSB.Proto;
using System.Text.Json.Nodes;
namespace MikuSB.GameServer.Server.CallGS.Handlers.House;
[HouseFunc("SetPlayerRingInfo")]
public class SetPlayerRingInfo : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var root = HouseJson.ParseObject(param);
if (root == null) return;
var ringId = HouseJson.NumField(root, "RingId");
var ringPos = HouseJson.NumField(root, "RingPos");
var ringOffset = HouseJson.NumField(root, "RingOffset");
var sync = new NtfSyncPlayer();
if (ringPos is >= 1 and <= 10 && ringOffset is >= 0 and <= 2)
{
var sid = HouseAttr.PlayerRingInfoSidBase + (uint)ringPos;
var prev = HouseAttr.Read(connection.Player!, sid);
var shift = ringOffset * 10;
var mask = ~(0x3ffu << shift);
var next = (prev & mask) | (((uint)ringId & 0x3ffu) << shift);
await HouseAttr.SetAsync(connection, sid, next, sync, deleteIfZero: true, sendImmediate: true);
}
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Synthesize(root), sync);
}
}
[HouseFunc("ReadGirlLoveStory")]
public class ReadGirlLoveStory : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var root = HouseJson.ParseObject(param);
if (root == null) return;
var girlId = HouseJson.NumField(root, "GirlId");
var index = HouseJson.NumField(root, "Index");
var sync = new NtfSyncPlayer();
if (girlId > 0 && index is >= 0 and <= 30)
{
var sid = (uint)(girlId * 50 + 2);
var prev = HouseAttr.Read(connection.Player!, sid);
var next = prev | (1u << index);
await HouseAttr.SetAsync(connection, sid, next, sync, sendImmediate: true);
}
var rsp = new JsonObject
{
["GirlId"] = girlId,
["Index"] = index,
["tbReward"] = new JsonArray(),
["FuncName"] = "ReadGirlLoveStorySuccess"
};
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Success(rsp), sync);
}
}

View File

@@ -0,0 +1,90 @@
using MikuSB.Proto;
using System.Text.Json.Nodes;
namespace MikuSB.GameServer.Server.CallGS.Handlers.House;
[HouseFunc("UnLockWaterGunGameItem")]
public class UnLockWaterGunGameItem : IHouseFuncHandler
{
private const uint HouseHotSpringInfoStart = 15000;
private const uint WaterGunItemCollectBegin = 17;
public async Task Handle(Connection connection, string param)
{
var root = HouseJson.ParseObject(param);
if (root == null) return;
var sync = new NtfSyncPlayer();
if (root["tbItem"] is JsonArray items)
{
var touched = new Dictionary<uint, uint>();
foreach (var node in items)
{
var itemId = HouseJson.ToInt(node);
if (itemId is < 0 or > 60) continue;
var offset = itemId / 30;
var bit = itemId % 30;
var sid = HouseHotSpringInfoStart + WaterGunItemCollectBegin + (uint)offset;
var cur = touched.TryGetValue(sid, out var existing)
? existing
: HouseAttr.Read(connection.Player!, sid);
touched[sid] = cur | (1u << bit);
}
foreach (var (sid, value) in touched)
await HouseAttr.SetAsync(connection, sid, value, sync);
}
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Synthesize(root), sync);
}
}
[HouseFunc("RouletteEnd")]
public class RouletteEnd : IHouseFuncHandler
{
private const uint HouseHotSpringInfoStart = 15000;
private const uint MaxRoundsInWaterGun = 16;
private const uint HasCompleteGameGuide = 19;
private const uint HotSpringGirlInfoStart = 15100;
private const uint HotSpringGirlAttrCount = 10;
private const uint HotSpringGirlHasCompleteStory = 1;
private const int RoulettePlayModePlot = 1;
private const int RoulettePlayModeEndless = 2;
private const int RouletteEndReasonSuccess = 0;
private const int RouletteTutorialGirlId = 5;
public async Task Handle(Connection connection, string param)
{
var root = HouseJson.ParseObject(param);
if (root == null) return;
var modeType = HouseJson.NumField(root, "ModeType");
var girlId = HouseJson.NumField(root, "GirlId");
var isSuccess = HouseJson.NumField(root, "IsSuccess");
var rounds = HouseJson.NumField(root, "Rounds");
var sync = new NtfSyncPlayer();
if (isSuccess == RouletteEndReasonSuccess)
{
if (modeType == RoulettePlayModePlot && girlId > 0)
{
await HouseAttr.SetAsync(
connection,
HotSpringGirlInfoStart + (uint)(girlId * (int)HotSpringGirlAttrCount) + HotSpringGirlHasCompleteStory,
1,
sync);
if (girlId == RouletteTutorialGirlId)
await HouseAttr.SetAsync(connection, HouseHotSpringInfoStart + HasCompleteGameGuide, 1, sync);
}
else if (modeType == RoulettePlayModeEndless && rounds > 0)
{
var sid = HouseHotSpringInfoStart + MaxRoundsInWaterGun;
var prev = HouseAttr.Read(connection.Player!, sid);
if ((uint)rounds > prev)
await HouseAttr.SetAsync(connection, sid, (uint)rounds, sync);
}
}
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Synthesize(root), sync);
}
}

View File

@@ -0,0 +1,97 @@
using MikuSB.Proto;
using System.Text.Json.Nodes;
namespace MikuSB.GameServer.Server.CallGS.Handlers.House;
[HouseFunc("ChangeNpcSuit")]
public class ChangeNpcSuit : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var root = HouseJson.ParseObject(param);
if (root == null) return;
var npcId = HouseJson.NumField(root, "NpcId");
var suitId = HouseJson.NumField(root, "SuitId");
var sync = new NtfSyncPlayer();
await HouseAttr.SetAsync(connection, (uint)(npcId * 50 + 7), (uint)Math.Max(0, suitId), sync, sendImmediate: true);
var rsp = new JsonObject
{
["NpcId"] = npcId,
["SuitId"] = suitId,
["npcSuit"] = new JsonObject
{
["NpcId"] = npcId,
["SuitId"] = suitId
},
["FuncName"] = "ChangeNpcSuitSuccess"
};
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Success(rsp), sync);
}
}
[HouseFunc("ChangeNpcSuitByAreaId")]
public class ChangeNpcSuitByAreaId : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var root = HouseJson.ParseObject(param);
if (root == null) return;
var npcId = HouseJson.NumField(root, "NpcId");
var areaId = HouseJson.NumField(root, "AreaId");
var suitId = HouseJson.NumField(root, "SuitId");
var sync = new NtfSyncPlayer();
if (npcId > 0 && areaId > 0)
{
uint[] slotSids = Enumerable.Range(24, 6).Select(i => (uint)(npcId * 50 + i)).ToArray();
uint? chosenSid = null;
foreach (var sid in slotSids)
{
if ((HouseAttr.Read(connection.Player!, sid) & 0xffffu) == (uint)areaId)
{
chosenSid = sid;
break;
}
}
if (chosenSid == null)
{
foreach (var sid in slotSids)
{
if (HouseAttr.Read(connection.Player!, sid) == 0)
{
chosenSid = sid;
break;
}
}
}
chosenSid ??= slotSids[0];
var packed = (((uint)suitId & 0xffffu) << 16) | ((uint)areaId & 0xffffu);
await HouseAttr.SetAsync(connection, chosenSid.Value, packed, sync, sendImmediate: true);
}
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Synthesize(root), sync);
}
}
[HouseFunc("ChangeGirlBeachSuitId")]
public class ChangeGirlBeachSuitId : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var root = HouseJson.ParseObject(param);
if (root == null) return;
var npcId = HouseJson.NumField(root, "NpcId");
var suitId = HouseJson.NumField(root, "SuitId");
var sync = new NtfSyncPlayer();
if (npcId > 0)
await HouseAttr.SetAsync(connection, (uint)(npcId * 50 + 8), (uint)Math.Max(0, suitId), sync, sendImmediate: true);
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Synthesize(root), sync);
}
}

View File

@@ -0,0 +1,74 @@
using System.Text.Json.Nodes;
namespace MikuSB.GameServer.Server.CallGS.Handlers.House;
// PubGameEnter — returns nSeed for client-side game initialization.
[HouseFunc("PubGameEnter")]
public class PubGameEnter : IHouseFuncHandler
{
private static readonly Random Random = new();
public async Task Handle(Connection connection, string param)
{
var rsp = new JsonObject
{
["FuncName"] = "PubGameEnter",
["nSeed"] = Random.Next(1, 1_000_000_000),
["nModeType"] = 1,
["bIsGuide"] = false,
["bHasTry"] = false
};
await CallGSRouter.SendScript(connection, "House_Request", rsp.ToJsonString());
}
}
[HouseFunc("PubGameMulExit")]
public class PubGameMulExit : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var rsp = new JsonObject { ["FuncName"] = "PubGameMulExit" };
await CallGSRouter.SendScript(connection, "House_Request", rsp.ToJsonString());
}
}
// PubGameSettlement — nAddExp=0 on private server.
[HouseFunc("PubGameSettlement")]
public class PubGameSettlement : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var rsp = new JsonObject { ["FuncName"] = "PubGameSettlement", ["nAddExp"] = 0 };
await CallGSRouter.SendScript(connection, "House_Request", rsp.ToJsonString());
}
}
[HouseFunc("PubGameGetReward")]
public class PubGameGetReward : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var rsp = new JsonObject { ["FuncName"] = "PubGameGetReward" };
await CallGSRouter.SendScript(connection, "House_Request", rsp.ToJsonString());
}
}
[HouseFunc("PubGameGetAchReward")]
public class PubGameGetAchReward : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var rsp = new JsonObject { ["FuncName"] = "PubGameGetAchReward" };
await CallGSRouter.SendScript(connection, "House_Request", rsp.ToJsonString());
}
}
[HouseFunc("PubGameAchievementFinish")]
public class PubGameAchievementFinish : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var rsp = new JsonObject { ["FuncName"] = "PubGameAchievementFinish" };
await CallGSRouter.SendScript(connection, "House_Request", rsp.ToJsonString());
}
}

View File

@@ -0,0 +1,200 @@
using MikuSB.Database.Player;
using MikuSB.GameServer.Game.Player;
using MikuSB.Proto;
using System.Text.Json.Nodes;
namespace MikuSB.GameServer.Server.CallGS.Handlers.House;
internal static class HouseRequestScript
{
internal static readonly Dictionary<string, string> FuncNameMap = new()
{
["ChangeGirlBeachSuitId"] = "ChangeGirlBeachSuitIdSuccess",
["ChangeNpcSuitByAreaId"] = "ChangeNpcSuitByAreaIdRsp",
["SetGroupFurIndex"] = "SetGroupFurSuccess",
["RouletteEnd"] = "RouletteEndRsp",
["UnLockWaterGunGameItem"] = "UnLockWaterGunGameItemRsp",
["ChangeNpcSuit"] = "ChangeNpcSuitSuccess",
["GiveGiftToArea"] = "GiveGiftToAreaSuccess",
["BuyRefreshMapTimes"] = "BuyRefreshMapTimesSuccess",
["CompleteHousePuzzleTask"] = "CompleteHousePuzzleTaskSuccess",
["CompleteStarWishTask"] = "CompleteStarWishTaskSuccess",
["SetPlayerRingInfo"] = "SetPlayerRingInfoSuccess",
["ReadGirlLoveStory"] = "ReadGirlLoveStorySuccess",
["GirlRoomChange"] = "GirlRoomChangeSuccess",
["GirlRegister"] = "GirlRegisterSuccess"
};
internal static string Synthesize(JsonObject request)
{
var funcName = request["FuncName"]?.GetValue<string>();
var response = CreateSuccessObject();
foreach (var (key, value) in request)
{
if (key is "FuncName" or "tblog")
continue;
response[key] = value?.DeepClone();
}
if (!string.IsNullOrEmpty(funcName))
response["FuncName"] = FuncNameMap.TryGetValue(funcName, out var renamed) ? renamed : funcName;
return response.ToJsonString();
}
internal static string Success(JsonObject? extra = null) => CreateSuccessObject(extra).ToJsonString();
internal static JsonObject CreateSuccessObject(JsonObject? extra = null)
{
var response = new JsonObject
{
["bSuccess"] = true,
["nResult"] = 0
};
if (extra != null)
{
foreach (var (key, value) in extra)
response[key] = value?.DeepClone();
}
return response;
}
}
internal static class HouseJson
{
internal static JsonObject? ParseObject(string json)
{
try
{
return JsonNode.Parse(json) as JsonObject;
}
catch
{
return null;
}
}
internal static int NumField(JsonObject? obj, string name) => ToInt(obj?[name]);
internal static int ToInt(JsonNode? node)
{
if (node == null) return 0;
try
{
if (node is JsonValue value)
{
if (value.TryGetValue<int>(out var i)) return i;
if (value.TryGetValue<uint>(out var u)) return unchecked((int)u);
if (value.TryGetValue<long>(out var l)) return unchecked((int)l);
if (value.TryGetValue<string>(out var s) && int.TryParse(s, out var parsed)) return parsed;
}
}
catch
{
}
return 0;
}
}
internal static class HouseAttr
{
internal const uint Gid = 101;
internal const uint BedroomStartSid = 2550;
internal const uint BedroomRegisteredNoRoom = 100;
internal const uint PlayerRingInfoSidBase = 3174;
internal const uint BedroomPerFloor = 8;
internal static uint Read(PlayerInstance player, uint sid)
{
var attr = player.Data.Attrs.FirstOrDefault(x => x.Gid == Gid && x.Sid == sid);
return attr?.Val ?? 0;
}
internal static async Task SetAsync(
Connection connection,
uint sid,
uint value,
NtfSyncPlayer sync,
bool deleteIfZero = false,
bool sendImmediate = false)
{
var player = connection.Player!;
var attr = player.Data.Attrs.FirstOrDefault(x => x.Gid == Gid && x.Sid == sid);
if (value == 0 && deleteIfZero)
{
if (attr != null)
player.Data.Attrs.Remove(attr);
}
else
{
if (attr == null)
{
attr = new PlayerAttr { Gid = Gid, Sid = sid };
player.Data.Attrs.Add(attr);
}
attr.Val = value;
}
sync.Custom[player.ToPackedAttrKey(Gid, sid)] = value;
sync.Custom[player.ToShiftedAttrKey(Gid, sid)] = value;
if (sendImmediate)
await player.SendPacket(CmdIds.NtfSetAttr, new NtfSetAttr { Gid = Gid, Sid = sid, Val = value });
}
internal static uint BedroomSlotSid(int roomId) => BedroomStartSid + (uint)roomId;
internal static uint GirlRoomNumSid(int girlId) => (uint)(girlId * 50 + 1);
internal static async Task MoveGirlIntoRoomAsync(Connection connection, int girlId, int roomId, NtfSyncPlayer sync)
{
var player = connection.Player!;
var oldRoom = (int)Read(player, GirlRoomNumSid(girlId));
var targetSlotSid = BedroomSlotSid(roomId);
var oldSlotSid = oldRoom is >= 1 and < 100 ? BedroomSlotSid(oldRoom) : 0;
var occupant = Read(player, targetSlotSid);
await SetAsync(connection, targetSlotSid, (uint)girlId, sync);
await SetAsync(connection, GirlRoomNumSid(girlId), (uint)roomId, sync);
if (oldRoom is >= 1 and < 100 && oldRoom != roomId)
await SetAsync(connection, oldSlotSid, 0, sync);
if (occupant > 0 && occupant != (uint)girlId)
{
if (oldRoom is >= 1 and < 100 && oldRoom != roomId)
{
await SetAsync(connection, oldSlotSid, occupant, sync);
await SetAsync(connection, GirlRoomNumSid((int)occupant), (uint)oldRoom, sync);
}
else
{
await SetAsync(connection, GirlRoomNumSid((int)occupant), BedroomRegisteredNoRoom, sync);
}
}
}
internal static uint GetNextBedroomSid(PlayerInstance player, uint floorId)
{
var floorStartSid = BedroomStartSid + ((floorId - 2) * BedroomPerFloor) + 1;
for (uint i = 0; i < BedroomPerFloor; i++)
{
var sid = floorStartSid + i;
var exists = player.Data.Attrs.Any(x =>
x.Gid == Gid &&
x.Sid == sid);
if (!exists)
return sid;
}
return 0;
}
internal static uint PackArcadePropUse(int type, int id, ushort count) =>
(((uint)count & 0xffffu) << 16) | (((uint)id & 0xffu) << 8) | ((uint)type & 0xffu);
}

View File

@@ -0,0 +1,117 @@
using MikuSB.Proto;
using System.Text.Json.Nodes;
namespace MikuSB.GameServer.Server.CallGS.Handlers.House;
[HouseFunc("GameEnterMainUI")]
public class ThrowGameEnterMainUI : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var rsp = new JsonObject
{
["FuncName"] = "GameEnterMainUI",
["tblockGirlList"] = new JsonArray()
};
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Success(rsp));
}
}
[HouseFunc("ThrowGameTutorialFinish")]
public class ThrowGameTutorialFinish : IHouseFuncHandler
{
private const uint HouseBeachInfoStart = 17000;
private const uint ThrowGameTutorialOffset = 10;
public async Task Handle(Connection connection, string param)
{
var root = HouseJson.ParseObject(param);
if (root == null) return;
var index = Math.Max(0, HouseJson.NumField(root, "Index"));
var sync = new NtfSyncPlayer();
await HouseAttr.SetAsync(connection, HouseBeachInfoStart + ThrowGameTutorialOffset + (uint)index, 1, sync);
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Synthesize(root), sync);
}
}
[HouseFunc("ThrowGameEnter")]
public class ThrowGameEnter : IHouseFuncHandler
{
private static readonly Random Random = new();
public async Task Handle(Connection connection, string param)
{
var rsp = new JsonObject
{
["FuncName"] = "ThrowGameEnter",
["nSeed"] = Random.Next(1, 1_000_000_000)
};
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Success(rsp));
}
}
[HouseFunc("ThrowGameSettlement")]
public class ThrowGameSettlement : IHouseFuncHandler
{
private const uint HouseBeachInfoStart = 17000;
private const uint ThrowGameChallengePointsOffset = 2;
private const uint ThrowGameUnlockDrinkStartOffset = 50;
private const int ThrowGameModeChallenge = 2;
public async Task Handle(Connection connection, string param)
{
var root = HouseJson.ParseObject(param);
if (root == null) return;
var modeType = HouseJson.NumField(root, "nModeType");
var score = Math.Max(0, HouseJson.NumField(root, "nScore"));
var sync = new NtfSyncPlayer();
if (root["tbUnlockDrink"] is JsonObject unlockDrink && unlockDrink["nDrinkIDs"] is JsonArray drinks)
{
foreach (var drinkNode in drinks)
{
var drinkId = HouseJson.ToInt(drinkNode);
if (drinkId <= 0) continue;
var sid = HouseBeachInfoStart + ThrowGameUnlockDrinkStartOffset + (uint)drinkId;
var prev = HouseAttr.Read(connection.Player!, sid);
await HouseAttr.SetAsync(connection, sid, prev + 1, sync);
}
}
if (modeType == ThrowGameModeChallenge && score > 0)
{
var sid = HouseBeachInfoStart + ThrowGameChallengePointsOffset;
var prev = HouseAttr.Read(connection.Player!, sid);
if ((uint)score > prev)
await HouseAttr.SetAsync(connection, sid, (uint)score, sync);
}
var rsp = new JsonObject
{
["nAddExp"] = score,
["FuncName"] = "ThrowGameSettlement"
};
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Success(rsp), sync);
}
}
[HouseFunc("ThrowGameGetLevelReward")]
public class ThrowGameGetLevelReward : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var rsp = new JsonObject { ["FuncName"] = "ThrowGameGetLevelReward" };
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Success(rsp));
}
}
[HouseFunc("ThrowGameGetAchReward")]
public class ThrowGameGetAchReward : IHouseFuncHandler
{
public async Task Handle(Connection connection, string param)
{
var rsp = new JsonObject { ["FuncName"] = "ThrowGameGetAchReward" };
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Success(rsp));
}
}

View File

@@ -0,0 +1,12 @@
namespace MikuSB.GameServer.Server.CallGS.Handlers.House;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class HouseFuncAttribute(string funcName) : Attribute
{
public string FuncName { get; } = funcName;
}
public interface IHouseFuncHandler
{
Task Handle(Connection connection, string param);
}

View File

@@ -0,0 +1,42 @@
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.House;
[CallGSApi("House_Request")]
public class House_Request : ICallGSHandler
{
private static readonly Dictionary<string, IHouseFuncHandler> Handlers = [];
static House_Request()
{
foreach (var type in Assembly.GetExecutingAssembly().GetTypes())
{
foreach (var attr in type.GetCustomAttributes<HouseFuncAttribute>())
Handlers[attr.FuncName] = (IHouseFuncHandler)Activator.CreateInstance(type)!;
}
}
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<HouseRequestParam>(param);
if (req?.FuncName == null) return;
if (Handlers.TryGetValue(req.FuncName, out var handler))
{
await handler.Handle(connection, param);
return;
}
var root = HouseJson.ParseObject(param);
if (root == null) return;
await CallGSRouter.SendScript(connection, "House_Request", HouseRequestScript.Synthesize(root));
}
}
internal sealed class HouseRequestParam
{
[JsonPropertyName("FuncName")]
public string? FuncName { get; set; }
}

View File

@@ -1,4 +1,5 @@
using MikuSB.Database; using MikuSB.Database;
using MikuSB.Enums.Player;
using MikuSB.Proto; using MikuSB.Proto;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@@ -8,8 +9,6 @@ namespace MikuSB.GameServer.Server.CallGS.Handlers.Misc;
[CallGSApi("PlayerSetting_ChangeShowCard")] [CallGSApi("PlayerSetting_ChangeShowCard")]
public class PlayerSetting_ChangeShowCard : ICallGSHandler public class PlayerSetting_ChangeShowCard : ICallGSHandler
{ {
private const int ShowItemGirlIndex = 4;
public async Task Handle(Connection connection, string param, ushort seqNo) public async Task Handle(Connection connection, string param, ushort seqNo)
{ {
var player = connection.Player!; var player = connection.Player!;
@@ -23,13 +22,10 @@ public class PlayerSetting_ChangeShowCard : ICallGSHandler
await CallGSRouter.SendScript(connection, "PlayerSetting_ChangeShowCard", "{}"); await CallGSRouter.SendScript(connection, "PlayerSetting_ChangeShowCard", "{}");
return; return;
} }
player.SetShowItem((int)ProfileShowItemTypeEnum.SHOWITEM_GIRL, card.Guid);
player.SetShowItem(ShowItemGirlIndex, card.Guid);
DatabaseHelper.SaveDatabaseType(player.Data); DatabaseHelper.SaveDatabaseType(player.Data);
var sync = new NtfSyncPlayer(); var sync = new NtfSyncPlayer();
sync.ShowItems.AddRange(player.Data.ShowItems); sync.ShowItems.AddRange(player.Data.ShowItems);
await CallGSRouter.SendScript(connection, "PlayerSetting_ChangeShowCard", "{}", sync); await CallGSRouter.SendScript(connection, "PlayerSetting_ChangeShowCard", "{}", sync);
} }
} }

View File

@@ -0,0 +1,50 @@
using MikuSB.Database;
using MikuSB.Enums.Player;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Misc;
[CallGSApi("PlayerSetting_SetProfileFace")]
public class PlayerSetting_SetProfileFace : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<SetProfileFaceParam>(param);
if (req == null)
return;
if (req.HeadItemId > 0)
{
var item = player.InventoryManager.GetNormalItem(req.HeadItemId);
if (item == null)
{
await CallGSRouter.SendScript(connection, "PlayerSetting_SetProfileFace", "{\"err\":\"error.BadParam\"}");
return;
}
player.SetShowItem((int)ProfileShowItemTypeEnum.SHOWITEM_FACE, item.UniqueId);
}
if (req.FrameItemId > 0)
{
var item = player.InventoryManager.GetNormalItem(req.FrameItemId);
if (item == null)
{
await CallGSRouter.SendScript(connection, "PlayerSetting_SetProfileFace", "{\"err\":\"error.BadParam\"}");
return;
}
player.SetShowItem((int)ProfileShowItemTypeEnum.SHOWITEM_FRAME, item.UniqueId);
}
DatabaseHelper.SaveDatabaseType(player.Data);
var sync = new NtfSyncPlayer();
sync.ShowItems.AddRange(player.Data.ShowItems);
await CallGSRouter.SendScript(connection, "PlayerSetting_SetProfileFace", "null", sync);
}
}
internal sealed class SetProfileFaceParam
{
[JsonPropertyName("nHeadItemID")] public uint HeadItemId { get; set; }
[JsonPropertyName("nFrameItemID")] public uint FrameItemId { get; set; }
}

View File

@@ -0,0 +1,46 @@
using MikuSB.Database;
using MikuSB.Enums.Player;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Misc;
[CallGSApi("PlayerSetting_SetShowBadge")]
public class PlayerSetting_SetShowBadge : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<SetShowBadgeParam>(param);
if (req == null)
{
await CallGSRouter.SendScript(connection, "PlayerSetting_SetShowBadge", "{\"err\":\"error.BadParam\"}");
return;
}
var slots = new[]
{
ProfileShowItemTypeEnum.SHOWITEM_BADGE1,
ProfileShowItemTypeEnum.SHOWITEM_BADGE2,
ProfileShowItemTypeEnum.SHOWITEM_BADGE3
};
for (int i = 0; i < slots.Length; i++)
{
var uniqueId = i < req.Badges.Count ? req.Badges[i] : 0;
player.SetShowItem((int)slots[i], uniqueId);
}
DatabaseHelper.SaveDatabaseType(player.Data);
var sync = new NtfSyncPlayer();
sync.ShowItems.AddRange(player.Data.ShowItems);
await CallGSRouter.SendScript(connection, "PlayerSetting_SetShowBadge", "null", sync);
}
}
internal sealed class SetShowBadgeParam
{
[JsonPropertyName("tbBadge")]
public List<uint> Badges { get; set; } = [];
}

View File

@@ -0,0 +1,39 @@
using MikuSB.Database;
using MikuSB.Enums.Player;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Misc;
[CallGSApi("PlayerSetting_SetShowBubble")]
public class PlayerSetting_SetShowBubble : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<SetShowBubbleParam>(param);
if (req == null)
return;
var item = player.InventoryManager.GetNormalItem(req.Id);
if (item == null)
{
await CallGSRouter.SendScript(connection, "PlayerSetting_SetShowBubble", "{\"err\":\"error.BadParam\"}");
return;
}
player.SetShowItem((int)ProfileShowItemTypeEnum.SHOWITEM_BUBBLE, item.UniqueId);
DatabaseHelper.SaveDatabaseType(player.Data);
var sync = new NtfSyncPlayer();
sync.ShowItems.AddRange(player.Data.ShowItems);
await CallGSRouter.SendScript(connection, "PlayerSetting_SetShowBubble", "null", sync);
}
}
internal sealed class SetShowBubbleParam
{
[JsonPropertyName("nID")]
public uint Id { get; set; }
}

View File

@@ -0,0 +1,39 @@
using MikuSB.Database;
using MikuSB.Enums.Player;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Misc;
[CallGSApi("PlayerSetting_SetShowCover")]
public class PlayerSetting_SetShowCover : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<SetShowCoverParam>(param);
if (req == null)
return;
var item = player.InventoryManager.GetNormalItem(req.Id);
if (item == null)
{
await CallGSRouter.SendScript(connection, "PlayerSetting_SetShowCover", "{\"err\":\"error.BadParam\"}");
return;
}
player.SetShowItem((int)ProfileShowItemTypeEnum.SHOWITEM_COVER, item.UniqueId);
DatabaseHelper.SaveDatabaseType(player.Data);
var sync = new NtfSyncPlayer();
sync.ShowItems.AddRange(player.Data.ShowItems);
await CallGSRouter.SendScript(connection, "PlayerSetting_SetShowCover", "null", sync);
}
}
internal sealed class SetShowCoverParam
{
[JsonPropertyName("nID")]
public uint Id { get; set; }
}

View File

@@ -0,0 +1,39 @@
using MikuSB.Database;
using MikuSB.Enums.Player;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Misc;
[CallGSApi("PlayerSetting_SetShowNameCard")]
public class PlayerSetting_SetShowNameCard : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<SetShowNameCardParam>(param);
if (req == null)
return;
var item = player.InventoryManager.GetNormalItem(req.Id);
if (item == null)
{
await CallGSRouter.SendScript(connection, "PlayerSetting_SetShowNameCard", "{\"err\":\"error.BadParam\"}");
return;
}
player.SetShowItem((int)ProfileShowItemTypeEnum.SHOWITEM_NAMECARD, item.UniqueId);
DatabaseHelper.SaveDatabaseType(player.Data);
var sync = new NtfSyncPlayer();
sync.ShowItems.AddRange(player.Data.ShowItems);
await CallGSRouter.SendScript(connection, "PlayerSetting_SetShowNameCard", "null", sync);
}
}
internal sealed class SetShowNameCardParam
{
[JsonPropertyName("nID")]
public uint Id { get; set; }
}

View File

@@ -0,0 +1,39 @@
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Misc;
[CallGSApi("SettingChange")]
public class SettingChange : ICallGSHandler
{
private const uint PlayerSettingGid = 44;
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var changes = JsonSerializer.Deserialize<List<SettingChangeParam>>(param) ?? [];
var player = connection.Player!;
var sync = new NtfSyncPlayer();
foreach (var change in changes)
{
var value = player.Data.StrAttrs
.FirstOrDefault(x => x.Gid == PlayerSettingGid && x.Sid == change.Id)?
.Val;
if (value == null)
continue;
sync.CustomStr[player.ToShiftedAttrKey(PlayerSettingGid, change.Id)] = value;
}
if (sync.CustomStr.Count > 0)
await connection.SendPacket(CmdIds.NtfSyncAttr, sync);
}
}
internal sealed class SettingChangeParam
{
[JsonPropertyName("id")]
public uint Id { get; set; }
}

View File

@@ -0,0 +1,178 @@
using MikuSB.Data;
using MikuSB.Database.Player;
using MikuSB.GameServer.Game.Player;
using MikuSB.Proto;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Rogue3D;
internal static class Rogue3DStateHelper
{
private const uint GroupId = 124;
private const uint LevelPassStart = 20;
private const uint DailyBuffStart = 51;
private const uint DailyBuffEnd = 65;
private const int DailyBuffBitCount = 10;
private const int DailyBuffBitsPerValue = DailyBuffBitCount + 1;
private const uint DailyBuffMask = (1u << DailyBuffBitCount) - 1;
private const uint UnlockDiff1Sid = LevelPassStart + 1;
private const uint UnlockDiff2Sid = LevelPassStart + 2;
private const uint UnlockDiff3Sid = LevelPassStart + 3;
private const uint UnlockDiff4Sid = LevelPassStart + 4;
private static uint[]? ShuffledDailyBuffIds;
public static NtfSyncPlayer EnsureUnlockState(PlayerInstance player)
{
var sync = new NtfSyncPlayer();
EnsureMinAttr(player, UnlockDiff1Sid, 1, sync);
EnsureMinAttr(player, UnlockDiff2Sid, 1, sync);
EnsureMinAttr(player, UnlockDiff3Sid, 1, sync);
EnsureMinAttr(player, UnlockDiff4Sid, 1, sync);
foreach (var scienceSid in GetUnlockTalentScienceSids())
{
EnsureMinAttr(player, scienceSid, 1, sync);
}
EnsureDailyBuffAttrs(player, sync);
return sync;
}
private static IEnumerable<uint> GetUnlockTalentScienceSids()
{
return GameData.Rogue3DTalentData.Values
.Select(x => x.UnlockCondition)
.Where(x => x > 0)
.Distinct()
.OrderBy(x => x);
}
private static void EnsureDailyBuffAttrs(PlayerInstance player, NtfSyncPlayer sync)
{
var buffIds = GetOrCreateDailyBuffIds()
.Take((int)(DailyBuffEnd - DailyBuffStart + 1) * 3)
.ToArray();
var index = 0;
for (var sid = DailyBuffStart; sid <= DailyBuffEnd; sid++)
{
uint packed = 0;
for (var slot = 0; slot < 3 && index < buffIds.Length; slot++, index++)
{
packed |= (buffIds[index] & DailyBuffMask) << (slot * DailyBuffBitsPerValue);
}
SetAttr(player, sid, packed, sync);
}
}
private static uint[] GetOrCreateDailyBuffIds()
{
if (ShuffledDailyBuffIds != null)
{
return ShuffledDailyBuffIds;
}
var groupedBuffIds = GameData.Rogue3DDailyBuffData.Values
.Where(x => x.ScoreBuffId > 0 && x.ScoreBuffId <= DailyBuffMask)
.GroupBy(x => x.GroupId)
.OrderBy(x => x.Key)
.Select(x => x
.OrderBy(y => y.Id)
.Select(y => y.ScoreBuffId)
.Distinct()
.ToList())
.ToList();
var random = new Random();
foreach (var group in groupedBuffIds)
{
Shuffle(group, random);
}
Shuffle(groupedBuffIds, random);
var buffIds = new List<uint>();
var indexByGroup = new int[groupedBuffIds.Count];
var hasRemaining = true;
while (hasRemaining)
{
hasRemaining = false;
for (var i = 0; i < groupedBuffIds.Count; i++)
{
var group = groupedBuffIds[i];
var index = indexByGroup[i];
if (index >= group.Count)
{
continue;
}
buffIds.Add(group[index]);
indexByGroup[i] = index + 1;
hasRemaining = true;
}
}
ShuffledDailyBuffIds = buffIds.ToArray();
return ShuffledDailyBuffIds;
}
private static IEnumerable<uint> GetDailyBuffIds()
{
return GetOrCreateDailyBuffIds();
}
private static void Shuffle<T>(IList<T> list, Random random)
{
for (var i = list.Count - 1; i > 0; i--)
{
var swapIndex = random.Next(i + 1);
(list[i], list[swapIndex]) = (list[swapIndex], list[i]);
}
}
private static void EnsureMinAttr(PlayerInstance player, uint sid, uint value, NtfSyncPlayer sync, bool overwrite = false)
{
var attr = player.Data.Attrs.FirstOrDefault(x => x.Gid == GroupId && x.Sid == sid);
if (attr == null)
{
attr = new PlayerAttr { Gid = GroupId, Sid = sid, Val = value };
player.Data.Attrs.Add(attr);
AddSync(player, sync, sid, value);
return;
}
if ((!overwrite && attr.Val >= value) || (overwrite && attr.Val == value))
{
return;
}
attr.Val = value;
AddSync(player, sync, sid, value);
}
private static void SetAttr(PlayerInstance player, uint sid, uint value, NtfSyncPlayer sync)
{
var attr = player.Data.Attrs.FirstOrDefault(x => x.Gid == GroupId && x.Sid == sid);
if (attr == null)
{
attr = new PlayerAttr { Gid = GroupId, Sid = sid };
player.Data.Attrs.Add(attr);
}
if (attr.Val == value)
{
return;
}
attr.Val = value;
AddSync(player, sync, sid, value);
}
private static void AddSync(PlayerInstance player, NtfSyncPlayer sync, uint sid, uint value)
{
sync.Custom[player.ToPackedAttrKey(GroupId, sid)] = value;
sync.Custom[player.ToShiftedAttrKey(GroupId, sid)] = value;
}
}

View File

@@ -8,6 +8,7 @@ public class Rogue3D_CheckOpenAct : ICallGSHandler
{ {
public async Task Handle(Connection connection, string param, ushort seqNo) public async Task Handle(Connection connection, string param, ushort seqNo)
{ {
await CallGSRouter.SendScript(connection, "Rogue3D_CheckOpenAct", "{\"bOpen\":true}"); var sync = Rogue3DStateHelper.EnsureUnlockState(connection.Player!);
await CallGSRouter.SendScript(connection, "Rogue3D_CheckOpenAct", "{\"bOpen\":true}", sync);
} }
} }

View File

@@ -8,6 +8,7 @@ public class Rogue3D_SelectMode : ICallGSHandler
{ {
public async Task Handle(Connection connection, string param, ushort seqNo) public async Task Handle(Connection connection, string param, ushort seqNo)
{ {
await CallGSRouter.SendScript(connection, "Rogue3D_SelectMode", "{}"); var sync = Rogue3DStateHelper.EnsureUnlockState(connection.Player!);
await CallGSRouter.SendScript(connection, "Rogue3D_SelectMode", "{}", sync);
} }
} }

View File

@@ -0,0 +1,79 @@
using MikuSB.Database;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.SupporterCard;
[CallGSApi("SupporterCard_Equip")]
public class SupporterCard_Equip : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<SupporterCardEquipParam>(param);
if (req == null || req.CardId == 0 || req.SupportCardUid == 0)
{
await CallGSRouter.SendScript(connection, "Logistics_Equip", "{}");
return;
}
var card = player.CharacterManager.GetCharacterByGUID((uint)req.CardId);
if (card == null)
{
await CallGSRouter.SendScript(connection, "Logistics_Equip", "{}");
return;
}
var teamIndex = card.SupportTeamIndex;
var slot = GetTeamIndex((uint)req.EqSlot, teamIndex);
// If an existing card is equipped in this slot and bForce is false, ask for confirmation
if (!req.Force && req.CurrentEquippedUid != 0 && card.SupportSlots.TryGetValue(slot, out var existing) && existing != 0)
{
await CallGSRouter.SendScript(connection, "Logistics_Confirm", "{}");
return;
}
// Perform equip
card.SupportSlots[slot] = (uint)req.SupportCardUid;
DatabaseHelper.SaveDatabaseType(player.CharacterManager.CharacterData);
var sync = new NtfSyncPlayer();
sync.Items.Add(card.ToProto());
// Req_EquipChange (no Model) → Logistics_Change; Req_Equip (has Model) → Logistics_Equip
var responseApi = string.IsNullOrEmpty(req.Model) ? "Logistics_Change" : "Logistics_Equip";
await CallGSRouter.SendScript(connection, responseApi, "{}", sync);
}
private uint GetTeamIndex(uint slot, uint teamIndex)
{
if (teamIndex == 1) return slot;
if (teamIndex == 2) return slot + 7;
if (teamIndex == 3) return slot + 10;
return slot;
}
}
internal sealed class SupporterCardEquipParam
{
[JsonPropertyName("EqId")]
public int CardId { get; set; }
[JsonPropertyName("beEqId")]
public int SupportCardUid { get; set; }
[JsonPropertyName("EqSlot")]
public int EqSlot { get; set; }
[JsonPropertyName("BEqId")]
public int CurrentEquippedUid { get; set; }
[JsonPropertyName("bForce")]
public bool Force { get; set; }
[JsonPropertyName("Model")]
public string? Model { get; set; }
}

View File

@@ -0,0 +1,119 @@
using MikuSB.Data;
using MikuSB.Database;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.SupporterCard;
[CallGSApi("SupporterCard_Upgrade")]
public class SupporterCard_Upgrade : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var player = connection.Player!;
var req = JsonSerializer.Deserialize<SupporterCardUpgradeParam>(param);
if (req == null || req.SupportCardUid == 0 || req.Materials == null || req.Materials.Count == 0)
{
await CallGSRouter.SendScript(connection, "Logistics_Upgrade", "{}");
return;
}
var supportCard = player.InventoryManager.InventoryData.Items.GetValueOrDefault((uint)req.SupportCardUid);
if (supportCard == null)
{
await CallGSRouter.SendScript(connection, "Logistics_Upgrade", "{}");
return;
}
var supportCardExcel = GameData.SupportCardData.FirstOrDefault(x => x.TemplateId == supportCard.TemplateId);
var maxLevel = supportCardExcel?.MaxLevel ?? 10;
// Validate all materials exist with sufficient count
foreach (var mat in req.Materials)
{
var item = player.InventoryManager.InventoryData.Items.GetValueOrDefault((uint)mat.Id);
if (item == null || item.ItemCount < mat.Num)
{
await CallGSRouter.SendScript(connection, "Logistics_Upgrade", "{}");
return;
}
}
// Consume materials and accumulate exp
var syncItems = new List<Item>();
uint gainedExp = 0;
foreach (var mat in req.Materials)
{
var item = player.InventoryManager.InventoryData.Items[(uint)mat.Id];
// Look up ProvideExp: check SupportCardData first, then SuppliesData
uint provideExp = 0;
var scExcel = GameData.SupportCardData.FirstOrDefault(x => x.TemplateId == item.TemplateId);
if (scExcel != null)
provideExp = scExcel.ProvideExp;
else if (GameData.SuppliesData.TryGetValue((uint)item.TemplateId, out var supExcel))
provideExp = supExcel.ProvideExp;
gainedExp += provideExp * (uint)mat.Num;
item.ItemCount -= (uint)mat.Num;
var proto = item.ToProto();
if (item.ItemCount == 0)
{
player.InventoryManager.InventoryData.Items.Remove(item.UniqueId);
proto.Count = 0;
}
syncItems.Add(proto);
}
// Apply exp and level up
supportCard.Exp += gainedExp;
while (supportCard.Level < maxLevel)
{
var expNeeded = GetExpNeeded(supportCard.Level + 1);
if (expNeeded == 0 || supportCard.Exp < expNeeded) break;
supportCard.Exp -= expNeeded;
supportCard.Level++;
}
if (supportCard.Level >= maxLevel)
{
supportCard.Exp = 0;
supportCard.Level = maxLevel;
}
syncItems.Add(supportCard.ToProto());
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
var sync = new NtfSyncPlayer();
sync.Items.AddRange(syncItems);
await CallGSRouter.SendScript(connection, "Logistics_Upgrade", "{}", sync);
}
private static uint GetExpNeeded(uint level)
{
if (GameData.UpgradeExpData.TryGetValue((int)level, out var row))
return row.SusNeedExp;
return 0;
}
}
internal sealed class SupporterCardUpgradeParam
{
[JsonPropertyName("Id")]
public int SupportCardUid { get; set; }
[JsonPropertyName("tbMaterials")]
public List<UpgradeMaterial> Materials { get; set; } = [];
}
internal sealed class UpgradeMaterial
{
[JsonPropertyName("Id")]
public int Id { get; set; }
[JsonPropertyName("Num")]
public uint Num { get; set; }
}

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

@@ -0,0 +1,47 @@
using MikuSB.Proto;
using System.Text.Json;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Girl;
[CallGSApi("Weapon_ReplacePart")]
public class Weapon_ReplacePart : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<WeaponPartReplaceParam>(param);
if (req == null)
{
await CallGSRouter.SendScript(connection, "Weapon_ReplacePart", "{\"sErr\":\"error.BadParam\"}");
return;
}
var player = connection.Player!;
var weaponData = player.InventoryManager.GetWeaponItem(req.Id);
if (weaponData == null)
{
await CallGSRouter.SendScript(connection, "Weapon_ReplacePart", "{}");
return;
}
uint partId = 0;
if (req.PartId != -1)
{
var partData = player.InventoryManager.GetNormalItem((uint)req.PartId);
if (partData != null) partId = partData.UniqueId;
}
weaponData.PartSlots[req.Type] = partId;
var sync = new NtfSyncPlayer
{
Items = { weaponData.ToProto() }
};
await CallGSRouter.SendScript(connection, "Weapon_ReplacePart", "null", sync);
}
}
internal sealed class WeaponPartReplaceParam
{
public int PartId { get; set; }
public uint Type { get; set; }
public uint Id { get; set; }
}

View File

@@ -0,0 +1,43 @@
using MikuSB.Enums.Item;
using MikuSB.Proto;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MikuSB.GameServer.Server.CallGS.Handlers.Girl;
[CallGSApi("Weapon_ShowDefaultPart")]
public class Weapon_ShowDefaultPart : ICallGSHandler
{
public async Task Handle(Connection connection, string param, ushort seqNo)
{
var req = JsonSerializer.Deserialize<WeaponShowDefaultPartParam>(param);
if (req == null)
{
await CallGSRouter.SendScript(connection, "Weapon_ShowDefaultPart", "{\"sErr\":\"error.BadParam\"}");
return;
}
var player = connection.Player!;
var weaponData = player.InventoryManager.GetWeaponItem(req.Id);
if (weaponData == null)
{
await CallGSRouter.SendScript(connection, "Weapon_ShowDefaultPart", "{}");
return;
}
if (req.Flag == 1) weaponData.Flag = ItemFlagEnum.FLAG_WEAPON_DEFAULT;
else weaponData.Flag = ItemFlagEnum.FLAG_READED;
var sync = new NtfSyncPlayer
{
Items = { weaponData.ToProto() }
};
await CallGSRouter.SendScript(connection, "Weapon_ShowDefaultPart", "null", sync);
}
}
internal sealed class WeaponShowDefaultPartParam
{
[JsonPropertyName("nFlag")] public int Flag { get; set; }
public uint Id { get; set; }
}

View File

@@ -4,12 +4,14 @@ using MikuSB.Database.Account;
using MikuSB.Database.Player; using MikuSB.Database.Player;
using MikuSB.GameServer.Game.Player; using MikuSB.GameServer.Game.Player;
using MikuSB.GameServer.Server.CallGS; using MikuSB.GameServer.Server.CallGS;
using MikuSB.GameServer.Server.CallGS.Handlers.Girl;
using MikuSB.GameServer.Server.Packet.Send.Friend; using MikuSB.GameServer.Server.Packet.Send.Friend;
using MikuSB.GameServer.Server.Packet.Send.Login; using MikuSB.GameServer.Server.Packet.Send.Login;
using MikuSB.GameServer.Server.Packet.Send.Misc; using MikuSB.GameServer.Server.Packet.Send.Misc;
using MikuSB.Proto; using MikuSB.Proto;
using MikuSB.TcpSharp; using MikuSB.TcpSharp;
using MikuSB.Util; using MikuSB.Util;
using System.Text.Json.Nodes;
namespace MikuSB.GameServer.Server.Packet.Recv.Login; namespace MikuSB.GameServer.Server.Packet.Recv.Login;
@@ -19,11 +21,13 @@ public class HandlerReqLogin : Handler
public override async Task OnHandle(Connection connection, byte[] data, ushort seqNo) public override async Task OnHandle(Connection connection, byte[] data, ushort seqNo)
{ {
var req = ReqLogin.Parser.ParseFrom(data); var req = ReqLogin.Parser.ParseFrom(data);
var account = AccountData.GetAccountByUid(1); var account = AccountData.GetAccountByComboToken(req.Token)
?? AccountData.GetAccountByDispatchToken(req.Token)
?? AccountData.GetAccountByUid(10001)
?? AccountData.GetAccountByUid(1);
if (account == null) if (account == null)
{ {
AccountData.CreateAccount("MIKU", 0, ""); account = AccountData.CreateAccount("default@mikusb.local", 10001, "");
account = AccountData.GetAccountByUid(1);
if (account == null) if (account == null)
{ {
await connection.SendPacket(CmdIds.NtfLogout); await connection.SendPacket(CmdIds.NtfLogout);
@@ -43,15 +47,89 @@ public class HandlerReqLogin : Handler
connection.State = SessionStateEnum.WAITING_FOR_LOGIN; connection.State = SessionStateEnum.WAITING_FOR_LOGIN;
var pd = DatabaseHelper.GetInstance<PlayerGameData>(account.Uid); var pd = DatabaseHelper.GetInstance<PlayerGameData>(account.Uid);
connection.Player = pd == null ? new PlayerInstance(account.Uid) : new PlayerInstance(pd); connection.Player = pd == null ? new PlayerInstance(account.Uid) : new PlayerInstance(pd);
if (connection.Player.Data.EnsureDisplayName())
DatabaseHelper.UpdateInstance(connection.Player.Data);
connection.DebugFile = Path.Combine(ConfigManager.Config.Path.LogPath, "Debug/", $"{account.Uid}/", connection.DebugFile = Path.Combine(ConfigManager.Config.Path.LogPath, "Debug/", $"{account.Uid}/",
$"Debug-{DateTime.Now:yyyy-MM-dd HH-mm-ss}.log"); $"Debug-{DateTime.Now:yyyy-MM-dd HH-mm-ss}.log");
await connection.Player.OnEnterGame(); await connection.Player.OnEnterGame();
connection.Player.Connection = connection; connection.Player.Connection = connection;
await connection.SendPacket(new PacketRspLogin(connection.Player!)); await connection.SendPacket(new PacketRspLogin(connection.Player!));
await connection.SendPacket(new PacketNtfCallScript(connection.Player!));
await SendDebugLoginState(connection);
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!);
await connection.SendPacket(new PacketNtfCallScript(connection.Player!.InventoryManager.InventoryData)); await connection.SendPacket(new PacketNtfCallScript(connection.Player!.InventoryManager.InventoryData));
await SendGirlSkinTypeOnLogin(connection);
}
private static void ApplySavedGirlSkinTypes(PlayerInstance player)
{
var inventoryData = player.InventoryManager.InventoryData;
inventoryData.SkinTypesBySkinId ??= [];
var changed = false;
foreach (var (skinId, skinType) in inventoryData.SkinTypesBySkinId.ToArray())
{
var clamped = GirlSkin_ChangeSkinType.ClampClientSkinType(skinType);
if (clamped != skinType)
{
inventoryData.SkinTypesBySkinId[skinId] = clamped;
changed = true;
}
var skinData = GirlSkin_ChangeSkinType.GetOrCreateSkinItem(player, skinId);
if (skinData != null && skinData.SkinType != clamped)
{
skinData.SkinType = clamped;
changed = true;
}
}
if (changed)
DatabaseHelper.SaveDatabaseType(inventoryData);
}
private static async Task SendGirlSkinTypeOnLogin(Connection connection)
{
var player = connection.Player;
if (player == null)
return;
var inventoryData = player.InventoryManager.InventoryData;
inventoryData.SkinTypesBySkinId ??= [];
foreach (var (skinId, skinType) in inventoryData.SkinTypesBySkinId)
{
var clamped = GirlSkin_ChangeSkinType.ClampClientSkinType(skinType);
var skinData = GirlSkin_ChangeSkinType.GetOrCreateSkinItem(player, skinId);
var response = new JsonObject
{
["nType"] = clamped,
["nSkinId"] = skinId
};
if (skinData == null)
{
await CallGSRouter.SendScript(connection, "GirlSkin_ChangeSkinType", response.ToJsonString());
continue;
}
await CallGSRouter.SendScript(connection, "GirlSkin_ChangeSkinType", response.ToJsonString(), new NtfSyncPlayer
{
Items = { skinData.ToProto() }
});
}
}
private static async Task SendDebugLoginState(Connection connection)
{
var response = new JsonObject
{
["IsDebug"] = ConfigManager.Config.ServerOption.EnableGmMenu
};
await CallGSRouter.SendScript(connection, "gm.notifylogin", response.ToJsonString());
} }
} }

View File

@@ -1,4 +1,5 @@
using MikuSB.Database.Player; using MikuSB.Database;
using MikuSB.Database.Player;
using MikuSB.Proto; using MikuSB.Proto;
namespace MikuSB.GameServer.Server.Packet.Recv.Login; namespace MikuSB.GameServer.Server.Packet.Recv.Login;
@@ -12,7 +13,11 @@ public class HandlerNtfSetAttr : Handler
var player = connection.Player!; var player = connection.Player!;
var attr = player.Data.Attrs var attr = player.Data.Attrs
.FirstOrDefault(x => x.Gid == req.Gid && x.Sid == req.Sid); .FirstOrDefault(x => x.Gid == req.Gid && x.Sid == req.Sid);
if (attr != null) attr.Val = req.Val;
if (attr != null)
{
attr.Val = req.Val;
}
else else
{ {
player.Data.Attrs.Add(new PlayerAttr player.Data.Attrs.Add(new PlayerAttr
@@ -22,6 +27,7 @@ public class HandlerNtfSetAttr : Handler
Val = req.Val Val = req.Val
}); });
} }
DatabaseHelper.SaveDatabaseType(player.Data);
await player.OnHeartBeat(); await player.OnHeartBeat();
} }
} }

View File

@@ -0,0 +1,19 @@
using MikuSB.Database;
using MikuSB.Proto;
namespace MikuSB.GameServer.Server.Packet.Recv.Login;
[Opcode(CmdIds.NtfSetStrAttr)]
public class HandlerNtfSetStrAttr : Handler
{
public override async Task OnHandle(Connection connection, byte[] data, ushort seqNo)
{
var req = NtfSetStrAttr.Parser.ParseFrom(data);
var player = connection.Player!;
player.SetStrAttr(req.Gid, req.Sid, req.Val);
DatabaseHelper.SaveDatabaseType(player.Data);
await player.OnHeartBeat();
}
}

View File

@@ -1,4 +1,6 @@
using MikuSB.Proto; using Google.Protobuf;
using MikuSB.Database;
using MikuSB.Proto;
namespace MikuSB.GameServer.Server.Packet.Recv.Login; namespace MikuSB.GameServer.Server.Packet.Recv.Login;
@@ -7,6 +9,57 @@ public class HandlerReqRename : Handler
{ {
public override async Task OnHandle(Connection connection, byte[] data, ushort seqNo) public override async Task OnHandle(Connection connection, byte[] data, ushort seqNo)
{ {
var player = connection.Player;
if (player != null)
{
var requestedName = ParseDisplayName(data);
player.SetDisplayName(requestedName);
DatabaseHelper.UpdateInstance(player.Data);
await player.OnHeartBeat();
}
await connection.SendPacket(CmdIds.RspRename); await connection.SendPacket(CmdIds.RspRename);
} }
private static string? ParseDisplayName(byte[] data)
{
if (data.Length == 0)
return null;
try
{
var input = new CodedInputStream(data);
while (!input.IsAtEnd)
{
var tag = input.ReadTag();
if (tag == 0)
break;
if (WireFormat.GetTagWireType(tag) == WireFormat.WireType.LengthDelimited)
{
var value = input.ReadString();
if (!string.IsNullOrWhiteSpace(value))
return value;
}
else
{
input.SkipLastField();
}
}
}
catch
{
// Fall back to raw UTF-8 payload handling below.
}
try
{
var rawText = System.Text.Encoding.UTF8.GetString(data).Trim('\0', ' ', '\r', '\n', '\t');
return string.IsNullOrWhiteSpace(rawText) ? null : rawText;
}
catch
{
return null;
}
}
} }

View File

@@ -1,6 +1,6 @@
using MikuSB.Database.Character; using MikuSB.Database.Character;
using MikuSB.Database.Inventory; using MikuSB.Database.Inventory;
using MikuSB.GameServer.Game.Inventory; using MikuSB.GameServer.Game.Player;
using MikuSB.Proto; using MikuSB.Proto;
using MikuSB.TcpSharp; using MikuSB.TcpSharp;
@@ -53,6 +53,36 @@ public class PacketNtfCallScript : BasePacket
SetData(proto); SetData(proto);
} }
public PacketNtfCallScript(List<GameSupportCardInfo> cards) : base(CmdIds.NtfScript)
{
var proto = new NtfCallScript
{
Api = "",
Arg = "{}",
ExtraSync = new NtfSyncPlayer
{
Items = { cards.Select(x => x.ToProto()) }
}
};
SetData(proto);
}
public PacketNtfCallScript(List<GameSkinInfo> skins) : base(CmdIds.NtfScript)
{
var proto = new NtfCallScript
{
Api = "",
Arg = "{}",
ExtraSync = new NtfSyncPlayer
{
Items = { skins.Select(x => x.ToProto()) }
}
};
SetData(proto);
}
public PacketNtfCallScript(InventoryData inventory) : base(CmdIds.NtfScript) public PacketNtfCallScript(InventoryData inventory) : base(CmdIds.NtfScript)
{ {
var proto = new NtfCallScript var proto = new NtfCallScript
@@ -65,7 +95,37 @@ public class PacketNtfCallScript : BasePacket
foreach (var item in inventory.Items.Values) extraSync.Items.Add(item.ToProto()); foreach (var item in inventory.Items.Values) extraSync.Items.Add(item.ToProto());
foreach (var skin in inventory.Skins.Values) extraSync.Items.Add(skin.ToProto()); foreach (var skin in inventory.Skins.Values) extraSync.Items.Add(skin.ToProto());
foreach (var weapon in inventory.Weapons.Values) extraSync.Items.Add(weapon.ToProto()); foreach (var weapon in inventory.Weapons.Values) extraSync.Items.Add(weapon.ToProto());
foreach (var supportCard in inventory.SupportCards.Values) extraSync.Items.Add(supportCard.ToProto());
proto.ExtraSync = extraSync; proto.ExtraSync = extraSync;
SetData(proto); SetData(proto);
} }
public PacketNtfCallScript(PlayerInstance Player) : base(CmdIds.NtfScript)
{
Player.BuildPlayerAttr(true);
var proto = new NtfCallScript
{
Api = "",
Arg = "{}"
};
var sync = new NtfSyncPlayer();
foreach (var x in Player.Data.Attrs)
{
uint gid = x.Gid;
uint sid = x.Sid;
uint val = x.Val;
if (gid == 0)
{
sync.Custom[sid] = val;
continue;
}
sync.Custom[Player.ToPackedAttrKey(gid, sid)] = val;
sync.Custom[Player.ToShiftedAttrKey(gid, sid)] = val;
}
proto.ExtraSync = sync;
SetData(proto);
}
} }

View File

@@ -0,0 +1,357 @@
using System.Collections;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text;
using MikuSB.Util;
namespace MikuSB.Loader;
public static class GameLaunchService
{
public static int Launch(params string[] extraGameArguments)
{
ConfigManager.LoadConfig();
var options = LaunchOptions.FromConfig(extraGameArguments);
return Launch(options);
}
public static int Launch(LaunchOptions options)
{
var startupInfo = new STARTUPINFOW
{
cb = Marshal.SizeOf<STARTUPINFOW>()
};
var commandLine = BuildCommandLine(options.GamePath, options.GameArguments);
var workingDirectory = options.WorkingDirectory ?? Path.GetDirectoryName(options.GamePath)
?? throw new InvalidOperationException("Unable to determine working directory.");
var environment = BuildEnvironmentBlock(options.EnvironmentVariables);
try
{
if (!CreateProcessW(
lpApplicationName: options.GamePath,
lpCommandLine: commandLine,
lpProcessAttributes: IntPtr.Zero,
lpThreadAttributes: IntPtr.Zero,
bInheritHandles: false,
dwCreationFlags: CreationFlags.CREATE_SUSPENDED | CreationFlags.CREATE_UNICODE_ENVIRONMENT,
lpEnvironment: environment,
lpCurrentDirectory: workingDirectory,
lpStartupInfo: ref startupInfo,
lpProcessInformation: out var processInfo))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to create game process.");
}
try
{
foreach (var patchPath in options.PatchPaths)
InjectDll(processInfo.hProcess, patchPath);
if (ResumeThread(processInfo.hThread) == uint.MaxValue)
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to resume game process.");
return processInfo.dwProcessId;
}
finally
{
CloseHandle(processInfo.hThread);
CloseHandle(processInfo.hProcess);
}
}
finally
{
if (environment != IntPtr.Zero)
Marshal.FreeHGlobal(environment);
}
}
private static void InjectDll(IntPtr processHandle, string dllPath)
{
var dllBytes = Encoding.Unicode.GetBytes(dllPath + '\0');
var remoteBuffer = VirtualAllocEx(
processHandle,
IntPtr.Zero,
(nuint)dllBytes.Length,
AllocationType.Commit | AllocationType.Reserve,
MemoryProtection.ReadWrite);
if (remoteBuffer == IntPtr.Zero)
throw new Win32Exception(Marshal.GetLastWin32Error(), "VirtualAllocEx failed.");
try
{
if (!WriteProcessMemory(processHandle, remoteBuffer, dllBytes, dllBytes.Length, out var written) ||
written.ToInt64() != dllBytes.Length)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "WriteProcessMemory failed.");
}
var kernel32 = GetModuleHandleW("kernel32.dll");
if (kernel32 == IntPtr.Zero)
throw new Win32Exception(Marshal.GetLastWin32Error(), "GetModuleHandleW(kernel32.dll) failed.");
var loadLibrary = GetProcAddress(kernel32, "LoadLibraryW");
if (loadLibrary == IntPtr.Zero)
throw new Win32Exception(Marshal.GetLastWin32Error(), "GetProcAddress(LoadLibraryW) failed.");
var remoteThread = CreateRemoteThread(
processHandle,
IntPtr.Zero,
0,
loadLibrary,
remoteBuffer,
0,
out _);
if (remoteThread == IntPtr.Zero)
throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateRemoteThread failed.");
try
{
var waitResult = WaitForSingleObject(remoteThread, 10_000);
if (waitResult != 0)
throw new Win32Exception($"Remote LoadLibraryW timed out or failed: {waitResult}");
}
finally
{
CloseHandle(remoteThread);
}
}
finally
{
VirtualFreeEx(processHandle, remoteBuffer, 0, FreeType.Release);
}
}
private static string BuildCommandLine(string exePath, IReadOnlyList<string> gameArgs)
{
var parts = new List<string> { Quote(exePath) };
parts.AddRange(gameArgs.Select(Quote));
return string.Join(' ', parts);
}
private static IntPtr BuildEnvironmentBlock(IReadOnlyDictionary<string, string> variables)
{
if (variables.Count == 0)
return IntPtr.Zero;
var merged = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
merged[(string)entry.Key] = entry.Value?.ToString() ?? string.Empty;
foreach (var pair in variables)
merged[pair.Key] = pair.Value;
var payload = string.Join('\0', merged.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
.Select(x => $"{x.Key}={x.Value}")) + "\0\0";
return Marshal.StringToHGlobalUni(payload);
}
private static string Quote(string value)
{
if (value.Length == 0)
return "\"\"";
if (!value.Any(char.IsWhiteSpace) && !value.Contains('"'))
return value;
return "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
}
[Flags]
private enum CreationFlags : uint
{
CREATE_SUSPENDED = 0x00000004,
CREATE_UNICODE_ENVIRONMENT = 0x00000400
}
[Flags]
private enum AllocationType : uint
{
Commit = 0x1000,
Reserve = 0x2000
}
[Flags]
private enum MemoryProtection : uint
{
ReadWrite = 0x04
}
[Flags]
private enum FreeType : uint
{
Release = 0x8000
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct STARTUPINFOW
{
public int cb;
public string? lpReserved;
public string? lpDesktop;
public string? lpTitle;
public int dwX;
public int dwY;
public int dwXSize;
public int dwYSize;
public int dwXCountChars;
public int dwYCountChars;
public int dwFillAttribute;
public int dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool CreateProcessW(
string? lpApplicationName,
string lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles,
CreationFlags dwCreationFlags,
IntPtr lpEnvironment,
string? lpCurrentDirectory,
ref STARTUPINFOW lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr VirtualAllocEx(
IntPtr hProcess,
IntPtr lpAddress,
nuint dwSize,
AllocationType flAllocationType,
MemoryProtection flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool VirtualFreeEx(
IntPtr hProcess,
IntPtr lpAddress,
nuint dwSize,
FreeType dwFreeType);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
byte[] lpBuffer,
int nSize,
out IntPtr lpNumberOfBytesWritten);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern IntPtr GetModuleHandleW(string lpModuleName);
[DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
private static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr CreateRemoteThread(
IntPtr hProcess,
IntPtr lpThreadAttributes,
nuint dwStackSize,
IntPtr lpStartAddress,
IntPtr lpParameter,
uint dwCreationFlags,
out int lpThreadId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint ResumeThread(IntPtr hThread);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);
}
public sealed class LaunchOptions
{
public required string GamePath { get; init; }
public required IReadOnlyList<string> PatchPaths { get; init; }
public string? WorkingDirectory { get; init; }
public required IReadOnlyList<string> GameArguments { get; init; }
public required IReadOnlyDictionary<string, string> EnvironmentVariables { get; init; }
public static LaunchOptions FromConfig(IEnumerable<string>? extraGameArguments = null)
{
var config = ConfigManager.Config;
var gamePath = ResolvePath(config.Loader.GamePath, AppContext.BaseDirectory);
var patchPaths = ResolvePatchPaths(config.Loader.PatchPaths, AppContext.BaseDirectory);
var gameArgs = new List<string>(config.Loader.Arguments ?? []);
if (extraGameArguments is not null)
gameArgs.AddRange(extraGameArguments.Where(x => !string.IsNullOrWhiteSpace(x)));
var env = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (config.Loader.SetAllProxy && config.Proxy.Enabled)
env["ALL_PROXY"] = $"socks5h://127.0.0.1:{config.Proxy.Port}";
if (string.IsNullOrWhiteSpace(gamePath))
throw new InvalidOperationException("Loader.GamePath is not configured.");
if (!File.Exists(gamePath))
throw new FileNotFoundException("Game executable not found.", gamePath);
if (patchPaths.Count == 0)
throw new InvalidOperationException("At least one patch path is required.");
foreach (var patchPath in patchPaths)
{
if (!File.Exists(patchPath))
throw new FileNotFoundException("Patch DLL not found.", patchPath);
}
var workingDirectory = Path.GetDirectoryName(gamePath);
if (string.IsNullOrWhiteSpace(workingDirectory) || !Directory.Exists(workingDirectory))
throw new DirectoryNotFoundException($"Working directory not found: {workingDirectory}");
return new LaunchOptions
{
GamePath = Path.GetFullPath(gamePath),
PatchPaths = patchPaths,
WorkingDirectory = Path.GetFullPath(workingDirectory),
GameArguments = gameArgs,
EnvironmentVariables = env
};
}
private static string? ResolvePath(string? value, string baseDirectory)
{
if (string.IsNullOrWhiteSpace(value))
return null;
return Path.IsPathRooted(value)
? value
: Path.GetFullPath(Path.Combine(baseDirectory, value));
}
private static List<string> ResolvePatchPaths(IEnumerable<string>? values, string baseDirectory)
{
var result = new List<string>();
if (values is null)
return result;
foreach (var value in values)
{
var resolved = ResolvePath(value, baseDirectory);
if (!string.IsNullOrWhiteSpace(resolved))
result.Add(resolved);
}
return result;
}
}

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>MikuSB.Loader</AssemblyName>
<RootNamespace>MikuSB.Loader</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers> <RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>

View File

@@ -102,11 +102,6 @@ static void UpdateResources(string resourcePackagePath, string resourceTargetDir
ZipFile.ExtractToDirectory(resourcePackagePath, resourceStagingDirectory, overwriteFiles: true); ZipFile.ExtractToDirectory(resourcePackagePath, resourceStagingDirectory, overwriteFiles: true);
var extractedRoot = Directory.GetDirectories(resourceStagingDirectory).FirstOrDefault() ?? resourceStagingDirectory; var extractedRoot = Directory.GetDirectories(resourceStagingDirectory).FirstOrDefault() ?? resourceStagingDirectory;
var excelOutputSource = Path.Combine(extractedRoot, "ExcelOutput"); Directory.CreateDirectory(resourceTargetDirectory);
if (!Directory.Exists(excelOutputSource)) CopyDirectory(extractedRoot, resourceTargetDirectory);
throw new DirectoryNotFoundException($"ExcelOutput directory was not found in resource package: {excelOutputSource}");
var excelOutputTarget = Path.Combine(resourceTargetDirectory, "ExcelOutput");
Directory.CreateDirectory(excelOutputTarget);
CopyDirectory(excelOutputSource, excelOutputTarget);
} }

View File

@@ -24,6 +24,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Proxy", "Proxy\Proxy.csproj
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MikuSB.Updater", "MikuSB.Updater\MikuSB.Updater.csproj", "{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MikuSB.Updater", "MikuSB.Updater\MikuSB.Updater.csproj", "{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MikuSB.Loader", "MikuSB.Loader\MikuSB.Loader.csproj", "{B7AE1E7E-6A42-4E64-B2B1-2EB522F9E3A1}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -62,6 +64,10 @@ Global
{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}.Debug|Any CPU.Build.0 = Debug|Any CPU {CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}.Release|Any CPU.ActiveCfg = Release|Any CPU {CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}.Release|Any CPU.Build.0 = Release|Any CPU {CE0F3A4B-8C55-4A31-A1B5-A0CB1C7F0A11}.Release|Any CPU.Build.0 = Release|Any CPU
{B7AE1E7E-6A42-4E64-B2B1-2EB522F9E3A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B7AE1E7E-6A42-4E64-B2B1-2EB522F9E3A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B7AE1E7E-6A42-4E64-B2B1-2EB522F9E3A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B7AE1E7E-6A42-4E64-B2B1-2EB522F9E3A1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<CETCompat>false</CETCompat> <CETCompat>false</CETCompat>
@@ -50,7 +50,6 @@
Targets="Restore;Publish" Targets="Restore;Publish"
RemoveProperties="PublishProfile" RemoveProperties="PublishProfile"
Properties="Configuration=$(Configuration);RuntimeIdentifier=$(RuntimeIdentifier);SelfContained=false;PublishSingleFile=true;PublishDir=$(_UpdaterPublishDir)" /> Properties="Configuration=$(Configuration);RuntimeIdentifier=$(RuntimeIdentifier);SelfContained=false;PublishSingleFile=true;PublishDir=$(_UpdaterPublishDir)" />
<Copy <Copy
SourceFiles="$(_UpdaterPublishDir)MikuSB.Updater.exe" SourceFiles="$(_UpdaterPublishDir)MikuSB.Updater.exe"
DestinationFiles="$(PublishDir)MikuSB.Updater.exe" DestinationFiles="$(PublishDir)MikuSB.Updater.exe"

Some files were not shown because too many files have changed in this diff Show More