Compare commits

...

26 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
3c2397cdbd README: update feature list 2024-08-05 15:08:00 +00:00
356fc5278e server version: milestone 2 reached (0.2.0) 2024-08-05 17:59:18 +03:00
50f694add0 H.D.D. and Combat commissions implementation
Implement Combat commissions (PureHollowBattle and LongFight) (including Rally commissions)
Refactor some battle structures
Unlock hollow quests (QuestInfo and YorozuyaInfo)
2024-08-05 17:45:13 +03:00
f99165e452 chore: move new dependencies to workspace's Cargo.toml 2024-08-04 16:20:31 +03:00
8d1211750b chore: fix warnings 2024-08-04 15:46:43 +03:00
285fc0b6fa NewsStand interaction 2024-08-04 15:40:06 +03:00
55b7ed3beb Gacha System implementation
## Abstract

This PR implements

- The Gacha System for client. Player can pull in defined pools with a similar experience to zenless & mihoyo gacha, and these status can be saved through player save and recovered.
- `player kick` command in extra. Command `player kick [player_uid] (optional: [reason_id])` can be used to kick a player to offline.

## Support list

- Similar probability to mihoyo gacha
- QingYi & Jane Doe's Agent & W-Engine banner
- Standard Banner:
  - Get a S Agent in the first 50 pulls
  - Get a 20% discount for the first 5 10-pulls
  - Choose a S Agent once you pulled for 300
- ~~Bangboo Banner~~ (not working due to other reasons)
  - Choosing your demanded S bangboo for 100% UP

## Principle

- A complex configuration file `gacha.jsonc` is introduced to define all behaviours.
- Gacha status is saved via `GachaModelBin`.

For more informations about the config and save, an article is available [here](https://yyheggegg.github.io/mihomo-gio-blogs/zzz-gacha-impl-en/).

## Known issues

- You can not see any bangboos in the collection when choosing Bangboo.
- **Specifically for 1.1 Beta**, performing gacha may lead to random client issues, including:
  - The TVs showing rarity ending up in blank after its flash.
  - Game become black screen.
  - If clicking 'Skip' but not fast enough, game'll stuck and not able to do anything. You may try to click 'Skip' scarcely when 'REC' shows, or after all animations has stopped.

Co-authored-by: YYHEggEgg <53960525+YYHEggEgg@users.noreply.github.com>
Reviewed-on: #1
Co-authored-by: YYHEggEgg <yyheggegg@xeondev.com>
Co-committed-by: YYHEggEgg <yyheggegg@xeondev.com>
2024-08-04 11:41:23 +00:00
220fbc42f0 Implement bangboo for battles (works in archive quests) 2024-08-01 02:43:13 +03:00
7d7bd76ae8 Implement MainCityBgmConfigTemplate 2024-08-01 01:38:05 +03:00
26fe9551d4 MainCity BGM 2024-07-31 14:08:06 +03:00
970dde020a Working OpenUI interacts and Archive battles
Implement OpenUI interacts (currently configs only for Yorozuya and Archive)
Fully working Archive (battles and cutscenes)
2024-07-30 03:04:55 +03:00
2ccdf84dd0 SceneUnitManager: properly determine and spawn only default objects 2024-07-29 23:33:13 +03:00
fc121c7cba generated more configs for transitions 2024-07-27 00:27:23 +03:00
cbe169fe1a Implement event configs, more interactions work
Implement interact event-action configurations.
More interactions work now, for more transitions, we just need to generate more configs (soon)
NOTE: this commit BREAKS your nap_gameserver.toml. To fix it, you have to add 'event_config_path' property into [assets] section of your config.
2024-07-26 22:44:44 +03:00
c710716fa1 [WIP] event graph stuff
implementing EventGraph stuff, currently hardcoded interaction for 10000001 (street to workshop transition)
2024-07-25 22:39:59 +03:00
d196ef861a SceneUnitManager: create default scene units 2024-07-25 19:49:39 +03:00
d3cd213d27 use AvatarBaseID instead of raw integer in more places 2024-07-25 13:03:37 +03:00
a7da14c109 don't panic if readline lib fails to initialize
fixes startup crash on non-standard TTYs
2024-07-25 12:36:22 +03:00
99123a15ef newtype fun
use newtypes for template ids, pros:
1) enforces id validation
2) ease of use (helper method for getting template struct right away)
2024-07-25 01:24:48 +03:00
69634fda64 Implement weapons
Implement Weapon add, save, equip
Implement 'item add_weapon' command
2024-07-24 21:36:02 +03:00
370f141f88 more commands, unlock only default avatars at beginning
Implement 'avatar add' command
Implemented 'player procedure' command
2024-07-23 18:12:05 +03:00
4b86d62d3f Networking improvement and player nickname change console command
split TcpStream into read and write halves, so it's possible to recv and send concurrently
implement 'player nickname' command
2024-07-23 00:04:59 +03:00
9af61cd33f simple CLI command system
Implement CLI command system
command for changing player's frontend avatar
2024-07-22 19:47:04 +03:00
123 changed files with 453997 additions and 888 deletions

View file

@ -13,6 +13,8 @@ tokio-util = { version = "0.7.10", features = ["io"] }
# Serialization
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
jsonc-parser = { version = "0.23.0", features = ["serde"] }
chrono = { version = "0.4.38", features = ["serde"] }
toml = "0.8.4"
rbase64 = "2.0.3"
prost = "0.12.3"
@ -61,6 +63,7 @@ atomic_enum = "0.3.0"
num_enum = "0.7.2"
dashmap = "6.0.1"
regex = "1.10.5"
rustyline-async = "0.4.2"
ansi_term = "0.12.1"
# Internal

View file

@ -6,9 +6,13 @@
### Current features
- Logging in
- Fully playable prologue
- HollowDeepDive with Combat and Rally commissions
- Archive (playable cutscenes and battles)
- Open world
- World exploration with any character (can be changed with a command)
- Player progress saving with PostgreSQL
- Unlock all characters
- Training battle
- Player management command system
- Training
### Requirements
- [Rust](https://www.rust-lang.org/tools/install)

View file

@ -0,0 +1,215 @@
[
{
"event_id": 10000001,
"actions": [
{
"$type": "SwitchSection",
"section_id": 2,
"camera_x": 1,
"camera_y": 6000,
"transform": "Workshop_PlayerPos_FromStreet"
}
]
},
{
"event_id": 10000002,
"actions": [
{
"$type": "SwitchSection",
"section_id": 3,
"camera_x": 1,
"camera_y": 6000,
"transform": "Garage_PlayerPos_FromStreet"
}
]
},
{
"event_id": 10000003,
"actions": [
{
"$type": "SwitchSection",
"section_id": 1,
"camera_x": 1,
"camera_y": 6000,
"transform": "Street_PlayerPos_FromWorkshop"
}
]
},
{
"event_id": 10000004,
"actions": [
{
"$type": "SwitchSection",
"section_id": 3,
"camera_x": 1,
"camera_y": 6000,
"transform": "Garage_PlayerPos_FromWorkshop"
}
]
},
{
"event_id": 10000005,
"actions": [
{
"$type": "SwitchSection",
"section_id": 1,
"camera_x": 1,
"camera_y": 6000,
"transform": "Street_PlayerPos_FromGarage"
}
]
},
{
"event_id": 10000006,
"actions": [
{
"$type": "SwitchSection",
"section_id": 2,
"camera_x": 1,
"camera_y": 6000,
"transform": "Workshop_PlayerPos_FromGarage"
}
]
},
{
"event_id": 10000016,
"actions": [
{
"$type": "SwitchSection",
"section_id": 3,
"camera_x": 1,
"camera_y": 6000,
"transform": "Garage_PlayerPos_FromStreet2"
}
]
},
{
"event_id": 10000017,
"actions": [
{
"$type": "SwitchSection",
"section_id": 1,
"camera_x": 1,
"camera_y": 6000,
"transform": "Street_PlayerPos_FromGarage2"
}
]
},
{
"event_id": 15001601,
"actions": [
{
"$type": "SwitchSection",
"section_id": 153,
"camera_x": 1,
"camera_y": 6000,
"transform": "Subway_PlayerPos_FromStreet"
}
]
},
{
"event_id": 15001102,
"actions": [
{
"$type": "SwitchSection",
"section_id": 1,
"camera_x": 1,
"camera_y": 6000,
"transform": "Street_PlayerPos_FromZero"
}
]
},
{
"event_id": 15001302,
"actions": [
{
"$type": "SwitchSection",
"section_id": 101,
"camera_x": 1,
"camera_y": 6000,
"transform": "Square_PlayerPos_FromFortuneHIA"
}
]
},
{
"event_id": 15001303,
"actions": [
{
"$type": "SwitchSection",
"section_id": 101,
"camera_x": 1,
"camera_y": 6000,
"transform": "Square_PlayerPos_FromFortuneHIA"
}
]
},
{
"event_id": 15001701,
"actions": [
{
"$type": "SwitchSection",
"section_id": 153,
"camera_x": 1,
"camera_y": 6000,
"transform": "Subway_PlayerPos_FromFortuneSquare01"
}
]
},
{
"event_id": 15001702,
"actions": [
{
"$type": "SwitchSection",
"section_id": 103,
"camera_x": 1,
"camera_y": 6000,
"transform": "HIA_PlayerPos_FromFortuneSquare01"
}
]
},
{
"event_id": 15001701,
"actions": [
{
"$type": "SwitchSection",
"section_id": 153,
"camera_x": 1,
"camera_y": 6000,
"transform": "Subway_PlayerPos_FromFortuneSquare02"
}
]
},
{
"event_id": 10000009,
"actions": [
{
"$type": "OpenUi",
"ui": "UIYorozuyaPage",
"args": 0,
"store_template_id": 1161
}
]
},
{
"event_id": 10000010,
"actions": [
{
"$type": "OpenUi",
"ui": "UIMainStoryPage",
"args": 0,
"store_template_id": 1191
}
]
},
{
"event_id": 15000301,
"actions": [
{
"$type": "OpenUi",
"ui": "UINewsStandPageController",
"args": 0,
"store_template_id": 1061
}
]
}
]

View file

@ -0,0 +1,342 @@
[
{
"ID": 107010011,
"QuestName": "QuestName_107010011",
"QuestDesc": "QuestDesc_107010011",
"Target": "QuestTarget_107010011",
"QuestType": 1,
"Chapter": 1,
"Difficulty": 1,
"MonsterLevel": 1,
"RecommendedLevel": 1,
"EBCDABBGHMF": 3,
"MLLPFMLKIKF": 0,
"HollowID": 19900014,
"FirstBattleEventID": 19900014,
"BattleEventID": 19900019,
"GGAEGKMMGLN": 999,
"BattleRank": "1|101|208",
"Slot1Avatar": 101000101,
"Slot2Avatar": -1,
"Slot3Avatar": -1,
"Buddy": 0,
"HGJGIMKPHLH": false,
"LHNGFLLKHED": 0,
"JGKFLKJNNHI": false,
"KIFDIIKMIBJ": "Chapter0",
"EDECGDLJIEB": "UI/Sprite/A1DynamicLoad/Hollow/ImgMission/UnPacker/ImgMission01.png",
"JGBDOPDIDHA": "",
"DGLENAGIGBO": false,
"NPEBHOLENNI": 205,
"NEIIOOLBAPD": false,
"NABBCKLEDME": 0,
"FBDEJOEECMJ": "",
"CPJIMNGMCDO": 0
},
{
"ID": 107010021,
"QuestName": "QuestName_107010021",
"QuestDesc": "QuestDesc_107010021",
"Target": "QuestTarget_107010021",
"QuestType": 1,
"Chapter": 1,
"Difficulty": 1,
"MonsterLevel": 1,
"RecommendedLevel": 1,
"EBCDABBGHMF": 3,
"MLLPFMLKIKF": 0,
"HollowID": 19900015,
"FirstBattleEventID": 19900015,
"BattleEventID": 19900015,
"GGAEGKMMGLN": 999,
"BattleRank": "1|101|208",
"Slot1Avatar": 101000201,
"Slot2Avatar": 101000202,
"Slot3Avatar": -1,
"Buddy": 0,
"HGJGIMKPHLH": false,
"LHNGFLLKHED": 0,
"JGKFLKJNNHI": false,
"KIFDIIKMIBJ": "Chapter0",
"EDECGDLJIEB": "UI/Sprite/A1DynamicLoad/Hollow/ImgMission/UnPacker/ImgMission01.png",
"JGBDOPDIDHA": "",
"DGLENAGIGBO": false,
"NPEBHOLENNI": 205,
"NEIIOOLBAPD": false,
"NABBCKLEDME": 0,
"FBDEJOEECMJ": "",
"CPJIMNGMCDO": 0
},
{
"ID": 107010031,
"QuestName": "QuestName_107010031",
"QuestDesc": "QuestDesc_107010031",
"Target": "QuestTarget_107010031",
"QuestType": 1,
"Chapter": 1,
"Difficulty": 1,
"MonsterLevel": 1,
"RecommendedLevel": 1,
"EBCDABBGHMF": 3,
"MLLPFMLKIKF": 0,
"HollowID": 19900016,
"FirstBattleEventID": 19900016,
"BattleEventID": 19900016,
"GGAEGKMMGLN": 999,
"BattleRank": "1|101|208",
"Slot1Avatar": 101000302,
"Slot2Avatar": 101000301,
"Slot3Avatar": -1,
"Buddy": 0,
"HGJGIMKPHLH": false,
"LHNGFLLKHED": 0,
"JGKFLKJNNHI": false,
"KIFDIIKMIBJ": "Chapter0",
"EDECGDLJIEB": "UI/Sprite/A1DynamicLoad/Hollow/ImgMission/UnPacker/ImgMission01.png",
"JGBDOPDIDHA": "",
"DGLENAGIGBO": false,
"NPEBHOLENNI": 205,
"NEIIOOLBAPD": false,
"NABBCKLEDME": 0,
"FBDEJOEECMJ": "",
"CPJIMNGMCDO": 0
},
{
"ID": 107010051,
"QuestName": "QuestName_107010051",
"QuestDesc": "QuestDesc_107010051",
"Target": "QuestTarget_107010051",
"QuestType": 1,
"Chapter": 1,
"Difficulty": 1,
"MonsterLevel": 6,
"RecommendedLevel": 6,
"EBCDABBGHMF": 3,
"MLLPFMLKIKF": 0,
"HollowID": 19900017,
"FirstBattleEventID": 19900017,
"BattleEventID": 19900017,
"GGAEGKMMGLN": 999,
"BattleRank": "1|101|208",
"Slot1Avatar": 101000501,
"Slot2Avatar": 101000502,
"Slot3Avatar": 101000503,
"Buddy": 101000504,
"HGJGIMKPHLH": true,
"LHNGFLLKHED": 2,
"JGKFLKJNNHI": true,
"KIFDIIKMIBJ": "Chapter0",
"EDECGDLJIEB": "UI/Sprite/A1DynamicLoad/Hollow/ImgMission/UnPacker/ImgMission01.png",
"JGBDOPDIDHA": "",
"DGLENAGIGBO": false,
"NPEBHOLENNI": 205,
"NEIIOOLBAPD": false,
"NABBCKLEDME": 0,
"FBDEJOEECMJ": "",
"CPJIMNGMCDO": 0
},
{
"ID": 107020021,
"QuestName": "QuestName_107020021",
"QuestDesc": "QuestDesc_107020021",
"Target": "QuestTarget_107020021",
"QuestType": 1,
"Chapter": 2,
"Difficulty": 1,
"MonsterLevel": 9,
"RecommendedLevel": 9,
"EBCDABBGHMF": 3,
"MLLPFMLKIKF": 0,
"HollowID": 10202101,
"FirstBattleEventID": 10202101,
"BattleEventID": 10202101,
"GGAEGKMMGLN": 999,
"BattleRank": "1|101|208",
"Slot1Avatar": 1000010101,
"Slot2Avatar": 1000010102,
"Slot3Avatar": 1000010103,
"Buddy": 1000010104,
"HGJGIMKPHLH": true,
"LHNGFLLKHED": 2,
"JGKFLKJNNHI": false,
"KIFDIIKMIBJ": "Chapter0",
"EDECGDLJIEB": "UI/Sprite/A1DynamicLoad/Hollow/ImgMission/UnPacker/ImgMission01.png",
"JGBDOPDIDHA": "",
"DGLENAGIGBO": false,
"NPEBHOLENNI": 205,
"NEIIOOLBAPD": false,
"NABBCKLEDME": 0,
"FBDEJOEECMJ": "",
"CPJIMNGMCDO": 0
},
{
"ID": 107020031,
"QuestName": "QuestName_107020031",
"QuestDesc": "QuestDesc_107020031",
"Target": "QuestTarget_107020031",
"QuestType": 1,
"Chapter": 2,
"Difficulty": 1,
"MonsterLevel": 9,
"RecommendedLevel": 9,
"EBCDABBGHMF": 3,
"MLLPFMLKIKF": 0,
"HollowID": 10202102,
"FirstBattleEventID": 10202102,
"BattleEventID": 10202102,
"GGAEGKMMGLN": 999,
"BattleRank": "1|101|208",
"Slot1Avatar": 1000010201,
"Slot2Avatar": 1000010202,
"Slot3Avatar": 1000010203,
"Buddy": 1000010204,
"HGJGIMKPHLH": true,
"LHNGFLLKHED": 2,
"JGKFLKJNNHI": false,
"KIFDIIKMIBJ": "Chapter0",
"EDECGDLJIEB": "UI/Sprite/A1DynamicLoad/Hollow/ImgMission/UnPacker/ImgMission01.png",
"JGBDOPDIDHA": "",
"DGLENAGIGBO": false,
"NPEBHOLENNI": 205,
"NEIIOOLBAPD": false,
"NABBCKLEDME": 0,
"FBDEJOEECMJ": "",
"CPJIMNGMCDO": 0
},
{
"ID": 107020041,
"QuestName": "QuestName_107020041",
"QuestDesc": "QuestDesc_107020041",
"Target": "QuestTarget_107020041",
"QuestType": 1,
"Chapter": 2,
"Difficulty": 1,
"MonsterLevel": 9,
"RecommendedLevel": 9,
"EBCDABBGHMF": 3,
"MLLPFMLKIKF": 0,
"HollowID": 10202103,
"FirstBattleEventID": 10202103,
"BattleEventID": 10202103,
"GGAEGKMMGLN": 999,
"BattleRank": "1|101|208",
"Slot1Avatar": 1000010301,
"Slot2Avatar": 1000010302,
"Slot3Avatar": 1000010303,
"Buddy": 1000010304,
"HGJGIMKPHLH": true,
"LHNGFLLKHED": 2,
"JGKFLKJNNHI": false,
"KIFDIIKMIBJ": "Chapter0",
"EDECGDLJIEB": "UI/Sprite/A1DynamicLoad/Hollow/ImgMission/UnPacker/ImgMission01.png",
"JGBDOPDIDHA": "",
"DGLENAGIGBO": false,
"NPEBHOLENNI": 205,
"NEIIOOLBAPD": false,
"NABBCKLEDME": 0,
"FBDEJOEECMJ": "",
"CPJIMNGMCDO": 0
},
{
"ID": 107030021,
"QuestName": "QuestName_107030021",
"QuestDesc": "QuestDesc_107030021",
"Target": "QuestTarget_107030021",
"QuestType": 1,
"Chapter": 3,
"Difficulty": 1,
"MonsterLevel": 11,
"RecommendedLevel": 11,
"EBCDABBGHMF": 4,
"MLLPFMLKIKF": 0,
"HollowID": 10202104,
"FirstBattleEventID": 10202104,
"BattleEventID": 10202104,
"GGAEGKMMGLN": 999,
"BattleRank": "1|101|208",
"Slot1Avatar": 1000010601,
"Slot2Avatar": 1000010602,
"Slot3Avatar": 1000010603,
"Buddy": 1000010604,
"HGJGIMKPHLH": true,
"LHNGFLLKHED": 2,
"JGKFLKJNNHI": false,
"KIFDIIKMIBJ": "Chapter0",
"EDECGDLJIEB": "UI/Sprite/A1DynamicLoad/Hollow/ImgMission/UnPacker/ImgMission01.png",
"JGBDOPDIDHA": "",
"DGLENAGIGBO": false,
"NPEBHOLENNI": 205,
"NEIIOOLBAPD": false,
"NABBCKLEDME": 0,
"FBDEJOEECMJ": "",
"CPJIMNGMCDO": 0
},
{
"ID": 107030031,
"QuestName": "QuestName_107030031",
"QuestDesc": "QuestDesc_107030031",
"Target": "QuestTarget_107030031",
"QuestType": 1,
"Chapter": 3,
"Difficulty": 1,
"MonsterLevel": 12,
"RecommendedLevel": 12,
"EBCDABBGHMF": 4,
"MLLPFMLKIKF": 0,
"HollowID": 10202106,
"FirstBattleEventID": 10202106,
"BattleEventID": 10202106,
"GGAEGKMMGLN": 999,
"BattleRank": "1|101|208",
"Slot1Avatar": 1000010701,
"Slot2Avatar": 1000010702,
"Slot3Avatar": 1000010703,
"Buddy": 1000010704,
"HGJGIMKPHLH": true,
"LHNGFLLKHED": 2,
"JGKFLKJNNHI": false,
"KIFDIIKMIBJ": "Chapter0",
"EDECGDLJIEB": "UI/Sprite/A1DynamicLoad/Hollow/ImgMission/UnPacker/ImgMission01.png",
"JGBDOPDIDHA": "",
"DGLENAGIGBO": false,
"NPEBHOLENNI": 205,
"NEIIOOLBAPD": false,
"NABBCKLEDME": 0,
"FBDEJOEECMJ": "",
"CPJIMNGMCDO": 0
},
{
"ID": 107030041,
"QuestName": "QuestName_107030041",
"QuestDesc": "QuestDesc_107030041",
"Target": "QuestTarget_107030041",
"QuestType": 1,
"Chapter": 3,
"Difficulty": 1,
"MonsterLevel": 13,
"RecommendedLevel": 13,
"EBCDABBGHMF": 4,
"MLLPFMLKIKF": 0,
"HollowID": 10202108,
"FirstBattleEventID": 10202108,
"BattleEventID": 10202108,
"GGAEGKMMGLN": 999,
"BattleRank": "1|101|208",
"Slot1Avatar": 1000010801,
"Slot2Avatar": 1000010802,
"Slot3Avatar": 1000010803,
"Buddy": 1000010804,
"HGJGIMKPHLH": true,
"LHNGFLLKHED": 2,
"JGKFLKJNNHI": false,
"KIFDIIKMIBJ": "Chapter0",
"EDECGDLJIEB": "UI/Sprite/A1DynamicLoad/Hollow/ImgMission/UnPacker/ImgMission01.png",
"JGBDOPDIDHA": "",
"DGLENAGIGBO": false,
"NPEBHOLENNI": 205,
"NEIIOOLBAPD": false,
"NABBCKLEDME": 0,
"FBDEJOEECMJ": "",
"CPJIMNGMCDO": 0
}
]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

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
}
]

