Compare commits

...

3 commits

Author SHA1 Message Date
99ab5b239d Implement 'avatar add_all' command 2024-08-07 13:35:34 +03:00
84397a847e Implement proper Avatar and Buddy UnitID (Base/Robot), refactor template id
TemplateID::new now returns Result<T, TemplateNotFoundError>
Cleanup code in some of new handlers
2024-08-06 21:42:03 +03:00
28864ff1e4 Teleport Map & Switching Dynamic Wallpaper Support, Gacha optimization (#2)
## Abstract

This PR implements

- Showing Teleports on map
- Quick Menu usage
- Dynamic Wallpaper switching
- Settings UP for Bangboo pool (via CLI)

## Support list

- The support of Teleport with map.
- Unlocking the 3 buttons (Interknow, DMs, Map) as a prerequisite of the previous feature. By pressing 'F' you can also modify the buttons in Quick Menu.
- Changing Dynamic Wallpaper to a specific one instead of being randomly chosen. **Notice that you can't use one's Dynamic Wallpaper if you don't own this agent.**
- Fixed some Gacha bugs, including:
  - Obtained items won't be sent to your bag unless you're pulling the standard banner;
  - Obtained items won't show in bag until relogin;
- Alternative command `gacha up` to choose the UP Bangboo. Start with `gacha up [player_uid]` and follow the guide to fill params.

## Principle

- Player's `UnlockModelBin` stores Quick Menu data, with the player's chosen buttons and their positions.
- Separate Gacha's DTO & Saving model to make it more standardized, and ensure ID validation is always performed.
- Player's bag information is now sync to client appropriately after finishing any gacha operations.
- `gacha up` search for pools with a `chooseable` Category Guarantee Policy, then list included items if user has provided the target pool's `gacha_schedule_id`.

## Known issues

- **Specifically for 1.1 Beta**, teleport may meet these issues leading to a 'black screen', forcing you to restart the client (or use `player kick` command as an alternative).
  - Don't try to teleport to `治安局光映分署`, it won't succeed. This is the 32nd teleport out of the 31 ones defined visible in assets.
  - Don't close the page after opening map. Teleport to somewhere.
- `gacha up` command is an alternative to in-game operation. You still can not see any bangboos in the collection when choosing Bangboo.

Co-authored-by: YYHEggEgg <53960525+YYHEggEgg@users.noreply.github.com>
Reviewed-on: #2
Co-authored-by: YYHEggEgg <yyheggegg@xeondev.com>
Co-committed-by: YYHEggEgg <yyheggegg@xeondev.com>
2024-08-06 17:15:04 +00:00
66 changed files with 76997 additions and 1012 deletions

View file

@ -0,0 +1,266 @@
[
{
"ID": 50001,
"BuddyType": 1,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_50001",
"DIIDBBGLDOL": "Bangboo_Name_50001",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 9831098947874880184
},
{
"ID": 53001,
"BuddyType": 2,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_53001",
"DIIDBBGLDOL": "Bangboo_Name_53001",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 8017298617212498953
},
{
"ID": 53002,
"BuddyType": 2,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_53002",
"DIIDBBGLDOL": "Bangboo_Name_53002",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 15759244567804428825
},
{
"ID": 53003,
"BuddyType": 2,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_53003",
"DIIDBBGLDOL": "Bangboo_Name_53003",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 793541769103470830
},
{
"ID": 53004,
"BuddyType": 2,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_53004",
"DIIDBBGLDOL": "Bangboo_Name_53004",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 15333283710424173893
},
{
"ID": 53005,
"BuddyType": 2,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_53005",
"DIIDBBGLDOL": "Bangboo_Name_53005",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 6625485447473768309
},
{
"ID": 53006,
"BuddyType": 2,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_53006",
"DIIDBBGLDOL": "Bangboo_Name_53006",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 12516213533679239810
},
{
"ID": 53007,
"BuddyType": 2,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_53007",
"DIIDBBGLDOL": "Bangboo_Name_53007",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 1205030983673817181
},
{
"ID": 53008,
"BuddyType": 2,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_53008",
"DIIDBBGLDOL": "Bangboo_Name_53008",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 825481331324073038
},
{
"ID": 53009,
"BuddyType": 2,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_53009",
"DIIDBBGLDOL": "Bangboo_Name_53009",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 1109142289374885163
},
{
"ID": 53010,
"BuddyType": 2,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_53010",
"DIIDBBGLDOL": "Bangboo_Name_53010",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 7654740197435466664
},
{
"ID": 53011,
"BuddyType": 2,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_53011",
"DIIDBBGLDOL": "Bangboo_Name_53011",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 13043263780191768021
},
{
"ID": 53012,
"BuddyType": 2,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_53012",
"DIIDBBGLDOL": "Bangboo_Name_53012",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 2166541009547495725
},
{
"ID": 54001,
"BuddyType": 2,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_54001",
"DIIDBBGLDOL": "Bangboo_Name_54001",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 12126171553945369848
},
{
"ID": 54002,
"BuddyType": 2,
"ECHNDHDIIAC": 3,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_54002",
"DIIDBBGLDOL": "Bangboo_Name_54002",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 13558320022686804846
},
{
"ID": 54003,
"BuddyType": 2,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_54003",
"DIIDBBGLDOL": "Bangboo_Name_54003",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 9057893760084610210
},
{
"ID": 54004,
"BuddyType": 2,
"ECHNDHDIIAC": 2,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_54004",
"DIIDBBGLDOL": "Bangboo_Name_54004",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 7098007717545413607
},
{
"ID": 54005,
"BuddyType": 2,
"ECHNDHDIIAC": 1,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_54005",
"DIIDBBGLDOL": "Bangboo_Name_54005",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 7896901026978333696
},
{
"ID": 54006,
"BuddyType": 2,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_54006",
"DIIDBBGLDOL": "Bangboo_Name_54006",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 15413390933002967244
},
{
"ID": 54008,
"BuddyType": 2,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_54008",
"DIIDBBGLDOL": "Bangboo_Name_54008",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 13745481736816990381
},
{
"ID": 54009,
"BuddyType": 2,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_54009",
"DIIDBBGLDOL": "Bangboo_Name_54009",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 4565374391606395253
},
{
"ID": 54013,
"BuddyType": 2,
"ECHNDHDIIAC": 0,
"OHLKAFPBJHD": 0,
"GDDJBFHBJNK": 1,
"LAFKHMCKNIO": "Bangboo_Name_en_54013",
"DIIDBBGLDOL": "Bangboo_Name_54013",
"PCNEIBEDMCO": "BK_Eous",
"HIMPMHKGGIC": "",
"ANDDIMCDBME": 4076165347655962714
}
]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
[
{
"QuickAccessIndex": 1,
"QuickFuncID": 1005,
"RequiredUnlockIDs": [
1005
],
"EFPBDDJIJBO": 1040140154,
"BOKMLGNMJPB": null
},
{
"QuickAccessIndex": 2,
"QuickFuncID": 1022,
"RequiredUnlockIDs": [
1022
],
"EFPBDDJIJBO": 1040140049,
"BOKMLGNMJPB": null
},
{
"QuickAccessIndex": 3,
"QuickFuncID": 1009,
"RequiredUnlockIDs": [
1009
],
"EFPBDDJIJBO": 1050060115,
"BOKMLGNMJPB": null
}
]

View file

@ -0,0 +1,112 @@
[
{
"BtnID": 1001,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1002,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1003,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1004,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1005,
"BOMNEKHOGIH": "UI/Sprite/A1DynamicLoad/MainCityMenus/Packer/BtnInterKnot.png",
"PECPFBBOGKO": "UI/Sprite/A1DynamicLoad/MainCityMenus/Packer/BtnInterKnot.png"
},
{
"BtnID": 1006,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1007,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1008,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1009,
"BOMNEKHOGIH": "UI/Sprite/A1DynamicLoad/MainCityMenus/Packer/BtnGuide.png",
"PECPFBBOGKO": "UI/Sprite/A1DynamicLoad/MainCityMenus/Packer/BtnGuide.png"
},
{
"BtnID": 1010,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1011,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1013,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1014,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1015,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1016,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1017,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1018,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1019,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1020,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
},
{
"BtnID": 1021,
"BOMNEKHOGIH": "UI/Sprite/A1DynamicLoad/MainCityMenus/Packer/BtnCamera.png",
"PECPFBBOGKO": "UI/Sprite/A1DynamicLoad/MainCityMenus/Packer/BtnCamera.png"
},
{
"BtnID": 1022,
"BOMNEKHOGIH": "UI/Sprite/A1DynamicLoad/MainCityMenus/Packer/BtnMessage.png",
"PECPFBBOGKO": "UI/Sprite/A1DynamicLoad/MainCityMenus/Packer/BtnMessage.png"
},
{
"BtnID": 1023,
"BOMNEKHOGIH": "",
"PECPFBBOGKO": ""
}
]

View file

@ -0,0 +1,519 @@
[
{
"RobotBuddyID": 19053001,
"BuddyID": 53001,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1053001,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19053002,
"BuddyID": 53002,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1053002,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19053003,
"BuddyID": 53003,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1053003,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19053004,
"BuddyID": 53004,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1053004,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19053005,
"BuddyID": 53005,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1053005,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19053006,
"BuddyID": 53006,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1053006,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19053007,
"BuddyID": 53007,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1053007,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19053008,
"BuddyID": 53008,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1053008,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19053009,
"BuddyID": 53009,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1053009,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19053010,
"BuddyID": 53010,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1053010,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19053011,
"BuddyID": 53011,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1053011,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19053012,
"BuddyID": 53012,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1053012,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19054001,
"BuddyID": 54001,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1054001,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19054002,
"BuddyID": 54002,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1054002,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19054003,
"BuddyID": 54003,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1054003,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19054004,
"BuddyID": 54004,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1054004,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19054005,
"BuddyID": 54005,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1054005,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19054006,
"BuddyID": 54006,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1054006,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19054008,
"BuddyID": 54008,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1054008,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19054009,
"BuddyID": 54009,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1054009,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 19054013,
"BuddyID": 54013,
"CharLevel": 20,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 1054013,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 101000404,
"BuddyID": 53006,
"CharLevel": 1,
"CharUpgradeLevel": 1,
"CharStar": 1,
"GHGKBFEKDND": 53006,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 101000504,
"BuddyID": 54005,
"CharLevel": 6,
"CharUpgradeLevel": 1,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1000010104,
"BuddyID": 54005,
"CharLevel": 9,
"CharUpgradeLevel": 1,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1000010204,
"BuddyID": 54005,
"CharLevel": 9,
"CharUpgradeLevel": 1,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1000010304,
"BuddyID": 54005,
"CharLevel": 9,
"CharUpgradeLevel": 1,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1000010604,
"BuddyID": 54005,
"CharLevel": 11,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1000010704,
"BuddyID": 54005,
"CharLevel": 12,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1000010804,
"BuddyID": 54005,
"CharLevel": 13,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1001000404,
"BuddyID": 54005,
"CharLevel": 8,
"CharUpgradeLevel": 1,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1001010104,
"BuddyID": 54005,
"CharLevel": 10,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1001010204,
"BuddyID": 54005,
"CharLevel": 15,
"CharUpgradeLevel": 2,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1001020204,
"BuddyID": 54002,
"CharLevel": 24,
"CharUpgradeLevel": 3,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1001020304,
"BuddyID": 54002,
"CharLevel": 24,
"CharUpgradeLevel": 3,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1001020404,
"BuddyID": 54002,
"CharLevel": 26,
"CharUpgradeLevel": 3,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1001020904,
"BuddyID": 54002,
"CharLevel": 28,
"CharUpgradeLevel": 3,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1001021104,
"BuddyID": 54009,
"CharLevel": 32,
"CharUpgradeLevel": 4,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1001021204,
"BuddyID": 54009,
"CharLevel": 33,
"CharUpgradeLevel": 4,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1001030204,
"BuddyID": 54005,
"CharLevel": 34,
"CharUpgradeLevel": 4,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1001030304,
"BuddyID": 54004,
"CharLevel": 35,
"CharUpgradeLevel": 4,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1001030404,
"BuddyID": 54004,
"CharLevel": 36,
"CharUpgradeLevel": 4,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1001030504,
"BuddyID": 54004,
"CharLevel": 38,
"CharUpgradeLevel": 4,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1009010104,
"BuddyID": 53006,
"CharLevel": 25,
"CharUpgradeLevel": 3,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1009010204,
"BuddyID": 53006,
"CharLevel": 35,
"CharUpgradeLevel": 4,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1009010304,
"BuddyID": 53006,
"CharLevel": 45,
"CharUpgradeLevel": 5,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1009010404,
"BuddyID": 53006,
"CharLevel": 55,
"CharUpgradeLevel": 6,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
},
{
"RobotBuddyID": 1009010504,
"BuddyID": 53006,
"CharLevel": 60,
"CharUpgradeLevel": 6,
"CharStar": 1,
"GHGKBFEKDND": 0,
"ACGEEJMKBBO": 0,
"INPPGCADDPO": 0,
"FMPOMKKHELJ": 0
}
]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -150,7 +150,7 @@
{
"gacha_schedule_id": 5001001,
"gacha_parent_schedule_id": 5001,
"comment": "1.0, Full, Persistent, Bangboo",
"comment": "1.1, Full, Persistent, Bangboo",
"gacha_type": 5,
"cost_item_id": 112, // 邦布券
"start_time": "2024-07-04T06:00:00+08:00",
@ -168,6 +168,7 @@
],
"categories": {
"Standard:Bangboo": {
"item_type": "GACHA_ADDED_ITEM_TYPE_BANGBOO",
"item_ids": [
54002, // 阿全
54005, // 艾米莉安
@ -180,8 +181,10 @@
"category_weight": 1
},
"Event-Exclusive:Bangboo": {
"item_type": "GACHA_ADDED_ITEM_TYPE_BANGBOO",
"item_ids": [
54004, // 巴特勒
54012, // 阿崔巡查
],
"is_promotional_items": true,
"category_weight": 1
@ -199,6 +202,7 @@
],
"categories": {
"Standard:Bangboo": {
"item_type": "GACHA_ADDED_ITEM_TYPE_BANGBOO",
"item_ids": [
53001, // 企鹅布
53003, // 寻宝布
@ -224,6 +228,7 @@
"probability_model_tag": "get-B",
"categories": {
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
12001, // 「月相」-望
12002, // 「月相」-晦
@ -267,6 +272,7 @@
],
"categories": {
"Standard:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"item_ids": [
1021, // 猫又
1101, // 珂蕾妲
@ -278,6 +284,7 @@
"category_weight": 50
},
"Event-Exclusive:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"is_promotional_items": true,
"item_ids": [
1191, // 艾莲
@ -300,6 +307,7 @@
],
"categories": {
"Standard:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"item_ids": [
1011, // 安比
1031, // 妮可
@ -312,6 +320,7 @@
"category_weight": 3525
},
"Event-Exclusive:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"is_promotional_items": true,
"item_ids": [
1111, // 安东
@ -320,6 +329,7 @@
"category_weight": 3525
},
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
13001, // 街头巨星
13002, // 时光切片
@ -352,6 +362,7 @@
"probability_model_tag": "get-B",
"categories": {
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
12001, // 「月相」-望
12002, // 「月相」-晦
@ -398,6 +409,7 @@
],
"categories": {
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
14102, // 钢铁肉垫
14110, // 燃狱齿轮
@ -409,6 +421,7 @@
"category_weight": 25
},
"Event-Exclusive:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
14119, // 深海访客
],
@ -433,6 +446,7 @@
],
"categories": {
"Standard:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"item_ids": [
1011, // 安比
1031, // 妮可
@ -447,6 +461,7 @@
"category_weight": 3750
},
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
13001, // 街头巨星
13002, // 时光切片
@ -468,6 +483,7 @@
"category_weight": 13125
},
"Event-Exclusive:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
13113, // 含羞恶面
13111, // 旋钻机-赤轴
@ -485,6 +501,7 @@
"probability_model_tag": "get-B",
"categories": {
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
12001, // 「月相」-望
12002, // 「月相」-晦
@ -528,6 +545,7 @@
],
"categories": {
"Standard:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"item_ids": [
1021, // 猫又
1101, // 珂蕾妲
@ -539,6 +557,7 @@
"category_weight": 50
},
"Event-Exclusive:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"is_promotional_items": true,
"item_ids": [
1241 // 朱鸢
@ -558,6 +577,7 @@
],
"categories": {
"Standard:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"item_ids": [
1011, // 安比
1061, // 可琳
@ -570,6 +590,7 @@
"category_weight": 3525
},
"Event-Exclusive:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"is_promotional_items": true,
"item_ids": [
1031, // 妮可
@ -578,6 +599,7 @@
"category_weight": 3525
},
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
13001, // 街头巨星
13002, // 时光切片
@ -610,6 +632,7 @@
"probability_model_tag": "get-B",
"categories": {
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
12001, // 「月相」-望
12002, // 「月相」-晦
@ -653,6 +676,7 @@
],
"categories": {
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
14102, // 钢铁肉垫
14110, // 燃狱齿轮
@ -664,6 +688,7 @@
"category_weight": 25
},
"Event-Exclusive:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
14124 // 防暴者VI型
],
@ -683,6 +708,7 @@
],
"categories": {
"Standard:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"item_ids": [
1011, // 安比
1031, // 妮可
@ -697,6 +723,7 @@
"category_weight": 3750
},
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
13007, // 正版变身器
13002, // 时光切片
@ -718,6 +745,7 @@
"category_weight": 13125
},
"Event-Exclusive:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
13103, // 聚宝箱
13112, // 比格气缸
@ -735,6 +763,7 @@
"probability_model_tag": "get-B",
"categories": {
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
12001, // 「月相」-望
12002, // 「月相」-晦
@ -780,6 +809,7 @@
],
"categories": {
"Standard:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"item_ids": [
1021, // 猫又
1101, // 珂蕾妲
@ -791,6 +821,7 @@
"category_weight": 50
},
"Event-Exclusive:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"is_promotional_items": true,
"item_ids": [
1251, // 青衣
@ -810,6 +841,7 @@
],
"categories": {
"Standard:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"item_ids": [
1011, // 安比
1031, // 妮可
@ -823,6 +855,7 @@
"category_weight": 3525
},
"Event-Exclusive:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"is_promotional_items": true,
"item_ids": [
1131, // 苍角
@ -831,6 +864,7 @@
"category_weight": 3525
},
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
13001, // 街头巨星
13002, // 时光切片
@ -864,6 +898,7 @@
"probability_model_tag": "get-B",
"categories": {
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
12001, // 「月相」-望
12002, // 「月相」-晦
@ -907,6 +942,7 @@
],
"categories": {
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
14102, // 钢铁肉垫
14110, // 燃狱齿轮
@ -918,6 +954,7 @@
"category_weight": 25
},
"Event-Exclusive:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
14125, // 玉壶青冰
],
@ -937,6 +974,7 @@
],
"categories": {
"Standard:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"item_ids": [
1011, // 安比
1031, // 妮可
@ -949,6 +987,7 @@
"category_weight": 3750
},
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
13001, // 街头巨星
13002, // 时光切片
@ -971,6 +1010,7 @@
"category_weight": 13125
},
"Event-Exclusive:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
13113, // 含羞恶面
13127, // 维序者-特化型
@ -988,6 +1028,7 @@
"probability_model_tag": "get-B",
"categories": {
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
12001, // 「月相」-望
12002, // 「月相」-晦
@ -1031,6 +1072,7 @@
],
"categories": {
"Standard:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"item_ids": [
1021, // 猫又
1101, // 珂蕾妲
@ -1042,6 +1084,7 @@
"category_weight": 50
},
"Event-Exclusive:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"is_promotional_items": true,
"item_ids": [
1261, // 简
@ -1061,6 +1104,7 @@
],
"categories": {
"Standard:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"item_ids": [
1011, // 安比
1031, // 妮可
@ -1074,6 +1118,7 @@
"category_weight": 3525
},
"Event-Exclusive:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"is_promotional_items": true,
"item_ids": [
1271, // 赛斯
@ -1082,6 +1127,7 @@
"category_weight": 3525
},
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
13001, // 街头巨星
13002, // 时光切片
@ -1115,6 +1161,7 @@
"probability_model_tag": "get-B",
"categories": {
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
12001, // 「月相」-望
12002, // 「月相」-晦
@ -1158,6 +1205,7 @@
],
"categories": {
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
14102, // 钢铁肉垫
14110, // 燃狱齿轮
@ -1169,6 +1217,7 @@
"category_weight": 25
},
"Event-Exclusive:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
14126, // 淬锋钳刺
],
@ -1188,6 +1237,7 @@
],
"categories": {
"Standard:Agent": {
"item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER",
"item_ids": [
1011, // 安比
1031, // 妮可
@ -1200,6 +1250,7 @@
"category_weight": 3750
},
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
13001, // 街头巨星
13002, // 时光切片
@ -1222,6 +1273,7 @@
"category_weight": 13125
},
"Event-Exclusive:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
13127, // 维序者-特化型
13113, // 含羞恶面
@ -1239,6 +1291,7 @@
"probability_model_tag": "get-B",
"categories": {
"Standard:W-Engine": {
"item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON",
"item_ids": [
12001, // 「月相」-望
12002, // 「月相」-晦

View file

@ -1,7 +1,6 @@
use std::collections::{HashMap, HashSet};
use chrono::{prelude::Local, DateTime};
use proto::GachaAddedItemType;
use serde::{Deserialize, Deserializer};
use tracing;
@ -152,13 +151,64 @@ impl DiscountPolicyCollection {
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum GachaAddedItemType {
#[default]
None = 0,
Weapon = 1,
Character = 2,
Bangboo = 3,
}
impl GachaAddedItemType {
pub fn as_str_name(&self) -> &'static str {
match self {
GachaAddedItemType::None => "GACHA_ADDED_ITEM_TYPE_NONE",
GachaAddedItemType::Weapon => "GACHA_ADDED_ITEM_TYPE_WEAPON",
GachaAddedItemType::Character => "GACHA_ADDED_ITEM_TYPE_CHARACTER",
GachaAddedItemType::Bangboo => "GACHA_ADDED_ITEM_TYPE_BANGBOO",
}
}
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"GACHA_ADDED_ITEM_TYPE_NONE" => Some(Self::None),
"GACHA_ADDED_ITEM_TYPE_WEAPON" => Some(Self::Weapon),
"GACHA_ADDED_ITEM_TYPE_CHARACTER" => Some(Self::Character),
"GACHA_ADDED_ITEM_TYPE_BANGBOO" => Some(Self::Bangboo),
_ => None,
}
}
}
impl From<i32> for GachaAddedItemType {
fn from(value: i32) -> Self {
match value {
1 => Self::Weapon,
2 => Self::Character,
3 => Self::Bangboo,
_ => Self::None
}
}
}
impl Into<i32> for GachaAddedItemType {
fn into(self) -> i32 {
match self {
Self::Weapon => 1,
Self::Character => 2,
Self::Bangboo => 3,
Self::None => 0,
}
}
}
#[derive(Debug, Default, Deserialize)]
pub struct GachaCategoryInfo {
#[serde(default)]
pub is_promotional_items: bool,
pub item_ids: Vec<u32>,
pub category_weight: u32,
#[serde(default, deserialize_with = "from_str")]
#[serde(deserialize_with = "from_str")]
pub item_type: GachaAddedItemType,
}

View file

@ -2,7 +2,7 @@ use serde::Deserialize;
use super::BattleEventConfigID;
template_id!(ArchiveBattleQuest u32 id);
template_id!(ArchiveBattleQuest id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -1,6 +1,6 @@
use serde::Deserialize;
template_id!(ArchiveFileQuest u32 id);
template_id!(ArchiveFileQuest id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -1,6 +1,6 @@
use serde::Deserialize;
template_id!(AvatarBase u32 id);
template_id!(AvatarBase id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -2,7 +2,7 @@ use serde::Deserialize;
use super::OnceRewardID;
template_id!(BattleEventConfig u32 id);
template_id!(BattleEventConfig id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -2,7 +2,7 @@ use serde::Deserialize;
use super::BattleEventConfigID;
template_id!(BattleGroupConfig u32 id);
template_id!(BattleGroupConfig id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -0,0 +1,11 @@
use serde::Deserialize;
template_id!(BuddyBase id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct BuddyBaseTemplate {
#[serde(rename = "ID")]
pub id: BuddyBaseID,
pub buddy_type: u32,
}

View file

@ -1,6 +1,6 @@
use serde::Deserialize;
template_id!(HollowConfig u32 id);
template_id!(HollowConfig id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -1,6 +1,6 @@
use serde::Deserialize;
template_id!(HollowQuest u32 id);
template_id!(HollowQuest id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -0,0 +1,10 @@
use serde::Deserialize;
template_id!(Item id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct ItemTemplate {
#[serde(rename = "ID")]
pub id: ItemID,
}

View file

@ -1,6 +1,6 @@
use serde::Deserialize;
template_id!(MainCityBgmConfig u32 id);
template_id!(MainCityBgmConfig id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -1,6 +1,6 @@
use serde::Deserialize;
template_id!(MainCityDefaultObject u32 tag_id);
template_id!(MainCityDefaultObject tag_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -1,6 +1,6 @@
use serde::Deserialize;
template_id!(MainCityObject u32 tag_id);
template_id!(MainCityObject tag_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -1,29 +1,31 @@
use paste::paste;
use std::sync::OnceLock;
use thiserror::Error;
use super::DataLoadError;
#[derive(Debug, Error)]
#[error("template with id {0} not found")]
pub struct TemplateNotFoundError(pub u32);
macro_rules! template_id {
($type_name:ident $underlying_type:ident $id_field:ident) => {
($type_name:ident $id_field:ident) => {
::paste::paste! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ::serde::Deserialize, ::serde::Serialize)]
pub struct [<$type_name ID>]($underlying_type);
pub struct [<$type_name ID>](u32);
impl [<$type_name ID>] {
pub fn new(id: $underlying_type) -> Option<Self> {
if crate::tables::[<$type_name:snake _template_tb>]::iter().any(|tmpl| tmpl.$id_field.value() == id) {
Some(Self(id))
}
else {
None
}
pub fn new(id: u32) -> Result<Self, super::TemplateNotFoundError> {
crate::tables::[<$type_name:snake _template_tb>]::iter()
.any(|tmpl| tmpl.$id_field.value() == id)
.then_some(Self(id)).ok_or(super::TemplateNotFoundError(id))
}
pub fn new_unchecked(id: $underlying_type) -> Self {
pub const fn new_unchecked(id: u32) -> Self {
Self(id)
}
pub fn value(&self) -> $underlying_type {
pub fn value(&self) -> u32 {
self.0
}
@ -76,6 +78,7 @@ macro_rules! template_tables {
template_tables! {
AvatarBaseTemplate;
BuddyBaseTemplate;
UnlockConfigTemplate;
SectionConfigTemplate;
ProcedureConfigTemplate;
@ -94,4 +97,10 @@ template_tables! {
SubAreaDataTemplate;
VariableDataTemplate;
OnceRewardTemplate;
QuickAccessTemplate;
QuickFuncTemplate;
TeleportConfigTemplate;
ItemTemplate;
RobotConfigTemplate;
RobotBuddyConfigTemplate;
}

View file

@ -1,6 +1,6 @@
use serde::Deserialize;
template_id!(OnceReward u32 reward_id);
template_id!(OnceReward reward_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -1,6 +1,6 @@
use serde::Deserialize;
template_id!(PostGirlConfig u32 id);
template_id!(PostGirlConfig id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -1,6 +1,6 @@
use serde::Deserialize;
template_id!(ProcedureConfig u32 procedure_id);
template_id!(ProcedureConfig procedure_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -0,0 +1,11 @@
use serde::Deserialize;
template_id!(QuickAccess quick_func_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct QuickAccessTemplate {
pub quick_access_index: u32,
#[serde(rename = "QuickFuncID")]
pub quick_func_id: QuickAccessID,
}

View file

@ -0,0 +1,10 @@
use serde::Deserialize;
template_id!(QuickFunc btn_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct QuickFuncTemplate {
#[serde(rename = "BtnID")]
pub btn_id: QuickFuncID,
}

View file

@ -0,0 +1,17 @@
use serde::Deserialize;
use super::BuddyBaseID;
template_id!(RobotBuddyConfig robot_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct RobotBuddyConfigTemplate {
#[serde(rename = "RobotBuddyID")]
pub robot_id: RobotBuddyConfigID,
#[serde(rename = "BuddyID")]
pub buddy_id: BuddyBaseID,
pub char_level: u32,
pub char_upgrade_level: u32,
pub char_star: u32,
}

View file

@ -0,0 +1,24 @@
use serde::Deserialize;
use super::{AvatarBaseID, WeaponID};
template_id!(RobotConfig robot_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct RobotConfigTemplate {
#[serde(rename = "RobotID")]
pub robot_id: RobotConfigID,
#[serde(rename = "CharacterID")]
pub character_id: AvatarBaseID,
pub char_level: u32,
pub char_upgrade_level: u32,
pub char_star: u32,
pub skill_levels: Vec<u32>,
pub talent_level: u32,
#[serde(rename = "WeaponID")]
pub weapon_id: WeaponID,
pub weapon_level: u32,
pub weapon_upgrade_level: u32,
pub weapon_refine_level: u32,
}

View file

@ -1,6 +1,6 @@
use serde::Deserialize;
template_id!(SectionConfig u32 section_id);
template_id!(SectionConfig section_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -2,7 +2,7 @@ use std::u32;
use serde::Deserialize;
template_id!(SubAreaData u32 area_id);
template_id!(SubAreaData area_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -0,0 +1,11 @@
use serde::Deserialize;
template_id!(TeleportConfig teleport_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct TeleportConfigTemplate {
#[serde(rename = "TeleportID")]
pub teleport_id: TeleportConfigID,
pub client_visible: u32,
}

View file

@ -2,7 +2,7 @@ use serde::Deserialize;
use super::BattleEventConfigID;
template_id!(TrainingQuest u32 id);
template_id!(TrainingQuest id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -1,6 +1,6 @@
use serde::Deserialize;
template_id!(UnlockConfig i32 id);
template_id!(UnlockConfig id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -2,7 +2,7 @@ use std::u32;
use serde::Deserialize;
template_id!(VariableData u32 id);
template_id!(VariableData id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -1,6 +1,6 @@
use serde::Deserialize;
template_id!(Weapon u32 item_id);
template_id!(Weapon item_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]

View file

@ -1,4 +1,4 @@
use data::tables::AvatarBaseID;
use data::tables::{self, AvatarBaseID};
use proto::{AddAvatarPerformType, AddAvatarScNotify, PlayerSyncScNotify};
use crate::ServerState;
@ -17,7 +17,7 @@ pub async fn add(
let uid = args[0].parse::<u32>()?;
let avatar_id = args[1].parse::<u32>()?;
let Some(avatar_id) = AvatarBaseID::new(avatar_id) else {
let Ok(avatar_id) = AvatarBaseID::new(avatar_id) else {
return Ok(format!("avatar with id {avatar_id} doesn't exist"));
};
@ -55,3 +55,62 @@ pub async fn add(
"successfully added avatar {avatar_id} to player {uid}"
))
}
pub async fn add_all(
args: ArgSlice<'_>,
state: &ServerState,
) -> Result<String, Box<dyn std::error::Error>> {
const USAGE: &str = "Usage: avatar add_all [player_uid]";
if args.len() != 1 {
return Ok(USAGE.to_string());
}
let uid = args[0].parse::<u32>()?;
let Some(player_lock) = state.player_mgr.get_player(uid).await else {
return Ok(String::from("player not found"));
};
let (session_id, avatar_sync, avatar_id_list) = {
let mut player = player_lock.lock().await;
let avatar_id_list = tables::avatar_base_template_tb::iter()
.filter(|tmpl| tmpl.id.value() < 2000 && !player.role_model.has_avatar(tmpl.id))
.map(|tmpl| tmpl.id)
.collect::<Vec<_>>();
avatar_id_list
.iter()
.for_each(|id| player.role_model.add_avatar(*id));
(
player.current_session_id(),
player.role_model.avatar_sync(),
avatar_id_list,
)
};
if let Some(session) = session_id.map(|id| state.session_mgr.get(id)).flatten() {
for id in avatar_id_list {
session
.notify(AddAvatarScNotify {
avatar_id: id.value(),
perform_type: AddAvatarPerformType::ShowPopup.into(),
..Default::default()
})
.await?;
}
session
.notify(PlayerSyncScNotify {
avatar: Some(avatar_sync),
..Default::default()
})
.await?;
} else {
state.player_mgr.save_and_remove(uid).await;
}
Ok(format!("successfully added all avatars to player {uid}"))
}

View file

@ -0,0 +1,125 @@
use data::{gacha::global_gacha_config, tables::ItemID};
use crate::ServerState;
use super::ArgSlice;
pub async fn up(
args: ArgSlice<'_>,
state: &ServerState,
) -> Result<String, Box<dyn std::error::Error>> {
const USAGE: &str = "Usage: gacha up [player_uid] (start a gacha UP setting guide (available for Bangboo pool))";
if args.len() == 0 {
return Ok(USAGE.to_string());
}
let uid = args[0].parse::<u32>()?;
let Some(player_lock) = state.player_mgr.get_player(uid).await else {
return Ok(String::from("player not found"));
};
let gachaconf = global_gacha_config();
let pool_list: Vec<(u32, &String)> = gachaconf
.character_gacha_pool_list
.iter()
.filter(|pool| {
pool.gacha_items.iter().any(|rarity_items| {
rarity_items
.category_guarantee_policy_tags
.iter()
.map(|tag| gachaconf.category_guarantee_policy_map.get(tag).unwrap())
.any(|policy| policy.chooseable)
})
})
.map(|pool| (pool.gacha_schedule_id, &pool.comment))
.collect::<Vec<_>>();
if args.len() == 1 {
return Ok(format!(
"{}\n{}\n{}",
"Choose a target gacha pool first:",
pool_list
.iter()
.map(|(id, comment)| format!("- gacha_schedule_id: {id} ({comment})"))
.collect::<Vec<_>>()
.join("\n"),
"Use 'gacha up [player_uid] [gacha_schedule_id]' to continue."
));
}
let target_pool_id = args[1].parse::<u32>()?;
let accepted_pool_id_list = pool_list.into_iter().map(|(id, _)| id).collect::<Vec<_>>();
if !accepted_pool_id_list.contains(&target_pool_id) {
return Ok(String::from("Gacha Pool not found or not allowed to set UP. Use gacha up [player_uid] to view available pool ids."));
}
let target_pool = gachaconf
.character_gacha_pool_list
.iter()
.filter(|pool| pool.gacha_schedule_id == target_pool_id)
.last()
.unwrap();
let chooseable_items: Vec<(ItemID, &String)> = target_pool
.gacha_items
.iter()
.filter(|rarity_items| {
rarity_items
.category_guarantee_policy_tags
.iter()
.map(|tag| gachaconf.category_guarantee_policy_map.get(tag).unwrap())
.any(|policy| policy.chooseable)
})
.map(|items| {
items
.categories
.iter()
.map(|(category_tag, category)| {
category
.item_ids
.iter()
.map(|id| (ItemID::new_unchecked(id.clone()), category_tag))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
.concat()
})
.collect::<Vec<_>>()
.concat();
if args.len() == 2 {
return Ok(format!(
"{}\n{}\nUse 'gacha up [player_uid] {target_pool_id} [item_id]' to set your UP.",
"Choose the UP item you want:",
chooseable_items
.iter()
.map(|(item_id, category_tag)| format!("- ID {item_id} (category: {category_tag})"))
.collect::<Vec<_>>()
.join("\n")
));
}
let accepted_ids = chooseable_items
.iter()
.map(|(item_id, _)| item_id.value())
.collect::<Vec<_>>();
let item_id = args[2].parse::<u32>()?;
if !accepted_ids.contains(&item_id) {
return Ok(format!("Item ID not found or not included in UP list. Use gacha up [player_uid] {target_pool_id} to view available item ids."));
}
let mut player = player_lock.lock().await;
if player
.gacha_model
.choose_gacha_up(target_pool, &ItemID::new_unchecked(item_id))
{
Ok(format!(
"successfully set your gacha pool: {} (comment: {}) 100% UP to {item_id}. Close & Open Gacha Page to see result.",
target_pool.gacha_schedule_id, target_pool.comment
))
} else {
Ok(format!(
"failed to set your gacha pool: {} (comment: {}) 100% UP to {item_id} (unexpected maybe bug)",
target_pool.gacha_schedule_id, target_pool.comment
))
}
}

View file

@ -17,7 +17,7 @@ pub async fn add_weapon(
let uid = args[0].parse::<u32>()?;
let weapon_id = args[1].parse::<u32>()?;
let Some(weapon_id) = WeaponID::new(weapon_id) else {
let Ok(weapon_id) = WeaponID::new(weapon_id) else {
return Ok(format!("weapon with id {weapon_id} doesn't exist"));
};

View file

@ -6,6 +6,7 @@ use rustyline_async::{Readline, ReadlineEvent, SharedWriter};
use crate::ServerState;
mod avatar;
mod gacha;
mod item;
mod player;
@ -85,8 +86,10 @@ impl CommandManager {
player::avatar "[player_uid] [avatar_id]" "changes player avatar for main city";
player::nickname "[player_uid] [nickname]" "changes player nickname";
player::procedure "[player_uid] [procedure_id]" "changes current beginner procedure id, parameter -1 can be used for skipping it";
avatar::add "[player_uid] [avatar_id]" "gives avatar with specified id to player";
item::add_weapon "[player_uid] [weapon_id]" "gives weapon with specified id to player";
player::kick "[player_uid] [reason]" "kick the specified player (reason is optional)";
avatar::add "[player_uid] [avatar_id]" "gives avatar with specified id to player";
avatar::add_all "[player_uid]" "gives all avatars to player";
item::add_weapon "[player_uid] [weapon_id]" "gives weapon with specified id to player";
gacha::up "[player_uid]" "start a gacha UP setting guide (available for Bangboo pool)";
}
}

View file

@ -17,7 +17,7 @@ pub async fn avatar(
let uid = args[0].parse::<u32>()?;
let avatar_id = args[1].parse::<u32>()?;
let Some(avatar_id) = AvatarBaseID::new(avatar_id) else {
let Ok(avatar_id) = AvatarBaseID::new(avatar_id) else {
return Ok(format!("avatar with id {avatar_id} doesn't exist"));
};
@ -103,7 +103,7 @@ pub async fn procedure(
let procedure_id = args[1].parse::<i32>()?;
let procedure_id = match procedure_id {
1.. => ProcedureConfigID::new(procedure_id as u32),
1.. => ProcedureConfigID::new(procedure_id as u32).ok(),
_ => None,
};

View file

@ -1,8 +1,8 @@
use crate::logic::{EOperator, ESystem};
use crate::logic::{game::LogicError, EOperator, ESystem};
use super::*;
use data::tables;
use data::tables::{self, PostGirlConfigID, QuickFuncID};
pub async fn on_get_tips_info(
_session: &NetSession,
@ -21,23 +21,34 @@ pub async fn on_get_client_systems_info(
player: &mut Player,
_req: GetClientSystemsInfoCsReq,
) -> NetResult<GetClientSystemsInfoScRsp> {
let mut post_girl_data = PostGirlData {
post_girl_list: tables::post_girl_config_template_tb::iter()
.map(|template| PostGirlItem {
template_id: template.id.value(),
unlock_time: 1720052644,
})
.collect(),
..Default::default()
};
if let Some(post_girl_id) = player.basic_data_model.selected_post_girl_id {
post_girl_data
.selected_post_girl_id_list
.push(post_girl_id.value());
}
Ok(GetClientSystemsInfoScRsp {
retcode: Retcode::RetSucc.into(),
info: Some(ClientSystemsInfo {
post_girl_data: Some(PostGirlData {
selected_post_girl_id_list: tables::post_girl_config_template_tb::iter()
.map(|template| template.id.value())
.collect(),
post_girl_list: tables::post_girl_config_template_tb::iter()
.map(|template| PostGirlItem {
template_id: template.id.value(),
unlock_time: 1000,
})
post_girl_data: Some(post_girl_data),
unlock_data: Some(player.lock_model.to_client()),
calling_card_data: Some(CallingCardData::default()),
teleport_data: Some(TeleportData {
unlock_id_list: tables::teleport_config_template_tb::iter()
.filter(|template| template.client_visible > 0)
.map(|template| template.teleport_id.value() as i32)
.collect(),
..Default::default()
}),
unlock_data: Some(player.lock_model.to_client()),
hbhfjgbahgf: Some(Aboegnnepmi::default()),
..Default::default()
}),
})
@ -183,3 +194,62 @@ pub async fn on_interact_with_scene_object(
retcode: Retcode::RetSucc.into(),
})
}
pub async fn on_mod_quick_menu(
session: &NetSession,
player: &mut Player,
req: ModQuickMenuCsReq,
) -> NetResult<ModQuickMenuScRsp> {
req.quick_access_data_list.iter().for_each(|data| {
player
.lock_model
.mod_quick_access(data.quick_access_index, QuickFuncID::new(data.btn_id).ok())
});
session
.notify(PlayerSyncScNotify {
client_systems_sync: Some(ClientSystemsSync {
quick_access_data_list: player.lock_model.quick_access_to_client(),
..Default::default()
}),
..Default::default()
})
.await?;
Ok(ModQuickMenuScRsp {
retcode: Retcode::RetSucc.into(),
..Default::default()
})
}
pub async fn on_change_post_girl(
session: &NetSession,
player: &mut Player,
req: ChangePostGirlCsReq,
) -> NetResult<ChangePostGirlScRsp> {
let post_girl_id = *req
.new_selected_post_girl_id_list
.get(0)
.ok_or(Retcode::RetFail)?;
let post_girl_id = PostGirlConfigID::new(post_girl_id).map_err(LogicError::from)?;
player.basic_data_model.selected_post_girl_id = Some(post_girl_id);
session
.notify(PlayerSyncScNotify {
client_systems_sync: Some(ClientSystemsSync {
post_girl_data: Some(PostGirlSync {
selected_post_girl_id_list: vec![post_girl_id.value()],
..Default::default()
}),
..Default::default()
}),
..Default::default()
})
.await?;
Ok(ChangePostGirlScRsp {
retcode: Retcode::RetSucc.into(),
..Default::default()
})
}

View file

@ -1,19 +1,18 @@
use std::{
cmp::min,
collections::{
hash_map::Entry::{Occupied, Vacant},
HashSet,
},
};
use data::{
gacha::{gacha_config::*, global_gacha_config},
tables::{AvatarBaseID, WeaponID},
gacha::{
gacha_config::{CharacterGachaPool, GachaAddedItemType},
global_gacha_config,
},
tables::{AvatarBaseID, ItemID, WeaponID},
};
use proto::{GainItemInfo, GetGachaDataScRsp};
use super::*;
use crate::{
handlers::core::NetError,
logic::{item::ItemModel, role::RoleModel},
};
use chrono::{DateTime, Local};
use proto::*;
pub async fn on_get_gacha_data(
_session: &NetSession,
@ -32,7 +31,7 @@ pub async fn on_get_gacha_data(
Ok(GetGachaDataScRsp {
retcode: Retcode::RetSucc.into(),
gacha_type: req.gacha_type,
gacha_data: Some(generate_all_gacha_info(_player, &Local::now())),
gacha_data: Some(_player.gacha_model.to_client(&Local::now())),
})
}
}
@ -51,101 +50,58 @@ pub async fn on_do_gacha(
&gachaconf.character_gacha_pool_list,
&req.gacha_parent_schedule_id,
&pull_time,
);
if let None = target_pool {
tracing::info!(
"refuse gacha because: pool of parent_schedule_id {} not found",
req.gacha_parent_schedule_id
);
return Ok(DoGachaScRsp {
retcode: Retcode::RetFail.into(),
..Default::default()
});
};
let target_pool = target_pool.unwrap();
)?;
// tracing::info!("cost_item_count: {}", req.cost_item_count);
let mut pull_count = if req.cost_item_count > 1 { 10 } else { 1 };
let cost_count = pull_count;
if pull_count == 10 {
let discount_tag = &gachaconf.common_properties.ten_pull_discount_tag;
if target_pool.discount_policy_tags.contains(&discount_tag) {
let gacha_bin = &mut gacha_model.gacha_bin;
let status_bin = gacha_bin
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap();
let discount_policy = gachaconf
.discount_policies
.ten_pull_discount_map
.get(discount_tag)
.unwrap();
let usage = status_bin.discount_usage_map.get_mut(discount_tag).unwrap();
if *usage < discount_policy.use_limit {
*usage += 1;
// cost_count = discount_policy.discounted_prize;
}
}
}
if cost_count != req.cost_item_count {
let pull_count = if req.cost_item_count > 1 { 10 } else { 1 };
let mut cost_count = gacha_model.get_actual_cost_count(target_pool, &pull_count);
if pull_count != req.cost_item_count {
tracing::info!(
"refuse gacha because: expected cost item {cost_count}, found {}",
req.cost_item_count
);
return Ok(DoGachaScRsp {
retcode: Retcode::RetFail.into(),
..Default::default()
});
return Err(NetError::from(Retcode::RetFail));
} else {
// TODO: cost resource
}
let mut gain_item_list: Vec<GainItemInfo> = vec![];
while pull_count > 0 {
while cost_count > 0 {
let pull_result = gacha_model.perform_pull_pool(&pull_time, target_pool);
let extra_item_bin = pull_result.extra_item_bin.clone().unwrap();
let uid = match GachaAddedItemType::try_from(pull_result.item_type) {
Ok(enum_val) => match enum_val {
GachaAddedItemType::Weapon => match WeaponID::new(pull_result.obtained_item_id) {
Some(id) => item_model.add_weapon(id).value(),
None => 0,
},
GachaAddedItemType::Character => {
match AvatarBaseID::new(pull_result.obtained_item_id) {
Some(id) => {
role_model.add_avatar(id);
0
}
None => 0,
}
}
_ => 0,
},
Err(_err) => 0,
};
if extra_item_bin.extra_item_id != 0 {
item_model.add_resource(
extra_item_bin.extra_item_id,
extra_item_bin.extra_item_count,
let uid = add_item(
role_model,
item_model,
&pull_result.obtained_item_id,
&pull_result.item_type,
)?;
let (mut extra_item_id, mut extra_item_count) = (0, 0);
if let Some(extra_resources) = &pull_result.extra_resources {
(extra_item_id, extra_item_count) = (
extra_resources.extra_item_id.value(),
extra_resources.extra_item_count.clone(),
);
item_model.add_resource(extra_item_id, extra_item_count);
}
gain_item_list.push(GainItemInfo {
item_id: pull_result.obtained_item_id,
extra_item_id: extra_item_bin.extra_item_id,
extra_item_count: extra_item_bin.extra_item_count,
item_id: pull_result.obtained_item_id.value(),
extra_item_id,
extra_item_count,
uid,
num: 1,
..GainItemInfo::default()
});
pull_count -= 1;
gacha_model.gacha_bin.gacha_records.push(pull_result);
cost_count -= 1;
gacha_model.gacha_records.push(pull_result);
}
_session
.notify(construct_sync(role_model, item_model))
.await?;
Ok(DoGachaScRsp {
retcode: Retcode::RetSucc.into(),
gain_item_list,
gacha_data: Some(generate_all_gacha_info(_player, &pull_time)),
gacha_data: Some(gacha_model.to_client(&pull_time)),
cost_item_count: req.cost_item_count,
})
}
@ -156,123 +112,31 @@ pub async fn on_gacha_free_agent(
req: GachaFreeAgentCsReq,
) -> NetResult<GachaFreeAgentScRsp> {
let gachaconf = global_gacha_config();
let gacha_model = &mut _player.gacha_model;
let role_model = &mut _player.role_model;
let item_model = &mut _player.item_model;
let pull_time = Local::now();
let target_pool = get_gacha_pool(
&gachaconf.character_gacha_pool_list,
&req.gacha_parent_schedule_id,
&pull_time,
);
if let None = target_pool {
tracing::info!(
"refuse free agent because: pool of parent_schedule_id {} not found",
req.gacha_parent_schedule_id
);
return Ok(GachaFreeAgentScRsp {
retcode: Retcode::RetFail.into(),
..Default::default()
});
};
let target_pool = target_pool.unwrap();
let gacha_bin = &mut _player.gacha_model.gacha_bin;
let sharing_guarantee_category_tag = &target_pool.sharing_guarantee_info_category;
let status_bin = gacha_bin
.gacha_status_map
.get_mut(sharing_guarantee_category_tag)
.unwrap();
)?;
let mut free_select_policy: Option<&FreeSelectItem> = None;
let mut free_select_progress: u32 = 0;
let mut free_select_required_pull: u32 = 0;
for discount_policy_tag in target_pool.discount_policy_tags.iter() {
if gachaconf
.discount_policies
.free_select_map
.contains_key(discount_policy_tag)
{
let policy = gachaconf
.discount_policies
.free_select_map
.get(discount_policy_tag)
.unwrap();
let free_select_demand_idx = usize::try_from(
*(status_bin
.discount_usage_map
.get(&policy.free_select_usage_record_tag)
.unwrap()),
)
.unwrap();
if policy.milestones.len() <= free_select_demand_idx {
continue;
}
let free_select_actual_progress = status_bin
.discount_usage_map
.get(&policy.free_select_progress_record_tag)
.unwrap();
free_select_policy = Some(policy);
free_select_required_pull = policy
.milestones
.get(free_select_demand_idx)
.unwrap()
.to_owned();
free_select_progress = min(free_select_required_pull, *free_select_actual_progress);
}
let item_id = ItemID::new(req.avatar_id);
if item_id.is_err() {
return Err(NetError::from(Retcode::RetFail));
}
let item_id = item_id.unwrap();
if let None = free_select_policy {
tracing::info!(
"refuse free agent because: pool of parent_schedule_id {} hasn't defined free agent discount yet (or used up chance)",
req.gacha_parent_schedule_id
);
return Ok(GachaFreeAgentScRsp {
retcode: Retcode::RetFail.into(),
..Default::default()
});
} else if free_select_progress < free_select_required_pull {
tracing::info!(
"refuse free agent because: use pulled {free_select_progress} (after last free agent) in parent_schedule_id {}, required {free_select_required_pull}",
req.gacha_parent_schedule_id
);
return Ok(GachaFreeAgentScRsp {
retcode: Retcode::RetFail.into(),
..Default::default()
});
}
let free_select_policy = free_select_policy.unwrap();
let mut has_demanded_item = false;
for rarity_items in target_pool.gacha_items.iter() {
if rarity_items.rarity != free_select_policy.rarity {
continue;
}
for (category_tag, category) in rarity_items.categories.iter() {
if !free_select_policy.category_tags.contains(category_tag) {
continue;
}
has_demanded_item |= category.item_ids.contains(&req.avatar_id);
}
}
if !has_demanded_item {
tracing::info!(
"refuse free agent because: pool of parent_schedule_id {} doesn't have demanded item {}",
req.gacha_parent_schedule_id, req.avatar_id
);
return Ok(GachaFreeAgentScRsp {
retcode: Retcode::RetFail.into(),
..Default::default()
});
let item_type = gacha_model.request_free_agent(target_pool, &item_id);
if item_type == GachaAddedItemType::None {
return Err(NetError::from(Retcode::RetFail));
}
let _ = add_item(role_model, item_model, &item_id, &item_type);
role_model.add_avatar(AvatarBaseID::new(req.avatar_id).unwrap());
(*status_bin
.discount_usage_map
.get_mut(&free_select_policy.free_select_usage_record_tag)
.unwrap()) += 1;
(*status_bin
.discount_usage_map
.get_mut(&free_select_policy.free_select_progress_record_tag)
.unwrap()) -= free_select_required_pull;
_session
.notify(construct_sync(role_model, item_model))
.await?;
Ok(GachaFreeAgentScRsp {
retcode: Retcode::RetSucc.into(),
})
@ -290,330 +154,82 @@ pub async fn on_choose_gacha_up(
&gachaconf.character_gacha_pool_list,
&req.gacha_parent_schedule_id,
&pull_time,
);
if let None = target_pool {
return Ok(ChooseGachaUpScRsp {
retcode: Retcode::RetFail.into(),
..Default::default()
});
};
let target_pool = target_pool.unwrap();
)?;
for rarity_items in target_pool.gacha_items.iter() {
if rarity_items.rarity != gachaconf.common_properties.s_item_rarity {
continue;
}
for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
let category_guarantee_policy = gachaconf
.category_guarantee_policy_map
.get(guarantee_policy_tag)
.unwrap();
if !category_guarantee_policy.chooseable {
continue;
}
let mut up_category: Option<&String> = None;
for (category_tag, category) in rarity_items.categories.iter() {
if category.item_ids.contains(&req.item_id) {
up_category = Some(category_tag);
break;
}
}
if let None = up_category {
return Ok(ChooseGachaUpScRsp {
retcode: Retcode::RetFail.into(),
..Default::default()
});
};
let up_category = up_category.unwrap();
let progress_bin = gacha_model
.gacha_bin
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap()
.rarity_status_map
.get_mut(&rarity_items.rarity)
.unwrap();
match progress_bin
.categories_chosen_guarantee_item_map
.entry(guarantee_policy_tag.clone())
{
Occupied(mut occupied_entry) => {
occupied_entry.insert(req.item_id);
}
Vacant(vacant_entry) => {
vacant_entry.insert(req.item_id);
}
};
match progress_bin
.categories_chosen_guarantee_category_map
.entry(up_category.clone())
{
Occupied(mut occupied_entry) => {
occupied_entry.insert(up_category.clone());
}
Vacant(vacant_entry) => {
vacant_entry.insert(up_category.clone());
}
};
return Ok(ChooseGachaUpScRsp {
retcode: Retcode::RetSucc.into(),
..Default::default()
});
}
let item_id = ItemID::new(req.item_id);
if item_id.is_err() {
return Err(NetError::from(Retcode::RetFail));
}
let item_id = item_id.unwrap();
Ok(ChooseGachaUpScRsp {
retcode: Retcode::RetFail.into(),
retcode: if gacha_model.choose_gacha_up(target_pool, &item_id) {
Retcode::RetSucc.into()
} else {
Retcode::RetFail.into()
},
..Default::default()
})
}
fn generate_gacha_info_from_pool(
gacha_bin: &GachaModelBin,
target_pool: &CharacterGachaPool,
common_properties: &GachaCommonProperties,
) -> Gacha {
let gachaconf = data::gacha::global_gacha_config();
let sharing_guarantee_category_tag = &target_pool.sharing_guarantee_info_category;
let status_bin = gacha_bin
.gacha_status_map
.get(sharing_guarantee_category_tag)
.unwrap();
let pity_s = status_bin
.rarity_status_map
.get(&common_properties.s_item_rarity)
.unwrap()
.pity;
let pity_a = status_bin
.rarity_status_map
.get(&common_properties.a_item_rarity)
.unwrap()
.pity;
let mut discount_ten_roll_prize: u32 = 0;
let mut discount_avaliable_num: u32 = 0;
let mut advanced_s_guarantee: u32 = 0;
let mut free_select_progress: u32 = 0;
let mut free_select_required_pull: u32 = 0;
let mut free_select_policy: Option<&FreeSelectItem> = None;
for discount_policy_tag in target_pool.discount_policy_tags.iter() {
if common_properties.newcomer_advanced_s_tag == *discount_policy_tag {
let policy = gachaconf
.discount_policies
.advanced_guarantee_map
.get(discount_policy_tag)
.unwrap();
if status_bin
.discount_usage_map
.get(discount_policy_tag)
.unwrap()
< &policy.use_limit
{
advanced_s_guarantee = policy.guarantee_pity - pity_s + 1;
}
} else if common_properties.ten_pull_discount_tag == *discount_policy_tag {
let policy = gachaconf
.discount_policies
.ten_pull_discount_map
.get(discount_policy_tag)
.unwrap();
let discount_usage = status_bin
.discount_usage_map
.get(discount_policy_tag)
.unwrap();
if discount_usage < &policy.use_limit {
discount_ten_roll_prize = policy.discounted_prize;
discount_avaliable_num = policy.use_limit - discount_usage;
}
} else if gachaconf
.discount_policies
.free_select_map
.contains_key(discount_policy_tag)
{
let policy = gachaconf
.discount_policies
.free_select_map
.get(discount_policy_tag)
.unwrap();
let free_select_demand_idx = usize::try_from(
*(status_bin
.discount_usage_map
.get(&policy.free_select_usage_record_tag)
.unwrap()),
)
.unwrap();
if policy.milestones.len() <= free_select_demand_idx {
continue;
}
let free_select_actual_progress = status_bin
.discount_usage_map
.get(&policy.free_select_progress_record_tag)
.unwrap();
free_select_policy = Some(policy);
free_select_required_pull = policy
.milestones
.get(free_select_demand_idx)
.unwrap()
.to_owned();
free_select_progress = min(free_select_required_pull, *free_select_actual_progress);
}
}
let mut up_s_item_list: Vec<u32> = vec![];
let mut up_a_item_list: Vec<u32> = vec![];
let mut free_select_item_list: Vec<u32> = vec![];
let mut chooseable_up_list: Vec<u32> = vec![];
let mut chosen_up_item: u32 = 0;
let mut s_guarantee: u32 = 0;
let mut a_guarantee: u32 = 0;
for rarity_items in target_pool.gacha_items.iter() {
let mut chooseable_up_included_category_tags: Option<&HashSet<String>> = None;
let mut chooseable_policy_tag: Option<&String> = None;
for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
let category_guarantee_policy = gachaconf
.category_guarantee_policy_map
.get(guarantee_policy_tag)
.unwrap();
if !category_guarantee_policy.chooseable {
continue;
}
chooseable_policy_tag = Some(guarantee_policy_tag);
chooseable_up_included_category_tags =
Some(&category_guarantee_policy.included_category_tags);
if let Some(item) = status_bin
.rarity_status_map
.get(&rarity_items.rarity)
.unwrap()
.categories_chosen_guarantee_item_map
.get(guarantee_policy_tag)
{
chosen_up_item = item.clone();
}
}
for (category_tag, category) in rarity_items.categories.iter() {
let probability_model = gachaconf
.probability_model_map
.get(&rarity_items.probability_model_tag)
.unwrap();
let maximum_pity = &probability_model.maximum_guarantee_pity;
if rarity_items.rarity == common_properties.s_item_rarity {
if category.is_promotional_items {
up_s_item_list = category.item_ids.clone();
}
// tracing::info!("pity_s: {pity_s}");
// thread 'tokio-runtime-worker' panicked at nap_gameserver\src\handlers\gacha.rs:369:31:
// attempt to subtract with overflow
s_guarantee = maximum_pity - min(pity_s, maximum_pity.clone()) + 1;
}
if rarity_items.rarity == common_properties.a_item_rarity {
if category.is_promotional_items {
up_a_item_list = category.item_ids.clone();
}
// tracing::info!("pity_a: {pity_a}");
a_guarantee = maximum_pity - min(pity_a, maximum_pity.clone()) + 1;
}
if let Some(val) = free_select_policy {
if val.rarity == rarity_items.rarity && val.category_tags.contains(category_tag) {
free_select_item_list.append(&mut category.item_ids.clone());
}
}
if let Some(tags) = chooseable_up_included_category_tags {
if tags.contains(category_tag) {
chooseable_up_list.append(&mut category.item_ids.clone());
}
}
}
if let Some(_priority_policy_tag) = chooseable_policy_tag {
// if let Some(item) = status_bin
// .rarity_status_map
// .get(&rarity_items.rarity)
// .unwrap()
// .categories_chosen_guarantee_item_map
// .get(priority_policy_tag)
// {
if rarity_items.rarity == gachaconf.common_properties.s_item_rarity {
up_s_item_list = vec![];
} else if rarity_items.rarity == gachaconf.common_properties.a_item_rarity {
up_a_item_list = vec![];
}
// }
}
}
let need_item_info_list: Vec<NeedItemInfo> = vec![NeedItemInfo {
need_item_id: target_pool.cost_item_id,
need_item_count: 1,
}];
let mut result = Gacha {
gacha_schedule_id: target_pool.gacha_schedule_id,
gacha_parent_schedule_id: target_pool.gacha_parent_schedule_id,
gacha_type: target_pool.gacha_type,
start_timestamp: target_pool.start_time.timestamp(),
end_timestamp: target_pool.end_time.timestamp(),
discount_avaliable_num,
discount_ten_roll_prize,
advanced_s_guarantee,
s_guarantee,
a_guarantee,
need_item_info_list,
free_select_progress,
free_select_required_pull,
free_select_item_list,
chosen_up_item,
// nammdglepbk: 563,
// hgmcofcjmbg: 101,
// akggbhgkifd: chooseable_up_list.clone(),
chooseable_up_list,
..Gacha::default()
};
if up_s_item_list.len() > 0 {
result.up_s_item_list = up_s_item_list;
}
if up_a_item_list.len() > 0 {
result.up_a_item_list = up_a_item_list;
}
result
}
fn generate_all_gacha_info(_player: &Player, now: &DateTime<Local>) -> GachaData {
let gachaconf = global_gacha_config();
let gacha_bin = &_player.gacha_model.gacha_bin;
let mut gacha_list: Vec<Gacha> = vec![];
for target_pool in gachaconf.character_gacha_pool_list.iter() {
if target_pool.is_still_open(now) {
gacha_list.push(generate_gacha_info_from_pool(
&gacha_bin,
target_pool,
&gachaconf.common_properties,
));
}
}
// tracing::info!("gacha_list: {:?}", gacha_list);
GachaData {
random_number: 6167,
gacha_pool: Some(GachaPool { gacha_list }),
..GachaData::default()
}
}
fn get_gacha_pool<'conf>(
character_gacha_pool_list: &'conf Vec<CharacterGachaPool>,
gacha_parent_schedule_id: &u32,
pull_time: &DateTime<Local>,
) -> Option<&'conf CharacterGachaPool> {
) -> NetResult<&'conf CharacterGachaPool> {
for target_pool in character_gacha_pool_list.iter() {
if &target_pool.gacha_parent_schedule_id == gacha_parent_schedule_id
&& target_pool.is_still_open(pull_time)
{
return Some(target_pool);
return Ok(target_pool);
}
}
None
tracing::info!(
"refuse gacha op because: pool of parent_schedule_id {} not found or isn't in open time",
gacha_parent_schedule_id
);
Err(NetError::from(Retcode::RetFail))
}
/// Return is item UID (weapon specific)
fn add_item(
role_model: &mut RoleModel,
item_model: &mut ItemModel,
item_id: &ItemID,
item_type: &GachaAddedItemType,
) -> NetResult<u32> {
match item_type {
GachaAddedItemType::Character => match AvatarBaseID::new(item_id.value()) {
Ok(avatar_id) => {
role_model.add_avatar(avatar_id);
Ok(0)
}
Err(_) => {
tracing::info!("add item failed for avatar id {item_id}");
Err(NetError::from(Retcode::RetFail))
}
},
GachaAddedItemType::Weapon => match WeaponID::new(item_id.value()) {
Ok(weapon_id) => Ok(item_model.add_weapon(weapon_id).value()),
Err(_) => {
tracing::info!("add item failed for weapon id {item_id}");
Err(NetError::from(Retcode::RetFail))
}
},
GachaAddedItemType::Bangboo => Ok(0),
_ => {
tracing::info!(
"add item failed due to undefined item type (from {item_id}) in configuration"
);
Err(NetError::from(Retcode::RetFail))
}
}
}
fn construct_sync(role_model: &RoleModel, item_model: &ItemModel) -> PlayerSyncScNotify {
PlayerSyncScNotify {
avatar: Some(role_model.avatar_sync()),
item_sync: Some(item_model.item_sync()),
..Default::default()
}
}

View file

@ -1,5 +1,7 @@
use data::tables::AvatarBaseID;
use crate::logic::game::LogicError;
use super::*;
pub async fn on_get_item_data(
@ -23,7 +25,7 @@ pub async fn on_weapon_dress(
req: WeaponDressCsReq,
) -> NetResult<WeaponDressScRsp> {
player.dress_weapon(
AvatarBaseID::new(req.avatar_id).ok_or(Retcode::RetFail)?,
AvatarBaseID::new(req.avatar_id).map_err(LogicError::from)?,
req.weapon_uid.into(),
)?;
@ -45,7 +47,7 @@ pub async fn on_weapon_un_dress(
player: &mut Player,
req: WeaponUnDressCsReq,
) -> NetResult<WeaponUnDressScRsp> {
let avatar_id = AvatarBaseID::new(req.avatar_id).ok_or(Retcode::RetFail)?;
let avatar_id = AvatarBaseID::new(req.avatar_id).map_err(LogicError::from)?;
let avatar = player
.role_model
.avatar_list

View file

@ -126,6 +126,9 @@ req_handlers! {
gacha::DoGacha;
gacha::ChooseGachaUp;
gacha::GachaFreeAgent;
player::ModNickname;
client_systems::ModQuickMenu;
client_systems::ChangePostGirl;
}
notify_handlers! {

View file

@ -22,7 +22,7 @@ pub async fn on_create_role(
player: &mut Player,
req: CreateRoleCsReq,
) -> NetResult<CreateRoleScRsp> {
let avatar_id = AvatarBaseID::new(req.avatar_id).ok_or(Retcode::RetFail)?;
let avatar_id = AvatarBaseID::new(req.avatar_id).map_err(LogicError::from)?;
let GameInstance::Fresh(fresh_game) = &mut player.game_instance else {
return Err(NetError::from(Retcode::RetFail));
@ -58,6 +58,25 @@ pub async fn on_get_player_transaction(
})
}
pub async fn on_mod_nickname(
session: &NetSession,
_player: &mut Player,
_req: ModNicknameCsReq,
) -> NetResult<ModNicknameScRsp> {
_player.basic_data_model.nick_name = Some(_req.nick_name.to_string());
session
.notify(PlayerSyncScNotify {
basic_info: Some(_player.basic_data_model.player_basic_info()),
..Default::default()
})
.await?;
Ok(ModNicknameScRsp {
retcode: Retcode::RetSucc.into(),
..Default::default()
})
}
pub async fn on_keep_alive(
_session: &NetSession,
_player: &mut Player,

View file

@ -88,7 +88,7 @@ pub async fn on_begin_archive_battle_quest(
player: &mut Player,
req: BeginArchiveBattleQuestCsReq,
) -> NetResult<BeginArchiveBattleQuestScRsp> {
let quest_id = ArchiveBattleQuestID::new(req.quest_id).ok_or(Retcode::RetFail)?;
let quest_id = ArchiveBattleQuestID::new(req.quest_id).map_err(LogicError::from)?;
player.game_instance = GameInstance::Hollow(
HollowGame::create_archive_battle(

View file

@ -47,7 +47,7 @@ pub async fn on_advance_beginner_procedure(
};
let procedure_id =
ProcedureConfigID::new(req.procedure_id as u32).ok_or(Retcode::RetFail)?;
ProcedureConfigID::new(req.procedure_id as u32).map_err(LogicError::from)?;
fresh_game
.procedure_mgr
@ -168,7 +168,7 @@ pub async fn on_start_trial_fighting_mission(
player: &mut Player,
req: StartTrialFightingMissionCsReq,
) -> NetResult<StartTrialFightingMissionScRsp> {
let quest_id = TrainingQuestID::new(req.quest_id).ok_or(Retcode::RetFail)?;
let quest_id = TrainingQuestID::new(req.quest_id).map_err(LogicError::from)?;
player.game_instance = GameInstance::Hollow(
HollowGame::create_training_game(quest_id, ELocalPlayType::TrainingRoomFight, &req.avatars)
@ -273,7 +273,7 @@ pub async fn on_enter_section(
player: &mut Player,
req: EnterSectionCsReq,
) -> NetResult<EnterSectionScRsp> {
let section_id = SectionConfigID::new(req.section_id).ok_or(Retcode::RetFail)?;
let section_id = SectionConfigID::new(req.section_id).map_err(LogicError::from)?;
player.main_city_model.switch_section(section_id);
player.init_frontend_game()?;
@ -311,7 +311,7 @@ pub async fn on_start_hollow_quest(
) -> NetResult<StartHollowQuestScRsp> {
use crate::logic::{TimePeriodType, WeatherType};
let quest_id = HollowQuestID::new(req.quest_id).ok_or(Retcode::RetFail)?;
let quest_id = HollowQuestID::new(req.quest_id).map_err(LogicError::from)?;
let quest_type = EHollowQuestType::from(quest_id.template().hollow_quest_type);
match quest_type {

View file

@ -2,17 +2,17 @@ use std::collections::HashMap;
use crate::logic::BuddyTeamType;
use super::unit::{AvatarUnit, BuddyUnit};
use super::unit::{AvatarUnit, AvatarUnitID, BuddyUnit, BuddyUnitID};
pub struct TeamDataItem {
pub avatar_member_list: Vec<AvatarUnit>,
pub equipped_buddy_list: Vec<BuddyUnit>,
}
pub struct BuddyParam(pub u32, pub BuddyTeamType);
pub struct BuddyParam(pub BuddyUnitID, pub BuddyTeamType);
impl TeamDataItem {
pub fn new(avatars: &[u32], buddy_params: &[BuddyParam]) -> Self {
pub fn new(avatars: &[AvatarUnitID], buddy_params: &[BuddyParam]) -> Self {
Self {
avatar_member_list: avatars
.iter()

View file

@ -1,17 +1,36 @@
use std::collections::HashMap;
use crate::logic::BaseProperty;
use data::tables::{AvatarBaseID, RobotConfigID};
use proto::AvatarUnitInfo;
pub struct AvatarUnit {
pub avatar_id: u32,
pub avatar_id: AvatarUnitID,
pub mp_property_override: HashMap<BaseProperty, i32>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AvatarUnitID {
Base(AvatarBaseID),
Robot(RobotConfigID),
}
impl AvatarUnitID {
pub fn base_id(&self) -> AvatarBaseID {
match *self {
Self::Base(id) => id,
Self::Robot(id) => id.template().character_id,
}
}
}
impl AvatarUnit {
pub fn to_client(&self) -> AvatarUnitInfo {
AvatarUnitInfo {
avatar_id: self.avatar_id,
avatar_id: match self.avatar_id {
AvatarUnitID::Base(id) => id.value(),
AvatarUnitID::Robot(id) => id.value(),
},
mp_property_override_map: self
.mp_property_override
.iter()

View file

@ -1,19 +1,38 @@
use std::collections::HashMap;
use data::tables::{BuddyBaseID, RobotBuddyConfigID};
use proto::BuddyUnitInfo;
use crate::logic::{BaseProperty, BuddyTeamType};
pub struct BuddyUnit {
pub buddy_id: u32,
pub buddy_id: BuddyUnitID,
pub buddy_team: BuddyTeamType,
pub override_property_map: HashMap<BaseProperty, i32>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BuddyUnitID {
Base(BuddyBaseID),
Robot(RobotBuddyConfigID),
}
impl BuddyUnitID {
pub fn base_id(&self) -> BuddyBaseID {
match *self {
Self::Base(id) => id,
Self::Robot(id) => id.template().buddy_id,
}
}
}
impl BuddyUnit {
pub fn to_client(&self) -> BuddyUnitInfo {
BuddyUnitInfo {
buddy_id: self.buddy_id,
buddy_id: match self.buddy_id {
BuddyUnitID::Base(id) => id.value(),
BuddyUnitID::Robot(id) => id.value(),
},
r#type: self.buddy_team.to_protocol().into(),
mp_property_override_map: self
.override_property_map

View file

@ -1,5 +1,5 @@
mod avatar;
mod buddy;
pub use avatar::AvatarUnit;
pub use buddy::BuddyUnit;
pub use avatar::{AvatarUnit, AvatarUnitID};
pub use buddy::{BuddyUnit, BuddyUnitID};

View file

@ -0,0 +1,440 @@
use data::gacha::gacha_config::*;
use data::gacha::global_gacha_config;
use data::tables::ItemID;
use chrono::{DateTime, Local};
use proto::{Gacha, GachaData, GachaPool, NeedItemInfo};
use std::{
cmp::min,
collections::{
hash_map::Entry::{Occupied, Vacant},
HashSet,
},
};
use super::GachaModel;
impl GachaModel {
pub fn to_client(&self, now: &DateTime<Local>) -> GachaData {
let gachaconf = global_gacha_config();
let mut gacha_list: Vec<Gacha> = vec![];
for target_pool in gachaconf.character_gacha_pool_list.iter() {
if target_pool.is_still_open(now) {
gacha_list.push(
self.generate_gacha_info_from_pool(target_pool, &gachaconf.common_properties),
);
}
}
// tracing::info!("gacha_list: {:?}", gacha_list);
GachaData {
random_number: 6167,
gacha_pool: Some(GachaPool { gacha_list }),
..GachaData::default()
}
}
fn generate_gacha_info_from_pool(
&self,
target_pool: &CharacterGachaPool,
common_properties: &GachaCommonProperties,
) -> Gacha {
let gachaconf = data::gacha::global_gacha_config();
let sharing_guarantee_category_tag = &target_pool.sharing_guarantee_info_category;
let status_bin = self
.gacha_status_map
.get(sharing_guarantee_category_tag)
.unwrap();
let pity_s = status_bin
.rarity_status_map
.get(&common_properties.s_item_rarity)
.unwrap()
.pity;
let pity_a = status_bin
.rarity_status_map
.get(&common_properties.a_item_rarity)
.unwrap()
.pity;
let mut discount_ten_roll_prize: u32 = 0;
let mut discount_avaliable_num: u32 = 0;
let mut advanced_s_guarantee: u32 = 0;
let mut free_select_progress: u32 = 0;
let mut free_select_required_pull: u32 = 0;
let mut free_select_policy: Option<&FreeSelectItem> = None;
for discount_policy_tag in target_pool.discount_policy_tags.iter() {
if common_properties.newcomer_advanced_s_tag == *discount_policy_tag {
let policy = gachaconf
.discount_policies
.advanced_guarantee_map
.get(discount_policy_tag)
.unwrap();
if status_bin
.discount_usage_map
.get(discount_policy_tag)
.unwrap()
< &policy.use_limit
{
advanced_s_guarantee = policy.guarantee_pity - pity_s + 1;
}
} else if common_properties.ten_pull_discount_tag == *discount_policy_tag {
let policy = gachaconf
.discount_policies
.ten_pull_discount_map
.get(discount_policy_tag)
.unwrap();
let discount_usage = status_bin
.discount_usage_map
.get(discount_policy_tag)
.unwrap();
if discount_usage < &policy.use_limit {
discount_ten_roll_prize = policy.discounted_prize;
discount_avaliable_num = policy.use_limit - discount_usage;
}
} else if gachaconf
.discount_policies
.free_select_map
.contains_key(discount_policy_tag)
{
let policy = gachaconf
.discount_policies
.free_select_map
.get(discount_policy_tag)
.unwrap();
let free_select_demand_idx = usize::try_from(
*(status_bin
.discount_usage_map
.get(&policy.free_select_usage_record_tag)
.unwrap()),
)
.unwrap();
if policy.milestones.len() <= free_select_demand_idx {
continue;
}
let free_select_actual_progress = status_bin
.discount_usage_map
.get(&policy.free_select_progress_record_tag)
.unwrap();
free_select_policy = Some(policy);
free_select_required_pull = policy
.milestones
.get(free_select_demand_idx)
.unwrap()
.to_owned();
free_select_progress = min(free_select_required_pull, *free_select_actual_progress);
}
}
let mut up_s_item_list: Vec<u32> = vec![];
let mut up_a_item_list: Vec<u32> = vec![];
let mut free_select_item_list: Vec<u32> = vec![];
let mut chooseable_up_list: Vec<u32> = vec![];
let mut chosen_up_item: u32 = 0;
let mut s_guarantee: u32 = 0;
let mut a_guarantee: u32 = 0;
for rarity_items in target_pool.gacha_items.iter() {
let mut chooseable_up_included_category_tags: Option<&HashSet<String>> = None;
let mut chooseable_policy_tag: Option<&String> = None;
for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
let category_guarantee_policy = gachaconf
.category_guarantee_policy_map
.get(guarantee_policy_tag)
.unwrap();
if !category_guarantee_policy.chooseable {
continue;
}
chooseable_policy_tag = Some(guarantee_policy_tag);
chooseable_up_included_category_tags =
Some(&category_guarantee_policy.included_category_tags);
if let Some(item) = status_bin
.rarity_status_map
.get(&rarity_items.rarity)
.unwrap()
.categories_chosen_guarantee_item_map
.get(guarantee_policy_tag)
{
chosen_up_item = item.clone();
}
}
for (category_tag, category) in rarity_items.categories.iter() {
let probability_model = gachaconf
.probability_model_map
.get(&rarity_items.probability_model_tag)
.unwrap();
let maximum_pity = &probability_model.maximum_guarantee_pity;
if rarity_items.rarity == common_properties.s_item_rarity {
if category.is_promotional_items {
up_s_item_list = category.item_ids.clone();
}
// tracing::info!("pity_s: {pity_s}");
// thread 'tokio-runtime-worker' panicked at nap_gameserver\src\handlers\gacha.rs:369:31:
// attempt to subtract with overflow
s_guarantee = maximum_pity - min(pity_s, maximum_pity.clone()) + 1;
}
if rarity_items.rarity == common_properties.a_item_rarity {
if category.is_promotional_items {
up_a_item_list = category.item_ids.clone();
}
// tracing::info!("pity_a: {pity_a}");
a_guarantee = maximum_pity - min(pity_a, maximum_pity.clone()) + 1;
}
if let Some(val) = free_select_policy {
if val.rarity == rarity_items.rarity && val.category_tags.contains(category_tag)
{
free_select_item_list.append(&mut category.item_ids.clone());
}
}
if let Some(tags) = chooseable_up_included_category_tags {
if tags.contains(category_tag) {
chooseable_up_list.append(&mut category.item_ids.clone());
}
}
}
if let Some(_priority_policy_tag) = chooseable_policy_tag {
// if let Some(item) = status_bin
// .rarity_status_map
// .get(&rarity_items.rarity)
// .unwrap()
// .categories_chosen_guarantee_item_map
// .get(priority_policy_tag)
// {
if rarity_items.rarity == gachaconf.common_properties.s_item_rarity {
up_s_item_list = chooseable_up_list.clone();
} else if rarity_items.rarity == gachaconf.common_properties.a_item_rarity {
up_a_item_list = vec![];
}
// }
}
}
let need_item_info_list: Vec<NeedItemInfo> = vec![NeedItemInfo {
need_item_id: target_pool.cost_item_id,
need_item_count: 1,
}];
let mut result = Gacha {
gacha_schedule_id: target_pool.gacha_schedule_id,
gacha_parent_schedule_id: target_pool.gacha_parent_schedule_id,
gacha_type: target_pool.gacha_type,
start_timestamp: target_pool.start_time.timestamp(),
end_timestamp: target_pool.end_time.timestamp(),
discount_avaliable_num,
discount_ten_roll_prize,
advanced_s_guarantee,
s_guarantee,
a_guarantee,
need_item_info_list,
free_select_progress,
free_select_required_pull,
free_select_item_list,
chosen_up_item,
// nammdglepbk: 563,
// hgmcofcjmbg: 101,
// akggbhgkifd: chooseable_up_list.clone(),
chooseable_up_list,
..Gacha::default()
};
if up_s_item_list.len() > 0 {
result.up_s_item_list = up_s_item_list;
}
if up_a_item_list.len() > 0 {
result.up_a_item_list = up_a_item_list;
}
result
}
/// Get the actual item cost count (counting discount).
pub fn get_actual_cost_count<'bin, 'conf>(
&'bin mut self,
target_pool: &'conf CharacterGachaPool,
pull_count: &u32,
) -> u32 {
let gachaconf = global_gacha_config();
if *pull_count == 10 {
let discount_tag = &gachaconf.common_properties.ten_pull_discount_tag;
if target_pool.discount_policy_tags.contains(&discount_tag) {
let status_bin = self
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap();
let discount_policy = gachaconf
.discount_policies
.ten_pull_discount_map
.get(discount_tag)
.unwrap();
let usage = status_bin.discount_usage_map.get_mut(discount_tag).unwrap();
if *usage < discount_policy.use_limit {
*usage += 1;
return discount_policy.discounted_prize;
}
}
}
pull_count.clone()
}
pub fn request_free_agent<'bin, 'conf>(
&'bin mut self,
target_pool: &'conf CharacterGachaPool,
item_id: &ItemID,
) -> GachaAddedItemType {
let gachaconf = global_gacha_config();
let sharing_guarantee_category_tag = &target_pool.sharing_guarantee_info_category;
let status_bin = self
.gacha_status_map
.get_mut(sharing_guarantee_category_tag)
.unwrap();
let item_id = item_id.value();
let mut free_select_policy: Option<&FreeSelectItem> = None;
let mut free_select_progress: u32 = 0;
let mut free_select_required_pull: u32 = 0;
for discount_policy_tag in target_pool.discount_policy_tags.iter() {
if gachaconf
.discount_policies
.free_select_map
.contains_key(discount_policy_tag)
{
let policy = gachaconf
.discount_policies
.free_select_map
.get(discount_policy_tag)
.unwrap();
let free_select_demand_idx = usize::try_from(
*(status_bin
.discount_usage_map
.get(&policy.free_select_usage_record_tag)
.unwrap()),
)
.unwrap();
if policy.milestones.len() <= free_select_demand_idx {
continue;
}
let free_select_actual_progress = status_bin
.discount_usage_map
.get(&policy.free_select_progress_record_tag)
.unwrap();
free_select_policy = Some(policy);
free_select_required_pull = policy
.milestones
.get(free_select_demand_idx)
.unwrap()
.to_owned();
free_select_progress = min(free_select_required_pull, *free_select_actual_progress);
}
}
if let None = free_select_policy {
tracing::info!(
"refuse free agent because: pool of parent_schedule_id {} hasn't defined free agent discount yet (or used up chance)",
target_pool.gacha_parent_schedule_id
);
return GachaAddedItemType::None;
} else if free_select_progress < free_select_required_pull {
tracing::info!(
"refuse free agent because: use pulled {free_select_progress} (after last free agent) in parent_schedule_id {}, required {free_select_required_pull}",
target_pool.gacha_parent_schedule_id
);
return GachaAddedItemType::None;
}
let free_select_policy = free_select_policy.unwrap();
let mut item_type: GachaAddedItemType = GachaAddedItemType::None;
for rarity_items in target_pool.gacha_items.iter() {
if rarity_items.rarity != free_select_policy.rarity {
continue;
}
for (category_tag, category) in rarity_items.categories.iter() {
if !free_select_policy.category_tags.contains(category_tag) {
continue;
}
if category.item_ids.contains(&item_id) {
item_type = category.item_type.clone();
}
}
}
if item_type != GachaAddedItemType::None {
(*status_bin
.discount_usage_map
.get_mut(&free_select_policy.free_select_usage_record_tag)
.unwrap()) += 1;
(*status_bin
.discount_usage_map
.get_mut(&free_select_policy.free_select_progress_record_tag)
.unwrap()) -= free_select_required_pull;
}
item_type
}
pub fn choose_gacha_up<'bin, 'conf>(
&'bin mut self,
target_pool: &'conf CharacterGachaPool,
item_id: &ItemID,
) -> bool {
let gachaconf = global_gacha_config();
let item_id = item_id.value();
for rarity_items in target_pool.gacha_items.iter() {
for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
let category_guarantee_policy = gachaconf
.category_guarantee_policy_map
.get(guarantee_policy_tag)
.unwrap();
if !category_guarantee_policy.chooseable {
continue;
}
let mut up_category: Option<&String> = None;
for (category_tag, category) in rarity_items.categories.iter() {
if category.item_ids.contains(&item_id) {
up_category = Some(category_tag);
break;
}
}
if let None = up_category {
continue;
};
let up_category = up_category.unwrap();
let progress_bin = self
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap()
.rarity_status_map
.get_mut(&rarity_items.rarity)
.unwrap();
match progress_bin
.categories_chosen_guarantee_item_map
.entry(guarantee_policy_tag.clone())
{
Occupied(mut occupied_entry) => {
occupied_entry.insert(item_id);
}
Vacant(vacant_entry) => {
vacant_entry.insert(item_id);
}
};
match progress_bin
.categories_chosen_guarantee_category_map
.entry(up_category.clone())
{
Occupied(mut occupied_entry) => {
occupied_entry.insert(up_category.clone());
}
Vacant(vacant_entry) => {
vacant_entry.insert(up_category.clone());
}
};
return true;
}
}
false
}
}

View file

@ -1,20 +1,25 @@
use super::record::*;
use super::stat::*;
use data::gacha;
use data::gacha::gacha_config::*;
use proto::*;
use data::tables::ItemID;
use chrono::{DateTime, Local};
use proto::GachaModelBin;
use rand::{thread_rng, Rng};
use std::collections::{HashMap, HashSet};
use std::hash::{BuildHasher, Hash};
pub struct GachaModel {
pub gacha_bin: GachaModelBin,
pub gacha_status_map: HashMap<String, GachaStatus>,
pub gacha_records: Vec<GachaRecord>,
}
impl Default for GachaModel {
fn default() -> GachaModel {
let result = GachaModel {
gacha_bin: GachaModelBin::default(),
gacha_status_map: HashMap::new(),
gacha_records: vec![],
};
result.post_deserialize()
}
@ -22,19 +27,37 @@ impl Default for GachaModel {
impl GachaModel {
pub fn from_bin(gacha_bin: GachaModelBin) -> Self {
let result = Self { gacha_bin };
let result = Self {
gacha_status_map: gacha_bin
.gacha_status_map
.into_iter()
.map(|(k, v)| (k, GachaStatus::from_bin(v)))
.collect(),
gacha_records: gacha_bin
.gacha_records
.into_iter()
.map(|x| GachaRecord::from_bin(x))
.collect(),
};
result.post_deserialize()
}
pub fn to_bin(&self) -> GachaModelBin {
self.gacha_bin.clone()
GachaModelBin {
gacha_status_map: self
.gacha_status_map
.iter()
.map(|(k, v)| (k.clone(), v.to_bin()))
.collect(),
gacha_records: self.gacha_records.iter().map(|x| x.to_bin()).collect(),
..Default::default()
}
}
pub fn post_deserialize(mut self) -> GachaModel {
let gachaconf = gacha::global_gacha_config();
for gacha_pool in gachaconf.character_gacha_pool_list.iter() {
let gacha_comp_bin = &mut self.gacha_bin;
let mut gacha_status_map = &mut gacha_comp_bin.gacha_status_map;
let mut gacha_status_map = &mut self.gacha_status_map;
let status_bin = get_or_add(
&mut gacha_status_map,
&gacha_pool.sharing_guarantee_info_category,
@ -97,11 +120,11 @@ impl GachaModel {
&'bin mut self,
pull_time: &DateTime<Local>,
target_pool: &'conf CharacterGachaPool,
) -> GachaRecordBin {
let mut gacha_bin = &mut self.gacha_bin;
) -> GachaRecord {
let (rarity_items, progress_bin, status_bin, probability_model) =
determine_rarity(&gacha_bin, target_pool);
let (category_tag, category) = determine_category(rarity_items, progress_bin, target_pool);
self.determine_rarity(target_pool);
let (category_tag, category) =
self.determine_category(rarity_items, progress_bin, target_pool);
let result = determine_gacha_result(
pull_time,
category,
@ -110,11 +133,311 @@ impl GachaModel {
progress_bin,
rarity_items,
);
update_pity(&mut gacha_bin, rarity_items, probability_model, target_pool);
update_category_guarantee_info(&mut gacha_bin, rarity_items, &category_tag, target_pool);
update_discount(&mut gacha_bin, target_pool, &category_tag, rarity_items);
self.update_pity(rarity_items, probability_model, target_pool);
self.update_category_guarantee_info(rarity_items, &category_tag, target_pool);
self.update_discount(target_pool, &category_tag, rarity_items);
result
}
fn rand_rarity<'bin, 'conf>(
&'bin self,
target_pool: &'conf CharacterGachaPool,
status_bin: &'bin GachaStatus,
) -> (
&'conf GachaAvailableItemsInfo,
&'bin GachaProgress,
&'conf ProbabilityModel,
) {
let gachaconf = gacha::global_gacha_config();
let mut rng = thread_rng();
let rarity_status_map = &status_bin.rarity_status_map;
// gacha_items is already sorted by rarity descendingly in its post_configure.
for rarity_items in target_pool.gacha_items.iter() {
// Surely any judgement should be made on the current pity.
let progress_bin = rarity_status_map.get(&rarity_items.rarity).unwrap();
let pity = progress_bin.pity;
let probability_model = gachaconf
.probability_model_map
.get(&rarity_items.probability_model_tag)
.unwrap();
if rng.gen_range(0.0..100.0) <= probability_model.get_chance_percent(&pity) {
return (rarity_items, progress_bin, probability_model);
}
}
panic!("The user failed to get any items.");
}
fn determine_rarity<'bin, 'conf>(
&'bin self,
target_pool: &'conf CharacterGachaPool,
) -> (
&'conf GachaAvailableItemsInfo,
&'bin GachaProgress,
&'bin GachaStatus,
&'conf ProbabilityModel,
) {
let gachaconf = gacha::global_gacha_config();
let status_bin = self
.gacha_status_map
.get(&target_pool.sharing_guarantee_info_category)
.expect(&format!(
"post_deserialize forgot StatusBin/sharing_guarantee_info_category: {}",
target_pool.sharing_guarantee_info_category
));
let (mut rarity_items, mut progress_bin, mut probability_model) =
self.rand_rarity(target_pool, &status_bin);
// We should take AdvancedGuarantee discount into consideration.
for discount_tag in target_pool.discount_policy_tags.iter() {
if let Some(discount) = gachaconf
.discount_policies
.advanced_guarantee_map
.get(discount_tag)
{
if discount.rarity <= rarity_items.rarity {
continue;
}
if status_bin
.discount_usage_map
.get(discount_tag)
.expect(&format!(
"post_deserialize forgot StatusBin/discount_usage_map: {}",
discount_tag
))
>= &discount.use_limit
{
continue;
}
let higher_progress_bin = status_bin
.rarity_status_map
.get(&discount.rarity)
.expect(&format!(
"post_deserialize forgot StatusBin/rarity_status_map: {}",
&discount.rarity
));
if higher_progress_bin.pity >= discount.guarantee_pity {
let mut found_rarity_items = false;
for gacha_items in target_pool.gacha_items.iter() {
if gacha_items.rarity == discount.rarity {
rarity_items = gacha_items;
probability_model = gachaconf
.probability_model_map
.get(&gacha_items.probability_model_tag)
.unwrap();
found_rarity_items = true;
break;
}
}
assert!(found_rarity_items, "Handle AdvancedGuarantee Discount ({discount_tag}) error: The target rarity does not exist in this pool.");
progress_bin = higher_progress_bin;
}
}
}
(rarity_items, progress_bin, status_bin, probability_model)
}
fn determine_category<'bin, 'conf>(
&'bin self,
rarity_items: &'conf GachaAvailableItemsInfo,
progress_bin: &'bin GachaProgress,
target_pool: &'conf CharacterGachaPool,
) -> (String, &'conf GachaCategoryInfo) {
let gachaconf = gacha::global_gacha_config();
let mut category_tag_inited = false;
let mut category_tag_result: HashSet<String> = HashSet::new();
// First of all, if there's a chooseable category and
// it is SELECTED then we MUST give that category's item.
for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
let category_guarantee_policy = gachaconf
.category_guarantee_policy_map
.get(guarantee_policy_tag)
.unwrap();
if !category_guarantee_policy.chooseable {
continue;
}
// As we found a policy defined chooseable, we
// should head to look whether the user chose
// the category he want.
if let Some(category_tag) = progress_bin
.categories_chosen_guarantee_category_map
.get(guarantee_policy_tag)
{
// User chose a category; our work are done here.
category_tag_result.insert(category_tag.clone());
category_tag_inited = true;
}
}
// Then we should take a look at MustGainItem.
if !category_tag_inited {
for discount_policy_tag in target_pool.discount_policy_tags.iter() {
if let Some(discount) = gachaconf
.discount_policies
.must_gain_item_map
.get(discount_policy_tag)
{
if discount.rarity != rarity_items.rarity {
continue;
}
category_tag_result.insert(discount.category_tag.clone());
category_tag_inited = true;
}
}
}
// Otherwise, just select as normal.
if !category_tag_inited {
for tag in rarity_items.categories.keys() {
category_tag_result.insert(tag.clone());
}
for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
let category_guarantee_policy = gachaconf
.category_guarantee_policy_map
.get(guarantee_policy_tag)
.unwrap();
let failure_times = progress_bin.categories_progress_map
.get(guarantee_policy_tag)
.expect(&format!("post_deserialize forgot StatusBin/rarity_status_map[{}]/categories_progress_map: {}", &rarity_items.rarity, guarantee_policy_tag));
if failure_times >= &category_guarantee_policy.trigger_on_failure_times {
category_tag_result = category_tag_result
.intersection(&category_guarantee_policy.included_category_tags)
.cloned()
.collect();
}
}
// category_tag_inited = true;
}
let mut categories: Vec<(String, &GachaCategoryInfo)> = vec![];
let mut weight_sum = 0;
for result_tag in category_tag_result {
let category = rarity_items.categories.get(&result_tag).unwrap();
categories.push((result_tag, category));
weight_sum += category.category_weight;
}
let randomnum = rand::thread_rng().gen_range(0..weight_sum);
let mut enumerated_ranges_end = 0;
for category in categories.into_iter() {
if randomnum <= enumerated_ranges_end + category.1.category_weight {
return (category.0, category.1);
}
enumerated_ranges_end += category.1.category_weight;
}
panic!("No category is chosen.");
}
fn update_pity<'bin, 'conf>(
&'bin mut self,
rarity_items: &'conf GachaAvailableItemsInfo,
probability_model: &'conf ProbabilityModel,
target_pool: &'conf CharacterGachaPool,
) {
let status_bin = self
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap();
for (rarity, rarity_status) in status_bin.rarity_status_map.iter_mut() {
if (rarity == &rarity_items.rarity)
|| (probability_model.clear_status_on_higher_rarity_pulled
&& rarity < &rarity_items.rarity)
{
rarity_status.pity = 1;
} else {
rarity_status.pity += 1;
}
}
}
fn update_category_guarantee_info<'bin, 'conf>(
&'bin mut self,
rarity_items: &'conf GachaAvailableItemsInfo,
category_tag: &String,
target_pool: &'conf CharacterGachaPool,
) {
let gachaconf = gacha::global_gacha_config();
let status_bin = self
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap();
let progress_bin = status_bin
.rarity_status_map
.get_mut(&rarity_items.rarity)
.unwrap();
for policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
let policy = gachaconf
.category_guarantee_policy_map
.get(policy_tag)
.unwrap();
// TODO: Chooseable guarantee not implemented
let prev_failure = progress_bin
.categories_progress_map
.get_mut(policy_tag)
.expect(&format!(
"post_deserialize forgot StatusBin/rarity_status_map[{}]/categories_progress_map: {}",
rarity_items.rarity, policy_tag
));
if policy.included_category_tags.contains(category_tag) {
*prev_failure = 0;
} else {
*prev_failure += 1;
}
}
}
fn update_discount<'bin, 'conf>(
&'bin mut self,
target_pool: &'conf CharacterGachaPool,
category_tag: &String,
rarity_items: &GachaAvailableItemsInfo,
) {
let gachaconf = gacha::global_gacha_config();
for (policy_tag, policy) in gachaconf.discount_policies.must_gain_item_map.iter() {
if *category_tag != policy.category_tag {
continue;
}
if !target_pool.discount_policy_tags.contains(policy_tag) {
continue;
}
let status_bin = self
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap();
let usage = status_bin.discount_usage_map.get_mut(policy_tag).unwrap();
if *usage < policy.use_limit {
*usage += 1;
}
}
for (policy_tag, policy) in gachaconf.discount_policies.advanced_guarantee_map.iter() {
if rarity_items.rarity != policy.rarity {
continue;
}
if !target_pool.discount_policy_tags.contains(policy_tag) {
continue;
}
let status_bin = self
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap();
let usage = status_bin.discount_usage_map.get_mut(policy_tag).unwrap();
if *usage < policy.use_limit {
*usage += 1;
}
}
for (policy_tag, policy) in gachaconf.discount_policies.free_select_map.iter() {
if !target_pool.discount_policy_tags.contains(policy_tag) {
continue;
}
let status_bin = self
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap();
let progress = status_bin
.discount_usage_map
.get_mut(&policy.free_select_progress_record_tag)
.unwrap();
*progress += 1;
}
}
}
fn get_or_add<'a, K: Eq + PartialEq + Hash + Clone, V: Default, S: BuildHasher>(
@ -127,200 +450,14 @@ fn get_or_add<'a, K: Eq + PartialEq + Hash + Clone, V: Default, S: BuildHasher>(
map.get_mut(key).unwrap()
}
fn rand_rarity<'bin, 'conf>(
target_pool: &'conf CharacterGachaPool,
status_bin: &'bin GachaStatusBin,
) -> (
&'conf GachaAvailableItemsInfo,
&'bin GachaProgressBin,
&'conf ProbabilityModel,
) {
let gachaconf = gacha::global_gacha_config();
let mut rng = thread_rng();
let rarity_status_map = &status_bin.rarity_status_map;
// gacha_items is already sorted by rarity descendingly in its post_configure.
for rarity_items in target_pool.gacha_items.iter() {
// Surely any judgement should be made on the current pity.
let progress_bin = rarity_status_map.get(&rarity_items.rarity).unwrap();
let pity = progress_bin.pity;
let probability_model = gachaconf
.probability_model_map
.get(&rarity_items.probability_model_tag)
.unwrap();
if rng.gen_range(0.0..100.0) <= probability_model.get_chance_percent(&pity) {
return (rarity_items, progress_bin, probability_model);
}
}
panic!("The user failed to get any items.");
}
fn determine_rarity<'bin, 'conf>(
gacha_bin: &'bin GachaModelBin,
target_pool: &'conf CharacterGachaPool,
) -> (
&'conf GachaAvailableItemsInfo,
&'bin GachaProgressBin,
&'bin GachaStatusBin,
&'conf ProbabilityModel,
) {
let gachaconf = gacha::global_gacha_config();
let status_bin = gacha_bin
.gacha_status_map
.get(&target_pool.sharing_guarantee_info_category)
.expect(&format!(
"post_deserialize forgot StatusBin/sharing_guarantee_info_category: {}",
target_pool.sharing_guarantee_info_category
));
let (mut rarity_items, mut progress_bin, mut probability_model) =
rand_rarity(target_pool, &status_bin);
// We should take AdvancedGuarantee discount into consideration.
for discount_tag in target_pool.discount_policy_tags.iter() {
if let Some(discount) = gachaconf
.discount_policies
.advanced_guarantee_map
.get(discount_tag)
{
if discount.rarity <= rarity_items.rarity {
continue;
}
if status_bin
.discount_usage_map
.get(discount_tag)
.expect(&format!(
"post_deserialize forgot StatusBin/discount_usage_map: {}",
discount_tag
))
>= &discount.use_limit
{
continue;
}
let higher_progress_bin =
status_bin
.rarity_status_map
.get(&discount.rarity)
.expect(&format!(
"post_deserialize forgot StatusBin/rarity_status_map: {}",
&discount.rarity
));
if higher_progress_bin.pity >= discount.guarantee_pity {
let mut found_rarity_items = false;
for gacha_items in target_pool.gacha_items.iter() {
if gacha_items.rarity == discount.rarity {
rarity_items = gacha_items;
probability_model = gachaconf
.probability_model_map
.get(&gacha_items.probability_model_tag)
.unwrap();
found_rarity_items = true;
break;
}
}
assert!(found_rarity_items, "Handle AdvancedGuarantee Discount ({discount_tag}) error: The target rarity does not exist in this pool.");
progress_bin = higher_progress_bin;
}
}
}
(rarity_items, progress_bin, status_bin, probability_model)
}
fn determine_category<'bin, 'conf>(
rarity_items: &'conf GachaAvailableItemsInfo,
progress_bin: &'bin GachaProgressBin,
target_pool: &'conf CharacterGachaPool,
) -> (String, &'conf GachaCategoryInfo) {
let gachaconf = gacha::global_gacha_config();
let mut category_tag_inited = false;
let mut category_tag_result: HashSet<String> = HashSet::new();
// First of all, if there's a chooseable category and
// it is SELECTED then we MUST give that category's item.
for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
let category_guarantee_policy = gachaconf
.category_guarantee_policy_map
.get(guarantee_policy_tag)
.unwrap();
if !category_guarantee_policy.chooseable {
continue;
}
// As we found a policy defined chooseable, we
// should head to look whether the user chose
// the category he want.
if let Some(category_tag) = progress_bin
.categories_chosen_guarantee_category_map
.get(guarantee_policy_tag)
{
// User chose a category; our work are done here.
category_tag_result.insert(category_tag.clone());
category_tag_inited = true;
}
}
// Then we should take a look at MustGainItem.
if !category_tag_inited {
for discount_policy_tag in target_pool.discount_policy_tags.iter() {
if let Some(discount) = gachaconf
.discount_policies
.must_gain_item_map
.get(discount_policy_tag)
{
if discount.rarity != rarity_items.rarity {
continue;
}
category_tag_result.insert(discount.category_tag.clone());
category_tag_inited = true;
}
}
}
// Otherwise, just select as normal.
if !category_tag_inited {
for tag in rarity_items.categories.keys() {
category_tag_result.insert(tag.clone());
}
for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
let category_guarantee_policy = gachaconf
.category_guarantee_policy_map
.get(guarantee_policy_tag)
.unwrap();
let failure_times = progress_bin.categories_progress_map
.get(guarantee_policy_tag)
.expect(&format!("post_deserialize forgot StatusBin/rarity_status_map[{}]/categories_progress_map: {}", &rarity_items.rarity, guarantee_policy_tag));
if failure_times >= &category_guarantee_policy.trigger_on_failure_times {
category_tag_result = category_tag_result
.intersection(&category_guarantee_policy.included_category_tags)
.cloned()
.collect();
}
}
// category_tag_inited = true;
}
let mut categories: Vec<(String, &GachaCategoryInfo)> = vec![];
let mut weight_sum = 0;
for result_tag in category_tag_result {
let category = rarity_items.categories.get(&result_tag).unwrap();
categories.push((result_tag, category));
weight_sum += category.category_weight;
}
let randomnum = rand::thread_rng().gen_range(0..weight_sum);
let mut enumerated_ranges_end = 0;
for category in categories.into_iter() {
if randomnum <= enumerated_ranges_end + category.1.category_weight {
return (category.0, category.1);
}
enumerated_ranges_end += category.1.category_weight;
}
panic!("No category is chosen.");
}
fn determine_gacha_result<'bin, 'conf>(
pull_time: &DateTime<Local>,
category: &'conf GachaCategoryInfo,
target_pool: &'conf CharacterGachaPool,
status_bin: &'bin GachaStatusBin,
progress_bin: &'bin GachaProgressBin,
status_bin: &'bin GachaStatus,
progress_bin: &'bin GachaProgress,
rarity_items: &'conf GachaAvailableItemsInfo,
) -> GachaRecordBin {
) -> GachaRecord {
let gachaconf = gacha::global_gacha_config();
let item_pool_len = category.item_ids.len() as u32;
let mut item_id: Option<&u32> = None;
@ -361,7 +498,7 @@ fn determine_gacha_result<'bin, 'conf>(
.get(rand::thread_rng().gen_range(0..item_pool_len) as usize)
.unwrap(),
};
let mut extra_item_id: u32 = 0;
let mut extra_item_id: Option<ItemID> = None;
let mut extra_item_count: u32 = 0;
for extra_items_policy_tag in rarity_items.extra_items_policy_tags.iter() {
@ -372,134 +509,23 @@ fn determine_gacha_result<'bin, 'conf>(
// TODO: apply_on_owned_count in a context with bag
// TODO: That's what RoleModel should do, not me.
if extra_items_policy.apply_on_owned_count == 0 {
extra_item_id = extra_items_policy.id;
extra_item_id = ItemID::new(extra_items_policy.id).ok();
extra_item_count = extra_items_policy.count;
}
}
let extra_item_bin = GachaExtraItemBin {
extra_item_id,
extra_item_count,
currently_gained: 0,
let extra_resources = match extra_item_id {
Some(item_id) => Some(GachaExtraResources {
extra_item_id: item_id,
extra_item_count,
}),
None => None,
};
GachaRecordBin {
GachaRecord {
pull_timestamp: pull_time.timestamp(),
obtained_item_id: item_id.clone(),
obtained_item_id: ItemID::new_unchecked(item_id.clone()),
gacha_id: target_pool.gacha_schedule_id.clone(),
progress_map: status_bin.rarity_status_map.clone(),
extra_item_bin: Some(extra_item_bin),
item_type: category.item_type.into(),
}
}
fn update_pity<'bin, 'conf>(
gacha_bin: &'bin mut GachaModelBin,
rarity_items: &'conf GachaAvailableItemsInfo,
probability_model: &'conf ProbabilityModel,
target_pool: &'conf CharacterGachaPool,
) {
let status_bin = gacha_bin
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap();
for (rarity, rarity_status) in status_bin.rarity_status_map.iter_mut() {
if (rarity == &rarity_items.rarity)
|| (probability_model.clear_status_on_higher_rarity_pulled
&& rarity < &rarity_items.rarity)
{
rarity_status.pity = 1;
} else {
rarity_status.pity += 1;
}
}
}
fn update_category_guarantee_info<'bin, 'conf>(
gacha_bin: &'bin mut GachaModelBin,
rarity_items: &'conf GachaAvailableItemsInfo,
category_tag: &String,
target_pool: &'conf CharacterGachaPool,
) {
let gachaconf = gacha::global_gacha_config();
let status_bin = gacha_bin
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap();
let progress_bin = status_bin
.rarity_status_map
.get_mut(&rarity_items.rarity)
.unwrap();
for policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
let policy = gachaconf
.category_guarantee_policy_map
.get(policy_tag)
.unwrap();
// TODO: Chooseable guarantee not implemented
let prev_failure = progress_bin
.categories_progress_map
.get_mut(policy_tag)
.expect(&format!(
"post_deserialize forgot StatusBin/rarity_status_map[{}]/categories_progress_map: {}",
rarity_items.rarity, policy_tag
));
if policy.included_category_tags.contains(category_tag) {
*prev_failure = 0;
} else {
*prev_failure += 1;
}
}
}
fn update_discount<'bin, 'conf>(
gacha_bin: &'bin mut GachaModelBin,
target_pool: &'conf CharacterGachaPool,
category_tag: &String,
rarity_items: &GachaAvailableItemsInfo,
) {
let gachaconf = gacha::global_gacha_config();
for (policy_tag, policy) in gachaconf.discount_policies.must_gain_item_map.iter() {
if *category_tag != policy.category_tag {
continue;
}
if !target_pool.discount_policy_tags.contains(policy_tag) {
continue;
}
let status_bin = gacha_bin
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap();
let usage = status_bin.discount_usage_map.get_mut(policy_tag).unwrap();
if *usage < policy.use_limit {
*usage += 1;
}
}
for (policy_tag, policy) in gachaconf.discount_policies.advanced_guarantee_map.iter() {
if rarity_items.rarity != policy.rarity {
continue;
}
if !target_pool.discount_policy_tags.contains(policy_tag) {
continue;
}
let status_bin = gacha_bin
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap();
let usage = status_bin.discount_usage_map.get_mut(policy_tag).unwrap();
if *usage < policy.use_limit {
*usage += 1;
}
}
for (policy_tag, policy) in gachaconf.discount_policies.free_select_map.iter() {
if !target_pool.discount_policy_tags.contains(policy_tag) {
continue;
}
let status_bin = gacha_bin
.gacha_status_map
.get_mut(&target_pool.sharing_guarantee_info_category)
.unwrap();
let progress = status_bin
.discount_usage_map
.get_mut(&policy.free_select_progress_record_tag)
.unwrap();
*progress += 1;
extra_resources,
item_type: category.item_type.clone(),
}
}

View file

@ -1,4 +1,6 @@
mod client_op;
mod gacha_model;
mod record;
mod stat;
pub use gacha_model::GachaModel;

View file

@ -0,0 +1,85 @@
use data::gacha::gacha_config::*;
use data::tables::ItemID;
use proto::{GachaExtraItemBin, GachaRecordBin};
use std::collections::HashMap;
use super::stat::*;
#[derive(Debug, Clone)]
pub struct GachaExtraResources {
pub extra_item_id: ItemID,
pub extra_item_count: u32,
}
impl GachaExtraResources {
pub fn from_bin_opt(bin_opt: Option<GachaExtraItemBin>) -> Option<Self> {
match bin_opt {
None => None,
Some(bin) => {
let item_id_opt = ItemID::new(bin.extra_item_id);
match item_id_opt {
Err(_) => None,
Ok(extra_item_id) => Some(Self {
extra_item_id,
extra_item_count: bin.extra_item_count,
}),
}
}
}
}
pub fn to_bin_opt(opt: &Option<Self>) -> Option<GachaExtraItemBin> {
match opt {
None => None,
Some(this) => Some(GachaExtraItemBin {
extra_item_id: this.extra_item_id.value(),
extra_item_count: this.extra_item_count.clone(),
..Default::default()
}),
}
}
}
#[derive(Debug, Clone)]
pub struct GachaRecord {
pub pull_timestamp: i64,
pub obtained_item_id: ItemID,
pub gacha_id: u32,
/// The progress BEFORE this gacha is performed. key is rarity.
pub progress_map: HashMap<u32, GachaProgress>,
pub extra_resources: Option<GachaExtraResources>,
pub item_type: GachaAddedItemType,
}
impl GachaRecord {
pub fn from_bin(bin: GachaRecordBin) -> Self {
Self {
pull_timestamp: bin.pull_timestamp,
obtained_item_id: ItemID::new_unchecked(bin.obtained_item_id),
gacha_id: bin.gacha_id,
progress_map: bin
.progress_map
.into_iter()
.map(|(k, v)| (k, GachaProgress::from_bin(v)))
.collect(),
extra_resources: GachaExtraResources::from_bin_opt(bin.extra_item_bin),
item_type: GachaAddedItemType::from(bin.item_type),
}
}
pub fn to_bin(&self) -> GachaRecordBin {
GachaRecordBin {
pull_timestamp: self.pull_timestamp.clone(),
obtained_item_id: self.obtained_item_id.value(),
gacha_id: self.gacha_id.clone(),
progress_map: self
.progress_map
.iter()
.map(|(k, v)| (k.clone(), v.to_bin()))
.collect(),
extra_item_bin: GachaExtraResources::to_bin_opt(&self.extra_resources),
item_type: self.item_type.clone().into(),
}
}
}

View file

@ -0,0 +1,73 @@
use proto::*;
use std::collections::HashMap;
#[derive(Debug, Default, Clone)]
pub struct GachaProgress {
/// The pity (counting how many pulls) of this pull (in previous record) or the next pull (in status).
pub pity: u32,
/// The failure times of this category.
pub categories_progress_map: HashMap<String, u32>,
/// The selected priority (category) for a Chooseable category.
pub categories_chosen_guarantee_category_map: HashMap<String, String>,
/// The selectedpriority (a specified item) for a Chooseable category.
pub categories_chosen_guarantee_item_map: HashMap<String, u32>,
/// The failure times for selected priority (a specified item).
pub categories_chosen_guarantee_progress_map: HashMap<String, u32>,
}
impl GachaProgress {
pub fn from_bin(bin: GachaProgressBin) -> Self {
Self {
pity: bin.pity,
categories_progress_map: bin.categories_progress_map,
categories_chosen_guarantee_category_map: bin.categories_chosen_guarantee_category_map,
categories_chosen_guarantee_item_map: bin.categories_chosen_guarantee_item_map,
categories_chosen_guarantee_progress_map: bin.categories_chosen_guarantee_progress_map,
}
}
pub fn to_bin(&self) -> GachaProgressBin {
GachaProgressBin {
pity: self.pity,
categories_progress_map: self.categories_progress_map.clone(),
categories_chosen_guarantee_category_map: self
.categories_chosen_guarantee_category_map
.clone(),
categories_chosen_guarantee_item_map: self.categories_chosen_guarantee_item_map.clone(),
categories_chosen_guarantee_progress_map: self
.categories_chosen_guarantee_progress_map
.clone(),
}
}
}
#[derive(Debug, Default, Clone)]
pub struct GachaStatus {
pub rarity_status_map: HashMap<u32, GachaProgress>,
pub discount_usage_map: HashMap<String, u32>,
}
impl GachaStatus {
pub fn from_bin(bin: GachaStatusBin) -> Self {
Self {
rarity_status_map: bin
.rarity_status_map
.into_iter()
.map(|(k, v)| (k, GachaProgress::from_bin(v)))
.collect(),
discount_usage_map: bin.discount_usage_map,
}
}
pub fn to_bin(&self) -> GachaStatusBin {
GachaStatusBin {
rarity_status_map: self
.rarity_status_map
.iter()
.map(|(k, v)| (k.clone(), v.to_bin()))
.collect(),
discount_usage_map: self.discount_usage_map.clone(),
}
}
}

View file

@ -1,6 +1,7 @@
use common::util;
use data::tables::{
self, ArchiveBattleQuestID, BattleEventConfigID, HollowQuestID, TrainingQuestID,
self, ArchiveBattleQuestID, AvatarBaseID, BattleEventConfigID, BuddyBaseID, HollowQuestID,
RobotBuddyConfigID, RobotConfigID, TrainingQuestID,
};
use proto::{DungeonInfo, DungeonItemData, FightSceneInfo, SceneInfo, WeatherPoolInfo};
use thiserror::Error;
@ -8,7 +9,7 @@ use thiserror::Error;
use crate::logic::{
battle::{
drop::FightDropPool,
unit::{AvatarUnit, BuddyUnit},
unit::{AvatarUnit, AvatarUnitID, BuddyUnit, BuddyUnitID},
BuddyParam, DungeonQuestManager, TeamDataItem,
},
BuddyTeamType, EHollowQuestType, ELocalPlayType, ESceneType, TimePeriodType, WeatherType,
@ -22,6 +23,10 @@ pub enum HollowGameError {
QuestTypeNotSupported(u32, EHollowQuestType),
#[error("Battle group not found, quest id: {0}")]
BattleGroupNotFound(u32),
#[error("Invalid avatar id: {0}")]
InvalidAvatarID(u32),
#[error("Invalid robot id: {0}")]
InvalidRobotID(u32),
}
pub struct HollowGame {
@ -44,6 +49,15 @@ impl HollowGame {
) -> Result<Self, HollowGameError> {
let template = training_quest_id.template();
let avatars = match avatars
.iter()
.map(|id| AvatarBaseID::new(*id).map(|id| AvatarUnitID::Base(id)))
.collect::<Result<Vec<_>, _>>()
{
Ok(avatars) => avatars,
Err(err) => return Err(HollowGameError::InvalidAvatarID(err.0)),
};
Ok(Self {
quest_id: template.id.value(),
battle_event_id: template.battle_event_id,
@ -51,7 +65,7 @@ impl HollowGame {
weather: WeatherType::SunShine,
start_timestamp: util::cur_timestamp() as i64,
play_type,
team_data: TeamDataItem::new(avatars, &[]),
team_data: TeamDataItem::new(&avatars, &[]),
fight_drop_pool: FightDropPool::new(template.battle_event_id),
quest_manager: DungeonQuestManager::default(),
})
@ -65,6 +79,20 @@ impl HollowGame {
) -> Result<Self, HollowGameError> {
let template = archive_battle_quest_id.template();
let avatars = match avatars
.iter()
.map(|id| RobotConfigID::new(*id).map(|id| AvatarUnitID::Robot(id)))
.collect::<Result<Vec<_>, _>>()
{
Ok(avatars) => avatars,
Err(err) => return Err(HollowGameError::InvalidAvatarID(err.0)),
};
let buddy_params = match RobotBuddyConfigID::new(buddy_id) {
Ok(id) => vec![BuddyParam(BuddyUnitID::Robot(id), BuddyTeamType::Fighting)],
Err(_) => Vec::with_capacity(0),
};
Ok(Self {
quest_id: template.id.value(),
battle_event_id: template.battle_event_id,
@ -72,12 +100,7 @@ impl HollowGame {
weather: WeatherType::SunShine,
start_timestamp: util::cur_timestamp() as i64,
play_type,
team_data: TeamDataItem::new(
avatars,
&(buddy_id != 0)
.then_some(vec![BuddyParam(buddy_id, BuddyTeamType::Fighting)])
.unwrap_or_default(),
),
team_data: TeamDataItem::new(&avatars, &buddy_params),
fight_drop_pool: FightDropPool::new(template.battle_event_id),
quest_manager: DungeonQuestManager::default(),
})
@ -104,6 +127,20 @@ impl HollowGame {
return Err(HollowGameError::BattleGroupNotFound(template.id.value()));
};
let avatars = match avatars
.iter()
.map(|id| AvatarBaseID::new(*id).map(|id| AvatarUnitID::Base(id)))
.collect::<Result<Vec<_>, _>>()
{
Ok(avatars) => avatars,
Err(err) => return Err(HollowGameError::InvalidAvatarID(err.0)),
};
let buddy_params = match BuddyBaseID::new(buddy_id) {
Ok(id) => vec![BuddyParam(BuddyUnitID::Base(id), BuddyTeamType::Fighting)],
Err(_) => Vec::with_capacity(0),
};
Ok(Self {
quest_id: template.id.value(),
battle_event_id: battle_group.battle_event_id,
@ -113,12 +150,7 @@ impl HollowGame {
play_type: Self::get_play_type_by_quest_type(EHollowQuestType::from(
template.hollow_quest_type,
)),
team_data: TeamDataItem::new(
avatars,
&(buddy_id != 0)
.then_some(vec![BuddyParam(buddy_id, BuddyTeamType::Fighting)])
.unwrap_or_default(),
),
team_data: TeamDataItem::new(&avatars, &buddy_params),
fight_drop_pool: FightDropPool::new(battle_group.battle_event_id),
quest_manager: DungeonQuestManager::new_for_battle_group(battle_group.id),
})

View file

@ -1,5 +1,5 @@
use common::util;
use data::tables::{self, BattleEventConfigID, HollowQuestID};
use data::tables::{self, AvatarBaseID, BattleEventConfigID, BuddyBaseID, HollowQuestID};
use proto::{
DungeonInfo, DungeonItemData, FightQuestInfo, LongFightInfo, LongFightSceneInfo, SceneInfo,
WeatherPoolInfo,
@ -9,7 +9,7 @@ use thiserror::Error;
use crate::logic::{
battle::{
drop::FightDropPool,
unit::{AvatarUnit, BuddyUnit},
unit::{AvatarUnit, AvatarUnitID, BuddyUnit, BuddyUnitID},
BuddyParam, DungeonQuestManager, LogicVariableTable, TeamDataItem,
},
BuddyTeamType, EHollowQuestType, ELocalPlayType, ESceneType, TimePeriodType, WeatherType,
@ -23,6 +23,8 @@ pub enum LongFightGameError {
InvalidQuestType(EHollowQuestType),
#[error("Battle group not found, quest id: {0}")]
BattleGroupNotFound(u32),
#[error("Invalid avatar id: {0}")]
InvalidAvatarID(u32),
}
pub struct LongFightGame {
@ -39,7 +41,7 @@ pub struct LongFightGame {
}
impl LongFightGame {
const RALLY_GUIDANCE_BUDDY_ID: u32 = 50001;
const RALLY_GUIDANCE_BUDDY: BuddyBaseID = BuddyBaseID::new_unchecked(50001);
pub fn create_rally_game(
quest_id: HollowQuestID,
@ -61,13 +63,25 @@ impl LongFightGame {
return Err(LongFightGameError::BattleGroupNotFound(template.id.value()));
};
let avatars = match avatars
.iter()
.map(|id| AvatarBaseID::new(*id).map(|id| AvatarUnitID::Base(id)))
.collect::<Result<Vec<_>, _>>()
{
Ok(avatars) => avatars,
Err(err) => return Err(LongFightGameError::InvalidAvatarID(err.0)),
};
let mut buddy_params = vec![BuddyParam(
Self::RALLY_GUIDANCE_BUDDY_ID,
BuddyUnitID::Base(Self::RALLY_GUIDANCE_BUDDY),
BuddyTeamType::RallyGuidance,
)];
if buddy_id != 0 {
buddy_params.push(BuddyParam(buddy_id, BuddyTeamType::Fighting));
if let Ok(buddy_id) = BuddyBaseID::new(buddy_id) {
buddy_params.push(BuddyParam(
BuddyUnitID::Base(buddy_id),
BuddyTeamType::Fighting,
));
}
Ok(Self {
@ -77,7 +91,7 @@ impl LongFightGame {
time_period,
weather,
start_timestamp: util::cur_timestamp() as i64,
team_data: TeamDataItem::new(avatars, &buddy_params),
team_data: TeamDataItem::new(&avatars, &buddy_params),
variable_table: LogicVariableTable::new(battle_group.battle_event_id),
fight_drop_pool: FightDropPool::new(battle_group.battle_event_id),
quest_manager: DungeonQuestManager::new_for_battle_group(battle_group.id),

View file

@ -2,6 +2,7 @@ mod fresh;
mod frontend;
mod hollow;
mod long_fight;
use data::tables::TemplateNotFoundError;
pub use fresh::*;
pub use frontend::*;
pub use hollow::*;
@ -36,6 +37,8 @@ pub enum LogicError {
LongFight(#[from] LongFightGameError),
#[error("dungeon quest error: {0}")]
DungeonQuest(#[from] DungeonQuestError),
#[error("{0}")]
TemplateNotFound(#[from] TemplateNotFoundError),
}
impl GameInstance {

View file

@ -1,4 +1,4 @@
use data::tables::{AvatarBaseID, ProcedureConfigID};
use data::tables::{AvatarBaseID, PostGirlConfigID, ProcedureConfigID};
use proto::{BasicDataModelBin, PlayerBasicInfo};
pub struct BasicDataModel {
@ -8,6 +8,7 @@ pub struct BasicDataModel {
pub nick_name: Option<String>,
pub frontend_avatar_id: Option<AvatarBaseID>,
pub beginner_procedure_id: Option<ProcedureConfigID>,
pub selected_post_girl_id: Option<PostGirlConfigID>,
}
impl Default for BasicDataModel {
@ -19,6 +20,7 @@ impl Default for BasicDataModel {
nick_name: None,
frontend_avatar_id: None,
beginner_procedure_id: Some(ProcedureConfigID::new_unchecked(1)),
selected_post_girl_id: None,
}
}
}
@ -37,6 +39,10 @@ impl BasicDataModel {
avatar_id,
frontend_avatar_id: avatar_id,
kbjleelonfe: self.profile_icon,
has_nickname: match &self.nick_name {
Some(_name) => 1,
None => 0,
},
..Default::default()
}
}
@ -47,13 +53,14 @@ impl BasicDataModel {
exp: bin.exp,
profile_icon: bin.profile_icon,
frontend_avatar_id: match bin.frontend_avatar_id {
1.. => AvatarBaseID::new(bin.frontend_avatar_id as u32),
1.. => AvatarBaseID::new(bin.frontend_avatar_id as u32).ok(),
_ => None,
},
beginner_procedure_id: match bin.beginner_procedure_id {
1.. => ProcedureConfigID::new(bin.beginner_procedure_id as u32),
1.. => ProcedureConfigID::new(bin.beginner_procedure_id as u32).ok(),
_ => None,
},
selected_post_girl_id: PostGirlConfigID::new(bin.selected_post_girl_id).ok(),
nick_name: match bin.nick_name.is_empty() {
true => None,
false => Some(bin.nick_name),
@ -75,6 +82,10 @@ impl BasicDataModel {
.beginner_procedure_id
.map(|i| i.value() as i32)
.unwrap_or(-1),
selected_post_girl_id: match self.selected_post_girl_id {
Some(post_girl_id) => post_girl_id.value(),
None => 0,
},
}
}
}

View file

@ -1,11 +1,12 @@
use std::collections::BTreeSet;
use std::collections::{BTreeSet, HashMap};
use data::tables::UnlockConfigID;
use proto::{LockModelBin, UnlockData};
use data::tables::{quick_access_template_tb, QuickFuncID, UnlockConfigID};
use proto::{LockModelBin, QuickAccessData, QuickAccessType, UnlockData};
#[derive(Default)]
pub struct LockModel {
unlock_list: BTreeSet<UnlockConfigID>,
quick_access_list: HashMap<u32, QuickFuncID>,
}
impl LockModel {
@ -14,7 +15,12 @@ impl LockModel {
unlock_list: bin
.unlock_list
.into_iter()
.map(UnlockConfigID::new_unchecked)
.map(|id| UnlockConfigID::new_unchecked(id as u32))
.collect(),
quick_access_list: bin
.quick_access_list
.into_iter()
.map(|(k, v)| (k, QuickFuncID::new_unchecked(v)))
.collect(),
}
}
@ -25,19 +31,48 @@ impl LockModel {
.unlock_list
.clone()
.into_iter()
.map(|i| i.value())
.map(|i| i.value() as i32)
.collect(),
quick_access_list: self
.quick_access_list
.iter()
.map(|(k, v)| (k.clone(), v.value()))
.collect(),
}
}
pub fn quick_access_to_client(&self) -> Vec<QuickAccessData> {
let mut quick_access_data_list: Vec<QuickAccessData> = vec![];
for quick_access in quick_access_template_tb::iter() {
quick_access_data_list.push(QuickAccessData {
r#type: QuickAccessType::Direct.into(),
quick_access_index: quick_access.quick_access_index,
btn_id: quick_access.quick_func_id.value(),
});
}
for quick_access_index in 1..8 {
let mut btn_id = 0;
if let Some(id) = self.quick_access_list.get(&quick_access_index) {
btn_id = id.value();
}
quick_access_data_list.push(QuickAccessData {
r#type: QuickAccessType::QuickMenu.into(),
quick_access_index,
btn_id,
});
}
quick_access_data_list
}
pub fn to_client(&self) -> UnlockData {
UnlockData {
unlock_id_list: self
.unlock_list
.clone()
.into_iter()
.map(|i| i.value())
.map(|i| i.value() as i32)
.collect(),
quick_access_data_list: self.quick_access_to_client(),
..Default::default()
}
}
@ -49,4 +84,11 @@ impl LockModel {
pub fn is_unlock(&self, id: UnlockConfigID) -> bool {
self.unlock_list.contains(&id)
}
pub fn mod_quick_access(&mut self, index: u32, id: Option<QuickFuncID>) {
match id {
Some(quick_access_id) => self.quick_access_list.insert(index, quick_access_id),
None => self.quick_access_list.remove(&index),
};
}
}

View file

@ -22,15 +22,17 @@ impl RoleModel {
const DEFAULT_AVATARS: [u32; 2] = [1011, 1081];
pub fn add_avatar(&mut self, template_id: AvatarBaseID) {
if !self
.avatar_list
.iter()
.any(|a| a.template_id == template_id)
{
if !self.has_avatar(template_id) {
self.avatar_list.push(Avatar::new(template_id));
}
}
pub fn has_avatar(&self, template_id: AvatarBaseID) -> bool {
self.avatar_list
.iter()
.any(|a| a.template_id == template_id)
}
pub fn avatar_sync(&self) -> AvatarSync {
AvatarSync {
avatar_list: self.avatar_list.iter().map(Avatar::to_client).collect(),

View file

@ -12,7 +12,7 @@ impl SceneUnitManager {
let unit_vec = tables::main_city_object_template_tb::iter()
.filter(|tmpl| tmpl.get_section_name() == section_template.section_name)
.filter(|tmpl| MainCityDefaultObjectID::new(tmpl.tag_id.value()).is_some()) // check if npc tag present in default object table
.filter(|tmpl| MainCityDefaultObjectID::new(tmpl.tag_id.value()).is_ok()) // check if npc tag present in default object table
.map(|tmpl| SceneUnit::new(tmpl.tag_id))
.collect();

View file

@ -1267,7 +1267,7 @@ pub struct Lekadbbedgd {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Ijkfhelddih {
pub struct NewbieGroupData {
#[prost(int32, repeated, tag = "5")]
pub unlock_id_list: ::prost::alloc::vec::Vec<i32>,
}
@ -2250,13 +2250,13 @@ pub struct PlayerSyncScNotify {
#[prost(message, optional, tag = "2")]
pub kophiimbjek: ::core::option::Option<Feecjgonjfm>,
#[prost(message, optional, tag = "3")]
pub gbfdodbmboh: ::core::option::Option<Efadpnfaelh>,
pub client_systems_sync: ::core::option::Option<ClientSystemsSync>,
#[prost(message, optional, tag = "4")]
pub ejechecjicb: ::core::option::Option<Iapdkahajkn>,
#[prost(message, optional, tag = "5")]
pub fphameaeean: ::core::option::Option<Mbokdhgpobc>,
#[prost(message, optional, tag = "6")]
pub ogcaaminman: ::core::option::Option<Ecpbnceekbi>,
pub quest_data_sync: ::core::option::Option<QuestDataSync>,
#[prost(message, optional, tag = "7")]
pub aklfkgodkde: ::core::option::Option<Dbjpegchcch>,
#[prost(message, optional, tag = "8")]
@ -2278,7 +2278,7 @@ pub struct PlayerSyncScNotify {
#[prost(bool, tag = "46")]
pub pibodcappln: bool,
#[prost(message, optional, tag = "82")]
pub coagpnjkncf: ::core::option::Option<Bmhfpdagadk>,
pub collect_map_sync: ::core::option::Option<CollectMapSync>,
#[prost(message, optional, tag = "162")]
pub hffkhegeifp: ::core::option::Option<Lachoecdhnc>,
#[prost(message, optional, tag = "260")]
@ -2296,7 +2296,7 @@ pub struct PlayerSyncScNotify {
#[prost(message, optional, tag = "915")]
pub dcdddgnhdjj: ::core::option::Option<Dpiceolgbfi>,
#[prost(message, optional, tag = "1085")]
pub hlcjedhdlfo: ::core::option::Option<Ipimhifmfni>,
pub work_bench_sync: ::core::option::Option<WorkBenchSync>,
#[prost(message, optional, tag = "1142")]
pub ahpalhhnkdc: ::core::option::Option<Bnaicfpiiag>,
#[prost(message, optional, tag = "1440")]
@ -2306,7 +2306,7 @@ pub struct PlayerSyncScNotify {
#[prost(message, optional, tag = "1705")]
pub oijhohooded: ::core::option::Option<Edlekkbcaia>,
#[prost(message, optional, tag = "1728")]
pub pofhalhdikf: ::core::option::Option<Oaoecalella>,
pub vhs_store_sync: ::core::option::Option<VhsStoreSync>,
}
#[derive(proto_gen::CmdID)]
#[derive(proto_gen::XorFields)]
@ -3025,7 +3025,7 @@ pub struct PlayerBasicInfo {
pub ififobofjhd: bool,
#[xor(12709)]
#[prost(uint32, tag = "8")]
pub pmjdniklpgc: u32,
pub has_nickname: u32,
#[xor(8317)]
#[prost(uint32, tag = "2")]
pub level: u32,
@ -3210,7 +3210,7 @@ pub struct UnlockData {
#[prost(int32, repeated, tag = "1")]
pub unlock_id_list: ::prost::alloc::vec::Vec<i32>,
#[prost(message, repeated, tag = "3")]
pub ppfekhgdpbo: ::prost::alloc::vec::Vec<Jmkeekhhheb>,
pub quick_access_data_list: ::prost::alloc::vec::Vec<QuickAccessData>,
#[prost(int32, repeated, tag = "6")]
pub kefeekedhii: ::prost::alloc::vec::Vec<i32>,
}
@ -3910,17 +3910,17 @@ pub struct Hmchpbjljen {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Ooofknbkhel {
pub struct TarotCardData {
#[prost(int32, repeated, tag = "2")]
pub fpmfdgjhchm: ::prost::alloc::vec::Vec<i32>,
#[prost(int32, repeated, tag = "5")]
pub llnmdibojio: ::prost::alloc::vec::Vec<i32>,
pub not_viewed_list: ::prost::alloc::vec::Vec<i32>,
#[prost(int32, repeated, tag = "9")]
pub ihnjbpcpfkf: ::prost::alloc::vec::Vec<i32>,
#[prost(message, repeated, tag = "11")]
pub ofdfjpflbie: ::prost::alloc::vec::Vec<Ifndfopgcph>,
#[prost(int32, repeated, tag = "13")]
pub nkggmfpghgi: ::prost::alloc::vec::Vec<i32>,
pub obtained_list: ::prost::alloc::vec::Vec<i32>,
}
#[derive(proto_gen::CmdID)]
#[derive(proto_gen::XorFields)]
@ -5396,7 +5396,7 @@ pub struct Icgeifjjpma {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Aboegnnepmi {
pub struct CallingCardData {
#[prost(uint32, repeated, tag = "11")]
pub ocfaaakaomg: ::prost::alloc::vec::Vec<u32>,
#[prost(uint32, repeated, tag = "8")]
@ -5983,7 +5983,7 @@ pub struct Dgfbapkibje {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Jfkllcaiafj {
pub struct InterKnotSync {
#[prost(uint32, repeated, tag = "2")]
pub gbcaeaicghd: ::prost::alloc::vec::Vec<u32>,
#[prost(uint32, repeated, tag = "3")]
@ -8738,7 +8738,7 @@ pub struct Ojdhiaepnad {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Mjhicllmahe {
pub struct CheckItemSync {
#[prost(uint32, repeated, tag = "1")]
pub aighoeokkab: ::prost::alloc::vec::Vec<u32>,
}
@ -9359,11 +9359,11 @@ pub struct Ijkgfnpobob {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Glinfkmgjoj {
pub struct CurseData {
#[prost(int32, repeated, tag = "5")]
pub llnmdibojio: ::prost::alloc::vec::Vec<i32>,
pub not_viewed_list: ::prost::alloc::vec::Vec<i32>,
#[prost(int32, repeated, tag = "8")]
pub nkggmfpghgi: ::prost::alloc::vec::Vec<i32>,
pub obtained_list: ::prost::alloc::vec::Vec<i32>,
}
#[derive(proto_gen::CmdID)]
#[derive(proto_gen::XorFields)]
@ -10437,7 +10437,7 @@ pub struct Abnehcbnadl {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Baodmddgagi {
pub struct CallingCardSync {
#[prost(uint32, repeated, tag = "5")]
pub ocfaaakaomg: ::prost::alloc::vec::Vec<u32>,
#[prost(uint32, repeated, tag = "3")]
@ -10701,9 +10701,9 @@ pub struct Abjkiejnbak {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Oacoekjapel {
pub struct ChangePostGirlCsReq {
#[prost(uint32, repeated, tag = "4")]
pub fndfaabfafb: ::prost::alloc::vec::Vec<u32>,
pub new_selected_post_girl_id_list: ::prost::alloc::vec::Vec<u32>,
}
#[derive(proto_gen::CmdID)]
#[derive(proto_gen::XorFields)]
@ -11109,7 +11109,7 @@ pub struct Geemkgcbcng {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Oaoecalella {
pub struct VhsStoreSync {
#[prost(message, optional, tag = "11")]
pub cfaiklmcfkb: ::core::option::Option<Aimmcpelead>,
#[prost(bool, tag = "13")]
@ -11366,7 +11366,7 @@ pub struct Acpgildhoed {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Idbdhebkcgg {
pub struct ModQuickMenuScRsp {
#[xor(13685)]
#[prost(int32, tag = "1")]
pub retcode: i32,
@ -12173,9 +12173,9 @@ pub struct Mbfcgalihdj {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Dpnalcomaii {
pub struct ModQuickMenuCsReq {
#[prost(message, repeated, tag = "13")]
pub ppfekhgdpbo: ::prost::alloc::vec::Vec<Jmkeekhhheb>,
pub quick_access_data_list: ::prost::alloc::vec::Vec<QuickAccessData>,
}
#[derive(proto_gen::CmdID)]
#[derive(proto_gen::XorFields)]
@ -12649,7 +12649,7 @@ pub struct Akkbhjhoiao {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Fikgjigadki {
pub struct ModNicknameCsReq {
#[xor(13081)]
#[prost(uint32, tag = "12")]
pub avatar_id: u32,
@ -12839,7 +12839,7 @@ pub struct Okkjjhonnik {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Hgdnnkfjcmc {
pub struct MusicPlayerSync {
#[xor(12784)]
#[prost(uint32, tag = "11")]
pub mhgnhkmhlgj: u32,
@ -13605,7 +13605,7 @@ pub struct Bhomgogkadj {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Dmhpnnmggnm {
pub struct TrashbinHermitSync {
#[prost(message, repeated, tag = "15")]
pub klbehoieibi: ::prost::alloc::vec::Vec<Ddplihlfngg>,
}
@ -16157,41 +16157,59 @@ pub struct Fihgfkkpnae {
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ClientSystemsInfo {
#[prost(message, optional, tag = "1")]
pub ilplibfdina: ::core::option::Option<Ipijdhhpbci>,
pub teleport_data: ::core::option::Option<TeleportData>,
#[prost(message, optional, tag = "2")]
pub aklfkgodkde: ::core::option::Option<Iggbdkbgphj>,
#[prost(message, optional, tag = "3")]
pub ipfhhabegii: ::core::option::Option<Lcnicjccpde>,
#[prost(message, optional, tag = "4")]
pub post_girl_data: ::core::option::Option<PostGirlData>,
/// 音乐播放器,主要是收录列表。里面有个 enum 是不是循环模式的意思?
///
#[prost(message, optional, tag = "6")]
pub djhjojekegc: ::core::option::Option<Knohmlebcbm>,
pub music_player_data: ::core::option::Option<MusicPlayerData>,
/// 这个应该是解锁鸣徽之类的了。需要进一步研究
///
#[prost(message, optional, tag = "7")]
pub mhehfjdgkbd: ::core::option::Option<Ooofknbkhel>,
pub tarot_card_data: ::core::option::Option<TarotCardData>,
/// 这个大概跟剧情有关,下面有个表是已经玩过的过场 ID. 这个过场特指漫画式和几个人放屏幕上的对话式,要是给一个共性就是都可以跳过的
///
#[prost(message, optional, tag = "8")]
pub mopogebpbeo: ::core::option::Option<Oaljbkehldb>,
#[prost(message, optional, tag = "9")]
pub njidcmpcojg: ::core::option::Option<Ijkfhelddih>,
pub newbie_group_data: ::core::option::Option<NewbieGroupData>,
#[prost(message, optional, tag = "10")]
pub unlock_data: ::core::option::Option<UnlockData>,
#[prost(message, optional, tag = "11")]
pub geoagplahii: ::core::option::Option<NewsStandData>,
#[prost(message, optional, tag = "12")]
pub trashbin_hermit_data: ::core::option::Option<TrashbinHermitData>,
/// 猜测是收藏列表
///
#[prost(message, optional, tag = "13")]
pub obfepjfnlgi: ::core::option::Option<Jcdicmoemef>,
pub collect_data: ::core::option::Option<CollectData>,
/// 侵蚀症状..? 大概是曾获得过的意思
///
#[prost(message, optional, tag = "14")]
pub mkhjcnbgedm: ::core::option::Option<Glinfkmgjoj>,
pub curse_data: ::core::option::Option<CurseData>,
/// 绳网数据。大概有浏览过的帖子和发表了的回复之类
///
#[prost(message, optional, tag = "15")]
pub olgibdhglkg: ::core::option::Option<Gfkfedpdede>,
pub inter_knot_data: ::core::option::Option<InterKnotData>,
/// 成就数据
///
#[prost(message, optional, tag = "41")]
pub nhmefdfdmfi: ::core::option::Option<Abkjgdcbglg>,
pub achievement_data: ::core::option::Option<AchievementData>,
#[prost(message, repeated, tag = "200")]
pub gpfoecchico: ::prost::alloc::vec::Vec<Hmchpbjljen>,
/// 这个大概是喵吉长官的完成数据。不打算管
///
#[prost(message, optional, tag = "621")]
pub aigkcfkaadf: ::core::option::Option<Efokkbbamai>,
pub check_item_data: ::core::option::Option<CheckItemData>,
/// 好像是名片数据
///
#[prost(message, optional, tag = "800")]
pub hbhfjgbahgf: ::core::option::Option<Aboegnnepmi>,
pub calling_card_data: ::core::option::Option<CallingCardData>,
#[prost(map = "uint32, int32", tag = "864")]
pub oomkaabgbmb: ::std::collections::HashMap<u32, i32>,
#[prost(message, optional, tag = "1228")]
@ -18201,7 +18219,7 @@ pub struct GetMonthCardDayRewardCsReq {}
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Ipijdhhpbci {
pub struct TeleportData {
#[prost(int32, repeated, tag = "2")]
pub jmjmkpakijj: ::prost::alloc::vec::Vec<i32>,
#[prost(int32, repeated, tag = "15")]
@ -20150,41 +20168,41 @@ pub struct Ocdbdmkhcjl {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Efadpnfaelh {
pub struct ClientSystemsSync {
#[prost(uint32, repeated, tag = "1")]
pub hgfaaejijhk: ::prost::alloc::vec::Vec<u32>,
pub teleport_id_sync: ::prost::alloc::vec::Vec<u32>,
#[prost(message, optional, tag = "2")]
pub ofmndhbgdof: ::core::option::Option<Mgicaigalme>,
#[prost(message, optional, tag = "3")]
pub olgibdhglkg: ::core::option::Option<Jfkllcaiafj>,
pub inter_knot_data: ::core::option::Option<InterKnotSync>,
#[prost(message, optional, tag = "4")]
pub djhjojekegc: ::core::option::Option<Hgdnnkfjcmc>,
pub music_player_data: ::core::option::Option<MusicPlayerSync>,
#[prost(uint32, repeated, tag = "5")]
pub eccpblmlgjd: ::prost::alloc::vec::Vec<u32>,
#[prost(int32, repeated, tag = "6")]
pub nefkjpdkgkg: ::prost::alloc::vec::Vec<i32>,
pub newbie_group_sync: ::prost::alloc::vec::Vec<i32>,
#[prost(uint32, repeated, tag = "7")]
pub phnclmchljo: ::prost::alloc::vec::Vec<u32>,
#[prost(message, optional, tag = "8")]
pub nhmefdfdmfi: ::core::option::Option<Jdbfjibpilf>,
pub achievement_data: ::core::option::Option<AchievementSync>,
#[prost(int32, repeated, tag = "9")]
pub eghlpajmnmj: ::prost::alloc::vec::Vec<i32>,
pub unlock_id_sync: ::prost::alloc::vec::Vec<i32>,
#[prost(message, repeated, tag = "11")]
pub ppfekhgdpbo: ::prost::alloc::vec::Vec<Jmkeekhhheb>,
pub quick_access_data_list: ::prost::alloc::vec::Vec<QuickAccessData>,
#[prost(uint32, repeated, tag = "12")]
pub mbpddlclekm: ::prost::alloc::vec::Vec<u32>,
pub teleport_area_id_sync: ::prost::alloc::vec::Vec<u32>,
#[prost(uint32, repeated, tag = "13")]
pub ooiblmdebdf: ::prost::alloc::vec::Vec<u32>,
#[prost(message, optional, tag = "14")]
pub trashbin_hermit_data: ::core::option::Option<Dmhpnnmggnm>,
pub trashbin_hermit_data: ::core::option::Option<TrashbinHermitSync>,
#[prost(message, optional, tag = "15")]
pub geoagplahii: ::core::option::Option<Noggnmadkfj>,
#[prost(message, optional, tag = "201")]
pub post_girl_data: ::core::option::Option<Jakchjjkccg>,
pub post_girl_data: ::core::option::Option<PostGirlSync>,
#[prost(message, optional, tag = "281")]
pub aigkcfkaadf: ::core::option::Option<Mjhicllmahe>,
pub check_item_data: ::core::option::Option<CheckItemSync>,
#[prost(message, optional, tag = "576")]
pub hbhfjgbahgf: ::core::option::Option<Baodmddgagi>,
pub calling_card_data: ::core::option::Option<CallingCardSync>,
#[prost(message, repeated, tag = "719")]
pub gpfoecchico: ::prost::alloc::vec::Vec<Hmchpbjljen>,
#[prost(message, optional, tag = "947")]
@ -20554,7 +20572,7 @@ pub struct Poceahbflhh {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Gfkfedpdede {
pub struct InterKnotData {
#[prost(uint32, repeated, tag = "2")]
pub gbcaeaicghd: ::prost::alloc::vec::Vec<u32>,
#[prost(message, repeated, tag = "6")]
@ -21557,11 +21575,11 @@ pub struct Jlmgodmceei {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Jcdicmoemef {
pub struct CollectData {
#[prost(int32, repeated, tag = "5")]
pub nkggmfpghgi: ::prost::alloc::vec::Vec<i32>,
pub obtained_list: ::prost::alloc::vec::Vec<i32>,
#[prost(int32, repeated, tag = "10")]
pub llnmdibojio: ::prost::alloc::vec::Vec<i32>,
pub not_viewed_list: ::prost::alloc::vec::Vec<i32>,
}
#[derive(proto_gen::CmdID)]
#[derive(proto_gen::XorFields)]
@ -21643,7 +21661,7 @@ pub struct Maiibgeljck {}
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Mlcoggemeon {
pub struct ChangePostGirlScRsp {
#[xor(1039)]
#[prost(int32, tag = "15")]
pub retcode: i32,
@ -22121,7 +22139,7 @@ pub struct Nlahgfcdang {
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Kakpeoaekgb {
#[prost(uint32, repeated, tag = "2")]
pub nkggmfpghgi: ::prost::alloc::vec::Vec<u32>,
pub obtained_list: ::prost::alloc::vec::Vec<u32>,
#[prost(uint32, repeated, tag = "12")]
pub ihnjbpcpfkf: ::prost::alloc::vec::Vec<u32>,
#[prost(message, repeated, tag = "14")]
@ -25132,7 +25150,7 @@ pub struct Fiplebkibam {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Goemgnfnmbj {
pub struct ModNicknameScRsp {
#[xor(2680)]
#[prost(int32, tag = "14")]
pub retcode: i32,
@ -25528,21 +25546,21 @@ pub struct Bhdmjhjmagn {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Jmkeekhhheb {
pub struct QuickAccessData {
#[xor(1243)]
#[prost(uint32, tag = "9")]
pub bdccceafoak: u32,
#[prost(enumeration = "Ihjpmaigida", tag = "14")]
pub quick_access_index: u32,
#[prost(enumeration = "QuickAccessType", tag = "14")]
pub r#type: i32,
#[xor(8904)]
#[prost(uint32, tag = "15")]
pub kabhjlklfla: u32,
pub btn_id: u32,
}
#[derive(proto_gen::CmdID)]
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Jakchjjkccg {
pub struct PostGirlSync {
#[prost(uint32, repeated, tag = "3")]
pub selected_post_girl_id_list: ::prost::alloc::vec::Vec<u32>,
#[prost(uint32, repeated, tag = "5")]
@ -25971,7 +25989,7 @@ pub struct Pkamllhinjh {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Abkjgdcbglg {
pub struct AchievementData {
#[prost(message, repeated, tag = "13")]
pub emldeipfmma: ::prost::alloc::vec::Vec<Gokcdfmheel>,
}
@ -28136,7 +28154,7 @@ pub struct Cedamedgpaj {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Efokkbbamai {
pub struct CheckItemData {
#[prost(uint32, repeated, tag = "3")]
pub aighoeokkab: ::prost::alloc::vec::Vec<u32>,
}
@ -29373,7 +29391,7 @@ pub struct Dchlkminkdl {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Bmhfpdagadk {
pub struct CollectMapSync {
#[prost(int32, repeated, tag = "1")]
pub oeaialfmgng: ::prost::alloc::vec::Vec<i32>,
#[prost(int32, repeated, tag = "3")]
@ -29846,7 +29864,7 @@ pub struct Cmheepmblfb {}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Adkopojpgdl {
#[prost(message, optional, tag = "1")]
pub ogcaaminman: ::core::option::Option<StringEntry>,
pub quest_data_sync: ::core::option::Option<StringEntry>,
#[prost(enumeration = "Iojdgcmbnmj", tag = "2")]
pub ghggdeglhka: i32,
#[prost(enumeration = "QuestStatus", tag = "3")]
@ -32078,7 +32096,7 @@ pub struct Labeghdefnk {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Ecpbnceekbi {
pub struct QuestDataSync {
#[prost(message, optional, tag = "1")]
pub khlflpbafip: ::core::option::Option<Fgddalkmick>,
#[prost(message, optional, tag = "2")]
@ -33166,7 +33184,7 @@ pub struct Ekiechelnjg {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Ipimhifmfni {
pub struct WorkBenchSync {
#[prost(uint32, repeated, tag = "10")]
pub docddodnlon: ::prost::alloc::vec::Vec<u32>,
#[prost(uint32, repeated, tag = "4")]
@ -33474,7 +33492,7 @@ pub struct Ddjecikoocn {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Knohmlebcbm {
pub struct MusicPlayerData {
#[xor(13457)]
#[prost(uint32, tag = "13")]
pub mhgnhkmhlgj: u32,
@ -34261,7 +34279,7 @@ pub struct Jelnghnjbph {
#[derive(proto_gen::XorFields)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Jdbfjibpilf {
pub struct AchievementSync {
#[prost(message, repeated, tag = "15")]
pub emldeipfmma: ::prost::alloc::vec::Vec<Gokcdfmheel>,
}
@ -37711,29 +37729,29 @@ impl Monopkiegjo {
#[derive(proto_gen::XorFields)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum Ihjpmaigida {
Mdmamoibbhc = 0,
Opbobdiclco = 1,
Apfhncfijli = 2,
pub enum QuickAccessType {
None = 0,
Direct = 1,
QuickMenu = 2,
}
impl Ihjpmaigida {
impl QuickAccessType {
/// String value of the enum field names used in the ProtoBuf definition.
///
/// The values are not transformed in any way and thus are considered stable
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
pub fn as_str_name(&self) -> &'static str {
match self {
Ihjpmaigida::Mdmamoibbhc => "IHJPMAIGIDA_MDMAMOIBBHC",
Ihjpmaigida::Opbobdiclco => "IHJPMAIGIDA_OPBOBDICLCO",
Ihjpmaigida::Apfhncfijli => "IHJPMAIGIDA_APFHNCFIJLI",
QuickAccessType::None => "QUICK_ACCESS_TYPE_NONE",
QuickAccessType::Direct => "QUICK_ACCESS_TYPE_DIRECT",
QuickAccessType::QuickMenu => "QUICK_ACCESS_TYPE_QUICK_MENU",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"IHJPMAIGIDA_MDMAMOIBBHC" => Some(Self::Mdmamoibbhc),
"IHJPMAIGIDA_OPBOBDICLCO" => Some(Self::Opbobdiclco),
"IHJPMAIGIDA_APFHNCFIJLI" => Some(Self::Apfhncfijli),
"QUICK_ACCESS_TYPE_NONE" => Some(Self::None),
"QUICK_ACCESS_TYPE_DIRECT" => Some(Self::Direct),
"QUICK_ACCESS_TYPE_QUICK_MENU" => Some(Self::QuickMenu),
_ => None,
}
}

View file

@ -14,12 +14,16 @@ pub struct BasicDataModelBin {
pub frontend_avatar_id: i32,
#[prost(int32, tag = "6")]
pub beginner_procedure_id: i32,
#[prost(uint32, tag = "7")]
pub selected_post_girl_id: u32,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct LockModelBin {
#[prost(int32, repeated, tag = "1")]
pub unlock_list: ::prost::alloc::vec::Vec<i32>,
#[prost(map = "uint32, uint32", tag = "2")]
pub quick_access_list: ::std::collections::HashMap<u32, u32>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]