mirror of
https://github.com/MikuLeaks/MikuSB.git
synced 2026-06-04 11:24:00 +00:00
Compare commits
74 Commits
v0.3
...
7507d10072
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7507d10072 | ||
|
|
8a7a919d04 | ||
|
|
2d1079a80d | ||
|
|
677e4027b9 | ||
|
|
4dc28ae423 | ||
|
|
1fce4ca2e6 | ||
|
|
93c59e5730 | ||
|
|
f7814b0537 | ||
|
|
2fdda3157f | ||
|
|
f07c5f77fd | ||
|
|
2883ac3d41 | ||
|
|
49f4fdfda5 | ||
|
|
bcc5716f97 | ||
|
|
4bd66621bf | ||
|
|
14a611bf8e | ||
|
|
283358f1ec | ||
|
|
5d8b3f2bad | ||
|
|
a992bc86c5 | ||
|
|
d97eae6f5b | ||
|
|
04121eb357 | ||
|
|
391879a483 | ||
|
|
8e3323dd94 | ||
|
|
7e83d2ccd2 | ||
|
|
408113963b | ||
|
|
fcf0bf0843 | ||
|
|
2bf7554c53 | ||
|
|
50121d619b | ||
|
|
e8bb77e90e | ||
|
|
5f173ce8d2 | ||
|
|
9b8fa5d7c8 | ||
|
|
1969c1ec89 | ||
|
|
e32319aa50 | ||
|
|
846139347a | ||
|
|
d1102b444c | ||
|
|
3611624073 | ||
|
|
c61ac08dd3 | ||
|
|
720f56c708 | ||
|
|
5fa42bdb23 | ||
|
|
5f3fc0c746 | ||
|
|
2fe795d29a | ||
|
|
5af23a8113 | ||
|
|
ee79486748 | ||
|
|
0cf0e3beb4 | ||
|
|
8d6e0d7638 | ||
|
|
b256fe7b01 | ||
|
|
5f0de1a9f0 | ||
|
|
e93dadafe8 | ||
|
|
069ee6aa2a | ||
|
|
34b93ad55d | ||
|
|
3eca16a661 | ||
|
|
64f977173f | ||
|
|
0e62fcc9b2 | ||
|
|
f4ad74e00d | ||
|
|
ccdfbee828 | ||
|
|
5d0f587fb9 | ||
|
|
94f972ff82 | ||
|
|
2f8d0c6afc | ||
|
|
1b4f3531d1 | ||
|
|
1cac82d10d | ||
|
|
1e4d93bab1 | ||
|
|
f6204134a9 | ||
|
|
57ce0e183b | ||
|
|
5f92f2c116 | ||
|
|
16d1413cd8 | ||
|
|
00d5f35c30 | ||
|
|
7e45bd8dcb | ||
|
|
ac5762fc42 | ||
|
|
12df57b273 | ||
|
|
c109c82a91 | ||
|
|
d214778af1 | ||
|
|
1fc300300f | ||
|
|
0e408c9dad | ||
|
|
7b6d1a3054 | ||
|
|
8415969ffe |
57
.github/workflows/release.yml
vendored
57
.github/workflows/release.yml
vendored
@@ -9,6 +9,9 @@ on:
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
|
||||
jobs:
|
||||
build-release:
|
||||
runs-on: windows-latest
|
||||
@@ -18,7 +21,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Read version
|
||||
id: version
|
||||
@@ -54,3 +57,55 @@ jobs:
|
||||
files: |
|
||||
artifacts/MikuSB-win-x64.zip
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<CETCompat>false</CETCompat>
|
||||
@@ -21,7 +21,6 @@
|
||||
<PackageReference Include="SQLitePCLRaw.provider.e_sqlite3" Version="2.1.10" />
|
||||
<PackageReference Include="SqlSugarCore" Version="5.1.4.172" />
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ public class HttpServerConfig
|
||||
public string BindAddress { get; set; } = "0.0.0.0";
|
||||
public string PublicAddress { get; set; } = "127.0.0.1";
|
||||
public int Port { get; set; } = 21500;
|
||||
public bool EnableLog { get; set; } = true;
|
||||
public bool EnableLog { get; set; } = false;
|
||||
|
||||
public string GetDisplayAddress()
|
||||
{
|
||||
@@ -60,6 +60,7 @@ public class ServerOption
|
||||
public string FallbackLanguage { get; set; } = "EN";
|
||||
public string[] DefaultPermissions { get; set; } = ["Admin"];
|
||||
public ServerProfile ServerProfile { get; set; } = new();
|
||||
public bool EnableGmMenu { get; set; } = false;
|
||||
public bool AutoCreateUser { get; set; } = true;
|
||||
public bool SavePersonalDebugFile { get; set; } = false;
|
||||
public bool AutoSendResponseWhenNoHandler { get; set; } = true;
|
||||
|
||||
@@ -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 uint Genre { get; set; }
|
||||
|
||||
34
Common/Data/Excel/BreakExcel.cs
Normal file
34
Common/Data/Excel/BreakExcel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 int ID { get; set; }
|
||||
|
||||
24
Common/Data/Excel/CallItemExcel.cs
Normal file
24
Common/Data/Excel/CallItemExcel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace MikuSB.Data.Excel;
|
||||
|
||||
[ResourceEntity("card.json")]
|
||||
[ResourceEntity("item/templates/card.json")]
|
||||
public class CardExcel : ExcelResource
|
||||
{
|
||||
public uint Genre { get; set; }
|
||||
@@ -19,6 +19,7 @@ public class CardExcel : ExcelResource
|
||||
[JsonProperty("profile")] public List<List<int>> Profile { get; set; } = [];
|
||||
public List<List<int>> Pieces { get; set; } = [];
|
||||
public List<int> Attribute { get; set; } = [];
|
||||
[JsonProperty("SpineID")] public uint SpineId { get; set; }
|
||||
public override uint GetId()
|
||||
{
|
||||
return Icon;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace MikuSB.Data.Excel;
|
||||
|
||||
[ResourceEntity("card_skin.json")]
|
||||
[ResourceEntity("item/templates/card_skin.json")]
|
||||
public class CardSkinExcel : ExcelResource
|
||||
{
|
||||
public uint Genre { get; set; }
|
||||
|
||||
27
Common/Data/Excel/CardSkinPartsExcel.cs
Normal file
27
Common/Data/Excel/CardSkinPartsExcel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace MikuSB.Data.Excel;
|
||||
|
||||
[ResourceEntity("level.json")]
|
||||
[ResourceEntity("chapter/level.json")]
|
||||
public class ChapterLevelExcel : ExcelResource
|
||||
{
|
||||
public uint ID { get; set; }
|
||||
|
||||
14
Common/Data/Excel/DailyLevelExcel.cs
Normal file
14
Common/Data/Excel/DailyLevelExcel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace MikuSB.Data.Excel;
|
||||
|
||||
[ResourceEntity("manifestation.json")]
|
||||
[ResourceEntity("item/templates/manifestation.json")]
|
||||
public class ManifestationExcel : ExcelResource
|
||||
{
|
||||
public uint Genre { get; set; }
|
||||
|
||||
41
Common/Data/Excel/NodeConditionExcel.cs
Normal file
41
Common/Data/Excel/NodeConditionExcel.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace MikuSB.Data.Excel;
|
||||
|
||||
// nodecondition.json: NodeConditionId → NodeXCost per sub-node (1-9)
|
||||
[ResourceEntity("item/skill/nodecondition.json")]
|
||||
public class NodeConditionExcel : ExcelResource
|
||||
{
|
||||
[JsonProperty("ID")] public uint Id { get; set; }
|
||||
|
||||
[JsonProperty("Node1Cost")] public List<List<int>> Node1Cost { get; set; } = [];
|
||||
[JsonProperty("Node2Cost")] public List<List<int>> Node2Cost { get; set; } = [];
|
||||
[JsonProperty("Node3Cost")] public List<List<int>> Node3Cost { get; set; } = [];
|
||||
[JsonProperty("Node4Cost")] public List<List<int>> Node4Cost { get; set; } = [];
|
||||
[JsonProperty("Node5Cost")] public List<List<int>> Node5Cost { get; set; } = [];
|
||||
[JsonProperty("Node6Cost")] public List<List<int>> Node6Cost { get; set; } = [];
|
||||
[JsonProperty("Node7Cost")] public List<List<int>> Node7Cost { get; set; } = [];
|
||||
[JsonProperty("Node8Cost")] public List<List<int>> Node8Cost { get; set; } = [];
|
||||
[JsonProperty("Node9Cost")] public List<List<int>> Node9Cost { get; set; } = [];
|
||||
|
||||
public List<List<int>> GetNodeCost(int subIdx) => subIdx switch
|
||||
{
|
||||
1 => Node1Cost,
|
||||
2 => Node2Cost,
|
||||
3 => Node3Cost,
|
||||
4 => Node4Cost,
|
||||
5 => Node5Cost,
|
||||
6 => Node6Cost,
|
||||
7 => Node7Cost,
|
||||
8 => Node8Cost,
|
||||
9 => Node9Cost,
|
||||
_ => []
|
||||
};
|
||||
|
||||
public override uint GetId() => Id;
|
||||
|
||||
public override void Loaded()
|
||||
{
|
||||
GameData.NodeConditionData[Id] = this;
|
||||
}
|
||||
}
|
||||
22
Common/Data/Excel/ProfileExcel.cs
Normal file
22
Common/Data/Excel/ProfileExcel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace MikuSB.Data.Excel;
|
||||
|
||||
[ResourceEntity("recycle.json")]
|
||||
[ResourceEntity("item/recycle.json")]
|
||||
public class RecycleExcel : ExcelResource
|
||||
{
|
||||
public int ID { get; set; }
|
||||
|
||||
18
Common/Data/Excel/Rogue3DDailyBuffExcel.cs
Normal file
18
Common/Data/Excel/Rogue3DDailyBuffExcel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace MikuSB.Data.Excel;
|
||||
|
||||
[ResourceEntity("server_01_difficult.json")]
|
||||
[ResourceEntity("dlc/rogue3d/server_01_difficult.json")]
|
||||
public class Rogue3DDifficultExcel : ExcelResource
|
||||
{
|
||||
[JsonProperty("DifficultID")] public uint DifficultId { get; set; }
|
||||
|
||||
19
Common/Data/Excel/Rogue3DSeasonExcel.cs
Normal file
19
Common/Data/Excel/Rogue3DSeasonExcel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
31
Common/Data/Excel/Rogue3DTalentExcel.cs
Normal file
31
Common/Data/Excel/Rogue3DTalentExcel.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
35
Common/Data/Excel/SpineExcel.cs
Normal file
35
Common/Data/Excel/SpineExcel.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace MikuSB.Data.Excel;
|
||||
|
||||
// spine.json: SpineId → Node{i}Req (nodecondition ID per master node index)
|
||||
[ResourceEntity("item/skill/spine.json")]
|
||||
public class SpineExcel : ExcelResource
|
||||
{
|
||||
[JsonProperty("ID")] public uint Id { get; set; }
|
||||
|
||||
[JsonProperty("Node1Req")] public uint Node1Req { get; set; }
|
||||
[JsonProperty("Node2Req")] public uint Node2Req { get; set; }
|
||||
[JsonProperty("Node3Req")] public uint Node3Req { get; set; }
|
||||
[JsonProperty("Node4Req")] public uint Node4Req { get; set; }
|
||||
[JsonProperty("Node5Req")] public uint Node5Req { get; set; }
|
||||
[JsonProperty("Node6Req")] public uint Node6Req { get; set; }
|
||||
|
||||
public uint GetNodeReq(int mastIdx) => mastIdx switch
|
||||
{
|
||||
1 => Node1Req,
|
||||
2 => Node2Req,
|
||||
3 => Node3Req,
|
||||
4 => Node4Req,
|
||||
5 => Node5Req,
|
||||
6 => Node6Req,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
public override uint GetId() => Id;
|
||||
|
||||
public override void Loaded()
|
||||
{
|
||||
GameData.SpineData[Id] = this;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace MikuSB.Data.Excel;
|
||||
|
||||
[ResourceEntity("suplies.json")]
|
||||
[ResourceEntity("item/templates/suplies.json")]
|
||||
public class SuppliesExcel : ExcelResource
|
||||
{
|
||||
public uint Genre { get; set; }
|
||||
|
||||
32
Common/Data/Excel/SupportCardExcel.cs
Normal file
32
Common/Data/Excel/SupportCardExcel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace MikuSB.Data.Excel;
|
||||
|
||||
[ResourceEntity("upgrade_exp.json")]
|
||||
[ResourceEntity("item/upgrade_exp.json")]
|
||||
public class UpgradeExpExcel : ExcelResource
|
||||
{
|
||||
public int Lv { get; set; }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace MikuSB.Data.Excel;
|
||||
|
||||
[ResourceEntity("weapon.json")]
|
||||
[ResourceEntity("item/templates/weapon.json")]
|
||||
public class WeaponExcel : ExcelResource
|
||||
{
|
||||
public uint Genre { get; set; }
|
||||
|
||||
24
Common/Data/Excel/WeaponPartsExcel.cs
Normal file
24
Common/Data/Excel/WeaponPartsExcel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
24
Common/Data/Excel/WeaponSkinExcel.cs
Normal file
24
Common/Data/Excel/WeaponSkinExcel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,19 @@ public static class GameData
|
||||
public static Dictionary<uint, ArItemExcel> ArItemData { get; private set; } = [];
|
||||
public static Dictionary<uint, ManifestationExcel> ManifestationData { get; private set; } = [];
|
||||
public static Dictionary<uint, Rogue3DDifficultExcel> Rogue3DDifficultData { get; private set; } = [];
|
||||
public static Dictionary<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, 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 class GameResourceTemplateId
|
||||
|
||||
@@ -46,7 +46,7 @@ public class ResourceManager
|
||||
foreach (var fileName in attribute.FileName)
|
||||
try
|
||||
{
|
||||
var path = ConfigManager.Config.Path.ResourcePath + "/ExcelOutput/" + fileName;
|
||||
var path = ConfigManager.Config.Path.ResourcePath + "/" + fileName;
|
||||
var file = new FileInfo(path);
|
||||
if (!file.Exists)
|
||||
{
|
||||
|
||||
@@ -19,14 +19,18 @@ public class CharacterInfo
|
||||
public int Exp { get; set; }
|
||||
public uint Break { get; set; }
|
||||
public int Evolue { get; set; }
|
||||
public uint ProLevel { get; set; }
|
||||
public int Trust { get; set; }
|
||||
public uint WeaponUniqueId { 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 uint Expiration { 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> 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 uint Count { get; set; } = 1;
|
||||
|
||||
@@ -45,14 +49,19 @@ public class CharacterInfo
|
||||
Exp = ToUInt32(Exp),
|
||||
Break = Break,
|
||||
Evolue = ToUInt32(Evolue),
|
||||
ProLevel = ProLevel,
|
||||
Trust = ToUInt32(Trust)
|
||||
}
|
||||
};
|
||||
proto.Enhance.Spines.AddRange(Spines.Select(x => (ulong)x));
|
||||
proto.Enhance.Affixs.AddRange(Affixs);
|
||||
|
||||
proto.Slots[4] = WeaponUniqueId;
|
||||
proto.Slots[5] = SkinId;
|
||||
proto.Slots[(uint)ItemCardSlotTypeEnum.SLOT_WEAPON] = WeaponUniqueId;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ public class DatabaseHelper
|
||||
public static readonly ConcurrentDictionary<int, List<BaseDatabaseDataHelper>> UidInstanceMap = [];
|
||||
public static readonly List<int> ToSaveUidList = [];
|
||||
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 LoadAllData;
|
||||
|
||||
@@ -77,15 +80,11 @@ public class DatabaseHelper
|
||||
|
||||
while (!res.IsCompleted)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
LastSaveTick = DateTime.UtcNow.Ticks;
|
||||
|
||||
SaveThread = new Thread(() =>
|
||||
{
|
||||
while (true) CalcSaveDatabase();
|
||||
});
|
||||
SaveThread.Start();
|
||||
_cts = new CancellationTokenSource();
|
||||
_saveTask = RunAutoSave(_cts.Token);
|
||||
|
||||
LoadAllData = true;
|
||||
}
|
||||
@@ -248,6 +247,35 @@ public class DatabaseHelper
|
||||
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
|
||||
public static void CalcSaveDatabase()
|
||||
{
|
||||
@@ -257,6 +285,10 @@ public class DatabaseHelper
|
||||
|
||||
public static void SaveDatabase()
|
||||
{
|
||||
// ensure only one SaveDatabase() runnig
|
||||
if (Interlocked.Exchange(ref _saving, 1) == 1)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var prev = DateTime.Now;
|
||||
@@ -281,11 +313,18 @@ public class DatabaseHelper
|
||||
Math.Round(t, 2).ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
ToSaveUidList.Clear();
|
||||
|
||||
// Thread.Sleep(5000); // for test if saving process taking too long
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("An error occurred while saving the database", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// release lock
|
||||
Volatile.Write(ref _saving, 0);
|
||||
}
|
||||
|
||||
LastSaveTick = DateTime.UtcNow.Ticks;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ public class InventoryData : BaseDatabaseDataHelper
|
||||
|
||||
[SugarColumn(IsJson = true)]
|
||||
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
|
||||
@@ -26,30 +32,37 @@ public class BaseGameItemInfo
|
||||
public uint ItemCount { get; set; }
|
||||
public ItemTypeEnum ItemType { get; set; }
|
||||
public ItemFlagEnum Flag { get; set; } = ItemFlagEnum.FLAG_READED;
|
||||
public uint Level { get; set; }
|
||||
public uint Exp { get; set; }
|
||||
|
||||
public virtual Item ToProto()
|
||||
{
|
||||
return new Item
|
||||
var proto = new Item
|
||||
{
|
||||
Id = UniqueId,
|
||||
Template = TemplateId,
|
||||
Count = ItemCount,
|
||||
Flag = (uint)Flag
|
||||
};
|
||||
if (Level > 0 || Exp > 0)
|
||||
proto.Enhance = new Enhance { Level = Level, Exp = Exp };
|
||||
return proto;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class GrowableItemInfo : BaseGameItemInfo
|
||||
{
|
||||
public bool IsLocked { get; set; }
|
||||
public uint Level { get; set; }
|
||||
public uint Exp { get; set; }
|
||||
public new uint Level { get; set; }
|
||||
public new uint Exp { get; set; }
|
||||
public uint Break { get; set; }
|
||||
public uint Evolue { get; set; }
|
||||
public uint EquipAvatarId { get; set; }
|
||||
}
|
||||
|
||||
public class GameWeaponInfo : GrowableItemInfo
|
||||
{
|
||||
[SugarColumn(IsJson = true)] public Dictionary<uint, ulong> PartSlots { get; set; } = [];
|
||||
public override Item ToProto()
|
||||
{
|
||||
var proto = new Item
|
||||
@@ -62,14 +75,18 @@ public class GameWeaponInfo : GrowableItemInfo
|
||||
{
|
||||
Level = Level,
|
||||
Exp = Exp,
|
||||
Break = Break
|
||||
Break = Break,
|
||||
Evolue = Evolue
|
||||
}
|
||||
};
|
||||
foreach (var (slot, uid) in PartSlots) proto.Slots[slot] = uid;
|
||||
return proto;
|
||||
}
|
||||
}public class GameSkinInfo : BaseGameItemInfo
|
||||
}
|
||||
public class GameSkinInfo : BaseGameItemInfo
|
||||
{
|
||||
public uint Level { get; set; }
|
||||
[SugarColumn(IsJson = true)] public Dictionary<uint, ulong> PartSlots { get; set; } = [];
|
||||
public uint SkinType { get; set; }
|
||||
public override Item ToProto()
|
||||
{
|
||||
var proto = new Item
|
||||
@@ -79,6 +96,31 @@ public class GameWeaponInfo : GrowableItemInfo
|
||||
Count = ItemCount,
|
||||
Flag = (uint)Flag,
|
||||
};
|
||||
proto.Slots[(uint)ItemSkinSlotTypeEnum.SLOT_CARD_SKIL_TYPE] = Math.Min(SkinType, 1);
|
||||
foreach (var (slot, uid) in PartSlots) proto.Slots[slot] = uid;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace MikuSB.Database.Player;
|
||||
[SugarTable("Player")]
|
||||
public class PlayerGameData : BaseDatabaseDataHelper
|
||||
{
|
||||
public const string DefaultDisplayName = "Miku";
|
||||
|
||||
public string? Name { get; set; } = "";
|
||||
public string? Signature { get; set; } = "MikuPS";
|
||||
public uint Level { get; set; } = 100;
|
||||
@@ -16,6 +18,7 @@ public class PlayerGameData : BaseDatabaseDataHelper
|
||||
public Sex Gender { get; set; } = Sex.Female;
|
||||
public uint Vigor { get; set; } = 240;
|
||||
[SugarColumn(IsJson = true)] public List<PlayerAttr> Attrs { get; set; } = [];
|
||||
[SugarColumn(IsJson = true)] public List<PlayerStrAttr> StrAttrs { get; set; } = [];
|
||||
[SugarColumn(IsJson = true)] public List<ulong> ShowItems { get; set; } = [];
|
||||
|
||||
public static PlayerGameData? GetPlayerByUid(long uid)
|
||||
@@ -24,13 +27,30 @@ public class PlayerGameData : BaseDatabaseDataHelper
|
||||
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()
|
||||
{
|
||||
var displayName = NormalizeDisplayName(Name);
|
||||
var proto = new PlayerProfile
|
||||
{
|
||||
Pid = (uint)Uid,
|
||||
Account = Name,
|
||||
Name = Name,
|
||||
Account = displayName,
|
||||
Name = displayName,
|
||||
Level = Level,
|
||||
Sex = Gender,
|
||||
Sign = Signature,
|
||||
@@ -45,4 +65,11 @@ public class PlayerAttr
|
||||
public uint Gid { get; set; }
|
||||
public uint Sid { get; set; }
|
||||
public uint Val { get; set; }
|
||||
}
|
||||
|
||||
public class PlayerStrAttr
|
||||
{
|
||||
public uint Gid { get; set; }
|
||||
public uint Sid { get; set; }
|
||||
public string Val { get; set; } = "";
|
||||
}
|
||||
@@ -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,// 心选角色
|
||||
}
|
||||
|
||||
@@ -25,4 +25,56 @@ public enum ItemTypeEnum
|
||||
TYPE_MAIN_SCENE = 21, //主界面场景道具
|
||||
TYPE_AR = 24, //AR道具
|
||||
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, // 心选角色
|
||||
}
|
||||
24
Common/Enums/Player/ProfileTypeEnum.cs
Normal file
24
Common/Enums/Player/ProfileTypeEnum.cs
Normal 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, //聊天气泡
|
||||
}
|
||||
@@ -35,6 +35,13 @@ public class ServerTextCHS
|
||||
/// </summary>
|
||||
public class WordTextCHS
|
||||
{
|
||||
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 Avatar => "角色";
|
||||
public string Material => "材料";
|
||||
@@ -121,6 +128,7 @@ public class CommandTextCHS
|
||||
public HelpTextCHS Help { get; } = new();
|
||||
public GirlTextCHS Girl { get; } = new();
|
||||
public GiveAllTextCHS GiveAll { get; } = new();
|
||||
public DebugTextCHS Debug { get; } = new();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -218,11 +226,13 @@ public class GirlTextCHS
|
||||
|
||||
public string Usage =>
|
||||
"用法: /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>";
|
||||
|
||||
public string NotFound => "角色不存在!";
|
||||
public string Added => "已为玩家添加 {0} 个角色!";
|
||||
public string UpdateLevel => "已将 {1} 个角色等级设置为 {0}!";
|
||||
public string UpdateNeuronicLevel => "已将 {1} 个角色的神经元等级设置为 {0}!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -230,11 +240,32 @@ public class GirlTextCHS
|
||||
/// </summary>
|
||||
public class GiveAllTextCHS
|
||||
{
|
||||
public string Desc => "给玩家所有物品\n" +
|
||||
"备注: -1 代表全部";
|
||||
public string Usage => "用法: /giveall weapon <detail/-1> -p<particular> -l<level>";
|
||||
public string WeaponNotFound => "找不到武器!";
|
||||
public string WeaponAdded => "已添加 {0} 把武器给玩家!";
|
||||
public string Desc => "给予玩家所有物品\n" +
|
||||
"注意:-1 表示全部";
|
||||
public string Usage => "用法:/giveall weapon <detail/-1> -p<特定> -l<等級>\n" +
|
||||
"用法:/giveall weaponskin <detail/-1> -p<特定>\n" +
|
||||
"用法:/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 => "个人调试文件输出已禁用。";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -35,6 +35,13 @@ public class ServerTextCHT
|
||||
/// </summary>
|
||||
public class WordTextCHT
|
||||
{
|
||||
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 Avatar => "角色";
|
||||
public string Material => "材料";
|
||||
@@ -121,6 +128,7 @@ public class CommandTextCHT
|
||||
public HelpTextCHT Help { get; } = new();
|
||||
public GirlTextCHT Girl { get; } = new();
|
||||
public GiveAllTextCHT GiveAll { get; } = new();
|
||||
public DebugTextCHT Debug { get; } = new();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -218,11 +226,13 @@ public class GirlTextCHT
|
||||
|
||||
public string Usage =>
|
||||
"用法: /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>";
|
||||
|
||||
public string NotFound => "角色不存在!";
|
||||
public string Added => "已為玩家新增 {0} 個角色!";
|
||||
public string UpdateLevel => "已將 {1} 個角色等級設為 {0}!";
|
||||
public string UpdateNeuronicLevel => "已將 {1} 個角色的神經元等級設置為 {0}!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -230,11 +240,32 @@ public class GirlTextCHT
|
||||
/// </summary>
|
||||
public class GiveAllTextCHT
|
||||
{
|
||||
public string Desc => "給玩家所有物品\n" +
|
||||
"備註: -1 代表全部";
|
||||
public string Usage => "用法: /giveall weapon <detail/-1> -p<particular> -l<level>";
|
||||
public string WeaponNotFound => "找不到武器!";
|
||||
public string WeaponAdded => "已添加 {0} 把武器給玩家!";
|
||||
public string Desc => "給予玩家所有物品\n" +
|
||||
"注意:-1 表示全部";
|
||||
public string Usage => "用法:/giveall weapon <detail/-1> -p<特定> -l<等級>\n" +
|
||||
"用法:/giveall weaponskin <detail/-1> -p<特定>\n" +
|
||||
"用法:/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 => "個人調試檔案輸出已停用。";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -35,10 +35,14 @@ public class ServerTextEN
|
||||
/// </summary>
|
||||
public class WordTextEN
|
||||
{
|
||||
public string Star => "Star";
|
||||
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 Material => "Material";
|
||||
public string Stigmata => "Stigmata";
|
||||
public string SupportCard => "Support Card";
|
||||
public string Weapon => "Weapon";
|
||||
public string Banner => "Gacha";
|
||||
public string Activity => "Activity";
|
||||
@@ -83,6 +87,7 @@ public class CommandTextEN
|
||||
public HelpTextEN Help { get; } = new();
|
||||
public GirlTextEN Girl { get; } = new();
|
||||
public GiveAllTextEN GiveAll { get; } = new();
|
||||
public DebugTextEN Debug { get; } = new();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -187,11 +192,13 @@ public class GirlTextEN
|
||||
|
||||
public string Usage =>
|
||||
"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>";
|
||||
|
||||
public string NotFound => "Character not found!";
|
||||
public string Added => "Granted {0} character(s) to player!";
|
||||
public string UpdateLevel => "Set {1} character(s) to level {0}!";
|
||||
public string UpdateNeuronicLevel => "Set {1} character(s) Neuronic to level {0}!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -201,9 +208,30 @@ public class GiveAllTextEN
|
||||
{
|
||||
public string Desc => "Give all items to player\n"+
|
||||
"Note: -1 means all";
|
||||
public string Usage => "Usage: /giveall weapon <detail/-1> -p<particular> -l<level>";
|
||||
public string WeaponNotFound => "Weapon not found!";
|
||||
public string WeaponAdded => "Added {0} weapon(s) to player!";
|
||||
public string Usage => "Usage: /giveall weapon <detail/-1> -p<particular> -l<level>\n" +
|
||||
"Usage: /giveall weaponskin <detail/-1> -p<particular>\n" +
|
||||
"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.";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -53,7 +53,7 @@ public class IConsole
|
||||
Console.WriteLine();
|
||||
Input = [];
|
||||
CursorIndex = 0;
|
||||
if (InputHistory.Count >= HistoryMaxCount)
|
||||
if (InputHistory.Count >= HistoryMaxCount)
|
||||
InputHistory.RemoveAt(0);
|
||||
InputHistory.Add(input);
|
||||
HistoryIndex = InputHistory.Count;
|
||||
@@ -80,7 +80,7 @@ public class IConsole
|
||||
public static void HandleUpArrow()
|
||||
{
|
||||
if (InputHistory.Count == 0) return;
|
||||
|
||||
|
||||
if (HistoryIndex > 0)
|
||||
{
|
||||
HistoryIndex--;
|
||||
@@ -94,7 +94,7 @@ public class IConsole
|
||||
public static void HandleDownArrow()
|
||||
{
|
||||
if (HistoryIndex >= InputHistory.Count) return;
|
||||
|
||||
|
||||
HistoryIndex++;
|
||||
if (HistoryIndex >= InputHistory.Count)
|
||||
{
|
||||
@@ -102,7 +102,7 @@ public class IConsole
|
||||
Input = [];
|
||||
CursorIndex = 0;
|
||||
}
|
||||
else
|
||||
else
|
||||
{
|
||||
var history = InputHistory[HistoryIndex];
|
||||
Input = [.. history];
|
||||
@@ -114,7 +114,7 @@ public class IConsole
|
||||
public static void HandleLeftArrow()
|
||||
{
|
||||
if (CursorIndex <= 0) return;
|
||||
|
||||
|
||||
var (left, _) = Console.GetCursorPosition();
|
||||
CursorIndex--;
|
||||
Console.SetCursorPosition(left - GetWidth(Input[CursorIndex].ToString()), Console.CursorTop);
|
||||
@@ -123,7 +123,7 @@ public class IConsole
|
||||
public static void HandleRightArrow()
|
||||
{
|
||||
if (CursorIndex >= Input.Count) return;
|
||||
|
||||
|
||||
var (left, _) = Console.GetCursorPosition();
|
||||
CursorIndex++;
|
||||
Console.SetCursorPosition(left + GetWidth(Input[CursorIndex - 1].ToString()), Console.CursorTop);
|
||||
@@ -148,13 +148,29 @@ public class IConsole
|
||||
|
||||
#endregion
|
||||
|
||||
public static string ListenConsole()
|
||||
public static async Task ListenConsole(CancellationToken exitToken)
|
||||
{
|
||||
while (true)
|
||||
while (!exitToken.IsCancellationRequested)
|
||||
{
|
||||
ConsoleKeyInfo keyInfo;
|
||||
try { keyInfo = Console.ReadKey(true); }
|
||||
catch (InvalidOperationException) { continue; }
|
||||
try
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -38,9 +38,9 @@ public class PlayerCommandSender(PlayerInstance player) : ICommandSender
|
||||
Type = ChatType.Friend,
|
||||
Sender = (uint)ConfigManager.Config.ServerOption.ServerProfile.Uid,
|
||||
Recver = (uint)Player.Uid,
|
||||
Text = msg,
|
||||
Text = ChatMessageHelper.NormalizeForClient(msg),
|
||||
Profile = Player.ToServerFriendProto(),
|
||||
TimeStamp = (uint)Extensions.GetUnixMs()
|
||||
TimeStamp = ChatMessageHelper.BuildClientTimestamp()
|
||||
};
|
||||
await Player.SendPacket(CmdIds.NtfFriendChat, data);
|
||||
}
|
||||
|
||||
69
GameServer/Command/Commands/CommandDebug.cs
Normal file
69
GameServer/Command/Commands/CommandDebug.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -84,4 +84,49 @@ public class CommandGirl : ICommands
|
||||
level.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()));
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ public class CommandGiveAll : ICommands
|
||||
if (await arg.GetOption('l') is not int level) return;
|
||||
|
||||
var detail = arg.GetInt(0);
|
||||
level = Math.Clamp(level, 1, 80);
|
||||
level = Math.Clamp(level, 1, 90);
|
||||
var player = arg.Target!.Player!;
|
||||
List<GameWeaponInfo> weapons = [];
|
||||
if (detail == -1)
|
||||
@@ -36,12 +36,226 @@ public class CommandGiveAll : ICommands
|
||||
var weapon = await player.InventoryManager!.AddWeaponItem(ItemTypeEnum.TYPE_WEAPON, (uint)detail,(uint)particular,1,(uint)level,false);
|
||||
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;
|
||||
}
|
||||
weapons.Add(weapon);
|
||||
}
|
||||
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()));
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ public class CharacterManager(PlayerInstance player) : BasePlayerManager(player)
|
||||
var weaponInfo = await Player.InventoryManager!.AddWeaponItem((ItemTypeEnum)CharacterExcel.DefaultWeaponGPDL[0], CharacterExcel.DefaultWeaponGPDL[1], CharacterExcel.DefaultWeaponGPDL[2], CharacterExcel.DefaultWeaponGPDL[3],sendPacket:false);
|
||||
if (weaponInfo != null) character.WeaponUniqueId = weaponInfo.UniqueId;
|
||||
|
||||
var skinInfo = Player.InventoryManager!.GetNormalItemGDPL(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);
|
||||
if (skinInfo != null) character.SkinId = skinInfo.UniqueId;
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player)
|
||||
TemplateId = templateId,
|
||||
UniqueId = InventoryData.NextUniqueUid++,
|
||||
Level = weaponLevel,
|
||||
Break = weaponData.InitBreak,
|
||||
ItemType = ItemTypeEnum.TYPE_WEAPON,
|
||||
Break = GetWeaponBreak(weaponLevel),
|
||||
ItemType = genre,
|
||||
ItemCount = 1
|
||||
};
|
||||
InventoryData.Weapons[weaponInfo.UniqueId] = weaponInfo;
|
||||
@@ -35,6 +35,17 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player)
|
||||
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)
|
||||
{
|
||||
return InventoryData.Weapons.GetValueOrDefault(uniqueId);
|
||||
@@ -51,27 +62,43 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player)
|
||||
return InventoryData.Weapons.Values.FirstOrDefault(x => x.TemplateId == templateId);
|
||||
}
|
||||
|
||||
public async ValueTask<BaseGameItemInfo?> AddSkinItem(ItemTypeEnum genre, uint detail, uint particular, uint level = 1, bool sendPacket = true)
|
||||
public async ValueTask<GameSkinInfo?> AddSkinItem(ItemTypeEnum genre, uint detail, uint particular, uint level = 1, bool sendPacket = true)
|
||||
{
|
||||
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);
|
||||
if (skinData == null) return null;
|
||||
|
||||
var templateId = GameResourceTemplateId.FromGdpl((uint)genre,detail,particular,level);
|
||||
var skinInfo = new BaseGameItemInfo
|
||||
if (InventoryData.Items.Values.Any(x => x.TemplateId == templateId)) return null;
|
||||
var skinInfo = new GameSkinInfo
|
||||
{
|
||||
TemplateId = templateId,
|
||||
UniqueId = InventoryData.NextUniqueUid++,
|
||||
ItemType = ItemTypeEnum.TYPE_CARD_SKIN,
|
||||
ItemType = genre,
|
||||
ItemCount = 1
|
||||
};
|
||||
InventoryData.Items[skinInfo.UniqueId] = skinInfo;
|
||||
InventoryData.Skins[skinInfo.UniqueId] = skinInfo;
|
||||
|
||||
if (sendPacket) await Player.SendPacket(new PacketNtfCallScript([skinInfo]));
|
||||
|
||||
return skinInfo;
|
||||
}
|
||||
|
||||
public GameSkinInfo? GetSkinItem(uint uniqueId)
|
||||
{
|
||||
return InventoryData.Skins.GetValueOrDefault(uniqueId);
|
||||
}
|
||||
|
||||
public GameSkinInfo? GetSkinItemByTemplateId(ulong templateId)
|
||||
{
|
||||
return InventoryData.Skins.Values.FirstOrDefault(x => x.TemplateId == templateId);
|
||||
}
|
||||
|
||||
public GameSkinInfo? GetSkinItemGDPL(ItemTypeEnum genre, uint detail, uint particular, uint level)
|
||||
{
|
||||
var templateId = GameResourceTemplateId.FromGdpl((uint)genre, detail, particular, level);
|
||||
return InventoryData.Skins.Values.FirstOrDefault(x => x.TemplateId == templateId);
|
||||
}
|
||||
|
||||
public async ValueTask<BaseGameItemInfo?> AddArItem(ItemTypeEnum genre, uint detail, uint particular, uint level = 1, bool sendPacket = true)
|
||||
{
|
||||
if (genre != ItemTypeEnum.TYPE_AR) return null;
|
||||
@@ -84,7 +111,7 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player)
|
||||
{
|
||||
TemplateId = templateId,
|
||||
UniqueId = InventoryData.NextUniqueUid++,
|
||||
ItemType = ItemTypeEnum.TYPE_AR,
|
||||
ItemType = genre,
|
||||
ItemCount = 1
|
||||
};
|
||||
InventoryData.Items[arInfo.UniqueId] = arInfo;
|
||||
@@ -94,6 +121,44 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player)
|
||||
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)
|
||||
{
|
||||
if (genre != ItemTypeEnum.TYPE_MANIFESTATION) return null;
|
||||
@@ -106,7 +171,7 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player)
|
||||
{
|
||||
TemplateId = templateId,
|
||||
UniqueId = InventoryData.NextUniqueUid++,
|
||||
ItemType = ItemTypeEnum.TYPE_MANIFESTATION,
|
||||
ItemType = genre,
|
||||
ItemCount = 1
|
||||
};
|
||||
InventoryData.Items[manifestInfo.UniqueId] = manifestInfo;
|
||||
@@ -162,4 +227,110 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player)
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
27
GameServer/Game/Player/ChatMessageHelper.cs
Normal file
27
GameServer/Game/Player/ChatMessageHelper.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ public class PlayerInstance(PlayerGameData data)
|
||||
{
|
||||
// new player
|
||||
IsNewPlayer = true;
|
||||
Data.Name = AccountData.GetAccountByUid(uid)?.Username;
|
||||
Data.Name = PlayerGameData.NormalizeDisplayName(AccountData.GetAccountByUid(uid)?.Username);
|
||||
|
||||
DatabaseHelper.CreateInstance(Data);
|
||||
|
||||
@@ -79,34 +79,6 @@ public class PlayerInstance(PlayerGameData data)
|
||||
|
||||
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();
|
||||
|
||||
@@ -134,6 +106,7 @@ public class PlayerInstance(PlayerGameData data)
|
||||
public async ValueTask OnEnterGame()
|
||||
{
|
||||
if (!Initialized) await InitialPlayerManager();
|
||||
Data.EnsureDisplayName();
|
||||
await CharacterManager.RepairCharacterWeapons();
|
||||
await EnsureSupplies();
|
||||
}
|
||||
@@ -187,9 +160,9 @@ public class PlayerInstance(PlayerGameData data)
|
||||
Sender = sendUid,
|
||||
Recver = recvUid,
|
||||
Emoji = emojiId ?? 0,
|
||||
Text = message ?? "",
|
||||
Text = ChatMessageHelper.NormalizeForClient(message),
|
||||
Profile = Data.ToProfileProto(),
|
||||
TimeStamp = (uint)Extensions.GetUnixMs()
|
||||
TimeStamp = ChatMessageHelper.BuildClientTimestamp()
|
||||
};
|
||||
|
||||
await SendPacket(CmdIds.NtfFriendChat, data);
|
||||
@@ -225,16 +198,19 @@ public class PlayerInstance(PlayerGameData data)
|
||||
|
||||
public Proto.Player ToPlayerProto()
|
||||
{
|
||||
BuildPlayerAttr();
|
||||
var displayName = PlayerGameData.NormalizeDisplayName(Data.Name);
|
||||
var proto = new Proto.Player
|
||||
{
|
||||
Pid = (ulong)Data.Uid,
|
||||
Account = Data.Name,
|
||||
Provider = Data.Name,
|
||||
Name = Data.Name,
|
||||
Account = displayName,
|
||||
Provider = displayName,
|
||||
Name = displayName,
|
||||
Level = Data.Level,
|
||||
Sex = Data.Gender,
|
||||
Vigor = Data.Vigor,
|
||||
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());
|
||||
@@ -259,6 +235,11 @@ public class PlayerInstance(PlayerGameData data)
|
||||
return proto;
|
||||
}
|
||||
|
||||
public void SetDisplayName(string? name)
|
||||
{
|
||||
Data.Name = PlayerGameData.NormalizeDisplayName(name);
|
||||
}
|
||||
|
||||
public void SetShowItem(int index, ulong itemId)
|
||||
{
|
||||
if (index <= 0)
|
||||
@@ -286,6 +267,38 @@ public class PlayerInstance(PlayerGameData data)
|
||||
return (gid << 16) | sid;
|
||||
}
|
||||
|
||||
public void BuildPlayerAttr()
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<(uint Gid, uint Sid, uint Value)> BuildLobbyBootstrapAttrs()
|
||||
{
|
||||
// GuideLogic uses group 4. Value 999 is safely above every configured step count,
|
||||
@@ -330,6 +343,12 @@ public class PlayerInstance(PlayerGameData data)
|
||||
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);
|
||||
}
|
||||
|
||||
// Main Scene 0 mean default scene
|
||||
yield return (132, 1, 0);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<CETCompat>false</CETCompat>
|
||||
@@ -22,7 +22,6 @@
|
||||
<ProjectReference Include="..\Common\Common.csproj" />
|
||||
<ProjectReference Include="..\TcpSharp\TcpSharp.csproj" />
|
||||
<ProjectReference Include="..\Proto\Proto.csproj" />
|
||||
<PackageReference Include="System.IO.Pipelines" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -12,11 +12,19 @@ public class EnterGirlRoom : ICallGSHandler
|
||||
var req = JsonSerializer.Deserialize<EnterGirlRoomParam>(param);
|
||||
var response = new JsonObject
|
||||
{
|
||||
["nCardId"] = req?.CardId ?? 1,
|
||||
["nSkinId"] = req?.SkinId ?? 0,
|
||||
["bOpen"] = true
|
||||
["nCardId"] = 0,
|
||||
["nSkinId"] = 0,
|
||||
["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());
|
||||
}
|
||||
}
|
||||
@@ -26,6 +34,6 @@ internal sealed class EnterGirlRoomParam
|
||||
[JsonPropertyName("nSkinId")]
|
||||
public int SkinId { get; set; }
|
||||
|
||||
[JsonPropertyName("nCardID")]
|
||||
[JsonPropertyName("nCardId")]
|
||||
public uint CardId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using MikuSB.Database;
|
||||
using MikuSB.Proto;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MikuSB.GameServer.Server.CallGS.Handlers.Girl;
|
||||
|
||||
[CallGSApi("GirlCard_ProLevelPromote")]
|
||||
public class GirlCard_ProLevelPromote : ICallGSHandler
|
||||
{
|
||||
private const uint MaxProLevel = 3;
|
||||
|
||||
public async Task Handle(Connection connection, string param, ushort seqNo)
|
||||
{
|
||||
var player = connection.Player!;
|
||||
var req = JsonSerializer.Deserialize<ProLevelPromoteParam>(param);
|
||||
if (req == null || req.CardId == 0)
|
||||
{
|
||||
await CallGSRouter.SendScript(connection, "GirlCard_ProLevelPromote", "{\"sErr\":\"error.BadParam\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
var card = player.CharacterManager.GetCharacterByGUID((uint)req.CardId);
|
||||
if (card == null)
|
||||
{
|
||||
await CallGSRouter.SendScript(connection, "GirlCard_ProLevelPromote", "{\"sErr\":\"error.BadParam\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (card.ProLevel >= MaxProLevel)
|
||||
{
|
||||
await CallGSRouter.SendScript(connection, "GirlCard_ProLevelPromote", "{\"sErr\":\"error.BadParam\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
card.ProLevel++;
|
||||
|
||||
DatabaseHelper.SaveDatabaseType(player.CharacterManager.CharacterData);
|
||||
|
||||
var sync = new NtfSyncPlayer();
|
||||
sync.Items.Add(card.ToProto());
|
||||
|
||||
// s2c callback takes no params — return empty arg
|
||||
await CallGSRouter.SendScript(connection, "GirlCard_ProLevelPromote", "{}", sync);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ProLevelPromoteParam
|
||||
{
|
||||
[JsonPropertyName("nID")]
|
||||
public int CardId { get; set; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
using System.Text.Json;
|
||||
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.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -10,21 +16,83 @@ public class GirlSkin_ChangeSkinType : ICallGSHandler
|
||||
public async Task Handle(Connection connection, string param, ushort seqNo)
|
||||
{
|
||||
var req = JsonSerializer.Deserialize<ChangeSkinTypeParam>(param);
|
||||
var skinType = ClampClientSkinType(req?.Type ?? 0);
|
||||
var response = new JsonObject
|
||||
{
|
||||
["nType"] = req?.Type ?? 1,
|
||||
["nType"] = skinType,
|
||||
["nSkinId"] = req?.SkinId
|
||||
};
|
||||
// TODO change type in proto Item ??
|
||||
await CallGSRouter.SendScript(connection, "GirlSkin_ChangeSkinType", response.ToJsonString());
|
||||
if (req == null)
|
||||
{
|
||||
await CallGSRouter.SendScript(connection, "GirlSkin_ChangeSkinType", response.ToJsonString());
|
||||
return;
|
||||
}
|
||||
|
||||
var player = connection.Player!;
|
||||
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)
|
||||
{
|
||||
await CallGSRouter.SendScript(connection, "GirlSkin_ChangeSkinType", response.ToJsonString());
|
||||
return;
|
||||
}
|
||||
|
||||
var sync = new NtfSyncPlayer
|
||||
{
|
||||
Items = { skinData.ToProto() }
|
||||
};
|
||||
|
||||
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
|
||||
{
|
||||
[JsonPropertyName("nType")]
|
||||
public int? Type { get; set; }
|
||||
public uint Type { get; set; }
|
||||
|
||||
[JsonPropertyName("nSkinId")]
|
||||
public uint? SkinId { get; set; }
|
||||
public uint SkinId { get; set; }
|
||||
}
|
||||
|
||||
182
GameServer/Server/CallGS/Handlers/Girl/GirlSpine_ChildUnLock.cs
Normal file
182
GameServer/Server/CallGS/Handlers/Girl/GirlSpine_ChildUnLock.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using MikuSB.Data;
|
||||
using MikuSB.Database;
|
||||
using MikuSB.Database.Player;
|
||||
using MikuSB.Proto;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MikuSB.GameServer.Server.CallGS.Handlers.Girl;
|
||||
|
||||
// Spine node encoding: (MastIdx << 8) | SubIdx stored as uint in CharacterInfo.Spines
|
||||
// GetStrAttribute(Gid=30, Sid=Detail) stores JSON: { "<Particular>": { "ns": <MastIdx>, "tbn": [0,0], "tbr": [] } }
|
||||
[CallGSApi("GirlSpine_ChildUnLock")]
|
||||
public class GirlSpine_ChildUnLock : ICallGSHandler
|
||||
{
|
||||
private const uint SpineStrAttrGid = 30;
|
||||
|
||||
public async Task Handle(Connection connection, string param, ushort seqNo)
|
||||
{
|
||||
var player = connection.Player!;
|
||||
var req = JsonSerializer.Deserialize<ChildUnLockParam>(param);
|
||||
if (req == null || req.CardId == 0 || req.Info == null || req.Materials == null)
|
||||
{
|
||||
await CallGSRouter.SendScript(connection, "GirlSpine_ChildUnLock", "{\"sErr\":\"error.BadParam\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
var card = player.CharacterManager.GetCharacterByGUID((uint)req.CardId);
|
||||
if (card == null)
|
||||
{
|
||||
await CallGSRouter.SendScript(connection, "GirlSpine_ChildUnLock", "{\"sErr\":\"error.BadParam\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
var mastIdx = req.Info.Indx;
|
||||
var subIdx = req.Info.InSubIdx;
|
||||
if (mastIdx <= 0 || subIdx <= 0)
|
||||
{
|
||||
await CallGSRouter.SendScript(connection, "GirlSpine_ChildUnLock", "{\"sErr\":\"error.BadParam\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Spines[MastIdx-1] is a bitmask; bit (SubIdx-1) = 1 means that sub-node is unlocked.
|
||||
// GetSpine(MastIdx, SubIdx) checks (Spines[MastIdx-1] & (1 << (SubIdx-1))) != 0
|
||||
int spineListIdx = mastIdx - 1;
|
||||
uint spineBit = 1u << (subIdx - 1);
|
||||
|
||||
while (card.Spines.Count <= spineListIdx)
|
||||
card.Spines.Add(0);
|
||||
|
||||
if ((card.Spines[spineListIdx] & spineBit) != 0)
|
||||
{
|
||||
await CallGSRouter.SendScript(connection, "GirlSpine_ChildUnLock", "{\"sErr\":\"tip.girlcard_alread_break\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Consume materials
|
||||
var requestedMaterials = new Dictionary<ulong, uint>();
|
||||
foreach (var row in req.Materials)
|
||||
{
|
||||
if (row == null || row.Count < 5) continue;
|
||||
var genre = (uint)Math.Max(0, row[0]);
|
||||
var detail = (uint)Math.Max(0, row[1]);
|
||||
var particular = (uint)Math.Max(0, row[2]);
|
||||
var level = (uint)Math.Max(0, row[3]);
|
||||
var count = (uint)Math.Max(0, row[4]);
|
||||
if (genre == 0 || detail == 0 || particular == 0 || level == 0 || count == 0) continue;
|
||||
var tid = GameResourceTemplateId.FromGdpl(genre, detail, particular, level);
|
||||
requestedMaterials[tid] = requestedMaterials.GetValueOrDefault(tid) + count;
|
||||
}
|
||||
|
||||
var syncItems = new List<Item>();
|
||||
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, "GirlSpine_ChildUnLock", "{\"sErr\":\"tip.not_material\"}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Unlock the spine node by setting the corresponding bit
|
||||
card.Spines[spineListIdx] |= spineBit;
|
||||
syncItems.Add(card.ToProto());
|
||||
|
||||
// Extract Detail and Particular from TemplateId for StrAttr
|
||||
var cardDetail = (uint)((card.TemplateId >> 16) & 0xFFFF);
|
||||
var cardParticular = (uint)((card.TemplateId >> 32) & 0xFFFF);
|
||||
|
||||
// Build and persist StrAttr JSON: { "<particular>": { "ns": mastIdx, "tbn": [0,0], "tbr": [] } }
|
||||
UpdateSpineStrAttr(player.Data, cardDetail, cardParticular, mastIdx);
|
||||
|
||||
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
|
||||
DatabaseHelper.SaveDatabaseType(player.CharacterManager.CharacterData);
|
||||
DatabaseHelper.SaveDatabaseType(player.Data);
|
||||
|
||||
// Send NtfSetStrAttr so client's GetStrAttribute(30, Detail) returns fresh data
|
||||
var strAttrData = GetSpineStrAttrJson(player.Data, cardDetail);
|
||||
var ntfStrAttr = new NtfSetStrAttr { Gid = SpineStrAttrGid, Sid = cardDetail, Val = strAttrData };
|
||||
await connection.Player!.SendPacket(CmdIds.NtfSetStrAttr, ntfStrAttr);
|
||||
|
||||
var sync = new NtfSyncPlayer();
|
||||
sync.Items.AddRange(syncItems);
|
||||
|
||||
var rsp = $"{{\"tb\":{{\"D\":{cardDetail},\"pId\":{req.CardId},\"MastIdx\":{mastIdx},\"SubIdx\":{subIdx}}}}}";
|
||||
await CallGSRouter.SendScript(connection, "GirlSpine_ChildUnLock", rsp, sync);
|
||||
}
|
||||
|
||||
private static void UpdateSpineStrAttr(PlayerGameData data, uint detail, uint particular, int mastIdx)
|
||||
{
|
||||
var existing = data.StrAttrs.FirstOrDefault(x => x.Gid == SpineStrAttrGid && x.Sid == detail);
|
||||
if (existing == null)
|
||||
{
|
||||
existing = new PlayerStrAttr { Gid = SpineStrAttrGid, Sid = detail, Val = "{}" };
|
||||
data.StrAttrs.Add(existing);
|
||||
}
|
||||
|
||||
var root = JsonSerializer.Deserialize<Dictionary<string, SpineStrData>>(existing.Val)
|
||||
?? new Dictionary<string, SpineStrData>();
|
||||
|
||||
var key = particular.ToString();
|
||||
if (!root.TryGetValue(key, out var entry))
|
||||
entry = new SpineStrData();
|
||||
|
||||
entry.Ns = mastIdx;
|
||||
root[key] = entry;
|
||||
|
||||
existing.Val = JsonSerializer.Serialize(root);
|
||||
}
|
||||
|
||||
private static string GetSpineStrAttrJson(PlayerGameData data, uint detail)
|
||||
{
|
||||
var existing = data.StrAttrs.FirstOrDefault(x => x.Gid == SpineStrAttrGid && x.Sid == detail);
|
||||
return existing?.Val ?? "{}";
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ChildUnLockParam
|
||||
{
|
||||
[JsonPropertyName("pId")]
|
||||
public int CardId { get; set; }
|
||||
|
||||
[JsonPropertyName("tbInfo")]
|
||||
public NodeInfo? Info { get; set; }
|
||||
|
||||
[JsonPropertyName("tbMat")]
|
||||
public List<List<int>> Materials { get; set; } = [];
|
||||
}
|
||||
|
||||
internal sealed class NodeInfo
|
||||
{
|
||||
[JsonPropertyName("Indx")]
|
||||
public int Indx { get; set; }
|
||||
|
||||
[JsonPropertyName("InSubIdx")]
|
||||
public int InSubIdx { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class SpineStrData
|
||||
{
|
||||
[JsonPropertyName("ns")]
|
||||
public int Ns { get; set; } = 0;
|
||||
|
||||
[JsonPropertyName("tbn")]
|
||||
public List<int> Tbn { get; set; } = [0, 0];
|
||||
|
||||
[JsonPropertyName("tbr")]
|
||||
public List<int> Tbr { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using MikuSB.Data;
|
||||
using MikuSB.Database;
|
||||
using MikuSB.Proto;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MikuSB.GameServer.Server.CallGS.Handlers.Girl;
|
||||
|
||||
[CallGSApi("GirlSpine_UnlockNodeOneKey")]
|
||||
public class GirlSpine_UnlockNodeOneKey : ICallGSHandler
|
||||
{
|
||||
public async Task Handle(Connection connection, string param, ushort seqNo)
|
||||
{
|
||||
var player = connection.Player!;
|
||||
var req = JsonSerializer.Deserialize<OneKeyUnlockParam>(param);
|
||||
if (req == null || req.CardId == 0 || req.MastIdx <= 0 || req.SubIdxList == null || req.SubIdxList.Count == 0)
|
||||
{
|
||||
await CallGSRouter.SendScript(connection, "GirlSpine_ChildUnLock", "{\"sErr\":\"error.BadParam\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
var card = player.CharacterManager.GetCharacterByGUID((uint)req.CardId);
|
||||
if (card == null)
|
||||
{
|
||||
await CallGSRouter.SendScript(connection, "GirlSpine_ChildUnLock", "{\"sErr\":\"error.BadParam\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Look up costs from config: CardExcel → SpineId → SpineExcel → NodeConditionId → NodeConditionExcel
|
||||
var cardTemplateId = card.TemplateId;
|
||||
var cardDetail = (uint)((cardTemplateId >> 16) & 0xFFFF);
|
||||
var cardParticular = (uint)((cardTemplateId >> 32) & 0xFFFF);
|
||||
|
||||
var cardExcel = GameData.CardData.Values.FirstOrDefault(
|
||||
x => x.Detail == cardDetail && x.Particular == cardParticular);
|
||||
|
||||
var requestedMaterials = new Dictionary<ulong, uint>();
|
||||
|
||||
if (cardExcel != null && GameData.SpineData.TryGetValue(cardExcel.SpineId, out var spineExcel))
|
||||
{
|
||||
var nodeCondId = spineExcel.GetNodeReq(req.MastIdx);
|
||||
if (nodeCondId != 0 && GameData.NodeConditionData.TryGetValue(nodeCondId, out var nodeCond))
|
||||
{
|
||||
int spineListIdx = req.MastIdx - 1;
|
||||
while (card.Spines.Count <= spineListIdx) card.Spines.Add(0);
|
||||
var currentMask = card.Spines[spineListIdx];
|
||||
|
||||
foreach (var subIdx in req.SubIdxList)
|
||||
{
|
||||
if (subIdx <= 0) continue;
|
||||
uint bit = 1u << (subIdx - 1);
|
||||
if ((currentMask & bit) != 0) continue; // already unlocked, skip cost
|
||||
|
||||
foreach (var row in nodeCond.GetNodeCost(subIdx))
|
||||
{
|
||||
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, "GirlSpine_ChildUnLock", "{\"sErr\":\"tip.not_material\"}");
|
||||
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);
|
||||
}
|
||||
|
||||
// Unlock all specified sub-nodes
|
||||
int mastSpineIdx = req.MastIdx - 1;
|
||||
while (card.Spines.Count <= mastSpineIdx) card.Spines.Add(0);
|
||||
foreach (var subIdx in req.SubIdxList)
|
||||
{
|
||||
if (subIdx <= 0) continue;
|
||||
card.Spines[mastSpineIdx] |= 1u << (subIdx - 1);
|
||||
}
|
||||
syncItems.Add(card.ToProto());
|
||||
|
||||
DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData);
|
||||
DatabaseHelper.SaveDatabaseType(player.CharacterManager.CharacterData);
|
||||
|
||||
var sync = new NtfSyncPlayer();
|
||||
sync.Items.AddRange(syncItems);
|
||||
|
||||
// No s2c handler exists for GirlSpine_UnlockNodeOneKey — reuse GirlSpine_ChildUnLock
|
||||
// which calls UI.CloseConnection() and triggers OnNerveNodeUp to refresh the UI.
|
||||
var lastSubIdx = req.SubIdxList.Count > 0 ? req.SubIdxList[^1] : 9;
|
||||
var rsp = $"{{\"tb\":{{\"D\":{cardDetail},\"pId\":{req.CardId},\"MastIdx\":{req.MastIdx},\"SubIdx\":{lastSubIdx}}}}}";
|
||||
await CallGSRouter.SendScript(connection, "GirlSpine_ChildUnLock", rsp, sync);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class OneKeyUnlockParam
|
||||
{
|
||||
[JsonPropertyName("pId")]
|
||||
public int CardId { get; set; }
|
||||
|
||||
[JsonPropertyName("nIdx")]
|
||||
public int MastIdx { get; set; }
|
||||
|
||||
[JsonPropertyName("tbOneKey")]
|
||||
public List<int> SubIdxList { get; set; } = [];
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
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 sync = new NtfSyncPlayer();
|
||||
if (girlId > 0)
|
||||
await HouseAttr.SetAsync(connection, HouseAttr.GirlRoomNumSid(girlId), HouseAttr.BedroomRegisteredNoRoom, 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
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 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 PackArcadePropUse(int type, int id, ushort count) =>
|
||||
(((uint)count & 0xffffu) << 16) | (((uint)id & 0xffu) << 8) | ((uint)type & 0xffu);
|
||||
}
|
||||
117
GameServer/Server/CallGS/Handlers/House/House_Func/HouseThrow.cs
Normal file
117
GameServer/Server/CallGS/Handlers/House/House_Func/HouseThrow.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
42
GameServer/Server/CallGS/Handlers/House/House_Request.cs
Normal file
42
GameServer/Server/CallGS/Handlers/House/House_Request.cs
Normal 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; }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using MikuSB.Database;
|
||||
using MikuSB.Enums.Player;
|
||||
using MikuSB.Proto;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -8,8 +8,6 @@ namespace MikuSB.GameServer.Server.CallGS.Handlers.Misc;
|
||||
[CallGSApi("PlayerSetting_ChangeShowCard")]
|
||||
public class PlayerSetting_ChangeShowCard : ICallGSHandler
|
||||
{
|
||||
private const int ShowItemGirlIndex = 4;
|
||||
|
||||
public async Task Handle(Connection connection, string param, ushort seqNo)
|
||||
{
|
||||
var player = connection.Player!;
|
||||
@@ -23,13 +21,9 @@ public class PlayerSetting_ChangeShowCard : ICallGSHandler
|
||||
await CallGSRouter.SendScript(connection, "PlayerSetting_ChangeShowCard", "{}");
|
||||
return;
|
||||
}
|
||||
|
||||
player.SetShowItem(ShowItemGirlIndex, card.Guid);
|
||||
DatabaseHelper.SaveDatabaseType(player.Data);
|
||||
|
||||
player.SetShowItem((int)ProfileShowItemTypeEnum.SHOWITEM_GIRL, card.Guid);
|
||||
var sync = new NtfSyncPlayer();
|
||||
sync.ShowItems.AddRange(player.Data.ShowItems);
|
||||
|
||||
await CallGSRouter.SendScript(connection, "PlayerSetting_ChangeShowCard", "{}", sync);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
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);
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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);
|
||||
}
|
||||
|
||||
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; } = [];
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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);
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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);
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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);
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using MikuSB.Database.Player;
|
||||
using MikuSB.GameServer.Server.CallGS.Handlers.Misc;
|
||||
using MikuSB.Proto;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MikuSB.GameServer.Server.CallGS.Handlers.Preview;
|
||||
|
||||
[CallGSApi("RecordConfession")]
|
||||
public class RecordConfession : ICallGSHandler
|
||||
{
|
||||
private const int MainSceneGID = 132;
|
||||
public async Task Handle(Connection connection, string param, ushort seqNo)
|
||||
{
|
||||
var req = JsonSerializer.Deserialize<RecordConfessionParam>(param);
|
||||
if (req == null) return;
|
||||
var sid = req.Id + 10;
|
||||
var player = connection.Player!;
|
||||
var attr = player.Data.Attrs
|
||||
.FirstOrDefault(x => x.Gid == MainSceneGID && x.Sid == sid);
|
||||
if (attr == null)
|
||||
{
|
||||
attr = new PlayerAttr
|
||||
{
|
||||
Gid = MainSceneGID,
|
||||
Sid = sid,
|
||||
Val = 1
|
||||
};
|
||||
player.Data.Attrs.Add(attr);
|
||||
}
|
||||
var sync = new NtfSyncPlayer();
|
||||
sync.Custom[player.ToPackedAttrKey(MainSceneGID, sid)] = attr.Val;
|
||||
sync.Custom[player.ToShiftedAttrKey(MainSceneGID, sid)] = attr.Val;
|
||||
await CallGSRouter.SendScript(connection, "RecordConfession", "{}", sync);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RecordConfessionParam
|
||||
{
|
||||
[JsonPropertyName("nIdx")]
|
||||
public uint Id { get; set; }
|
||||
}
|
||||
178
GameServer/Server/CallGS/Handlers/Rogue3D/Rogue3DStateHelper.cs
Normal file
178
GameServer/Server/CallGS/Handlers/Rogue3D/Rogue3DStateHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ public class Rogue3D_CheckOpenAct : ICallGSHandler
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ public class Rogue3D_SelectMode : ICallGSHandler
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
98
GameServer/Server/CallGS/Handlers/Weapon/Weapon_Break.cs
Normal file
98
GameServer/Server/CallGS/Handlers/Weapon/Weapon_Break.cs
Normal 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; }
|
||||
}
|
||||
77
GameServer/Server/CallGS/Handlers/Weapon/Weapon_Evolution.cs
Normal file
77
GameServer/Server/CallGS/Handlers/Weapon/Weapon_Evolution.cs
Normal 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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -4,12 +4,14 @@ using MikuSB.Database.Account;
|
||||
using MikuSB.Database.Player;
|
||||
using MikuSB.GameServer.Game.Player;
|
||||
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.Login;
|
||||
using MikuSB.GameServer.Server.Packet.Send.Misc;
|
||||
using MikuSB.Proto;
|
||||
using MikuSB.TcpSharp;
|
||||
using MikuSB.Util;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace MikuSB.GameServer.Server.Packet.Recv.Login;
|
||||
|
||||
@@ -43,15 +45,88 @@ public class HandlerReqLogin : Handler
|
||||
connection.State = SessionStateEnum.WAITING_FOR_LOGIN;
|
||||
var pd = DatabaseHelper.GetInstance<PlayerGameData>(account.Uid);
|
||||
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}/",
|
||||
$"Debug-{DateTime.Now:yyyy-MM-dd HH-mm-ss}.log");
|
||||
await connection.Player.OnEnterGame();
|
||||
connection.Player.Connection = connection;
|
||||
await connection.SendPacket(new PacketRspLogin(connection.Player!));
|
||||
await SendDebugLoginState(connection);
|
||||
|
||||
await connection.Player.OnHeartBeat();
|
||||
await connection.SendPacket(new PacketNtfUpdateFriend(connection.Player!));
|
||||
ApplySavedGirlSkinTypes(connection.Player!);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using MikuSB.Proto;
|
||||
using Google.Protobuf;
|
||||
using MikuSB.Database;
|
||||
using MikuSB.Proto;
|
||||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using MikuSB.Database.Character;
|
||||
using MikuSB.Database.Inventory;
|
||||
using MikuSB.GameServer.Game.Inventory;
|
||||
using MikuSB.GameServer.Game.Player;
|
||||
using MikuSB.Proto;
|
||||
using MikuSB.TcpSharp;
|
||||
|
||||
@@ -53,6 +53,21 @@ public class PacketNtfCallScript : BasePacket
|
||||
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(InventoryData inventory) : base(CmdIds.NtfScript)
|
||||
{
|
||||
var proto = new NtfCallScript
|
||||
@@ -62,9 +77,40 @@ public class PacketNtfCallScript : BasePacket
|
||||
};
|
||||
|
||||
var extraSync = new NtfSyncPlayer();
|
||||
foreach (var item in inventory.Items.Values) if ((item.TemplateId & 0xFFFF) != 5) 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 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;
|
||||
SetData(proto);
|
||||
}
|
||||
|
||||
public PacketNtfCallScript(PlayerInstance Player) : base(CmdIds.NtfScript)
|
||||
{
|
||||
Player.BuildPlayerAttr();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -11,6 +11,8 @@ if (!argsMap.TryGetValue("--package", out var packagePath)
|
||||
}
|
||||
|
||||
argsMap.TryGetValue("--pid", out var pidValue);
|
||||
argsMap.TryGetValue("--resource-package", out var resourcePackagePath);
|
||||
argsMap.TryGetValue("--resource-target", out var resourceTargetDirectory);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -23,6 +25,9 @@ try
|
||||
ZipFile.ExtractToDirectory(packagePath, stagingDirectory, overwriteFiles: true);
|
||||
CopyDirectory(stagingDirectory, targetDirectory);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(resourcePackagePath) && !string.IsNullOrWhiteSpace(resourceTargetDirectory))
|
||||
UpdateResources(resourcePackagePath, resourceTargetDirectory);
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = restartExecutable,
|
||||
@@ -88,3 +93,15 @@ static bool ShouldSkip(string relativePath)
|
||||
{
|
||||
return relativePath.StartsWith("Config", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
static void UpdateResources(string resourcePackagePath, string resourceTargetDirectory)
|
||||
{
|
||||
var resourceStagingDirectory = Path.Combine(Path.GetTempPath(), "MikuSB", "resource-staging", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(resourceStagingDirectory);
|
||||
|
||||
ZipFile.ExtractToDirectory(resourcePackagePath, resourceStagingDirectory, overwriteFiles: true);
|
||||
|
||||
var extractedRoot = Directory.GetDirectories(resourceStagingDirectory).FirstOrDefault() ?? resourceStagingDirectory;
|
||||
Directory.CreateDirectory(resourceTargetDirectory);
|
||||
CopyDirectory(extractedRoot, resourceTargetDirectory);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<CETCompat>false</CETCompat>
|
||||
<RootNamespace>MikuSB.MikuSB</RootNamespace>
|
||||
<AssemblyName>MikuSB</AssemblyName>
|
||||
<ApplicationIcon>Source\Kiana.ico</ApplicationIcon>
|
||||
<ApplicationIcon>Source\Snowbreak.ico</ApplicationIcon>
|
||||
<SatelliteResourceLanguages>false</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
Projects="..\MikuSB.Updater\MikuSB.Updater.csproj"
|
||||
Targets="Restore;Publish"
|
||||
RemoveProperties="PublishProfile"
|
||||
Properties="Configuration=$(Configuration);RuntimeIdentifier=$(RuntimeIdentifier);SelfContained=true;PublishSingleFile=true;PublishDir=$(_UpdaterPublishDir)" />
|
||||
Properties="Configuration=$(Configuration);RuntimeIdentifier=$(RuntimeIdentifier);SelfContained=false;PublishSingleFile=true;PublishDir=$(_UpdaterPublishDir)" />
|
||||
|
||||
<Copy
|
||||
SourceFiles="$(_UpdaterPublishDir)MikuSB.Updater.exe"
|
||||
|
||||
@@ -7,6 +7,7 @@ using MikuSB.GameServer.Server.CallGS;
|
||||
using MikuSB.GameServer.Server.Packet;
|
||||
using MikuSB.Internationalization;
|
||||
using MikuSB.MikuSB.Tool;
|
||||
using MikuSB.MikuSB.Update;
|
||||
using MikuSB.Proto;
|
||||
using MikuSB.SdkServer;
|
||||
using MikuSB.TcpSharp;
|
||||
@@ -146,6 +147,7 @@ public class LoaderManager : MikuSB
|
||||
// Load the game data
|
||||
try
|
||||
{
|
||||
await UpdateService.EnsureResourcesPresentAsync();
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadingItem", I18NManager.Translate("Word.GameData")));
|
||||
ResourceManager.LoadGameData();
|
||||
}
|
||||
@@ -158,7 +160,7 @@ public class LoaderManager : MikuSB
|
||||
}
|
||||
}
|
||||
|
||||
public static void InitCommand()
|
||||
public static async Task InitCommand(CancellationToken exitToken)
|
||||
{
|
||||
// Register the command handlers
|
||||
try
|
||||
@@ -176,6 +178,6 @@ public class LoaderManager : MikuSB
|
||||
IConsole.OnConsoleExcuteCommand += CommandExecutor.ConsoleExcuteCommand;
|
||||
CommandExecutor.OnRunCommand += (sender, e) => { CommandManager.HandleCommand(e, sender); };
|
||||
|
||||
IConsole.ListenConsole();
|
||||
await IConsole.ListenConsole(exitToken);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,10 @@ public class MikuSB
|
||||
public static readonly Listener Listener = new();
|
||||
public static readonly CommandManager CommandManager = new();
|
||||
|
||||
// for exit signal
|
||||
private static readonly CancellationTokenSource _cts = new();
|
||||
private static int _exitCode = 0;
|
||||
|
||||
public static async Task Main()
|
||||
{
|
||||
var time = DateTime.Now;
|
||||
@@ -50,44 +54,58 @@ public class MikuSB
|
||||
ResourceManager.IsLoaded = true;
|
||||
|
||||
HandbookGenerator.GenerateAll();
|
||||
LoaderManager.InitCommand();
|
||||
var consoleTask = Task.Run(() => LoaderManager.InitCommand(_cts.Token), _cts.Token);
|
||||
|
||||
var elapsed = DateTime.Now - time;
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.ServerStarted",
|
||||
Math.Round(elapsed.TotalSeconds, 2).ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
await consoleTask;
|
||||
|
||||
await ProcessExit(Volatile.Read(ref _exitCode));
|
||||
}
|
||||
|
||||
# region Exit
|
||||
#region Exit
|
||||
|
||||
private static void RegisterExitEvent()
|
||||
{
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) =>
|
||||
{
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.Shutdown"));
|
||||
ProcessExit();
|
||||
RequestShutdown(0);
|
||||
};
|
||||
AppDomain.CurrentDomain.UnhandledException += (obj, arg) =>
|
||||
{
|
||||
Logger.Error(I18NManager.Translate("Server.ServerInfo.UnhandledException", obj.GetType().Name),
|
||||
(Exception)arg.ExceptionObject);
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.Shutdown"));
|
||||
ProcessExit();
|
||||
Environment.Exit(1);
|
||||
RequestShutdown(1);
|
||||
};
|
||||
|
||||
Console.CancelKeyPress += (_, eventArgs) =>
|
||||
{
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.CancelKeyPressed"));
|
||||
eventArgs.Cancel = true;
|
||||
Environment.Exit(0);
|
||||
RequestShutdown(0);
|
||||
};
|
||||
}
|
||||
|
||||
private static void ProcessExit()
|
||||
private static void RequestShutdown(int exitCode)
|
||||
{
|
||||
Interlocked.Exchange(ref _exitCode, exitCode);
|
||||
_cts.Cancel();
|
||||
}
|
||||
|
||||
private static async Task ProcessExit(int exitCode)
|
||||
{
|
||||
SocketListener.Connections.Values.ToList().ForEach(x => x.Stop(true));
|
||||
DatabaseHelper.SaveThread?.Interrupt();
|
||||
DatabaseHelper.SaveDatabase();
|
||||
|
||||
DatabaseHelper.Stop(); // notify stop
|
||||
await DatabaseHelper.WaitAsync(); // wait AutoSave thread exit
|
||||
|
||||
DatabaseHelper.SaveDatabase(); // final flush
|
||||
|
||||
Environment.Exit(exitCode);
|
||||
}
|
||||
|
||||
# endregion
|
||||
|
||||
@@ -9,7 +9,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<PublishDir>bin\MikuSB-Win64-Debug</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>false</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
|
||||
@@ -9,9 +9,9 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<PublishDir>bin\MikuSB-MultiFile\</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<SelfContained>false</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
|
||||
@@ -9,7 +9,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<PublishDir>bin\MikuSB-OneFile\</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 422 KiB |
BIN
MikuSB/Source/Snowbreak.ico
Normal file
BIN
MikuSB/Source/Snowbreak.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
@@ -1,6 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using MikuSB.Util;
|
||||
@@ -12,10 +13,20 @@ public static class UpdateService
|
||||
private static readonly Logger Logger = new("Updater");
|
||||
private static readonly bool UpdateEnabled = true;
|
||||
private static readonly bool AskBeforeUpdate = true;
|
||||
private static readonly string RepositoryOwner = "DevilProMT";
|
||||
private static readonly string RepositoryOwner = "MikuLeaks";
|
||||
private static readonly string RepositoryName = "MikuSB";
|
||||
private static readonly string AssetName = "MikuSB-win-x64.zip";
|
||||
private static readonly int TimeoutSeconds = 5;
|
||||
private static readonly int ReleaseCheckTimeoutSeconds = 10;
|
||||
private static readonly int PackageDownloadTimeoutSeconds = 300;
|
||||
private static readonly int ResourceDownloadTimeoutSeconds = 300;
|
||||
private static readonly int ChecksumDownloadTimeoutSeconds = 30;
|
||||
private static readonly string ResourceArchiveUrl =
|
||||
"https://github.com/Kei-Luna/MikuSB-Resource/archive/refs/heads/main.zip";
|
||||
private static readonly string[] RequiredResourceFiles =
|
||||
[
|
||||
"item/templates/card.json",
|
||||
"item/templates/weapon.json"
|
||||
];
|
||||
|
||||
public static async Task<bool> TryStartSelfUpdateAsync()
|
||||
{
|
||||
@@ -42,8 +53,9 @@ public static class UpdateService
|
||||
Logger.Info($"Current build version: {BuildVersion.Current}");
|
||||
|
||||
using var client = CreateHttpClient();
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(Math.Max(1, TimeoutSeconds)));
|
||||
var release = await GetLatestReleaseAsync(client, cts.Token);
|
||||
using var releaseCts =
|
||||
new CancellationTokenSource(TimeSpan.FromSeconds(Math.Max(1, ReleaseCheckTimeoutSeconds)));
|
||||
var release = await GetLatestReleaseAsync(client, releaseCts.Token);
|
||||
if (release == null)
|
||||
return false;
|
||||
|
||||
@@ -70,14 +82,18 @@ public static class UpdateService
|
||||
|
||||
var packagePath = Path.Combine(tempRoot, asset.Name);
|
||||
Logger.Info($"Downloading update {release.TagName}.");
|
||||
await DownloadFileAsync(client, asset.DownloadUrl, packagePath, cts.Token);
|
||||
await DownloadFileAsync(client, asset.DownloadUrl, packagePath, PackageDownloadTimeoutSeconds);
|
||||
|
||||
var resourcePackagePath = Path.Combine(tempRoot, "MikuSB-Resource-main.zip");
|
||||
Logger.Info("Downloading resource package.");
|
||||
await DownloadFileAsync(client, ResourceArchiveUrl, resourcePackagePath, ResourceDownloadTimeoutSeconds);
|
||||
|
||||
var checksumAsset = release.Assets.FirstOrDefault(x =>
|
||||
string.Equals(x.Name, AssetName + ".sha256", StringComparison.OrdinalIgnoreCase));
|
||||
if (checksumAsset != null)
|
||||
{
|
||||
var checksumPath = Path.Combine(tempRoot, checksumAsset.Name);
|
||||
await DownloadFileAsync(client, checksumAsset.DownloadUrl, checksumPath, cts.Token);
|
||||
await DownloadFileAsync(client, checksumAsset.DownloadUrl, checksumPath, ChecksumDownloadTimeoutSeconds);
|
||||
VerifySha256(packagePath, checksumPath);
|
||||
}
|
||||
|
||||
@@ -90,7 +106,9 @@ public static class UpdateService
|
||||
ArgumentList =
|
||||
{
|
||||
"--package", packagePath,
|
||||
"--resource-package", resourcePackagePath,
|
||||
"--target", AppContext.BaseDirectory,
|
||||
"--resource-target", Path.Combine(AppContext.BaseDirectory, ConfigManager.Config.Path.ResourcePath),
|
||||
"--restart", Path.Combine(AppContext.BaseDirectory, "MikuSB.exe"),
|
||||
"--pid", Environment.ProcessId.ToString()
|
||||
}
|
||||
@@ -112,6 +130,15 @@ public static class UpdateService
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task EnsureResourcesPresentAsync()
|
||||
{
|
||||
if (!AreRequiredResourcesPresent())
|
||||
{
|
||||
Logger.Warn("Required resources are missing. Downloading resource package.");
|
||||
await DownloadAndInstallResourcesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private static string StageUpdaterExecutable()
|
||||
{
|
||||
var sourceDirectory = AppContext.BaseDirectory;
|
||||
@@ -131,6 +158,39 @@ public static class UpdateService
|
||||
return stagedUpdaterPath;
|
||||
}
|
||||
|
||||
private static bool AreRequiredResourcesPresent()
|
||||
{
|
||||
var resourcePath = Path.Combine(AppContext.BaseDirectory, ConfigManager.Config.Path.ResourcePath);
|
||||
if (!Directory.Exists(resourcePath))
|
||||
return false;
|
||||
|
||||
return RequiredResourceFiles.All(fileName => File.Exists(Path.Combine(resourcePath, fileName)));
|
||||
}
|
||||
|
||||
private static async Task DownloadAndInstallResourcesAsync()
|
||||
{
|
||||
using var client = CreateHttpClient();
|
||||
var tempRoot = Path.Combine(Path.GetTempPath(), "MikuSB", "resources", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(tempRoot);
|
||||
|
||||
var resourcePackagePath = Path.Combine(tempRoot, "MikuSB-Resource-main.zip");
|
||||
await DownloadFileAsync(client, ResourceArchiveUrl, resourcePackagePath, ResourceDownloadTimeoutSeconds);
|
||||
InstallResourcesFromArchive(resourcePackagePath,
|
||||
Path.Combine(AppContext.BaseDirectory, ConfigManager.Config.Path.ResourcePath));
|
||||
}
|
||||
|
||||
private static void InstallResourcesFromArchive(string resourcePackagePath, string resourceTargetDirectory)
|
||||
{
|
||||
var resourceStagingDirectory = Path.Combine(Path.GetTempPath(), "MikuSB", "resource-staging", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(resourceStagingDirectory);
|
||||
|
||||
ZipFile.ExtractToDirectory(resourcePackagePath, resourceStagingDirectory, overwriteFiles: true);
|
||||
|
||||
var extractedRoot = Directory.GetDirectories(resourceStagingDirectory).FirstOrDefault() ?? resourceStagingDirectory;
|
||||
Directory.CreateDirectory(resourceTargetDirectory);
|
||||
CopyDirectory(extractedRoot, resourceTargetDirectory);
|
||||
}
|
||||
|
||||
private static bool ConfirmUpdate(string latestVersion)
|
||||
{
|
||||
Console.Write($"New version found: {BuildVersion.Current} -> {latestVersion}. Update now? [Y/n]: ");
|
||||
@@ -152,7 +212,7 @@ public static class UpdateService
|
||||
{
|
||||
var client = new HttpClient
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(Math.Max(1, TimeoutSeconds))
|
||||
Timeout = Timeout.InfiniteTimeSpan
|
||||
};
|
||||
|
||||
client.DefaultRequestHeaders.UserAgent.Add(
|
||||
@@ -189,14 +249,32 @@ public static class UpdateService
|
||||
HttpClient client,
|
||||
string downloadUrl,
|
||||
string destinationPath,
|
||||
CancellationToken cancellationToken)
|
||||
int timeoutSeconds)
|
||||
{
|
||||
using var response = await client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(Math.Max(1, timeoutSeconds)));
|
||||
using var response = await client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var source = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
await using var source = await response.Content.ReadAsStreamAsync(cts.Token);
|
||||
await using var destination = File.Create(destinationPath);
|
||||
await source.CopyToAsync(destination, cancellationToken);
|
||||
await source.CopyToAsync(destination, cts.Token);
|
||||
}
|
||||
|
||||
private static void CopyDirectory(string sourceDirectory, string targetDirectory)
|
||||
{
|
||||
foreach (var directory in Directory.GetDirectories(sourceDirectory, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(sourceDirectory, directory);
|
||||
Directory.CreateDirectory(Path.Combine(targetDirectory, relativePath));
|
||||
}
|
||||
|
||||
foreach (var file in Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(sourceDirectory, file);
|
||||
var destinationPath = Path.Combine(targetDirectory, relativePath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
|
||||
File.Copy(file, destinationPath, overwrite: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void VerifySha256(string packagePath, string checksumPath)
|
||||
|
||||
11020
Proto/Core.cs
Normal file
11020
Proto/Core.cs
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user