View file

@ -0,0 +1,212 @@
[
{
"ID": 100001,
"FOPDNLNNHDJ": 1000,
"DIIDBBGLDOL": "HollowGroupNameText_Arcade",
"EFPBDDJIJBO": "1040140114",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_RallyQuest.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_L.prefab",
"LDKNDDHOHND": "FF0019FF",
"EEBAIANJMEC": "006EFFFF|FF001900",
"EKMLECIKMID": false,
"FNIGJDFMHBA": "",
"BECFHCOGPKG": ""
},
{
"ID": 100101,
"FOPDNLNNHDJ": 1001,
"DIIDBBGLDOL": "HollowGroupNameText_Story",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Story.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_M.prefab",
"LDKNDDHOHND": "FF9500FF",
"EEBAIANJMEC": "FF9500FF|FFDE0000",
"EKMLECIKMID": false,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_01"
},
{
"ID": 100102,
"FOPDNLNNHDJ": 1001,
"DIIDBBGLDOL": "HollowGroupNameText_exploration",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Explore.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_M02.prefab",
"LDKNDDHOHND": "00A1FFFF",
"EEBAIANJMEC": "00A1FFFF|00FFD300",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips03.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_03"
},
{
"ID": 100103,
"FOPDNLNNHDJ": 1001,
"DIIDBBGLDOL": "HollowGroupNameText_combat",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Combat.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_S.prefab",
"LDKNDDHOHND": "00A1FFFF",
"EEBAIANJMEC": "00A1FFFF|00FFD300",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips02.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_02"
},
{
"ID": 100201,
"FOPDNLNNHDJ": 1002,
"DIIDBBGLDOL": "HollowGroupNameText_Story",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Story.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_M.prefab",
"LDKNDDHOHND": "FF9500FF",
"EEBAIANJMEC": "FF9500FF|FFDE0000",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_01"
},
{
"ID": 100202,
"FOPDNLNNHDJ": 1002,
"DIIDBBGLDOL": "HollowGroupNameText_exploration",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Explore.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_M02.prefab",
"LDKNDDHOHND": "00A1FFFF",
"EEBAIANJMEC": "00A1FFFF|00FFD300",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips03.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_03"
},
{
"ID": 100203,
"FOPDNLNNHDJ": 1002,
"DIIDBBGLDOL": "HollowGroupNameText_combat",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Combat.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_S.prefab",
"LDKNDDHOHND": "00A1FFFF",
"EEBAIANJMEC": "00A1FFFF|00FFD300",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips02.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_02"
},
{
"ID": 100301,
"FOPDNLNNHDJ": 1003,
"DIIDBBGLDOL": "HollowGroupNameText_Story",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Story.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_M.prefab",
"LDKNDDHOHND": "FF9500FF",
"EEBAIANJMEC": "FF9500FF|FFDE0000",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_01"
},
{
"ID": 100302,
"FOPDNLNNHDJ": 1003,
"DIIDBBGLDOL": "HollowGroupNameText_exploration",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Explore.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_M02.prefab",
"LDKNDDHOHND": "00A1FFFF",
"EEBAIANJMEC": "00A1FFFF|00FFD300",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips03.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_03"
},
{
"ID": 100303,
"FOPDNLNNHDJ": 1003,
"DIIDBBGLDOL": "HollowGroupNameText_combat",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Combat.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_S.prefab",
"LDKNDDHOHND": "00A1FFFF",
"EEBAIANJMEC": "00A1FFFF|00FFD300",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips02.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_02"
},
{
"ID": 100401,
"FOPDNLNNHDJ": 1004,
"DIIDBBGLDOL": "HollowGroupNameText_BigWorld",
"EFPBDDJIJBO": "1100140008",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_HighRisk.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_L.prefab",
"LDKNDDHOHND": "00A1FFFF",
"EEBAIANJMEC": "00A1FFFF|00FFD300",
"EKMLECIKMID": false,
"FNIGJDFMHBA": "",
"BECFHCOGPKG": ""
},
{
"ID": 100501,
"FOPDNLNNHDJ": 1005,
"DIIDBBGLDOL": "HollowGroupNameText_Story",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Story.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_M.prefab",
"LDKNDDHOHND": "FF9500FF",
"EEBAIANJMEC": "FF9500FF|FFDE0000",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_01"
},
{
"ID": 100502,
"FOPDNLNNHDJ": 1005,
"DIIDBBGLDOL": "HollowGroupNameText_exploration",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Explore.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_M02.prefab",
"LDKNDDHOHND": "00A1FFFF",
"EEBAIANJMEC": "00A1FFFF|00FFD300",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips03.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_03"
},
{
"ID": 100503,
"FOPDNLNNHDJ": 1005,
"DIIDBBGLDOL": "HollowGroupNameText_combat",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Combat.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_S.prefab",
"LDKNDDHOHND": "00A1FFFF",
"EEBAIANJMEC": "00A1FFFF|00FFD300",
"EKMLECIKMID": true,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips02.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_02"
},
{
"ID": 109901,
"FOPDNLNNHDJ": 1099,
"DIIDBBGLDOL": "HollowGroupNameText_Story",
"EFPBDDJIJBO": "",
"OCBLGHECPLH": "Hollow_LockedHint_01",
"BEIEFNLAJCH": "UI/Sprite/A1DynamicLoad/YorozuyaPage/UnPacker/Icon_Story.png",
"CCFBKFKFJGM": "UI/3D/Yorozuya/Yorozuya_Hollow_Sphere_M.prefab",
"LDKNDDHOHND": "FF9500FF",
"EEBAIANJMEC": "FF9500FF|FFDE0000",
"EKMLECIKMID": false,
"FNIGJDFMHBA": "UI/Menus/Widget/Yorozuya/Icon_MapTips.prefab",
"BECFHCOGPKG": "YorozuyaMapTipsUnLockChange_01"
}
]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,32 @@
[
{
"ID": 0,
"PlayEventName": "Play_BGM_Maincity",
"StateName": "Normal"
},
{
"ID": 1001,
"PlayEventName": "Play_BGM_Maincity",
"StateName": "Hack_Maincity"
},
{
"ID": 1002,
"PlayEventName": "Play_BGM_Maincity",
"StateName": "WorkShop_OnMission"
},
{
"ID": 1003,
"PlayEventName": "Play_BGM_Maincity",
"StateName": "SummerActivity_Ready"
},
{
"ID": 1004,
"PlayEventName": "Play_BGM_Maincity",
"StateName": "SummerActivity_Start"
},
{
"ID": 1005,
"PlayEventName": "Play_BGM_Maincity",
"StateName": "Mission_Accomplish"
}
]

View file

@ -0,0 +1,800 @@
[
{
"TagID": 1,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 2,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 3,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 4,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 5,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 6,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 7,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 8,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 9,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 10,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 1001,
"MCHJEPCIEOC": "1040140086",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 1002,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 1003,
"MCHJEPCIEOC": "1040140214",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 1004,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 1005,
"MCHJEPCIEOC": "1040140077",
"EFPBDDJIJBO": "1040140077",
"InteractIDs": [10000014],
"MOBKGMGMIDA": ""
},
{
"TagID": 1006,
"MCHJEPCIEOC": "1040140073",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 1008,
"MCHJEPCIEOC": "1040140070",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 1009,
"MCHJEPCIEOC": "1040140126",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 1011,
"MCHJEPCIEOC": "1040140091",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 1012,
"MCHJEPCIEOC": "1040140071",
"EFPBDDJIJBO": "1040140071",
"InteractIDs": [10000029],
"MOBKGMGMIDA": ""
},
{
"TagID": 1018,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 2016,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "1040140073",
"InteractIDs": [10000015],
"MOBKGMGMIDA": ""
},
{
"TagID": 2017,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 2018,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 2019,
"MCHJEPCIEOC": "1040140082",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 2028,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "1040140117",
"InteractIDs": [100700],
"MOBKGMGMIDA": ""
},
{
"TagID": 2099,
"MCHJEPCIEOC": "1040140115",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": "0:1|0:2|0:3"
},
{
"TagID": 5004,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 5005,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 5007,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 5009,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 5010,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 5011,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 5012,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 5013,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 5014,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 5015,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 5016,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 5017,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 5018,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 5019,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 10022,
"MCHJEPCIEOC": "1060140220",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 20281,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 100301,
"MCHJEPCIEOC": "1040140072",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 100303,
"MCHJEPCIEOC": "1040140072",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 100503,
"MCHJEPCIEOC": "1040140126",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 100603,
"MCHJEPCIEOC": "1040140070",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 100901,
"MCHJEPCIEOC": "1040890044",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 100910,
"MCHJEPCIEOC": "1040140263",
"EFPBDDJIJBO": "1040140263",
"InteractIDs": [15001404],
"MOBKGMGMIDA": ""
},
{
"TagID": 100913,
"MCHJEPCIEOC": "1040140264",
"EFPBDDJIJBO": "1040140264",
"InteractIDs": [15001405],
"MOBKGMGMIDA": ""
},
{
"TagID": 100914,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 100915,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 100916,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 101016,
"MCHJEPCIEOC": "1040140071",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 101017,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 101018,
"MCHJEPCIEOC": "1040140071",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 101202,
"MCHJEPCIEOC": "1040140115",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": "0:4"
},
{
"TagID": 102101,
"MCHJEPCIEOC": "1040140114",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 102102,
"MCHJEPCIEOC": "1040140298",
"EFPBDDJIJBO": "1040140298",
"InteractIDs": [15001109],
"MOBKGMGMIDA": ""
},
{
"TagID": 102201,
"MCHJEPCIEOC": "1040140215",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 102202,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "1060140051",
"InteractIDs": [15001104],
"MOBKGMGMIDA": ""
},
{
"TagID": 102204,
"MCHJEPCIEOC": "1040140298",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 102205,
"MCHJEPCIEOC": "1040140193",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 102302,
"MCHJEPCIEOC": "1040140193",
"EFPBDDJIJBO": "1040140193",
"InteractIDs": [15001108],
"MOBKGMGMIDA": ""
},
{
"TagID": 102501,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 102502,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 102503,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 102504,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 102601,
"MCHJEPCIEOC": "1060140044",
"EFPBDDJIJBO": "1060140044",
"InteractIDs": [15001107],
"MOBKGMGMIDA": ""
},
{
"TagID": 102602,
"MCHJEPCIEOC": "1060140044",
"EFPBDDJIJBO": "1060140044",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 103101,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "1040140339",
"InteractIDs": [15001201],
"MOBKGMGMIDA": ""
},
{
"TagID": 103201,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 103202,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 103203,
"MCHJEPCIEOC": "1040140339",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 103302,
"MCHJEPCIEOC": "1040140260",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 104101,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 104102,
"MCHJEPCIEOC": "1040140260",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 104108,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 104110,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 104111,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 104112,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 104113,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 104201,
"MCHJEPCIEOC": "1040140302",
"EFPBDDJIJBO": "1040140302",
"InteractIDs": [15001801],
"MOBKGMGMIDA": ""
},
{
"TagID": 104202,
"MCHJEPCIEOC": "1030140001",
"EFPBDDJIJBO": "1030140001",
"InteractIDs": [15001802],
"MOBKGMGMIDA": ""
},
{
"TagID": 104205,
"MCHJEPCIEOC": "1030140002",
"EFPBDDJIJBO": "1030140002",
"InteractIDs": [15001803],
"MOBKGMGMIDA": ""
},
{
"TagID": 104206,
"MCHJEPCIEOC": "1030140003",
"EFPBDDJIJBO": "1030140003",
"InteractIDs": [15001804],
"MOBKGMGMIDA": ""
},
{
"TagID": 202646,
"MCHJEPCIEOC": "1040140279",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 500021,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": "0:4"
},
{
"TagID": 500022,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": "0:3"
},
{
"TagID": 500023,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": "0:2"
},
{
"TagID": 500024,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": "0:1"
},
{
"TagID": 500025,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": "0:2"
},
{
"TagID": 500026,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": "0:1"
},
{
"TagID": 504001,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 504002,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 504003,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 504004,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 504005,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 504006,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 504007,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 504008,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 504009,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 504010,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 520001,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 520002,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": "0:3"
},
{
"TagID": 41120101,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 41120201,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 41120202,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 41120203,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 41120301,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 41120302,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 41120401,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": "0:1|0:2|0:3"
},
{
"TagID": 41500001,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
},
{
"TagID": 241062001,
"MCHJEPCIEOC": "",
"EFPBDDJIJBO": "",
"InteractIDs": [],
"MOBKGMGMIDA": ""
}
]

File diff suppressed because it is too large Load diff

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

View file

