Merge branch 'pr-3-salmon'

This commit is contained in:
raphaeIl 2024-05-30 00:04:13 +08:00
commit a4a8e0b6f8
15 changed files with 5407 additions and 225 deletions

13
.config/dotnet-tools.json Normal file
View File

@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "0.28.2",
"commands": [
"dotnet-csharpier"
],
"rollForward": false
}
}
}

9
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,9 @@
{
"[csharp]": {
"editor.tabSize": 4,
"editor.insertSpaces": true,
"editor.defaultFormatter": "csharpier.csharpier-vscode",
"editor.formatOnSave": true,
"editor.formatOnPaste": true
}
}

127
README.md
View File

@ -1,76 +1,69 @@
# SCHALE.GameServer # SCHALE.GameServer
## Running > [!TIP]
By default the server is configured to run with SQL Server Express in `appsettings.json`. If you wanna use other edition of SQL Server Express change the `ConnectionStrings` in there. > For original README please refer to <https://github.com/rafi1212122/SCHALE.GameServer>
Alternatively this software can run in docker too (`docker compose up --build`). ## Prerequisites
## Connecting - Some computer knowledge
- Run the game with this [frida script](https://gist.githubusercontent.com/raphaeIl/c4ca030411186c9417da22d8d7864c4d/raw/00b69c5bacdf79c24972411bd80d785eed3841ce/ba.js) - [.NET SDK 8.0](https://dotnet.microsoft.com/zh-cn/download/dotnet/8.0)
- [SQL Express](https://www.microsoft.com/zh-tw/sql-server/sql-server-downloads)
- [SQL Server Management Studio (SSMS)](https://learn.microsoft.com/zh-tw/sql/ssms/download-sql-server-management-studio-ssms?view=sql-server-ver16)
- [LD Player 9](https://www.ldplayer.tw/)
- [Python](https://www.python.org/)
- [Frida](https://frida.re/)
- [frida-server-16.2.5-android-x86_64](https://github.com/frida/frida/releases)
## Discuss ## Steps
[Discord Server](https://discord.gg/fbsRYc7bBA)
## How to use (E.g Windows and LDPlayer 9) 1. Start SQL server
1. Download this repo. 2. Start private game server
```sh 3. Start LD Player
git clone https://github.com/rafi1212122/SCHALE.GameServer.git 4. Start Frida server
5. Start ブルアカ
6. Inject Frida script
7. Enjoy :smile:
### SQL server
Use SSMS to connect with default settings except that you have to check "Trust server certificate".
### Game server
```bash
# in this repo
cd SCHALE.GameServer
dotnet run
``` ```
2. Download .NET SDK, SQL Server Express, and install them.
- .NET SDK<br> ### Frida server
<https://dotnet.microsoft.com/zh-cn/download/dotnet?cid=getdotnetcorecli>
- SQL Server Express<br> 1. Extract `frida-server-16.2.5-android-x86_64.xz`
<https://go.microsoft.com/fwlink/p/?linkid=2216019&clcid=0x804&culture=zh-cn&country=cn> to `LDPlayer/frida-server-16.2.5-android-x86_64`.
3. Download Visual Studio and Install C# and Database Extensions.<br> 2. Turn on LD Player
[https://visualstudio.microsoft.com/zh-hans/vs/](https://visualstudio.microsoft.com/vs/) 3. Turn on root and adb in the settings of LD Player.
4. Use Visual Studio to open `SCHALE.GameServer\SCHALE.GameServer.sln` and make it. 4.
5. Download `Excel.zip` and unzip and add the excels to the following path: `SCHALE.GameServer\bin\Debug\net8.0\Resources\excel`.
6. Modify the IP address in `SCHALE.GameServer-master\SCHALE.GameServer\bin\Debug\net8.0\Config.json`. ```bash
7. Open `SCHALE.GameServer-master\SCHALE.GameServer\bin\Debug\net8.0\SCHALE.GameServer.exe`. # in LDPlayer
8. Download python and install frida<br>You may need to add python site packages to PATH if "frida" command is missing.<br> cd LDPlayer9
<https://www.python.org/> ./adb.exe push ../frida-server-16.2.5-android-x86_64 /data/local/tmp/frida-server
```sh ./adb.exe shell
pip install frida-tools su
pip install frida cd /data/local/tmp
chmod 755 frida-server
./frida-server
``` ```
9. Download frida-server(if you use emulator, download x86), find "frida-server-[Latest Version]-android-x86_64.xz" and download & unzip it, before push you may rename the bin file to "frida-server".<br>
<https://github.com/frida/frida/releases> ### Inject Frida script
10. Download adb.<br>
<https://developer.android.google.cn/tools/releases/platform-tools?hl=zh-cn#downloads> > [!NOTE]
11. Use adb connect emulator and start frida-server(enable root first) > Edit line 5 of [ba.js](./ba.js) to your own server IP.
```sh
adb root > [!WARNING]
adb push frida-server /data/local/tmp/ > Do this fast when you open ブルアカ and see the Yostar logo.
adb shell "chmod 755 /data/local/tmp/frida-server"
adb shell "/data/local/tmp/frida-server &" ```bash
# in this repo
frida -U "ブルアカ" -l ba.js --realm=emulated
``` ```
12. Start BluearchiveJP and use frida script
1. download this [frida script](https://gist.githubusercontent.com/raphaeIl/c4ca030411186c9417da22d8d7864c4d/raw/00b69c5bacdf79c24972411bd80d785eed3841ce/ba.js) and modify `SERVER_ADDRESS`
2. Start BluearchiveJP first and then start frida
3. `frida -U "ブルアカ" -l ba.js --realm=emulated`
13. Skip Tutorial<br>
If you have never finished any tutorials try this<br>
`INSERT INTO [dbo].[AccountTutorials] ([AccountServerId], [TutorialIds]) VALUES (1, N'[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]');`<br>
or you have finished the front part of the tutorial use this<br>
`UPDATE [dbo].[AccountTutorials] SET [TutorialIds] = N'[1, 2, 3, 4, 5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]' WHERE [AccountServerId] = [Your Server ID];`
## Command
Go to club and input `/help` to view command usage
## Troubleshooting
##### YostarRequestFail:INIT_FAILED(Initialization failed)
- change `SERVER_ADDRESS` in `ba.js` to your IPV4 and not `0.0.0.0`
##### アカウント情報の連携中にエラーガ発生乚ま乚た
- restart server or without `Resources\excel`
##### can enter game but black screen
- close server and delete database, then open server.<br>
if it isn't work, you may need to pass the official tutorial first.
##### failed to load club
- change `Address` in `config.json` to your IPV4
##### command not work
- the excel you are using may have problem, change your excel.

1
Resources/data/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*

1
Resources/download/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*

1
Resources/url.txt Normal file
View File

@ -0,0 +1 @@
https://prod-clientpatch.bluearchiveyostar.com/r67_utfwo6vcvx7rhl017phc

View File

@ -1,8 +1,8 @@
using SCHALE.Common.Database; using SCHALE.Common.Database;
using SCHALE.Common.Database.ModelExtensions;
using SCHALE.Common.FlatData;
using SCHALE.Common.NetworkProtocol; using SCHALE.Common.NetworkProtocol;
using SCHALE.GameServer.Services; using SCHALE.GameServer.Services;
using SCHALE.Common.FlatData;
using SCHALE.Common.Database.ModelExtensions;
namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
{ {
@ -12,7 +12,13 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
private readonly SCHALEContext context; private readonly SCHALEContext context;
private readonly ExcelTableService excelTableService; private readonly ExcelTableService excelTableService;
public Account(IProtocolHandlerFactory protocolHandlerFactory, ISessionKeyService _sessionKeyService, SCHALEContext _context, ExcelTableService _excelTableService) : base(protocolHandlerFactory) public Account(
IProtocolHandlerFactory protocolHandlerFactory,
ISessionKeyService _sessionKeyService,
SCHALEContext _context,
ExcelTableService _excelTableService
)
: base(protocolHandlerFactory)
{ {
sessionKeyService = _sessionKeyService; sessionKeyService = _sessionKeyService;
context = _context; context = _context;
@ -24,7 +30,9 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
{ {
string[] uidToken = req.EnterTicket.Split(':'); string[] uidToken = req.EnterTicket.Split(':');
var account = context.GuestAccounts.SingleOrDefault(x => x.Uid == long.Parse(uidToken[0]) && x.Token == uidToken[1]); var account = context.GuestAccounts.SingleOrDefault(x =>
x.Uid == long.Parse(uidToken[0]) && x.Token == uidToken[1]
);
if (account is null) if (account is null)
{ {
@ -38,7 +46,9 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
return new AccountCheckYostarResponse() return new AccountCheckYostarResponse()
{ {
ResultState = 1, ResultState = 1,
SessionKey = sessionKeyService.Create(account.Uid) ?? new SessionKey() { MxToken = req.EnterTicket } SessionKey =
sessionKeyService.Create(account.Uid)
?? new SessionKey() { MxToken = req.EnterTicket }
}; };
} }
@ -47,10 +57,7 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
{ {
if (req.SessionKey is null || req.SessionKey.AccountServerId == 0) if (req.SessionKey is null || req.SessionKey.AccountServerId == 0)
{ {
return new ErrorPacket() return new ErrorPacket() { ErrorCode = WebAPIErrorCode.AccountAuthNotCreated };
{
ErrorCode = WebAPIErrorCode.AccountAuthNotCreated
};
} }
else else
{ {
@ -69,16 +76,28 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
{ OpenConditionContent.Cafe, OpenConditionLockReason.StageClear }, { OpenConditionContent.Cafe, OpenConditionLockReason.StageClear },
{ OpenConditionContent.Unit_Growth_Skill, OpenConditionLockReason.None }, { OpenConditionContent.Unit_Growth_Skill, OpenConditionLockReason.None },
{ OpenConditionContent.Unit_Growth_LevelUp, OpenConditionLockReason.None }, { OpenConditionContent.Unit_Growth_LevelUp, OpenConditionLockReason.None },
{ OpenConditionContent.Unit_Growth_Transcendence, OpenConditionLockReason.None }, {
OpenConditionContent.Unit_Growth_Transcendence,
OpenConditionLockReason.None
},
{ OpenConditionContent.WeekDungeon, OpenConditionLockReason.StageClear }, { OpenConditionContent.WeekDungeon, OpenConditionLockReason.StageClear },
{ OpenConditionContent.Arena, OpenConditionLockReason.StageClear }, { OpenConditionContent.Arena, OpenConditionLockReason.StageClear },
{ OpenConditionContent.Academy, OpenConditionLockReason.StageClear }, { OpenConditionContent.Academy, OpenConditionLockReason.StageClear },
{ OpenConditionContent.Equip, OpenConditionLockReason.None }, { OpenConditionContent.Equip, OpenConditionLockReason.None },
{ OpenConditionContent.Item, OpenConditionLockReason.None }, { OpenConditionContent.Item, OpenConditionLockReason.None },
{ OpenConditionContent.Mission, OpenConditionLockReason.None }, { OpenConditionContent.Mission, OpenConditionLockReason.None },
{ OpenConditionContent.WeekDungeon_Chase, OpenConditionLockReason.StageClear }, {
{ OpenConditionContent.__Deprecated_WeekDungeon_FindGift, OpenConditionLockReason.None }, OpenConditionContent.WeekDungeon_Chase,
{ OpenConditionContent.__Deprecated_WeekDungeon_Blood, OpenConditionLockReason.None }, OpenConditionLockReason.StageClear
},
{
OpenConditionContent.__Deprecated_WeekDungeon_FindGift,
OpenConditionLockReason.None
},
{
OpenConditionContent.__Deprecated_WeekDungeon_Blood,
OpenConditionLockReason.None
},
{ OpenConditionContent.Story_Sub, OpenConditionLockReason.None }, { OpenConditionContent.Story_Sub, OpenConditionLockReason.None },
{ OpenConditionContent.Story_Replay, OpenConditionLockReason.None }, { OpenConditionContent.Story_Replay, OpenConditionLockReason.None },
{ OpenConditionContent.None, OpenConditionLockReason.None }, { OpenConditionContent.None, OpenConditionLockReason.None },
@ -98,24 +117,46 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
{ OpenConditionContent.Story, OpenConditionLockReason.None }, { OpenConditionContent.Story, OpenConditionLockReason.None },
{ OpenConditionContent.Tactic_Speed, OpenConditionLockReason.StageClear }, { OpenConditionContent.Tactic_Speed, OpenConditionLockReason.StageClear },
{ OpenConditionContent.Cafe_Invite, OpenConditionLockReason.StageClear }, { OpenConditionContent.Cafe_Invite, OpenConditionLockReason.StageClear },
{ OpenConditionContent.Cafe_Invite_2, OpenConditionLockReason.CafeRank | OpenConditionLockReason.StageClear }, {
{ OpenConditionContent.EventMiniGame_1, OpenConditionLockReason.StageClear }, OpenConditionContent.Cafe_Invite_2,
OpenConditionLockReason.CafeRank | OpenConditionLockReason.StageClear
},
{
OpenConditionContent.EventMiniGame_1,
OpenConditionLockReason.StageClear
},
{ OpenConditionContent.SchoolDungeon, OpenConditionLockReason.StageClear }, { OpenConditionContent.SchoolDungeon, OpenConditionLockReason.StageClear },
{ OpenConditionContent.TimeAttackDungeon, OpenConditionLockReason.StageClear }, {
OpenConditionContent.TimeAttackDungeon,
OpenConditionLockReason.StageClear
},
{ OpenConditionContent.ShiftingCraft, OpenConditionLockReason.StageClear }, { OpenConditionContent.ShiftingCraft, OpenConditionLockReason.StageClear },
{ OpenConditionContent.Tactic_Skip, OpenConditionLockReason.StageClear }, { OpenConditionContent.Tactic_Skip, OpenConditionLockReason.StageClear },
{ OpenConditionContent.Mulligan, OpenConditionLockReason.StageClear }, { OpenConditionContent.Mulligan, OpenConditionLockReason.StageClear },
{ OpenConditionContent.EventPermanent, OpenConditionLockReason.StageClear }, { OpenConditionContent.EventPermanent, OpenConditionLockReason.StageClear },
{ OpenConditionContent.Main_L_1_2, OpenConditionLockReason.ScenarioModeClear }, {
{ OpenConditionContent.Main_L_1_3, OpenConditionLockReason.ScenarioModeClear }, OpenConditionContent.Main_L_1_2,
{ OpenConditionContent.Main_L_1_4, OpenConditionLockReason.ScenarioModeClear }, OpenConditionLockReason.ScenarioModeClear
},
{
OpenConditionContent.Main_L_1_3,
OpenConditionLockReason.ScenarioModeClear
},
{
OpenConditionContent.Main_L_1_4,
OpenConditionLockReason.ScenarioModeClear
},
{ OpenConditionContent.EliminateRaid, OpenConditionLockReason.StageClear }, { OpenConditionContent.EliminateRaid, OpenConditionLockReason.StageClear },
{ OpenConditionContent.Cafe_2, OpenConditionLockReason.StageClear }, { OpenConditionContent.Cafe_2, OpenConditionLockReason.StageClear },
{ OpenConditionContent.MultiFloorRaid, OpenConditionLockReason.StageClear } { OpenConditionContent.MultiFloorRaid, OpenConditionLockReason.StageClear }
}, },
MissionProgressDBs = [.. context.MissionProgresses.Where(x => x.AccountServerId == account.ServerId)] MissionProgressDBs =
[
.. context.MissionProgresses.Where(x =>
x.AccountServerId == account.ServerId
)
]
}; };
} }
} }
@ -124,10 +165,7 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
{ {
if (req.SessionKey is null) if (req.SessionKey is null)
{ {
return new ErrorPacket() return new ErrorPacket() { ErrorCode = WebAPIErrorCode.InvalidSession };
{
ErrorCode = WebAPIErrorCode.InvalidSession
};
} }
string[] uidToken = req.SessionKey.MxToken.Split(':'); string[] uidToken = req.SessionKey.MxToken.Split(':');
@ -137,18 +175,27 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
context.SaveChanges(); context.SaveChanges();
// Default items // Default items
context.Items.Add(new() context.Items.Add(
new()
{ {
AccountServerId = account.ServerId, AccountServerId = account.ServerId,
UniqueId = 2, UniqueId = 2,
StackCount = 5 StackCount = 5
}); }
);
// Default chars // Default chars
var defaultCharacters = excelTableService.GetTable<DefaultCharacterExcelTable>().UnPack().DataList; var defaultCharacters = excelTableService
var newCharacters = defaultCharacters.Select(x => .GetTable<DefaultCharacterExcelTable>()
.UnPack()
.DataList;
var newCharacters = defaultCharacters
.Select(x =>
{ {
var characterExcel = excelTableService.GetTable<CharacterExcelTable>().UnPack().DataList.Find(y => y.Id == x.CharacterId); var characterExcel = excelTableService
.GetTable<CharacterExcelTable>()
.UnPack()
.DataList.Find(y => y.Id == x.CharacterId);
return new CharacterDB() return new CharacterDB()
{ {
@ -165,10 +212,18 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
LeaderSkillLevel = x.LeaderSkillLevel, LeaderSkillLevel = x.LeaderSkillLevel,
IsNew = true, IsNew = true,
IsLocked = true, IsLocked = true,
EquipmentServerIds = characterExcel is not null ? characterExcel.EquipmentSlot.Select(x => (long)0).ToList() : [0, 0, 0], EquipmentServerIds = characterExcel is not null
PotentialStats = { { 1, 0 }, { 2, 0 }, { 3, 0 } } ? characterExcel.EquipmentSlot.Select(x => (long)0).ToList()
: [0, 0, 0],
PotentialStats =
{
{ 1, 0 },
{ 2, 0 },
{ 3, 0 }
}
}; };
}).ToList(); })
.ToList();
account.AddCharacters(context, [.. newCharacters]); account.AddCharacters(context, [.. newCharacters]);
context.SaveChanges(); context.SaveChanges();
@ -176,19 +231,26 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
var favCharacter = defaultCharacters.Find(x => x.FavoriteCharacter); var favCharacter = defaultCharacters.Find(x => x.FavoriteCharacter);
if (favCharacter is not null) if (favCharacter is not null)
{ {
account.RepresentCharacterServerId = (int)newCharacters.First(x => x.UniqueId == favCharacter.CharacterId).ServerId; account.RepresentCharacterServerId = (int)
newCharacters.First(x => x.UniqueId == favCharacter.CharacterId).ServerId;
} }
if (newCharacters.Count > 0) if (newCharacters.Count > 0)
{ {
context.Echelons.Add(new() context.Echelons.Add(
new()
{ {
AccountServerId = account.ServerId, AccountServerId = account.ServerId,
EchelonNumber = 1, EchelonNumber = 1,
EchelonType = EchelonType.Adventure, EchelonType = EchelonType.Adventure,
LeaderServerId = newCharacters[0].ServerId, LeaderServerId = newCharacters[0].ServerId,
MainSlotServerIds = newCharacters.Take(3).Select(x => x.ServerId).Append(0).ToList(), MainSlotServerIds = newCharacters
.Take(3)
.Select(x => x.ServerId)
.Append(0)
.ToList(),
SupportSlotServerIds = [0, 0] SupportSlotServerIds = [0, 0]
}); }
);
} }
context.SaveChanges(); context.SaveChanges();
@ -206,17 +268,14 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
account.Nickname = req.Nickname; account.Nickname = req.Nickname;
context.SaveChanges(); context.SaveChanges();
return new AccountNicknameResponse() return new AccountNicknameResponse() { AccountDB = account };
{
AccountDB = account
};
} }
[ProtocolHandler(Protocol.Account_LoginSync)] [ProtocolHandler(Protocol.Account_LoginSync)]
public ResponsePacket LoginSyncHandler(AccountLoginSyncRequest req) public ResponsePacket LoginSyncHandler(AccountLoginSyncRequest req)
{ {
var account = sessionKeyService.GetAccount(req.SessionKey); var account = sessionKeyService.GetAccount(req.SessionKey);
ArgumentNullException.ThrowIfNull(account);
return new AccountLoginSyncResponse() return new AccountLoginSyncResponse()
{ {
@ -224,7 +283,7 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
{ {
AccountCurrencyDB = new AccountCurrencyDB AccountCurrencyDB = new AccountCurrencyDB
{ {
AccountLevel = 1, AccountLevel = 90,
AcademyLocationRankSum = 1, AcademyLocationRankSum = 1,
CurrencyDict = new Dictionary<CurrencyTypes, long> CurrencyDict = new Dictionary<CurrencyTypes, long>
{ {
@ -260,23 +319,71 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
{ CurrencyTypes.AcademyTicket, DateTime.Parse("2024-04-26T19:29:12") }, { CurrencyTypes.AcademyTicket, DateTime.Parse("2024-04-26T19:29:12") },
{ CurrencyTypes.ArenaTicket, DateTime.Parse("2024-04-26T19:29:12") }, { CurrencyTypes.ArenaTicket, DateTime.Parse("2024-04-26T19:29:12") },
{ CurrencyTypes.RaidTicket, DateTime.Parse("2024-04-26T19:29:12") }, { CurrencyTypes.RaidTicket, DateTime.Parse("2024-04-26T19:29:12") },
{ CurrencyTypes.WeekDungeonChaserATicket, DateTime.Parse("2024-04-26T19:29:12") }, {
{ CurrencyTypes.WeekDungeonChaserBTicket, DateTime.Parse("2024-04-26T19:29:12") }, CurrencyTypes.WeekDungeonChaserATicket,
{ CurrencyTypes.WeekDungeonChaserCTicket, DateTime.Parse("2024-04-26T19:29:12") }, DateTime.Parse("2024-04-26T19:29:12")
{ CurrencyTypes.SchoolDungeonATicket, DateTime.Parse("2024-04-26T19:29:12") }, },
{ CurrencyTypes.SchoolDungeonBTicket, DateTime.Parse("2024-04-26T19:29:12") }, {
{ CurrencyTypes.SchoolDungeonCTicket, DateTime.Parse("2024-04-26T19:29:12") }, CurrencyTypes.WeekDungeonChaserBTicket,
{ CurrencyTypes.TimeAttackDungeonTicket, DateTime.Parse("2024-04-26T19:29:12") }, DateTime.Parse("2024-04-26T19:29:12")
},
{
CurrencyTypes.WeekDungeonChaserCTicket,
DateTime.Parse("2024-04-26T19:29:12")
},
{
CurrencyTypes.SchoolDungeonATicket,
DateTime.Parse("2024-04-26T19:29:12")
},
{
CurrencyTypes.SchoolDungeonBTicket,
DateTime.Parse("2024-04-26T19:29:12")
},
{
CurrencyTypes.SchoolDungeonCTicket,
DateTime.Parse("2024-04-26T19:29:12")
},
{
CurrencyTypes.TimeAttackDungeonTicket,
DateTime.Parse("2024-04-26T19:29:12")
},
{ CurrencyTypes.MasterCoin, DateTime.Parse("2024-04-26T19:29:12") }, { CurrencyTypes.MasterCoin, DateTime.Parse("2024-04-26T19:29:12") },
{ CurrencyTypes.WorldRaidTicketA, DateTime.Parse("2024-04-26T19:29:12") }, {
{ CurrencyTypes.WorldRaidTicketB, DateTime.Parse("2024-04-26T19:29:12") }, CurrencyTypes.WorldRaidTicketA,
{ CurrencyTypes.WorldRaidTicketC, DateTime.Parse("2024-04-26T19:29:12") }, DateTime.Parse("2024-04-26T19:29:12")
{ CurrencyTypes.ChaserTotalTicket, DateTime.Parse("2024-04-26T19:29:12") }, },
{ CurrencyTypes.SchoolDungeonTotalTicket, DateTime.Parse("2024-04-26T19:29:12") }, {
{ CurrencyTypes.EliminateTicketA, DateTime.Parse("2024-04-26T19:29:12") }, CurrencyTypes.WorldRaidTicketB,
{ CurrencyTypes.EliminateTicketB, DateTime.Parse("2024-04-26T19:29:12") }, DateTime.Parse("2024-04-26T19:29:12")
{ CurrencyTypes.EliminateTicketC, DateTime.Parse("2024-04-26T19:29:12") }, },
{ CurrencyTypes.EliminateTicketD, DateTime.Parse("2024-04-26T19:29:12") } {
CurrencyTypes.WorldRaidTicketC,
DateTime.Parse("2024-04-26T19:29:12")
},
{
CurrencyTypes.ChaserTotalTicket,
DateTime.Parse("2024-04-26T19:29:12")
},
{
CurrencyTypes.SchoolDungeonTotalTicket,
DateTime.Parse("2024-04-26T19:29:12")
},
{
CurrencyTypes.EliminateTicketA,
DateTime.Parse("2024-04-26T19:29:12")
},
{
CurrencyTypes.EliminateTicketB,
DateTime.Parse("2024-04-26T19:29:12")
},
{
CurrencyTypes.EliminateTicketC,
DateTime.Parse("2024-04-26T19:29:12")
},
{
CurrencyTypes.EliminateTicketD,
DateTime.Parse("2024-04-26T19:29:12")
}
} }
} }
}, },
@ -287,10 +394,7 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
WeaponDBs = [.. account.Weapons], WeaponDBs = [.. account.Weapons],
CostumeDBs = [], CostumeDBs = [],
}, },
ItemListResponse = new ItemListResponse() ItemListResponse = new ItemListResponse() { ItemDBs = [.. account.Items], },
{
ItemDBs = [.. account.Items],
},
EchelonListResponse = new EchelonListResponse() EchelonListResponse = new EchelonListResponse()
{ {
@ -298,7 +402,8 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
}, },
EventContentPermanentListResponse = new EventContentPermanentListResponse() EventContentPermanentListResponse = new EventContentPermanentListResponse()
{ {
PermanentDBs = [ PermanentDBs =
[
new() { EventContentId = 900801 }, new() { EventContentId = 900801 },
new() { EventContentId = 900802 }, new() { EventContentId = 900802 },
new() { EventContentId = 900803 }, new() { EventContentId = 900803 },
@ -327,10 +432,7 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
ClanLoginResponse = new ClanLoginResponse() ClanLoginResponse = new ClanLoginResponse()
{ {
AccountClanMemberDB = new() AccountClanMemberDB = new() { AccountId = account.ServerId }
{
AccountId = account.ServerId
}
}, },
EliminateRaidLoginResponse = new EliminateRaidLoginResponse() EliminateRaidLoginResponse = new EliminateRaidLoginResponse()
@ -350,11 +452,15 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
[ProtocolHandler(Protocol.Account_GetTutorial)] [ProtocolHandler(Protocol.Account_GetTutorial)]
public ResponsePacket GetTutorialHandler(AccountGetTutorialRequest req) public ResponsePacket GetTutorialHandler(AccountGetTutorialRequest req)
{ {
var tutorialIds = context.AccountTutorials.SingleOrDefault(x => x.AccountServerId == sessionKeyService.GetAccount(req.SessionKey).ServerId)?.TutorialIds; var tutorialIds = context
.AccountTutorials.SingleOrDefault(x =>
x.AccountServerId == sessionKeyService.GetAccount(req.SessionKey).ServerId
)
?.TutorialIds;
return new AccountGetTutorialResponse() return new AccountGetTutorialResponse()
{ {
TutorialIds = tutorialIds ?? [] TutorialIds = tutorialIds ?? Enumerable.Range(1, 27).Select(i => (long)i).ToList()
}; };
} }
@ -362,7 +468,9 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
public ResponsePacket SetTutorialHandler(AccountSetTutorialRequest req) public ResponsePacket SetTutorialHandler(AccountSetTutorialRequest req)
{ {
var account = sessionKeyService.GetAccount(req.SessionKey); var account = sessionKeyService.GetAccount(req.SessionKey);
var tutorial = context.AccountTutorials.SingleOrDefault(x => x.AccountServerId == account.ServerId); var tutorial = context.AccountTutorials.SingleOrDefault(x =>
x.AccountServerId == account.ServerId
);
if (tutorial == null) if (tutorial == null)
{ {
tutorial = new() tutorial = new()
@ -401,45 +509,48 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
[ProtocolHandler(Protocol.Toast_List)] [ProtocolHandler(Protocol.Toast_List)]
public ResponsePacket ToastListHandler(ToastListRequest req) public ResponsePacket ToastListHandler(ToastListRequest req)
{ {
return new ToastListResponse(); return new ToastListResponse();
} }
[ProtocolHandler(Protocol.ContentLog_UIOpenStatistics)] [ProtocolHandler(Protocol.ContentLog_UIOpenStatistics)]
public ResponsePacket ContentLog_UIOpenStatisticsHandler(ContentLogUIOpenStatisticsRequest req) public ResponsePacket ContentLog_UIOpenStatisticsHandler(
ContentLogUIOpenStatisticsRequest req
)
{ {
return new ContentLogUIOpenStatisticsResponse(); return new ContentLogUIOpenStatisticsResponse();
} }
[ProtocolHandler(Protocol.Event_RewardIncrease)] [ProtocolHandler(Protocol.Event_RewardIncrease)]
public ResponsePacket Event_RewardIncreaseHandler(EventRewardIncreaseRequest req) public ResponsePacket Event_RewardIncreaseHandler(EventRewardIncreaseRequest req)
{ {
return new EventRewardIncreaseResponse(); return new EventRewardIncreaseResponse();
} }
[ProtocolHandler(Protocol.OpenCondition_EventList)] [ProtocolHandler(Protocol.OpenCondition_EventList)]
public ResponsePacket OpenCondition_EventListHandler(OpenConditionEventListRequest req) public ResponsePacket OpenCondition_EventListHandler(OpenConditionEventListRequest req)
{ {
return new OpenConditionEventListResponse() return new OpenConditionEventListResponse()
{ {
// all open for now ig // all open for now ig
StaticOpenConditions = Enum.GetValues(typeof(OpenConditionContent)).Cast<OpenConditionContent>().ToDictionary(key => key, key => OpenConditionLockReason.None) StaticOpenConditions = Enum.GetValues(typeof(OpenConditionContent))
.Cast<OpenConditionContent>()
.ToDictionary(key => key, key => OpenConditionLockReason.None)
}; };
} }
[ProtocolHandler(Protocol.Notification_EventContentReddotCheck)] [ProtocolHandler(Protocol.Notification_EventContentReddotCheck)]
public ResponsePacket Notification_EventContentReddotCheckHandler(NotificationEventContentReddotRequest req) public ResponsePacket Notification_EventContentReddotCheckHandler(
NotificationEventContentReddotRequest req
)
{ {
return new NotificationEventContentReddotResponse(); return new NotificationEventContentReddotResponse();
} }
[ProtocolHandler(Protocol.Billing_PurchaseListByYostar)] [ProtocolHandler(Protocol.Billing_PurchaseListByYostar)]
public ResponsePacket Billing_PurchaseListByYostarHandler(BillingPurchaseListByYostarRequest req) public ResponsePacket Billing_PurchaseListByYostarHandler(
BillingPurchaseListByYostarRequest req
)
{ {
return new BillingPurchaseListByYostarResponse(); return new BillingPurchaseListByYostarResponse();
} }
@ -461,5 +572,4 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
return new MiniGameMissionListResponse(); return new MiniGameMissionListResponse();
} }
} }
} }

View File

@ -0,0 +1,327 @@
using SCHALE.Common.Database;
using SCHALE.Common.FlatData;
using SCHALE.Common.NetworkProtocol;
using SCHALE.GameServer.Services;
namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
{
public class Arena(
IProtocolHandlerFactory protocolHandlerFactory,
ISessionKeyService _sessionKeyService,
SCHALEContext _context
) : ProtocolHandlerBase(protocolHandlerFactory)
{
private readonly ISessionKeyService sessionKeyService = _sessionKeyService;
private readonly SCHALEContext context = _context;
private EquipmentDB? GetEquipmentDB(long accountServerId, long equipmentServerId)
{
if (equipmentServerId == 0)
return null;
return context.Equipment.First(c =>
c.AccountServerId == accountServerId && c.ServerId == equipmentServerId
);
}
private ArenaCharacterDB? Convert(long accountServerId, long characterServerId)
{
if (characterServerId == 0)
return null;
var characterDB = context.Characters.First(c =>
c.AccountServerId == accountServerId && c.ServerId == characterServerId
);
return Convert(characterDB);
}
private ArenaCharacterDB Convert(CharacterDB db)
{
var res = new ArenaCharacterDB
{
UniqueId = db.UniqueId,
StarGrade = db.StarGrade,
Level = db.Level,
PublicSkillLevel = db.PublicSkillLevel,
ExSkillLevel = db.ExSkillLevel,
PassiveSkillLevel = db.PassiveSkillLevel,
ExtraPassiveSkillLevel = db.ExtraPassiveSkillLevel,
LeaderSkillLevel = db.LeaderSkillLevel,
EquipmentDBs = db
.EquipmentServerIds.Select(i => GetEquipmentDB(db.AccountServerId, i))
.Where(i => i != null)
.ToList()!,
FavorRankInfo = new Dictionary<long, long>
{
// TODO: add all
{ db.UniqueId, db.FavorRank }
},
PotentialStats = db.PotentialStats
};
var weaponDB = context.Weapons.FirstOrDefault(w =>
w.AccountServerId == db.AccountServerId && w.BoundCharacterServerId == db.ServerId
);
if (weaponDB != null)
res.WeaponDB = weaponDB;
var gearDB = context.Gears.FirstOrDefault(w =>
w.AccountServerId == db.AccountServerId && w.BoundCharacterServerId == db.ServerId
);
if (gearDB != null)
res.GearDB = gearDB;
return res;
}
private ArenaTeamSettingDB Convert(EchelonDB db)
{
var LeaderCharacterId = context
.Characters.First(c =>
c.AccountServerId == db.AccountServerId && c.ServerId == db.LeaderServerId
)
.UniqueId;
return new ArenaTeamSettingDB()
{
EchelonType = db.EchelonType,
LeaderCharacterId = LeaderCharacterId,
MainCharacters = db
.MainSlotServerIds.Select(i => Convert(db.AccountServerId, i))
.Where(i => i != null)
.ToList()!,
SupportCharacters = db
.SupportSlotServerIds.Select(i => Convert(db.AccountServerId, i))
.Where(i => i != null)
.ToList()!,
MapId = 1001,
};
}
private static readonly ArenaTeamSettingDB dummyTeam =
new()
{
EchelonType = EchelonType.ArenaDefence,
LeaderCharacterId = 10065,
MainCharacters =
[
new ArenaCharacterDB()
{
UniqueId = 10065,
StarGrade = 3,
Level = 90,
PublicSkillLevel = 1,
ExSkillLevel = 1,
PassiveSkillLevel = 1,
ExtraPassiveSkillLevel = 1,
LeaderSkillLevel = 1
}
],
MapId = 1001,
};
private ArenaTeamSettingDB? GetDefense(long accountId)
{
var defense = context.Echelons.FirstOrDefault(e =>
e.AccountServerId == accountId
&& e.EchelonType == EchelonType.ArenaDefence
&& e.EchelonNumber == 1
&& e.ExtensionType == EchelonExtensionType.Base
);
if (defense == null)
return null;
return Convert(defense);
}
private static List<ArenaUserDB> DummyOpponent(ArenaTeamSettingDB? team)
{
return
[
new ArenaUserDB()
{
RepresentCharacterUniqueId = 20024,
NickName = "your",
Rank = 2,
Level = 90,
TeamSettingDB = team ?? dummyTeam
},
new ArenaUserDB()
{
RepresentCharacterUniqueId = 10059,
NickName = "defense",
Rank = 3,
Level = 90,
TeamSettingDB = team ?? dummyTeam
},
new ArenaUserDB()
{
RepresentCharacterUniqueId = 10065,
NickName = "team",
Rank = 4,
Level = 90,
TeamSettingDB = team ?? dummyTeam
}
];
}
[ProtocolHandler(Protocol.Arena_EnterLobby)]
public ResponsePacket EnterLobbyHandler(ArenaEnterLobbyRequest req)
{
return new ArenaEnterLobbyResponse()
{
ArenaPlayerInfoDB = new()
{
CurrentSeasonId = 6,
PlayerGroupId = 1,
CurrentRank = 1,
SeasonRecord = 1,
AllTimeRecord = 1
},
OpponentUserDBs = DummyOpponent(GetDefense(req.AccountId)),
MapId = 1001
};
}
[ProtocolHandler(Protocol.Arena_OpponentList)]
public ResponsePacket OpponentListHandler(ArenaOpponentListRequest req)
{
return new ArenaOpponentListResponse()
{
PlayerRank = 1,
OpponentUserDBs = DummyOpponent(GetDefense(req.AccountId))
};
}
[ProtocolHandler(Protocol.Arena_SyncEchelonSettingTime)]
public ResponsePacket SyncEchelonSettingTimeHandler(ArenaSyncEchelonSettingTimeRequest req)
{
return new ArenaSyncEchelonSettingTimeResponse() { EchelonSettingTime = DateTime.Now };
}
[ProtocolHandler(Protocol.Arena_EnterBattlePart1)]
public ResponsePacket EnterBattlePart1Handler(ArenaEnterBattlePart1Request req)
{
var attack = context.Echelons.First(e =>
e.AccountServerId == req.AccountId
&& e.EchelonType == EchelonType.ArenaAttack
&& e.EchelonNumber == 1
&& e.ExtensionType == EchelonExtensionType.Base
);
ArenaUserDB arenaUserDB =
new()
{
RepresentCharacterUniqueId = 10059,
NickName = "You",
Rank = 1,
Level = 90,
TeamSettingDB = Convert(attack)
};
return new ArenaEnterBattlePart1Response()
{
ArenaBattleDB = new()
{
Season = 6,
Group = 1,
BattleStartTime = DateTime.Now,
Seed = 1,
AttackingUserDB = arenaUserDB,
DefendingUserDB = DummyOpponent(GetDefense(req.AccountId))[0]
}
};
}
[ProtocolHandler(Protocol.Arena_EnterBattlePart2)]
public ResponsePacket EnterBattlePart2Handler(ArenaEnterBattlePart2Request req)
{
return new ArenaEnterBattlePart2Response()
{
ArenaBattleDB = req.ArenaBattleDB,
ArenaPlayerInfoDB = new ArenaPlayerInfoDB()
{
CurrentSeasonId = 6,
PlayerGroupId = 1,
CurrentRank = 1,
SeasonRecord = 1,
AllTimeRecord = 1
},
AccountCurrencyDB = new AccountCurrencyDB
{
AccountLevel = 90,
AcademyLocationRankSum = 1,
CurrencyDict = new Dictionary<CurrencyTypes, long>
{
{ CurrencyTypes.Gem, long.MaxValue }, // gacha currency 600
{ CurrencyTypes.GemPaid, 0 },
{ CurrencyTypes.GemBonus, long.MaxValue }, // default blue gem?
{ CurrencyTypes.Gold, 962_350_000 }, // credit 10,000
{ CurrencyTypes.ActionPoint, long.MaxValue }, // energy 24
{ CurrencyTypes.AcademyTicket, 3 },
{ CurrencyTypes.ArenaTicket, 5 },
{ CurrencyTypes.RaidTicket, 3 },
{ CurrencyTypes.WeekDungeonChaserATicket, 0 },
{ CurrencyTypes.WeekDungeonChaserBTicket, 0 },
{ CurrencyTypes.WeekDungeonChaserCTicket, 0 },
{ CurrencyTypes.SchoolDungeonATicket, 0 },
{ CurrencyTypes.SchoolDungeonBTicket, 0 },
{ CurrencyTypes.SchoolDungeonCTicket, 0 },
{ CurrencyTypes.TimeAttackDungeonTicket, 3 },
{ CurrencyTypes.MasterCoin, 0 },
{ CurrencyTypes.WorldRaidTicketA, 40 },
{ CurrencyTypes.WorldRaidTicketB, 40 },
{ CurrencyTypes.WorldRaidTicketC, 40 },
{ CurrencyTypes.ChaserTotalTicket, 6 },
{ CurrencyTypes.SchoolDungeonTotalTicket, 6 },
{ CurrencyTypes.EliminateTicketA, 1 },
{ CurrencyTypes.EliminateTicketB, 1 },
{ CurrencyTypes.EliminateTicketC, 1 },
{ CurrencyTypes.EliminateTicketD, 1 }
},
UpdateTimeDict = new Dictionary<CurrencyTypes, DateTime>
{
{ CurrencyTypes.ActionPoint, DateTime.Parse("2024-04-26T19:29:12") },
{ CurrencyTypes.AcademyTicket, DateTime.Parse("2024-04-26T19:29:12") },
{ CurrencyTypes.ArenaTicket, DateTime.Parse("2024-04-26T19:29:12") },
{ CurrencyTypes.RaidTicket, DateTime.Parse("2024-04-26T19:29:12") },
{
CurrencyTypes.WeekDungeonChaserATicket,
DateTime.Parse("2024-04-26T19:29:12")
},
{
CurrencyTypes.WeekDungeonChaserBTicket,
DateTime.Parse("2024-04-26T19:29:12")
},
{
CurrencyTypes.WeekDungeonChaserCTicket,
DateTime.Parse("2024-04-26T19:29:12")
},
{
CurrencyTypes.SchoolDungeonATicket,
DateTime.Parse("2024-04-26T19:29:12")
},
{
CurrencyTypes.SchoolDungeonBTicket,
DateTime.Parse("2024-04-26T19:29:12")
},
{
CurrencyTypes.SchoolDungeonCTicket,
DateTime.Parse("2024-04-26T19:29:12")
},
{
CurrencyTypes.TimeAttackDungeonTicket,
DateTime.Parse("2024-04-26T19:29:12")
},
{ CurrencyTypes.MasterCoin, DateTime.Parse("2024-04-26T19:29:12") },
{ CurrencyTypes.WorldRaidTicketA, DateTime.Parse("2024-04-26T19:29:12") },
{ CurrencyTypes.WorldRaidTicketB, DateTime.Parse("2024-04-26T19:29:12") },
{ CurrencyTypes.WorldRaidTicketC, DateTime.Parse("2024-04-26T19:29:12") },
{ CurrencyTypes.ChaserTotalTicket, DateTime.Parse("2024-04-26T19:29:12") },
{
CurrencyTypes.SchoolDungeonTotalTicket,
DateTime.Parse("2024-04-26T19:29:12")
},
{ CurrencyTypes.EliminateTicketA, DateTime.Parse("2024-04-26T19:29:12") },
{ CurrencyTypes.EliminateTicketB, DateTime.Parse("2024-04-26T19:29:12") },
{ CurrencyTypes.EliminateTicketC, DateTime.Parse("2024-04-26T19:29:12") },
{ CurrencyTypes.EliminateTicketD, DateTime.Parse("2024-04-26T19:29:12") }
}
}
};
}
}
}

View File

@ -1,4 +1,5 @@
using SCHALE.Common.Database; using Microsoft.EntityFrameworkCore;
using SCHALE.Common.Database;
using SCHALE.Common.Database.ModelExtensions; using SCHALE.Common.Database.ModelExtensions;
using SCHALE.Common.FlatData; using SCHALE.Common.FlatData;
using SCHALE.Common.NetworkProtocol; using SCHALE.Common.NetworkProtocol;
@ -6,37 +7,36 @@ using SCHALE.GameServer.Services;
namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers
{ {
public class Echelon : ProtocolHandlerBase public class Echelon(
IProtocolHandlerFactory protocolHandlerFactory,
ISessionKeyService _sessionKeyService,
SCHALEContext _context,
ExcelTableService _excelTableService,
ILogger<Echelon> _logger
) : ProtocolHandlerBase(protocolHandlerFactory)
{ {
private readonly ISessionKeyService sessionKeyService; private readonly ISessionKeyService sessionKeyService = _sessionKeyService;
private readonly SCHALEContext context; private readonly SCHALEContext context = _context;
private readonly ExcelTableService excelTableService; private readonly ExcelTableService excelTableService = _excelTableService;
private readonly ILogger<Echelon> logger = _logger;
public Echelon(IProtocolHandlerFactory protocolHandlerFactory, ISessionKeyService _sessionKeyService, SCHALEContext _context, ExcelTableService _excelTableService) : base(protocolHandlerFactory)
{
sessionKeyService = _sessionKeyService;
context = _context;
excelTableService = _excelTableService;
}
[ProtocolHandler(Protocol.Echelon_List)] [ProtocolHandler(Protocol.Echelon_List)]
public ResponsePacket ListHandler(EchelonListRequest req) public ResponsePacket ListHandler(EchelonListRequest req)
{ {
var account = sessionKeyService.GetAccount(req.SessionKey); var account = sessionKeyService.GetAccount(req.SessionKey);
return new EchelonListResponse() return new EchelonListResponse() { EchelonDBs = [.. account.Echelons] };
{
EchelonDBs = account.Echelons.ToList()
};
} }
[ProtocolHandler(Protocol.Echelon_Save)] [ProtocolHandler(Protocol.Echelon_Save)]
public ResponsePacket SaveHandler(EchelonSaveRequest req) public ResponsePacket SaveHandler(EchelonSaveRequest req)
{ {
return new EchelonSaveResponse() var db = req.EchelonDB;
{
EchelonDB = req.EchelonDB, context.Echelons.Add(db);
}; context.SaveChanges();
return new EchelonSaveResponse() { EchelonDB = req.EchelonDB, };
} }
} }
} }

View File

@ -0,0 +1,81 @@
using Microsoft.AspNetCore.Mvc;
using SCHALE.Common.Crypto;
using SCHALE.Common.NetworkProtocol;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace SCHALE.GameServer.Controllers.Data
{
[ApiController]
[Route("/data")]
public class DataController : ControllerBase
{
private readonly string absFolder;
private readonly ILogger<DataController> logger;
public DataController(ILogger<DataController> _logger)
{
logger = _logger;
var folder = Path.GetDirectoryName(AppContext.BaseDirectory);
string dataFolder;
while (true)
{
dataFolder = Path.Join(folder, "Resources/data");
if (Directory.Exists(dataFolder)) break;
folder = Path.GetDirectoryName(folder);
if (folder == null)
throw new FileNotFoundException($"Excel folder is not found.");
}
absFolder = dataFolder;
}
string? AbsPath(string relPath)
{
string filePath = Path.Combine(absFolder, relPath);
if (!System.IO.File.Exists(filePath)) return null;
logger.LogDebug($"Use our own {relPath}.");
return filePath;
}
[HttpGet("TableBundles/{fileName}")]
public IActionResult GetTableBundles(string fileName)
{
string relPath = $"TableBundles/{fileName}";
string? filePath = AbsPath(relPath);
if (filePath == null) return CatchAll(relPath);
if (fileName.EndsWith(".json"))
{
var jsonContent = System.IO.File.ReadAllText(filePath);
return Content(jsonContent, "application/json");
}
if (fileName.EndsWith(".zip"))
{
var fileStream = System.IO.File.OpenRead(filePath);
return File(fileStream, "application/zip", fileName);
}
if (fileName.EndsWith(".bytes"))
{
var fileStream = System.IO.File.OpenRead(filePath);
return File(fileStream, "application/octet-stream", fileName);
}
return CatchAll(relPath);
}
[HttpGet("{*catchAll}")]
public IActionResult CatchAll(string catchAll)
{
logger.LogDebug("Data path: {path}", catchAll);
string re = $"https://prod-clientpatch.bluearchiveyostar.com/r68_10iazxytt13razwn7x9n_3/{catchAll}";
return Redirect(re);
// return RedirectPermanent(re);
}
}
}

View File

@ -1,35 +1,48 @@
using Microsoft.AspNetCore.Server.Kestrel.Core; using System.Collections;
using Serilog.Events; using System.Net.NetworkInformation;
using Serilog;
using System.Reflection; using System.Reflection;
using System.Text;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.EntityFrameworkCore;
using SCHALE.Common.Crypto;
using SCHALE.Common.Database; using SCHALE.Common.Database;
using SCHALE.GameServer.Commands;
using SCHALE.GameServer.Controllers.Api.ProtocolHandlers; using SCHALE.GameServer.Controllers.Api.ProtocolHandlers;
using SCHALE.GameServer.Services; using SCHALE.GameServer.Services;
using Microsoft.EntityFrameworkCore;
using SCHALE.GameServer.Services.Irc; using SCHALE.GameServer.Services.Irc;
using SCHALE.GameServer.Commands;
using SCHALE.GameServer.Utils; using SCHALE.GameServer.Utils;
using System.Net.NetworkInformation; using Serilog;
using Serilog.Events;
namespace SCHALE.GameServer namespace SCHALE.GameServer
{ {
public class GameServer public class GameServer
{ {
public static void Main(string[] args) public static async Task Main(string[] args)
{ {
var config = new ConfigurationBuilder() var config = new ConfigurationBuilder()
.SetBasePath(Path.GetDirectoryName(AppContext.BaseDirectory)!) .SetBasePath(Path.GetDirectoryName(AppContext.BaseDirectory)!)
.AddJsonFile("appsettings.json") .AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", true) .AddJsonFile(
$"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json",
true
)
.AddJsonFile("appsettings.Local.json", true) .AddJsonFile("appsettings.Local.json", true)
.Build(); .Build();
{ {
var logFilePath = Path.Combine(Path.GetDirectoryName(AppContext.BaseDirectory)!, "logs", "log.txt"); var logFilePath = Path.Combine(
Path.GetDirectoryName(AppContext.BaseDirectory)!,
"logs",
"log.txt"
);
if (File.Exists(logFilePath)) if (File.Exists(logFilePath))
{ {
var prevLogFilePath = Path.Combine(Path.GetDirectoryName(logFilePath)!, "log-prev.txt"); var prevLogFilePath = Path.Combine(
Path.GetDirectoryName(logFilePath)!,
"log-prev.txt"
);
if (File.Exists(prevLogFilePath)) if (File.Exists(prevLogFilePath))
File.Delete(prevLogFilePath); File.Delete(prevLogFilePath);
@ -38,7 +51,11 @@ namespace SCHALE.GameServer
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.WriteTo.Console() .WriteTo.Console()
.WriteTo.File(logFilePath, restrictedToMinimumLevel: LogEventLevel.Verbose, shared: true) .WriteTo.File(
logFilePath,
restrictedToMinimumLevel: LogEventLevel.Verbose,
shared: true
)
.ReadFrom.Configuration(config) .ReadFrom.Configuration(config)
.CreateBootstrapLogger(); .CreateBootstrapLogger();
} }
@ -46,6 +63,9 @@ namespace SCHALE.GameServer
Log.Information("Starting..."); Log.Information("Starting...");
try try
{ {
Log.Information("Downloading Excels...");
await ExcelTableService.Init();
// Load Commands // Load Commands
CommandFactory.LoadCommands(); CommandFactory.LoadCommands();
@ -54,17 +74,36 @@ namespace SCHALE.GameServer
if (Config.Instance.Address == "127.0.0.1") if (Config.Instance.Address == "127.0.0.1")
{ {
Config.Instance.Address = NetworkInterface.GetAllNetworkInterfaces().Where(i => i.NetworkInterfaceType != NetworkInterfaceType.Loopback && i.OperationalStatus == OperationalStatus.Up).First().GetIPProperties().UnicastAddresses.Where(a => a.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork).First().Address.ToString(); Config.Instance.Address = NetworkInterface
.GetAllNetworkInterfaces()
.Where(i =>
i.NetworkInterfaceType != NetworkInterfaceType.Loopback
&& i.OperationalStatus == OperationalStatus.Up
)
.First()
.GetIPProperties()
.UnicastAddresses.Where(a =>
a.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork
)
.First()
.Address.ToString();
Config.Save(); Config.Save();
} }
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<KestrelServerOptions>(op => op.AllowSynchronousIO = true); builder.Services.Configure<KestrelServerOptions>(op =>
op.AllowSynchronousIO = true
);
builder.Host.UseSerilog(); builder.Host.UseSerilog();
// Add services to the container. // Add services to the container.
builder.Services.AddSQLServerProvider(config.GetConnectionString("SQLServer") ?? throw new ArgumentNullException("ConnectionStrings/SQLServer in appsettings is missing")); builder.Services.AddSQLServerProvider(
config.GetConnectionString("SQLServer")
?? throw new ArgumentNullException(
"ConnectionStrings/SQLServer in appsettings is missing"
)
);
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddProtocolHandlerFactory(); builder.Services.AddProtocolHandlerFactory();
builder.Services.AddMemorySessionKeyService(); builder.Services.AddMemorySessionKeyService();
@ -72,7 +111,10 @@ namespace SCHALE.GameServer
builder.Services.AddIrcService(); builder.Services.AddIrcService();
// Add all Handler Groups // Add all Handler Groups
var handlerGroups = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsSubclassOf(typeof(ProtocolHandlerBase))); var handlerGroups = Assembly
.GetExecutingAssembly()
.GetTypes()
.Where(t => t.IsSubclassOf(typeof(ProtocolHandlerBase)));
foreach (var handlerGroup in handlerGroups) foreach (var handlerGroup in handlerGroups)
builder.Services.AddProtocolHandlerGroupByType(handlerGroup); builder.Services.AddProtocolHandlerGroupByType(handlerGroup);

View File

@ -7,6 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DotNetZip" Version="1.16.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.2"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

View File

@ -1,18 +1,79 @@
using Google.FlatBuffers; using System.IO;
using SCHALE.Common.Crypto; using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Google.FlatBuffers;
using Ionic.Zip;
using SCHALE.Common.Crypto;
using Serilog;
namespace SCHALE.GameServer.Services namespace SCHALE.GameServer.Services
{ {
// TODO: High priority, cache UnPack-ed table! // TODO: High priority, cache UnPack-ed table!
public class ExcelTableService public class ExcelTableService(ILogger<ExcelTableService> _logger)
{ {
private readonly ILogger<ExcelTableService> logger; private readonly ILogger<ExcelTableService> logger = _logger;
private readonly Dictionary<Type, object> caches = []; private readonly Dictionary<Type, object> caches = [];
public ExcelTableService(ILogger<ExcelTableService> _logger) private static string? ResourcesFolder;
private static string? ExcelFolder;
private static string GetUrl()
{ {
logger = _logger; string urlPath;
if (ResourcesFolder == null)
{
var folder = Path.GetDirectoryName(AppContext.BaseDirectory);
while (true)
{
urlPath = Path.Join(folder, "Resources/url.txt");
if (File.Exists(urlPath))
break;
folder = Path.GetDirectoryName(folder);
if (folder == null)
throw new FileNotFoundException($"Resources folder is not found.");
}
ResourcesFolder = Path.GetDirectoryName(urlPath);
}
else
{
urlPath = Path.Join(ResourcesFolder, "url.txt");
}
string url = File.ReadAllText(urlPath);
return url + "/";
}
private static async Task GetZip()
{
string url = GetUrl();
string filePath = "TableBundles/Excel.zip";
string zipPath = Path.Combine(ResourcesFolder!, "download", filePath);
ExcelFolder = zipPath[..^4];
if (File.Exists(zipPath))
return;
Directory.CreateDirectory(Path.GetDirectoryName(zipPath)!);
using HttpClient client = new();
HttpResponseMessage response = await client.GetAsync(url + filePath);
response.EnsureSuccessStatusCode();
byte[] content = await response.Content.ReadAsByteArrayAsync();
File.WriteAllBytes(zipPath, content);
using ZipFile zip = ZipFile.Read(zipPath);
//zip.Password = "/wy5f3hIGGXLOIUDS9DZ";
zip.Password = Convert.ToBase64String(TableService.CreatePassword(Path.GetFileName(filePath)));
foreach (ZipEntry e in zip)
{
e.Extract(ExcelFolder, ExtractExistingFileAction.OverwriteSilently);
}
}
public static async Task Init()
{
await GetZip();
} }
/// <summary> /// <summary>
@ -21,14 +82,16 @@ namespace SCHALE.GameServer.Services
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
/// <returns></returns> /// <returns></returns>
/// <exception cref="FileNotFoundException"></exception> /// <exception cref="FileNotFoundException"></exception>
public T GetTable<T>() where T : IFlatbufferObject public T GetTable<T>()
where T : IFlatbufferObject
{ {
var type = typeof(T); var type = typeof(T);
if (caches.TryGetValue(type, out var cache)) if (caches.TryGetValue(type, out var cache))
return (T)cache; return (T)cache;
var bytesFilePath = Path.Join(Path.GetDirectoryName(AppContext.BaseDirectory), "Resources/excel/", $"{type.Name.ToLower()}.bytes"); ArgumentNullException.ThrowIfNull(ExcelFolder);
var bytesFilePath = Path.Join(ExcelFolder, $"{type.Name.ToLower()}.bytes");
if (!File.Exists(bytesFilePath)) if (!File.Exists(bytesFilePath))
{ {
throw new FileNotFoundException($"bytes files for {type.Name} not found"); throw new FileNotFoundException($"bytes files for {type.Name} not found");
@ -36,7 +99,12 @@ namespace SCHALE.GameServer.Services
var bytes = File.ReadAllBytes(bytesFilePath); var bytes = File.ReadAllBytes(bytesFilePath);
TableEncryptionService.XOR(type.Name, bytes); TableEncryptionService.XOR(type.Name, bytes);
var inst = type.GetMethod($"GetRootAs{type.Name}", BindingFlags.Static | BindingFlags.Public, [typeof(ByteBuffer)])!.Invoke(null, [new ByteBuffer(bytes)]); var inst = type.GetMethod(
$"GetRootAs{type.Name}",
BindingFlags.Static | BindingFlags.Public,
[typeof(ByteBuffer)]
)!
.Invoke(null, [new ByteBuffer(bytes)]);
caches.Add(type, inst!); caches.Add(type, inst!);
logger.LogDebug("{Excel} loaded and cached", type.Name); logger.LogDebug("{Excel} loaded and cached", type.Name);

View File

@ -79,8 +79,8 @@ namespace SCHALE.Common.Utils
UniqueId = x.UniqueId, UniqueId = x.UniqueId,
BoundCharacterServerId = x.ServerId, BoundCharacterServerId = x.ServerId,
IsLocked = false, IsLocked = false,
StarGrade = 5, StarGrade = 3,
Level = 200 Level = 50
}; };
}); });
@ -132,17 +132,17 @@ namespace SCHALE.Common.Utils
{ {
UniqueId = characterId, UniqueId = characterId,
StarGrade = 5, StarGrade = 5,
Level = 200, Level = 90,
Exp = 0, Exp = 0,
PublicSkillLevel = 10, PublicSkillLevel = 10,
ExSkillLevel = 5, ExSkillLevel = 5,
PassiveSkillLevel = 10, PassiveSkillLevel = 10,
ExtraPassiveSkillLevel = 10, ExtraPassiveSkillLevel = 10,
LeaderSkillLevel = 1, LeaderSkillLevel = 1,
FavorRank = 500, FavorRank = 20,
IsNew = true, IsNew = true,
IsLocked = true, IsLocked = true,
PotentialStats = { { 1, 25 }, { 2, 25 }, { 3, 25 } }, PotentialStats = { { 1, 0 }, { 2, 0 }, { 3, 0 } },
EquipmentServerIds = [0, 0, 0] EquipmentServerIds = [0, 0, 0]
}; };
} }

4535
ba.js Normal file

File diff suppressed because it is too large Load Diff