@ -0,0 +1,882 @@
[
{
"AreaID": 500000101,
"BattleEventID": 5000001,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000001_1.asset",
"MDMOGIDKCLG": "metro4_2",
"LLPNKBBJHKL": "PlayerPos_E2",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000102,
"BattleEventID": 5000001,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000001_2.asset",
"MDMOGIDKCLG": "metro4_2",
"LLPNKBBJHKL": "PlayerPos_E1",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000103,
"BattleEventID": 5000001,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000001_3.asset",
"MDMOGIDKCLG": "metro4_2",
"LLPNKBBJHKL": "PlayerPos_E1",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000201,
"BattleEventID": 5000002,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000002_1.asset",
"MDMOGIDKCLG": "Construction2_3_Boss",
"LLPNKBBJHKL": "PlayerPos_B5",
"TimePeriod": "Night",
"Weather": "Thunder",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000202,
"BattleEventID": 5000002,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000002_2.asset",
"MDMOGIDKCLG": "Construction2_3_Boss",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Night",
"Weather": "Thunder",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000203,
"BattleEventID": 5000002,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000002_3.asset",
"MDMOGIDKCLG": "Construction2_3_Boss",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Night",
"Weather": "Thunder",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000301,
"BattleEventID": 5000003,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000003_1.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_3_1",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000302,
"BattleEventID": 5000003,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000003_2.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_3_1",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000303,
"BattleEventID": 5000003,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000003_3.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_3_1",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000401,
"BattleEventID": 5000004,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000004_1.asset",
"MDMOGIDKCLG": "Stage_Zero_SpecialRoom_5",
"LLPNKBBJHKL": "PlayerPos_E1",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000402,
"BattleEventID": 5000004,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000004_2.asset",
"MDMOGIDKCLG": "Stage_Zero_SpecialRoom_5",
"LLPNKBBJHKL": "PlayerPos_SP",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 500000403,
"BattleEventID": 5000004,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5000004_3.asset",
"MDMOGIDKCLG": "Stage_Zero_SpecialRoom_5",
"LLPNKBBJHKL": "PlayerPos_SP",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501010101,
"BattleEventID": 5010101,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010101_1.asset",
"MDMOGIDKCLG": "metro4_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint1",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501010102,
"BattleEventID": 5010101,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010101_2.asset",
"MDMOGIDKCLG": "metro4_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint2",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501010103,
"BattleEventID": 5010101,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010101_3.asset",
"MDMOGIDKCLG": "metro4_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint3",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501010104,
"BattleEventID": 5010101,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010101_4.asset",
"MDMOGIDKCLG": "Stage_Zero_ComplexRoom_22",
"LLPNKBBJHKL": "SpecialPlayerPos_1",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501010105,
"BattleEventID": 5010101,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010101_5.asset",
"MDMOGIDKCLG": "metro4_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint5",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501011101,
"BattleEventID": 5010111,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010111_1.asset",
"MDMOGIDKCLG": "metro4_2_Rally",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Night",
"Weather": "Rain",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501011102,
"BattleEventID": 5010111,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010111_2.asset",
"MDMOGIDKCLG": "metro4_2_Rally",
"LLPNKBBJHKL": "RallyPlayerPos_C1",
"TimePeriod": "Night",
"Weather": "Rain",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501011103,
"BattleEventID": 5010111,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010111_3.asset",
"MDMOGIDKCLG": "metro4_2_Rally",
"LLPNKBBJHKL": "RallyPlayerPos_A1",
"TimePeriod": "Night",
"Weather": "Rain",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501011104,
"BattleEventID": 5010111,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010111_4.asset",
"MDMOGIDKCLG": "Stage_Zero_ComplexRoom_22",
"LLPNKBBJHKL": "SpecialPlayerPos_1",
"TimePeriod": "Morning",
"Weather": "Rain",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501011105,
"BattleEventID": 5010111,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010111_5.asset",
"MDMOGIDKCLG": "metro4_2_Rally",
"LLPNKBBJHKL": "PlayerPos_F2",
"TimePeriod": "Night",
"Weather": "Rain",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501012101,
"BattleEventID": 5010121,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010121_1.asset",
"MDMOGIDKCLG": "metro4_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint6",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501012102,
"BattleEventID": 5010121,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010121_2.asset",
"MDMOGIDKCLG": "metro4_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint7",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501012103,
"BattleEventID": 5010121,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010121_3.asset",
"MDMOGIDKCLG": "metro4_2_Rally",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501012104,
"BattleEventID": 5010121,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010121_4.asset",
"MDMOGIDKCLG": "metro4_2_Rally",
"LLPNKBBJHKL": "PlayerPos_E2",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020200,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_0.asset",
"MDMOGIDKCLG": "Stage_VR_TrainingRoom_30R",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020201,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_1.asset",
"MDMOGIDKCLG": "Construction3_4_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_CheckPoint1",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020202,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_2.asset",
"MDMOGIDKCLG": "Construction3_4_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_CheckPoint2",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020203,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_3.asset",
"MDMOGIDKCLG": "Construction3_4_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_CheckPoint3",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020204,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_4.asset",
"MDMOGIDKCLG": "Construction3_4_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_CheckPoint4",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020205,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_5.asset",
"MDMOGIDKCLG": "Construction3_4_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_CheckPoint5",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020206,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_6.asset",
"MDMOGIDKCLG": "Stage_Zero_ComplexRoom_25_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_CheckPoint6",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020207,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_7.asset",
"MDMOGIDKCLG": "Construction3_4_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_CheckPoint7",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020208,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_6.asset",
"MDMOGIDKCLG": "Stage_Zero_ComplexRoom_25_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_CheckPoint6",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501020209,
"BattleEventID": 5010201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010201_9.asset",
"MDMOGIDKCLG": "Construction2_3_Boss",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Night",
"Weather": "Thunder",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021101,
"BattleEventID": 5010211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010211_1.asset",
"MDMOGIDKCLG": "Construction2_5_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint1",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021102,
"BattleEventID": 5010211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010211_2.asset",
"MDMOGIDKCLG": "Construction2_5_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint2",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021103,
"BattleEventID": 5010211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010211_3.asset",
"MDMOGIDKCLG": "Construction2_5_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint3",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021104,
"BattleEventID": 5010211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010211_4.asset",
"MDMOGIDKCLG": "Stage_Zero_ComplexRoom_12_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint4",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021105,
"BattleEventID": 5010211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010211_5.asset",
"MDMOGIDKCLG": "Construction2_5_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint5",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021106,
"BattleEventID": 5010211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010211_6.asset",
"MDMOGIDKCLG": "Construction2_5_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint6",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021107,
"BattleEventID": 5010211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010211_7.asset",
"MDMOGIDKCLG": "Construction2_5_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint3",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021301,
"BattleEventID": 5010213,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010213_1.asset",
"MDMOGIDKCLG": "Construction2_3_Boss",
"LLPNKBBJHKL": "PlayerPos_B5",
"TimePeriod": "Night",
"Weather": "Thunder",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021302,
"BattleEventID": 5010213,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010213_2.asset",
"MDMOGIDKCLG": "Construction2_3_Boss",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Night",
"Weather": "Thunder",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021303,
"BattleEventID": 5010213,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010213_3.asset",
"MDMOGIDKCLG": "Construction2_3_Boss",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Night",
"Weather": "Thunder",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501021304,
"BattleEventID": 5010213,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010213_4.asset",
"MDMOGIDKCLG": "Construction2_3_Boss",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Night",
"Weather": "Thunder",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501022101,
"BattleEventID": 5010221,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010221_1.asset",
"MDMOGIDKCLG": "Construction3_4_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_CheckPoint9",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501022102,
"BattleEventID": 5010221,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010221_2.asset",
"MDMOGIDKCLG": "Construction3_4_SuccessiveLevel",
"LLPNKBBJHKL": "PlayerPos_C2",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501022103,
"BattleEventID": 5010221,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010221_3.asset",
"MDMOGIDKCLG": "Construction2_3_Zero",
"LLPNKBBJHKL": "PlayerPos_E4_Rally",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501022104,
"BattleEventID": 5010221,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010221_4.asset",
"MDMOGIDKCLG": "Construction2_3_Zero",
"LLPNKBBJHKL": "PlayerPos_D4_Rally",
"TimePeriod": "Evening",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501030101,
"BattleEventID": 5010301,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010301_1.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_2_3_Successive",
"LLPNKBBJHKL": "SpecialPlayerPos_B1",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501030102,
"BattleEventID": 5010301,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010301_2.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_2_1_Successive",
"LLPNKBBJHKL": "PlayerPos_CheckPoint1",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501030103,
"BattleEventID": 5010301,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010301_3.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_2_1_Successive",
"LLPNKBBJHKL": "PlayerPos_CheckPoint2",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501030104,
"BattleEventID": 5010301,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010301_4.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_2_1_Successive",
"LLPNKBBJHKL": "PlayerPos_CheckPoint3",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501030105,
"BattleEventID": 5010301,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010301_5.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_2_1_Successive",
"LLPNKBBJHKL": "PlayerPos_CheckPoint4",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501030106,
"BattleEventID": 5010301,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010301_6.asset",
"MDMOGIDKCLG": "Stage_Zero_ComplexRoom_15_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPointSideQuest1",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501030107,
"BattleEventID": 5010301,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010301_7.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_2_1_Successive",
"LLPNKBBJHKL": "SpecialRallyPos_7",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031101,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_1.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_2_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint1",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031102,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_2.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_2_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint2",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031103,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_3.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_2_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint3",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031104,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_4.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_2_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint4",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031105,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_5.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_2_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint5",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031106,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_6.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_2_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint6",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031107,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_7.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_1_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint7",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031108,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_8.asset",
"MDMOGIDKCLG": "Stage_Zero_ComplexRoom_15_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint8",
"TimePeriod": "Morning",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031109,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_9.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_2_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint9",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031110,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_10.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_1_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint10",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031111,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_11.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_2_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint11",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501031112,
"BattleEventID": 5010311,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010311_12.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_1_1_Rally",
"LLPNKBBJHKL": "PlayerPos_CheckPoint12",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 501032101,
"BattleEventID": 5010321,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010321_1.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_1_2_Zero",
"LLPNKBBJHKL": "PlayerPos_A5_Rally",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501032102,
"BattleEventID": 5010321,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010321_2.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_1_2_Zero",
"LLPNKBBJHKL": "PlayerPos_A6_Rally",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501032103,
"BattleEventID": 5010321,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010321_3.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_1_2_Zero",
"LLPNKBBJHKL": "PlayerPos_C5_Rally",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 501032104,
"BattleEventID": 5010321,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5010321_4.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_H_1_1_Rally",
"LLPNKBBJHKL": "PlayerPos_A1",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": true,
"MBIMPLIHABC": ""
},
{
"AreaID": 503020101,
"BattleEventID": 5030201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5030201_1.asset",
"MDMOGIDKCLG": "Stage_MetroArea_4_1_Lycaon",
"LLPNKBBJHKL": "PlayerPos_Ly1",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 503020102,
"BattleEventID": 5030201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5030201_2.asset",
"MDMOGIDKCLG": "Stage_MetroArea_4_1_Lycaon",
"LLPNKBBJHKL": "PlayerPos_Ly2",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 503020103,
"BattleEventID": 5030201,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5030201_3.asset",
"MDMOGIDKCLG": "Stage_MetroArea_4_1_Lycaon",
"LLPNKBBJHKL": "PlayerPos_Ly3",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 503021101,
"BattleEventID": 5030211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5030211_1.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_1_2_Lycaon",
"LLPNKBBJHKL": "PlayerPos_Ly1",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 503021102,
"BattleEventID": 5030211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5030211_2.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_1_2_Lycaon",
"LLPNKBBJHKL": "PlayerPos_Ly2",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
},
{
"AreaID": 503021103,
"BattleEventID": 5030211,
"NELIKCGAKEJ": "Level/FC_Main/FC_Successive/FC_5030211_3.asset",
"MDMOGIDKCLG": "Stage_SkyScraper_L_1_2_Lycaon",
"LLPNKBBJHKL": "PlayerPos_Ly3",
"TimePeriod": "Night",
"Weather": "SunShine",
"HAIGBCBDJPL": false,
"MBIMPLIHABC": ""
}
]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,604 @@
[
{
"ID": 500000101,
"BattleEventID": 5000001,
"VariableName": "IsArea1Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000102,
"BattleEventID": 5000001,
"VariableName": "IsArea2Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000103,
"BattleEventID": 5000001,
"VariableName": "IsArea3Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000104,
"BattleEventID": 5000001,
"VariableName": "IsArea4Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000105,
"BattleEventID": 5000001,
"VariableName": "IsArea5Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000106,
"BattleEventID": 5000001,
"VariableName": "IsArea6Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000107,
"BattleEventID": 5000001,
"VariableName": "SideQuest",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000108,
"BattleEventID": 5000001,
"VariableName": "SecondSideQuest",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000109,
"BattleEventID": 5000001,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 500000201,
"BattleEventID": 5000002,
"VariableName": "whichAreaIn",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 500000202,
"BattleEventID": 5000002,
"VariableName": "IsArea2Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000203,
"BattleEventID": 5000002,
"VariableName": "IsArea3Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000204,
"BattleEventID": 5000002,
"VariableName": "IsArea4Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000205,
"BattleEventID": 5000002,
"VariableName": "IsArea5Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 500000206,
"BattleEventID": 5000002,
"VariableName": "SecretMission",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 500000207,
"BattleEventID": 5000002,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 500000301,
"BattleEventID": 5000003,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 500000401,
"BattleEventID": 5000004,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 501010100,
"BattleEventID": 5010101,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 501010101,
"BattleEventID": 5010101,
"VariableName": "SideQuest",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501010102,
"BattleEventID": 5010101,
"VariableName": "IsSideQuestFinsih",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501010103,
"BattleEventID": 5010101,
"VariableName": "RevivedTime",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501010104,
"BattleEventID": 5010101,
"VariableName": "IsSideQuestRestart",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501011101,
"BattleEventID": 5010111,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 501011102,
"BattleEventID": 5010111,
"VariableName": "SideQuest",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501011103,
"BattleEventID": 5010111,
"VariableName": "IsSideQuestFinsih",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501011104,
"BattleEventID": 5010111,
"VariableName": "RevivedTime",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501011105,
"BattleEventID": 5010111,
"VariableName": "GroundSideQuest",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501011106,
"BattleEventID": 5010111,
"VariableName": "IsSideQuestRestart",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501012101,
"BattleEventID": 5010121,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "4"
},
{
"ID": 501012102,
"BattleEventID": 5010121,
"VariableName": "RevivedTime",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501020200,
"BattleEventID": 5010201,
"VariableName": "IsEnterTeachLevel",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020201,
"BattleEventID": 5010201,
"VariableName": "IsArea1Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020202,
"BattleEventID": 5010201,
"VariableName": "IsArea2Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020203,
"BattleEventID": 5010201,
"VariableName": "IsArea3Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020204,
"BattleEventID": 5010201,
"VariableName": "IsArea4Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020205,
"BattleEventID": 5010201,
"VariableName": "IsArea5Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020206,
"BattleEventID": 5010201,
"VariableName": "IsArea6Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020207,
"BattleEventID": 5010201,
"VariableName": "IsArea7Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020208,
"BattleEventID": 5010201,
"VariableName": "SideQuest",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020209,
"BattleEventID": 5010201,
"VariableName": "IsArea8Finish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020210,
"BattleEventID": 5010201,
"VariableName": "SecondSideQuest",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020211,
"BattleEventID": 5010201,
"VariableName": "StartSecondPhase",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020212,
"BattleEventID": 5010201,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 501020213,
"BattleEventID": 5010201,
"VariableName": "IsSideQuestRestart",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020214,
"BattleEventID": 5010201,
"VariableName": "IsSideQuestFinsih",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501020215,
"BattleEventID": 5010201,
"VariableName": "RevivedTime",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501020216,
"BattleEventID": 5010201,
"VariableName": "LifeCoinGet",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501021101,
"BattleEventID": 5010211,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 501021102,
"BattleEventID": 5010211,
"VariableName": "IsSideQuestRestart",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501021103,
"BattleEventID": 5010211,
"VariableName": "IsSideQuestFinsih",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501021104,
"BattleEventID": 5010211,
"VariableName": "RevivedTime",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501021105,
"BattleEventID": 5010211,
"VariableName": "BossHealth",
"ValueType": "float",
"InitialValue": "1"
},
{
"ID": 501021106,
"BattleEventID": 5010211,
"VariableName": "CargoCartStopped",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501021107,
"BattleEventID": 5010211,
"VariableName": "IsLastAreaFinish",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501021301,
"BattleEventID": 5010213,
"VariableName": "whichAreaIn",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 501022101,
"BattleEventID": 5010221,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "4"
},
{
"ID": 501022102,
"BattleEventID": 5010221,
"VariableName": "RevivedTime",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501030101,
"BattleEventID": 5010301,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 501030102,
"BattleEventID": 5010301,
"VariableName": "IsSideQuestFinsih",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501030103,
"BattleEventID": 5010301,
"VariableName": "RevivedTime",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501030104,
"BattleEventID": 5010301,
"VariableName": "DoorStatus1",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501030105,
"BattleEventID": 5010301,
"VariableName": "DoorStatus2",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501030106,
"BattleEventID": 5010301,
"VariableName": "DoorStatus3",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501030107,
"BattleEventID": 5010301,
"VariableName": "DoorStatus4",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501030108,
"BattleEventID": 5010301,
"VariableName": "DoorStatus5",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501030109,
"BattleEventID": 5010301,
"VariableName": "IsSideQuestRestart",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501030110,
"BattleEventID": 5010301,
"VariableName": "IsTrickFinish",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501031101,
"BattleEventID": 5010311,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 501031102,
"BattleEventID": 5010311,
"VariableName": "IsSideQuestFinsih",
"ValueType": "bool",
"InitialValue": "false"
},
{
"ID": 501031103,
"BattleEventID": 5010311,
"VariableName": "RevivedTime",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501031104,
"BattleEventID": 5010311,
"VariableName": "IsAllyRescued",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501031105,
"BattleEventID": 5010311,
"VariableName": "IsShortcutOpened",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501031106,
"BattleEventID": 5010311,
"VariableName": "IsGuardDefeated",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501031107,
"BattleEventID": 5010311,
"VariableName": "DoorStatus1",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501031108,
"BattleEventID": 5010311,
"VariableName": "DoorStatus2",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501031109,
"BattleEventID": 5010311,
"VariableName": "DoorStatus3",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501031110,
"BattleEventID": 5010311,
"VariableName": "DoorStatus4",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501031111,
"BattleEventID": 5010311,
"VariableName": "DoorStatus5",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 501031112,
"BattleEventID": 5010311,
"VariableName": "IsAllyImposterCatched",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501031113,
"BattleEventID": 5010311,
"VariableName": "IsSideQuestRestart",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501031114,
"BattleEventID": 5010311,
"VariableName": "LifeCoinGet",
"ValueType": "bool",
"InitialValue": "FALSE"
},
{
"ID": 501032101,
"BattleEventID": 5010321,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "4"
},
{
"ID": 501032102,
"BattleEventID": 5010321,
"VariableName": "RevivedTime",
"ValueType": "int",
"InitialValue": "0"
},
{
"ID": 503020101,
"BattleEventID": 5030201,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
},
{
"ID": 503021101,
"BattleEventID": 5030211,
"VariableName": "AreaSelect",
"ValueType": "int",
"InitialValue": "1"
}
]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -16,8 +16,7 @@ rand_mt.workspace = true
# Database
sqlx.workspace = true
# Util
rustyline-async.workspace = true
thiserror.workspace = true
rand.workspace = true
byteorder.workspace = true

View file

@ -1,2 +1,2 @@
pub const SERVER_VERSION: &str = "0.1.0";
pub const SERVER_VERSION: &str = "0.2.0";
pub const CLIENT_VERSION: &str = "CNBeta1.1.1";

View file

@ -1,6 +1,18 @@
pub fn init_tracing() {
use env_logger::Target;
use rustyline_async::SharedWriter;
use tracing_log::log::LevelFilter;
pub fn init_tracing(out: Option<SharedWriter>) {
#[cfg(target_os = "windows")]
ansi_term::enable_ansi_support().unwrap();
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
let target = match out {
Some(out) => Target::Pipe(Box::new(out)),
None => Target::Stdout,
};
env_logger::builder()
.target(target)
.filter(None, LevelFilter::Info)
.init();
}

View file

@ -7,6 +7,9 @@ version.workspace = true
# Serialization
serde.workspace = true
serde_json.workspace = true
jsonc-parser.workspace = true
chrono.workspace = true
prost.workspace = true
# Util
paste.workspace = true
@ -14,3 +17,6 @@ thiserror.workspace = true
# Tracing
tracing.workspace = true
# Internal
proto.workspace = true

View file

@ -0,0 +1,42 @@
use paste::paste;
use serde::Deserialize;
macro_rules! actions {
($($name:ident;)*) => {
paste! {
$(
mod [<$name:snake>];
pub use [<$name:snake>]::[<Config $name>];
)*
#[derive(Deserialize, Debug)]
#[serde(tag = "$type")]
pub enum ActionConfig {
$(
$name([<Config $name>]),
)*
}
impl ActionConfig {
pub fn to_protocol(&self) -> ::proto::ActionInfo {
use ::prost::Message;
match self {
$(
Self::$name(config) => ::proto::ActionInfo {
body: config.to_protocol().encode_to_vec(),
action_type: ::proto::ActionType::$name.into(),
..Default::default()
},
)*
}
}
}
}
};
}
actions! {
SwitchSection;
OpenUi;
}

View file

@ -0,0 +1,19 @@
use serde::Deserialize;
#[derive(Deserialize, Debug)]
pub struct ConfigOpenUi {
pub ui: String,
pub args: i32,
pub store_template_id: i32,
}
impl ConfigOpenUi {
pub fn to_protocol(&self) -> ::proto::ActionOpenUi {
::proto::ActionOpenUi {
ui: self.ui.clone(),
args: self.args,
store_template_id: self.store_template_id,
..Default::default()
}
}
}

View file

@ -0,0 +1,21 @@
use serde::Deserialize;
#[derive(Deserialize, Debug)]
pub struct ConfigSwitchSection {
pub section_id: u32,
pub transform: String,
pub camera_x: u32,
pub camera_y: u32,
}
impl ConfigSwitchSection {
pub fn to_protocol(&self) -> ::proto::ActionSwitchSection {
::proto::ActionSwitchSection {
section: self.section_id,
transform: self.transform.clone(),
camera_x: self.camera_x,
camera_y: self.camera_y,
..Default::default()
}
}
}

29
nap_data/src/event/mod.rs Normal file
View file

@ -0,0 +1,29 @@
use std::sync::OnceLock;
use serde::Deserialize;
use crate::{action::ActionConfig, DataLoadError};
const INTERACT_CONFIG_JSON: &str = "Interacts.json";
static EVENT_GRAPHS: OnceLock<Vec<EventGraphConfig>> = OnceLock::new();
#[derive(Deserialize, Debug)]
pub struct EventGraphConfig {
pub event_id: u32,
pub actions: Vec<ActionConfig>,
}
pub(crate) fn load_event_graphs(path: &str) -> Result<(), DataLoadError> {
let data = std::fs::read_to_string(format!("{path}/{INTERACT_CONFIG_JSON}"))
.map_err(|err| DataLoadError::IoError(err))?;
let event_graphs = serde_json::from_str::<Vec<EventGraphConfig>>(&data)
.map_err(|err| DataLoadError::FromJsonError(String::from("EventGraphCollection"), err))?;
EVENT_GRAPHS.set(event_graphs).unwrap();
Ok(())
}
pub fn interacts() -> std::slice::Iter<'static, EventGraphConfig> {
EVENT_GRAPHS.get().unwrap().iter()
}

View file

@ -0,0 +1,305 @@
use std::collections::{HashMap, HashSet};
use chrono::{prelude::Local, DateTime};
use serde::{Deserialize, Deserializer};
use tracing;
#[derive(Debug, Default, Deserialize)]
pub struct ExtraItemsPolicy {
pub id: u32,
pub count: u32,
#[serde(default)]
pub apply_on_owned_count: u32,
}
#[derive(Debug, Default, Deserialize)]
pub struct ProbabilityPoint {
pub start_pity: u32,
pub start_chance_percent: f64,
#[serde(default)]
pub increment_percent: f64,
}
#[derive(Debug, Default, Deserialize)]
pub struct ProbabilityModel {
#[serde(default)]
pub clear_status_on_higher_rarity_pulled: bool,
pub points: Vec<ProbabilityPoint>,
// This value is for display only, so it's set when
// the maximum guarantee is not equal to the
// automatically calculated value (commonly, less than).
#[serde(default)]
pub maximum_guarantee_pity: u32,
#[serde(skip_deserializing)]
probability_percents: Vec<f64>,
}
impl ProbabilityModel {
fn get_maximum_guarantee(&self) -> u32 {
self.probability_percents.len() as u32 - 1
}
pub fn post_configure(&mut self, tag: &String) {
self.points.sort_by_key(|point| point.start_pity);
let mut probability_percents: Vec<f64> = vec![0.0];
for (i, point) in self.points.iter().enumerate() {
if i > 0 {
let last_point = &self.points[i - 1];
let last_stop_percent = last_point.start_chance_percent
+ last_point.increment_percent
* (point.start_pity - last_point.start_pity) as f64;
if last_stop_percent > point.start_chance_percent {
tracing::warn!("Gacha - ProbabilityModel '{tag}': The start chance of '{point:?}' is less than the value inherited from the previous point.");
}
}
let mut max_pity = 2000;
if i < self.points.len() - 1 {
let next_point = &self.points[i + 1];
max_pity = next_point.start_pity - 1;
let max_probability = point.start_chance_percent
+ point.increment_percent
* (next_point.start_pity - 1 - point.start_pity) as f64;
assert!(max_probability < 100.0, "Gacha - ProbabilityModel '{tag}': Probability already reached 100% in '{point:?}' (though points with higher pity left)");
}
let mut pity = point.start_pity;
let mut percent = point.start_chance_percent;
while pity <= max_pity {
if max_pity >= 2000 && percent >= 100.0 {
probability_percents.push(100.0);
break;
}
probability_percents.push(percent);
percent += point.increment_percent;
pity += 1;
}
assert!(pity <= 2000, "Gacha - ProbabilityModel '{tag}' (point {i}): Haven't reached 100% guarantee probability at Pity 2001. The current probability is {percent}%. Crazy.");
}
self.probability_percents = probability_percents;
if self.maximum_guarantee_pity <= 0 {
self.maximum_guarantee_pity = self.get_maximum_guarantee();
}
}
pub fn get_chance_percent(&self, pity: &u32) -> f64 {
// The vec length is 1 bigger than the maximum pity (1-based)
let guarantee_pity = self.probability_percents.len() - 1;
let idx = *pity as usize;
if idx > guarantee_pity {
return self.probability_percents[guarantee_pity];
}
self.probability_percents[idx]
}
}
#[allow(dead_code)]
#[derive(Debug, Default, Deserialize)]
pub struct CategoryGuaranteePolicy {
pub included_category_tags: HashSet<String>,
pub trigger_on_failure_times: u32,
pub clear_status_on_target_changed: bool,
pub chooseable: bool,
}
#[derive(Debug, Default, Deserialize)]
pub struct TenPullDiscount {
pub use_limit: u32,
pub discounted_prize: u32,
}
#[derive(Debug, Default, Deserialize)]
pub struct AdvancedGuarantee {
pub use_limit: u32,
pub rarity: u32,
pub guarantee_pity: u32,
}
#[derive(Debug, Default, Deserialize)]
pub struct MustGainItem {
pub use_limit: u32,
pub rarity: u32,
pub category_tag: String,
}
#[derive(Debug, Default, Deserialize)]
pub struct FreeSelectItem {
pub milestones: Vec<u32>,
pub rarity: u32,
pub category_tags: Vec<String>,
pub free_select_progress_record_tag: String,
pub free_select_usage_record_tag: String,
}
#[derive(Debug, Default, Deserialize)]
pub struct DiscountPolicyCollection {
pub ten_pull_discount_map: HashMap<String, TenPullDiscount>,
pub must_gain_item_map: HashMap<String, MustGainItem>,
pub advanced_guarantee_map: HashMap<String, AdvancedGuarantee>,
pub free_select_map: HashMap<String, FreeSelectItem>,
}
impl DiscountPolicyCollection {
pub fn post_configure(&mut self) {
for (tag, ten_pull_discount) in self.ten_pull_discount_map.iter() {
let discounted_prize = ten_pull_discount.discounted_prize;
assert!(discounted_prize < 10, "Gacha - DiscountPolicy '{tag}': ten_pull_discount's value should be smaller than 10 (read {discounted_prize}).");
}
}
}
#[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(deserialize_with = "from_str")]
pub item_type: GachaAddedItemType,
}
pub fn from_str<'de, D>(deserializer: D) -> Result<GachaAddedItemType, D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
let result = GachaAddedItemType::from_str_name(&s);
match result {
Some(val) => Ok(val),
None => Ok(GachaAddedItemType::None)
}
}
#[derive(Debug, Default, Deserialize)]
pub struct GachaAvailableItemsInfo {
pub rarity: u32,
#[serde(default)]
pub extra_items_policy_tags: Vec<String>,
pub categories: HashMap<String, GachaCategoryInfo>,
pub probability_model_tag: String,
#[serde(default)]
pub category_guarantee_policy_tags: Vec<String>,
}
#[allow(dead_code)]
#[derive(Debug, Default, Deserialize)]
pub struct CharacterGachaPool {
pub gacha_schedule_id: u32,
pub gacha_parent_schedule_id: u32,
pub comment: String,
pub gacha_type: u32,
pub cost_item_id: u32,
pub start_time: DateTime<Local>,
pub end_time: DateTime<Local>,
#[serde(default)]
pub discount_policy_tags: Vec<String>,
pub sharing_guarantee_info_category: String,
pub gacha_items: Vec<GachaAvailableItemsInfo>,
}
impl CharacterGachaPool {
pub fn is_still_open(&self, now: &DateTime<Local>) -> bool {
self.start_time <= *now && *now <= self.end_time
}
pub fn post_configure(&mut self, probability_model_map: &HashMap<String, ProbabilityModel>) {
self.gacha_items
.sort_by_key(|item_list| u32::MAX - item_list.rarity);
for items_info in self.gacha_items.iter_mut() {
assert!(probability_model_map.contains_key(&items_info.probability_model_tag), "Gacha - CharacterGachaPool '{}': Specified ProbabilityModel tag '{}' that does not exist.", self.gacha_schedule_id, items_info.probability_model_tag);
}
}
}
#[derive(Debug, Default, Deserialize)]
pub struct GachaCommonProperties {
pub up_item_category_tag: String,
pub s_item_rarity: u32,
pub a_item_rarity: u32,
// TODO: PostConfigure check
pub ten_pull_discount_tag: String,
pub newcomer_advanced_s_tag: String,
}
#[derive(Debug, Default, Deserialize)]
pub struct GachaConfiguration {
pub character_gacha_pool_list: Vec<CharacterGachaPool>,
pub probability_model_map: HashMap<String, ProbabilityModel>,
pub category_guarantee_policy_map: HashMap<String, CategoryGuaranteePolicy>,
pub extra_items_policy_map: HashMap<String, ExtraItemsPolicy>,
pub discount_policies: DiscountPolicyCollection,
pub common_properties: GachaCommonProperties,
}
impl GachaConfiguration {
pub fn post_configure(&mut self) {
assert!(
self.category_guarantee_policy_map
.contains_key(&self.common_properties.up_item_category_tag),
"The UP category should be valid in policy map."
);
for (tag, policy) in self.probability_model_map.iter_mut() {
policy.post_configure(&tag);
}
self.discount_policies.post_configure();
for character_pool in self.character_gacha_pool_list.iter_mut() {
character_pool.post_configure(&self.probability_model_map);
}
}
}

45
nap_data/src/gacha/mod.rs Normal file
View file

@ -0,0 +1,45 @@
use std::sync::OnceLock;
use gacha_config::GachaConfiguration;
use jsonc_parser::{parse_to_serde_value, ParseOptions};
use serde::Deserialize;
use crate::{action::ActionConfig, DataLoadError};
pub mod gacha_config;
const GACHA_CONFIG_NAME: &str = "gacha.jsonc";
static GACHACONF: OnceLock<GachaConfiguration> = OnceLock::new();
#[derive(Deserialize, Debug)]
pub struct EventGraphConfig {
pub event_id: u32,
pub actions: Vec<ActionConfig>,
}
pub(crate) fn load_gacha_config(path: &str) -> Result<(), DataLoadError> {
let jsonc_data = std::fs::read_to_string(format!("{path}/{GACHA_CONFIG_NAME}"))
.map_err(|err| DataLoadError::IoError(err))?;
let json_value = parse_to_serde_value(
&jsonc_data,
&ParseOptions {
allow_comments: true,
allow_loose_object_property_names: false,
allow_trailing_commas: true,
},
)
.map_err(|err| DataLoadError::JsoncParseError(err))?
.unwrap();
let mut result = serde_json::from_value::<GachaConfiguration>(json_value)
.map_err(|err| DataLoadError::FromJsonError(String::from("GachaConfiguration"), err))?;
result.post_configure();
GACHACONF.set(result).unwrap();
Ok(())
}
pub fn global_gacha_config() -> &'static GachaConfiguration {
GACHACONF.get().unwrap()
}

View file

@ -1,14 +1,20 @@
pub mod action;
pub mod event;
pub mod gacha;
pub mod tables;
use std::{collections::HashMap, sync::OnceLock};
use jsonc_parser::errors::ParseError;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Serialize, Deserialize)]
pub struct AssetsConfig {
pub filecfg_path: String,
pub event_config_path: String,
pub usm_keys_path: String,
pub gacha_config_path: String,
}
#[derive(Error, Debug)]
@ -17,16 +23,20 @@ pub enum DataLoadError {
IoError(#[from] std::io::Error),
#[error("from_json failed for type {0}, error: {1}")]
FromJsonError(String, serde_json::Error),
#[error("jsonc_parser parse as json error: {0}")]
JsoncParseError(#[from] ParseError),
}
static USM_KEY_MAP: OnceLock<HashMap<u32, u64>> = OnceLock::new();
pub fn init_data(config: &AssetsConfig) -> Result<(), DataLoadError> {
tables::load_tables(&config.filecfg_path)?;
event::load_event_graphs(&config.event_config_path)?;
if let Err(err) = load_usm_keys(&config.usm_keys_path) {
tracing::warn!("failed to load USM keys, in-game cutscenes will not work! Reason: {err}");
USM_KEY_MAP.set(HashMap::new()).unwrap();
}
gacha::load_gacha_config(&config.gacha_config_path)?;
Ok(())
}

View file

@ -0,0 +1,30 @@
use serde::Deserialize;
use super::BattleEventConfigID;
template_id!(ArchiveBattleQuest id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct ArchiveBattleQuestTemplate {
#[serde(rename = "ID")]
pub id: ArchiveBattleQuestID,
pub quest_name: String,
pub quest_desc: String,
pub target: String,
pub quest_type: u32,
pub chapter: u32,
pub difficulty: u32,
pub monster_level: u32,
#[serde(rename = "HollowID")]
pub hollow_id: u32,
#[serde(rename = "FirstBattleEventID")]
pub first_battle_event_id: u32,
#[serde(rename = "BattleEventID")]
pub battle_event_id: BattleEventConfigID,
pub battle_rank: String,
pub slot1_avatar: i32,
pub slot2_avatar: i32,
pub slot3_avatar: i32,
pub buddy: i32,
}

View file

@ -0,0 +1,20 @@
use serde::Deserialize;
template_id!(ArchiveFileQuest id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct ArchiveFileQuestTemplate {
#[serde(rename = "ID")]
pub id: ArchiveFileQuestID,
#[serde(rename = "ArchiveID")]
pub archive_id: u32,
pub difficulty_lv: u32,
pub show_type: u32,
pub recommended_hit_types: Vec<u32>,
pub video: String,
pub archive_file_name: String,
pub archive_file_num: String,
pub archive_file_ni_ds: Vec<u32>,
pub auto_distribution: bool,
}

View file

@ -1,10 +1,12 @@
use serde::Deserialize;
template_id!(AvatarBase id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct AvatarBaseTemplate {
#[serde(rename = "ID")]
pub id: u32,
pub id: AvatarBaseID,
pub code_name: String,
pub name: String,
pub full_name: String,

View file

@ -0,0 +1,16 @@
use serde::Deserialize;
use super::OnceRewardID;
template_id!(BattleEventConfig id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct BattleEventConfigTemplate {
#[serde(rename = "ID")]
pub id: BattleEventConfigID,
#[serde(rename = "LevelDesignID")]
pub level_design_id: u32,
pub normal_drop: String,
pub special_drop: Vec<OnceRewardID>,
}

View file

@ -0,0 +1,16 @@
use serde::Deserialize;
use super::BattleEventConfigID;
template_id!(BattleGroupConfig id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct BattleGroupConfigTemplate {
#[serde(rename = "ID")]
pub id: BattleGroupConfigID,
#[serde(rename = "QuestID")]
pub quest_id: u32,
#[serde(rename = "BattleEventID")]
pub battle_event_id: BattleEventConfigID,
}

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

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

View file

@ -0,0 +1,13 @@
use serde::Deserialize;
template_id!(HollowQuest id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct HollowQuestTemplate {
#[serde(rename = "ID")]
pub id: HollowQuestID,
#[serde(rename = "ChessBoardID")]
pub chess_board_id: u32,
pub hollow_quest_type: u32,
}

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

@ -0,0 +1,12 @@
use serde::Deserialize;
template_id!(MainCityBgmConfig id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct MainCityBgmConfigTemplate {
#[serde(rename = "ID")]
pub id: MainCityBgmConfigID,
pub play_event_name: String,
pub state_name: String,
}

View file

@ -0,0 +1,12 @@
use serde::Deserialize;
template_id!(MainCityDefaultObject tag_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct MainCityDefaultObjectTemplate {
#[serde(rename = "TagID")]
pub tag_id: MainCityDefaultObjectID,
#[serde(rename = "InteractIDs")]
pub interact_ids: Vec<u32>,
}

View file

@ -0,0 +1,28 @@
use serde::Deserialize;
template_id!(MainCityObject tag_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct MainCityObjectTemplate {
#[serde(rename = "TagID")]
pub tag_id: MainCityObjectID,
#[serde(rename = "NPCID")]
pub npc_id: u32,
pub create_position: String,
pub create_type: u32,
#[serde(rename = "DefaultInteractIDs")]
pub default_interact_ids: Vec<u32>,
pub interact_name: String,
pub interact_shape: u32,
pub interact_scale: String,
pub focus_interact_scale: f32,
pub camera_story_key: String,
}
impl MainCityObjectTemplate {
pub fn get_section_name(&self) -> String {
let section_name = self.create_position.split('_').next().unwrap();
format!("SectionName_{section_name}")
}
}

View file

@ -1,13 +1,53 @@
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 $id_field:ident) => {
::paste::paste! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ::serde::Deserialize, ::serde::Serialize)]
pub struct [<$type_name ID>](u32);
impl [<$type_name ID>] {
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 const fn new_unchecked(id: u32) -> Self {
Self(id)
}
pub fn value(&self) -> u32 {
self.0
}
pub fn template(&self) -> &[<$type_name Template>] {
crate::tables::[<$type_name:snake _template_tb>]::iter().find(|tmpl| tmpl.$id_field == *self).unwrap()
}
}
impl ::std::fmt::Display for [<$type_name ID>] {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
f.write_fmt(format_args!("{}", self.0))
}
}
}
};
}
macro_rules! template_tables {
($($template_type:ident;)*) => {
$(paste! {
mod [<$template_type:snake>];
pub use [<$template_type:snake>]::$template_type;
pub use [<$template_type:snake>]::*;
})*
$(paste! {
@ -38,9 +78,29 @@ macro_rules! template_tables {
template_tables! {
AvatarBaseTemplate;
BuddyBaseTemplate;
UnlockConfigTemplate;
SectionConfigTemplate;
ProcedureConfigTemplate;
PostGirlConfigTemplate;
TrainingQuestTemplate;
WeaponTemplate;
MainCityObjectTemplate;
MainCityDefaultObjectTemplate;
MainCityBgmConfigTemplate;
ArchiveFileQuestTemplate;
ArchiveBattleQuestTemplate;
HollowQuestTemplate;
HollowConfigTemplate;
BattleEventConfigTemplate;
BattleGroupConfigTemplate;
SubAreaDataTemplate;
VariableDataTemplate;
OnceRewardTemplate;
QuickAccessTemplate;
QuickFuncTemplate;
TeleportConfigTemplate;
ItemTemplate;
RobotConfigTemplate;
RobotBuddyConfigTemplate;
}

View file

@ -0,0 +1,19 @@
use serde::Deserialize;
template_id!(OnceReward reward_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct OnceRewardTemplate {
#[serde(rename = "RewardID")]
pub reward_id: OnceRewardID,
pub reward_list: Vec<RewardItem>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct RewardItem {
#[serde(rename = "RewardItemID")]
pub reward_item_id: u32,
pub reward_amount: u32,
}

View file

@ -1,9 +1,11 @@
use serde::Deserialize;
template_id!(PostGirlConfig id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct PostGirlConfigTemplate {
#[serde(rename = "ID")]
pub id: u32,
pub id: PostGirlConfigID,
pub name: String,
}

View file

@ -1,14 +1,16 @@
use serde::Deserialize;
template_id!(ProcedureConfig procedure_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct ProcedureConfigTemplate {
#[serde(rename = "ProcedureID")]
pub procedure_id: i32,
pub procedure_id: ProcedureConfigID,
pub procedure_type: u32,
#[serde(rename = "ContentID")]
pub content_id: String,
pub jump_tos: Vec<i32>,
pub jump_tos: Vec<u32>,
pub procedure_banks: Vec<String>,
pub procedure_event: String,
}

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,9 +1,11 @@
use serde::Deserialize;
template_id!(SectionConfig section_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct SectionConfigTemplate {
pub section_id: u32,
pub section_id: SectionConfigID,
pub photo_name: String,
pub name: String,
pub primary_entry_name: String,

View file

@ -0,0 +1,16 @@
use std::u32;
use serde::Deserialize;
template_id!(SubAreaData area_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct SubAreaDataTemplate {
#[serde(rename = "AreaID")]
pub area_id: SubAreaDataID,
#[serde(rename = "BattleEventID")]
pub battle_event_id: u32,
pub time_period: String,
pub weather: String,
}

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

@ -1,9 +1,13 @@
use serde::Deserialize;
use super::BattleEventConfigID;
template_id!(TrainingQuest id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct TrainingQuestTemplate {
pub id: u32,
pub id: TrainingQuestID,
pub training_type: u32,
pub battle_event_id: u32,
pub battle_event_id: BattleEventConfigID,
}

View file

@ -1,9 +1,11 @@
use serde::Deserialize;
template_id!(UnlockConfig id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct UnlockConfigTemplate {
#[serde(rename = "ID")]
pub id: i32,
pub id: UnlockConfigID,
pub name: String,
}

View file

@ -0,0 +1,25 @@
use std::u32;
use serde::Deserialize;
template_id!(VariableData id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct VariableDataTemplate {
#[serde(rename = "ID")]
pub id: VariableDataID,
#[serde(rename = "BattleEventID")]
pub battle_event_id: u32,
pub variable_name: String,
pub value_type: VariableValueType,
pub initial_value: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VariableValueType {
Int,
Bool,
Float,
}

View file

@ -0,0 +1,36 @@
use serde::Deserialize;
template_id!(Weapon item_id);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct WeaponTemplate {
#[serde(rename = "ItemID")]
pub item_id: WeaponID,
pub code_name: String,
#[serde(rename = "Type")]
pub weapon_type: u32,
pub base_property: IntPropertyIntValue,
pub rand_property: IntPropertyIntValue,
pub star_limit: u32,
pub exp_recycle: u32,
pub refine_costs: Vec<IntItemIDIntNumber>,
pub refine_initial: u32,
pub refine_limit: u32,
}
// nice names, mihoyo
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct IntPropertyIntValue {
pub property: u32,
pub value: i32,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct IntItemIDIntNumber {
#[serde(rename = "ItemID")]
pub item_id: u32,
pub number: u32,
}

View file

@ -15,6 +15,7 @@ rbase64.workspace = true
toml.workspace = true
serde.workspace = true
serde_json.workspace = true
chrono.workspace = true
# Database
sqlx.workspace = true
@ -28,6 +29,7 @@ rand.workspace = true
atomic_enum.workspace = true
num_enum.workspace = true
dashmap.workspace = true
rustyline-async.workspace = true
# Tracing
tracing.workspace = true

View file

@ -0,0 +1,116 @@
use data::tables::{self, AvatarBaseID};
use proto::{AddAvatarPerformType, AddAvatarScNotify, PlayerSyncScNotify};
use crate::ServerState;
use super::ArgSlice;
pub async fn add(
args: ArgSlice<'_>,
state: &ServerState,
) -> Result<String, Box<dyn std::error::Error>> {
const USAGE: &str = "Usage: avatar add [player_uid] [avatar_id]";
if args.len() != 2 {
return Ok(USAGE.to_string());
}
let uid = args[0].parse::<u32>()?;
let avatar_id = args[1].parse::<u32>()?;
let Ok(avatar_id) = AvatarBaseID::new(avatar_id) else {
return Ok(format!("avatar with id {avatar_id} doesn't exist"));
};
let Some(player_lock) = state.player_mgr.get_player(uid).await else {
return Ok(String::from("player not found"));
};
let (session_id, avatar_sync) = {
let mut player = player_lock.lock().await;
player.role_model.add_avatar(avatar_id);
(player.current_session_id(), player.role_model.avatar_sync())
};
if let Some(session) = session_id.map(|id| state.session_mgr.get(id)).flatten() {
session
.notify(AddAvatarScNotify {
avatar_id: avatar_id.value(),
perform_type: AddAvatarPerformType::Gacha.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 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

@ -0,0 +1,51 @@
use data::tables::WeaponID;
use proto::PlayerSyncScNotify;
use crate::ServerState;
use super::ArgSlice;
pub async fn add_weapon(
args: ArgSlice<'_>,
state: &ServerState,
) -> Result<String, Box<dyn std::error::Error>> {
const USAGE: &str = "Usage: item add_weapon [player_uid] [weapon_id]";
if args.len() != 2 {
return Ok(USAGE.to_string());
}
let uid = args[0].parse::<u32>()?;
let weapon_id = args[1].parse::<u32>()?;
let Ok(weapon_id) = WeaponID::new(weapon_id) else {
return Ok(format!("weapon with id {weapon_id} doesn't exist"));
};
let Some(player_lock) = state.player_mgr.get_player(uid).await else {
return Ok(String::from("player not found"));
};
let (session_id, weapon_uid, item_sync) = {
let mut player = player_lock.lock().await;
let uid = player.item_model.add_weapon(weapon_id);
(
player.current_session_id(),
uid.value(),
player.item_model.item_sync(),
)
};
if let Some(session) = session_id.map(|id| state.session_mgr.get(id)).flatten() {
session
.notify(PlayerSyncScNotify {
item_sync: Some(item_sync),
..Default::default()
})
.await?;
} else {
state.player_mgr.save_and_remove(uid).await;
}
Ok(format!("successfully added weapon, item uid: {weapon_uid}"))
}

View file

@ -0,0 +1,95 @@
use std::io::Write;
use std::sync::Arc;
use rustyline_async::{Readline, ReadlineEvent, SharedWriter};
use crate::ServerState;
mod avatar;
mod gacha;
mod item;
mod player;
type ArgSlice<'a> = &'a [&'a str];
pub struct CommandManager {
state: Arc<ServerState>,
}
macro_rules! commands {
($($category:ident::$action:ident $usage:tt $desc:tt;)*) => {
async fn exec(state: &ServerState, cmd: &str) -> String {
let input = cmd.split(" ").collect::<Vec<&str>>();
if input.len() == 1 && *input.get(0).unwrap() == "help" {
return Self::help_message();
}
let (Some(category), Some(action)) = (input.get(0), input.get(1)) else {
return String::new();
};
let args = &input[2..];
match match (*category, *action) {
$(
(stringify!($category), stringify!($action)) => {
$category::$action(args, state).await
}
)*,
_ => {
::tracing::info!("unknown command");
return Self::help_message();
}
} {
Ok(s) => s,
Err(err) => format!("failed to execute command: {err}"),
}
}
fn help_message() -> String {
concat!("available commands:\n",
$(stringify!($category), " ", stringify!($action), " ", $usage, " - ", $desc, "\n",)*
"help - shows this message"
).to_string()
}
};
}
impl CommandManager {
pub fn new(state: Arc<ServerState>) -> Self {
Self { state }
}
pub async fn run(&self, mut rl: Readline, mut out: SharedWriter) {
loop {
match rl.readline().await {
Ok(ReadlineEvent::Line(line)) => {
let str = Self::exec(&self.state, &line).await;
writeln!(&mut out, "{str}").unwrap();
rl.add_history_entry(line);
}
Ok(ReadlineEvent::Eof) | Ok(ReadlineEvent::Interrupted) => {
rl.flush().unwrap();
drop(rl);
// TODO: maybe disconnect and save all players
std::process::exit(0);
}
_ => continue,
}
}
}
commands! {
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";
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

@ -0,0 +1,170 @@
use data::tables::{AvatarBaseID, ProcedureConfigID};
use proto::{DisconnectReason, DisconnectScNotify, PlayerSyncScNotify};
use crate::ServerState;
use super::ArgSlice;
pub async fn avatar(
args: ArgSlice<'_>,
state: &ServerState,
) -> Result<String, Box<dyn std::error::Error>> {
const USAGE: &str = "Usage: player avatar [player_uid] [avatar_id]";
if args.len() != 2 {
return Ok(USAGE.to_string());
}
let uid = args[0].parse::<u32>()?;
let avatar_id = args[1].parse::<u32>()?;
let Ok(avatar_id) = AvatarBaseID::new(avatar_id) else {
return Ok(format!("avatar with id {avatar_id} doesn't exist"));
};
let Some(player_lock) = state.player_mgr.get_player(uid).await else {
return Ok(String::from("player not found"));
};
let should_save = {
let mut player = player_lock.lock().await;
player.basic_data_model.frontend_avatar_id = Some(avatar_id);
player.current_session_id().is_none()
};
if should_save {
state.player_mgr.save_and_remove(uid).await;
}
Ok(format!(
"changed frontend_avatar_id, you have to re-enter main city now"
))
}
pub async fn nickname(
args: ArgSlice<'_>,
state: &ServerState,
) -> Result<String, Box<dyn std::error::Error>> {
const USAGE: &str = "Usage: player nickname [uid] [nickname]";
if args.len() != 2 {
return Ok(USAGE.to_string());
}
let uid = args[0].parse::<u32>()?;
let nickname = args[1].trim();
if !matches!(nickname.len(), (4..=15)) {
return Ok(String::from(
"nickname should contain at least 4 and not more than 15 characters",
));
}
let Some(player_lock) = state.player_mgr.get_player(uid).await else {
return Ok(String::from("player not found"));
};
let (session_id, basic_info) = {
let mut player = player_lock.lock().await;
player.basic_data_model.nick_name = Some(nickname.to_string());
(
player.current_session_id(),
player.basic_data_model.player_basic_info(),
)
};
if let Some(session) = session_id.map(|id| state.session_mgr.get(id)).flatten() {
session
.notify(PlayerSyncScNotify {
basic_info: Some(basic_info),
..Default::default()
})
.await?;
} else {
state.player_mgr.save_and_remove(uid).await;
}
Ok(format!(
"successfully changed player {uid} nickname to {nickname}"
))
}
pub async fn procedure(
args: ArgSlice<'_>,
state: &ServerState,
) -> Result<String, Box<dyn std::error::Error>> {
const USAGE: &str = "Usage: player procedure [uid] [procedure_id]";
if args.len() != 2 {
return Ok(USAGE.to_string());
}
let uid = args[0].parse::<u32>()?;
let procedure_id = args[1].parse::<i32>()?;
let procedure_id = match procedure_id {
1.. => ProcedureConfigID::new(procedure_id as u32).ok(),
_ => None,
};
let Some(player_lock) = state.player_mgr.get_player(uid).await else {
return Ok(String::from("player not found"));
};
let session_id = {
let mut player = player_lock.lock().await;
player.basic_data_model.beginner_procedure_id = procedure_id;
player.current_session_id()
};
if session_id.is_none() {
state.player_mgr.save_and_remove(uid).await;
}
Ok(format!(
"successfully changed procedure_id to {procedure_id:?}"
))
}
pub async fn kick(
args: ArgSlice<'_>,
state: &ServerState,
) -> Result<String, Box<dyn std::error::Error>> {
const USAGE: &str = "Usage: player kick [player_uid]";
if args.len() > 2 {
return Ok(USAGE.to_string());
}
let uid = args[0].parse::<u32>()?;
let default_reason = DisconnectReason::ServerKick.into();
let reason = match args.get(1) {
Some(arg) => match arg.parse::<i32>() {
Ok(val) => val,
Err(_err) => default_reason,
},
None => default_reason,
};
let reason_str = match DisconnectReason::try_from(reason) {
Ok(converted_enum) => converted_enum.as_str_name().to_owned(),
Err(_err) => reason.to_string(),
};
let Some(player_lock) = state.player_mgr.get_player(uid).await else {
return Ok(String::from("player not found"));
};
let session_id = player_lock.lock().await.current_session_id();
if let Some(session) = session_id.map(|id| state.session_mgr.get(id)).flatten() {
session
.notify(DisconnectScNotify { reason: reason })
.await?;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
session.shutdown().await?;
Ok(format!("kicked player, uid: {uid}, reason: {reason_str}"))
} else {
Ok(format!("player uid: {uid} is not online yet."))
}
}

View file

@ -19,7 +19,9 @@ impl Default for NapGSConfig {
database_credentials: DatabaseCredentials::default(),
assets: AssetsConfig {
filecfg_path: String::from("assets/FileCfg"),
event_config_path: String::from("assets/EventConfig"),
usm_keys_path: String::from("assets/VideoUSMEncKeys.json"),
gacha_config_path: String::from("assets/GachaConfig"),
},
}
}

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,
@ -12,7 +12,7 @@ pub async fn on_get_tips_info(
Ok(GetTipsInfoScRsp {
retcode: Retcode::RetSucc.into(),
tips_info: Some(TipsInfo::default()),
ofolagfmcmo: req.ofolagfmcmo, // tips group type
r#type: req.r#type, // tips group type
})
}
@ -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)
.collect(),
post_girl_list: tables::post_girl_config_template_tb::iter()
.map(|template| PostGirlItem {
template_id: template.id,
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()
}),
})
@ -54,6 +65,16 @@ pub async fn on_get_news_stand_data(
})
}
pub async fn on_news_stand_seen(
_session: &NetSession,
_player: &mut Player,
_req: NewsStandSeenCsReq,
) -> NetResult<NewsStandSeenScRsp> {
Ok(NewsStandSeenScRsp {
retcode: Retcode::RetSucc.into(),
})
}
pub async fn on_get_trashbin_hermit_data(
_session: &NetSession,
_player: &mut Player,
@ -124,6 +145,22 @@ pub async fn on_player_operation(
})
}
pub async fn on_player_system_parameter_change(
_session: &NetSession,
_player: &mut Player,
req: PlayerSystemParameterChangeCsReq,
) -> NetResult<PlayerSystemParameterChangeScRsp> {
tracing::info!(
"PlayerSystemParameterChange(type={}, param={})",
req.r#type,
req.params,
);
Ok(PlayerSystemParameterChangeScRsp {
retcode: Retcode::RetSucc.into(),
})
}
pub async fn on_pop_up_window_seen(
_session: &NetSession,
_player: &mut Player,
@ -157,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

@ -0,0 +1,25 @@
use crate::logic::ENPCInteraction;
use super::*;
pub async fn on_run_event_graph(
session: &NetSession,
_player: &mut Player,
req: RunEventGraphCsReq,
) -> NetResult<RunEventGraphScRsp> {
session
.notify(UpdateEventGraphScNotify {
owner_type: req.owner_type,
tag: req.tag,
ddiamibnibg: req.ddiamibnibg,
npc_interaction: ENPCInteraction::OnInteract.to_string(),
ppabhkhbalm: true,
gainclnemhc: req.owner_id,
..Default::default()
})
.await?;
Ok(RunEventGraphScRsp {
retcode: Retcode::RetSucc.into(),
})
}

View file

@ -1,13 +1,235 @@
use data::{
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};
pub async fn on_get_gacha_data(
_session: &NetSession,
_player: &mut Player,
req: GetGachaDataCsReq,
) -> NetResult<GetGachaDataScRsp> {
Ok(GetGachaDataScRsp {
if req.gacha_type != 3 {
// tracing::info!("non-supported gacha type {}", body.gacha_type);
Ok(GetGachaDataScRsp {
retcode: Retcode::RetSucc.into(),
gacha_type: req.gacha_type,
gacha_data: Some(GachaData::default()),
})
} else {
// tracing::info!("construct gacha info");
Ok(GetGachaDataScRsp {
retcode: Retcode::RetSucc.into(),
gacha_type: req.gacha_type,
gacha_data: Some(_player.gacha_model.to_client(&Local::now())),
})
}
}
pub async fn on_do_gacha(
_session: &NetSession,
_player: &mut Player,
req: DoGachaCsReq,
) -> NetResult<DoGachaScRsp> {
let gachaconf = global_gacha_config();
let gacha_model = &mut _player.gacha_model;
let item_model = &mut _player.item_model;
let role_model = &mut _player.role_model;
let pull_time = Local::now();
let target_pool = get_gacha_pool(
&gachaconf.character_gacha_pool_list,
&req.gacha_parent_schedule_id,
&pull_time,
)?;
// tracing::info!("cost_item_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 Err(NetError::from(Retcode::RetFail));
} else {
// TODO: cost resource
}
let mut gain_item_list: Vec<GainItemInfo> = vec![];
while cost_count > 0 {
let pull_result = gacha_model.perform_pull_pool(&pull_time, target_pool);
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.value(),
extra_item_id,
extra_item_count,
uid,
num: 1,
..GainItemInfo::default()
});
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(),
gacha_type: req.gacha_type,
gacha_data: Some(GachaData::default()),
gain_item_list,
gacha_data: Some(gacha_model.to_client(&pull_time)),
cost_item_count: req.cost_item_count,
})
}
pub async fn on_gacha_free_agent(
_session: &NetSession,
_player: &mut Player,
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,
)?;
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();
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);
_session
.notify(construct_sync(role_model, item_model))
.await?;
Ok(GachaFreeAgentScRsp {
retcode: Retcode::RetSucc.into(),
})
}
pub async fn on_choose_gacha_up(
_session: &NetSession,
_player: &mut Player,
req: ChooseGachaUpCsReq,
) -> NetResult<ChooseGachaUpScRsp> {
let gachaconf = global_gacha_config();
let gacha_model = &mut _player.gacha_model;
let pull_time = Local::now();
let target_pool = get_gacha_pool(
&gachaconf.character_gacha_pool_list,
&req.gacha_parent_schedule_id,
&pull_time,
)?;
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: if gacha_model.choose_gacha_up(target_pool, &item_id) {
Retcode::RetSucc.into()
} else {
Retcode::RetFail.into()
},
..Default::default()
})
}
fn get_gacha_pool<'conf>(
character_gacha_pool_list: &'conf Vec<CharacterGachaPool>,
gacha_parent_schedule_id: &u32,
pull_time: &DateTime<Local>,
) -> 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 Ok(target_pool);
}
}
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,3 +1,7 @@
use data::tables::AvatarBaseID;
use crate::logic::game::LogicError;
use super::*;
pub async fn on_get_item_data(
@ -10,6 +14,57 @@ pub async fn on_get_item_data(
Ok(GetItemDataScRsp {
retcode: Retcode::RetSucc.into(),
resource_list: item_model.resources.iter().map(|i| i.to_client()).collect(),
weapon_list: item_model.weapons.iter().map(|w| w.to_client()).collect(),
..Default::default()
})
}
pub async fn on_weapon_dress(
session: &NetSession,
player: &mut Player,
req: WeaponDressCsReq,
) -> NetResult<WeaponDressScRsp> {
player.dress_weapon(
AvatarBaseID::new(req.avatar_id).map_err(LogicError::from)?,
req.weapon_uid.into(),
)?;
session
.notify(PlayerSyncScNotify {
avatar: Some(player.role_model.avatar_sync()),
item_sync: Some(player.item_model.item_sync()),
..Default::default()
})
.await?;
Ok(WeaponDressScRsp {
retcode: Retcode::RetSucc.into(),
})
}
pub async fn on_weapon_un_dress(
session: &NetSession,
player: &mut Player,
req: WeaponUnDressCsReq,
) -> NetResult<WeaponUnDressScRsp> {
let avatar_id = AvatarBaseID::new(req.avatar_id).map_err(LogicError::from)?;
let avatar = player
.role_model
.avatar_list
.iter_mut()
.find(|a| a.template_id == avatar_id)
.ok_or(Retcode::RetFail)?;
avatar.weapon_uid = None;
session
.notify(PlayerSyncScNotify {
avatar: Some(player.role_model.avatar_sync()),
..Default::default()
})
.await?;
Ok(WeaponUnDressScRsp {
retcode: Retcode::RetSucc.into(),
})
}

View file

@ -9,6 +9,7 @@ mod client_systems;
mod collect;
mod daily_challenge;
mod embattles;
mod event_graph;
mod fairy;
mod friend;
mod gacha;
@ -85,7 +86,7 @@ req_handlers! {
character_quest::GetPhotoWallData;
month_card::GetMonthCardDayReward;
world::EnterWorld;
world::EnterSection;
world::SyncHallEvent;
world::SavePosInMainCity;
world::WorldInitFinish;
world::AdvanceBeginnerProcedure;
@ -95,6 +96,12 @@ req_handlers! {
world::StartTrialFightingMission;
world::EndBattle;
world::LeaveCurDungeon;
world::InteractWithUnit;
world::EnterSection;
world::JumpPageSystem;
world::StartHollowQuest;
world::FinishHollowBattleEvent;
world::LongFightProgressUpdate;
client_systems::ReportUiLayoutPlatform;
client_systems::PlayerOperation;
client_systems::UnlockNewbieGroup;
@ -102,6 +109,8 @@ req_handlers! {
client_systems::PopUpWindowSeen;
client_systems::ReportSystemSettingsChange;
client_systems::InteractWithSceneObject;
client_systems::PlayerSystemParameterChange;
client_systems::NewsStandSeen;
perform::PerformTrigger;
perform::PerformEnd;
perform::PerformJump;
@ -109,6 +118,17 @@ req_handlers! {
misc::GetServerTimestamp;
misc::GetCutSceneKeyInfo;
embattles::ReportBattleTeam;
item::WeaponDress;
item::WeaponUnDress;
event_graph::RunEventGraph;
quest::BeginArchiveBattleQuest;
quest::FinishArchiveQuest;
gacha::DoGacha;
gacha::ChooseGachaUp;
gacha::GachaFreeAgent;
player::ModNickname;
client_systems::ModQuickMenu;
client_systems::ChangePostGirl;
}
notify_handlers! {

View file

@ -1,5 +1,3 @@
use super::core::NetError;
use crate::logic::{
game::{GameInstance, LogicError},
procedure::ProcedureAction,
@ -12,14 +10,12 @@ pub async fn on_perform_trigger(
player: &mut Player,
req: PerformTriggerCsReq,
) -> NetResult<PerformTriggerScRsp> {
let GameInstance::Fresh(fresh_game) = &mut player.game_instance else {
return Err(NetError::from(Retcode::RetFail));
};
fresh_game
.procedure_mgr
.on_action(ProcedureAction::PerformTrigger)
.map_err(LogicError::from)?;
if let GameInstance::Fresh(fresh_game) = &mut player.game_instance {
fresh_game
.procedure_mgr
.on_action(ProcedureAction::PerformTrigger)
.map_err(LogicError::from)?;
}
Ok(PerformTriggerScRsp {
retcode: Retcode::RetSucc.into(),
@ -32,14 +28,12 @@ pub async fn on_perform_end(
player: &mut Player,
_req: PerformEndCsReq,
) -> NetResult<PerformEndScRsp> {
let GameInstance::Fresh(fresh_game) = &mut player.game_instance else {
return Err(NetError::from(Retcode::RetFail));
};
fresh_game
.procedure_mgr
.on_action(ProcedureAction::PerformEnd)
.map_err(LogicError::from)?;
if let GameInstance::Fresh(fresh_game) = &mut player.game_instance {
fresh_game
.procedure_mgr
.on_action(ProcedureAction::PerformEnd)
.map_err(LogicError::from)?;
}
Ok(PerformEndScRsp {
retcode: Retcode::RetSucc.into(),

View file

@ -1,3 +1,5 @@
use data::tables::AvatarBaseID;
use crate::logic::game::{GameInstance, LogicError};
use crate::logic::procedure::ProcedureAction;
@ -20,6 +22,8 @@ pub async fn on_create_role(
player: &mut Player,
req: CreateRoleCsReq,
) -> NetResult<CreateRoleScRsp> {
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));
};
@ -29,7 +33,7 @@ pub async fn on_create_role(
.on_action(ProcedureAction::SelectRole)
.map_err(LogicError::from)?;
player.set_frontend_avatar(req.avatar_id as i32)?;
player.set_frontend_avatar(avatar_id)?;
session
.notify(PlayerSyncScNotify {
@ -54,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

@ -1,3 +1,10 @@
use data::tables::{self, ArchiveBattleQuestID};
use crate::logic::{
game::{GameInstance, HollowGame, LogicError},
ELocalPlayType, EQuestType,
};
use super::*;
pub async fn on_get_quest_data(
@ -8,7 +15,24 @@ pub async fn on_get_quest_data(
Ok(GetQuestDataScRsp {
retcode: Retcode::RetSucc.into(),
quest_type: req.quest_type,
quest_data: Some(QuestData::default()),
quest_data: Some(QuestData {
quest_collection_list: vec![
QuestCollection {
quest_type: EQuestType::ArchiveFile as u32,
quest_id_list: tables::archive_file_quest_template_tb::iter()
.map(|tmpl| tmpl.id.value())
.collect(),
..Default::default()
},
QuestCollection {
quest_type: EQuestType::Hollow as u32,
quest_id_list: tables::hollow_quest_template_tb::iter()
.map(|tmpl| tmpl.id.value())
.collect(),
..Default::default()
},
],
}),
})
}
@ -19,7 +43,22 @@ pub async fn on_get_yorozuya_info(
) -> NetResult<GetYorozuyaInfoScRsp> {
Ok(GetYorozuyaInfoScRsp {
retcode: Retcode::RetSucc.into(),
yorozuya_info: Some(YorozuyaInfo::default()),
yorozuya_info: Some(YorozuyaInfo {
odohdljfdlf: vec![1000, 1001, 1002, 1003, 1004],
apmojjlcooa: vec![1000, 1001, 1002, 1003, 1004],
akiddbalfoa: vec![10010001, 10010002, 10010004],
npgjhahijkb: tables::hollow_config_template_tb::iter()
.map(|tmpl| tmpl.id.value())
.collect(),
eoljpdnjgeg: tables::hollow_quest_template_tb::iter()
.map(|tmpl| Ofhlkjeakif {
nnkcanmllod: tmpl.id.value(),
kkjlnkehddj: Some(Cgpajijemlj::default()),
..Default::default()
})
.collect(),
..Default::default()
}),
})
}
@ -30,6 +69,54 @@ pub async fn on_get_archive_info(
) -> NetResult<GetArchiveInfoScRsp> {
Ok(GetArchiveInfoScRsp {
retcode: Retcode::RetSucc.into(),
archive_info: Some(ArchiveInfo::default()),
archive_info: Some(ArchiveInfo {
hollow_archive_id_list: (1..99999).collect(),
videotaps_info: tables::archive_file_quest_template_tb::iter()
.map(|tmpl| VideotapeInfo {
archive_file_id: tmpl.id.value(),
finished: true,
..Default::default()
})
.collect(),
..Default::default()
}),
})
}
pub async fn on_begin_archive_battle_quest(
session: &NetSession,
player: &mut Player,
req: BeginArchiveBattleQuestCsReq,
) -> NetResult<BeginArchiveBattleQuestScRsp> {
let quest_id = ArchiveBattleQuestID::new(req.quest_id).map_err(LogicError::from)?;
player.game_instance = GameInstance::Hollow(
HollowGame::create_archive_battle(
quest_id,
ELocalPlayType::ArchiveBattle,
&req.avatars,
req.buddy_id,
)
.map_err(LogicError::from)?,
);
let world_init_notify = player.game_instance.create_world_init_notify()?;
session.notify(world_init_notify).await?;
Ok(BeginArchiveBattleQuestScRsp {
retcode: Retcode::RetSucc.into(),
quest_id: req.quest_id,
})
}
pub async fn on_finish_archive_quest(
_session: &NetSession,
_player: &mut Player,
req: FinishArchiveQuestCsReq,
) -> NetResult<FinishArchiveQuestScRsp> {
Ok(FinishArchiveQuestScRsp {
retcode: Retcode::RetSucc.into(),
quest_id: req.quest_id,
..Default::default()
})
}

View file

@ -1,7 +1,14 @@
use data::{
event,
tables::{HollowQuestID, ProcedureConfigID, SectionConfigID, TrainingQuestID},
};
use super::core::NetError;
use crate::{
logic::{game::*, procedure::ProcedureAction, ELocalPlayType},
logic::{
game::*, procedure::ProcedureAction, EHollowQuestType, ELocalPlayType, ENPCInteraction,
},
net::NetSessionState,
};
@ -14,10 +21,8 @@ pub async fn on_enter_world(
) -> NetResult<EnterWorldScRsp> {
session.set_state(NetSessionState::EndBasicsReq);
if player.basic_data_model.beginner_procedure_id != -1 {
player.game_instance = GameInstance::Fresh(FreshGame::new(
player.basic_data_model.beginner_procedure_id,
))
if let Some(procedure_id) = player.basic_data_model.beginner_procedure_id {
player.game_instance = GameInstance::Fresh(FreshGame::new(procedure_id))
} else {
player.init_frontend_game()?;
}
@ -41,9 +46,12 @@ pub async fn on_advance_beginner_procedure(
return Err(NetError::from(Retcode::RetFail));
};
let procedure_id =
ProcedureConfigID::new(req.procedure_id as u32).map_err(LogicError::from)?;
fresh_game
.procedure_mgr
.try_complete_procedure(req.procedure_id)
.try_complete_procedure(procedure_id)
.map_err(LogicError::from)?;
player.basic_data_model.beginner_procedure_id = fresh_game.procedure_mgr.procedure_id();
@ -119,12 +127,12 @@ pub async fn on_beginner_battle_rebegin(
})
}
pub async fn on_enter_section(
pub async fn on_sync_hall_event(
_session: &NetSession,
_player: &mut Player,
_req: EnterSectionCsReq,
) -> NetResult<EnterSectionScRsp> {
Ok(EnterSectionScRsp {
_req: SyncHallEventCsReq,
) -> NetResult<SyncHallEventScRsp> {
Ok(SyncHallEventScRsp {
retcode: Retcode::RetSucc.into(),
})
}
@ -160,8 +168,10 @@ pub async fn on_start_trial_fighting_mission(
player: &mut Player,
req: StartTrialFightingMissionCsReq,
) -> NetResult<StartTrialFightingMissionScRsp> {
let quest_id = TrainingQuestID::new(req.quest_id).map_err(LogicError::from)?;
player.game_instance = GameInstance::Hollow(
HollowGame::create_training_game(req.quest_id, ELocalPlayType::TrainingRoomFight)
HollowGame::create_training_game(quest_id, ELocalPlayType::TrainingRoomFight, &req.avatars)
.map_err(LogicError::from)?,
);
@ -174,10 +184,42 @@ pub async fn on_start_trial_fighting_mission(
}
pub async fn on_end_battle(
_session: &NetSession,
_player: &mut Player,
_req: EndBattleCsReq,
session: &NetSession,
player: &mut Player,
req: EndBattleCsReq,
) -> NetResult<EndBattleScRsp> {
match &mut player.game_instance {
GameInstance::Hollow(hollow) if hollow.quest_manager.has_active_quests() => {
hollow
.quest_manager
.finish_quest(hollow.battle_event_id.value())
.map_err(LogicError::from)?;
session
.notify(DungeonQuestFinishedScNotify {
result: req.battle_result.unwrap().result as u32,
quest_id: hollow.quest_id,
..Default::default()
})
.await?;
}
GameInstance::LongFight(fight) => {
fight
.quest_manager
.finish_quest(fight.battle_event_id.value())
.map_err(LogicError::from)?;
session
.notify(DungeonQuestFinishedScNotify {
result: req.battle_result.unwrap().result as u32,
quest_id: fight.quest_id,
..Default::default()
})
.await?;
}
_ => (),
};
Ok(EndBattleScRsp {
battle_reward: Some(BattleRewardInfo::default()),
retcode: Retcode::RetSucc.into(),
@ -198,3 +240,132 @@ pub async fn on_leave_cur_dungeon(
retcode: Retcode::RetSucc.into(),
})
}
pub async fn on_interact_with_unit(
session: &NetSession,
_player: &mut Player,
req: InteractWithUnitCsReq,
) -> NetResult<InteractWithUnitScRsp> {
tracing::info!("interact: {req:?}");
if let Some(graph) = event::interacts().find(|e| e.event_id == req.interaction as u32) {
session
.notify(SyncEventInfoScNotify {
owner_id: req.interaction as u32,
npc_interaction: ENPCInteraction::OnInteract.to_string(),
tag: req.unit_tag as u32,
owner_type: EventGraphOwnerType::SceneUnit.into(),
action_list: graph.actions.iter().map(|a| a.to_protocol()).collect(),
..Default::default()
})
.await?;
} else {
tracing::warn!("no event graph for interaction: {}", req.interaction);
}
Ok(InteractWithUnitScRsp {
retcode: Retcode::RetSucc.into(),
})
}
pub async fn on_enter_section(
session: &NetSession,
player: &mut Player,
req: EnterSectionCsReq,
) -> NetResult<EnterSectionScRsp> {
let section_id = SectionConfigID::new(req.section_id).map_err(LogicError::from)?;
player.main_city_model.switch_section(section_id);
player.init_frontend_game()?;
let GameInstance::Frontend(frontend_game) = &mut player.game_instance else {
unreachable!()
};
frontend_game.set_entry_transform(req.transform);
session
.notify(player.game_instance.create_world_init_notify()?)
.await?;
Ok(EnterSectionScRsp {
retcode: Retcode::RetSucc.into(),
})
}
pub async fn on_jump_page_system(
_session: &NetSession,
_player: &mut Player,
_req: JumpPageSystemCsReq,
) -> NetResult<JumpPageSystemScRsp> {
Ok(JumpPageSystemScRsp {
retcode: Retcode::RetSucc.into(),
..Default::default()
})
}
pub async fn on_start_hollow_quest(
session: &NetSession,
player: &mut Player,
req: StartHollowQuestCsReq,
) -> NetResult<StartHollowQuestScRsp> {
use crate::logic::{TimePeriodType, WeatherType};
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 {
EHollowQuestType::RallyBattle => {
player.game_instance = GameInstance::LongFight(
LongFightGame::create_rally_game(
quest_id,
&req.avatars,
req.buddy_id,
TimePeriodType::from_str(&req.quest_time_period),
WeatherType::from_str(&req.quest_weather),
)
.map_err(LogicError::from)?,
)
}
_ => {
player.game_instance = GameInstance::Hollow(
HollowGame::create_pure_hollow_battle(
quest_id,
&req.avatars,
req.buddy_id,
TimePeriodType::from_str(&req.quest_time_period),
WeatherType::from_str(&req.quest_weather),
)
.map_err(LogicError::from)?,
)
}
}
let world_init_notify = player.game_instance.create_world_init_notify()?;
session.notify(world_init_notify).await?;
Ok(StartHollowQuestScRsp {
retcode: Retcode::RetSucc.into(),
quest_id: 0,
})
}
pub async fn on_finish_hollow_battle_event(
_session: &NetSession,
_player: &mut Player,
_req: FinishHollowBattleEventCsReq,
) -> NetResult<FinishHollowBattleEventScRsp> {
Ok(FinishHollowBattleEventScRsp {
retcode: Retcode::RetSucc.into(),
})
}
pub async fn on_long_fight_progress_update(
_session: &NetSession,
_player: &mut Player,
_req: LongFightProgressUpdateCsReq,
) -> NetResult<LongFightProgressUpdateScRsp> {
Ok(LongFightProgressUpdateScRsp {
retcode: Retcode::RetSucc.into(),
})
}

View file

@ -0,0 +1,67 @@
use std::collections::HashMap;
use data::tables::{BattleEventConfigID, OnceRewardID};
use proto::{FightDropInfo, FightReward, RewardItems};
pub struct FightDropPool {
special_drop: Vec<SpecialReward>,
}
pub struct SpecialReward {
pub reward_id: OnceRewardID,
pub item_count_map: HashMap<u32, u32>,
}
impl FightDropPool {
pub fn new(battle_event_id: BattleEventConfigID) -> Self {
let template = battle_event_id.template();
Self {
special_drop: template
.special_drop
.iter()
.map(|id| SpecialReward::new(*id))
.collect(),
}
}
pub fn to_client(&self) -> FightDropInfo {
FightDropInfo {
normal_drop_list: Vec::new(),
special_drop_list: self
.special_drop
.iter()
.map(SpecialReward::to_client)
.collect(),
..Default::default()
}
}
}
impl SpecialReward {
pub fn new(template_id: OnceRewardID) -> Self {
let template = template_id.template();
Self {
reward_id: template_id,
item_count_map: template
.reward_list
.iter()
.map(|r| (r.reward_item_id, r.reward_amount))
.collect(),
}
}
pub fn to_client(&self) -> FightReward {
FightReward {
reward_id: self.reward_id.value(),
fight_reward_map: HashMap::from([(
0,
RewardItems {
reward_item_map: self.item_count_map.clone(),
},
)]),
..Default::default()
}
}
}

View file

@ -0,0 +1,2 @@
mod fight_drop;
pub use fight_drop::FightDropPool;

View file

@ -0,0 +1,9 @@
pub mod drop;
mod quest;
mod team;
pub mod unit;
mod variable;
pub use quest::{DungeonQuestError, DungeonQuestManager};
pub use team::{BuddyParam, TeamDataItem};
pub use variable::LogicVariableTable;

View file

@ -0,0 +1,44 @@
use data::tables::BattleGroupConfigID;
use proto::DungeonQuestInfo;
use thiserror::Error;
#[derive(Default)]
pub struct DungeonQuestManager {
inner_quests: Vec<u32>,
}
#[derive(Error, Debug)]
pub enum DungeonQuestError {
#[error("dungeon inner quest with id {0} is not active")]
QuestNotActive(u32),
}
impl DungeonQuestManager {
pub fn new_for_battle_group(battle_group_id: BattleGroupConfigID) -> Self {
Self {
inner_quests: vec![battle_group_id.template().battle_event_id.value()],
}
}
pub fn finish_quest(&mut self, quest_id: u32) -> Result<(), DungeonQuestError> {
let idx = self
.inner_quests
.iter()
.position(|id| *id == quest_id)
.ok_or(DungeonQuestError::QuestNotActive(quest_id))?;
self.inner_quests.remove(idx);
Ok(())
}
pub fn has_active_quests(&self) -> bool {
!self.inner_quests.is_empty()
}
pub fn to_client(&self) -> DungeonQuestInfo {
DungeonQuestInfo {
inner_quest_id_list: self.inner_quests.clone(),
..Default::default()
}
}
}

View file

@ -0,0 +1,34 @@
use std::collections::HashMap;
use crate::logic::BuddyTeamType;
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 BuddyUnitID, pub BuddyTeamType);
impl TeamDataItem {
pub fn new(avatars: &[AvatarUnitID], buddy_params: &[BuddyParam]) -> Self {
Self {
avatar_member_list: avatars
.iter()
.map(|id| AvatarUnit {
avatar_id: *id,
mp_property_override: HashMap::new(),
})
.collect(),
equipped_buddy_list: buddy_params
.iter()
.map(|param| BuddyUnit {
buddy_id: param.0,
buddy_team: param.1,
override_property_map: HashMap::new(),
})
.collect(),
}
}
}

View file

@ -0,0 +1,42 @@
use std::collections::HashMap;
use crate::logic::BaseProperty;
use data::tables::{AvatarBaseID, RobotConfigID};
use proto::AvatarUnitInfo;
pub struct AvatarUnit {
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: match self.avatar_id {
AvatarUnitID::Base(id) => id.value(),
AvatarUnitID::Robot(id) => id.value(),
},
mp_property_override_map: self
.mp_property_override
.iter()
.map(|(prop, value)| (*prop as u32, *value))
.collect(),
..Default::default()
}
}
}

View file

@ -0,0 +1,44 @@
use std::collections::HashMap;
use data::tables::{BuddyBaseID, RobotBuddyConfigID};
use proto::BuddyUnitInfo;
use crate::logic::{BaseProperty, BuddyTeamType};
pub struct BuddyUnit {
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: 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
.iter()
.map(|(prop, value)| (*prop as u32, *value))
.collect(),
}
}
}

View file

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

View file

@ -0,0 +1,74 @@
use std::collections::HashMap;
use data::tables::{self, BattleEventConfigID, VariableDataID, VariableValueType};
use proto::FightVariable;
pub struct LogicVariableTable {
variable_map: HashMap<String, LogicVariable>,
}
impl LogicVariableTable {
pub fn new(battle_event_id: BattleEventConfigID) -> Self {
let event_id = battle_event_id.value();
Self {
variable_map: tables::variable_data_template_tb::iter()
.filter(|tmpl| tmpl.battle_event_id == event_id)
.map(|tmpl| (tmpl.variable_name.clone(), LogicVariable::new(tmpl.id)))
.collect(),
}
}
pub fn to_client(&self) -> HashMap<String, FightVariable> {
self.variable_map
.iter()
.map(|(name, var)| (name.clone(), var.to_client()))
.collect()
}
}
pub enum LogicVariable {
Int(i64),
Float(f64),
Bool(bool),
}
impl LogicVariable {
pub fn new(id: VariableDataID) -> Self {
let template = id.template();
match template.value_type {
VariableValueType::Int => Self::Int(template.initial_value.parse().unwrap_or_default()),
VariableValueType::Bool => Self::Bool(
template
.initial_value
.to_lowercase()
.parse()
.unwrap_or_default(),
),
VariableValueType::Float => {
Self::Float(template.initial_value.parse().unwrap_or_default())
}
}
}
pub fn to_client(&self) -> FightVariable {
match self {
Self::Int(val) => FightVariable {
r#type: 1,
int_value: *val,
..Default::default()
},
Self::Bool(val) => FightVariable {
r#type: 4,
int_value: *val as i64,
..Default::default()
},
Self::Float(val) => FightVariable {
r#type: 2,
float_value: *val,
..Default::default()
},
}
}
}

View file

@ -12,6 +12,43 @@ pub enum ESceneType {
Fight = 3,
Fresh = 4,
MultiFight = 5,
LongFight = 7,
}
#[allow(dead_code)]
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[repr(u32)]
pub enum EQuestType {
ArchiveFile = 1,
DungeonInner = 2,
Hollow = 3,
Manual = 4,
MainCity = 5,
HollowChallenge = 6,
ArchiveBattle = 7,
Knowledge = 8,
Daily = 9,
}
#[allow(dead_code)]
#[derive(Debug, PartialEq, Eq, Clone, Copy, FromPrimitive)]
#[repr(u32)]
pub enum EHollowQuestType {
ChallengeChaos = 6,
BossRushBattle = 14,
World = 8,
MainQuest = 1,
SideQuest = 2,
#[default]
Common = 0,
Challenge = 5,
PromoteBattle = 11,
DifficutyBattle = 12,
AvatarSide = 7,
RallyBattle = 13,
Urgent = 3,
UrgentSupplement = 4,
NormalBattle = 10,
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, FromPrimitive)]
@ -88,6 +125,17 @@ pub enum TimePeriodType {
Night = 2,
}
impl TimePeriodType {
pub fn from_str(s: &str) -> Self {
match s {
"Morning" => Self::Morning,
"Evening" => Self::Evening,
"Night" => Self::Night,
_ => Self::Morning,
}
}
}
impl Display for TimePeriodType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
@ -108,12 +156,44 @@ pub enum WeatherType {
None = 0xFFFFFFFF,
}
impl WeatherType {
pub fn from_str(s: &str) -> Self {
match s {
"SunShine" => Self::SunShine,
"Fog" => Self::Fog,
"Cloudy" => Self::Cloudy,
"Rain" => Self::Rain,
"Thunder" => Self::Thunder,
"ThickFog" => Self::ThickFog,
"ThickCloudy" => Self::ThickCloudy,
_ => Self::SunShine,
}
}
}
impl Display for WeatherType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
#[repr(u32)]
pub enum ENPCInteraction {
OnStart = 0,
OnEnd = 1,
OnInteract = 2,
OnAddInteract = 3,
OnRemoveInteract = 4,
}
impl Display for ENPCInteraction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}
#[derive(Default, Debug, Clone, Copy)]
pub enum ELocalPlayType {
TrainingRoom = 290,
@ -142,3 +222,108 @@ pub enum ELocalPlayType {
#[default]
Unknown = 0,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u32)]
pub enum BaseProperty {
CurHP = 0,
MaxHpFinal = 36,
AddedDamageRatioFire = 52,
AddedDamageRatioElecBattle = 583,
FeverGetRatio = 45,
AtkBattle = 560,
MaxArmor = 18,
ElementMysteryBattle = 577,
DamageTakeRatio = 42,
Defence = 3,
CritRes = 23,
MaxShield = 20,
CritDmgBattle = 565,
DamageTakeRatioBattle = 573,
BreakStunBattle = 561,
AddedElementAccumulationRatio = 48,
CurSP = 7,
DefBattle = 562,
CritResBattle = 564,
CritDmg = 6,
ElementMystery = 46,
AddedDamageRatioEtherBattle = 584,
EnduranceBattle = 569,
CritBattle = 563,
CurBuddyBattery = 56,
MaxHP = 1,
CurBreakPoint = 27,
Unknown = 4294967295,
SPRecover = 31,
PenRatio = 21,
ResistBreakLevel = 35,
CritDmgRes = 24,
AddedDamageRatioPhysics = 51,
AddedDamageRatioFireBattle = 581,
AddedDamageRatioBattle = 572,
PenDelta = 22,
AutoRecoverStunRate = 37,
AddedDamageRatioIce = 53,
SpGetRatio = 44,
MaxBuddyBattery = 57,
AddedDamageRatioEther = 55,
HpHealRatioBattle = 571,
AddedDamageRatioIceBattle = 582,
Level = 49,
ElementAbnormalPower = 50,
StunMaxBase = 551,
Attack = 2,
MaxSP = 8,
CurMaxHpHealPercent = 32,
MaxStunRuntime = 33,
AllDamageResistBattle = 574,
AddedDamageRatio = 41,
AddedElementAccumulationRatioBattle = 578,
Custom = 10,
Crit = 4,
SpGetRatioBattle = 575,
ResistBreakPoint = 28,
AddedDamageRatioElec = 54,
MaxStun = 12,
HpMaxBase = 550,
CurStun = 11,
CurrentArmor = 17,
HpRecoverRate = 47,
ElementAbnormalPowerBattle = 579,
FeverGetRatioBattle = 576,
CritDmgResBattle = 566,
SpRecoverBattle = 570,
CurEndurance = 25,
PenDeltaBattle = 568,
AccumulationValue = 58,
MaxEndurance = 26,
DestroyRecoverStunRate = 38,
HpHealRatio = 40,
SpMaxBase = 552,
PenRatioBattle = 567,
AddedDamageRatioPhysicsBattle = 580,
BreakStun = 13,
CurrentShield = 19,
Luck = 5,
AllDamageResist = 43,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum BuddyTeamType {
RallyGuidance = 0,
Fighting = 1,
Assisting = 2,
}
impl BuddyTeamType {
pub fn to_protocol(&self) -> ::proto::BuddyTeamType {
match *self {
Self::RallyGuidance => ::proto::BuddyTeamType::RallyGuidance,
Self::Fighting => ::proto::BuddyTeamType::Fighting,
Self::Assisting => ::proto::BuddyTeamType::Assisting,
}
}
}

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

@ -0,0 +1,531 @@
use super::record::*;
use super::stat::*;
use data::gacha;
use data::gacha::gacha_config::*;
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_status_map: HashMap<String, GachaStatus>,
pub gacha_records: Vec<GachaRecord>,
}
impl Default for GachaModel {
fn default() -> GachaModel {
let result = GachaModel {
gacha_status_map: HashMap::new(),
gacha_records: vec![],
};
result.post_deserialize()
}
}
impl GachaModel {
pub fn from_bin(gacha_bin: GachaModelBin) -> Self {
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 {
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 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,
);
for rarity_items in gacha_pool.gacha_items.iter() {
let progress_bin =
get_or_add(&mut status_bin.rarity_status_map, &rarity_items.rarity);
if progress_bin.pity <= 0 {
progress_bin.pity = 1;
}
for category_guarantee_policy_tag in
rarity_items.category_guarantee_policy_tags.iter()
{
get_or_add(
&mut progress_bin.categories_progress_map,
&category_guarantee_policy_tag,
);
let guarantee_policy = gachaconf
.category_guarantee_policy_map
.get(category_guarantee_policy_tag)
.unwrap();
if !guarantee_policy.chooseable {
continue;
}
get_or_add(
&mut progress_bin.categories_chosen_guarantee_progress_map,
&category_guarantee_policy_tag,
);
}
}
for discount_policy_tag in gacha_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();
get_or_add(
&mut status_bin.discount_usage_map,
&policy.free_select_progress_record_tag,
);
get_or_add(
&mut status_bin.discount_usage_map,
&policy.free_select_usage_record_tag,
);
} else {
get_or_add(&mut status_bin.discount_usage_map, &discount_policy_tag);
}
}
}
self
}
pub fn perform_pull_pool<'bin, 'conf>(
&'bin mut self,
pull_time: &DateTime<Local>,
target_pool: &'conf CharacterGachaPool,
) -> GachaRecord {
let (rarity_items, progress_bin, status_bin, probability_model) =
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,
target_pool,
status_bin,
progress_bin,
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>(
map: &'a mut HashMap<K, V, S>,
key: &K,
) -> &'a mut V {
if !map.contains_key(key) {
map.insert(key.clone(), V::default());
}
map.get_mut(key).unwrap()
}
fn determine_gacha_result<'bin, 'conf>(
pull_time: &DateTime<Local>,
category: &'conf GachaCategoryInfo,
target_pool: &'conf CharacterGachaPool,
status_bin: &'bin GachaStatus,
progress_bin: &'bin GachaProgress,
rarity_items: &'conf GachaAvailableItemsInfo,
) -> GachaRecord {
let gachaconf = gacha::global_gacha_config();
let item_pool_len = category.item_ids.len() as u32;
let mut item_id: Option<&u32> = None;
// We should see whether user's search priority exists.
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;
}
// Firstly, judge whether the user failed enough times.
// The user is limited to get only this category's item,
// so we should record the user's failure to get his
// selected item elsewhere.
if progress_bin
.categories_chosen_guarantee_progress_map
.get(guarantee_policy_tag)
.unwrap()
< &category_guarantee_policy.trigger_on_failure_times
{
continue;
}
// We directly look whether user chose an UP item.
if let Some(item) = progress_bin
.categories_chosen_guarantee_item_map
.get(guarantee_policy_tag)
{
item_id = Some(item);
}
}
let item_id = match item_id {
Some(val) => val,
None => category
.item_ids
.get(rand::thread_rng().gen_range(0..item_pool_len) as usize)
.unwrap(),
};
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() {
let extra_items_policy = gachaconf
.extra_items_policy_map
.get(extra_items_policy_tag)
.unwrap();
// 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 = ItemID::new(extra_items_policy.id).ok();
extra_item_count = extra_items_policy.count;
}
}
let extra_resources = match extra_item_id {
Some(item_id) => Some(GachaExtraResources {
extra_item_id: item_id,
extra_item_count,
}),
None => None,
};
GachaRecord {
pull_timestamp: pull_time.timestamp(),
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_resources,
item_type: category.item_type.clone(),
}
}

View file

@ -0,0 +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,3 +1,4 @@
use data::tables::ProcedureConfigID;
use proto::{DungeonInfo, FreshSceneInfo, SceneInfo};
use crate::logic::{procedure::ProcedureManager, ESceneType};
@ -9,7 +10,7 @@ pub struct FreshGame {
}
impl FreshGame {
pub fn new(start_procedure_id: i32) -> Self {
pub fn new(start_procedure_id: ProcedureConfigID) -> Self {
Self {
procedure_mgr: ProcedureManager::new(start_procedure_id),
}
@ -25,7 +26,11 @@ impl NapGameMode for FreshGame {
Some(SceneInfo {
scene_type: self.scene_type() as u32,
fresh_scene_info: Some(FreshSceneInfo {
beginner_procedure_id: (self.procedure_mgr.procedure_id() - 1) as u32,
beginner_procedure_id: self
.procedure_mgr
.procedure_id()
.map(|i| i.value() - 1)
.unwrap_or(0),
}),
..Default::default()
})

View file

@ -1,17 +1,24 @@
use data::tables;
use data::tables::{AvatarBaseID, MainCityBgmConfigID, SectionConfigID};
use proto::*;
use thiserror::Error;
use crate::logic::{math::Vector3f, time::MainCityTime, ESceneType};
use crate::logic::{
math::Vector3f,
scene::{SceneUnit, SceneUnitManager},
time::MainCityTime,
ESceneType,
};
use super::NapGameMode;
pub struct FrontendGame {
section_id: u32,
frontend_avatar_id: i32,
section_id: SectionConfigID,
frontend_avatar_id: AvatarBaseID,
main_city_bgm: MainCityBgmConfigID,
scene_unit_mgr: SceneUnitManager,
camera_x: u32,
camera_y: u32,
born_pos: String,
entry_transform: String,
main_city_time: MainCityTime,
avatar_pos: Vector3f,
avatar_rot: Vector3f,
@ -19,14 +26,16 @@ pub struct FrontendGame {
#[derive(Error, Debug)]
pub enum FrontendGameError {
#[error("section id is invalid ({0})")]
InvalidSection(u32),
#[error("player's frontend avatar is None")]
NoFrontendAvatar,
}
impl FrontendGame {
const DEFAULT_BGM_ID: u32 = 1005;
pub fn new(
section_id: u32,
avatar_id: i32,
section_id: SectionConfigID,
avatar_id: AvatarBaseID,
main_city_time: MainCityTime,
avatar_pos: Vector3f,
avatar_rot: Vector3f,
@ -34,11 +43,12 @@ impl FrontendGame {
let instance = Self {
section_id,
main_city_time,
main_city_bgm: MainCityBgmConfigID::new_unchecked(Self::DEFAULT_BGM_ID),
scene_unit_mgr: SceneUnitManager::new(section_id),
frontend_avatar_id: avatar_id,
camera_x: 0xFFFFFFFF,
camera_y: 0xFFFFFFFF,
born_pos: Self::get_default_stage_entry_name(section_id)
.ok_or(FrontendGameError::InvalidSection(section_id))?,
entry_transform: section_id.template().primary_entry_name.clone(),
avatar_pos,
avatar_rot,
};
@ -47,10 +57,8 @@ impl FrontendGame {
Ok(instance)
}
fn get_default_stage_entry_name(section_id: u32) -> Option<String> {
tables::section_config_template_tb::iter()
.find(|tmpl| tmpl.section_id == section_id)
.map(|tmpl| tmpl.primary_entry_name.clone())
pub fn set_entry_transform(&mut self, transform_name: String) {
self.entry_transform = transform_name;
}
}
@ -59,14 +67,15 @@ impl NapGameMode for FrontendGame {
Some(SceneInfo {
scene_type: self.scene_type() as u32,
hall_scene_info: Some(HallSceneInfo {
section_id: self.section_id,
frontend_avatar_id: self.frontend_avatar_id as u32,
section_id: self.section_id.value(),
frontend_avatar_id: self.frontend_avatar_id.value(),
main_city_bgm_id: self.main_city_bgm.value(),
camera_x: self.camera_x,
camera_y: self.camera_y,
born_pos: self
transform: self
.avatar_pos
.is_zero()
.then_some(self.born_pos.clone())
.then_some(self.entry_transform.clone())
.unwrap_or_default(),
day_of_week: self.main_city_time.day_of_week.value(),
time_of_day: self.main_city_time.time_of_day.value(),
@ -74,6 +83,12 @@ impl NapGameMode for FrontendGame {
position: self.avatar_pos.to_vec(),
rotation: self.avatar_rot.to_vec(),
}),
scene_unit_list: self
.scene_unit_mgr
.unit_vec
.iter()
.map(SceneUnit::protocol_info)
.collect(),
..Default::default()
}),
..Default::default()

View file

@ -1,45 +1,169 @@
use common::util;
use data::tables;
use data::tables::{
self, ArchiveBattleQuestID, AvatarBaseID, BattleEventConfigID, BuddyBaseID, HollowQuestID,
RobotBuddyConfigID, RobotConfigID, TrainingQuestID,
};
use proto::{DungeonInfo, DungeonItemData, FightSceneInfo, SceneInfo, WeatherPoolInfo};
use thiserror::Error;
use crate::logic::{ELocalPlayType, ESceneType, TimePeriodType, WeatherType};
use crate::logic::{
battle::{
drop::FightDropPool,
unit::{AvatarUnit, AvatarUnitID, BuddyUnit, BuddyUnitID},
BuddyParam, DungeonQuestManager, TeamDataItem,
},
BuddyTeamType, EHollowQuestType, ELocalPlayType, ESceneType, TimePeriodType, WeatherType,
};
use super::NapGameMode;
#[derive(Error, Debug)]
pub enum HollowGameError {
#[error("quest id is invalid: {0}")]
InvalidQuestId(u32),
#[error("Quest ({0}) type is not supported: {1:?}")]
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 {
pub quest_id: u32,
pub battle_event_id: u32,
pub battle_event_id: BattleEventConfigID,
pub time_period: TimePeriodType,
pub weather: WeatherType,
pub play_type: ELocalPlayType,
pub start_timestamp: i64,
pub team_data: TeamDataItem,
pub fight_drop_pool: FightDropPool,
pub quest_manager: DungeonQuestManager,
}
impl HollowGame {
pub fn create_training_game(
training_quest_id: u32,
training_quest_id: TrainingQuestID,
play_type: ELocalPlayType,
avatars: &[u32],
) -> Result<Self, HollowGameError> {
let template = tables::training_quest_template_tb::iter()
.find(|tmpl| tmpl.id == training_quest_id)
.ok_or(HollowGameError::InvalidQuestId(training_quest_id))?;
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,
quest_id: template.id.value(),
battle_event_id: template.battle_event_id,
time_period: TimePeriodType::Morning,
weather: WeatherType::SunShine,
start_timestamp: util::cur_timestamp() as i64,
play_type,
team_data: TeamDataItem::new(&avatars, &[]),
fight_drop_pool: FightDropPool::new(template.battle_event_id),
quest_manager: DungeonQuestManager::default(),
})
}
pub fn create_archive_battle(
archive_battle_quest_id: ArchiveBattleQuestID,
play_type: ELocalPlayType,
avatars: &[u32],
buddy_id: u32,
) -> 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,
time_period: TimePeriodType::Morning,
weather: WeatherType::SunShine,
start_timestamp: util::cur_timestamp() as i64,
play_type,
team_data: TeamDataItem::new(&avatars, &buddy_params),
fight_drop_pool: FightDropPool::new(template.battle_event_id),
quest_manager: DungeonQuestManager::default(),
})
}
pub fn create_pure_hollow_battle(
quest_id: HollowQuestID,
avatars: &[u32],
buddy_id: u32,
time_period: TimePeriodType,
weather: WeatherType,
) -> Result<Self, HollowGameError> {
let template = quest_id.template();
if template.chess_board_id != 0 {
return Err(HollowGameError::QuestTypeNotSupported(
template.id.value(),
EHollowQuestType::from(template.hollow_quest_type),
));
}
let Some(battle_group) = tables::battle_group_config_template_tb::iter()
.find(|tmpl| tmpl.quest_id == template.id.value())
else {
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,
time_period,
weather,
start_timestamp: util::cur_timestamp() as i64,
play_type: Self::get_play_type_by_quest_type(EHollowQuestType::from(
template.hollow_quest_type,
)),
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),
})
}
pub fn get_play_type_by_quest_type(quest_type: EHollowQuestType) -> ELocalPlayType {
match quest_type {
EHollowQuestType::NormalBattle => ELocalPlayType::PureHollowBattle,
EHollowQuestType::BossRushBattle => ELocalPlayType::BossRushBattle,
EHollowQuestType::DifficutyBattle => ELocalPlayType::PureHollowBattleHardmode,
_ => ELocalPlayType::PureHollowBattle,
}
}
}
impl NapGameMode for HollowGame {
@ -50,14 +174,16 @@ impl NapGameMode for HollowGame {
fn scene_info(&self) -> Option<SceneInfo> {
Some(SceneInfo {
scene_type: self.scene_type() as u32,
battle_event_id: self.battle_event_id,
battle_event_id: self.battle_event_id.value(),
play_type: self.play_type as u32,
fight_scene_info: Some(FightSceneInfo {
nmhdkmcabjg: true,
weather_pool: Some(WeatherPoolInfo {
time_period: self.time_period.to_string(),
weather: self.weather.to_string(),
..Default::default()
}),
fight_drop_info: Some(self.fight_drop_pool.to_client()),
..Default::default()
}),
..Default::default()
@ -69,6 +195,19 @@ impl NapGameMode for HollowGame {
quest_id: self.quest_id,
start_timestamp: self.start_timestamp,
dungeon_item_data: Some(DungeonItemData::default()),
avatar_list: self
.team_data
.avatar_member_list
.iter()
.map(AvatarUnit::to_client)
.collect(),
buddy_list: self
.team_data
.equipped_buddy_list
.iter()
.map(BuddyUnit::to_client)
.collect(),
dungeon_quest_info: Some(self.quest_manager.to_client()),
..Default::default()
})
}

View file

@ -0,0 +1,155 @@
use common::util;
use data::tables::{self, AvatarBaseID, BattleEventConfigID, BuddyBaseID, HollowQuestID};
use proto::{
DungeonInfo, DungeonItemData, FightQuestInfo, LongFightInfo, LongFightSceneInfo, SceneInfo,
WeatherPoolInfo,
};
use thiserror::Error;
use crate::logic::{
battle::{
drop::FightDropPool,
unit::{AvatarUnit, AvatarUnitID, BuddyUnit, BuddyUnitID},
BuddyParam, DungeonQuestManager, LogicVariableTable, TeamDataItem,
},
BuddyTeamType, EHollowQuestType, ELocalPlayType, ESceneType, TimePeriodType, WeatherType,
};
use super::NapGameMode;
#[derive(Error, Debug)]
pub enum LongFightGameError {
#[error("Tried to run quest of type {0:?} using LongFight logic")]
InvalidQuestType(EHollowQuestType),
#[error("Battle group not found, quest id: {0}")]
BattleGroupNotFound(u32),
#[error("Invalid avatar id: {0}")]
InvalidAvatarID(u32),
}
pub struct LongFightGame {
pub quest_id: u32,
pub battle_event_id: BattleEventConfigID,
pub play_type: ELocalPlayType,
pub time_period: TimePeriodType,
pub weather: WeatherType,
pub start_timestamp: i64,
pub team_data: TeamDataItem,
pub variable_table: LogicVariableTable,
pub fight_drop_pool: FightDropPool,
pub quest_manager: DungeonQuestManager,
}
impl LongFightGame {
const RALLY_GUIDANCE_BUDDY: BuddyBaseID = BuddyBaseID::new_unchecked(50001);
pub fn create_rally_game(
quest_id: HollowQuestID,
avatars: &[u32],
buddy_id: u32,
time_period: TimePeriodType,
weather: WeatherType,
) -> Result<Self, LongFightGameError> {
let template = quest_id.template();
let quest_type = EHollowQuestType::from(template.hollow_quest_type);
if quest_type != EHollowQuestType::RallyBattle {
return Err(LongFightGameError::InvalidQuestType(quest_type));
}
let Some(battle_group) = tables::battle_group_config_template_tb::iter()
.find(|tmpl| tmpl.quest_id == template.id.value())
else {
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(
BuddyUnitID::Base(Self::RALLY_GUIDANCE_BUDDY),
BuddyTeamType::RallyGuidance,
)];
if let Ok(buddy_id) = BuddyBaseID::new(buddy_id) {
buddy_params.push(BuddyParam(
BuddyUnitID::Base(buddy_id),
BuddyTeamType::Fighting,
));
}
Ok(Self {
quest_id: template.id.value(),
battle_event_id: battle_group.battle_event_id,
play_type: ELocalPlayType::RallyLongFight,
time_period,
weather,
start_timestamp: util::cur_timestamp() as i64,
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),
})
}
}
impl NapGameMode for LongFightGame {
fn scene_type(&self) -> ESceneType {
ESceneType::LongFight
}
fn scene_info(&self) -> Option<SceneInfo> {
Some(SceneInfo {
scene_type: self.scene_type() as u32,
battle_event_id: self.battle_event_id.value(),
play_type: self.play_type as u32,
long_fight_scene_info: Some(LongFightSceneInfo {
fight_data: Some(LongFightInfo {
fight_quest_info: Some(FightQuestInfo {
fight_variable_map: self.variable_table.to_client(),
..Default::default()
}),
..Default::default()
}),
fight_drop_info: Some(self.fight_drop_pool.to_client()),
weather_pool: Some(WeatherPoolInfo {
time_period: self.time_period.to_string(),
weather: self.weather.to_string(),
febgjinpcbp: true,
bejeblcfcha: true,
..Default::default()
}),
..Default::default()
}),
..Default::default()
})
}
fn dungeon_info(&self) -> Option<DungeonInfo> {
Some(DungeonInfo {
quest_id: self.quest_id,
start_timestamp: self.start_timestamp,
dungeon_item_data: Some(DungeonItemData::default()),
avatar_list: self
.team_data
.avatar_member_list
.iter()
.map(AvatarUnit::to_client)
.collect(),
buddy_list: self
.team_data
.equipped_buddy_list
.iter()
.map(BuddyUnit::to_client)
.collect(),
dungeon_quest_info: Some(self.quest_manager.to_client()),
..Default::default()
})
}
}

View file

@ -1,20 +1,24 @@
mod fresh;
mod frontend;
mod hollow;
mod long_fight;
use data::tables::TemplateNotFoundError;
pub use fresh::*;
pub use frontend::*;
pub use hollow::*;
pub use long_fight::*;
use proto::{DungeonInfo, SceneInfo, WorldInitScNotify};
use thiserror::Error;
use super::{procedure::ProcedureError, ESceneType};
use super::{battle::DungeonQuestError, procedure::ProcedureError, ESceneType};
#[derive(Default)]
pub enum GameInstance {
Frontend(FrontendGame),
Fresh(FreshGame),
Hollow(HollowGame),
LongFight(LongFightGame),
#[default]
Null,
}
@ -29,6 +33,12 @@ pub enum LogicError {
Procedure(#[from] ProcedureError),
#[error("hollow error: {0}")]
Hollow(#[from] HollowGameError),
#[error("longfight error: {0}")]
LongFight(#[from] LongFightGameError),
#[error("dungeon quest error: {0}")]
DungeonQuest(#[from] DungeonQuestError),
#[error("{0}")]
TemplateNotFound(#[from] TemplateNotFoundError),
}
impl GameInstance {
@ -41,6 +51,7 @@ impl GameInstance {
Self::Frontend(game) => game,
Self::Fresh(game) => game,
Self::Hollow(game) => game,
Self::LongFight(game) => game,
Self::Null => return Err(LogicError::GameIsNull),
}))
}

View file

@ -1,24 +1,31 @@
use super::ResourceItem;
use proto::ItemModelBin;
use super::{ItemUID, ResourceItem, Weapon};
use data::tables::WeaponID;
use proto::{ItemModelBin, ItemSync};
pub struct ItemModel {
uid_counter: u32,
pub resources: Vec<ResourceItem>,
pub weapons: Vec<Weapon>,
}
impl ItemModel {
pub fn from_bin(bin: ItemModelBin) -> Self {
Self {
uid_counter: bin.item_uid_counter,
resources: bin
.resource_list
.into_iter()
.map(ResourceItem::from_bin)
.collect(),
weapons: bin.weapon_list.into_iter().map(Weapon::from_bin).collect(),
}
}
pub fn to_bin(&self) -> ItemModelBin {
ItemModelBin {
item_uid_counter: self.uid_counter,
resource_list: self.resources.iter().map(ResourceItem::to_bin).collect(),
weapon_list: self.weapons.iter().map(Weapon::to_bin).collect(),
}
}
@ -36,12 +43,34 @@ impl ItemModel {
});
}
}
pub fn add_weapon(&mut self, template_id: WeaponID) -> ItemUID {
let uid = self.next_uid();
self.weapons.push(Weapon::new(template_id, uid));
uid
}
pub fn item_sync(&self) -> ItemSync {
ItemSync {
resource_list: self.resources.iter().map(|i| i.to_client()).collect(),
weapon_list: self.weapons.iter().map(|w| w.to_client()).collect(),
..Default::default()
}
}
fn next_uid(&mut self) -> ItemUID {
self.uid_counter += 1;
self.uid_counter.into()
}
}
impl Default for ItemModel {
fn default() -> Self {
Self {
uid_counter: 0,
resources: Vec::new(),
weapons: Vec::new(),
}
}
}

View file

@ -1,5 +1,22 @@
mod item_model;
mod resource;
mod weapon;
pub use item_model::ItemModel;
pub use resource::ResourceItem;
pub use weapon::Weapon;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct ItemUID(u32);
impl ItemUID {
pub fn value(&self) -> u32 {
self.0
}
}
impl From<u32> for ItemUID {
fn from(value: u32) -> Self {
Self(value)
}
}

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