Compare commits
382 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0624334c4 | |||
| 34bac4c5b5 | |||
| b81385efdb | |||
| 2aae90e65a | |||
| 64f2cb8f9e | |||
| 2820b8866c | |||
| a39881fa89 | |||
| 9d4116f035 | |||
| 287296cf48 | |||
| b491a57f57 | |||
| 19e7f1c677 | |||
| 8a7e19757a | |||
| 70c57e7727 | |||
| 4a8415308e | |||
| 0e3df10fc0 | |||
| 33b95015a2 | |||
| 2ecef68a72 | |||
| 0db0a55e6b | |||
| 0aedfcc17f | |||
| 581f95051d | |||
| 31005ec39d | |||
| b0b3bb6140 | |||
| 7e4bc52d99 | |||
| b9f1a1d964 | |||
| a48f79eafa | |||
| 907c4fda3c | |||
| 3189b71d46 | |||
| 6ae08e9b05 | |||
| 7cd5aa1c2d | |||
| 6d6a8621bb | |||
| db254a977b | |||
| 454bcf107b | |||
| 52688982ea | |||
| 2432d8b32b | |||
| 7f71b87b9b | |||
| 4faad54872 | |||
| e2da4322e2 | |||
| f44706570a | |||
| b452b11854 | |||
| f2b5f0950f | |||
| f43563edb3 | |||
| bec6d741d4 | |||
| d93e6405c3 | |||
| a2e3f4882d | |||
| ef101894d1 | |||
| 6eb896f83d | |||
| c7812bf764 | |||
| 11f49af6f9 | |||
| af1c51b2b5 | |||
| f7c63d82f9 | |||
| a00c25ee17 | |||
| 913f7d04f7 | |||
| b37224a453 | |||
| 8375c61236 | |||
| 424f191bc6 | |||
| 90152b4138 | |||
| c8041558f5 | |||
| 1f10d03923 | |||
| bb560c1153 | |||
| 72794ad50e | |||
| af1c0a548d | |||
| 2f5d547c19 | |||
| 32f056c6eb | |||
| ac62cc455c | |||
| 79f85f46dc | |||
| e2e5875c8d | |||
| 3868a9fc50 | |||
| 28cb1c52b5 | |||
| 70325793d9 | |||
| a2d1eb4532 | |||
| b17ccd264a | |||
| eaa02b2b78 | |||
| c3b3cf5140 | |||
| 3be7b5f56b | |||
| 14bf23c496 | |||
| 5b79785c96 | |||
| f92fe61aa7 | |||
| b7c9fb3864 | |||
| 294d180e68 | |||
| 7dc5a02a83 | |||
| 82004b05dc | |||
| a4f69f6ca3 | |||
| 66571d751f | |||
| 680a1a797c | |||
| 543bbb45dc | |||
| 38504b3133 | |||
| f0d15be552 | |||
| 0383dc90b8 | |||
| 4e4ba5650d | |||
| 29baaf2d95 | |||
| 67e64d6836 | |||
| af8c27dcef | |||
| 163ec73c04 | |||
| b74ad9d639 | |||
| 42c72b92ac | |||
| b46be572a6 | |||
| 5d2d4cf2ad | |||
| 2ba4224a83 | |||
| 9687a0e522 | |||
| cd77fae4e3 | |||
| f2f1007cee | |||
| db2c2a4774 | |||
| f16b8ef983 | |||
| bd13950ba6 | |||
| cda86e586d | |||
| 255878bf60 | |||
| 1d42faac3e | |||
| 350a89f3da | |||
| 5bfda213c7 | |||
| d3d63dd36c | |||
| 4dd7b75232 | |||
| 26abf2f306 | |||
| 9ff7d6fff3 | |||
| 8c514a0688 | |||
| 08ba5d821b | |||
| 35e2a9d6f4 | |||
| 46e509aa69 | |||
| 198db59816 | |||
| 46667bce46 | |||
| 639c1c3e95 | |||
| 07ebafa8c6 | |||
| f548fc04e2 | |||
| c55b19dbc0 | |||
| c78c91d408 | |||
| e07f65eec5 | |||
| cfbbdc7216 | |||
| cb34b350b0 | |||
| 23f3bfabaa | |||
| b66069c10b | |||
| 093ba1fd38 | |||
| a312191ced | |||
| 841c722178 | |||
| 1ed2112bff | |||
| d015406fa6 | |||
| eea9eaf672 | |||
| c79e5017ad | |||
| c3d56f630e | |||
| b1f419e337 | |||
| 068ef68dd6 | |||
| 51d74b092a | |||
| 884a5ce75a | |||
| d0c3e1b7d8 | |||
| 0fe28c021a | |||
| e6e599d760 | |||
| 753c8da4bb | |||
| 8165f240dc | |||
| f98fae470b | |||
| fcc274ce3e | |||
| 47533e1a5f | |||
| 20f5a92d81 | |||
| dcea0e4520 | |||
| 729d9af4b0 | |||
| 01afe12487 | |||
| c57dc64950 | |||
| 07996444a1 | |||
| ba53f67097 | |||
| 7fad72ef9c | |||
| 964f646654 | |||
| 7a23b37c0c | |||
| bfd5c246de | |||
| b89f18ce4e | |||
| 97cf9c5093 | |||
| c6e930b994 | |||
| 611193610b | |||
| 4c735d055e | |||
| adb79e8a41 | |||
| 0f4e4fa48e | |||
| 5bf868e2aa | |||
| f6f5c358eb | |||
| 50f3ebca5e | |||
| ef89699d59 | |||
| b6817e278a | |||
| 4830f5a41e | |||
| 340fbb8ca5 | |||
| 7aa05f39e2 | |||
| 5e2cc6f07f | |||
| 34f05e5162 | |||
| d75891e78b | |||
| 9bf1114535 | |||
| 9084910235 | |||
| 33407f88d7 | |||
| 82854604b8 | |||
| 6ac2ceca45 | |||
| 082f88d242 | |||
| 0fff4ebd4e | |||
| 36a370078c | |||
| 1788aebd00 | |||
| 0de3d2737f | |||
| fc6b0992e9 | |||
| 111d45220e | |||
| fed1044813 | |||
| 3b9c887dbe | |||
| 80a57f9d3e | |||
| db3cecdd2b | |||
| e13b5950ca | |||
| fe1d5a874a | |||
| ea76a537fd | |||
| be0569d2cb | |||
| c5e8d2c77c | |||
| 408bc1befc | |||
| 86e98fbfe5 | |||
| c85b3c144e | |||
| 9311483932 | |||
| c15e154846 | |||
| 02e8f8ea8b | |||
| 31ddde6e80 | |||
| 4a23d86f56 | |||
| 1453cd4c9c | |||
| be8130b621 | |||
| 9e8f7a6c6b | |||
| d052163a9e | |||
| f188ea1554 | |||
| a9894e2d05 | |||
| 0a60a24783 | |||
| d8f8dfc53f | |||
| cc8dd77d51 | |||
| d5d85bf5d9 | |||
| 2dff814e8f | |||
| ad86acd8ef | |||
| 68be13dd62 | |||
| 9e0dfc7749 | |||
| 3747025a11 | |||
| e5d4ae1f80 | |||
| 07ea97a6ea | |||
| 9a5d8f9d1a | |||
| ad2312efee | |||
| dfe1944d2b | |||
| 695404165b | |||
| d3bc2dad4f | |||
| 194e408863 | |||
| b2350a537d | |||
| ba4681e35d | |||
| 3b9684d8ac | |||
| d32c5f1d61 | |||
| cf2c8f0699 | |||
| c8681bcf05 | |||
| fe256cff2a | |||
| 1df03c45f7 | |||
| 458e2ef0cd | |||
| dd4284ab63 | |||
| 251cc80233 | |||
| c6baed2d23 | |||
| 90e2889204 | |||
| ea4f6da48e | |||
| b69cf96aa9 | |||
| cbf4540602 | |||
| 058d1ede54 | |||
| d3c2a0bad0 | |||
| 83f5487e7b | |||
| d3d89f0168 | |||
| b7257a793f | |||
| e50d7a4e65 | |||
| 4be431471c | |||
| 649a7c9871 | |||
| 7fc3cca11b | |||
| c9d7fe1c2a | |||
| 612b5d28ba | |||
| 70207896e3 | |||
| 08437844e4 | |||
| e13b220be9 | |||
| fccc0f7346 | |||
| 1449bf090b | |||
| c9902e386f | |||
| fb7d70c943 | |||
| e066c383a0 | |||
| 0e9f66f72e | |||
| ec99dad874 | |||
| b85fd4fced | |||
| 2050173666 | |||
| df29a60a6e | |||
| 78e407a70f | |||
| 04e2f94e2b | |||
| 4124f2714a | |||
| e21365db78 | |||
| dae7946526 | |||
| 6a37a2de3d | |||
| 4f650bebf0 | |||
| eb5827e059 | |||
| 6917f40d3e | |||
| efe2515a44 | |||
| c6ce39623e | |||
| 962ee6874e | |||
| 2fda85c750 | |||
| f1e00ccf0e | |||
| 09b7885013 | |||
| e126015b5f | |||
| 4ff4c86047 | |||
| cd4a8050d7 | |||
| c09bd56e19 | |||
| 6945a55584 | |||
| 32c79a7b6a | |||
| 57f47f147a | |||
| 6a65940720 | |||
| 40dcbb77ad | |||
| f479f586cb | |||
| a24d0ad703 | |||
| ac39db2f36 | |||
| 9b4da7e3b3 | |||
| 1f1f4bd815 | |||
| 00258d4607 | |||
| 3aaaf0353e | |||
| f54d7b0476 | |||
| 111260cdf3 | |||
| 91c8cba0d2 | |||
| 0f8dcd3713 | |||
| e89802f288 | |||
| c1ac34c1f7 | |||
| c74a931986 | |||
| 686bae25f3 | |||
| ff5d0af7ad | |||
| 8518349cce | |||
| 818204a93f | |||
| eea12d8d75 | |||
| 43ee4a9c5a | |||
| 7ee7af0b0f | |||
| d15f1cc1a3 | |||
| 4f2432cbac | |||
| 60f6b609da | |||
| 1058998550 | |||
| c00b554b56 | |||
| 0bd3bb7b77 | |||
| b6cfb5b2a2 | |||
| c1bcd45ea1 | |||
| 5ba652aa38 | |||
| 1ba50e96ca | |||
| 7b7c9d371f | |||
| 09ac8921fe | |||
| 29a4347f2b | |||
| 68cf06c6d0 | |||
| 5307051e04 | |||
| 045ff9b169 | |||
| c1122e1f90 | |||
| d478e9b0be | |||
| 2aa699b5b0 | |||
| c96cfad4d2 | |||
| bf26e437ff | |||
| 9efdf88101 | |||
| 4273ae84f4 | |||
| b49408a88b | |||
| 764fbf8841 | |||
| f74b416c19 | |||
| 8104fd0853 | |||
| 910555f299 | |||
| 2dd7601dbd | |||
| d7e390e494 | |||
| c8b001411e | |||
| a5265874a2 | |||
| 81eaa893b9 | |||
| a0e84b5d5c | |||
| e8891adf8e | |||
| 1a2d5c1772 | |||
| 65b9048ab6 | |||
| ccd1b56cae | |||
| 5382e12b8d | |||
| 2cdebd5f20 | |||
| 61e5460bc1 | |||
| c100d76a5b | |||
| d59b59cd51 | |||
| c7059874d3 | |||
| dfc451e86a | |||
| d1022e9b53 | |||
| 7c9309f6c5 | |||
| 441457a873 | |||
| 9255037f50 | |||
| 4c95adcdb3 | |||
| 2ef6acaa0e | |||
| a8061efc0d | |||
| 206552ed63 | |||
| 9e48259414 | |||
| ad32c0a986 | |||
| 84ed80365c | |||
| 87440437fb | |||
| 2aca408a9e | |||
| 3991d7b534 | |||
| 3823fc94f1 | |||
| 190e89181e | |||
| 20ca2529ac | |||
| 143da7e5a5 | |||
| 37b95f35c2 | |||
| aed2c61706 | |||
| 0955d1e5fd | |||
| e51924bf49 |
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report incorrect behavior or unexpected errors
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
Write a clear and concise description of what the bug is, and what you expected to happen instead.
|
||||
|
||||
**To reproduce**
|
||||
Fill in steps to reproduce the behavior, such as:
|
||||
1. Connect to server
|
||||
2. Create a game
|
||||
3. Start quest X
|
||||
4. Talk to NPC Y
|
||||
|
||||
**Game version(s) (choose one or more of the following):**
|
||||
DC NTE, DC prototype, DC v1, DC v2, PC, GC Ep1&2, GC Ep3, Xbox, BB
|
||||
|
||||
**Server log output**
|
||||
On macOS/Linux, or in a Cygwin shell on Windows, you can run the server as `./newserv 2>&1 | tee server-log.txt` to generate a log file. Do that, then do whatever you need to do to get the bug to happen, then upload the log file here.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
Write a clear and concise description of what the problem is. For example, "I'm always frustrated when [...]"
|
||||
|
||||
**Describe the solution you'd like**
|
||||
Write a clear and concise description of what you want to happen.
|
||||
|
||||
**Game version(s) (choose one or more of the following):**
|
||||
DC NTE, DC prototype, DC v1, DC v2, PC, GC Ep1&2, GC Ep3, Xbox, BB
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -2,6 +2,7 @@
|
||||
.DS_Store
|
||||
|
||||
# Build products
|
||||
src/Revision.cc
|
||||
newserv
|
||||
|
||||
# CMake files
|
||||
@@ -26,6 +27,7 @@ system/players/*.psocard
|
||||
system/players/*.nsc
|
||||
system/players/*.nsa
|
||||
system/teams/*.json
|
||||
system/teams/*.bmp
|
||||
system/patch-pc/.metadata-cache.json
|
||||
system/patch-bb/.metadata-cache.json
|
||||
|
||||
|
||||
+27
-4
@@ -27,10 +27,12 @@ link_directories(${LOCAL_LIB_DIR})
|
||||
find_path (LIBEVENT_INCLUDE_DIR NAMES event.h)
|
||||
find_library (LIBEVENT_LIBRARY NAMES event)
|
||||
find_library (LIBEVENT_CORE NAMES event_core)
|
||||
find_library (LIBEVENT_PTHREADS NAMES event_pthreads)
|
||||
set (LIBEVENT_INCLUDE_DIRS ${LIBEVENT_INCLUDE_DIR})
|
||||
set (LIBEVENT_LIBRARIES
|
||||
${LIBEVENT_LIBRARY}
|
||||
${LIBEVENT_CORE})
|
||||
${LIBEVENT_CORE}
|
||||
${LIBEVENT_PTHREADS})
|
||||
|
||||
find_package(phosg REQUIRED)
|
||||
find_package(Iconv REQUIRED)
|
||||
@@ -38,9 +40,24 @@ find_package(resource_file QUIET)
|
||||
|
||||
|
||||
|
||||
# Git metadata
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/src/Revision.cc ${CMAKE_CURRENT_SOURCE_DIR}/src/__Revision__.cc
|
||||
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/src/Revision-generate.sh ${CMAKE_CURRENT_SOURCE_DIR}/src/Revision.cc
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
)
|
||||
add_custom_target(
|
||||
newserv-Revision-cc
|
||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/Revision.cc ${CMAKE_CURRENT_SOURCE_DIR}/src/__Revision__.cc
|
||||
)
|
||||
|
||||
|
||||
|
||||
# Executable definition
|
||||
|
||||
set(SOURCES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Revision.cc
|
||||
src/AFSArchive.cc
|
||||
src/BattleParamsIndex.cc
|
||||
src/BMLArchive.cc
|
||||
@@ -66,10 +83,12 @@ set(SOURCES
|
||||
src/Episode3/RulerServer.cc
|
||||
src/Episode3/Server.cc
|
||||
src/Episode3/Tournament.cc
|
||||
src/EventUtils.cc
|
||||
src/FileContentsCache.cc
|
||||
src/FunctionCompiler.cc
|
||||
src/GSLArchive.cc
|
||||
src/GVMEncoder.cc
|
||||
src/HTTPServer.cc
|
||||
src/IPFrameInfo.cc
|
||||
src/IPStackSimulator.cc
|
||||
src/ItemCreator.cc
|
||||
@@ -85,6 +104,7 @@ set(SOURCES
|
||||
src/Map.cc
|
||||
src/Menu.cc
|
||||
src/NetworkAddresses.cc
|
||||
src/PatchServer.cc
|
||||
src/PatchFileIndex.cc
|
||||
src/PlayerFilesManager.cc
|
||||
src/PlayerSubordinates.cc
|
||||
@@ -100,17 +120,16 @@ set(SOURCES
|
||||
src/ReceiveCommands.cc
|
||||
src/ReceiveSubcommands.cc
|
||||
src/ReplaySession.cc
|
||||
src/Revision.cc
|
||||
src/SaveFileFormats.cc
|
||||
src/SendCommands.cc
|
||||
src/Server.cc
|
||||
src/ServerShell.cc
|
||||
src/ServerState.cc
|
||||
src/Shell.cc
|
||||
src/StaticGameData.cc
|
||||
src/TeamIndex.cc
|
||||
src/Text.cc
|
||||
src/TextArchive.cc
|
||||
src/UnicodeTextSet.cc
|
||||
src/TextIndex.cc
|
||||
src/Version.cc
|
||||
src/WordSelectTable.cc
|
||||
)
|
||||
@@ -122,6 +141,10 @@ endif()
|
||||
add_executable(newserv ${SOURCES})
|
||||
target_include_directories(newserv PUBLIC ${LIBEVENT_INCLUDE_DIR} ${Iconv_INCLUDE_DIRS})
|
||||
target_link_libraries(newserv phosg ${LIBEVENT_LIBRARIES} ${Iconv_LIBRARIES} pthread)
|
||||
add_dependencies(newserv newserv-Revision-cc)
|
||||
|
||||
# target_compile_options(newserv PRIVATE -fsanitize=address)
|
||||
# target_link_options(newserv PRIVATE -fsanitize=address)
|
||||
|
||||
if(resource_file_FOUND)
|
||||
target_compile_definitions(newserv PUBLIC HAVE_RESOURCE_FILE)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# newserv <img align="right" src="s-newserv.png" />
|
||||
# newserv <img align="right" src="static/s-newserv.png" />
|
||||
|
||||
newserv is a game server, proxy, and reverse-engineering tool for Phantasy Star Online (PSO).
|
||||
|
||||
@@ -9,83 +9,220 @@ Feel free to submit GitHub issues if you find bugs or have feature requests. I'd
|
||||
See TODO.md for a list of known issues and future work I've curated, or go to the GitHub issue tracker for issues and requests submitted by the community.
|
||||
|
||||
**Table of contents**
|
||||
* Background
|
||||
* [History](#history)
|
||||
* [Other server projects](#other-server-projects)
|
||||
* [Compatibility](#compatibility)
|
||||
* Setup
|
||||
* [Configuration](#configuration)
|
||||
* [Server setup](#server-setup)
|
||||
* [Client patch directories for PC and BB](#client-patch-directories)
|
||||
* [How to connect](#how-to-connect)
|
||||
* Features and configuration
|
||||
* [Installing quests](#installing-quests)
|
||||
* [Item tables and drop modes](#item-tables-and-drop-modes)
|
||||
* [Cross-version play](#cross-version-play)
|
||||
* [Episode 3 features](#episode-3-features)
|
||||
* [Client patch directories for PC and BB](#client-patch-directories)
|
||||
* [Memory patches and DOL files for GC](#memory-patches-and-dol-files)
|
||||
* [Memory patches, client functions, and DOL files](#memory-patches-client-functions-and-dol-files)
|
||||
* [Using newserv as a proxy](#using-newserv-as-a-proxy)
|
||||
* [Chat commands](#chat-commands)
|
||||
* How to connect
|
||||
* Connecting local clients
|
||||
* [PSO DC](#pso-dc)
|
||||
* [PSO DC on Flycast](#pso-dc-on-flycast)
|
||||
* [PSO PC](#pso-pc)
|
||||
* [PSO GC on a real GameCube](#pso-gc-on-a-real-gamecube)
|
||||
* [PSO GC on Dolphin](#pso-gc-on-dolphin)
|
||||
* [PSO BB](#pso-bb)
|
||||
* [Connecting external clients](#connecting-external-clients)
|
||||
* [Non-server features](#non-server-features)
|
||||
|
||||
## Compatibility
|
||||
# History
|
||||
|
||||
newserv supports several versions of PSO, including various development prototypes. Specifically:
|
||||
| Version | Lobbies | Games | Proxy |
|
||||
|----------------|--------------|--------------|--------------|
|
||||
| DC NTE | Yes | Yes | No |
|
||||
| DC 11/2000 | Yes | Yes | No |
|
||||
| DC 12/2000 | Yes | Yes | Yes |
|
||||
| DC 01/2001 | Yes | Yes | Yes |
|
||||
| DC V1 | Yes | Yes | Yes |
|
||||
| DC 08/2001 | Yes | Yes | Yes |
|
||||
| DC V2 | Yes | Yes | Yes |
|
||||
| PC NTE | Yes (3) | Yes | No |
|
||||
| PC | Yes | Yes | Yes |
|
||||
| GC Ep1&2 NTE | Yes | Yes | Yes |
|
||||
| GC Ep1&2 | Yes | Yes | Yes |
|
||||
| GC Ep1&2 Plus | Yes | Yes | Yes |
|
||||
| GC Ep3 NTE | Yes | Partial (1) | Yes |
|
||||
| GC Ep3 | Yes | Yes | Yes |
|
||||
| Xbox Ep1&2 | Yes | Yes | Yes |
|
||||
| BB (vanilla) | Yes | Yes (2) | Yes |
|
||||
| BB (Tethealla) | Yes | Yes (2) | Yes |
|
||||
The history of this project essentially mirrors my development as a software engineer from the beginning of my hobby until now. If you don't care about the story, skip to the "Compatibility" or "Setup" sections below.
|
||||
|
||||
I originally purchased PSO GC when I heard about PSUL, and wanted to play around with running homebrew on my GameCube. This pathway eventually led to [GCARS-CS](https://github.com/fuzziqersoftware/gcars-cs), but that's another story.
|
||||
|
||||
<img align="left" src="static/s-khyps.png" /> After playing PSO for a while, both offline and online, I wrote a proxy called Khyps sometime in 2003. This was back in the days of the official Sega servers, where vulnerabilities weren't addressed in a timely manner or at all. It was common for malicious players using their own proxies or Action Replay codes (a story for another time) to send invalid commands that the servers would blindly forward, and cause the receiving clients to crash. These crashes were more than simply inconvenient; they could also corrupt your save data, destroying the hours of work you may have put into hunting items and leveling up your character.
|
||||
|
||||
For a while it was essentially necessary to use a proxy to go online at all, so the proxy could block these invalid commands. Khyps was designed primarily with this function in mind, though it also implemented some convenient cheats, like the ability to give yourself or other players infinite HP and allow you to teleport to different places without using an in-game teleporter.
|
||||
|
||||
<img align="left" src="static/s-khyller.png" /> After Khyps I took on the larger challenge of writing a server, which resulted in Khyller sometime in 2005. This was the first server of any type I had ever written. This project eventually evolved into a full-featured environment supporting all versions of the game that I had access to - at the time, PC, GC, and BB. (However, I suspect from reading the ancient source files that Khyller's BB support was very buggy.) As Khyller evolved, the code became increasingly cumbersome, littered with debugging filth that I never cleaned up and odd coding patterns I had picked up over the years. My understanding of the C++ language was woefully incomplete as well (as opposed to now, when it is still incomplete but not woefully so), which resulted in Khyller being essentially a C project that had a couple of classes in it.
|
||||
|
||||
<img align="left" src="static/s-aeon.png" /> Sometime in 2006 or 2007, I abandoned Khyller and rebuilt the entire thing from scratch, resulting in Aeon. Aeon was substantially cleaner in code than Khyller but still fairly hard to work with, and it lacked a few of the more arcane features I had originally written (for example, the ability to convert any quest into a download quest). In addition, the code still had some stability problems... it turns out that Aeon's concurrency primitives were simply incorrect. I had derived the concept of a mutex myself, before taking any real computer engineering classes, but had implemented it incorrectly. I made the race window as small as possible, but Aeon would still randomly crash after running seemingly fine for a few days.
|
||||
|
||||
At the time of its inception, Aeon was also called newserv, and you may find some beta releases floating around the Internet with filenames like `newserv-b3.zip`. I had released betas 1, 2, and 3 before I released the entire source of beta 5 and stopped working on the project when I went to college. This was around the time when I switched from writing software primarily on Windows to primarily on macOS and Linux, so Aeon beta 5 was the last server I wrote that specifically targeted Windows. (newserv, which you're looking at now, is a bit tedious to compile on Windows but does work.)
|
||||
|
||||
<img align="left" src="static/s-newserv.png" /> After a long hiatus from PSO and much professional and personal development in my technical abilities, I was reminiscing sometime in October 2018 by reading my old code archives. Somehow inspired when I came across Aeon, I spent a weekend and a couple more evenings rewriting the entire project again, cleaning up ancient patterns I had used eleven years ago, replacing entire modules with simple STL containers, and eliminating even more support files in favor of configuration autodetection. The code is now suitably modern and stable, and I'm not embarrassed by its existence, as I am by Aeon beta 5's source code and my archive of Khyller (which, thankfully, no one else ever saw).
|
||||
|
||||
## Other server projects
|
||||
|
||||
Independently of this project, there are many other PSO servers out there. Those that I know of that are (or were) public are listed here in approximate chronological order:
|
||||
|
||||
* (Early 2000s) **[Schtserv](https://schtserv.com/)**: The first public-access PSO server; written in Delphi by Schthack. Still active and popular as of this writing (early 2024). Schtserv is also the only other unofficial server to support all versions of PSO, including Episode 3.
|
||||
* (2005) **Khyller**: An early attempt of mine to support PSO PC, GC, and BB. See above for more details.
|
||||
* (2006) **Aeon**: My second attempt. Better than Khyller, but still unreliable.
|
||||
* (2008) **Tethealla**: A fairly extensive implementation of PSOBB, written in C by Sodaboy. The public version of Tethealla has been [officially disowned](https://www.pioneer2.net/community/threads/tethealla-server-forums-removal.26365/) (as it is now more than 15 years old), but closed-source development continues. [Ephinea](https://ephinea.pioneer2.net/), currently the most popular PSOBB server, is the continuation of this project. Several other modern PSOBB servers are forks of the initial public version of Tethealla as well.
|
||||
* (2008) **[Sylverant](https://sylverant.net/)** [(source)](https://sourceforge.net/projects/sylverant/): The second public-access PSO server; written in C by BlueCrab. Still active and popular as of this writing (early 2024).
|
||||
* (2015) **[Archon](https://github.com/dcrodman/archon)**: A PSOBB server written in Go by Drew Rodman.
|
||||
* (2015) **[Idola](https://github.com/HybridEidolon/idolapsoserv)**: A PSOBB server written in Rust by HybridEidolon. Functionality status unknown; the project has been archived.
|
||||
* (2017) **[Aselia](https://github.com/Solybum/Aselia)**: A PSOBB server written written in C# by Soly. It seems this was planned to be open-source at some point, but that has not (yet) happened.
|
||||
* (2018) **newserv**: This project right here.
|
||||
* (2019) **[Mechonis](https://gitlab.com/sora3087/mechonis)**: A PSOBB server with a microservice architecture written in TypeScript by TrueVision.
|
||||
* (2021) **[Phantasmal World](https://github.com/DaanVandenBosch/phantasmal-world)**: A set of PSO tools, including a web-based model viewer and quest builder, and a PSO server, written by Daan Vanden Bosch.
|
||||
* (2021) **[Elseware](http://git.sharnoth.com/jake/elseware)**: A PSOBB server written in Rust by Jake.
|
||||
* (2022) **[PSOSERVER](https://github.com/Sancaros/PSOSERVER)**: A server for all versions, written in C by Sancaros and based on Sylverant and newserv.
|
||||
|
||||
# Compatibility
|
||||
|
||||
newserv supports all known versions of PSO, including development prototypes. Specifically:
|
||||
| Version | Lobbies | Games | Proxy |
|
||||
|-----------------|----------|----------|----------|
|
||||
| DC NTE | Yes | Yes | No |
|
||||
| DC 11/2000 | Yes | Yes | No |
|
||||
| DC 12/2000 | Yes | Yes | Yes |
|
||||
| DC 01/2001 | Yes | Yes | Yes |
|
||||
| DC V1 | Yes | Yes | Yes |
|
||||
| DC 08/2001 | Yes | Yes | Yes |
|
||||
| DC V2 | Yes | Yes | Yes |
|
||||
| PC NTE | Yes (3) | Yes | No |
|
||||
| PC | Yes | Yes | Yes |
|
||||
| GC Ep1&2 NTE | Yes | Yes | Yes |
|
||||
| GC Ep1&2 | Yes | Yes | Yes |
|
||||
| GC Ep1&2 Plus | Yes | Yes | Yes |
|
||||
| GC Ep3 NTE | Yes | Yes (1) | Yes |
|
||||
| GC Ep3 | Yes | Yes | Yes |
|
||||
| Xbox Ep1&2 Beta | Yes | Yes | Yes |
|
||||
| Xbox Ep1&2 | Yes | Yes | Yes |
|
||||
| BB (vanilla) | Yes | Yes (2) | Yes |
|
||||
| BB (Tethealla) | Yes | Yes (2) | Yes |
|
||||
|
||||
*Notes:*
|
||||
1. *Players can create games, edit decks, trade cards, and participate in auctions, but CARD battles don't work on Episode 3 Trial Edition on newserv.*
|
||||
1. *Ep3 NTE battles are not well-tested; some things may not work. See notes/ep3-nte-differences.txt for a list of known differences between NTE and the final version. NTE and non-NTE players cannot battle each other.*
|
||||
2. *Some BB-specific features are not well-tested (for example, some quests that use rare commands may not work properly). Please submit a GitHub issue if you find something that doesn't work.*
|
||||
3. *This is the only version of PSO that doesn't have any way to identify the player's account - there is no serial number or username. For this reason, AllowUnregisteredUsers must be enabled in config.json to support PC NTE, and PC NTE players receive a random Guild Card number every time they connect. To prevent abuse, PC NTE support can be disabled in config.json.*
|
||||
|
||||
## Setup
|
||||
# Setup
|
||||
|
||||
### Configuration
|
||||
## Server setup
|
||||
|
||||
Currently newserv works on macOS, Windows, and Ubuntu Linux. It will likely work on other Linux flavors too.
|
||||
|
||||
There is a fairly recent macOS ARM64 release on the newserv GitHub repository. You may need to install libevent manually even if you use this release (run `brew install libevent`).
|
||||
### Windows/macOS
|
||||
|
||||
There is a fairly recent Windows release on the newserv GitHub repository also. It's built with Cygwin, and all the necessary DLL files should be included. That said, I've only tested it on my own machine and there is no CI for Windows builds like there is for macOS and Linux, so if it doesn't work for you, please open a GitHub issue to let me know.
|
||||
1. Download the latest release-windows-amd64.zip or release-macos-arm64.zip file from the [releases page](https://github.com/fuzziqersoftware/newserv/releases).
|
||||
2. Extract the contents of the release folder to a location on your computer.
|
||||
3. Edit the config.example.json file in the system folder as needed, then rename it to config.json.
|
||||
4. If you plan to play Blue Burst on newserv, set up the patch directory. See [client patch directories](#client-patch-directories) for more information.
|
||||
5. Run the newserv executable.
|
||||
|
||||
If you're not using a release from the GitHub repository, do this to build newserv:
|
||||
1. If you're on Windows, install Cygwin. While doing so, install the `cmake`, `gcc-core`, `gcc-g++`, `git`, `libevent2.1_7`, `make`, `libiconv-devel`, and `zlib` packages. Do the rest of these steps inside a Cygwin shell (not a Windows cmd shell or PowerShell).
|
||||
2. Make sure you have CMake, libevent, and libiconv installed. (On macOS, `brew install cmake libevent libiconv`; on most Linuxes, `sudo apt-get install cmake libevent-dev`; on Windows, you already did this in step 1.)
|
||||
3. Build and install phosg (https://github.com/fuzziqersoftware/phosg).
|
||||
4. Optionally, install resource_dasm (https://github.com/fuzziqersoftware/resource_dasm). This will enable newserv to send memory patches and load DOL files on PSO GC clients. PSO GC clients can play PSO normally on newserv without this.
|
||||
### Linux
|
||||
|
||||
There are currently no precompiled releases for Linux. To run newserv on Linux, see the "Building from source" section below.
|
||||
|
||||
### Building from source
|
||||
|
||||
1. Install the packages newserv depends on.
|
||||
* If you're on Windows, install [Cygwin](https://www.cygwin.com/). While doing so, install the `cmake`, `gcc-core`, `gcc-g++`, `git`, `libevent2.1_7`, `make`, `libiconv-devel`, and `zlib` packages. Do the rest of these steps inside a Cygwin shell (not a Windows cmd shell or PowerShell).
|
||||
* If you're on macOS, run `brew install cmake libevent libiconv`.
|
||||
* If you're on Linux, run `sudo apt-get install cmake libevent-dev` (or use your Linux distribution's package manager).
|
||||
3. Build and install [phosg](https://github.com/fuzziqersoftware/phosg).
|
||||
4. Optionally, install [resource_dasm](https://github.com/fuzziqersoftware/resource_dasm). This will enable newserv to send memory patches and load DOL files on PSO GC clients. PSO GC clients can play PSO normally on newserv without this.
|
||||
5. Run `cmake . && make` in the newserv directory.
|
||||
|
||||
After building newserv or downloading a release, do this to set it up and use it:
|
||||
1. In the system/ directory, make a copy of config.example.json named config.json, and edit it appropriately.
|
||||
2. If you plan to play PSO Blue Burst on newserv, set up the patch directory. See the "Client patch directories" section below.
|
||||
3. Run `./newserv` in the newserv directory. This will start the game server and run the interactive shell. You may need `sudo` if newserv's built-in DNS server is enabled.
|
||||
4. If you set AllowUnregisteredUsers to false in config.json, use the interactive shell to add your license. Run `help` in the shell to see how to do this.
|
||||
5. Set your client's network settings appropriately and start an online game. See the "Connecting local clients" or "Connecting remote clients" section to see how to get your game client to connect.
|
||||
After building newserv, edit system/config.example.json as needed and rename it to system/config.json, set up [client patch directories](#client-patch-directories) if you're planning to play Blue Burst, then run `./newserv` in newserv's directory.
|
||||
|
||||
To use newserv in other ways (e.g. for translating data), see the end of this document.
|
||||
|
||||
### Installing quests
|
||||
## Client patch directories
|
||||
|
||||
newserv implements a patch server for PSO PC and PSO BB game data. Any file or directory you put in the system/patch-bb or system/patch-pc directories will be synced to clients when they connect to the patch server.
|
||||
|
||||
For Blue Burst set up, the below is mandatory for a smooth experience:
|
||||
|
||||
1. Browse to your chosen client's data directory.
|
||||
2. Copy all the map_*.dat files and the data.gsl file and place them in `system/patch-bb/data`
|
||||
|
||||
For BB clients, newserv reads some files out of the patch data to implement game logic, so it's important that certain game files are synchronized between the server and the client. newserv contains defaults for these files in the system/maps/bb-v4 directory, but if these don't match the client's copies of the files, odd behavior will occur in games.
|
||||
|
||||
To make server startup faster, newserv caches the modification times, sizes, and checksums of the files in the patch directories. If the patch server appears to be misbehaving, try deleting the .metadata-cache.json file in the relevant patch directory to force newserv to recompute all the checksums. Also, in the case when checksums are cached, newserv may not actually load the data for a patch file until it's needed by a client. Therefore, modifying any part of the patch tree while newserv is running can cause clients to see an inconsistent view of it.
|
||||
|
||||
Patch directory contents are cached in memory. If you've changed any of these files, you can run `reload patch-indexes` in the interactive shell to make the changes take effect without restarting the server.
|
||||
|
||||
## How to connect
|
||||
|
||||
### PSO DC
|
||||
|
||||
Depending on the version of PSO DC that you have, the instructions to connect to a newserv instance will vary.
|
||||
|
||||
If you have NTE, USv1, EUv1, or EUv2 and a Broadband Adapter, edit the broadband DNS address to newserv's IP address with newserv's DNS server running. Otherwise, it is necessary to patch the disc or use a codebreaker code to remove the Hunter License server check and/or redirect PSO to the newserv instance. Patching the disc or creating a codebreaker code is beyond the scope of this document.
|
||||
|
||||
### PSO DC on Flycast
|
||||
|
||||
If you're emulating PSO DC, the NTE, USv1, EUv1, and EUv2 versions will connect to newserv by setting the following options in Flycast's `emu.cfg` file under `[network]`:
|
||||
- DNS = Your newserv's server address (newserv's DNS server must be running on port 53)
|
||||
- EmulateBBA = yes
|
||||
- Enable = yes
|
||||
|
||||
It is also necessary to save any DNS information to the flash memory of the Dreamcast to use the BBA - the easiest way to do this is to use the website option in USv2 and then choose the save to flash option.
|
||||
|
||||
If the server is running on the same machine as Flycast, this might not work, even if you point Flycast's DNS queries at your local IP address (instead of 127.0.0.1). In this case, you can modify the loaded executable in memory to make it connect anywhere you want. There is a script included with newserv that can do this on macOS; a similar technique could be done manually using scanmem on Linux or Cheat Engine on Windows. To use the script, do this:
|
||||
1. Build and install [memwatch](https://github.com/fuzziqersoftware/memwatch).
|
||||
2. Start Flycast and run PSO. (You must start PSO before running the script; it won't work if you run the script before loading the game.)
|
||||
3. Run `sudo patch_flycast_memory.py <original-destination>`. Replace `<original-destination>` with the hostname that PSO wants to connect to (you can find this out by using Wireshark and looking for DNS queries). The script may take up to a minute; you can continue using Flycast while it runs, but don't start an online game until the script is done.
|
||||
4. Run newserv and start an online game in PSO.
|
||||
|
||||
If you use this method, you'll have to run the script every time you start PSO in Flycast, but you won't have to run it again if you start another online game without restarting emulation.
|
||||
|
||||
If using JPv1, JPv2, or USv2, it is also necessary to remove the Hunter Licence server check, either with a disc patch or codebreaker code. Patching the disc or creating a codebreaker code is beyond the scope of this document.
|
||||
|
||||
### PSO PC
|
||||
|
||||
PSO PC has its connection addresses in `pso.exe`. Hex edit the executable with the connection address you want to connect to. Common server addresses to search for to replace are:
|
||||
- pso20.sonic.isao.net
|
||||
- sg207634.sonicteam.com
|
||||
- pso-mp01.sonic.isao.net
|
||||
- gsproduc.ath.cx
|
||||
- sylverant.net
|
||||
|
||||
The version of PSO PC I have has the server addresses starting at offset 0x29CB34 in pso.exe. Change those addresses to "localhost" (without quotes) if you just want to connect to a locally-running newserv instance. Alternatively, you can add an entry to the Windows hosts file (C:\Windows\System32\drivers\etc\hosts) to redirect the connection to 127.0.0.1 (localhost) or any other IP address.
|
||||
|
||||
### PSO GC on a real GameCube
|
||||
|
||||
You can make PSO connect to newserv by setting its default gateway and DNS server addresses in network settings to newserv's address. newserv's DNS server must be running on port 53 and must be accessible to the GameCube.
|
||||
|
||||
If you have PSO Plus or Episode III, it won't want to connect to a server on the same local network as the GameCube itself, as determined by the GameCube's IP address and subnet mask. In the old days, one way to get around this was to create a fake network adapter on the server (or use an existing real one) that has an IP address on a different subnet, tell the GameCube that the server is the default gateway (as above), and have the server reply to the DNS request with its non-local IP address. To do this with newserv, just set LocalAddress in the config file to a different interface. For example, if the GameCube is on the 192.168.0.x network and your other adapter has address 10.0.1.6, set newserv's LocalAddress to 10.0.1.6 and set PSO's DNS server and default gateway addresses to the server's 192.168.0.x address. This may not work on modern systems or on non-Windows machines - I haven't tested it in many years.
|
||||
|
||||
### PSO GC on a Wii or Wii U
|
||||
|
||||
Using a Wii or Wii U to connect to newserv requires the Wii or vWii to be softmodded. How to do this is beyond the scope of this document.
|
||||
|
||||
Nintendont includes BBA emulation and is compatible with all PSO GameCube versions except Episodes I&II Trial Edition. To use Nintendont, enable BBA emulation in Nintendont's settings and follow the instructions in the above section (PSO GC on a real GameCube).
|
||||
|
||||
Devolution includes modem emulation and is compatible with all PSO GameCube versions including Episodes I&II Trial Edition. newserv can act as a PPP server, which Devolution can directly cnnect to. To do this:
|
||||
1. Enable the PPPRawListen option according to the comments in config.json.
|
||||
2. Start newserv.
|
||||
3. In the game's network settings, set the username and password to anything (they cannot be blank), and set the phone number to the number that newserv outputs to the console during startup. (It will be near the end of all the startup log messages.) If your Wii is on the same network as newserv, use the local number; otherwise, use the external number.
|
||||
|
||||
### PSO GC on Dolphin
|
||||
|
||||
If you're using the HLE BBA type, set the BBA's DNS server address to newserv's IP address and it should work. (If newserv is on the same machine as Dolphin, you will need to use an action replay code directed at 127.0.0.1 to connect, as PSO rejects DNS queries from the same IP address.) Set PSO's network settings the same as listed below.
|
||||
|
||||
If you're using the TAP BBA type, you'll have to set PSO's network settings appropriately for your tap interface. Set the DNS server address in PSO's network settings to newserv's IP address.
|
||||
|
||||
If you're using a version of Dolphin with tapserver support, you can make it connect to a newserv instance running on the same machine via the tapserver interface. You do not need to install or run tapserver. To do this:
|
||||
1. Set Dolphin's BBA type to tapserver (Config -> GameCube -> SP1).
|
||||
2. Enable newserv's IP stack simulator according to the comments in config.json and start newserv.
|
||||
3. In PSO's network settings, enable DHCP ("Automatically obtain an IP address"), set DNS server address to "Automatic", and leave DHCP Hostname as "Not set". Leave the proxy server settings blank.
|
||||
4. Start an online game.
|
||||
|
||||
### PSO BB
|
||||
|
||||
The PSO BB client has been modified and distributed in many different forms. newserv supports most, but not all, of the common distributions. Unlike other versions, it's important that the client and server have the same map files, so make sure to set up the patch directory based on the client you'll be using with newserv. (See the "Client patch directories" section for instructions on setting this up.)
|
||||
|
||||
The original Japanese and US versions of PSO BB should work, but you'll have to modify your hosts file or edit psobb.exe to point to your newserv instance. The original versions are packed, so this is a more involved process than simply opening the executable in a hex editor and finding/replacing some strings.
|
||||
|
||||
Alternatively, you can use the Tethealla client (https://archive.org/details/psobb-tethealla-client); you can find the connection addresses starting at 0x56D724 in psobb.exe. Overwrite these addresses with your server's hostname or IP address, and you should be able to connect.
|
||||
|
||||
### Connecting external clients
|
||||
|
||||
If you want to accept connections from outside your local network, you'll need to set ExternalAddress to your public IP address in the configuration file, and you'll likely need to open some ports in your router's NAT configuration - specifically, all the TCP ports listed in PortConfiguration in config.json.
|
||||
|
||||
For GC clients, you'll have to use newserv's built-in DNS server or set up your own DNS server as well. If you want external clients to be able to use your DNS server, you'll have to forward UDP port 53 to your newserv instance. Remote players can then connect to your server by entering your DNS server's IP address in their client's network configuration.
|
||||
|
||||
# Server feature configuration
|
||||
|
||||
## Installing quests
|
||||
|
||||
newserv automatically finds quests in the subdirectories of the system/quests/ directory. To install your own quests, or to use quests you've saved using the proxy's Save Files option, just put them in one of the subdirectories there and name them appropriately. The subdirectories and their behaviors (e.g. in which game modes they should appear and for which PSO versions) is defined in the QuestCategories field in config.json.
|
||||
|
||||
@@ -137,9 +274,9 @@ Episode 3 download quests consist only of a .bin file - there is no correspondin
|
||||
|
||||
When newserv indexes the quests during startup, it will warn (but not fail) if any quests are corrupt or in unrecognized formats.
|
||||
|
||||
Quest contents are cached in memory, but if you've changed the contents of the quests directory, you can re-index the quests without restarting the server by running `reload quests` in the interactive shell. The new quests will be available immediately, but any games with quests already in progress will continue using the old versions of the quests until those quests end.
|
||||
Quest contents are cached in memory, but if you've changed the contents of the quests directory, you can re-index the quests without restarting the server by running `reload quest-index` in the interactive shell. The new quests will be available immediately, but any games with quests already in progress will continue using the old versions of the quests until those quests end.
|
||||
|
||||
### Item tables and drop modes
|
||||
## Item tables and drop modes
|
||||
|
||||
newserv supports server-side item generation on all game versions, except for the earliest DC prototypes (NTE and 11/2000). By default, the game behaves as it did on the original servers - on all versions except BB, item drops are controlled by the leader client in each game, and on BB, item drops are controlled by the server.
|
||||
|
||||
@@ -152,11 +289,11 @@ There are five different available behaviors for item drops:
|
||||
|
||||
In the `SERVER_PRIVATE` and `SERVER_DUPLICATE` modes, there is no incentive to pick up items before another player, since other players cannot pick up the items you see dropped from boxes and enemies. However, if you pick up an item and drop it later, it can then be seen and picked up by any player.
|
||||
|
||||
The drop mode can be changed at any time during a game with the `$dropmode` chat command. If the mode is changed after some items have already been dropped, the existing items retain their visibility (that is, they still can't be picked up by other players since they were dropped before the mode was changed). You can configure which drop modes are used by default, and which modes players are allowed to choose, in config.json. See the comments above the AllowedDropModes and DefaultDropMode keys.
|
||||
The drop mode can be changed at any time during a game with the `$dropmode` chat command. If the mode is changed after some items have already been dropped, the existing items retain their visibility (that is, items dropped in private mode still can't be picked up by other players since they were dropped before the mode was changed). You can configure which drop modes are used by default, and which modes players are allowed to choose, in config.json. See the comments above the AllowedDropModes and DefaultDropMode keys.
|
||||
|
||||
In the server drop modes, the item tables used to generate common items are in the `system/item-tables/ItemPT-*` files. (The V2 files are used for V1 as well.) The rare item tables are in the `rare-table-*.json` files. Unlike the original formats, it's possible to make each enemy drop multiple different rare items at different rates, though the default tables never do this.
|
||||
|
||||
### Cross-version play
|
||||
## Cross-version play
|
||||
|
||||
All versions of PSO can see and interact with each other in the lobby. newserv also allows some versions to play in-game with each other:
|
||||
* DC V1 players can join DC V2 games if the difficulty level isn't set to Ultimate and the creator chose to allow V1 players.
|
||||
@@ -166,7 +303,7 @@ All versions of PSO can see and interact with each other in the lobby. newserv a
|
||||
|
||||
In V1/V2 cross-version play, when any of the server drop modes are used, the server uses the drop table corresponding to the version the game was created with. (For example, if a DC V1 player created the game, rare-table-v1.json will be used, even after V2 players join.)
|
||||
|
||||
### Episode 3 features
|
||||
## Episode 3 features
|
||||
|
||||
newserv supports many features unique to Episode 3:
|
||||
* CARD battles. Not every combination of abilities has been tested yet, so if you find a feature or card ability that doesn't work like it's supposed to, please make a GitHub issue and describe the situation (the attacking card(s), defending card(s), and ability or condition that didn't work).
|
||||
@@ -177,11 +314,11 @@ newserv supports many features unique to Episode 3:
|
||||
* Participating in card auctions. (The auction contents must be configured in config.json.)
|
||||
* Decorations in lobbies. Currently only images are supported; the game also supports loading custom 3D models in lobbies, but newserv does not implement this (yet).
|
||||
|
||||
#### Battle records
|
||||
### Battle records
|
||||
|
||||
After playing a battle, you can save the record of the battle with the `$saverec` command. You can then replay the battle later by using the `$playrec` command in a lobby - this will create a spectator team and play the recording of the battle as if it were happening in realtime. Note that there is a bug in older versions of Dolphin that seems to be frequently triggered when playing battle records, which causes the emulator to crash with the message `QObject::~QObject: Timers cannot be stopped from another thread`. To avoid this, use the latest version of Dolphin.
|
||||
|
||||
#### Tournaments
|
||||
### Tournaments
|
||||
|
||||
Tournaments work differently than they did on Sega's servers. Tournaments can be created with the `create-tournament` shell command, which enables players to register for them. (Use `help` to see all the arguments - there are many!) The `start-tournament` shell command starts the tournament (and prevents further registrations), but this doesn't schedule any matches. Instead, players who are ready to play their next match can all stand at the 4-player battle table near the lobby warp in the same CARD lobby, and the tournament match will start automatically.
|
||||
|
||||
@@ -189,7 +326,7 @@ These tournament semantics mean that there can be multiple matches in the same t
|
||||
|
||||
The Meseta rewards for winning tournament matches can be configured in config.json.
|
||||
|
||||
#### Episode 3 files
|
||||
### Episode 3 files
|
||||
|
||||
Episode 3 state and game data is stored in the system/ep3 directory. The files in there are:
|
||||
* card-definitions.mnr: Compressed card definition list, sent to Episode 3 clients at connect time. Card stats and abilities can be changed by editing this file.
|
||||
@@ -203,44 +340,35 @@ Episode 3 state and game data is stored in the system/ep3 directory. The files i
|
||||
|
||||
There is no public editor for Episode 3 maps and quests, but the format is described fairly thoroughly in src/Episode3/DataIndexes.hh (see the MapDefinition structure). You'll need to use `newserv decompress-prs ...` to decompress .bin or .mnm files before editing them, but you don't need to compress the files again to use them - just put the .bind or .mnmd file in the maps directory and newserv will make it available.
|
||||
|
||||
Like quests, Episode 3 card definitions, maps, and quests are cached in memory. If you've changed any of these files, you can run `reload ep3` in the interactive shell to make the changes take effect without restarting the server.
|
||||
Like quests, Episode 3 card definitions, maps, and quests are cached in memory. If you've changed any of these files, you can run `reload ep3-data` in the interactive shell to make the changes take effect without restarting the server.
|
||||
|
||||
### Client patch directories
|
||||
## Memory patches, client functions, and DOL files
|
||||
|
||||
If you're not playing PSO Blue Burst on newserv, you can skip these steps.
|
||||
|
||||
newserv implements a patch server for PSO PC and PSO BB game data. Any file or directory you put in the system/patch-bb or system/patch-pc directories will be synced to clients when they connect to the patch server.
|
||||
|
||||
To make server startup faster, newserv caches the modification times, sizes, and checksums of the files in the patch directories. If the patch server appears to be misbehaving, try deleting the .metadata-cache.json file in the relevant patch directory to force newserv to recompute all the checksums. Also, in the case when checksums are cached, newserv may not actually load the data for a patch file until it's needed by a client. Therefore, modifying any part of the patch tree while newserv is running can cause clients to see an inconsistent view of it.
|
||||
|
||||
For BB clients, newserv reads some files out of the patch data to implement game logic, so it's important that certain game files are synchronized between the server and the client. newserv contains defaults for these files in the system/blueburst/map directory, but if these don't match the client's copies of the files, odd behavior will occur in games.
|
||||
|
||||
Specifically, the patch-bb directory should contain at least the data.gsl file and all map_*.dat files from the version of PSOBB that you want to play on newserv. You can copy these files out of the client's data directory from a clean installation, and put them in system/patch-bb/data.
|
||||
|
||||
Patch directory contents are cached in memory. If you've changed any of these files, you can run `reload patches` in the interactive shell to make the changes take effect without restarting the server.
|
||||
|
||||
### Memory patches and DOL files
|
||||
|
||||
Everything in this section requires resource_dasm to be installed, so newserv can use the PowerPC assembler and disassembler from its libresource_file library. If resource_dasm is not installed, newserv will still build and run, but these features will not be available.
|
||||
Everything in this section requires resource_dasm to be installed, so newserv can use the assemblers and disassemblers from its libresource_file library. If resource_dasm is not installed, newserv will still build and run, but these features will not be available.
|
||||
|
||||
In addition, these features are only supported for the following game versions:
|
||||
* PSO GameCube Episodes 1&2 JP, USA, and EU (not Plus)
|
||||
* PSO GameCube Episodes 1&2 Plus JP v1.04 (not v1.05)
|
||||
* PSO GameCube Episodes 1&2 Trial Edition
|
||||
* PSO GameCube Episodes 1&2 JP, USA, and EU but not Plus
|
||||
* PSO GameCube Episodes 1&2 Plus JP v1.4 but not v1.5
|
||||
* PSO GameCube Episode 3 Trial Edition
|
||||
* PSO GameCube Episode 3 JP
|
||||
* PSO GameCube Episode 3 USA (experimental; must be manually enabled in config.json)
|
||||
* PSO Xbox (all versions)
|
||||
* PSO BB
|
||||
|
||||
You can put memory patches in the system/ppc directory with filenames like PatchName.patch.s and they will appear in the Patches menu for PSO GC clients that support patching. Memory patches are written in PowerPC assembly and are compiled when newserv is started. The PowerPC assembly system's features are documented in the comments in system/ppc/WriteMemory.s - this file is not a memory patch itself, but it describes how memory patches may be written and the restrictions that apply to them.
|
||||
*Note: newserv uses the shorter GameCube versioning convention, where discs labeled DOL-XXXX-0-0Y are version 1.Y. The PSO community seems to use the convention 1.0Y in some places instead, but these are the same version. For example, the version that newserv calls v1.4 is the same as v1.04, and is labeled DOL-GPOJ-0-04 on the underside of the disc.*
|
||||
|
||||
newserv comes with a set of patches for Episodes 1&2 based on AR codes originally made by Ralf at GC-Forever. Many of them were originally posted in [this thread](https://www.gc-forever.com/forums/viewtopic.php?f=38&t=2050).
|
||||
You can put memory patches in the system/client-functions directory with filenames like PatchName.patch.s and they will appear in the Patches menu for PSO GC, XB, and BB clients that support patching. Memory patches are written in PowerPC or x86 assembly and are compiled when newserv is started. The assembly system's features are documented in the comments in system/client-functions/WriteMemory.ppc.s.
|
||||
|
||||
You can also put DOL files in the system/dol directory, and they will appear in the Programs menu. Selecting a DOL file there will load the file into the GameCube's memory and run it, just like the old homebrew loaders (PSUL and PSOload) did. For this to work, ReadMemoryWord.s, WriteMemory.s, and RunDOL.s must be present in the system/ppc directory. This has been tested on Dolphin but not on a real GameCube, so results may vary.
|
||||
newserv comes with a set of patches for GC Episodes 1&2 based on AR codes originally made by Ralf at GC-Forever and Aleron Ives. Many of them were originally posted in [this thread](https://www.gc-forever.com/forums/viewtopic.php?f=38&t=2050).
|
||||
|
||||
You can also put DOL files in the system/dol directory, and they will appear in the Programs menu for GC clients. Selecting a DOL file there will load the file into the GameCube's memory and run it, just like the old homebrew loaders (PSUL and PSOload) did. For this to work, ReadMemoryWord.ppc.s, WriteMemory.ppc.s, and RunDOL.ppc.s must be present in the system/client-functions directory. This has been tested on Dolphin but not on a real GameCube, so results may vary.
|
||||
|
||||
Like other kinds of data, functions and DOL files are cached in memory. If you've changed any of these files, you can run `reload functions` or `reload dol-files` in the interactive shell to make the changes take effect without restarting the server.
|
||||
|
||||
I mainly built the DOL loading functionality for documentation purposes. By now, there are many better ways to load homebrew code on an unmodified GameCube, but to my knowledge there isn't another open-source implementation of this method in existence.
|
||||
|
||||
### Using newserv as a proxy
|
||||
## Using newserv as a proxy
|
||||
|
||||
If you want to play online on remote servers rather than running your own server, newserv also includes a PSO proxy. Currently this works with PSO GC and may work with PC and DC; it also works with some BB clients in specific situations.
|
||||
|
||||
@@ -275,7 +403,7 @@ The remote server will probably try to assign you a Guild Card number that doesn
|
||||
|
||||
Some chat commands (see below) have the same basic function on the proxy server but have different effects or conditions. In addition, there are some server shell commands that affect clients on the proxy (run `help` in the shell to see what they are). If there's only one proxy session open, the shell's proxy commands will affect that session. Otherwise, you'll have to specify which session to affect with the `on` prefix - to send a chat message in LinkedSession:17205AE4, for example, you would run `on 17205AE4 chat ...`.
|
||||
|
||||
### Chat commands
|
||||
## Chat commands
|
||||
|
||||
newserv supports a variety of commands players can use by chatting in-game. Any chat message that begins with `$` is treated as a chat command. (If you actually want to send a chat message starting with `$`, type `$$` instead.) On the DC 11/2000 prototype, `@` is used instead of `$` for all chat commands, since `$` does not appear on the English virtual keyboard.
|
||||
|
||||
@@ -283,36 +411,53 @@ Some commands only work on the game server and not on the proxy server. The chat
|
||||
|
||||
* Information commands
|
||||
* `$li`: Shows basic information about the lobby or game you're in. If you're on the proxy server, shows information about your connection instead (remote Guild Card number, client ID, etc.).
|
||||
* `$si` (game server only): Shows basic information about the server.
|
||||
* `$ping`: Shows round-trip ping time from the server to you. On the proxy server, shows the ping time from you to the proxy and from the proxy to the server.
|
||||
* `$matcount` (game server only): Shows how many of each type of material you've used.
|
||||
* `$itemnotifs <mode>`: Enables item drop notification messages. If the game has private drops enabled, you will only see a notification if the dropped item is visible to you; you won't be notified of other players' drops. The modes are:
|
||||
* `off`: No notifications are shown.
|
||||
* `rare`: You are notified when a rare item drops.
|
||||
* `on`: You are notified when any item drops, except Meseta.
|
||||
* `every`: You are notified when any item drops, including Meseta.
|
||||
* `$what` (game server only): Shows the type, name, and stats of the nearest item on the ground.
|
||||
* `$where` (game server only): Shows your current floor number and coordinates. Mainly useful for debugging.
|
||||
|
||||
* Debugging commands
|
||||
* `$debug` (game server only): Enable or disable debug. You need the DEBUG permission in your user license to use this command. When debug is enabled, you'll see in-game messages from the server when you take certain actions. You'll also be placed into the highest available slot in lobbies and games instead of the lowest, which is useful for finding commands for which newserv doesn't handle client IDs properly. This setting also disables certain safeguards and allows you to do some things that might crash your client.
|
||||
* `$quest <number>`: Load a quest by quest number. Can be used to load battle or challenge quests with only one player present.
|
||||
* `$debug` (game server only): Enable or disable debug. You need the DEBUG permission in your user license to use this command. Enabling debug does a few things:
|
||||
* You'll see in-game messages from the server when you take certain actions, like killing an enemy in BB.
|
||||
* You'll see the rare seed value and floor variations when you join a game.
|
||||
* You'll be placed into the highest available slot in lobbies and games instead of the lowest, unless you're joining a BB solo-mode game.
|
||||
* You'll be able to join games with any PSO version, not only those for which crossplay is normally supported. Be prepared for client crashes and other client-side brokenness if you do this. Please do not submit any issues for broken behaviors in crossplay, unless the situation is explicitly supported (see the "Cross-version play" section above).
|
||||
* The rest of the commands in this section are enabled on the game server. (They are always enabled on the proxy server.)
|
||||
* `$quest <number>` (game server only): Load a quest by quest number. Can be used to load battle or challenge quests with only one player present.
|
||||
* `$qcall <function-id>`: Call a quest function on your client.
|
||||
* `$qcheck <flag-num>`: Show the value of a quest flag.
|
||||
* `$qset <flag-num>` or `$qclear <flag-num>`: Set or clear a global quest flag for everyone in the game.
|
||||
* `$qsync <reg-num> <value>`: Set a quest register's value on your client. `<reg-num>` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `<value>` is parsed as a floating-point value instead of as an integer.
|
||||
* `$qcheck <flag-num>` (game server only): Show the value of a quest flag. This command can be used without debug mode enabled. If you're in a game, show the value of the flag in that game; if you're in the lobby, show the saved value of that quest flag for your character (BB only).
|
||||
* `$qset <flag-num>` or `$qclear <flag-num>`: Set or clear a quest flag for everyone in the game. If you're in the lobby and on BB, set or clear the saved value of a quest flag in your character file.
|
||||
* `$qgread <flag-num>` (game server only): Get the value of a quest counter ("global flag"). This command can be used without debug mode enabled.
|
||||
* `$qgwrite <flag-num> <value>` (game server only): Set the value of a quest counter ("global flag") for yourself.
|
||||
* `$qsync <reg-num> <value>`: Set a quest register's value for yourself only. `<reg-num>` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `<value>` is parsed as a floating-point value instead of as an integer.
|
||||
* `$qsyncall <reg-num> <value>`: Set a quest register's value for everyone in the game. `<reg-num>` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `<value>` is parsed as a floating-point value instead of as an integer.
|
||||
* `$gc` (game server only): Send your own Guild Card to yourself.
|
||||
* `$persist` (game server only): Enable or disable persistence for the current game. When persistence is on, the game will not be deleted when the last player leaves. The state of enemies and objects on the map will be reset when the last player leaves.
|
||||
* `$sc <data>`: Send a command to yourself.
|
||||
* `$ss <data>` (proxy server only): Send a command to the remote server.
|
||||
* `$meseta <amount>` (game server only; Episode 3 only): Add the given amount to your Meseta total.
|
||||
* `$auction` (Episode 3 only): Bring up the CARD Auction menu, regardless of how many players are in the game or if you have a VIP card.
|
||||
* `$ep3battledebug` (game server only; Episode 3 only): Enable or disable TCard00_Select. If enabled, the game will enter the debug menu when you start a battle.
|
||||
|
||||
* Personal state commands
|
||||
* `$arrow <color-id>`: Changes your lobby arrow color.
|
||||
* `$secid <section-id>`: Sets your override section ID. After running this command, any games you create will use your override section ID for rare drops instead of your character's actual section ID. To revert to your actual section id, run `$secid` with no name after it. On the proxy server, this will not work if the remote server controls item drops (e.g. on BB, or on Schtserv with server drops enabled). If the server does not allow cheat mode anywhere (that is, "CheatModeBehavior" is "Off" in config.json), this command does nothing.
|
||||
* `$secid <section-id>`: Sets your override section ID. After running this command, any games you create will use your override section ID for rare drops instead of your character's actual section ID. If you're in a game and you are the leader of the game, this also immediately changes the item tables used by the server when creating items. To revert to your actual section id, run `$secid` with no name after it. On the proxy server, this will not work if the remote server controls item drops (e.g. on BB, or on Schtserv with server drops enabled). If the server does not allow cheat mode anywhere (that is, "CheatModeBehavior" is "Off" in config.json), this command does nothing.
|
||||
* `$rand <seed>`: Sets your override random seed (specified as a 32-bit hex value). This will make any games you create use the given seed for rare enemies. This also makes item drops deterministic in Blue Burst games hosted by newserv. On the proxy server, this command can cause desyncs with other players in the same game, since they will not see the overridden random seed. To remove the override, run `$rand` with no arguments. If the server does not allow cheat mode anywhere (that is, "CheatModeBehavior" is "Off" in config.json), this command does nothing.
|
||||
* `$ln [name-or-type]`: Sets the lobby number. Visible only to you. This command exists because some non-lobby maps can be loaded as lobbies with invalid lobby numbers. See the "GC lobby types" and "Ep3 lobby types" entries in the information menu for acceptable values here. Note that non-lobby maps do not have a lobby counter, so there's no way to exit the lobby without using either `$ln` again or `$exit`. On the game server, `$ln` reloads the lobby immediately; on the proxy server, it doesn't take effect until you load another lobby yourself (which means you'll like have to use `$exit` to escape). Run this command with no argument to return to the default lobby.
|
||||
* `$swa`: Enables or disables switch assist. When enabled, the server will attempt to automatically unlock two-player doors in non-quest games if you step on both switches sequentially.
|
||||
* `$exit`: If you're in a lobby, sends you to the main menu (which ends your proxy session, if you're in one). If you're in a game or spectator team, sends you to the lobby (but does not end your proxy session if you're in one). Does nothing if you're in a non-Episode 3 game and no quest is in progress.
|
||||
* `$patch <name>`: Run a patch on your client. `<name>` must exactly match the name of a patch on the server.
|
||||
|
||||
* Character data commands
|
||||
* Character data commands (game server only)
|
||||
* `$savechar <slot>`: Saves your current character data on the server in the specified slot (each serial number has 4 slots, numbered 1-4). These slots are separate from BB character slots; using this command does not affect BB characters.
|
||||
* `$loadchar <slot>` (v1 and v2 only): Loads your character data from the specified slot. The changes will be undone if you join a game - to save your changes, disconnect from the lobby.
|
||||
* `$bbchar <username> <password> <slot>`: Use this command when playing on a non-BB version of PSO. If the username and password are correct, this command converts your current character to BB format and saves it on the server in the given slot (1-4). Any character already in that slot is overwritten. (This command is similar to `$savechar`, except it overwrites a BB character slot, and can transfer characters across accounts.) Note that the character's chat data, quick menu config, and bank contents are not copied, since there is no way for the server to request those types of data.
|
||||
* `$edit <stat> <value>`: Modifies your character data. If you are on V3 (GameCube/Xbox), or if the server does not allow cheat mode anywhere (that is, "CheatModeBehavior" is "Off" in config.json), this command does nothing. If you are on V1 or V2 (DC or PC, not BB), your changes will be undone if you join a game - to save your changes, disconnect from the lobby.
|
||||
* `$edit <stat> <value>`: Modifies your character data. If you are on V3 (GameCube/Xbox), this command does nothing. If you are on V1 or V2 (DC or PC, not BB), your changes will be undone if you join a game - to save your changes, disconnect from the lobby. If cheats are allowed on the server, `<stat>` can be any of `atp`, `mst`, `evp`, `hp`, `dfp`, `ata`, `lck`, `meseta`, `exp`, `level`, `namecolor`, `secid`, `name`, `npc`, or `tech`. If cheats are not allowed, only `namecolor`, `name`, and `npc` can be used.
|
||||
|
||||
* Blue Burst player commands (game server only)
|
||||
* `$bank [number]`: Switches your current bank, so you can access your other character's banks (if `number` is 1-4) or your shared account bank (if `number` is 0). If `number` is not given, switches back to your current character's bank.
|
||||
@@ -323,25 +468,26 @@ Some commands only work on the game server and not on the proxy server. The chat
|
||||
* `$minlevel <level>`: Sets the minimum level for players to join the current game.
|
||||
* `$password <password>`: Sets the game's join password. To unlock the game, run `$password` with nothing after it.
|
||||
* `$dropmode [mode]`: Changes the way item drops behave in the current game. `mode` can be `none`, `client`, `shared`, `private`, or `duplicate`. If `mode` is not given, tells you the current drop mode without changing it. See the "Item tables and drop modes" section for more information.
|
||||
* `$persist`: Enable or disable persistence for the current game. When persistence is on, the game will not be deleted when the last player leaves. The state of enemies on the map will be reset when the last player leaves, but dropped items will not be deleted. If the game is empty for too long (15 minutes by default), it is then deleted.
|
||||
|
||||
* Episode 3 commands (game server only)
|
||||
* `$spec`: Toggles the allow spectators flag for Episode 3 games. If any players are spectating when this flag is disabled, they will be sent back to the lobby.
|
||||
* `$inftime`: Toggles infinite-time mode. Must be used before starting a battle. If infinite-time mode is enabled, the overall and per-phase time limits will be disabled regardless of the values chosen during battle setup. After completing a battle, infinite-time mode is reset to the server's default value (which can be set in Episode3BehaviorFlags in config.json).
|
||||
* `$defrange <min>-<max>`: Sets the DEF dice range for the next battle. If this is used, the dice range set during battle rules setup will apply only to ATK dice; DEF dice will use this range instead. Assist cards and other dice effects will still apply. Dice exchange also still applies if it is enabled.
|
||||
* `$dicerange [d:L-H] [1:L-H] [a1:L-H] [d1:L-H]`: Sets override dice ranges for the next battle. The min and max dice values from the rules setup menu always apply to the ATK dice, but you can specify a different range for the DEF dice with `d:2-4` (for example). The `1:` override applies to the 1-player team in a 2v1 game (so you would set the 2-player team's desired dice range in the rules menu). You can also specify the 1-player team's ATK and DEF ranges separately with the `a1:` and `d1:` overrides. Note that these ranges will only be used if the chosen map or quest does not override them.
|
||||
* `$stat <what>`: Shows a statistic about your player or team in the current battle. `<what>` can be `duration`, `fcs-destroyed`, `cards-destroyed`, `damage-given`, `damage-taken`, `opp-cards-destroyed`, `own-cards-destroyed`, `move-distance`, `cards-set`, `fcs-set`, `attack-actions-set`, `techs-set`, `assists-set`, `defenses-self`, `defenses-ally`, `cards-drawn`, `max-attack-damage`, `max-combo`, `attacks-given`, `attacks-taken`, `sc-damage`, `damage-defended`, or `rank`.
|
||||
* `$surrender`: Causes your team to immediately lose the current battle.
|
||||
* `$saverec <name>`: Saves the recording of the last battle.
|
||||
* `$playrec <name>`: Plays a battle recording. This command creates a spectator team and replays the specified battle log within it. There is a bug in Dolphin that makes use of this command unstable in emulation (see the "Battle records" section above).
|
||||
|
||||
* Cheat mode commands
|
||||
* `$cheat`: Enables or disables cheat mode for the current game. All other cheat mode commands do nothing if cheat mode is disabled. By default, cheat mode is off in new games but can be enabled; there is an option in config.json that allows you to disable cheat mode entirely, or set it to on by default in new games.
|
||||
* `$infhp` / `$inftp`: Enables or disables infinite HP or TP mode. Applies to only you. In infinite HP mode, one-hit KO attacks will still kill you.
|
||||
* `$warpme <floor-id>`: Warps yourself to the given floor.
|
||||
* `$cheat` (game server only): Enables or disables cheat mode for the current game. All other cheat mode commands do nothing if cheat mode is disabled. By default, cheat mode is off in new games but can be enabled; there is an option in config.json that allows you to disable cheat mode entirely, or set it to on by default in new games. Cheat mode is always enabled on the proxy server, unless cheat mode is disabled on the entire server.
|
||||
* `$infhp` / `$inftp`: Enables or disables infinite HP or TP mode. Applies to only you. In infinite HP mode, one-hit KO attacks will still kill you. On V1 and V2, infinite HP also automatically cures status ailments.
|
||||
* `$warpme <floor-id>` (or `$warp <floor-id>`): Warps yourself to the given floor.
|
||||
* `$warpall <floor-id>`: Warps everyone in the game to the given floor. You must be the leader to use this command, unless you're on the proxy server.
|
||||
* `$next`: Warps yourself to the next floor.
|
||||
* `$swa`: Enables or disables switch assist. When enabled, the server will attempt to automatically unlock two-player doors in solo games if you step on both switches sequentially.
|
||||
* `$item <desc>` (or `$i <desc>`): Create an item. `desc` may be a description of the item (e.g. "Hell Saber +5 0/10/25/0/10") or a string of hex data specifying the item code. Item codes are 16 hex bytes; at least 2 bytes must be specified, and all unspecified bytes are zeroes. If you are on the proxy server, you must not be using Blue Burst for this command to work. On the game server, this command works for all versions.
|
||||
* `$unset <index>`: In an Episode 3 battle, removes one of your set cards from the field. `<index>` is the index of the set card as it appears on your screen - 1 is the card next to your SC's icon, 2 is the card to the right of 1, etc. This does not cause a Hunters-side SC to lose HP, as they normally do when their items are destroyed.
|
||||
* `$unset <index>` (game server only): In an Episode 3 battle, removes one of your set cards from the field. `<index>` is the index of the set card as it appears on your screen - 1 is the card next to your SC's icon, 2 is the card to the right of 1, etc. This does not cause a Hunters-side SC to lose HP, as they normally do when their items are destroyed.
|
||||
* `$dropmode [mode]` (proxy server): Changes the way item drops behave in the current game, if you are not on BB. Unlike the game server version of this command, using this on the proxy server requires cheats to be enabled. This works by intercepting the drop requests sent to and from the leader. (So, if you are the leader and not using server drop mode on the remote server, it affects the entire game; otherwise, it affects only items generated by your actions.) `mode` can be `none` (no drops), `default` (normal drops), or `proxy` (use newserv's drop tables instead of the remote server's). If `mode` is not given, tells you the current drop mode without changing it.
|
||||
|
||||
* Configuration commands
|
||||
* `$event <event>`: Sets the current holiday event in the current lobby. Holiday events are documented in the "Using $event" item in the information menu. If you're on the proxy server, this applies to all lobbies and games you join, but only you will see the new event - other players will not.
|
||||
@@ -349,94 +495,54 @@ Some commands only work on the game server and not on the proxy server. The chat
|
||||
* `$song <song-id>` (Episode 3 only): Plays a specific song in the current lobby.
|
||||
|
||||
* Administration commands (game server only)
|
||||
* `$ann <message>`: Sends an announcement message. The message text is sent to all players in all games and lobbies.
|
||||
* `$ann <message>`: Sends an announcement message. The message is sent as temporary on-screen text to all players in all games and lobbies.
|
||||
* `$ann! <message>`: Sends an announcement message. The message is sent as a Simple Mail message to all players in all games and lobbies.
|
||||
* `$ax <message>`: Sends a message to the server's terminal. This cannot be used to run server shell commands; it only prints text to stderr.
|
||||
* `$silence <identifier>`: Silences a player (remove their ability to chat) or unsilences a player. The identifier may be the player's name or Guild Card number.
|
||||
* `$kick <identifier>`: Disconnects a player. The identifier may be the player's name or Guild Card number.
|
||||
* `$ban <identifier>`: Bans a player. The identifier may be the player's name or Guild Card number.
|
||||
|
||||
### Connecting local clients
|
||||
# Non-server features
|
||||
|
||||
#### PSO DC
|
||||
newserv has many CLI options, which can be used to access functionality other than the game and proxy server. Run `newserv help` to see a full list of the options and how to use each one.
|
||||
|
||||
Some versions of PSO DC will connect to a private server if you just set their DNS server address (in the network configuration) to newserv's address, and enable newserv's DNS server. This will not work for other versions; for those, you'll need a cheat code. Creating such a code is beyond the scope of this document.
|
||||
The data formats that newserv can convert to/from are:
|
||||
|
||||
If you're emulating PSO DC or have a disc image, you can patch the appropriate files within the disc image to make it connect to any address you want. Creating such a patch is also beyond the scope of this document.
|
||||
| Format | Encode/compress action | Decode/extract action |
|
||||
|--------------------------------|---------------------------|------------------------------|
|
||||
| PRS compression | `compress-prs` | `decompress-prs` |
|
||||
| PR2/PRC compression | `compress-pr2` | `decompress-pr2` |
|
||||
| BC0 compression | `compress-bc0` | `decompress-bc0` |
|
||||
| Raw encrypted data | `encrypt-data` | `decrypt-data` |
|
||||
| Episode 3 command mask | `encrypt-trivial-data` | `decrypt-trivial-data` |
|
||||
| Challenge Mode rank text | `encrypt-challenge-data` | `decrypt-challenge-data` |
|
||||
| PSO DC quest file (.vms) | None | `decode-vms` |
|
||||
| PSO GC quest file (.gci) | None | `decode-gci` |
|
||||
| Download quest file (.dlq) | None | `decode-dlq` |
|
||||
| Server quest file (.qst) | `encode-qst` | `decode-qst` |
|
||||
| PSO PC save file | `encrypt-pc-save` | `decrypt-pc-save` |
|
||||
| PSO GC save file (.gci) | `encrypt-gci-save` | `decrypt-gci-save` |
|
||||
| PSO GC snapshot file | None | `decode-gci-snapshot` |
|
||||
| Quest script (.bin) | `assemble-quest-script` | `disassemble-quest-script` |
|
||||
| Quest map (.dat) | None | `disassemble-quest-map` |
|
||||
| AFS archive | None | `extract-afs` |
|
||||
| BML archive | None | `extract-bml` |
|
||||
| GSL archive | None | `extract-gsl` |
|
||||
| GVM texture | `encode-gvm` | None |
|
||||
| Text archive | `encode-text-archive` | `decode-text-archive` |
|
||||
| Unicode text set | `encode-unicode-text-set` | `decode-unicode-text-set` |
|
||||
| Word Select data set | None | `decode-word-select-set` |
|
||||
| Set data table | None | `disassemble-set-data-table` |
|
||||
| Rare item table (AFS/GSL/JSON) | `convert-rare-item-set` | `convert-rare-item-set` |
|
||||
|
||||
#### PSO DC on Flycast
|
||||
There are several actions that don't fit well into the table above, which let you do other things:
|
||||
|
||||
If you're emulating PSO DC, all versions will connect to newserv by setting the following options in Flycast's `emu.cfg` file under `[network]`:
|
||||
- DNS = Your newserv's server address (newserv's DNS server must be running on port 53)
|
||||
- EmulateBBA = yes
|
||||
- Enable = yes
|
||||
|
||||
It is also necessary to save any DNS information to the flash memory of the Dreamcast to use the BBA - the easiest way to do this is to use the website option in USv2 and then choose the save to flash option.
|
||||
|
||||
Once set up, the EU and US versions will work without any extra set up (other than the HL Check Disable code for USv2), while the JP versions require HL Check Disable codes to be running.
|
||||
|
||||
If the server is running on the same machine as Flycast, this might not work, even if you point Flycast's DNS queries at your local IP address (instead of 127.0.0.1). In this case, you can modify the loaded executable in memory to make it connect anywhere you want. There is a script included with newserv that can do this on macOS; a similar technique could be done manually using scanmem on Linux or Cheat Engine on Windows. To use the script, do this:
|
||||
1. Build and install memwatch (https://github.com/fuzziqersoftware/memwatch).
|
||||
2. Start Flycast and run PSO. (You must start PSO before running the script; it won't work if you run the script before loading the game.)
|
||||
3. Run `sudo patch_flycast_memory.py <original-destination>`. Replace `<original-destination>` with the hostname that PSO wants to connect to (you can find this out by using Wireshark and looking for DNS queries). The script may take up to a minute; you can continue using Flycast while it runs, but don't start an online game until the script is done.
|
||||
4. Run newserv and start an online game in PSO.
|
||||
|
||||
If you use this method, you'll have to run the script every time you start PSO in Flycast, but you won't have to run it again if you start another online game without restarting emulation.
|
||||
|
||||
#### PSO PC
|
||||
|
||||
The version of PSO PC I have has the server addresses starting at offset 0x29CB34 in pso.exe. Using a hex editor, change those to "localhost" (without quotes) if you just want to connect to a locally-running newserv instance. Alternatively, you can add an entry to the Windows hosts file (C:\Windows\System32\drivers\etc\hosts) to redirect the connection to 127.0.0.1 (localhost) or any other IP address.
|
||||
|
||||
#### PSO GC on a real GameCube
|
||||
|
||||
You can make PSO connect to newserv by setting its default gateway and DNS server addresses to newserv's address. newserv's DNS server must be running on port 53 and must be accessible to the GameCube.
|
||||
|
||||
If you have PSO Plus or Episode III, it won't want to connect to a server on the same local network as the GameCube itself, as determined by the GameCube's IP address and subnet mask. In the old days, one way to get around this was to create a fake network adapter on the server (or use an existing real one) that has an IP address on a different subnet, tell the GameCube that the server is the default gateway (as above), and have the server reply to the DNS request with its non-local IP address. To do this with newserv, just set LocalAddress in the config file to a different interface. For example, if the GameCube is on the 192.168.0.x network and your other adapter has address 10.0.1.6, set newserv's LocalAddress to 10.0.1.6 and set PSO's DNS server and default gateway addresses to the server's 192.168.0.x address. This may not work on modern systems or on non-Windows machines - I haven't tested it in many years.
|
||||
|
||||
#### PSO GC on Dolphin
|
||||
|
||||
If you're using the HLE BBA type, set the BBA's DNS server address to newserv's IP address and it should work. (If newserv is on the same machine as Dolphin, try your local IP address or 127.0.0.1.) In PSO, use the example values below in PSO's network configuration.
|
||||
|
||||
If you're using the TAP BBA type, you'll have to set PSO's network settings appropriately for your tap interface. Set the DNS server address in PSO's network settings to newserv's IP address.
|
||||
|
||||
If you're using a version of Dolphin with tapserver support, you can make it connect to a newserv instance running on the same machine via the tapserver interface. You do not need to install or run tapserver. To do this:
|
||||
1. Set Dolphin's BBA type to tapserver (Config -> GameCube -> SP1).
|
||||
2. Enable newserv's IP stack simulator according to the comments in config.json and start newserv.
|
||||
3. In PSO's network settings, enable DHCP ("Automatically obtain an IP address"), set DNS server address to "Automatic", and leave DHCP Hostname as "Not set". Leave the proxy server settings blank.
|
||||
4. Start an online game.
|
||||
|
||||
#### PSO BB
|
||||
|
||||
The PSO BB client has been modified and distributed in many different forms. newserv supports most, but not all, of the common distributions. Unlike other versions, it's important that the client and server have the same map files, so make sure to set up the patch directory based on the client you'll be using with newserv. (See the "Client patch directories" section for instructions on setting this up.)
|
||||
|
||||
The original Japanese and US versions of PSO BB should work, but you'll have to modify your hosts file or edit psobb.exe to point to your newserv instance. The original versions are packed, so this is a more involved process than simply opening the executable in a hex editor and finding/replacing some strings.
|
||||
|
||||
Alternatively, you can use the Tethealla client (https://archive.org/details/psobb-tethealla-client); you can find the connection addresses starting at 0x56D724 in psobb.exe. Overwrite these addresses with your server's hostname or IP address, and you should be able to connect.
|
||||
|
||||
### Connecting external clients
|
||||
|
||||
If you want to accept connections from outside your local network, you'll need to set ExternalAddress to your public IP address in the configuration file, and you'll likely need to open some ports in your router's NAT configuration - specifically, all the TCP ports listed in PortConfiguration in config.json.
|
||||
|
||||
For GC clients, you'll have to use newserv's built-in DNS server or set up your own DNS server as well. If you want external clients to be able to use your DNS server, you'll have to forward UDP port 53 to your newserv instance. Remote players can then connect to your server by entering your DNS server's IP address in their client's network configuration.
|
||||
|
||||
### Non-server features
|
||||
|
||||
newserv has many CLI options, which can be used to access functionality other than the game and proxy server. Run `newserv help` to see these options and how to use them. The non-server things newserv can do are:
|
||||
|
||||
* Compress or decompress data in PRS, PR2, or BC0 format (`compress-prs`, `compress-pr2`, `compress-bc0`, `decompress-prs`, `decompress-pr2`, `decompress-bc0`)
|
||||
* Compute the decompressed size of compressed PRS data without decompressing it (`prs-size`)
|
||||
* Encrypt or decrypt data using any PSO version's network encryption scheme (`encrypt-data`, `decrypt-data`)
|
||||
* Encrypt or decrypt data using Episode 3's trivial scheme (`encrypt-trivial-data`, `decrypt-trivial-data`)
|
||||
* Encrypt or decrypt data using the Challenge Mode text algorithm (`encrypt-challenge-data`, `decrypt-challenge-data`)
|
||||
* Encrypt or decrypt PSO GC save data (.gci files) (`encrypt-gci-save`, `decrypt-gci-save`)
|
||||
* Convert a PSO GC or Episode 3 snapshot file to a BMP image (`decode-gci-snapshot`)
|
||||
* Find the likely round1 or round2 seed for a corrupt save file (`salvage-gci`)
|
||||
* Run a brute-force search for a decryption seed (`find-decryption-seed`)
|
||||
* Convert quests in .gci, .vms, .dlq, or .qst format to .bin/.dat format (`decode-gci`, `decode-vms`, `decode-dlq`, `decode-qst`)
|
||||
* Convert quests in .bin/.dat to .qst format (`encode-qst`)
|
||||
* Convert text archives (e.g. TextEnglish.pr2) to JSON and vice versa (`decode-text-archive`, `encode-text-archive`)
|
||||
* Compile or disassemble quest scripts (`assemble-quest-script`, `disassemble-quest-script`)
|
||||
* Format Episode 3 game data in a human-readable manner (`show-ep3-maps`, `show-ep3-cards`)
|
||||
* Convert item data to a human-readable description, or vice versa (`describe-item`, `encode-item`)
|
||||
* Format Episode 3 game data in a human-readable manner (`show-ep3-maps`, `show-ep3-cards`, `generate-ep3-cards-html`)
|
||||
* Format Blue Burst battle parameter files in a human-readable manner (`show-battle-params`)
|
||||
* Search for rare enemy seeds that result in rare enemies on console versions (`find-rare-enemy-seeds`)
|
||||
* Convert item data to a human-readable description, or vice versa (`describe-item`)
|
||||
* Connect to another PSO server and pretend to be a client (`cat-client`)
|
||||
* Replay a session log for testing (`replay-log`)
|
||||
* Extract the contents of a .gsl or .bml archive (`extract-gsl`, `extract-bml`)
|
||||
* Generate or describe DC serial numbers (`generate-dc-serial-number`, `inspect-dc-serial-number`)
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
## General
|
||||
|
||||
- Encapsulate BB server-side random state and make replays deterministic
|
||||
- Write a simple status API
|
||||
- Implement per-game logging
|
||||
- Make reloading happen on separate threads so compression doesn't block active clients
|
||||
- Implement decrypt/encrypt actions for VMS files
|
||||
- Make UI strings localizable (e.g. entries in menus, welcome message, etc.)
|
||||
- Figure out what causes the corruption message on PC proxy sessions and fix it
|
||||
- Add an idle connection timeout for proxy sessions
|
||||
- Look into JP heart symbol bug on Linux
|
||||
- Clean up ItemParameterTable implementation (see comment at the top of the class definition)
|
||||
- Handle MeetUserExtensions properly in 41 and C4 commands on the proxy (rewrite the embedded 19 command and store a map of )
|
||||
|
||||
## PSO DC
|
||||
|
||||
- Investigate if https://crates.io/crates/blaze-ssl-async can be used to implement the HL check server
|
||||
|
||||
## Episode 3
|
||||
|
||||
- Enforce tournament deck restrictions (e.g. rank checks, No Assist option) when populating COMs at tournament start time
|
||||
- Add support for recording battles on the proxy server (both in primary and spectator teams)
|
||||
- Make `reload licenses` not vulnerable to online players' licenses overwriting licenses on disk somehow
|
||||
- Implement ranks (based on total Meseta earned)
|
||||
- Make an AR code that gets rid of the SAMPLE overlays on NTE
|
||||
|
||||
## PSO XBOX
|
||||
|
||||
@@ -25,5 +26,6 @@
|
||||
## PSOBB
|
||||
|
||||
- Test all quest item subcommands
|
||||
- Check if Commander Blade effect works and implement it if not
|
||||
- Figure out why Pouilly Slime EXP doesn't work
|
||||
- Make server-specified rare enemies work with maps loaded by the proxy
|
||||
- Implement serialization for various table types (ItemPMT, ItemPT, etc.)
|
||||
|
||||
+121
-35
@@ -1,29 +1,83 @@
|
||||
(Ep1&2 USA) Unlock all songs in BGM test
|
||||
Unlock all songs in BGM test
|
||||
(Note: sadly, there are no secret/unused ones)
|
||||
04368960 38600001
|
||||
04368964 4E800020
|
||||
Ep12-JP12 => 04367A68 38600001
|
||||
04367A6C 4E800020
|
||||
Ep12-JP13 => 04368ED8 38600001
|
||||
04368EDC 4E800020
|
||||
Ep12-JP14 => 0436A434 38600001
|
||||
0436A438 4E800020
|
||||
Ep12-JP15 => 0436A1E8 38600001
|
||||
0436A1EC 4E800020
|
||||
Ep12-US10 => 0436891C 38600001
|
||||
04368920 4E800020
|
||||
Ep12-US11 => 04368960 38600001
|
||||
04368964 4E800020
|
||||
Ep12-US12 => 0436A5B4 38600001
|
||||
0436A5B8 4E800020
|
||||
Ep12-EU => 043699A8 38600001
|
||||
043699AC 4E800020
|
||||
Ep3-NTE => 041EA948 38600001
|
||||
041EA94C 4E800020
|
||||
Ep3-JP => 041D8CF0 38600001
|
||||
041D8CF4 4E800020
|
||||
Ep3-US => 041D8D7C 38600001
|
||||
041D8D80 4E800020
|
||||
Ep3-EU => 041D93F0 38600001
|
||||
041D93F4 4E800020
|
||||
|
||||
(Ep1&2 USA v1.01) Play lobby (and event) music on Pioneer 2 also
|
||||
0417E0F0 60000000
|
||||
Play lobby (and event) music in Morgue also
|
||||
Ep12-JP12 => 0417DD34 60000000
|
||||
Ep12-JP13 => 0417E0E8 60000000
|
||||
Ep12-JP14 => 0417E24C 60000000
|
||||
Ep12-JP15 => 0417E1AC 60000000
|
||||
Ep12-US10 => 0417E0F0 60000000
|
||||
Ep12-US11 => 0417E0F0 60000000
|
||||
Ep12-US12 => 0417E210 60000000
|
||||
Ep12-EU => 0417E6D4 60000000
|
||||
Ep3-NTE => 040B8C7C 60000000
|
||||
Ep3-US => 040B7028 60000000
|
||||
Ep3-JP => 040B7044 60000000
|
||||
Ep3-EU => 040B746C 60000000
|
||||
|
||||
(Ep3 USA) Play lobby (and event) music in Morgue also
|
||||
040B7028 60000000
|
||||
Skip white logo screens during startup
|
||||
Ep12-JP12 => 0413EE54 38000007
|
||||
Ep12-JP13 => 0413F1DC 38000007
|
||||
Ep12-JP14 => 0413F338 38000007
|
||||
Ep12-JP15 => 0413F298 38000007
|
||||
Ep12-US10 => 0413F190 38000007
|
||||
Ep12-US11 => 0413F190 38000007
|
||||
Ep12-US12 => 0413F2A8 38000007
|
||||
Ep12-EU => 0413F524 38000007
|
||||
Ep3-NTE => 0409E10C 38000007
|
||||
Ep3-JP => 0409D810 38000007
|
||||
Ep3-US => 0409D774 38000007
|
||||
Ep3-EU => 0409D9A4 38000007
|
||||
|
||||
(Ep3 USA) Skip white logo screens during startup
|
||||
0409D774 38000007
|
||||
(Episodes 1&2 USA v1.01) Skip white logo screens during startup
|
||||
0413F190 38000007
|
||||
Skip agreement prompts before online game
|
||||
Ep12-JP12 => 0432737C 38000003
|
||||
Ep12-JP13 => 043283CC 38000003
|
||||
Ep12-JP14 => 043298E8 38000003
|
||||
Ep12-JP15 => 04329690 38000003
|
||||
Ep12-US10 => 04327D3C 38000003
|
||||
Ep12-US11 => 04327D80 38000003
|
||||
Ep12-US12 => 0432984C 38000003
|
||||
Ep12-EU => 04328C58 38000003
|
||||
Ep3-NTE => 041C67D0 38000003
|
||||
Ep3-JP => 041B5234 38000003
|
||||
Ep3-US => 041B50C8 38000003
|
||||
Ep3-EU => 041B574C 38000003
|
||||
|
||||
(Ep3 USA) Skip agreement prompts before online game
|
||||
041B50C8 38000003
|
||||
(Episodes 1&2 USA v1.01) Skip agreement prompt before online game
|
||||
04327D80 38000003
|
||||
Disable rate limit for pressing A during loading screens
|
||||
Ep3-NTE => 042E1030 38000000
|
||||
Ep3-JP => 042F8BE4 38000000
|
||||
Ep3-US => 042F9B30 38000000
|
||||
Ep3-EU => 042FA734 38000000
|
||||
|
||||
(Ep3 USA) Disable rate limit for pressing A during loading screens
|
||||
042F9B30 38000000
|
||||
|
||||
(Ep3 USA) Auto-press A as fast as possible during loading screens
|
||||
042F9AC0 60000000
|
||||
Auto-press A as fast as possible during loading screens
|
||||
Ep3-EU => 042FA6C4 60000000
|
||||
Ep3-US => 042F9AC0 60000000
|
||||
Ep3-NTE => 040C2C48 60000000
|
||||
Ep3-JP => 042F8B74 60000000
|
||||
|
||||
(Ep3 USA) Replace loading screen A button sounds with random sounds
|
||||
042F9B18 4804BB19
|
||||
@@ -32,6 +86,13 @@
|
||||
042F9B24 64630005
|
||||
042F9B28 38800000
|
||||
|
||||
(Ep3 NTE) Replace loading screen A button sounds with random sounds
|
||||
042E1018 480309A9
|
||||
042E101C 5463063E
|
||||
042E1020 60631400
|
||||
042E1024 64630005
|
||||
042E1028 38800000
|
||||
|
||||
(Ep3 USA) Change color of loading screens
|
||||
(Replace AA, RR, GG, BB appropriately)
|
||||
042FA704 3CC0AARR
|
||||
@@ -43,11 +104,16 @@
|
||||
0400BD64 EC5D00B2
|
||||
0400BD68 4E800020
|
||||
|
||||
(Ep3 USA) Disable darkening effect during battle details mode
|
||||
042F951C 4E800020
|
||||
Disable darkening effect during battle details mode
|
||||
Ep3-NTE => 042E09D8 4E800020
|
||||
Ep3-JP => 042F85D0 4E800020
|
||||
Ep3-US => 042F951C 4E800020
|
||||
Ep3-EU => 042FA120 4E800020
|
||||
|
||||
(Ep3 USA) Unlock all COM decks
|
||||
042CA908 38600001
|
||||
Unlock all COM decks
|
||||
Ep3-JP => 042C9B34 38600001
|
||||
Ep3-EU => 042CB414 38600001
|
||||
Ep3-US => 042CA908 38600001
|
||||
|
||||
(Ep3 USA) Enable all lobby counter options in non-CARD lobbies
|
||||
04096A8C 480000C0
|
||||
@@ -103,8 +169,11 @@
|
||||
040002BC 7C633050
|
||||
040002C0 4E800020
|
||||
|
||||
(Ep3 USA) Unlock all offline free battle maps
|
||||
042CAA00 38600001
|
||||
Unlock all offline free battle maps
|
||||
Ep3-NTE => 042BE538 38600001
|
||||
Ep3-JP => 042C9C2C 38600001
|
||||
Ep3-EU => 042CB50C 38600001
|
||||
Ep3-US => 042CAA00 38600001
|
||||
(This unlocks ALL maps, including a bunch of maps with garbage names that crash if you try to play them)
|
||||
|
||||
(Ep3 USA) Talk to auction counter offline to get all cards
|
||||
@@ -155,7 +224,7 @@
|
||||
0400BEAC 7C0803A6
|
||||
0400BEB0 482E9FC0
|
||||
|
||||
(Episodes 1&2 USA v1.01) Press L for enemy debug; enable various other debug messages
|
||||
(Episodes 1&2 USA v1.1) Press L for enemy debug; enable various other debug messages
|
||||
040FD9D8 38600001 # Various enemy debug messages
|
||||
00153E53 00000001 # Poison fog debug 1
|
||||
00153E4B 00000001 # Poison fog debug 2
|
||||
@@ -165,8 +234,10 @@
|
||||
025CB6AA 00000001
|
||||
TODO: Figure out more debug message conditionals (vars/functions) and add them here
|
||||
|
||||
(Episode 3 USA) Able to find VIP cards offline (but they're still rare)
|
||||
042C0B20 4800000C
|
||||
Able to find VIP cards offline (but they're still rare)
|
||||
Ep3-EU => 042C15DC 4800000C
|
||||
Ep3-JP => 042BFE24 4800000C
|
||||
Ep3-US => 042C0B20 4800000C
|
||||
|
||||
(Ep3 USA) Hold L when starting battle to enter debug menu
|
||||
042C5460 4BD3AF78
|
||||
@@ -181,8 +252,11 @@ TODO: Figure out more debug message conditionals (vars/functions) and add them h
|
||||
040003F8 3800001A
|
||||
040003FC 482C5068
|
||||
|
||||
(Ep3 USA) Dressing room always accessible
|
||||
041A16FC 38600001
|
||||
Dressing room always accessible
|
||||
Ep3-NTE => 041B2A2C 38600001
|
||||
Ep3-JP => 041A1920 38600001
|
||||
Ep3-EU => 041A1C84 38600001
|
||||
Ep3-US => 041A16FC 38600001
|
||||
|
||||
(Ep3 USA) Full dressing room v1
|
||||
Can't change your class, but you start with your existing appearance
|
||||
@@ -201,8 +275,11 @@ Go online with this code on after using the dressing room to fully save changes
|
||||
(Ep3 USA) Replace Options menu with debug menu
|
||||
04149E70 38600019
|
||||
|
||||
(Ep3 USA) Jukebox is free
|
||||
0430D1DC 48000024
|
||||
Jukebox is free
|
||||
Ep3-NTE => 042248C4 48000024 (useless because the jukebox isn't loaded in NTE, but apparently the code for it exists)
|
||||
Ep3-JP => 0430C178 48000024
|
||||
Ep3-US => 0430D1DC 48000024
|
||||
Ep3-EU => 0430DE3C 48000024
|
||||
|
||||
(Ep3 USA) Use own character in battle (online only)
|
||||
041FFAB0 4800001C
|
||||
@@ -233,8 +310,11 @@ Go online with this code on after using the dressing room to fully save changes
|
||||
0412F8D4 7D0803A6
|
||||
0412F8D8 4BEDEBF4
|
||||
|
||||
(Ep3 USA) Metal tiles don't appear in Simulator map
|
||||
04296904 4E800020
|
||||
Metal tiles don't appear in Simulator map
|
||||
Ep3-NTE => 0428FED8 4E800020
|
||||
Ep3-JP => 04296054 4E800020
|
||||
Ep3-US => 04296904 4E800020
|
||||
Ep3-EU => 04297278 4E800020
|
||||
|
||||
(Ep3 USA) Enable Boooo and Laughter soundchat sounds
|
||||
Note: Without a TextEnglish.pr2/pr3 patch, the menu items for these sounds will be blank (but they will still work)
|
||||
@@ -264,3 +344,9 @@ Note: Without a TextEnglish.pr2/pr3 patch, the menu items for these sounds will
|
||||
0442B6D8 804D804E
|
||||
0442B6DC 804F802A
|
||||
0442B6E0 802C0000
|
||||
|
||||
(Ep3 NTE) Use English language files
|
||||
0408E414 38600001
|
||||
0408E448 38000001
|
||||
0408E44C 900DA62C
|
||||
0408E450 4E800020
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
This is a list of common shared serials for the Dreamcast version of the game.
|
||||
These serials are listed in decimal format for use with newserv and are not valid
|
||||
for use in the game itself.
|
||||
|
||||
If you are looking for a serial number to use for your Dreamcast copy of the game,
|
||||
please use newserv's DC serial number generator, or PSO Tool GUI at
|
||||
https://segaxtreme.net/resources/pso-tool-gui-by-razorx.224/
|
||||
|
||||
To allow the below shared serials to be used on your server by multiple users, use
|
||||
the below command (this works if the serial is already registered too):
|
||||
|
||||
add-license serial=<serial-number> flags=80000000
|
||||
|
||||
---
|
||||
|
||||
144243108
|
||||
297233506
|
||||
400533035
|
||||
446310728
|
||||
532044219
|
||||
1315107383
|
||||
1567634924
|
||||
1748940599
|
||||
2004318071
|
||||
2309795986
|
||||
3811232030
|
||||
3828776100
|
||||
4098754580
|
||||
@@ -0,0 +1,56 @@
|
||||
List of differences in Ep3 NTE compared to Final:
|
||||
- Assist cards
|
||||
- - Dice Fever sets dice to 6, not 5, and there is no Dice Fever +
|
||||
- - Rich + and Charity + also don't exist
|
||||
- - Powerless Rain, Brave Wind, Influence, Fix apply at a different phase of the attack procedure
|
||||
- - Tech Field applies to SCs only; on Final, it applies to SCs and creatures
|
||||
- - The denominator for Vengeance is 2; on Final it's 3
|
||||
- - AP Absorption logic is different (TODO: see apply_ap_and_tp_adjust_assists_to_attack)
|
||||
- - God Whim can use ANY assist card, not only the normally-obtainable ones, and it assigns all 4 players an assist, not only those who already had assists
|
||||
- - Inflation and Deflation only cause +1 or -1 cost per action, not per card
|
||||
- - Exchange can be set on other players
|
||||
- - The SET_MV condition overrides Snail Pace and Stamina completely
|
||||
- - Stamina sets your effective MV to 99 instead of 9
|
||||
- - Land Price is 2x instead of 1.5x
|
||||
- - Shuffle All and Shuffle Group don't respect deck shuffle/loop disabled settings
|
||||
- - Assist Vanish clears immediately, which means it can override other assists that happen at the same time (Trash 1, Empty Hand, etc.); in Final it happens after those
|
||||
- Abilities
|
||||
- - Rampage and Pierce are not player-specific; that is, if an attack has Rampage against one target, it has Rampage against all targets (this distinction is important for conditional abilities like Major Rampage and Heavy Pierce)
|
||||
- - Several abilities don't exist (TODO: Which ones? 0x64 and above?)
|
||||
- - Abnormal conditions do not have priorities like they do on Final
|
||||
- - Ability Trap seems incompletely implemented (or not implemented at all?)
|
||||
- - It appears that Major Pierce doesn't work against Arkz SCs, and this was fixed in Final
|
||||
- Conditions
|
||||
- - Anti-Abnormality doesn't prevent Freeze, Drop, Guom, or Curse
|
||||
- - SCs can get Freeze (they can't in Final)
|
||||
- - Bosses do not have automatic Anti-Abnormality
|
||||
- - Ability Trap prevents all abnormal conditions
|
||||
- Traps
|
||||
- - Traps trigger as soon as you move into their tile; on Final, they trigger at the end of the Move phase
|
||||
- - Traps may use any assist card, and this can be configured in the map definition (TODO: verify this last part)
|
||||
- Rules
|
||||
- - Dice Boost does not exist
|
||||
- - ATK and DEF dice ranges can be set independently, but there are only 7 options for each: 1-6, 1-1, 2-2, 3-3, 4-4, 5-5, 6-6
|
||||
- - There may be a bug when either die is set to 1-1 so you'll always get 2 instead (TODO: verify this)
|
||||
- COM interference is not implemented
|
||||
- The target's defense power is computed after checking if the attack is Resta instead of before
|
||||
- Card definitions
|
||||
- - The n21 and n22 arg2 conditions don't exist
|
||||
- - The p25 condition finds cards with Paralyze or Fly in NTE, vs. Aerial or Fly in Final; looks like a copy-paste error by Sega
|
||||
- - The p36 condition includes SCs and items on NTE, but only SCs on Final
|
||||
- - The p41 condition includes only your team on NTE, but both teams on Final
|
||||
- - Several tokens can't be used in expr fields: ddm, sat, edm, ldm, rdm, fdm, ndm, ehp
|
||||
- Missing rule checks
|
||||
- - Boss SCs can use items
|
||||
- - Move logic is different, which I didn't reverse-engineer because I was too lazy and couldn't imagine how it could be meaningfully different from Final
|
||||
- - Many values are not clamped (in Final, many are clamped to 0-9 or -99-99)
|
||||
- - You can set cards that aren't actually in your hand
|
||||
- - The game assumes team A always is at the top facing down and team B is always at the bottom facing up; if the map defines them to start on different edges or facing different directions than expected, the creature summoning areas will be wrong
|
||||
- - Character HP rule completely overrides the HP stat on SC cards; in Final, the SC's HP stat is added to Character HP
|
||||
- - Boss SCs are not exempt from this either; they have the same HP as normal SCs
|
||||
- - Cards marked as dead but not yet deleted can still attack
|
||||
- - The HOLD (6) and CANNOT_DEFEND (7) conditions don't actually stop you from defending
|
||||
- - There is no hard limit of 1000 turns for any battle
|
||||
- - In case of a draw, the first two tiebreaker rules (number of dead SCs and remaining SC HP) are skipped
|
||||
- The server cannot override EXP result values (thus post-battle EXP loss cannot be disabled)
|
||||
- Surprisingly, the code for PBs is identical between NTE and Final; it seems like they didn't spend any time on PBs after NTE at all
|
||||
@@ -81,7 +81,7 @@ blr
|
||||
|
||||
|
||||
|
||||
Ep1&2 v1.01 version of the above code
|
||||
Ep1&2 v1.1 version of the above code
|
||||
|
||||
send_D9
|
||||
./m68kdasm --assemble-ppc32 --ppc32 --start-address=801DA398
|
||||
|
||||
@@ -72,7 +72,7 @@ def write_patches_for_code(
|
||||
f.write("reloc0:\n")
|
||||
f.write(" .offsetof start\n")
|
||||
f.write("start:\n")
|
||||
f.write(" .include WriteCodeBlocks\n")
|
||||
f.write(" .include WriteCodeBlocksGC\n")
|
||||
for region in write_regions:
|
||||
f.write(
|
||||
f" # region @ {region.address:08X} ({len(region.data) * 4} bytes)\n"
|
||||
|
||||
+1007
-984
File diff suppressed because it is too large
Load Diff
@@ -718,8 +718,8 @@ JP12------------- JP13------------- JP14------------- JP15------------- US10----
|
||||
80267EC8 4BDA57D8 80268B74 4BDA4B2C 80269BD0 4BDA3AD0 80269984 4BDA3D1C 80268874 4BDA4E2C 80268874 4BDA4E2C 80269C48 4BDA3A58 80269490 4BDA4210 b -0x0025B1D4 /* 8000D6A0 */
|
||||
|
||||
Improved Draw Distance of most objects
|
||||
DrawDistance
|
||||
*** name=DrawDistance
|
||||
Draw Distance
|
||||
*** name=Draw Distance
|
||||
*** desc=Extend the draw\ndistance of many\nobjects
|
||||
JP12------------- JP13------------- JP14------------- JP15------------- US10------------- US11------------- US12------------- EU--------------- DISASSEMBLY (US10)
|
||||
8000DFA0 C3C2C1F8 8000DFA0 C3C2C1F8 8000DFA0 C3C2C1F8 8000DFA0 C3C2C1F8 8000DFA0 C3C2C200 8000DFA0 C3C2C200 8000DFA0 C3C2C200 8000DFA0 C3C2C200 lfs f30, [r2 - 0x3E00]
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
########################################################################
|
||||
Restore JP PSOBB original IME Behavior
|
||||
########################################################################
|
||||
|
||||
The default Tethealla client included a custom patch to disable the IME
|
||||
system in-game which allows you to type in Japanese (presumably to allow
|
||||
English versions of Windows to type properly)
|
||||
|
||||
However, if you plan to play PSOBB in it's original Japanese language it
|
||||
is recommended you remove this patch to restore the original functions
|
||||
|
||||
|
||||
Open a unpacked PSOBB.exe in a hex editor and:
|
||||
|
||||
FIND 9CC38E
|
||||
|
||||
REPLACE WITH A8838F
|
||||
|
||||
|
||||
Make sure to install Japanese Language Support in Windows 10/11 to enable
|
||||
the Japanese keyboard and IME.
|
||||
|
||||
If there's a problem, you can also use the Legacy IME by heading into the
|
||||
options of the Japanese Language settings and scroll down to the bottom of
|
||||
the page to enable Legacy IME Support to restore the original Pre-Windows 7
|
||||
IME system.
|
||||
|
||||
Last but not least, remember the default Tethealla client is the original
|
||||
Japanese client, so you don't need to apply any other special patch but this
|
||||
one and make sure you have the original Japanese files set in your data folder
|
||||
the game should start entirely in Japanese.
|
||||
|
||||
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Regular → Executable
BIN
Binary file not shown.
Regular → Executable
BIN
Binary file not shown.
Regular → Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -9,6 +9,10 @@ inline void run_ar_code_translator(const std::string&, const std::string&, const
|
||||
throw std::runtime_error("resource_file is not available; install it and rebuild newserv");
|
||||
}
|
||||
|
||||
inline void run_xbe_patch_translator(const std::string&, const std::string&, const std::string&) {
|
||||
throw std::runtime_error("resource_file is not available; install it and rebuild newserv");
|
||||
}
|
||||
|
||||
inline std::vector<std::pair<uint32_t, std::string>> diff_dol_files(const std::string&, const std::string&) {
|
||||
throw std::runtime_error("resource_file is not available; install it and rebuild newserv");
|
||||
}
|
||||
|
||||
+242
-3
@@ -5,6 +5,7 @@
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Strings.hh>
|
||||
#include <resource_file/ExecutableFormats/DOLFile.hh>
|
||||
#include <resource_file/ExecutableFormats/XBEFile.hh>
|
||||
|
||||
using namespace std;
|
||||
|
||||
@@ -63,11 +64,11 @@ public:
|
||||
}
|
||||
void set_source_file(const string& filename) {
|
||||
this->src_filename = filename;
|
||||
this->src_file = files.at(this->src_filename);
|
||||
this->src_file = this->files.at(this->src_filename);
|
||||
}
|
||||
|
||||
void find_rtoc_global_regs() const {
|
||||
for (const auto& it : files) {
|
||||
for (const auto& it : this->files) {
|
||||
bool r2_high_found = false;
|
||||
bool r2_low_found = false;
|
||||
bool r13_high_found = false;
|
||||
@@ -258,7 +259,7 @@ public:
|
||||
}
|
||||
|
||||
unordered_map<string, uint32_t> results;
|
||||
for (const auto& it : files) {
|
||||
for (const auto& it : this->files) {
|
||||
if (it.second == this->src_file) {
|
||||
log.info("(%s) %08" PRIX32 " (from source)", it.first.c_str(), src_addr);
|
||||
results.emplace(it.first, src_addr);
|
||||
@@ -349,6 +350,231 @@ private:
|
||||
shared_ptr<const DOLFile> src_file;
|
||||
};
|
||||
|
||||
class XBEPatchTranslator {
|
||||
public:
|
||||
enum class ExpandMethod {
|
||||
FORWARD = 0,
|
||||
BACKWARD,
|
||||
BOTH,
|
||||
};
|
||||
|
||||
static const char* name_for_expand_method(ExpandMethod method) {
|
||||
switch (method) {
|
||||
case ExpandMethod::FORWARD:
|
||||
return "FORWARD";
|
||||
case ExpandMethod::BACKWARD:
|
||||
return "BACKWARD";
|
||||
case ExpandMethod::BOTH:
|
||||
return "BOTH";
|
||||
default:
|
||||
throw logic_error("invalid expand method");
|
||||
}
|
||||
}
|
||||
|
||||
XBEPatchTranslator(const string& directory)
|
||||
: log("[xbe-trans] "),
|
||||
directory(directory) {
|
||||
while (ends_with(this->directory, "/")) {
|
||||
this->directory.pop_back();
|
||||
}
|
||||
for (const auto& filename : list_directory(this->directory)) {
|
||||
if (ends_with(filename, ".xbe")) {
|
||||
string name = filename.substr(0, filename.size() - 4);
|
||||
string path = directory + "/" + filename;
|
||||
this->files.emplace(name, make_shared<XBEFile>(path.c_str()));
|
||||
this->log.info("Loaded %s", name.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
~XBEPatchTranslator() = default;
|
||||
|
||||
const string& get_source_filename() const {
|
||||
return this->src_filename;
|
||||
}
|
||||
void set_source_file(const string& filename) {
|
||||
this->src_filename = filename;
|
||||
this->src_file = this->files.at(this->src_filename);
|
||||
}
|
||||
|
||||
uint32_t find_match(
|
||||
shared_ptr<const XBEFile> dest_file,
|
||||
uint32_t src_address,
|
||||
uint32_t src_size,
|
||||
ExpandMethod expand_method) const {
|
||||
if (!this->src_file) {
|
||||
throw runtime_error("no source file selected");
|
||||
}
|
||||
|
||||
const XBEFile::Section* src_section = nullptr;
|
||||
for (const auto& sec : this->src_file->sections) {
|
||||
if (src_address >= sec.addr && src_address < sec.addr + sec.file_size) {
|
||||
src_section = &sec;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!src_section) {
|
||||
throw runtime_error("source address not within any section");
|
||||
}
|
||||
|
||||
const char* method_token = this->name_for_expand_method(expand_method);
|
||||
|
||||
size_t src_offset = src_address - src_section->addr;
|
||||
size_t src_bytes_available_before = src_offset;
|
||||
size_t src_bytes_available_after = src_section->file_size - src_offset - src_size;
|
||||
this->log.info("(find_match/%s) Source offset = %08zX with %zX/%zX bytes available before/after",
|
||||
method_token, src_offset, src_bytes_available_before, src_bytes_available_after);
|
||||
|
||||
size_t match_bytes_before = 0;
|
||||
size_t match_bytes_after = 0;
|
||||
while (match_bytes_before + match_bytes_after + src_size < 0x100) {
|
||||
size_t num_matches = 0;
|
||||
size_t last_match_address = 0;
|
||||
size_t match_length = match_bytes_before + match_bytes_after + src_size;
|
||||
auto src_r = this->src_file->read_from_addr(src_section->addr + src_offset - match_bytes_before, match_length);
|
||||
for (const auto& dest_section : dest_file->sections) {
|
||||
for (size_t dest_match_offset = 0; dest_match_offset + match_length <= dest_section.file_size; dest_match_offset++) {
|
||||
src_r.go(0);
|
||||
StringReader dest_r = dest_file->read_from_addr(dest_section.addr + dest_match_offset, match_length);
|
||||
size_t z;
|
||||
for (z = 0; z < match_length; z++) {
|
||||
uint8_t src_data = src_r.get_u8();
|
||||
uint8_t dest_data = dest_r.get_u8();
|
||||
if (src_data != dest_data) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (z == match_length) {
|
||||
num_matches++;
|
||||
last_match_address = dest_section.addr + dest_match_offset + match_bytes_before;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->log.info("(find_match/%s) For match length %zX, %zu matches found", method_token, match_length, num_matches);
|
||||
if (num_matches == 1) {
|
||||
return last_match_address;
|
||||
} else if (num_matches == 0) {
|
||||
throw runtime_error("did not find exactly one match");
|
||||
}
|
||||
bool can_expand_backward = false;
|
||||
bool can_expand_forward = false;
|
||||
switch (expand_method) {
|
||||
case ExpandMethod::BACKWARD:
|
||||
can_expand_backward = (src_bytes_available_before > match_bytes_before);
|
||||
break;
|
||||
case ExpandMethod::FORWARD:
|
||||
can_expand_forward = (src_bytes_available_after > match_bytes_after);
|
||||
break;
|
||||
case ExpandMethod::BOTH:
|
||||
can_expand_backward = (src_bytes_available_before > match_bytes_before);
|
||||
can_expand_forward = (src_bytes_available_after > match_bytes_after);
|
||||
break;
|
||||
default:
|
||||
throw logic_error("invalid expand method");
|
||||
}
|
||||
if (!can_expand_backward && !can_expand_forward) {
|
||||
throw runtime_error("no further expansion is allowed");
|
||||
}
|
||||
if (can_expand_backward) {
|
||||
match_bytes_before++;
|
||||
}
|
||||
if (can_expand_forward) {
|
||||
match_bytes_after++;
|
||||
}
|
||||
}
|
||||
throw runtime_error("scan field too long; too many matches");
|
||||
}
|
||||
|
||||
void find_all_matches(uint32_t src_addr, uint32_t src_size) const {
|
||||
if (!this->src_file) {
|
||||
throw runtime_error("no source file selected");
|
||||
}
|
||||
|
||||
unordered_map<string, uint32_t> results;
|
||||
for (const auto& it : files) {
|
||||
if (it.second == this->src_file) {
|
||||
log.info("(%s) %08" PRIX32 " (from source)", it.first.c_str(), src_addr);
|
||||
results.emplace(it.first, src_addr);
|
||||
} else {
|
||||
|
||||
array<future<uint32_t>, 3> futures;
|
||||
static const array<ExpandMethod, 3> methods = {
|
||||
ExpandMethod::FORWARD,
|
||||
ExpandMethod::BACKWARD,
|
||||
ExpandMethod::BOTH,
|
||||
};
|
||||
for (size_t z = 0; z < methods.size(); z++) {
|
||||
futures[z] = async(&XBEPatchTranslator::find_match, this, it.second, src_addr, src_size, methods[z]);
|
||||
}
|
||||
|
||||
unordered_set<uint32_t> match_addrs;
|
||||
for (size_t z = 0; z < futures.size(); z++) {
|
||||
const char* method_name = this->name_for_expand_method(methods[z]);
|
||||
try {
|
||||
uint32_t ret = futures[z].get();
|
||||
log.info("(%s) (%s) %08" PRIX32, it.first.c_str(), method_name, ret);
|
||||
match_addrs.emplace(ret);
|
||||
} catch (const exception& e) {
|
||||
log.error("(%s) (%s) failed: %s", it.first.c_str(), method_name, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
if (match_addrs.empty()) {
|
||||
log.error("(%s) no match found", it.first.c_str());
|
||||
} else if (match_addrs.size() > 1) {
|
||||
log.error("(%s) different matches found by different methods", it.first.c_str());
|
||||
} else {
|
||||
results.emplace(it.first, *match_addrs.begin());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto& it : results) {
|
||||
fprintf(stdout, "%s => %08" PRIX32 "\n", it.first.c_str(), it.second);
|
||||
}
|
||||
}
|
||||
|
||||
void handle_command(const string& command) {
|
||||
auto tokens = split(command, ' ');
|
||||
if (tokens.empty()) {
|
||||
throw runtime_error("no command given");
|
||||
}
|
||||
strip_trailing_whitespace(tokens[tokens.size() - 1]);
|
||||
|
||||
if (tokens[0] == "use") {
|
||||
this->set_source_file(tokens.at(1));
|
||||
} else if (tokens[0] == "match") {
|
||||
this->find_all_matches(stoul(tokens.at(1), nullptr, 16), stoul(tokens.at(2), nullptr, 16));
|
||||
} else if (!tokens[0].empty()) {
|
||||
throw runtime_error("unknown command");
|
||||
}
|
||||
}
|
||||
|
||||
void run_shell() {
|
||||
while (!feof(stdin)) {
|
||||
if (!this->src_filename.empty()) {
|
||||
fprintf(stdout, "ar-trans:%s/%s> ", this->directory.c_str(), this->src_filename.c_str());
|
||||
} else {
|
||||
fprintf(stdout, "ar-trans:%s> ", this->directory.c_str());
|
||||
}
|
||||
fflush(stdout);
|
||||
|
||||
string command = fgets(stdin);
|
||||
try {
|
||||
this->handle_command(command);
|
||||
} catch (const exception& e) {
|
||||
this->log.error("Failed: %s", e.what());
|
||||
}
|
||||
}
|
||||
fputc('\n', stdout);
|
||||
}
|
||||
|
||||
private:
|
||||
PrefixedLogger log;
|
||||
string directory;
|
||||
unordered_map<string, shared_ptr<const XBEFile>> files;
|
||||
string src_filename;
|
||||
shared_ptr<const XBEFile> src_file;
|
||||
};
|
||||
|
||||
void run_ar_code_translator(const std::string& directory, const std::string& use_filename, const std::string& command) {
|
||||
ARCodeTranslator trans(directory);
|
||||
if (!use_filename.empty()) {
|
||||
@@ -362,6 +588,19 @@ void run_ar_code_translator(const std::string& directory, const std::string& use
|
||||
}
|
||||
}
|
||||
|
||||
void run_xbe_patch_translator(const std::string& directory, const std::string& use_filename, const std::string& command) {
|
||||
XBEPatchTranslator trans(directory);
|
||||
if (!use_filename.empty()) {
|
||||
trans.set_source_file(use_filename);
|
||||
}
|
||||
|
||||
if (!command.empty()) {
|
||||
trans.handle_command(command);
|
||||
} else {
|
||||
trans.run_shell();
|
||||
}
|
||||
}
|
||||
|
||||
vector<pair<uint32_t, string>> diff_dol_files(const string& a_filename, const string& b_filename) {
|
||||
DOLFile a(a_filename.c_str());
|
||||
DOLFile b(b_filename.c_str());
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
void run_ar_code_translator(const std::string& initial_directory, const std::string& use_file, const std::string& command);
|
||||
void run_ar_code_translator(const std::string& directory, const std::string& use_filename, const std::string& command);
|
||||
void run_xbe_patch_translator(const std::string& directory, const std::string& use_filename, const std::string& command);
|
||||
std::vector<std::pair<uint32_t, std::string>> diff_dol_files(const std::string& a_filename, const std::string& b_filename);
|
||||
|
||||
@@ -20,7 +20,7 @@ void BattleParamsIndex::Table::print(FILE* stream) const {
|
||||
e.char_stats.dfp.load(),
|
||||
e.char_stats.ata.load(),
|
||||
e.char_stats.lck.load(),
|
||||
e.unknown_a1.load(),
|
||||
e.esp.load(),
|
||||
e.experience.load(),
|
||||
e.meseta.load());
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ public:
|
||||
/* 04 */ le_int16_t ata_bonus;
|
||||
/* 06 */ le_uint16_t unknown_a4;
|
||||
/* 08 */ le_float distance_x;
|
||||
/* 0C */ le_float angle_x;
|
||||
/* 0C */ le_uint32_t angle_x; // Out of 0x10000 (high 16 bits are unused)
|
||||
/* 10 */ le_float distance_y;
|
||||
/* 14 */ le_uint16_t unknown_a8;
|
||||
/* 16 */ le_uint16_t unknown_a9;
|
||||
|
||||
+65
-14
@@ -31,21 +31,19 @@
|
||||
|
||||
using namespace std;
|
||||
|
||||
CatSession::exit_shell::exit_shell() : runtime_error("shell exited") {}
|
||||
|
||||
CatSession::CatSession(
|
||||
shared_ptr<struct event_base> base,
|
||||
const struct sockaddr_storage& remote,
|
||||
Version version,
|
||||
shared_ptr<const PSOBBEncryption::KeyFile> bb_key_file)
|
||||
: Shell(base),
|
||||
log(string_printf("[CatSession:%s] ", name_for_enum(version)), proxy_server_log.min_level),
|
||||
channel(
|
||||
version,
|
||||
1,
|
||||
CatSession::dispatch_on_channel_input,
|
||||
CatSession::dispatch_on_channel_error,
|
||||
this,
|
||||
"CatSession"),
|
||||
: log(string_printf("[CatSession:%s] ", name_for_enum(version)), proxy_server_log.min_level),
|
||||
base(base),
|
||||
read_event(event_new(this->base.get(), 0, EV_READ | EV_PERSIST, CatSession::dispatch_read_stdin, this), event_free),
|
||||
channel(version, 1, CatSession::dispatch_on_channel_input, CatSession::dispatch_on_channel_error, this, "CatSession"),
|
||||
bb_key_file(bb_key_file) {
|
||||
|
||||
if (remote.ss_family != AF_INET) {
|
||||
throw runtime_error("remote is not AF_INET");
|
||||
}
|
||||
@@ -64,6 +62,14 @@ CatSession::CatSession(
|
||||
reinterpret_cast<const sockaddr*>(&remote), sizeof(struct sockaddr_in)) != 0) {
|
||||
throw runtime_error(string_printf("failed to connect (%d)", EVUTIL_SOCKET_ERROR()));
|
||||
}
|
||||
|
||||
event_add(this->read_event.get(), nullptr);
|
||||
this->poll.add(0, POLLIN);
|
||||
}
|
||||
|
||||
void CatSession::execute_command(const std::string& command) {
|
||||
string full_cmd = parse_data_string(command, nullptr, ParseDataFlags::ALLOW_FILES);
|
||||
send_command_with_header(this->channel, full_cmd.data(), full_cmd.size());
|
||||
}
|
||||
|
||||
void CatSession::dispatch_on_channel_input(
|
||||
@@ -129,9 +135,54 @@ void CatSession::on_channel_error(short events) {
|
||||
}
|
||||
}
|
||||
|
||||
void CatSession::print_prompt() {}
|
||||
|
||||
void CatSession::execute_command(const std::string& command) {
|
||||
string full_cmd = parse_data_string(command, nullptr, ParseDataFlags::ALLOW_FILES);
|
||||
send_command_with_header(this->channel, full_cmd.data(), full_cmd.size());
|
||||
void CatSession::dispatch_read_stdin(evutil_socket_t, short, void* ctx) {
|
||||
reinterpret_cast<CatSession*>(ctx)->read_stdin();
|
||||
}
|
||||
|
||||
void CatSession::read_stdin() {
|
||||
bool any_command_read = false;
|
||||
for (;;) {
|
||||
auto poll_result = this->poll.poll();
|
||||
short fd_events = 0;
|
||||
try {
|
||||
fd_events = poll_result.at(0);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
|
||||
if (!(fd_events & POLLIN)) {
|
||||
break;
|
||||
}
|
||||
|
||||
string command(2048, '\0');
|
||||
if (!fgets(command.data(), command.size(), stdin)) {
|
||||
if (!any_command_read) {
|
||||
// ctrl+d probably; we should exit
|
||||
fputc('\n', stderr);
|
||||
event_base_loopexit(this->base.get(), nullptr);
|
||||
return;
|
||||
} else {
|
||||
break; // probably not EOF; just no more commands for now
|
||||
}
|
||||
}
|
||||
|
||||
// trim the extra data off the string
|
||||
size_t len = strlen(command.c_str());
|
||||
if (len == 0) {
|
||||
break;
|
||||
}
|
||||
if (command[len - 1] == '\n') {
|
||||
len--;
|
||||
}
|
||||
command.resize(len);
|
||||
any_command_read = true;
|
||||
|
||||
try {
|
||||
execute_command(command);
|
||||
} catch (const exit_shell&) {
|
||||
event_base_loopexit(this->base.get(), nullptr);
|
||||
return;
|
||||
} catch (const exception& e) {
|
||||
fprintf(stderr, "FAILED: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-5
@@ -14,28 +14,41 @@
|
||||
#include "PSOEncryption.hh"
|
||||
#include "PSOProtocol.hh"
|
||||
#include "ServerState.hh"
|
||||
#include "Shell.hh"
|
||||
|
||||
class CatSession : public Shell {
|
||||
class CatSession {
|
||||
public:
|
||||
CatSession(
|
||||
std::shared_ptr<struct event_base> base,
|
||||
const struct sockaddr_storage& remote,
|
||||
Version version,
|
||||
std::shared_ptr<const PSOBBEncryption::KeyFile> bb_key_file);
|
||||
CatSession(const CatSession&) = delete;
|
||||
CatSession(CatSession&&) = delete;
|
||||
CatSession& operator=(const CatSession&) = delete;
|
||||
CatSession& operator=(CatSession&&) = delete;
|
||||
virtual ~CatSession() = default;
|
||||
|
||||
protected:
|
||||
PrefixedLogger log;
|
||||
std::shared_ptr<struct event_base> base;
|
||||
std::unique_ptr<struct event, void (*)(struct event*)> read_event;
|
||||
Poll poll;
|
||||
|
||||
Channel channel;
|
||||
std::shared_ptr<const PSOBBEncryption::KeyFile> bb_key_file;
|
||||
|
||||
virtual void print_prompt();
|
||||
class exit_shell : public std::runtime_error {
|
||||
public:
|
||||
exit_shell();
|
||||
~exit_shell() = default;
|
||||
};
|
||||
|
||||
virtual void execute_command(const std::string& command);
|
||||
|
||||
static void dispatch_on_channel_input(
|
||||
Channel& ch, uint16_t command, uint32_t flag, std::string& msg);
|
||||
static void dispatch_read_stdin(evutil_socket_t fd, short events, void* ctx);
|
||||
static void dispatch_on_channel_input(Channel& ch, uint16_t command, uint32_t flag, std::string& msg);
|
||||
static void dispatch_on_channel_error(Channel& ch, short events);
|
||||
void on_channel_input(uint16_t command, uint32_t flag, std::string& msg);
|
||||
void on_channel_error(short events);
|
||||
void read_stdin();
|
||||
};
|
||||
|
||||
+1
-1
@@ -271,7 +271,7 @@ void Channel::send(uint16_t cmd, uint32_t flag, const std::vector<std::pair<cons
|
||||
case Version::DC_V2:
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
case Version::XB_V3: {
|
||||
PSOCommandHeaderDCV3 header;
|
||||
|
||||
+448
-122
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -120,7 +120,7 @@ const vector<ChoiceSearchCategory> CHOICE_SEARCH_CATEGORIES({
|
||||
case Version::GC_V3:
|
||||
case Version::XB_V3:
|
||||
return (choice_id == 0x0004);
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
return (choice_id == 0x0005);
|
||||
case Version::BB_V4:
|
||||
|
||||
+147
-57
@@ -23,7 +23,15 @@ static atomic<uint64_t> next_id(1);
|
||||
|
||||
void Client::Config::set_flags_for_version(Version version, int64_t sub_version) {
|
||||
this->set_flag(Flag::PROXY_CHAT_COMMANDS_ENABLED);
|
||||
this->set_flag(Flag::PROXY_CHAT_FILTER_ENABLED);
|
||||
|
||||
// BB shares some sub_version values with GC Episode 3, so we handle it
|
||||
// separately first.
|
||||
if (version == Version::BB_V4) {
|
||||
this->set_flag(Flag::NO_D6);
|
||||
this->set_flag(Flag::SAVE_ENABLED);
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (sub_version) {
|
||||
case -1: // Initial check (before sub_version recognition)
|
||||
@@ -48,7 +56,7 @@ void Client::Config::set_flags_for_version(Version version, int64_t sub_version)
|
||||
break;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
break;
|
||||
case Version::XB_V3:
|
||||
@@ -56,11 +64,6 @@ void Client::Config::set_flags_for_version(Version version, int64_t sub_version)
|
||||
this->set_flag(Flag::NO_D6_AFTER_LOBBY);
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
break;
|
||||
case Version::BB_V4:
|
||||
this->set_flag(Flag::NO_D6);
|
||||
this->set_flag(Flag::SAVE_ENABLED);
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
break;
|
||||
default:
|
||||
throw logic_error("invalid game version");
|
||||
}
|
||||
@@ -70,8 +73,6 @@ void Client::Config::set_flags_for_version(Version version, int64_t sub_version)
|
||||
case 0x21: // DCv1 US
|
||||
this->set_flag(Flag::NO_D6);
|
||||
this->set_flag(Flag::NO_SEND_FUNCTION_CALL);
|
||||
// In the case of DCNTE, the IS_DC_TRIAL_EDITION flag is already set when
|
||||
// we get here
|
||||
break;
|
||||
case 0x23: // DCv1 EU
|
||||
this->set_flag(Flag::NO_D6);
|
||||
@@ -88,36 +89,32 @@ void Client::Config::set_flags_for_version(Version version, int64_t sub_version)
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_CHECKSUM_ONLY);
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
break;
|
||||
case 0x30: // GC Ep1&2 GameJam demo, GC Ep1&2 Trial Edition, GC Ep1&2 JP v1.02, at least one version of XB
|
||||
case 0x31: // GC Ep1&2 US v1.00, GC US v1.01, XB US
|
||||
case 0x34: // GC Ep1&2 JP v1.03
|
||||
// In the case of GC Trial Edition, the IS_GC_TRIAL_EDITION flag is
|
||||
// already set when we get here (because the client has used V2 encryption
|
||||
// instead of V3)
|
||||
case 0x30: // GC Ep1&2 GameJam demo, GC Ep1&2 Trial Edition, GC Ep1&2 JP v1.2, at least one version of XB
|
||||
case 0x31: // GC Ep1&2 US v1.0, GC US v1.1, XB US
|
||||
break;
|
||||
case 0x32: // GC Ep1&2 EU 50Hz
|
||||
case 0x33: // GC Ep1&2 EU 60Hz
|
||||
case 0x34: // GC Ep1&2 JP v1.3
|
||||
this->set_flag(Flag::NO_D6_AFTER_LOBBY);
|
||||
break;
|
||||
case 0x35: // GC Ep1&2 JP v1.04 (Plus)
|
||||
case 0x35: // GC Ep1&2 JP v1.4 (Plus)
|
||||
this->set_flag(Flag::NO_D6_AFTER_LOBBY);
|
||||
this->set_flag(Flag::ENCRYPTED_SEND_FUNCTION_CALL);
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
break;
|
||||
case 0x36: // GC Ep1&2 US v1.02 (Plus)
|
||||
case 0x39: // GC Ep1&2 JP v1.05 (Plus)
|
||||
case 0x36: // GC Ep1&2 US v1.2 (Plus)
|
||||
case 0x39: // GC Ep1&2 JP v1.5 (Plus)
|
||||
this->set_flag(Flag::NO_D6_AFTER_LOBBY);
|
||||
this->set_flag(Flag::NO_SEND_FUNCTION_CALL);
|
||||
break;
|
||||
case 0x40: // GC Ep3 JP and Trial Edition
|
||||
case 0x40: // GC Ep3 JP and Trial Edition (and BB)
|
||||
this->set_flag(Flag::NO_D6_AFTER_LOBBY);
|
||||
this->set_flag(Flag::ENCRYPTED_SEND_FUNCTION_CALL);
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
// sub_version can't be used to tell JP final and Trial Edition apart; we
|
||||
// instead look at header.flag in the 61 command and set the
|
||||
// IS_EP3_TRIAL_EDITION flag there.
|
||||
// instead look at header.flag in the 61 command and set the version then.
|
||||
break;
|
||||
case 0x41: // GC Ep3 US
|
||||
case 0x41: // GC Ep3 US (and BB)
|
||||
this->set_flag(Flag::NO_D6_AFTER_LOBBY);
|
||||
this->set_flag(Flag::USE_OVERFLOW_FOR_SEND_FUNCTION_CALL);
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
@@ -132,6 +129,38 @@ void Client::Config::set_flags_for_version(Version version, int64_t sub_version)
|
||||
}
|
||||
}
|
||||
|
||||
Client::ItemDropNotificationMode Client::Config::get_drop_notification_mode() const {
|
||||
uint8_t mode_s = (this->check_flag(Flag::ITEM_DROP_NOTIFICATIONS_1) ? 1 : 0) |
|
||||
(this->check_flag(Flag::ITEM_DROP_NOTIFICATIONS_2) ? 2 : 0);
|
||||
return static_cast<Client::ItemDropNotificationMode>(mode_s);
|
||||
}
|
||||
|
||||
void Client::Config::set_drop_notification_mode(ItemDropNotificationMode new_mode) {
|
||||
uint8_t mode_s = static_cast<uint8_t>(new_mode);
|
||||
if (mode_s & 1) {
|
||||
this->set_flag(Client::Flag::ITEM_DROP_NOTIFICATIONS_1);
|
||||
} else {
|
||||
this->clear_flag(Client::Flag::ITEM_DROP_NOTIFICATIONS_1);
|
||||
}
|
||||
if (mode_s & 2) {
|
||||
this->set_flag(Client::Flag::ITEM_DROP_NOTIFICATIONS_2);
|
||||
} else {
|
||||
this->clear_flag(Client::Flag::ITEM_DROP_NOTIFICATIONS_2);
|
||||
}
|
||||
}
|
||||
|
||||
bool Client::Config::should_update_vs(const Config& other) const {
|
||||
constexpr uint64_t mask = static_cast<uint64_t>(Flag::CLIENT_SIDE_MASK);
|
||||
return ((this->enabled_flags ^ other.enabled_flags) & mask) ||
|
||||
(this->specific_version != other.specific_version) ||
|
||||
(this->override_random_seed != other.override_random_seed) ||
|
||||
(this->override_section_id != other.override_section_id) ||
|
||||
(this->override_lobby_event != other.override_lobby_event) ||
|
||||
(this->override_lobby_number != other.override_lobby_number) ||
|
||||
(this->proxy_destination_address != other.proxy_destination_address) ||
|
||||
(this->proxy_destination_port != other.proxy_destination_port);
|
||||
}
|
||||
|
||||
Client::Client(
|
||||
shared_ptr<Server> server,
|
||||
struct bufferevent* bev,
|
||||
@@ -180,6 +209,10 @@ Client::Client(
|
||||
external_bank_character_index(-1),
|
||||
last_play_time_update(0) {
|
||||
this->config.set_flags_for_version(version, -1);
|
||||
auto s = server->get_state();
|
||||
if (is_v1_or_v2(this->version()) ? s->default_rare_notifs_enabled_v1_v2 : s->default_rare_notifs_enabled_v3_v4) {
|
||||
this->config.set_drop_notification_mode(ItemDropNotificationMode::RARES_ONLY);
|
||||
}
|
||||
this->config.specific_version = default_specific_version_for_version(version, -1);
|
||||
|
||||
this->last_switch_enabled_command.header.subcommand = 0;
|
||||
@@ -191,7 +224,8 @@ Client::Client(
|
||||
// Don't print data sent to patch clients to the logs. The patch server
|
||||
// protocol is fully understood and data logs for patch clients are generally
|
||||
// more annoying than helpful at this point.
|
||||
if ((this->channel.version == Version::PC_PATCH) || (this->channel.version == Version::BB_PATCH)) {
|
||||
if ((s->hide_download_commands) &&
|
||||
((this->channel.version == Version::PC_PATCH) || (this->channel.version == Version::BB_PATCH))) {
|
||||
this->channel.terminal_recv_color = TerminalFormat::END;
|
||||
this->channel.terminal_send_color = TerminalFormat::END;
|
||||
}
|
||||
@@ -221,9 +255,10 @@ void Client::reschedule_save_game_data_event() {
|
||||
}
|
||||
|
||||
void Client::reschedule_ping_and_timeout_events() {
|
||||
struct timeval ping_tv = usecs_to_timeval(30000000); // 30 seconds
|
||||
auto s = this->require_server_state();
|
||||
struct timeval ping_tv = usecs_to_timeval(s->client_ping_interval_usecs);
|
||||
event_add(this->send_ping_event.get(), &ping_tv);
|
||||
struct timeval idle_tv = usecs_to_timeval(60000000); // 1 minute
|
||||
struct timeval idle_tv = usecs_to_timeval(s->client_idle_timeout_usecs);
|
||||
event_add(this->idle_timeout_event.get(), &idle_tv);
|
||||
}
|
||||
|
||||
@@ -239,6 +274,24 @@ void Client::set_license(shared_ptr<License> l) {
|
||||
this->license = l;
|
||||
}
|
||||
|
||||
void Client::convert_license_to_temporary_if_nte() {
|
||||
// If the session is a prototype version and the license was created and we
|
||||
// should use a temporary license instead, delete the permanent license and
|
||||
// replace it with a temporary license.
|
||||
auto s = this->require_server_state();
|
||||
if (s->use_temp_licenses_for_prototypes &&
|
||||
this->config.check_flag(Client::Flag::LICENSE_WAS_CREATED) &&
|
||||
is_any_nte(this->version())) {
|
||||
this->log.info("Client is a prototype version and the license was created during this session; converting permanent license to temporary license");
|
||||
s->license_index->remove(this->license->serial_number);
|
||||
auto new_l = s->license_index->create_temporary_license();
|
||||
this->license->delete_file();
|
||||
*new_l = std::move(*this->license);
|
||||
this->set_license(new_l);
|
||||
this->config.clear_flag(Client::Flag::LICENSE_WAS_CREATED);
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<ServerState> Client::require_server_state() const {
|
||||
auto server = this->server.lock();
|
||||
if (!server) {
|
||||
@@ -296,40 +349,52 @@ shared_ptr<const TeamIndex::Team> Client::team() const {
|
||||
return team;
|
||||
}
|
||||
|
||||
bool Client::can_see_quest(shared_ptr<const Quest> q, uint8_t difficulty, size_t num_players) const {
|
||||
if (this->license && (this->license->flags & License::Flag::DISABLE_QUEST_REQUIREMENTS)) {
|
||||
bool Client::evaluate_quest_availability_expression(
|
||||
shared_ptr<const QuestAvailabilityExpression> expr,
|
||||
uint8_t event,
|
||||
uint8_t difficulty,
|
||||
size_t num_players,
|
||||
bool v1_present) const {
|
||||
if (this->license && this->license->check_flag(License::Flag::DISABLE_QUEST_REQUIREMENTS)) {
|
||||
return true;
|
||||
}
|
||||
if (!q->available_expression) {
|
||||
if (!expr) {
|
||||
return true;
|
||||
}
|
||||
string expr = q->available_expression->str();
|
||||
auto l = this->lobby.lock();
|
||||
auto p = this->character();
|
||||
QuestAvailabilityExpression::Env env = {
|
||||
.flags = &this->character()->quest_flags.data.at(difficulty),
|
||||
.flags = (l && !l->quest_flags_known) ? &l->quest_flag_values->data.at(difficulty) : &p->quest_flags.data.at(difficulty),
|
||||
.challenge_records = &p->challenge_records,
|
||||
.team = this->team(),
|
||||
.num_players = num_players,
|
||||
.event = event,
|
||||
.v1_present = v1_present,
|
||||
};
|
||||
int64_t ret = q->available_expression->evaluate(env);
|
||||
this->log.info("Evaluated quest availability expression %s => %s", expr.c_str(), ret ? "TRUE" : "FALSE");
|
||||
int64_t ret = expr->evaluate(env);
|
||||
if (this->log.should_log(LogLevel::INFO)) {
|
||||
string expr_str = expr->str();
|
||||
this->log.info("Evaluated quest availability expression %s => %s", expr_str.c_str(), ret ? "TRUE" : "FALSE");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool Client::can_play_quest(shared_ptr<const Quest> q, uint8_t difficulty, size_t num_players) const {
|
||||
if (this->license && (this->license->flags & License::Flag::DISABLE_QUEST_REQUIREMENTS)) {
|
||||
bool Client::can_see_quest(shared_ptr<const Quest> q, uint8_t event, uint8_t difficulty, size_t num_players, bool v1_present) const {
|
||||
return this->evaluate_quest_availability_expression(q->available_expression, event, difficulty, num_players, v1_present);
|
||||
}
|
||||
|
||||
bool Client::can_play_quest(shared_ptr<const Quest> q, uint8_t event, uint8_t difficulty, size_t num_players, bool v1_present) const {
|
||||
return this->evaluate_quest_availability_expression(q->enabled_expression, event, difficulty, num_players, v1_present);
|
||||
}
|
||||
|
||||
bool Client::can_use_chat_commands() const {
|
||||
if (!this->license) {
|
||||
return false;
|
||||
}
|
||||
if (this->license->check_flag(License::Flag::ALWAYS_ENABLE_CHAT_COMMANDS)) {
|
||||
return true;
|
||||
}
|
||||
if (!q->enabled_expression) {
|
||||
return true;
|
||||
}
|
||||
string expr = q->enabled_expression->str();
|
||||
QuestAvailabilityExpression::Env env = {
|
||||
.flags = &this->character()->quest_flags.data.at(difficulty),
|
||||
.team = this->team(),
|
||||
.num_players = num_players,
|
||||
};
|
||||
bool ret = q->enabled_expression->evaluate(env);
|
||||
this->log.info("Evaluating quest enabled expression %s => %s", expr.c_str(), ret ? "TRUE" : "FALSE");
|
||||
return ret;
|
||||
return this->require_server_state()->enable_chat_commands;
|
||||
}
|
||||
|
||||
void Client::dispatch_save_game_data(evutil_socket_t, short, void* ctx) {
|
||||
@@ -409,7 +474,7 @@ void Client::create_battle_overlay(shared_ptr<const BattleRules> rules, shared_p
|
||||
stats.reset_to_base(char_class, level_table);
|
||||
stats.advance_to_level(char_class, target_level, level_table);
|
||||
|
||||
stats.unknown_a1 = 40;
|
||||
stats.esp = 40;
|
||||
stats.meseta = 300;
|
||||
}
|
||||
if (rules->tech_disk_mode == BattleRules::TechDiskMode::LIMIT_LEVEL) {
|
||||
@@ -455,7 +520,7 @@ void Client::create_challenge_overlay(Version version, size_t template_index, sh
|
||||
overlay->disp.stats.reset_to_base(overlay->disp.visual.char_class, level_table);
|
||||
overlay->disp.stats.advance_to_level(overlay->disp.visual.char_class, tpl.level, level_table);
|
||||
|
||||
overlay->disp.stats.unknown_a1 = 40;
|
||||
overlay->disp.stats.esp = 40;
|
||||
overlay->disp.stats.unknown_a3 = 10.0;
|
||||
overlay->disp.stats.experience = level_table->stats_delta_for_level(overlay->disp.visual.char_class, overlay->disp.stats.level).experience;
|
||||
overlay->disp.stats.meseta = 0;
|
||||
@@ -750,7 +815,7 @@ void Client::load_all_files() {
|
||||
files_manager->set_character(this->character_filename(), this->character_data);
|
||||
this->character_data->inventory = nsc_data.inventory;
|
||||
this->character_data->disp = nsc_data.disp;
|
||||
this->character_data->play_time_seconds = nsc_data.disp.play_time;
|
||||
this->character_data->play_time_seconds = 0;
|
||||
this->character_data->unknown_a2 = nsc_data.unknown_a2;
|
||||
this->character_data->quest_flags = nsc_data.quest_flags;
|
||||
this->character_data->death_count = nsc_data.death_count;
|
||||
@@ -767,7 +832,7 @@ void Client::load_all_files() {
|
||||
this->character_data->battle_records = nsc_data.battle_records;
|
||||
this->character_data->challenge_records = nsc_data.challenge_records;
|
||||
this->character_data->tech_menu_config = nsc_data.tech_menu_config;
|
||||
this->character_data->quest_global_flags = nsc_data.quest_global_flags;
|
||||
this->character_data->quest_counters = nsc_data.quest_counters;
|
||||
if (nsa_data) {
|
||||
this->character_data->option_flags = nsa_data->option_flags;
|
||||
this->character_data->symbol_chats = nsa_data->symbol_chats;
|
||||
@@ -779,11 +844,6 @@ void Client::load_all_files() {
|
||||
}
|
||||
}
|
||||
|
||||
if (this->character_data) {
|
||||
this->license->auto_reply_message = this->character_data->auto_reply.decode();
|
||||
this->license->save();
|
||||
}
|
||||
|
||||
this->blocked_senders.clear();
|
||||
for (size_t z = 0; z < this->guild_card_data->blocked.size(); z++) {
|
||||
if (this->guild_card_data->blocked[z].present) {
|
||||
@@ -792,6 +852,10 @@ void Client::load_all_files() {
|
||||
}
|
||||
|
||||
if (this->character_data) {
|
||||
// Clear legacy play_time field
|
||||
this->character_data->disp.name.clear_after_bytes(0x18);
|
||||
this->license->auto_reply_message = this->character_data->auto_reply.decode();
|
||||
this->license->save();
|
||||
this->last_play_time_update = now();
|
||||
}
|
||||
}
|
||||
@@ -864,8 +928,7 @@ void Client::save_character_file() {
|
||||
// off each time we save. I'm lazy, so insert shrug emoji here.
|
||||
uint64_t t = now();
|
||||
uint64_t seconds = (t - this->last_play_time_update) / 1000000;
|
||||
this->character_data->disp.play_time += seconds;
|
||||
this->character_data->play_time_seconds = this->character_data->disp.play_time;
|
||||
this->character_data->play_time_seconds += seconds;
|
||||
player_data_log.info("Added %" PRIu64 " seconds to play time", seconds);
|
||||
this->last_play_time_update = t;
|
||||
}
|
||||
@@ -988,3 +1051,30 @@ void Client::use_character_bank(int8_t index) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Client::print_inventory(FILE* stream) const {
|
||||
auto s = this->require_server_state();
|
||||
auto p = this->character();
|
||||
fprintf(stream, "[PlayerInventory] Meseta: %" PRIu32 "\n", p->disp.stats.meseta.load());
|
||||
fprintf(stream, "[PlayerInventory] %hhu items\n", p->inventory.num_items);
|
||||
for (size_t x = 0; x < p->inventory.num_items; x++) {
|
||||
const auto& item = p->inventory.items[x];
|
||||
auto hex = item.data.hex();
|
||||
auto name = s->describe_item(this->version(), item.data, false);
|
||||
fprintf(stream, "[PlayerInventory] %2zu: [+%08" PRIX32 "] %s (%s)\n", x, item.flags.load(), hex.c_str(), name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void Client::print_bank(FILE* stream) const {
|
||||
auto s = this->require_server_state();
|
||||
auto p = this->character();
|
||||
fprintf(stream, "[PlayerBank] Meseta: %" PRIu32 "\n", p->bank.meseta.load());
|
||||
fprintf(stream, "[PlayerBank] %" PRIu32 " items\n", p->bank.num_items.load());
|
||||
for (size_t x = 0; x < p->bank.num_items; x++) {
|
||||
const auto& item = p->bank.items[x];
|
||||
const char* present_token = item.present ? "" : " (missing present flag)";
|
||||
auto hex = item.data.hex();
|
||||
auto name = s->describe_item(this->version(), item.data, false);
|
||||
fprintf(stream, "[PlayerBank] %3zu: %s (%s) (x%hu)%s\n", x, hex.c_str(), name.c_str(), item.amount.load(), present_token);
|
||||
}
|
||||
}
|
||||
|
||||
+80
-50
@@ -30,61 +30,76 @@ public:
|
||||
enum class Flag : uint64_t {
|
||||
// clang-format off
|
||||
|
||||
// This mask specifies which flags are sent to the client
|
||||
// TODO: It'd be nice to use a pattern here (e.g. all server-side flags are
|
||||
// in the high bits) but that would require re-recording or manually
|
||||
// rewriting all the tests
|
||||
CLIENT_SIDE_MASK = 0xFF3CFFFF7C0FFFFB,
|
||||
|
||||
// Version-related flags
|
||||
CHECKED_FOR_DC_V1_PROTOTYPE = 0x0000000000000002,
|
||||
NO_D6_AFTER_LOBBY = 0x0000000000000100,
|
||||
NO_D6 = 0x0000000000000200,
|
||||
FORCE_ENGLISH_LANGUAGE_BB = 0x0000000000000400,
|
||||
CHECKED_FOR_DC_V1_PROTOTYPE = 0x0000000000000002,
|
||||
LICENSE_WAS_CREATED = 0x0000000000000004, // Server-side only
|
||||
NO_D6_AFTER_LOBBY = 0x0000000000000100,
|
||||
NO_D6 = 0x0000000000000200,
|
||||
FORCE_ENGLISH_LANGUAGE_BB = 0x0000000000000400,
|
||||
|
||||
// Flags describing the behavior for send_function_call
|
||||
NO_SEND_FUNCTION_CALL = 0x0000000000001000,
|
||||
ENCRYPTED_SEND_FUNCTION_CALL = 0x0000000000002000,
|
||||
SEND_FUNCTION_CALL_CHECKSUM_ONLY = 0x0000000000004000,
|
||||
SEND_FUNCTION_CALL_NO_CACHE_PATCH = 0x0000000000008000,
|
||||
USE_OVERFLOW_FOR_SEND_FUNCTION_CALL = 0x0000000000010000,
|
||||
NO_SEND_FUNCTION_CALL = 0x0000000000001000,
|
||||
ENCRYPTED_SEND_FUNCTION_CALL = 0x0000000000002000,
|
||||
SEND_FUNCTION_CALL_CHECKSUM_ONLY = 0x0000000000004000,
|
||||
SEND_FUNCTION_CALL_NO_CACHE_PATCH = 0x0000000000008000,
|
||||
USE_OVERFLOW_FOR_SEND_FUNCTION_CALL = 0x0000000000010000,
|
||||
|
||||
// State flags
|
||||
LOADING = 0x0000000000100000,
|
||||
LOADING_QUEST = 0x0000000000200000,
|
||||
LOADING_RUNNING_JOINABLE_QUEST = 0x0000000000400000,
|
||||
LOADING_TOURNAMENT = 0x0000000000800000,
|
||||
IN_INFORMATION_MENU = 0x0000000001000000,
|
||||
AT_WELCOME_MESSAGE = 0x0000000002000000,
|
||||
SAVE_ENABLED = 0x0000000004000000,
|
||||
HAS_EP3_CARD_DEFS = 0x0000000008000000,
|
||||
HAS_EP3_MEDIA_UPDATES = 0x0000000010000000,
|
||||
USE_OVERRIDE_RANDOM_SEED = 0x0000000020000000,
|
||||
HAS_GUILD_CARD_NUMBER = 0x0000000040000000,
|
||||
AT_BANK_COUNTER = 0x0000000080000000,
|
||||
SHOULD_SEND_ARTIFICIAL_ITEM_STATE = 0x0001000000000000,
|
||||
SHOULD_SEND_ARTIFICIAL_FLAG_STATE = 0x0002000000000000,
|
||||
SHOULD_SEND_ENABLE_SAVE = 0x0004000000000000,
|
||||
LOADING = 0x0000000000100000, // Server-side only
|
||||
LOADING_QUEST = 0x0000000000200000, // Server-side only
|
||||
LOADING_RUNNING_JOINABLE_QUEST = 0x0000000000400000, // Server-side only
|
||||
LOADING_TOURNAMENT = 0x0000000000800000, // Server-side only
|
||||
IN_INFORMATION_MENU = 0x0000000001000000, // Server-side only
|
||||
AT_WELCOME_MESSAGE = 0x0000000002000000, // Server-side only
|
||||
SAVE_ENABLED = 0x0000000004000000,
|
||||
HAS_EP3_CARD_DEFS = 0x0000000008000000,
|
||||
HAS_EP3_MEDIA_UPDATES = 0x0000000010000000,
|
||||
USE_OVERRIDE_RANDOM_SEED = 0x0000000020000000,
|
||||
HAS_GUILD_CARD_NUMBER = 0x0000000040000000,
|
||||
AT_BANK_COUNTER = 0x0000000080000000, // Server-side only
|
||||
SHOULD_SEND_ARTIFICIAL_ITEM_STATE = 0x0001000000000000, // Server-side only
|
||||
SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE = 0x0040000000000000, // Server-side only
|
||||
SHOULD_SEND_ARTIFICIAL_OBJECT_STATE = 0x0080000000000000, // Server-side only
|
||||
SHOULD_SEND_ARTIFICIAL_FLAG_STATE = 0x0002000000000000, // Server-side only
|
||||
SHOULD_SEND_ENABLE_SAVE = 0x0004000000000000,
|
||||
SWITCH_ASSIST_ENABLED = 0x0000000100000000,
|
||||
|
||||
// Cheat mode flags
|
||||
SWITCH_ASSIST_ENABLED = 0x0000000100000000,
|
||||
INFINITE_HP_ENABLED = 0x0000000200000000,
|
||||
INFINITE_TP_ENABLED = 0x0000000400000000,
|
||||
DEBUG_ENABLED = 0x0000000800000000,
|
||||
// Cheat mode and option flags
|
||||
INFINITE_HP_ENABLED = 0x0000000200000000,
|
||||
INFINITE_TP_ENABLED = 0x0000000400000000,
|
||||
DEBUG_ENABLED = 0x0000000800000000,
|
||||
ITEM_DROP_NOTIFICATIONS_1 = 0x0010000000000000,
|
||||
ITEM_DROP_NOTIFICATIONS_2 = 0x0020000000000000,
|
||||
|
||||
// Proxy option flags
|
||||
PROXY_SAVE_FILES = 0x0000001000000000,
|
||||
PROXY_CHAT_COMMANDS_ENABLED = 0x0000002000000000,
|
||||
PROXY_CHAT_FILTER_ENABLED = 0x0000004000000000,
|
||||
PROXY_PLAYER_NOTIFICATIONS_ENABLED = 0x0000008000000000,
|
||||
PROXY_SUPPRESS_CLIENT_PINGS = 0x0000010000000000,
|
||||
PROXY_SUPPRESS_REMOTE_LOGIN = 0x0000020000000000,
|
||||
PROXY_ZERO_REMOTE_GUILD_CARD = 0x0000040000000000,
|
||||
PROXY_EP3_INFINITE_MESETA_ENABLED = 0x0000080000000000,
|
||||
PROXY_EP3_INFINITE_TIME_ENABLED = 0x0000100000000000,
|
||||
PROXY_RED_NAME_ENABLED = 0x0000200000000000,
|
||||
PROXY_BLANK_NAME_ENABLED = 0x0000400000000000,
|
||||
PROXY_BLOCK_FUNCTION_CALLS = 0x0000800000000000,
|
||||
PROXY_EP3_UNMASK_WHISPERS = 0x0008000000000000,
|
||||
PROXY_SAVE_FILES = 0x0000001000000000,
|
||||
PROXY_CHAT_COMMANDS_ENABLED = 0x0000002000000000,
|
||||
PROXY_PLAYER_NOTIFICATIONS_ENABLED = 0x0000008000000000,
|
||||
PROXY_SUPPRESS_CLIENT_PINGS = 0x0000010000000000,
|
||||
PROXY_SUPPRESS_REMOTE_LOGIN = 0x0000020000000000,
|
||||
PROXY_ZERO_REMOTE_GUILD_CARD = 0x0000040000000000,
|
||||
PROXY_EP3_INFINITE_MESETA_ENABLED = 0x0000080000000000,
|
||||
PROXY_EP3_INFINITE_TIME_ENABLED = 0x0000100000000000,
|
||||
PROXY_RED_NAME_ENABLED = 0x0000200000000000,
|
||||
PROXY_BLANK_NAME_ENABLED = 0x0000400000000000,
|
||||
PROXY_BLOCK_FUNCTION_CALLS = 0x0000800000000000,
|
||||
PROXY_EP3_UNMASK_WHISPERS = 0x0008000000000000,
|
||||
// clang-format on
|
||||
};
|
||||
enum class ItemDropNotificationMode {
|
||||
NOTHING = 0,
|
||||
RARES_ONLY = 1,
|
||||
ALL_ITEMS = 2,
|
||||
ALL_ITEMS_INCLUDING_MESETA = 3,
|
||||
};
|
||||
|
||||
static constexpr uint64_t DEFAULT_FLAGS = static_cast<uint64_t>(Flag::PROXY_CHAT_COMMANDS_ENABLED) |
|
||||
static_cast<uint64_t>(Flag::PROXY_CHAT_FILTER_ENABLED);
|
||||
static constexpr uint64_t DEFAULT_FLAGS = static_cast<uint64_t>(Flag::PROXY_CHAT_COMMANDS_ENABLED);
|
||||
|
||||
struct Config {
|
||||
uint64_t enabled_flags = DEFAULT_FLAGS; // Client::Flag enum
|
||||
@@ -101,6 +116,8 @@ public:
|
||||
bool operator==(const Config& other) const = default;
|
||||
bool operator!=(const Config& other) const = default;
|
||||
|
||||
bool should_update_vs(const Config& other) const;
|
||||
|
||||
[[nodiscard]] static inline bool check_flag(uint64_t enabled_flags, Flag flag) {
|
||||
return !!(enabled_flags & static_cast<uint64_t>(flag));
|
||||
}
|
||||
@@ -120,6 +137,9 @@ public:
|
||||
|
||||
void set_flags_for_version(Version version, int64_t sub_version);
|
||||
|
||||
ItemDropNotificationMode get_drop_notification_mode() const;
|
||||
void set_drop_notification_mode(ItemDropNotificationMode new_mode);
|
||||
|
||||
template <size_t Bytes>
|
||||
void parse_from(const parray<uint8_t, Bytes>& data) {
|
||||
StringReader r(data.data(), data.size());
|
||||
@@ -141,7 +161,7 @@ public:
|
||||
StringWriter w;
|
||||
w.put_u32l(CLIENT_CONFIG_MAGIC);
|
||||
w.put_u32l(this->specific_version);
|
||||
w.put_u64l(this->enabled_flags);
|
||||
w.put_u64l(this->enabled_flags & static_cast<uint64_t>(Flag::CLIENT_SIDE_MASK));
|
||||
w.put_u32l(this->override_random_seed);
|
||||
w.put_u32b(this->proxy_destination_address);
|
||||
w.put_u16l(this->proxy_destination_port);
|
||||
@@ -177,12 +197,10 @@ public:
|
||||
uint8_t bb_connection_phase;
|
||||
uint64_t ping_start_time;
|
||||
|
||||
// Patch server
|
||||
std::vector<PatchFileChecksumRequest> patch_file_checksum_requests;
|
||||
|
||||
// Lobby/positioning
|
||||
Config config;
|
||||
Config synced_config;
|
||||
std::unique_ptr<parray<le_uint32_t, 0x20>> override_variations;
|
||||
int32_t sub_version;
|
||||
float x;
|
||||
float z;
|
||||
@@ -265,6 +283,7 @@ public:
|
||||
}
|
||||
|
||||
void set_license(std::shared_ptr<License> l);
|
||||
void convert_license_to_temporary_if_nte();
|
||||
|
||||
void sync_config();
|
||||
|
||||
@@ -273,8 +292,16 @@ public:
|
||||
|
||||
std::shared_ptr<const TeamIndex::Team> team() const;
|
||||
|
||||
bool can_see_quest(std::shared_ptr<const Quest> q, uint8_t difficulty, size_t num_players) const;
|
||||
bool can_play_quest(std::shared_ptr<const Quest> q, uint8_t difficulty, size_t num_players) const;
|
||||
bool evaluate_quest_availability_expression(
|
||||
std::shared_ptr<const QuestAvailabilityExpression> expr,
|
||||
uint8_t event,
|
||||
uint8_t difficulty,
|
||||
size_t num_players,
|
||||
bool v1_present) const;
|
||||
bool can_see_quest(std::shared_ptr<const Quest> q, uint8_t event, uint8_t difficulty, size_t num_players, bool v1_present) const;
|
||||
bool can_play_quest(std::shared_ptr<const Quest> q, uint8_t event, uint8_t difficulty, size_t num_players, bool v1_present) const;
|
||||
|
||||
bool can_use_chat_commands() const;
|
||||
|
||||
static void dispatch_save_game_data(evutil_socket_t, short, void* ctx);
|
||||
void save_game_data();
|
||||
@@ -341,6 +368,9 @@ public:
|
||||
void use_character_bank(int8_t bb_character_index);
|
||||
void use_default_bank();
|
||||
|
||||
void print_inventory(FILE* stream) const;
|
||||
void print_bank(FILE* stream) const;
|
||||
|
||||
private:
|
||||
// The overlay character data is used in battle and challenge modes, when
|
||||
// character data is temporarily replaced in-game. In other play modes and in
|
||||
|
||||
+837
-609
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,123 @@
|
||||
#include "CommonItemSet.hh"
|
||||
|
||||
#include "AFSArchive.hh"
|
||||
#include "EnemyType.hh"
|
||||
#include "GSLArchive.hh"
|
||||
#include "StaticGameData.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
template <typename IntT, size_t Count>
|
||||
JSON to_json(const parray<IntT, Count>& v) {
|
||||
auto ret = JSON::list();
|
||||
for (size_t z = 0; z < Count; z++) {
|
||||
ret.emplace_back(v[z]);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <typename IntT, size_t Count>
|
||||
JSON to_json(const parray<CommonItemSet::Table::Range<IntT>, Count>& v) {
|
||||
auto ret = JSON::list();
|
||||
for (size_t z = 0; z < Count; z++) {
|
||||
ret.emplace_back(to_json(v[z]));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <typename IntT>
|
||||
JSON to_json(const CommonItemSet::Table::Range<IntT>& v) {
|
||||
if (v.min == v.max) {
|
||||
return JSON(v.min);
|
||||
} else {
|
||||
return JSON::list({v.min, v.max});
|
||||
}
|
||||
}
|
||||
|
||||
template <typename IntT, size_t Count1, size_t Count2>
|
||||
JSON to_json(const parray<parray<IntT, Count2>, Count1>& v) {
|
||||
auto ret = JSON::list();
|
||||
for (size_t z = 0; z < Count1; z++) {
|
||||
ret.emplace_back(to_json(v[z]));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
JSON CommonItemSet::Table::json() const {
|
||||
JSON enemy_meseta_ranges_json = JSON::dict();
|
||||
JSON enemy_type_drop_probs_json = JSON::dict();
|
||||
JSON enemy_item_classes_json = JSON::dict();
|
||||
for (size_t z = 0; z < 0x64; z++) {
|
||||
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
|
||||
for (Episode episode : episodes) {
|
||||
JSON enemy_meseta_ranges_episode_json = JSON::dict();
|
||||
JSON enemy_type_drop_probs_episode_json = JSON::dict();
|
||||
JSON enemy_item_classes_episode_json = JSON::dict();
|
||||
for (auto type : enemy_types_for_rare_table_index(episode, z)) {
|
||||
string type_str = name_for_enum(type);
|
||||
enemy_meseta_ranges_episode_json.emplace(type_str, to_json(this->enemy_meseta_ranges[z]));
|
||||
enemy_type_drop_probs_episode_json.emplace(type_str, this->enemy_type_drop_probs[z]);
|
||||
enemy_item_classes_episode_json.emplace(type_str, this->enemy_item_classes[z]);
|
||||
}
|
||||
string name = name_for_episode(episode);
|
||||
enemy_meseta_ranges_json.emplace(name, std::move(enemy_meseta_ranges_episode_json));
|
||||
enemy_type_drop_probs_json.emplace(name, std::move(enemy_type_drop_probs_episode_json));
|
||||
enemy_item_classes_json.emplace(name, std::move(enemy_item_classes_episode_json));
|
||||
}
|
||||
}
|
||||
return JSON::dict({
|
||||
{"BaseWeaponTypeProbTable", to_json(this->base_weapon_type_prob_table)},
|
||||
{"SubtypeBaseTable", to_json(this->subtype_base_table)},
|
||||
{"SubtypeAreaLengthTable", to_json(this->subtype_area_length_table)},
|
||||
{"GrindProbTable", to_json(this->grind_prob_table)},
|
||||
{"ArmorShieldTypeIndexProbTable", to_json(this->armor_shield_type_index_prob_table)},
|
||||
{"ArmorSlotCountProbTable", to_json(this->armor_slot_count_prob_table)},
|
||||
{"EnemyMesetaRanges", std::move(enemy_meseta_ranges_json)},
|
||||
{"EnemyTypeDropProbs", std::move(enemy_type_drop_probs_json)},
|
||||
{"EnemyItemClasses", std::move(enemy_item_classes_json)},
|
||||
{"BoxMesetaRanges", to_json(this->box_meseta_ranges)},
|
||||
{"HasRareBonusValueProbTable", this->has_rare_bonus_value_prob_table},
|
||||
{"BonusValueProbTable", to_json(this->bonus_value_prob_table)},
|
||||
{"NonRareBonusProbSpec", to_json(this->nonrare_bonus_prob_spec)},
|
||||
{"BonusTypeProbTable", to_json(this->bonus_type_prob_table)},
|
||||
{"SpecialMult", to_json(this->special_mult)},
|
||||
{"SpecialPercent", to_json(this->special_percent)},
|
||||
{"ToolClassProbTable", to_json(this->tool_class_prob_table)},
|
||||
{"TechniqueIndexProbTable", to_json(this->technique_index_prob_table)},
|
||||
{"TechniqueLevelRanges", to_json(this->technique_level_ranges)},
|
||||
{"ArmorOrShieldTypeBias", this->armor_or_shield_type_bias},
|
||||
{"UnitMaxStarsTable", to_json(this->unit_max_stars_table)},
|
||||
{"BoxItemClassProbTable", to_json(this->box_item_class_prob_table)},
|
||||
});
|
||||
}
|
||||
|
||||
JSON CommonItemSet::json() const {
|
||||
auto modes_dict = JSON::dict();
|
||||
static const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
|
||||
for (const auto& mode : modes) {
|
||||
auto episodes_dict = JSON::dict();
|
||||
static const array<Episode, 3> episodes = {Episode::EP1, Episode::EP2, Episode::EP4};
|
||||
for (const auto& episode : episodes) {
|
||||
auto difficulty_dict = JSON::dict();
|
||||
for (uint8_t difficulty = 0; difficulty < 4; difficulty++) {
|
||||
auto section_id_dict = JSON::dict();
|
||||
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
||||
try {
|
||||
auto table = this->get_table(episode, mode, difficulty, section_id);
|
||||
section_id_dict.emplace(name_for_section_id(section_id), table->json());
|
||||
} catch (const runtime_error&) {
|
||||
}
|
||||
}
|
||||
difficulty_dict.emplace(token_name_for_difficulty(difficulty), std::move(section_id_dict));
|
||||
}
|
||||
episodes_dict.emplace(token_name_for_episode(episode), std::move(difficulty_dict));
|
||||
}
|
||||
modes_dict.emplace(name_for_mode(mode), std::move(episodes_dict));
|
||||
}
|
||||
|
||||
return modes_dict;
|
||||
}
|
||||
|
||||
CommonItemSet::Table::Table(const StringReader& r, bool is_big_endian, bool is_v3) {
|
||||
if (is_big_endian) {
|
||||
this->parse_itempt_t<true>(r, is_v3);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/JSON.hh>
|
||||
|
||||
#include "GSLArchive.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
@@ -45,6 +46,7 @@ public:
|
||||
parray<parray<uint8_t, 10>, 7> box_item_class_prob_table;
|
||||
|
||||
void print_enemy_table(FILE* stream) const;
|
||||
JSON json() const;
|
||||
|
||||
private:
|
||||
template <bool IsBigEndian>
|
||||
@@ -256,6 +258,7 @@ public:
|
||||
};
|
||||
|
||||
std::shared_ptr<const Table> get_table(Episode episode, GameMode mode, uint8_t difficulty, uint8_t secid) const;
|
||||
JSON json() const;
|
||||
|
||||
protected:
|
||||
CommonItemSet() = default;
|
||||
@@ -298,22 +301,22 @@ struct ProbabilityTable {
|
||||
return this->items[--this->count];
|
||||
}
|
||||
|
||||
void shuffle(PSOLFGEncryption& random_crypt) {
|
||||
void shuffle(std::shared_ptr<PSOLFGEncryption> opt_rand_crypt) {
|
||||
for (size_t z = 1; z < this->count; z++) {
|
||||
size_t other_z = random_crypt.next() % (z + 1);
|
||||
size_t other_z = random_from_optional_crypt(opt_rand_crypt) % (z + 1);
|
||||
ItemT t = this->items[z];
|
||||
this->items[z] = this->items[other_z];
|
||||
this->items[other_z] = t;
|
||||
}
|
||||
}
|
||||
|
||||
ItemT sample(PSOLFGEncryption& random_crypt) const {
|
||||
ItemT sample(std::shared_ptr<PSOLFGEncryption> opt_rand_crypt) const {
|
||||
if (this->count == 0) {
|
||||
throw std::runtime_error("sample from empty probability table");
|
||||
} else if (this->count == 1) {
|
||||
return this->items[0];
|
||||
} else {
|
||||
return this->items[random_crypt.next() % this->count];
|
||||
return this->items[random_from_optional_crypt(opt_rand_crypt) % this->count];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+15
-2
@@ -834,9 +834,11 @@ uint8_t battle_param_index_for_enemy_type(Episode episode, EnemyType enemy_type)
|
||||
case EnemyType::GIRTABLULU:
|
||||
return 0x1F;
|
||||
case EnemyType::SAINT_MILLION:
|
||||
case EnemyType::SHAMBERTIN:
|
||||
case EnemyType::KONDRIEU:
|
||||
return 0x22;
|
||||
case EnemyType::SHAMBERTIN:
|
||||
return 0x26;
|
||||
case EnemyType::KONDRIEU:
|
||||
return 0x2A;
|
||||
default:
|
||||
throw runtime_error(string_printf("%s does not have battle parameters in Episode 4", name_for_enum(enemy_type)));
|
||||
}
|
||||
@@ -1094,3 +1096,14 @@ const vector<EnemyType>& enemy_types_for_rare_table_index(Episode episode, uint8
|
||||
return empty_vec;
|
||||
}
|
||||
}
|
||||
|
||||
bool enemy_type_is_rare(EnemyType type) {
|
||||
return ((type == EnemyType::HILDEBLUE) ||
|
||||
(type == EnemyType::AL_RAPPY) ||
|
||||
(type == EnemyType::NAR_LILY) ||
|
||||
(type == EnemyType::POUILLY_SLIME) ||
|
||||
(type == EnemyType::MERISSA_AA) ||
|
||||
(type == EnemyType::PAZUZU_ALT) ||
|
||||
(type == EnemyType::DORPHON_ECLAIR) ||
|
||||
(type == EnemyType::KONDRIEU));
|
||||
}
|
||||
|
||||
@@ -145,3 +145,4 @@ bool enemy_type_valid_for_episode(Episode episode, EnemyType enemy_type);
|
||||
uint8_t battle_param_index_for_enemy_type(Episode episode, EnemyType enemy_type);
|
||||
uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type);
|
||||
const std::vector<EnemyType>& enemy_types_for_rare_table_index(Episode episode, uint8_t rt_index);
|
||||
bool enemy_type_is_rare(EnemyType type);
|
||||
|
||||
@@ -6,21 +6,38 @@ using namespace std;
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
// Note: This order matches the order that the cards are defined in the original
|
||||
// code. This is relevant for consistency of results when choosing a random card
|
||||
// (for God Whim).
|
||||
const vector<uint16_t> ALL_ASSIST_CARD_IDS = {
|
||||
0x0018, 0x0019, 0x001A, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA,
|
||||
0x00FB, 0x00FC, 0x00FD, 0x00FE, 0x00FF, 0x0100, 0x0101, 0x0102, 0x0103,
|
||||
0x0104, 0x0105, 0x0106, 0x0107, 0x0108, 0x0109, 0x010A, 0x010B, 0x010C,
|
||||
0x010D, 0x010E, 0x010F, 0x0121, 0x0125, 0x0126, 0x0127, 0x0128, 0x0129,
|
||||
0x012A, 0x012B, 0x012C, 0x012D, 0x012E, 0x012F, 0x0130, 0x0131, 0x0132,
|
||||
0x0133, 0x0134, 0x0135, 0x0136, 0x0137, 0x0138, 0x0139, 0x013A, 0x013B,
|
||||
0x013C, 0x013D, 0x013E, 0x013F, 0x0140, 0x0141, 0x0142, 0x0143, 0x0144,
|
||||
0x0145, 0x0146, 0x0148, 0x014A, 0x014B, 0x014C, 0x014D, 0x014E, 0x023F,
|
||||
0x0240, 0x0241, 0x0242};
|
||||
const vector<uint16_t>& all_assist_card_ids(bool is_nte) {
|
||||
// Note: This order matches the order that the cards are defined in the original
|
||||
// code. This is relevant for consistency of results when choosing a random card
|
||||
// (for God Whim).
|
||||
static const vector<uint16_t> ALL_ASSIST_CARD_IDS_TRIAL = {
|
||||
0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x00FD,
|
||||
0x00FE, 0x00FF, 0x0100, 0x0101, 0x0102, 0x0103, 0x0104, 0x0105, 0x0106,
|
||||
0x0107, 0x0108, 0x0109, 0x010A, 0x010B, 0x010C, 0x010D, 0x010E, 0x010F,
|
||||
0x0121, 0x0125, 0x0126, 0x0127, 0x0128, 0x0129, 0x012A, 0x012B, 0x012C,
|
||||
0x012D, 0x012E, 0x012F, 0x0130, 0x0131, 0x0132, 0x0133, 0x0134, 0x0135,
|
||||
0x0136, 0x0137, 0x0138, 0x0139, 0x013A, 0x013B, 0x013C, 0x013D, 0x013E,
|
||||
0x013F, 0x0140, 0x0141, 0x0142, 0x0143, 0x0144, 0x0145, 0x0146, 0x0148,
|
||||
0x014A, 0x014B, 0x014C, 0x014D, 0x014E, 0x023F, 0x0240, 0x0241, 0x0242};
|
||||
static const vector<uint16_t> ALL_ASSIST_CARD_IDS_FINAL = {
|
||||
0x0018, 0x0019, 0x001A, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA,
|
||||
0x00FB, 0x00FC, 0x00FD, 0x00FE, 0x00FF, 0x0100, 0x0101, 0x0102, 0x0103,
|
||||
0x0104, 0x0105, 0x0106, 0x0107, 0x0108, 0x0109, 0x010A, 0x010B, 0x010C,
|
||||
0x010D, 0x010E, 0x010F, 0x0121, 0x0125, 0x0126, 0x0127, 0x0128, 0x0129,
|
||||
0x012A, 0x012B, 0x012C, 0x012D, 0x012E, 0x012F, 0x0130, 0x0131, 0x0132,
|
||||
0x0133, 0x0134, 0x0135, 0x0136, 0x0137, 0x0138, 0x0139, 0x013A, 0x013B,
|
||||
0x013C, 0x013D, 0x013E, 0x013F, 0x0140, 0x0141, 0x0142, 0x0143, 0x0144,
|
||||
0x0145, 0x0146, 0x0148, 0x014A, 0x014B, 0x014C, 0x014D, 0x014E, 0x023F,
|
||||
0x0240, 0x0241, 0x0242};
|
||||
return is_nte ? ALL_ASSIST_CARD_IDS_TRIAL : ALL_ASSIST_CARD_IDS_FINAL;
|
||||
}
|
||||
|
||||
AssistEffect assist_effect_number_for_card_id(uint16_t card_id) {
|
||||
AssistEffect assist_effect_number_for_card_id(uint16_t card_id, bool is_nte) {
|
||||
static const unordered_map<uint16_t, AssistEffect> card_id_to_effect_final_only({
|
||||
{0x0018, /* 0x0049 */ AssistEffect::DICE_FEVER_PLUS},
|
||||
{0x0019, /* 0x004A */ AssistEffect::RICH_PLUS},
|
||||
{0x001A, /* 0x004B */ AssistEffect::CHARITY_PLUS},
|
||||
});
|
||||
static const unordered_map<uint16_t, AssistEffect> card_id_to_effect({
|
||||
{0x00F5, /* 0x0001 */ AssistEffect::DICE_HALF},
|
||||
{0x00F6, /* 0x0002 */ AssistEffect::DICE_PLUS_1},
|
||||
@@ -94,15 +111,18 @@ AssistEffect assist_effect_number_for_card_id(uint16_t card_id) {
|
||||
{0x0240, /* 0x0046 */ AssistEffect::BOMB},
|
||||
{0x0241, /* 0x0047 */ AssistEffect::SKIP_TURN},
|
||||
{0x0242, /* 0x0048 */ AssistEffect::BATTLE_ROYALE},
|
||||
{0x0018, /* 0x0049 */ AssistEffect::DICE_FEVER_PLUS},
|
||||
{0x0019, /* 0x004A */ AssistEffect::RICH_PLUS},
|
||||
{0x001A, /* 0x004B */ AssistEffect::CHARITY_PLUS},
|
||||
});
|
||||
try {
|
||||
return card_id_to_effect.at(card_id);
|
||||
} catch (const out_of_range&) {
|
||||
return AssistEffect::NONE;
|
||||
}
|
||||
if (!is_nte) {
|
||||
try {
|
||||
return card_id_to_effect_final_only.at(card_id);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
}
|
||||
return AssistEffect::NONE;
|
||||
}
|
||||
|
||||
AssistServer::AssistServer(shared_ptr<Server> server)
|
||||
@@ -224,6 +244,7 @@ AssistEffect AssistServer::get_active_assist_by_index(size_t index) const {
|
||||
}
|
||||
|
||||
void AssistServer::populate_effects() {
|
||||
bool is_nte = this->server()->options.is_nte();
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
this->assist_card_defs[z] = nullptr;
|
||||
this->assist_effects[z] = AssistEffect::NONE;
|
||||
@@ -232,7 +253,7 @@ void AssistServer::populate_effects() {
|
||||
uint16_t card_id = hes->assist_card_id == 0xFFFF
|
||||
? this->card_id_for_card_ref(hes->assist_card_ref)
|
||||
: hes->assist_card_id.load();
|
||||
this->assist_effects[z] = assist_effect_number_for_card_id(card_id);
|
||||
this->assist_effects[z] = assist_effect_number_for_card_id(card_id, is_nte);
|
||||
if (this->assist_effects[z] != AssistEffect::NONE) {
|
||||
this->assist_card_defs[z] = this->definition_for_card_id(card_id);
|
||||
}
|
||||
|
||||
@@ -13,9 +13,8 @@ namespace Episode3 {
|
||||
|
||||
class Server;
|
||||
|
||||
extern const std::vector<uint16_t> ALL_ASSIST_CARD_IDS;
|
||||
|
||||
AssistEffect assist_effect_number_for_card_id(uint16_t card_id);
|
||||
const std::vector<uint16_t>& all_assist_card_ids(bool is_nte);
|
||||
AssistEffect assist_effect_number_for_card_id(uint16_t card_id, bool is_nte);
|
||||
|
||||
class AssistServer {
|
||||
public:
|
||||
@@ -40,17 +39,17 @@ private:
|
||||
|
||||
public:
|
||||
parray<AssistEffect, 4> assist_effects;
|
||||
std::shared_ptr<const CardIndex::CardEntry> assist_card_defs[4];
|
||||
bcarray<std::shared_ptr<const CardIndex::CardEntry>, 4> assist_card_defs;
|
||||
uint32_t num_assist_cards_set;
|
||||
parray<uint8_t, 4> client_ids_with_assists;
|
||||
parray<AssistEffect, 4> active_assist_effects;
|
||||
std::shared_ptr<const CardIndex::CardEntry> active_assist_card_defs[4];
|
||||
bcarray<std::shared_ptr<const CardIndex::CardEntry>, 4> active_assist_card_defs;
|
||||
uint32_t num_active_assists;
|
||||
std::shared_ptr<HandAndEquipState> hand_and_equip_states[4];
|
||||
std::shared_ptr<parray<CardShortStatus, 0x10>> card_short_statuses[4];
|
||||
std::shared_ptr<DeckEntry> deck_entries[4];
|
||||
std::shared_ptr<parray<ActionChainWithConds, 9>> set_card_action_chains[4];
|
||||
std::shared_ptr<parray<ActionMetadata, 9>> set_card_action_metadatas[4];
|
||||
bcarray<std::shared_ptr<HandAndEquipState>, 4> hand_and_equip_states;
|
||||
bcarray<std::shared_ptr<parray<CardShortStatus, 0x10>>, 4> card_short_statuses;
|
||||
bcarray<std::shared_ptr<DeckEntry>, 4> deck_entries;
|
||||
bcarray<std::shared_ptr<parray<ActionChainWithConds, 9>>, 4> set_card_action_chains;
|
||||
bcarray<std::shared_ptr<parray<ActionMetadata, 9>>, 4> set_card_action_metadatas;
|
||||
};
|
||||
|
||||
} // namespace Episode3
|
||||
|
||||
@@ -191,7 +191,7 @@ bool BattleRecord::is_map_definition_event(const Event& ev) {
|
||||
if (ev.type == Event::Type::BATTLE_COMMAND) {
|
||||
auto& header = check_size_t<G_CardBattleCommandHeader>(ev.data, 0xFFFF);
|
||||
if (header.subcommand == 0xB6) {
|
||||
auto& header = check_size_t<G_MapSubsubcommand_GC_Ep3_6xB6>(ev.data, 0xFFFF);
|
||||
auto& header = check_size_t<G_MapSubsubcommand_Ep3_6xB6>(ev.data, 0xFFFF);
|
||||
if (header.subsubcommand == 0x41) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -96,9 +96,7 @@ private:
|
||||
|
||||
class BattleRecordPlayer {
|
||||
public:
|
||||
BattleRecordPlayer(
|
||||
std::shared_ptr<const BattleRecord> rec,
|
||||
std::shared_ptr<struct event_base> base);
|
||||
BattleRecordPlayer(std::shared_ptr<const BattleRecord> rec, std::shared_ptr<struct event_base> base);
|
||||
~BattleRecordPlayer() = default;
|
||||
|
||||
std::shared_ptr<const BattleRecord> get_record() const;
|
||||
|
||||
+563
-297
File diff suppressed because it is too large
Load Diff
+19
-23
@@ -15,11 +15,7 @@ class PlayerState;
|
||||
|
||||
class Card : public std::enable_shared_from_this<Card> {
|
||||
public:
|
||||
Card(
|
||||
uint16_t card_id,
|
||||
uint16_t card_ref,
|
||||
uint16_t client_id,
|
||||
std::shared_ptr<Server> server);
|
||||
Card(uint16_t card_id, uint16_t card_ref, uint16_t client_id, std::shared_ptr<Server> server);
|
||||
void init();
|
||||
std::shared_ptr<Server> server();
|
||||
std::shared_ptr<const Server> server() const;
|
||||
@@ -34,29 +30,31 @@ public:
|
||||
int16_t value,
|
||||
int8_t dice_roll_value,
|
||||
int8_t random_percent);
|
||||
void apply_ap_adjust_assists_to_attack(
|
||||
void apply_ap_and_tp_adjust_assists_to_attack(
|
||||
std::shared_ptr<const Card> attacker_card,
|
||||
int16_t* inout_attacker_ap,
|
||||
int16_t* in_defense_power) const;
|
||||
int16_t* in_defense_power,
|
||||
int16_t* inout_attacker_tp) const;
|
||||
bool card_type_is_sc_or_creature() const;
|
||||
bool check_card_flag(uint32_t flags) const;
|
||||
void commit_attack(
|
||||
int16_t damage,
|
||||
std::shared_ptr<Card> attacker_card,
|
||||
G_ApplyConditionEffect_GC_Ep3_6xB4x06* cmd,
|
||||
G_ApplyConditionEffect_Ep3_6xB4x06* cmd,
|
||||
size_t strike_number,
|
||||
int16_t* out_effective_damage);
|
||||
int16_t compute_defense_power_for_attacker_card(
|
||||
std::shared_ptr<const Card> attacker_card);
|
||||
int16_t compute_defense_power_for_attacker_card(std::shared_ptr<const Card> attacker_card);
|
||||
void destroy_set_card(std::shared_ptr<Card> attacker_card);
|
||||
int32_t error_code_for_move_to_location(const Location& loc) const;
|
||||
void execute_attack(std::shared_ptr<Card> attacker_card);
|
||||
bool get_attack_condition_value(
|
||||
bool get_condition_value(
|
||||
ConditionType cond_type,
|
||||
uint16_t card_ref,
|
||||
uint8_t def_effect_index,
|
||||
uint16_t value,
|
||||
uint16_t* out_value) const;
|
||||
uint16_t card_ref = 0xFFFF,
|
||||
uint8_t def_effect_index = 0xFF,
|
||||
uint16_t value = 0xFFFF,
|
||||
uint16_t* out_value = nullptr) const;
|
||||
Condition* find_condition(ConditionType cond_type);
|
||||
const Condition* find_condition(ConditionType cond_type) const;
|
||||
std::shared_ptr<const CardIndex::CardEntry> get_definition() const;
|
||||
uint16_t get_card_ref() const;
|
||||
uint16_t get_card_id() const;
|
||||
@@ -70,23 +68,21 @@ public:
|
||||
void send_6xB4x4E_4C_4D_if_needed(bool always_send = false);
|
||||
void send_6xB4x4E_if_needed(bool always_send = false);
|
||||
void set_current_and_max_hp(int16_t hp);
|
||||
void set_current_hp(
|
||||
uint32_t new_hp, bool propagate_shared_hp = true, bool enforce_max_hp = true);
|
||||
void set_current_hp(uint32_t new_hp, bool propagate_shared_hp = true, bool enforce_max_hp = true);
|
||||
void update_stats_on_destruction();
|
||||
void clear_action_chain_and_metadata_and_most_flags();
|
||||
void compute_action_chain_results(
|
||||
bool apply_action_conditions, bool ignore_this_card_ap_tp);
|
||||
void compute_action_chain_results(bool apply_action_conditions, bool ignore_this_card_ap_tp);
|
||||
void unknown_802380C0();
|
||||
void unknown_80237F98(bool require_condition_20_or_21);
|
||||
void unknown_80237F88();
|
||||
void unknown_80235AA0();
|
||||
void unknown_80235AD4();
|
||||
void unknown_80235B10();
|
||||
void draw_phase_before();
|
||||
void action_phase_before();
|
||||
void move_phase_before();
|
||||
void unknown_80236374(std::shared_ptr<Card> other_card, const ActionState* as);
|
||||
void unknown_802379BC(uint16_t card_ref);
|
||||
void unknown_802379DC(const ActionState& pa);
|
||||
void unknown_80237A90(const ActionState& pa, uint16_t action_card_ref);
|
||||
void unknown_8023813C();
|
||||
void dice_phase_before();
|
||||
bool is_guard_item() const;
|
||||
bool unknown_80236554(std::shared_ptr<Card> other_card, const ActionState* as);
|
||||
void unknown_802362D8(std::shared_ptr<Card> other_card);
|
||||
|
||||
+970
-653
File diff suppressed because it is too large
Load Diff
+50
-50
@@ -45,45 +45,49 @@ public:
|
||||
};
|
||||
|
||||
struct AttackEnvStats {
|
||||
uint32_t num_set_cards; // "f" in expr
|
||||
uint32_t dice_roll_value1; // "d" in expr
|
||||
uint32_t effective_ap; // "ap" in expr
|
||||
uint32_t effective_tp; // "tp" in expr
|
||||
uint32_t current_hp; // "hp" in expr
|
||||
uint32_t max_hp; // "mhp" in expr
|
||||
uint32_t effective_ap_if_not_tech; // "dm" in expr
|
||||
uint32_t effective_ap_if_not_physical; // "tdm" in expr
|
||||
uint32_t player_num_destroyed_fcs; // "tf" in expr
|
||||
uint32_t player_num_atk_points; // "ac" in expr
|
||||
uint32_t defined_max_hp; // "php" in expr
|
||||
uint32_t dice_roll_value2; // "dc" in expr
|
||||
uint32_t card_cost; // "cs" in expr
|
||||
uint32_t total_num_set_cards; // "a" in expr
|
||||
uint32_t action_cards_ap; // "kap" in expr
|
||||
uint32_t action_cards_tp; // "ktp" in expr
|
||||
uint32_t unknown_a1; // "dn" in expr
|
||||
uint32_t num_item_or_creature_cards_in_hand; // "hf" in expr
|
||||
uint32_t num_destroyed_ally_fcs; // "df" in expr
|
||||
uint32_t target_team_num_set_cards; // "ff" in expr
|
||||
uint32_t condition_giver_team_num_set_cards; // "ef" in expr
|
||||
uint32_t num_native_creatures; // "bi" in expr
|
||||
uint32_t num_a_beast_creatures; // "ab" in expr
|
||||
uint32_t num_machine_creatures; // "mc" in expr
|
||||
uint32_t num_dark_creatures; // "dk" in expr
|
||||
uint32_t num_sword_type_items; // "sa" in expr
|
||||
uint32_t num_gun_type_items; // "gn" in expr
|
||||
uint32_t num_cane_type_items; // "wd" in expr
|
||||
uint32_t effective_ap_if_not_tech2; // "tt" in expr
|
||||
uint32_t team_dice_boost; // "lv" in expr
|
||||
uint32_t sc_effective_ap; // "adm" in expr
|
||||
uint32_t attack_bonus; // "ddm" in expr
|
||||
uint32_t num_sword_type_items_on_team; // "sat" in expr
|
||||
uint32_t target_attack_bonus; // "edm" in expr
|
||||
uint32_t last_attack_preliminary_damage; // "ldm" in expr
|
||||
uint32_t last_attack_damage; // "rdm" in expr
|
||||
uint32_t total_last_attack_damage; // "fdm" in expr
|
||||
uint32_t last_attack_damage_count; // "ndm" in expr
|
||||
uint32_t target_current_hp; // "ehp" in expr
|
||||
/* 00 */ uint32_t num_set_cards; // "f" in expr
|
||||
/* 04 */ uint32_t dice_roll_value1; // "d" in expr
|
||||
/* 08 */ uint32_t effective_ap; // "ap" in expr
|
||||
/* 0C */ uint32_t effective_tp; // "tp" in expr
|
||||
/* 10 */ uint32_t current_hp; // "hp" in expr
|
||||
/* 14 */ uint32_t max_hp; // "mhp" in expr
|
||||
/* 18 */ uint32_t effective_ap_if_not_tech; // "dm" in expr
|
||||
/* 1C */ uint32_t effective_ap_if_not_physical; // "tdm" in expr
|
||||
/* 20 */ uint32_t player_num_destroyed_fcs; // "tf" in expr
|
||||
/* 24 */ uint32_t player_num_atk_points; // "ac" in expr
|
||||
/* 28 */ uint32_t defined_max_hp; // "php" in expr
|
||||
/* 2C */ uint32_t dice_roll_value2; // "dc" in expr
|
||||
/* 30 */ uint32_t card_cost; // "cs" in expr
|
||||
/* 34 */ uint32_t total_num_set_cards; // "a" in expr
|
||||
/* 38 */ uint32_t action_cards_ap; // "kap" in expr
|
||||
/* 3C */ uint32_t action_cards_tp; // "ktp" in expr
|
||||
/* 40 */ uint32_t unknown_a1; // "dn" in expr
|
||||
/* 44 */ uint32_t num_item_or_creature_cards_in_hand; // "hf" in expr
|
||||
/* 48 */ uint32_t num_destroyed_ally_fcs; // "df" in expr
|
||||
/* 4C */ uint32_t target_team_num_set_cards; // "ff" in expr
|
||||
/* 50 */ uint32_t non_target_team_num_set_cards; // "ef" in expr
|
||||
/* 54 */ uint32_t num_native_creatures; // "bi" in expr
|
||||
/* 58 */ uint32_t num_a_beast_creatures; // "ab" in expr
|
||||
/* 5C */ uint32_t num_machine_creatures; // "mc" in expr
|
||||
/* 60 */ uint32_t num_dark_creatures; // "dk" in expr
|
||||
/* 64 */ uint32_t num_sword_type_items; // "sa" in expr
|
||||
/* 68 */ uint32_t num_gun_type_items; // "gn" in expr
|
||||
/* 6C */ uint32_t num_cane_type_items; // "wd" in expr
|
||||
/* 70 */ uint32_t effective_ap_if_not_tech2; // "tt" in expr
|
||||
/* 74 */ uint32_t team_dice_bonus; // "lv" in expr
|
||||
/* 78 */ uint32_t sc_effective_ap; // "adm" in expr
|
||||
// The following fields do not exist in Trial Edition. Because this struct
|
||||
// is never sent to the client, we use the full struct even when playing
|
||||
// Trial Edition, just for simplicity.
|
||||
/* 7C */ uint32_t attack_bonus; // "ddm" in expr
|
||||
/* 80 */ uint32_t num_sword_type_items_on_team; // "sat" in expr
|
||||
/* 84 */ uint32_t target_attack_bonus; // "edm" in expr
|
||||
/* 88 */ uint32_t last_attack_preliminary_damage; // "ldm" in expr
|
||||
/* 8C */ uint32_t last_attack_damage; // "rdm" in expr
|
||||
/* 90 */ uint32_t total_last_attack_damage; // "fdm" in expr
|
||||
/* 94 */ uint32_t last_attack_damage_count; // "ndm" in expr
|
||||
/* 98 */ uint32_t target_current_hp; // "ehp" in expr
|
||||
/* 9C */
|
||||
|
||||
AttackEnvStats();
|
||||
void clear();
|
||||
@@ -159,7 +163,7 @@ public:
|
||||
uint32_t* unknown_p11,
|
||||
uint16_t sc_card_ref);
|
||||
StatSwapType compute_stat_swap_type(std::shared_ptr<const Card> card) const;
|
||||
void compute_team_dice_boost(uint8_t team_id);
|
||||
void compute_team_dice_bonus(uint8_t team_id);
|
||||
bool condition_has_when_20_or_21(const Condition& cond) const;
|
||||
size_t count_action_cards_with_condition_for_all_current_attacks(
|
||||
ConditionType cond_type, uint16_t card_ref) const;
|
||||
@@ -309,14 +313,13 @@ public:
|
||||
const Location& card1_loc,
|
||||
std::shared_ptr<const Card> card2) const;
|
||||
void unknown_8024AAB8(const ActionState& as);
|
||||
void unknown_80244BE4(std::shared_ptr<Card> unknown_p2);
|
||||
void unknown_80244CA8(std::shared_ptr<Card> card);
|
||||
void move_phase_before_for_card(std::shared_ptr<Card> unknown_p2);
|
||||
void dice_phase_before_for_card(std::shared_ptr<Card> card);
|
||||
template <uint8_t When1, uint8_t When2>
|
||||
void unknown1_t(
|
||||
std::shared_ptr<Card> unknown_p2, const ActionState* existing_as = nullptr);
|
||||
void unknown_80249060(std::shared_ptr<Card> unknown_p2);
|
||||
void unknown_80249254(std::shared_ptr<Card> unknown_p2);
|
||||
void unknown_8024945C(std::shared_ptr<Card> unknown_p2, const ActionState& existing_as);
|
||||
void apply_effects_on_phase_change_t(std::shared_ptr<Card> unknown_p2, const ActionState* existing_as = nullptr);
|
||||
void draw_phase_before_for_card(std::shared_ptr<Card> unknown_p2);
|
||||
void action_phase_before_for_card(std::shared_ptr<Card> unknown_p2);
|
||||
void unknown_8024945C(std::shared_ptr<Card> unknown_p2, const ActionState* existing_as);
|
||||
void unknown_8024966C(std::shared_ptr<Card> unknown_p2, const ActionState* existing_as);
|
||||
static std::shared_ptr<Card> sc_card_for_card(std::shared_ptr<Card> unknown_p2);
|
||||
void unknown_8024A9D8(const ActionState& pa, uint16_t action_card_ref);
|
||||
@@ -333,9 +336,6 @@ public:
|
||||
|
||||
private:
|
||||
std::weak_ptr<Server> w_server;
|
||||
ActionState unknown_action_state_a1;
|
||||
ActionState action_state;
|
||||
uint16_t unknown_a2;
|
||||
};
|
||||
|
||||
} // namespace Episode3
|
||||
|
||||
+690
-315
File diff suppressed because it is too large
Load Diff
+83
-56
@@ -15,7 +15,7 @@
|
||||
|
||||
#include "../PlayerSubordinates.hh"
|
||||
#include "../Text.hh"
|
||||
#include "../TextArchive.hh"
|
||||
#include "../TextIndex.hh"
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
@@ -36,6 +36,8 @@ enum BehaviorFlag : uint32_t {
|
||||
DISABLE_MASKING = 0x00000080,
|
||||
DISABLE_INTERFERENCE = 0x00000100,
|
||||
ALLOW_NON_COM_INTERFERENCE = 0x00000200,
|
||||
IS_TRIAL_EDITION = 0x00000400,
|
||||
LOG_COMMANDS_IF_LOBBY_MISSING = 0x00000800,
|
||||
};
|
||||
|
||||
enum class StatSwapType : uint8_t {
|
||||
@@ -58,8 +60,6 @@ enum class AttackMedium : uint8_t {
|
||||
INVALID_FF = 0xFF,
|
||||
};
|
||||
|
||||
const char* name_for_attack_medium(AttackMedium medium);
|
||||
|
||||
enum class CriterionCode : uint8_t {
|
||||
NONE = 0x00,
|
||||
HU_CLASS_SC = 0x01,
|
||||
@@ -98,8 +98,6 @@ enum class CriterionCode : uint8_t {
|
||||
NON_PHYSICAL_NON_TECH_NON_UNKNOWN_ATTACK_MEDIUM_NON_SC = 0x22,
|
||||
};
|
||||
|
||||
const char* name_for_criterion_code(CriterionCode code);
|
||||
|
||||
enum class CardRank : uint8_t {
|
||||
N1 = 0x01,
|
||||
R1 = 0x02,
|
||||
@@ -137,8 +135,6 @@ enum class CardType : uint8_t {
|
||||
END_CARD_LIST = 0xFF,
|
||||
};
|
||||
|
||||
const char* name_for_card_type(CardType type);
|
||||
|
||||
enum class CardClass : uint16_t {
|
||||
HU_SC = 0x0000,
|
||||
RA_SC = 0x0001,
|
||||
@@ -162,8 +158,7 @@ enum class CardClass : uint16_t {
|
||||
ASSIST = 0x0028,
|
||||
};
|
||||
|
||||
const char* name_for_card_class(CardClass cc);
|
||||
bool card_class_is_tech_like(CardClass cc);
|
||||
bool card_class_is_tech_like(CardClass cc, bool is_nte);
|
||||
|
||||
enum class TargetMode : uint8_t {
|
||||
NONE = 0x00, // Used for defense cards, mags, shields, etc.
|
||||
@@ -299,7 +294,7 @@ enum class ConditionType : uint8_t {
|
||||
UNKNOWN_75 = 0x75,
|
||||
REFLECT = 0x76, // Generate reverse attack
|
||||
UNKNOWN_77 = 0x77,
|
||||
ANY = 0x78, // Not a real condition; used as a wildcard in search functions
|
||||
ANY = 0x78, // Not a real condition; used as a wildcard in search functions. Has value 0x64 on NTE
|
||||
UNKNOWN_79 = 0x79,
|
||||
UNKNOWN_7A = 0x7A,
|
||||
UNKNOWN_7B = 0x7B,
|
||||
@@ -309,8 +304,6 @@ enum class ConditionType : uint8_t {
|
||||
ANY_FF = 0xFF, // Used as a wildcard in some search functions
|
||||
};
|
||||
|
||||
const char* name_for_condition_type(ConditionType cond_type);
|
||||
|
||||
enum class AssistEffect : uint16_t {
|
||||
NONE = 0x0000,
|
||||
DICE_HALF = 0x0001,
|
||||
@@ -407,8 +400,6 @@ enum class ActionSubphase : uint8_t {
|
||||
INVALID_FF = 0xFF,
|
||||
};
|
||||
|
||||
const char* name_for_action_subphase(ActionSubphase subphase);
|
||||
|
||||
enum class SetupPhase : uint8_t {
|
||||
REGISTRATION = 0,
|
||||
STARTER_ROLLS = 1,
|
||||
@@ -438,7 +429,6 @@ enum class Direction : uint8_t {
|
||||
Direction turn_left(Direction d);
|
||||
Direction turn_right(Direction d);
|
||||
Direction turn_around(Direction d);
|
||||
const char* name_for_direction(Direction d);
|
||||
|
||||
struct Location {
|
||||
/* 00 */ uint8_t x;
|
||||
@@ -479,6 +469,7 @@ struct CardDefinition {
|
||||
|
||||
void decode_code();
|
||||
std::string str() const;
|
||||
JSON json() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct Effect {
|
||||
@@ -514,11 +505,12 @@ struct CardDefinition {
|
||||
|
||||
bool is_empty() const;
|
||||
static std::string str_for_arg(const std::string& arg);
|
||||
std::string str(const char* separator = ", ", const TextArchive* text_archive = nullptr) const;
|
||||
std::string str(const char* separator = ", ", const TextSet* text_archive = nullptr) const;
|
||||
JSON json() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
/* 0000 */ be_uint32_t card_id;
|
||||
/* 0004 */ parray<uint8_t, 0x40> jp_name;
|
||||
/* 0004 */ pstring<TextEncoding::SJIS, 0x40> jp_name;
|
||||
|
||||
// The list of card definitions ends with a "sentinel" definition that isn't a
|
||||
// real card, but instead has a negative number in the type field here.
|
||||
@@ -764,9 +756,9 @@ struct CardDefinition {
|
||||
// enormous comment? That's what this array stores.
|
||||
/* 009C */ parray<be_uint16_t, 2> drop_rates;
|
||||
|
||||
/* 00A0 */ pstring<TextEncoding::SJIS, 0x14> en_name;
|
||||
/* 00A0 */ pstring<TextEncoding::ISO8859, 0x14> en_name;
|
||||
/* 00B4 */ pstring<TextEncoding::SJIS, 0x0B> jp_short_name;
|
||||
/* 00BF */ pstring<TextEncoding::SJIS, 0x08> en_short_name;
|
||||
/* 00BF */ pstring<TextEncoding::ISO8859, 0x08> en_short_name;
|
||||
// These effects modify the card's behavior in various situations. Only
|
||||
// effects for which effect_num is not zero are used.
|
||||
/* 00C7 */ parray<Effect, 3> effects;
|
||||
@@ -780,7 +772,8 @@ struct CardDefinition {
|
||||
CardClass card_class() const;
|
||||
|
||||
void decode_range();
|
||||
std::string str(bool single_line = true, const TextArchive* text_archive = nullptr) const;
|
||||
std::string str(bool single_line = true, const TextSet* text_archive = nullptr) const;
|
||||
JSON json() const;
|
||||
} __attribute__((packed)); // 0x128 bytes in total
|
||||
|
||||
struct CardDefinitionsFooter {
|
||||
@@ -800,7 +793,7 @@ struct CardDefinitionsFooter {
|
||||
} __attribute__((packed));
|
||||
|
||||
struct DeckDefinition {
|
||||
/* 00 */ pstring<TextEncoding::SJIS, 0x10> name;
|
||||
/* 00 */ pstring<TextEncoding::MARKED, 0x10> name;
|
||||
/* 10 */ be_uint32_t client_id; // 0-3
|
||||
// List of card IDs. The card count is the number of nonzero entries here
|
||||
// before a zero entry (or 50 if no entries are nonzero). The first card ID is
|
||||
@@ -823,11 +816,11 @@ struct PlayerConfig {
|
||||
// The game splits this internally into two structures. The first column of
|
||||
// offsets is relative to the start of the first structure; the second column
|
||||
// is relative to the start of the second structure.
|
||||
/* 0000:---- */ pstring<TextEncoding::SJIS, 12> rank_text; // From B7 command
|
||||
/* 0000:---- */ pstring<TextEncoding::MARKED, 12> rank_text; // From B7 command
|
||||
/* 000C:---- */ parray<uint8_t, 0x1C> unknown_a1;
|
||||
/* 0028:---- */ parray<be_uint16_t, 20> tech_menu_shortcut_entries;
|
||||
/* 0050:---- */ parray<be_uint32_t, 10> choice_search_config;
|
||||
// This field maps to quest_global_flags on Episodes 1 & 2
|
||||
// This field maps to quest_counters on Episodes 1 & 2
|
||||
/* 0078:---- */ parray<be_uint32_t, 0x30> scenario_progress;
|
||||
// place_counts[0] and [1] from this field are added to the player's win and
|
||||
// loss count respectively when they're shown in the status menu. However,
|
||||
@@ -885,7 +878,7 @@ struct PlayerConfig {
|
||||
/* 2124:1FD0 */ be_uint32_t online_clv_exp; // CLvOn = this / 100
|
||||
struct PlayerReference {
|
||||
/* 00 */ be_uint32_t guild_card_number;
|
||||
/* 04 */ pstring<TextEncoding::SJIS, 0x18> player_name;
|
||||
/* 04 */ pstring<TextEncoding::MARKED, 0x18> name;
|
||||
} __attribute__((packed));
|
||||
// This array is updated when a battle is started (via a 6xB4x05 command). The
|
||||
// client adds the opposing players' info to ths first two entries here if the
|
||||
@@ -939,8 +932,8 @@ struct Rules {
|
||||
/* 00 */ uint8_t overall_time_limit = 0;
|
||||
/* 01 */ uint8_t phase_time_limit = 0; // In seconds; 0 = unlimited
|
||||
/* 02 */ AllowedCards allowed_cards = AllowedCards::ALL;
|
||||
/* 03 */ uint8_t min_dice = 1; // 0 = default (1)
|
||||
/* 04 */ uint8_t max_dice = 6; // 0 = default (6)
|
||||
/* 03 */ uint8_t min_dice_value = 1; // 0 = default (1)
|
||||
/* 04 */ uint8_t max_dice_value = 6; // 0 = default (6)
|
||||
/* 05 */ uint8_t disable_deck_shuffle = 0; // 0 = shuffle on, 1 = off
|
||||
/* 06 */ uint8_t disable_deck_loop = 0; // 0 = loop on, 1 = off
|
||||
/* 07 */ uint8_t char_hp = 15;
|
||||
@@ -951,8 +944,11 @@ struct Rules {
|
||||
/* 0C */ uint8_t disable_dice_boost = 0; // 0 = dice boost on, 1 = off
|
||||
// NOTE: The following fields are unused in PSO's implementation, but newserv
|
||||
// uses them to implement extended rules.
|
||||
/* 0D */ uint8_t def_dice_range = 0; // High 4 bits = min, low 4 = max
|
||||
/* 0E */ parray<uint8_t, 6> unused;
|
||||
/* 0D */ uint8_t def_dice_value_range = 0; // High 4 bits = min, low 4 = max
|
||||
// These fields specify override dice ranges for the 1-player team in 2v1
|
||||
/* 0E */ uint8_t atk_dice_value_range_2v1 = 0; // High 4 bits = min, low 4 = max
|
||||
/* 0F */ uint8_t def_dice_value_range_2v1 = 0; // High 4 bits = min, low 4 = max
|
||||
/* 10 */ parray<uint8_t, 4> unused;
|
||||
/* 14 */
|
||||
|
||||
// Annoyingly, this structure is a different size in Episode 3 Trial Edition.
|
||||
@@ -972,20 +968,22 @@ struct Rules {
|
||||
bool check_invalid_fields() const;
|
||||
bool check_and_reset_invalid_fields();
|
||||
|
||||
uint8_t min_def_dice() const;
|
||||
uint8_t max_def_dice() const;
|
||||
std::pair<uint8_t, uint8_t> atk_dice_range(bool is_1p_2v1) const;
|
||||
std::pair<uint8_t, uint8_t> def_dice_range(bool is_1p_2v1) const;
|
||||
|
||||
std::string str() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct RulesTrial {
|
||||
// The fields here have the same meaning as in the final version. The only
|
||||
// difference is that Dice Boost does not exist in the trial version.
|
||||
// Most fields here have the same meanings as in the final version.
|
||||
/* 00 */ uint8_t overall_time_limit = 0;
|
||||
/* 01 */ uint8_t phase_time_limit = 0;
|
||||
/* 02 */ AllowedCards allowed_cards = AllowedCards::ALL;
|
||||
/* 03 */ uint8_t atk_dice_max = 1;
|
||||
/* 04 */ uint8_t def_dice_max = 6;
|
||||
// In NTE, the dice behave differently than in non-NTE. A zero in either of
|
||||
// these fields means the corresponding die is random in the range [1, 6];
|
||||
// any nonzero value means that die will always take that value.
|
||||
/* 03 */ uint8_t atk_die_behavior = 0;
|
||||
/* 04 */ uint8_t def_die_behavior = 0;
|
||||
/* 05 */ uint8_t disable_deck_shuffle = 0;
|
||||
/* 06 */ uint8_t disable_deck_loop = 0;
|
||||
/* 07 */ uint8_t char_hp = 15;
|
||||
@@ -996,7 +994,7 @@ struct RulesTrial {
|
||||
/* 0C */
|
||||
|
||||
RulesTrial() = default;
|
||||
explicit RulesTrial(const Rules&);
|
||||
RulesTrial(const Rules&);
|
||||
operator Rules() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
@@ -1009,7 +1007,7 @@ struct StateFlags {
|
||||
/* 06 */ SetupPhase setup_phase;
|
||||
/* 07 */ RegistrationPhase registration_phase;
|
||||
/* 08 */ parray<le_uint32_t, 2> team_exp;
|
||||
/* 10 */ parray<uint8_t, 2> team_dice_boost;
|
||||
/* 10 */ parray<uint8_t, 2> team_dice_bonus;
|
||||
/* 12 */ uint8_t first_team_turn;
|
||||
/* 13 */ uint8_t tournament_flag;
|
||||
/* 14 */ parray<CardType, 4> client_sc_card_types;
|
||||
@@ -1156,6 +1154,7 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
/* 48 */
|
||||
|
||||
std::string str() const;
|
||||
JSON json() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
// This array specifies the camera zone maps. A camera zone map is a subset of
|
||||
@@ -1183,8 +1182,9 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
// 10 = blocked by rock (as if the corresponding map_tiles value was 00)
|
||||
// 20 = blocked by fence (as if the corresponding map_tiles value was 00)
|
||||
// 30-34 = teleporters (2 of each value may be present)
|
||||
// 40-44 = traps (one of each type is chosen at random to be a real trap at
|
||||
// battle start time)
|
||||
// 40-4F = traps on NTE
|
||||
// 40-44 = traps on non-NTE (one of each type is chosen at random to be a real
|
||||
// trap at battle start time)
|
||||
// 50 = blocked by metal box (appears as improperly-z-buffered teal cube in
|
||||
// preview; behaves like 10 and 20 in game)
|
||||
// The assist cards that each trap type can contain are:
|
||||
@@ -1198,10 +1198,10 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
/* 1D68 */ parray<uint8_t, 0x74> unknown_a5;
|
||||
/* 1DDC */ Rules default_rules;
|
||||
|
||||
/* 1DF0 */ pstring<TextEncoding::SJIS, 0x14> name;
|
||||
/* 1E04 */ pstring<TextEncoding::SJIS, 0x14> location_name;
|
||||
/* 1E18 */ pstring<TextEncoding::SJIS, 0x3C> quest_name; // == location_name if not a quest
|
||||
/* 1E54 */ pstring<TextEncoding::SJIS, 0x190> description;
|
||||
/* 1DF0 */ pstring<TextEncoding::MARKED, 0x14> name;
|
||||
/* 1E04 */ pstring<TextEncoding::MARKED, 0x14> location_name;
|
||||
/* 1E18 */ pstring<TextEncoding::MARKED, 0x3C> quest_name; // == location_name if not a quest
|
||||
/* 1E54 */ pstring<TextEncoding::MARKED, 0x190> description;
|
||||
|
||||
// These fields describe where the map cursor on the preview screen should
|
||||
// scroll to
|
||||
@@ -1209,9 +1209,10 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
/* 1FE6 */ be_uint16_t map_y;
|
||||
|
||||
struct NPCDeck {
|
||||
/* 00 */ pstring<TextEncoding::SJIS, 0x18> name;
|
||||
/* 00 */ pstring<TextEncoding::MARKED, 0x18> deck_name;
|
||||
/* 18 */ parray<be_uint16_t, 0x20> card_ids; // Last one appears to always be FFFF
|
||||
/* 58 */
|
||||
JSON json(uint8_t language) const;
|
||||
} __attribute__((packed));
|
||||
/* 1FE8 */ parray<NPCDeck, 3> npc_decks; // Unused if name[0] == 0
|
||||
|
||||
@@ -1223,10 +1224,11 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
/* 0000 */ parray<be_uint16_t, 2> unknown_a1;
|
||||
/* 0004 */ uint8_t is_arkz;
|
||||
/* 0005 */ parray<uint8_t, 3> unknown_a2;
|
||||
/* 0008 */ pstring<TextEncoding::SJIS, 0x10> name;
|
||||
/* 0008 */ pstring<TextEncoding::MARKED, 0x10> ai_name;
|
||||
// TODO: Figure out exactly how these are used and document here.
|
||||
/* 0018 */ parray<be_uint16_t, 0x7E> params;
|
||||
/* 0114 */
|
||||
JSON json(uint8_t language) const;
|
||||
} __attribute__((packed));
|
||||
/* 20F0 */ parray<AIParams, 3> npc_ai_params; // Unused if name[0] == 0
|
||||
|
||||
@@ -1257,9 +1259,9 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
// appears after the battle if it's not blank. dispatch_message appears right
|
||||
// before the player chooses a deck if it's not blank; usually it says
|
||||
// something like "You can only dispatch <character>".
|
||||
/* 2440 */ pstring<TextEncoding::SJIS, 0x190> before_message;
|
||||
/* 25D0 */ pstring<TextEncoding::SJIS, 0x190> after_message;
|
||||
/* 2760 */ pstring<TextEncoding::SJIS, 0x190> dispatch_message;
|
||||
/* 2440 */ pstring<TextEncoding::MARKED, 0x190> before_message;
|
||||
/* 25D0 */ pstring<TextEncoding::MARKED, 0x190> after_message;
|
||||
/* 2760 */ pstring<TextEncoding::MARKED, 0x190> dispatch_message;
|
||||
|
||||
struct DialogueSet {
|
||||
// Dialogue sets specify lines that COMs can say at certain points during
|
||||
@@ -1277,8 +1279,9 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
/* 0002 */ be_uint16_t percent_chance; // 0-100, or FFFF if unused
|
||||
// If the dialogue set activates, the game randomly chooses one of these
|
||||
// strings, excluding any that are empty or begin with the character '^'.
|
||||
/* 0004 */ parray<pstring<TextEncoding::SJIS, 0x40>, 4> strings;
|
||||
/* 0004 */ parray<pstring<TextEncoding::MARKED, 0x40>, 4> strings;
|
||||
/* 0104 */
|
||||
JSON json(uint8_t language) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
// There are up to 0x10 of these per valid NPC, but only the first 13 of them
|
||||
@@ -1361,6 +1364,7 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
|
||||
bool operator==(const EntryState& other) const = default;
|
||||
bool operator!=(const EntryState& other) const = default;
|
||||
JSON json() const;
|
||||
} __attribute__((packed));
|
||||
/* 5A10 */ parray<EntryState, 4> entry_states;
|
||||
/* 5A18 */
|
||||
@@ -1376,6 +1380,7 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
void assert_semantically_equivalent(const MapDefinition& other) const;
|
||||
|
||||
std::string str(const CardIndex* card_index, uint8_t language) const;
|
||||
JSON json(uint8_t language) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct MapDefinitionTrial {
|
||||
@@ -1396,19 +1401,19 @@ struct MapDefinitionTrial {
|
||||
/* 1C68 */ parray<parray<uint8_t, 0x10>, 0x10> modification_tiles;
|
||||
/* 1D68 */ parray<uint8_t, 0x74> unknown_a5;
|
||||
/* 1DD4 */ RulesTrial default_rules;
|
||||
/* 1DE8 */ pstring<TextEncoding::SJIS, 0x14> name;
|
||||
/* 1DFC */ pstring<TextEncoding::SJIS, 0x14> location_name;
|
||||
/* 1E10 */ pstring<TextEncoding::SJIS, 0x3C> quest_name;
|
||||
/* 1E4C */ pstring<TextEncoding::SJIS, 0x190> description;
|
||||
/* 1DE8 */ pstring<TextEncoding::MARKED, 0x14> name;
|
||||
/* 1DFC */ pstring<TextEncoding::MARKED, 0x14> location_name;
|
||||
/* 1E10 */ pstring<TextEncoding::MARKED, 0x3C> quest_name;
|
||||
/* 1E4C */ pstring<TextEncoding::MARKED, 0x190> description;
|
||||
/* 1FDC */ be_uint16_t map_x;
|
||||
/* 1FDE */ be_uint16_t map_y;
|
||||
/* 1FE0 */ parray<MapDefinition::NPCDeck, 3> npc_decks;
|
||||
/* 20E8 */ parray<MapDefinition::AIParams, 3> npc_ai_params;
|
||||
/* 2424 */ parray<uint8_t, 8> unknown_a7;
|
||||
/* 242C */ parray<be_int32_t, 3> npc_ai_params_entry_index;
|
||||
/* 2438 */ pstring<TextEncoding::SJIS, 0x190> before_message;
|
||||
/* 25C8 */ pstring<TextEncoding::SJIS, 0x190> after_message;
|
||||
/* 2758 */ pstring<TextEncoding::SJIS, 0x190> dispatch_message;
|
||||
/* 2438 */ pstring<TextEncoding::MARKED, 0x190> before_message;
|
||||
/* 25C8 */ pstring<TextEncoding::MARKED, 0x190> after_message;
|
||||
/* 2758 */ pstring<TextEncoding::MARKED, 0x190> dispatch_message;
|
||||
/* 28E8 */ parray<parray<MapDefinition::DialogueSet, 8>, 3> dialogue_sets;
|
||||
/* 4148 */ parray<be_uint16_t, 0x10> reward_card_ids;
|
||||
/* 4168 */ be_int32_t win_level_override;
|
||||
@@ -1458,6 +1463,7 @@ public:
|
||||
std::shared_ptr<const CardEntry> definition_for_name_normalized(const std::string& name) const;
|
||||
std::set<uint32_t> all_ids() const;
|
||||
uint64_t definitions_mtime() const;
|
||||
JSON definitions_json() const;
|
||||
|
||||
private:
|
||||
static std::string normalize_card_name(const std::string& name);
|
||||
@@ -1482,7 +1488,7 @@ public:
|
||||
VersionedMap(std::string&& compressed_data, uint8_t language);
|
||||
|
||||
std::shared_ptr<const MapDefinitionTrial> trial() const;
|
||||
const std::string& compressed(bool is_trial) const;
|
||||
const std::string& compressed(bool is_nte) const;
|
||||
|
||||
private:
|
||||
mutable std::shared_ptr<const MapDefinitionTrial> trial_map;
|
||||
@@ -1549,3 +1555,24 @@ template <>
|
||||
Episode3::AllowedCards enum_for_name<Episode3::AllowedCards>(const char* name);
|
||||
template <>
|
||||
const char* name_for_enum<Episode3::AllowedCards>(Episode3::AllowedCards allowed_cards);
|
||||
|
||||
template <>
|
||||
const char* name_for_enum<Episode3::BattlePhase>(Episode3::BattlePhase phase);
|
||||
template <>
|
||||
const char* name_for_enum<Episode3::SetupPhase>(Episode3::SetupPhase phase);
|
||||
template <>
|
||||
const char* name_for_enum<Episode3::RegistrationPhase>(Episode3::RegistrationPhase phase);
|
||||
template <>
|
||||
const char* name_for_enum<Episode3::ActionSubphase>(Episode3::ActionSubphase phase);
|
||||
template <>
|
||||
const char* name_for_enum<Episode3::AttackMedium>(Episode3::AttackMedium medium);
|
||||
template <>
|
||||
const char* name_for_enum<Episode3::CriterionCode>(Episode3::CriterionCode code);
|
||||
template <>
|
||||
const char* name_for_enum<Episode3::CardType>(Episode3::CardType type);
|
||||
template <>
|
||||
const char* name_for_enum<Episode3::CardClass>(Episode3::CardClass cc);
|
||||
template <>
|
||||
const char* name_for_enum<Episode3::ConditionType>(Episode3::ConditionType cond_type);
|
||||
template <>
|
||||
const char* name_for_enum<Episode3::Direction>(Episode3::Direction d);
|
||||
|
||||
+11
-10
@@ -88,15 +88,16 @@ bool DeckState::draw_card_by_ref(uint16_t card_ref) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the card is discarded, then it should be before the draw index, and we
|
||||
// can just change its state.
|
||||
if (this->entries[index].state == CardState::DISCARDED) {
|
||||
// If the card is discarded, then it should be before the draw index, and we
|
||||
// can just change its state.
|
||||
this->entries[index].state = CardState::IN_HAND;
|
||||
return true;
|
||||
|
||||
// If the card is still drawable, we need to move it so it's just in front of
|
||||
// the draw index, then immediately draw it
|
||||
} else if (this->entries[index].state == CardState::DRAWABLE) {
|
||||
// If the card is still drawable, we need to move it so it's just in front
|
||||
// of the draw index, then immediately draw it. Ep3 NTE does not handle this
|
||||
// case, but we do even when playing NTE.
|
||||
ssize_t ref_index;
|
||||
for (ref_index = this->card_refs.size(); ref_index >= 0; ref_index--) {
|
||||
if (this->card_refs[ref_index] == card_ref) {
|
||||
@@ -182,7 +183,7 @@ void DeckState::restart() {
|
||||
this->shuffle();
|
||||
}
|
||||
|
||||
void DeckState::do_mulligan() {
|
||||
void DeckState::do_mulligan(bool is_nte) {
|
||||
for (size_t z = 0; z < this->entries.size(); z++) {
|
||||
if (this->entries[z].state == CardState::DISCARDED) {
|
||||
this->entries[z].state = CardState::DRAWABLE;
|
||||
@@ -190,7 +191,7 @@ void DeckState::do_mulligan() {
|
||||
}
|
||||
this->draw_index = 1;
|
||||
|
||||
if (this->shuffle_enabled) {
|
||||
if (is_nte || this->shuffle_enabled) {
|
||||
// Get the next 5 cards from the deck, and put the previous 5 cards after
|
||||
// them (so they will be shuffled back in).
|
||||
for (uint8_t z = 0; z < 5; z++) {
|
||||
@@ -204,8 +205,8 @@ void DeckState::do_mulligan() {
|
||||
size_t max = this->num_drawable_cards() - 5;
|
||||
uint8_t base_index = this->draw_index + 5;
|
||||
for (size_t z = 0; z < this->card_refs.size(); z++) {
|
||||
uint8_t index1 = this->random_crypt->next() % max;
|
||||
uint8_t index2 = this->random_crypt->next() % max;
|
||||
uint8_t index1 = random_from_optional_crypt(this->opt_rand_crypt) % max;
|
||||
uint8_t index2 = random_from_optional_crypt(this->opt_rand_crypt) % max;
|
||||
uint16_t temp_ref = this->card_refs[base_index + index1];
|
||||
this->card_refs[base_index + index1] = this->card_refs[base_index + index2];
|
||||
this->card_refs[base_index + index2] = temp_ref;
|
||||
@@ -264,8 +265,8 @@ void DeckState::shuffle() {
|
||||
// instead swap each item with another random item (possibly itself) that
|
||||
// doesn't appear earlier than it in the array, but this is not what Sega
|
||||
// did.
|
||||
uint8_t index1 = this->draw_index + this->random_crypt->next() % max;
|
||||
uint8_t index2 = this->draw_index + this->random_crypt->next() % max;
|
||||
uint8_t index1 = this->draw_index + random_from_optional_crypt(this->opt_rand_crypt) % max;
|
||||
uint8_t index2 = this->draw_index + random_from_optional_crypt(this->opt_rand_crypt) % max;
|
||||
uint16_t temp_ref = this->card_refs[index1];
|
||||
this->card_refs[index1] = this->card_refs[index2];
|
||||
this->card_refs[index2] = temp_ref;
|
||||
|
||||
+18
-16
@@ -11,27 +11,29 @@
|
||||
namespace Episode3 {
|
||||
|
||||
struct NameEntry {
|
||||
parray<char, 0x10> name;
|
||||
uint8_t client_id;
|
||||
uint8_t present;
|
||||
uint8_t is_cpu_player;
|
||||
uint8_t unused;
|
||||
/* 00 */ pstring<TextEncoding::MARKED, 0x10> name;
|
||||
/* 10 */ uint8_t client_id;
|
||||
/* 11 */ uint8_t present;
|
||||
/* 12 */ uint8_t is_cpu_player;
|
||||
/* 13 */ uint8_t unused;
|
||||
/* 14 */
|
||||
|
||||
NameEntry();
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
|
||||
struct DeckEntry {
|
||||
pstring<TextEncoding::SJIS, 0x10> name;
|
||||
le_uint32_t team_id;
|
||||
parray<le_uint16_t, 31> card_ids;
|
||||
/* 00 */ pstring<TextEncoding::MARKED, 0x10> name;
|
||||
/* 10 */ le_uint32_t team_id;
|
||||
/* 14 */ parray<le_uint16_t, 31> card_ids;
|
||||
// If the following flag is not set to 3, then the God Whim assist effect can
|
||||
// use cards that are hidden from the player during deck building. The client
|
||||
// always sets this to 3, and it's not clear why this even exists.
|
||||
uint8_t god_whim_flag;
|
||||
uint8_t unused1;
|
||||
le_uint16_t player_level;
|
||||
parray<uint8_t, 2> unused2;
|
||||
/* 52 */ uint8_t god_whim_flag;
|
||||
/* 53 */ uint8_t unused1;
|
||||
/* 54 */ le_uint16_t player_level;
|
||||
/* 56 */ parray<uint8_t, 2> unused2;
|
||||
/* 58 */
|
||||
|
||||
DeckEntry();
|
||||
void clear();
|
||||
@@ -55,13 +57,13 @@ public:
|
||||
DeckState(
|
||||
uint8_t client_id,
|
||||
const parray<CardIDT, 0x1F>& card_ids,
|
||||
std::shared_ptr<PSOLFGEncryption> random_crypt)
|
||||
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt)
|
||||
: client_id(client_id),
|
||||
draw_index(1),
|
||||
card_ref_base(this->client_id << 8),
|
||||
shuffle_enabled(true),
|
||||
loop_enabled(true),
|
||||
random_crypt(random_crypt) {
|
||||
opt_rand_crypt(opt_rand_crypt) {
|
||||
for (size_t z = 0; z < card_ids.size(); z++) {
|
||||
auto& e = this->entries[z];
|
||||
e.card_id = card_ids[z];
|
||||
@@ -92,7 +94,7 @@ public:
|
||||
|
||||
void restart();
|
||||
void shuffle();
|
||||
void do_mulligan();
|
||||
void do_mulligan(bool is_nte);
|
||||
|
||||
void print(FILE* stream, std::shared_ptr<const CardIndex> card_index = nullptr) const;
|
||||
|
||||
@@ -110,7 +112,7 @@ private:
|
||||
parray<CardEntry, 31> entries;
|
||||
parray<uint16_t, 31> card_refs;
|
||||
|
||||
std::shared_ptr<PSOLFGEncryption> random_crypt;
|
||||
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt;
|
||||
};
|
||||
|
||||
} // namespace Episode3
|
||||
|
||||
@@ -43,7 +43,7 @@ void MapAndRulesState::clear() {
|
||||
this->num_team0_players = 0;
|
||||
this->unused2 = 0;
|
||||
this->start_facing_directions = 0;
|
||||
this->unused3 = 0;
|
||||
this->unknown_a3 = 0;
|
||||
this->map_number = 0;
|
||||
this->unused4 = 0;
|
||||
this->rules.clear();
|
||||
@@ -68,6 +68,37 @@ void MapAndRulesState::clear_occupied_bit_for_tile(uint8_t x, uint8_t y) {
|
||||
this->map.tiles[y][x] &= 0xEF;
|
||||
}
|
||||
|
||||
MapAndRulesStateTrial::MapAndRulesStateTrial(const MapAndRulesState& state)
|
||||
: map(state.map),
|
||||
num_players(state.num_players),
|
||||
unused1(state.unused1),
|
||||
environment_number(state.environment_number),
|
||||
num_players_per_team(state.num_players_per_team),
|
||||
num_team0_players(state.num_team0_players),
|
||||
unused2(state.unused2),
|
||||
unused5(state.start_facing_directions),
|
||||
unknown_a3(state.unknown_a3),
|
||||
map_number(state.map_number),
|
||||
unused4(state.unused4),
|
||||
rules(state.rules) {}
|
||||
|
||||
MapAndRulesStateTrial::operator MapAndRulesState() const {
|
||||
MapAndRulesState ret;
|
||||
ret.map = this->map;
|
||||
ret.num_players = this->num_players;
|
||||
ret.unused1 = this->unused1;
|
||||
ret.environment_number = this->environment_number;
|
||||
ret.num_players_per_team = this->num_players_per_team;
|
||||
ret.num_team0_players = this->num_team0_players;
|
||||
ret.unused2 = this->unused2;
|
||||
ret.start_facing_directions = this->unused5;
|
||||
ret.unknown_a3 = this->unknown_a3;
|
||||
ret.map_number = this->map_number;
|
||||
ret.unused4 = this->unused4;
|
||||
ret.rules = this->rules;
|
||||
return ret;
|
||||
}
|
||||
|
||||
OverlayState::OverlayState() {
|
||||
this->clear();
|
||||
}
|
||||
@@ -78,7 +109,7 @@ void OverlayState::clear() {
|
||||
}
|
||||
this->unused1.clear(0);
|
||||
this->unused2.clear(0);
|
||||
this->unused3.clear(0);
|
||||
this->trap_card_ids_nte.clear(0);
|
||||
}
|
||||
|
||||
} // namespace Episode3
|
||||
|
||||
+39
-17
@@ -10,10 +10,11 @@
|
||||
namespace Episode3 {
|
||||
|
||||
struct MapState {
|
||||
le_uint16_t width;
|
||||
le_uint16_t height;
|
||||
parray<parray<uint8_t, 0x10>, 0x10> tiles;
|
||||
parray<parray<uint8_t, 6>, 2> start_tile_definitions;
|
||||
/* 0000 */ le_uint16_t width = 0;
|
||||
/* 0002 */ le_uint16_t height = 0;
|
||||
/* 0004 */ parray<parray<uint8_t, 0x10>, 0x10> tiles;
|
||||
/* 0104 */ parray<parray<uint8_t, 6>, 2> start_tile_definitions;
|
||||
/* 0110 */
|
||||
|
||||
MapState();
|
||||
void clear();
|
||||
@@ -22,18 +23,19 @@ struct MapState {
|
||||
} __attribute__((packed));
|
||||
|
||||
struct MapAndRulesState {
|
||||
MapState map;
|
||||
uint8_t num_players;
|
||||
uint8_t unused1;
|
||||
uint8_t environment_number;
|
||||
uint8_t num_players_per_team;
|
||||
uint8_t num_team0_players;
|
||||
uint8_t unused2;
|
||||
le_uint16_t start_facing_directions;
|
||||
uint32_t unused3;
|
||||
le_uint32_t map_number;
|
||||
uint32_t unused4;
|
||||
Rules rules;
|
||||
/* 0000 */ MapState map;
|
||||
/* 0110 */ uint8_t num_players = 0;
|
||||
/* 0111 */ uint8_t unused1 = 0;
|
||||
/* 0112 */ uint8_t environment_number = 0;
|
||||
/* 0113 */ uint8_t num_players_per_team = 0;
|
||||
/* 0114 */ uint8_t num_team0_players = 0;
|
||||
/* 0115 */ uint8_t unused2 = 0;
|
||||
/* 0116 */ le_uint16_t start_facing_directions = 0;
|
||||
/* 0118 */ be_uint32_t unknown_a3 = 0;
|
||||
/* 011C */ le_uint32_t map_number = 0;
|
||||
/* 0120 */ be_uint32_t unused4 = 0;
|
||||
/* 0124 */ Rules rules;
|
||||
/* 0138 */
|
||||
|
||||
MapAndRulesState();
|
||||
void clear();
|
||||
@@ -45,11 +47,31 @@ struct MapAndRulesState {
|
||||
void clear_occupied_bit_for_tile(uint8_t x, uint8_t y);
|
||||
} __attribute__((packed));
|
||||
|
||||
struct MapAndRulesStateTrial {
|
||||
/* 0000 */ MapState map;
|
||||
/* 0110 */ uint8_t num_players = 0;
|
||||
/* 0111 */ uint8_t unused1 = 0;
|
||||
/* 0112 */ uint8_t environment_number = 0;
|
||||
/* 0113 */ uint8_t num_players_per_team = 0;
|
||||
/* 0114 */ uint8_t num_team0_players = 0;
|
||||
/* 0115 */ uint8_t unused2 = 0;
|
||||
/* 0116 */ le_uint16_t unused5 = 0;
|
||||
/* 0118 */ be_uint32_t unknown_a3 = 0;
|
||||
/* 011C */ le_uint32_t map_number = 0;
|
||||
/* 0120 */ be_uint32_t unused4 = 0;
|
||||
/* 0124 */ RulesTrial rules;
|
||||
/* 0130 */
|
||||
|
||||
MapAndRulesStateTrial() = default;
|
||||
MapAndRulesStateTrial(const MapAndRulesState& state);
|
||||
operator MapAndRulesState() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct OverlayState {
|
||||
parray<parray<uint8_t, 0x10>, 0x10> tiles;
|
||||
parray<le_uint32_t, 5> unused1;
|
||||
parray<le_uint32_t, 0x10> unused2;
|
||||
parray<le_uint16_t, 0x10> unused3;
|
||||
parray<le_uint16_t, 0x10> trap_card_ids_nte; // Unused on non-NTE
|
||||
|
||||
OverlayState();
|
||||
void clear();
|
||||
|
||||
+390
-201
File diff suppressed because it is too large
Load Diff
+20
-22
@@ -20,6 +20,7 @@ enum AssistFlag : uint32_t {
|
||||
// be bits used only by the client which are not documented here.
|
||||
|
||||
// clang-format off
|
||||
NONE = 0x0000,
|
||||
READY_TO_END_PHASE = 0x0001,
|
||||
DICE_WERE_EXCHANGED = 0x0002,
|
||||
HAS_WON_BATTLE = 0x0004,
|
||||
@@ -52,14 +53,14 @@ public:
|
||||
uint16_t card_ref_for_hand_index(size_t hand_index) const;
|
||||
int16_t compute_attack_or_defense_atk_costs(const ActionState& pa) const;
|
||||
void compute_total_set_cards_cost();
|
||||
size_t count_set_cards_for_env_stats_nte() const;
|
||||
size_t count_set_cards() const;
|
||||
size_t count_set_refs() const;
|
||||
void discard_all_assist_cards_from_hand();
|
||||
void discard_all_attack_action_cards_from_hand();
|
||||
void discard_all_item_and_creature_cards_from_hand();
|
||||
void discard_and_redraw_hand();
|
||||
bool discard_card_or_add_to_draw_pile(
|
||||
uint16_t card_ref, bool add_to_draw_pile);
|
||||
bool discard_card_or_add_to_draw_pile(uint16_t card_ref, bool add_to_draw_pile);
|
||||
void discard_random_hand_card();
|
||||
bool discard_ref_from_hand(uint16_t card_ref);
|
||||
void discard_set_assist_card();
|
||||
@@ -76,8 +77,7 @@ public:
|
||||
const Location& loc,
|
||||
uint8_t target_team_id) const;
|
||||
uint8_t get_atk_points() const;
|
||||
void get_short_status_for_card_index_in_hand(
|
||||
size_t hand_index, CardShortStatus* stat) const;
|
||||
void get_short_status_for_card_index_in_hand(size_t hand_index, CardShortStatus* stat) const;
|
||||
std::shared_ptr<DeckState> get_deck();
|
||||
uint8_t get_def_points() const;
|
||||
uint8_t get_dice_result(size_t which) const;
|
||||
@@ -96,8 +96,8 @@ public:
|
||||
bool is_mulligan_allowed() const;
|
||||
bool is_team_turn() const;
|
||||
void log_discard(uint16_t card_ref, uint16_t reason);
|
||||
bool move_card_to_location_by_card_index(
|
||||
size_t card_index, const Location& new_loc);
|
||||
uint16_t pop_from_discard_log(uint16_t reason);
|
||||
bool move_card_to_location_by_card_index(size_t card_index, const Location& new_loc);
|
||||
void move_null_hand_refs_to_end();
|
||||
void on_cards_destroyed();
|
||||
void replace_all_set_assists_with_random_assists();
|
||||
@@ -115,42 +115,40 @@ public:
|
||||
uint8_t assist_target_client_id,
|
||||
bool skip_error_checks_and_atk_sub);
|
||||
void set_initial_location();
|
||||
void set_map_occupied_bit_for_card_on_warp_tile(
|
||||
std::shared_ptr<const Card> card);
|
||||
void set_map_occupied_bit_for_card_on_warp_tile(std::shared_ptr<const Card> card);
|
||||
void set_map_occupied_bits_for_sc_and_creatures();
|
||||
void subtract_def_points(uint8_t cost);
|
||||
bool subtract_or_check_atk_or_def_points_for_action(
|
||||
const ActionState& pa, bool deduct_points);
|
||||
bool subtract_or_check_atk_or_def_points_for_action(const ActionState& pa, bool deduct_points);
|
||||
void subtract_atk_points(uint8_t cost);
|
||||
G_UpdateHand_GC_Ep3_6xB4x02 prepare_6xB4x02() const;
|
||||
void update_hand_and_equip_state_and_send_6xB4x02_if_needed(
|
||||
bool always_send = false);
|
||||
G_UpdateHand_Ep3_6xB4x02 prepare_6xB4x02() const;
|
||||
void update_hand_and_equip_state_and_send_6xB4x02_if_needed(bool always_send = false);
|
||||
void set_random_assist_card_from_hand_for_free();
|
||||
G_UpdateShortStatuses_GC_Ep3_6xB4x04 prepare_6xB4x04() const;
|
||||
G_UpdateShortStatuses_Ep3_6xB4x04 prepare_6xB4x04() const;
|
||||
void send_6xB4x04_if_needed(bool always_send = false);
|
||||
std::vector<uint16_t> get_card_refs_within_range_from_all_players(
|
||||
const parray<uint8_t, 9 * 9>& range,
|
||||
const Location& loc,
|
||||
CardType type) const;
|
||||
void unknown_80239460();
|
||||
void unknown_802394C4();
|
||||
void unknown_80239528();
|
||||
void draw_phase_before();
|
||||
void action_phase_before();
|
||||
void move_phase_before();
|
||||
void handle_before_turn_assist_effects();
|
||||
int16_t get_assist_turns_remaining();
|
||||
bool set_action_cards_for_action_state(const ActionState& pa);
|
||||
void unknown_8023C174();
|
||||
void handle_homesick_assist_effect(std::shared_ptr<Card> card);
|
||||
void dice_phase_before();
|
||||
void handle_homesick_assist_effect_from_bomb(std::shared_ptr<Card> card);
|
||||
void apply_main_die_assist_effects(uint8_t* die_value) const;
|
||||
void roll_main_dice();
|
||||
void roll_main_dice_or_apply_after_effects();
|
||||
void unknown_8023C110();
|
||||
void compute_team_dice_boost_after_draw_phase();
|
||||
void compute_team_dice_bonus_after_draw_phase();
|
||||
void send_6xB4x0A_for_set_card(size_t set_index);
|
||||
|
||||
private:
|
||||
std::weak_ptr<Server> w_server;
|
||||
|
||||
public:
|
||||
std::shared_ptr<Card> sc_card;
|
||||
std::shared_ptr<Card> set_cards[8];
|
||||
bcarray<std::shared_ptr<Card>, 8> set_cards;
|
||||
uint8_t client_id;
|
||||
uint16_t num_mulligans_allowed;
|
||||
CardType sc_card_type;
|
||||
|
||||
@@ -6,22 +6,6 @@ using namespace std;
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
template <size_t Count>
|
||||
std::string string_for_refs(const parray<le_uint16_t, Count>& card_refs) {
|
||||
string ret = "[";
|
||||
for (size_t z = 0; z < Count; z++) {
|
||||
if (card_refs[z] != 0xFFFF) {
|
||||
ret += string_printf("%zu:@$%04X ", z, card_refs[z].load());
|
||||
}
|
||||
}
|
||||
if (!ret.empty()) {
|
||||
ret.back() = ']'; // Replace the ' ' from the last added item
|
||||
} else {
|
||||
ret.push_back(']');
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
Condition::Condition() {
|
||||
this->clear();
|
||||
}
|
||||
@@ -77,20 +61,22 @@ void Condition::clear_FF() {
|
||||
this->unknown_a8 = 0xFF;
|
||||
}
|
||||
|
||||
std::string Condition::str() const {
|
||||
std::string Condition::str(shared_ptr<const Server> s) const {
|
||||
auto card_ref_str = s->debug_str_for_card_ref(this->card_ref);
|
||||
auto giver_ref_str = s->debug_str_for_card_ref(this->condition_giver_card_ref);
|
||||
return string_printf(
|
||||
"Condition[type=%s, turns=%hhu, a_arg=%hhd, dice=%hhu, flags=%02hhX, "
|
||||
"def_eff_index=%hhu, ref=@%04hX, value=%hd, giver_ref=@%04hX "
|
||||
"def_eff_index=%hhu, ref=%s, value=%hd, giver_ref=%s "
|
||||
"percent=%hhu value8=%hd order=%hu a8=%hu]",
|
||||
name_for_condition_type(this->type),
|
||||
name_for_enum(this->type),
|
||||
this->remaining_turns,
|
||||
this->a_arg_value,
|
||||
this->dice_roll_value,
|
||||
this->flags,
|
||||
this->card_definition_effect_index,
|
||||
this->card_ref.load(),
|
||||
card_ref_str.c_str(),
|
||||
this->value.load(),
|
||||
this->condition_giver_card_ref.load(),
|
||||
giver_ref_str.c_str(),
|
||||
this->random_percent,
|
||||
this->value8,
|
||||
this->order,
|
||||
@@ -114,13 +100,15 @@ void EffectResult::clear() {
|
||||
this->dice_roll_value = 0;
|
||||
}
|
||||
|
||||
std::string EffectResult::str() const {
|
||||
std::string EffectResult::str(shared_ptr<const Server> s) const {
|
||||
string attacker_ref_str = s->debug_str_for_card_ref(this->attacker_card_ref);
|
||||
string target_ref_str = s->debug_str_for_card_ref(this->target_card_ref);
|
||||
return string_printf(
|
||||
"EffectResult[att_ref=@%04hX, target_ref=@%04hX, value=%hhd, "
|
||||
"EffectResult[att_ref=%s, target_ref=%s, value=%hhd, "
|
||||
"cur_hp=%hhd, ap=%hhd, tp=%hhd, flags=%02hhX, op=%hhd, "
|
||||
"cond_index=%hhu, dice=%hhu]",
|
||||
this->attacker_card_ref.load(),
|
||||
this->target_card_ref.load(),
|
||||
attacker_ref_str.c_str(),
|
||||
target_ref_str.c_str(),
|
||||
this->value,
|
||||
this->current_hp,
|
||||
this->ap,
|
||||
@@ -148,12 +136,13 @@ bool CardShortStatus::operator!=(const CardShortStatus& other) const {
|
||||
return !this->operator==(other);
|
||||
}
|
||||
|
||||
std::string CardShortStatus::str() const {
|
||||
std::string CardShortStatus::str(shared_ptr<const Server> s) const {
|
||||
string loc_s = this->loc.str();
|
||||
string ref_str = s->debug_str_for_card_ref(this->card_ref);
|
||||
return string_printf(
|
||||
"CardShortStatus[ref=@%04hX, cur_hp=%hd, flags=%08" PRIX32 ", loc=%s, "
|
||||
"CardShortStatus[ref=%s, cur_hp=%hd, flags=%08" PRIX32 ", loc=%s, "
|
||||
"u1=%04hX, max_hp=%hhd, u2=%hhu]",
|
||||
this->card_ref.load(),
|
||||
ref_str.c_str(),
|
||||
this->current_hp.load(),
|
||||
this->card_flags.load(),
|
||||
loc_s.c_str(),
|
||||
@@ -195,23 +184,27 @@ void ActionState::clear() {
|
||||
this->original_attacker_card_ref = 0xFFFF;
|
||||
this->target_card_refs.clear(0xFFFF);
|
||||
this->action_card_refs.clear(0xFFFF);
|
||||
this->unused2 = 0xFFFF;
|
||||
}
|
||||
|
||||
std::string ActionState::str() const {
|
||||
string target_refs_s = string_for_refs(this->target_card_refs);
|
||||
string action_refs_s = string_for_refs(this->action_card_refs);
|
||||
std::string ActionState::str(shared_ptr<const Server> s) const {
|
||||
string attacker_ref_s = s->debug_str_for_card_ref(this->attacker_card_ref);
|
||||
string defense_ref_s = s->debug_str_for_card_ref(this->defense_card_ref);
|
||||
string original_attacker_ref_s = s->debug_str_for_card_ref(this->original_attacker_card_ref);
|
||||
string target_refs_s = s->debug_str_for_card_refs(this->target_card_refs);
|
||||
string action_refs_s = s->debug_str_for_card_refs(this->action_card_refs);
|
||||
return string_printf(
|
||||
"ActionState[client=%hu, u=%hhu, facing=%s, attacker_ref=@%04hX, "
|
||||
"def_ref=@%04hX, target_refs=%s, action_refs=%s, "
|
||||
"orig_attacker_ref=@%04hX]",
|
||||
"ActionState[client=%hu, u=%hhu, facing=%s, attacker_ref=%s, "
|
||||
"def_ref=%s, target_refs=%s, action_refs=%s, "
|
||||
"orig_attacker_ref=%s]",
|
||||
this->client_id.load(),
|
||||
this->unused,
|
||||
name_for_direction(this->facing_direction),
|
||||
this->attacker_card_ref.load(),
|
||||
this->defense_card_ref.load(),
|
||||
name_for_enum(this->facing_direction),
|
||||
attacker_ref_s.c_str(),
|
||||
defense_ref_s.c_str(),
|
||||
target_refs_s.c_str(),
|
||||
action_refs_s.c_str(),
|
||||
this->original_attacker_card_ref.load());
|
||||
original_attacker_ref_s.c_str());
|
||||
}
|
||||
|
||||
ActionChain::ActionChain() {
|
||||
@@ -234,8 +227,8 @@ bool ActionChain::operator==(const ActionChain& other) const {
|
||||
(this->damage_multiplier == other.damage_multiplier) &&
|
||||
(this->attack_number == other.attack_number) &&
|
||||
(this->tp_effect_bonus == other.tp_effect_bonus) &&
|
||||
(this->unused1 == other.unused1) &&
|
||||
(this->unused2 == other.unused2) &&
|
||||
(this->physical_attack_bonus_nte == other.physical_attack_bonus_nte) &&
|
||||
(this->tech_attack_bonus_nte == other.tech_attack_bonus_nte) &&
|
||||
(this->card_ap == other.card_ap) &&
|
||||
(this->card_tp == other.card_tp) &&
|
||||
(this->flags == other.flags) &&
|
||||
@@ -245,34 +238,35 @@ bool ActionChain::operator!=(const ActionChain& other) const {
|
||||
return !this->operator==(other);
|
||||
}
|
||||
|
||||
std::string ActionChain::str() const {
|
||||
string attack_action_card_refs_s = string_for_refs(this->attack_action_card_refs);
|
||||
string target_card_refs_s = string_for_refs(this->target_card_refs);
|
||||
std::string ActionChain::str(shared_ptr<const Server> s) const {
|
||||
string acting_card_ref_s = s->debug_str_for_card_ref(this->acting_card_ref);
|
||||
string unknown_card_ref_a3_s = s->debug_str_for_card_ref(this->unknown_card_ref_a3);
|
||||
string attack_action_card_refs_s = s->debug_str_for_card_refs(this->attack_action_card_refs);
|
||||
string target_card_refs_s = s->debug_str_for_card_refs(this->target_card_refs);
|
||||
return string_printf(
|
||||
"ActionChain[eff_ap=%hhd, eff_tp=%hhd, ap_bonus=%hhd, damage=%hhd, "
|
||||
"acting_ref=@%04hX, unknown_ref_a3=@%04hX, "
|
||||
"attack_action_refs=%s, attack_action_ref_count=%hhu, "
|
||||
"medium=%s, target_ref_count=%hhu, subphase=%s, "
|
||||
"strikes=%hhu, damage_mult=%hhd, attack_num=%hhu, "
|
||||
"tp_bonus=%hhd, u1=%hhu, u2=%hhu, card_ap=%hhd, "
|
||||
"acting_ref=%s, unknown_ref_a3=%s, attack_action_refs=%s, "
|
||||
"attack_action_ref_count=%hhu, medium=%s, target_ref_count=%hhu, "
|
||||
"subphase=%s, strikes=%hhu, damage_mult=%hhd, attack_num=%hhu, "
|
||||
"tp_bonus=%hhd, phys_bonus_nte=%hhu, tech_bonus_nte=%hhu, card_ap=%hhd, "
|
||||
"card_tp=%hhd, flags=%08" PRIX32 ", target_refs=%s]",
|
||||
this->effective_ap,
|
||||
this->effective_tp,
|
||||
this->ap_effect_bonus,
|
||||
this->damage,
|
||||
this->acting_card_ref.load(),
|
||||
this->unknown_card_ref_a3.load(),
|
||||
acting_card_ref_s.c_str(),
|
||||
unknown_card_ref_a3_s.c_str(),
|
||||
attack_action_card_refs_s.c_str(),
|
||||
this->attack_action_card_ref_count,
|
||||
name_for_attack_medium(this->attack_medium),
|
||||
name_for_enum(this->attack_medium),
|
||||
this->target_card_ref_count,
|
||||
name_for_action_subphase(this->action_subphase),
|
||||
name_for_enum(this->action_subphase),
|
||||
this->strike_count,
|
||||
this->damage_multiplier,
|
||||
this->attack_number,
|
||||
this->tp_effect_bonus,
|
||||
this->unused1,
|
||||
this->unused2,
|
||||
this->physical_attack_bonus_nte,
|
||||
this->tech_attack_bonus_nte,
|
||||
this->card_ap,
|
||||
this->card_tp,
|
||||
this->flags.load(),
|
||||
@@ -294,8 +288,8 @@ void ActionChain::clear() {
|
||||
this->damage_multiplier = 1;
|
||||
this->attack_number = 0xFF;
|
||||
this->tp_effect_bonus = 0;
|
||||
this->unused1 = 0;
|
||||
this->unused2 = 0;
|
||||
this->physical_attack_bonus_nte = 0;
|
||||
this->tech_attack_bonus_nte = 0;
|
||||
this->card_ap = 0;
|
||||
this->card_tp = 0;
|
||||
this->flags = 0;
|
||||
@@ -319,8 +313,8 @@ void ActionChain::clear_FF() {
|
||||
this->damage_multiplier = -1;
|
||||
this->attack_number = 0xFF;
|
||||
this->tp_effect_bonus = -1;
|
||||
this->unused1 = 0xFF;
|
||||
this->unused2 = 0xFF;
|
||||
this->physical_attack_bonus_nte = 0xFF;
|
||||
this->tech_attack_bonus_nte = 0xFF;
|
||||
this->card_ap = -1;
|
||||
this->card_tp = -1;
|
||||
this->flags = 0xFFFFFFFF;
|
||||
@@ -338,17 +332,17 @@ bool ActionChainWithConds::operator!=(const ActionChainWithConds& other) const {
|
||||
return !this->operator==(other);
|
||||
}
|
||||
|
||||
std::string ActionChainWithConds::str() const {
|
||||
std::string ActionChainWithConds::str(shared_ptr<const Server> s) const {
|
||||
string ret = "ActionChainWithConds[chain=";
|
||||
ret += this->chain.str();
|
||||
ret += this->chain.str(s);
|
||||
ret += ", conds=[";
|
||||
for (size_t z = 0; z < this->conditions.size(); z++) {
|
||||
if (this->conditions[z].type != ConditionType::NONE) {
|
||||
if (ret.back() != '=') {
|
||||
if (ret.back() != '[') {
|
||||
ret += ", ";
|
||||
}
|
||||
ret += string_printf("%zu:", z);
|
||||
ret += this->conditions[z].str();
|
||||
ret += this->conditions[z].str(s);
|
||||
}
|
||||
}
|
||||
ret += "]]";
|
||||
@@ -393,8 +387,8 @@ void ActionChainWithConds::reset() {
|
||||
this->chain.effective_tp = 0;
|
||||
this->chain.ap_effect_bonus = 0;
|
||||
this->chain.tp_effect_bonus = 0;
|
||||
this->chain.unused1 = 0;
|
||||
this->chain.unused2 = 0;
|
||||
this->chain.physical_attack_bonus_nte = 0;
|
||||
this->chain.tech_attack_bonus_nte = 0;
|
||||
this->chain.damage = 0;
|
||||
this->chain.strike_count = 1;
|
||||
this->chain.damage_multiplier = 1;
|
||||
@@ -439,7 +433,7 @@ void ActionChainWithConds::compute_attack_medium(shared_ptr<Server> server) {
|
||||
if (!ce) {
|
||||
continue;
|
||||
}
|
||||
if (card_class_is_tech_like(ce->def.card_class())) {
|
||||
if (card_class_is_tech_like(ce->def.card_class(), server->options.is_nte())) {
|
||||
this->chain.attack_medium = AttackMedium::TECH;
|
||||
}
|
||||
}
|
||||
@@ -481,6 +475,85 @@ bool ActionChainWithConds::can_apply_attack() const {
|
||||
return this->check_flag(4) ? false : (this->chain.target_card_ref_count != 0);
|
||||
}
|
||||
|
||||
uint8_t ActionChainWithConds::get_adjusted_move_ability_nte(uint8_t ability) const {
|
||||
for (size_t z = 0; z < this->conditions.size(); z++) {
|
||||
const auto& cond = this->conditions[z];
|
||||
switch (cond.type) {
|
||||
case ConditionType::IMMOBILE:
|
||||
case ConditionType::FREEZE:
|
||||
ability = 0;
|
||||
break;
|
||||
case ConditionType::SET_MV_COST_TO_0:
|
||||
ability = 99;
|
||||
break;
|
||||
case ConditionType::ADD_1_TO_MV_COST:
|
||||
ability--;
|
||||
break;
|
||||
case ConditionType::SCALE_MV_COST:
|
||||
if (cond.value == 0) {
|
||||
ability = 99;
|
||||
} else {
|
||||
ability /= cond.value;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ability;
|
||||
}
|
||||
|
||||
ActionChainWithCondsTrial::ActionChainWithCondsTrial(const ActionChainWithConds& src)
|
||||
: effective_ap(src.chain.effective_ap),
|
||||
effective_tp(src.chain.effective_tp),
|
||||
ap_effect_bonus(src.chain.ap_effect_bonus),
|
||||
damage(src.chain.damage),
|
||||
acting_card_ref(src.chain.acting_card_ref),
|
||||
unknown_card_ref_a3(src.chain.unknown_card_ref_a3),
|
||||
attack_action_card_refs(src.chain.attack_action_card_refs),
|
||||
attack_action_card_ref_count(src.chain.attack_action_card_ref_count),
|
||||
attack_medium(src.chain.attack_medium),
|
||||
target_card_ref_count(src.chain.target_card_ref_count),
|
||||
action_subphase(src.chain.action_subphase),
|
||||
strike_count(src.chain.strike_count),
|
||||
damage_multiplier(src.chain.damage_multiplier),
|
||||
attack_number(src.chain.attack_number),
|
||||
tp_effect_bonus(src.chain.tp_effect_bonus),
|
||||
physical_attack_bonus_nte(src.chain.physical_attack_bonus_nte),
|
||||
tech_attack_bonus_nte(src.chain.tech_attack_bonus_nte),
|
||||
card_ap(src.chain.card_ap),
|
||||
card_tp(src.chain.card_tp),
|
||||
flags(src.chain.flags),
|
||||
conditions(src.conditions),
|
||||
target_card_refs(src.chain.target_card_refs) {}
|
||||
|
||||
ActionChainWithCondsTrial::operator ActionChainWithConds() const {
|
||||
ActionChainWithConds ret;
|
||||
ret.chain.effective_ap = this->effective_ap;
|
||||
ret.chain.effective_tp = this->effective_tp;
|
||||
ret.chain.ap_effect_bonus = this->ap_effect_bonus;
|
||||
ret.chain.damage = this->damage;
|
||||
ret.chain.acting_card_ref = this->acting_card_ref;
|
||||
ret.chain.unknown_card_ref_a3 = this->unknown_card_ref_a3;
|
||||
ret.chain.attack_action_card_refs = this->attack_action_card_refs;
|
||||
ret.chain.attack_action_card_ref_count = this->attack_action_card_ref_count;
|
||||
ret.chain.attack_medium = this->attack_medium;
|
||||
ret.chain.target_card_ref_count = this->target_card_ref_count;
|
||||
ret.chain.action_subphase = this->action_subphase;
|
||||
ret.chain.strike_count = this->strike_count;
|
||||
ret.chain.damage_multiplier = this->damage_multiplier;
|
||||
ret.chain.attack_number = this->attack_number;
|
||||
ret.chain.tp_effect_bonus = this->tp_effect_bonus;
|
||||
ret.chain.physical_attack_bonus_nte = this->physical_attack_bonus_nte;
|
||||
ret.chain.tech_attack_bonus_nte = this->tech_attack_bonus_nte;
|
||||
ret.chain.card_ap = this->card_ap;
|
||||
ret.chain.card_tp = this->card_tp;
|
||||
ret.chain.flags = this->flags;
|
||||
ret.chain.target_card_refs = this->target_card_refs;
|
||||
ret.conditions = this->conditions;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ActionMetadata::ActionMetadata() {
|
||||
this->clear();
|
||||
}
|
||||
@@ -502,19 +575,20 @@ bool ActionMetadata::operator!=(const ActionMetadata& other) const {
|
||||
return !this->operator==(other);
|
||||
}
|
||||
|
||||
std::string ActionMetadata::str() const {
|
||||
string target_card_refs_s = string_for_refs(this->target_card_refs);
|
||||
string defense_card_refs_s = string_for_refs(this->defense_card_refs);
|
||||
string original_attacker_card_refs_s = string_for_refs(this->original_attacker_card_refs);
|
||||
std::string ActionMetadata::str(shared_ptr<const Server> s) const {
|
||||
string card_ref_s = s->debug_str_for_card_ref(this->card_ref);
|
||||
string target_card_refs_s = s->debug_str_for_card_refs(this->target_card_refs);
|
||||
string defense_card_refs_s = s->debug_str_for_card_refs(this->defense_card_refs);
|
||||
string original_attacker_card_refs_s = s->debug_str_for_card_refs(this->original_attacker_card_refs);
|
||||
return string_printf(
|
||||
"ActionMetadata[ref=@%04hX, target_ref_count=%hhu, def_ref_count=%hhu, "
|
||||
"ActionMetadata[ref=%s, target_ref_count=%hhu, def_ref_count=%hhu, "
|
||||
"subphase=%s, def_power=%hhd, def_bonus=%hhd, "
|
||||
"att_bonus=%hhd, flags=%08" PRIX32 ", target_refs=%s, "
|
||||
"defense_refs=%s, original_attacker_refs=%s]",
|
||||
this->card_ref.load(),
|
||||
card_ref_s.c_str(),
|
||||
this->target_card_ref_count,
|
||||
this->defense_card_ref_count,
|
||||
name_for_action_subphase(this->action_subphase),
|
||||
name_for_enum(this->action_subphase),
|
||||
this->defense_power,
|
||||
this->defense_bonus,
|
||||
this->attack_bonus,
|
||||
@@ -531,6 +605,8 @@ void ActionMetadata::clear() {
|
||||
this->action_subphase = ActionSubphase::INVALID_FF;
|
||||
this->defense_power = 0;
|
||||
this->defense_bonus = 0;
|
||||
// TODO: Ep3 NTE doesn't set attack_bonus to zero here. Is the field just
|
||||
// unused in NTE?
|
||||
this->attack_bonus = 0;
|
||||
this->flags = 0;
|
||||
this->target_card_refs.clear(0xFFFF);
|
||||
@@ -598,20 +674,22 @@ HandAndEquipState::HandAndEquipState() {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
std::string HandAndEquipState::str() const {
|
||||
string hand_card_refs_s = string_for_refs(this->hand_card_refs);
|
||||
string set_card_refs_s = string_for_refs(this->set_card_refs);
|
||||
string hand_card_refs2_s = string_for_refs(this->hand_card_refs2);
|
||||
string set_card_refs2_s = string_for_refs(this->set_card_refs2);
|
||||
std::string HandAndEquipState::str(shared_ptr<const Server> s) const {
|
||||
string assist_card_ref_s = s->debug_str_for_card_ref(this->assist_card_ref);
|
||||
string assist_card_ref2_s = s->debug_str_for_card_ref(this->assist_card_ref2);
|
||||
string assist_card_id_s = s->debug_str_for_card_id(this->assist_card_id);
|
||||
string sc_card_ref_s = s->debug_str_for_card_ref(this->sc_card_ref);
|
||||
string hand_card_refs_s = s->debug_str_for_card_refs(this->hand_card_refs);
|
||||
string set_card_refs_s = s->debug_str_for_card_refs(this->set_card_refs);
|
||||
string hand_card_refs2_s = s->debug_str_for_card_refs(this->hand_card_refs2);
|
||||
string set_card_refs2_s = s->debug_str_for_card_refs(this->set_card_refs2);
|
||||
return string_printf(
|
||||
"HandAndEquipState[dice=[%hhu, %hhu], atk=%hhu, def=%hhu, atk2=%hhu, "
|
||||
"a1=%hhu, total_set_cost=%hhu, is_cpu=%hhu, "
|
||||
"assist_flags=%08" PRIX32 ", hand_refs=%s, "
|
||||
"assist_ref=@%04hX, set_refs=%s, sc_ref=@%04hX, "
|
||||
"hand_refs2=%s, set_refs2=%s, assist_ref2=@%04hX, "
|
||||
"assist_set_num=%hu, assist_card_id=#%04hX, "
|
||||
"assist_turns=%hhu, assit_dely=%hhu, atk_bonus=%hhu, "
|
||||
"def_bonus=%hhu, u2=[%hhu, %hhu]]",
|
||||
"a1=%hhu, total_set_cost=%hhu, is_cpu=%hhu, assist_flags=%08" PRIX32 ", "
|
||||
"hand_refs=%s, assist_ref=%s, set_refs=%s, sc_ref=%s, hand_refs2=%s, "
|
||||
"set_refs2=%s, assist_ref2=%s, assist_set_num=%hu, assist_card_id=%s, "
|
||||
"assist_turns=%hhu, assist_delay=%hhu, atk_bonus=%hhu, def_bonus=%hhu, "
|
||||
"u2=[%hhu, %hhu]]",
|
||||
this->dice_results[0],
|
||||
this->dice_results[1],
|
||||
this->atk_points,
|
||||
@@ -622,14 +700,14 @@ std::string HandAndEquipState::str() const {
|
||||
this->is_cpu_player,
|
||||
this->assist_flags.load(),
|
||||
hand_card_refs_s.c_str(),
|
||||
this->assist_card_ref.load(),
|
||||
assist_card_ref_s.c_str(),
|
||||
set_card_refs_s.c_str(),
|
||||
this->sc_card_ref.load(),
|
||||
sc_card_ref_s.c_str(),
|
||||
hand_card_refs2_s.c_str(),
|
||||
set_card_refs2_s.c_str(),
|
||||
this->assist_card_ref2.load(),
|
||||
assist_card_ref2_s.c_str(),
|
||||
this->assist_card_set_number.load(),
|
||||
this->assist_card_id.load(),
|
||||
assist_card_id_s.c_str(),
|
||||
this->assist_remaining_turns,
|
||||
this->assist_delay_turns,
|
||||
this->atk_bonuses,
|
||||
@@ -759,6 +837,23 @@ const char* PlayerBattleStats::name_for_rank(uint8_t rank) {
|
||||
return RANK_NAMES[rank];
|
||||
}
|
||||
|
||||
PlayerBattleStatsTrial::PlayerBattleStatsTrial(const PlayerBattleStats& data)
|
||||
: damage_given(data.damage_given.load()),
|
||||
damage_taken(data.damage_taken.load()),
|
||||
num_opponent_cards_destroyed(data.num_opponent_cards_destroyed.load()),
|
||||
num_owned_cards_destroyed(data.num_owned_cards_destroyed.load()),
|
||||
total_move_distance(data.total_move_distance.load()) {}
|
||||
|
||||
PlayerBattleStatsTrial::operator PlayerBattleStats() const {
|
||||
PlayerBattleStats ret;
|
||||
ret.damage_given = this->damage_given.load();
|
||||
ret.damage_taken = this->damage_taken.load();
|
||||
ret.num_opponent_cards_destroyed = this->num_opponent_cards_destroyed.load();
|
||||
ret.num_owned_cards_destroyed = this->num_owned_cards_destroyed.load();
|
||||
ret.total_move_distance = this->total_move_distance.load();
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool is_card_within_range(
|
||||
const parray<uint8_t, 9 * 9>& range,
|
||||
const Location& anchor_loc,
|
||||
|
||||
@@ -13,19 +13,20 @@ class Server;
|
||||
class Card;
|
||||
|
||||
struct Condition {
|
||||
ConditionType type;
|
||||
uint8_t remaining_turns;
|
||||
int8_t a_arg_value;
|
||||
uint8_t dice_roll_value;
|
||||
uint8_t flags;
|
||||
uint8_t card_definition_effect_index;
|
||||
le_uint16_t card_ref;
|
||||
le_int16_t value;
|
||||
le_uint16_t condition_giver_card_ref;
|
||||
uint8_t random_percent;
|
||||
int8_t value8;
|
||||
uint8_t order;
|
||||
uint8_t unknown_a8;
|
||||
/* 00 */ ConditionType type;
|
||||
/* 01 */ uint8_t remaining_turns;
|
||||
/* 02 */ int8_t a_arg_value;
|
||||
/* 03 */ uint8_t dice_roll_value;
|
||||
/* 04 */ uint8_t flags;
|
||||
/* 05 */ uint8_t card_definition_effect_index;
|
||||
/* 06 */ le_uint16_t card_ref;
|
||||
/* 08 */ le_int16_t value;
|
||||
/* 0A */ le_uint16_t condition_giver_card_ref;
|
||||
/* 0C */ uint8_t random_percent;
|
||||
/* 0D */ int8_t value8;
|
||||
/* 0E */ uint8_t order;
|
||||
/* 0F */ uint8_t unknown_a8;
|
||||
/* 10 */
|
||||
|
||||
Condition();
|
||||
bool operator==(const Condition& other) const;
|
||||
@@ -34,38 +35,40 @@ struct Condition {
|
||||
void clear();
|
||||
void clear_FF();
|
||||
|
||||
std::string str() const;
|
||||
std::string str(std::shared_ptr<const Server> s) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct EffectResult {
|
||||
le_uint16_t attacker_card_ref;
|
||||
le_uint16_t target_card_ref;
|
||||
int8_t value;
|
||||
int8_t current_hp;
|
||||
int8_t ap;
|
||||
int8_t tp;
|
||||
uint8_t flags;
|
||||
int8_t operation; // May be a negative condition number
|
||||
uint8_t condition_index;
|
||||
uint8_t dice_roll_value;
|
||||
/* 00 */ le_uint16_t attacker_card_ref;
|
||||
/* 02 */ le_uint16_t target_card_ref;
|
||||
/* 04 */ int8_t value;
|
||||
/* 05 */ int8_t current_hp;
|
||||
/* 06 */ int8_t ap;
|
||||
/* 07 */ int8_t tp;
|
||||
/* 08 */ uint8_t flags;
|
||||
/* 09 */ int8_t operation; // May be a negative condition number
|
||||
/* 0A */ uint8_t condition_index;
|
||||
/* 0B */ uint8_t dice_roll_value;
|
||||
/* 0C */
|
||||
|
||||
EffectResult();
|
||||
bool operator==(const EffectResult& other) const;
|
||||
bool operator!=(const EffectResult& other) const;
|
||||
|
||||
std::string str() const;
|
||||
|
||||
void clear();
|
||||
|
||||
std::string str(std::shared_ptr<const Server> s) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct CardShortStatus {
|
||||
le_uint16_t card_ref;
|
||||
le_uint16_t current_hp;
|
||||
le_uint32_t card_flags;
|
||||
Location loc;
|
||||
le_uint16_t unused1;
|
||||
int8_t max_hp;
|
||||
uint8_t unused2;
|
||||
/* 00 */ le_uint16_t card_ref;
|
||||
/* 02 */ le_uint16_t current_hp;
|
||||
/* 04 */ le_uint32_t card_flags;
|
||||
/* 08 */ Location loc;
|
||||
/* 0C */ le_uint16_t unused1;
|
||||
/* 0E */ int8_t max_hp;
|
||||
/* 0F */ uint8_t unused2;
|
||||
/* 10 */
|
||||
|
||||
CardShortStatus();
|
||||
bool operator==(const CardShortStatus& other) const;
|
||||
@@ -74,18 +77,20 @@ struct CardShortStatus {
|
||||
void clear();
|
||||
void clear_FF();
|
||||
|
||||
std::string str() const;
|
||||
std::string str(std::shared_ptr<const Server> s) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct ActionState {
|
||||
le_uint16_t client_id;
|
||||
uint8_t unused;
|
||||
Direction facing_direction;
|
||||
le_uint16_t attacker_card_ref;
|
||||
le_uint16_t defense_card_ref;
|
||||
parray<le_uint16_t, 4 * 9> target_card_refs;
|
||||
parray<le_uint16_t, 9> action_card_refs;
|
||||
le_uint16_t original_attacker_card_ref;
|
||||
/* 00 */ le_uint16_t client_id;
|
||||
/* 02 */ uint8_t unused;
|
||||
/* 03 */ Direction facing_direction;
|
||||
/* 04 */ le_uint16_t attacker_card_ref;
|
||||
/* 06 */ le_uint16_t defense_card_ref;
|
||||
/* 08 */ parray<le_uint16_t, 4 * 9> target_card_refs;
|
||||
/* 50 */ parray<le_uint16_t, 8> action_card_refs;
|
||||
/* 60 */ le_uint16_t unused2;
|
||||
/* 62 */ le_uint16_t original_attacker_card_ref;
|
||||
/* 64 */
|
||||
|
||||
ActionState();
|
||||
bool operator==(const ActionState& other) const;
|
||||
@@ -93,31 +98,34 @@ struct ActionState {
|
||||
|
||||
void clear();
|
||||
|
||||
std::string str() const;
|
||||
std::string str(std::shared_ptr<const Server> s) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct ActionChain {
|
||||
int8_t effective_ap;
|
||||
int8_t effective_tp;
|
||||
int8_t ap_effect_bonus;
|
||||
int8_t damage;
|
||||
le_uint16_t acting_card_ref;
|
||||
le_uint16_t unknown_card_ref_a3;
|
||||
parray<le_uint16_t, 8> attack_action_card_refs;
|
||||
uint8_t attack_action_card_ref_count;
|
||||
AttackMedium attack_medium;
|
||||
uint8_t target_card_ref_count;
|
||||
ActionSubphase action_subphase;
|
||||
uint8_t strike_count;
|
||||
int8_t damage_multiplier;
|
||||
uint8_t attack_number;
|
||||
int8_t tp_effect_bonus;
|
||||
uint8_t unused1;
|
||||
uint8_t unused2;
|
||||
int8_t card_ap;
|
||||
int8_t card_tp;
|
||||
le_uint32_t flags;
|
||||
parray<le_uint16_t, 4 * 9> target_card_refs;
|
||||
// Note: Episode 3 Trial Edition has a different format for this structure.
|
||||
// See ActionChainWithCondsTrial for details.
|
||||
/* 00 */ int8_t effective_ap;
|
||||
/* 01 */ int8_t effective_tp;
|
||||
/* 02 */ int8_t ap_effect_bonus;
|
||||
/* 03 */ int8_t damage;
|
||||
/* 04 */ le_uint16_t acting_card_ref;
|
||||
/* 06 */ le_uint16_t unknown_card_ref_a3;
|
||||
/* 08 */ parray<le_uint16_t, 8> attack_action_card_refs;
|
||||
/* 18 */ uint8_t attack_action_card_ref_count;
|
||||
/* 19 */ AttackMedium attack_medium;
|
||||
/* 1A */ uint8_t target_card_ref_count;
|
||||
/* 1B */ ActionSubphase action_subphase;
|
||||
/* 1C */ uint8_t strike_count;
|
||||
/* 1D */ int8_t damage_multiplier;
|
||||
/* 1E */ uint8_t attack_number;
|
||||
/* 1F */ int8_t tp_effect_bonus;
|
||||
/* 20 */ int8_t physical_attack_bonus_nte;
|
||||
/* 21 */ int8_t tech_attack_bonus_nte;
|
||||
/* 22 */ int8_t card_ap;
|
||||
/* 23 */ int8_t card_tp;
|
||||
/* 24 */ le_uint32_t flags;
|
||||
/* 28 */ parray<le_uint16_t, 4 * 9> target_card_refs;
|
||||
/* 70 */
|
||||
|
||||
ActionChain();
|
||||
bool operator==(const ActionChain& other) const;
|
||||
@@ -126,12 +134,13 @@ struct ActionChain {
|
||||
void clear();
|
||||
void clear_FF();
|
||||
|
||||
std::string str() const;
|
||||
std::string str(std::shared_ptr<const Server> s) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct ActionChainWithConds {
|
||||
ActionChain chain;
|
||||
parray<Condition, 9> conditions;
|
||||
/* 0000 */ ActionChain chain;
|
||||
/* 0070 */ parray<Condition, 9> conditions;
|
||||
/* 0100 */
|
||||
|
||||
ActionChainWithConds();
|
||||
bool operator==(const ActionChainWithConds& other) const;
|
||||
@@ -161,28 +170,61 @@ struct ActionChainWithConds {
|
||||
void set_action_subphase_from_card(std::shared_ptr<const Card> card);
|
||||
bool can_apply_attack() const;
|
||||
|
||||
std::string str() const;
|
||||
uint8_t get_adjusted_move_ability_nte(uint8_t ability) const;
|
||||
|
||||
std::string str(std::shared_ptr<const Server> s) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct ActionChainWithCondsTrial {
|
||||
/* 0000 */ int8_t effective_ap;
|
||||
/* 0001 */ int8_t effective_tp;
|
||||
/* 0002 */ int8_t ap_effect_bonus;
|
||||
/* 0003 */ int8_t damage;
|
||||
/* 0004 */ le_uint16_t acting_card_ref;
|
||||
/* 0006 */ le_uint16_t unknown_card_ref_a3;
|
||||
/* 0008 */ parray<le_uint16_t, 8> attack_action_card_refs;
|
||||
/* 0018 */ uint8_t attack_action_card_ref_count;
|
||||
/* 0019 */ AttackMedium attack_medium;
|
||||
/* 001A */ uint8_t target_card_ref_count;
|
||||
/* 001B */ ActionSubphase action_subphase;
|
||||
/* 001C */ uint8_t strike_count;
|
||||
/* 001D */ int8_t damage_multiplier;
|
||||
/* 001E */ uint8_t attack_number;
|
||||
/* 001F */ int8_t tp_effect_bonus;
|
||||
/* 0020 */ int8_t physical_attack_bonus_nte;
|
||||
/* 0021 */ int8_t tech_attack_bonus_nte;
|
||||
/* 0022 */ int8_t card_ap;
|
||||
/* 0023 */ int8_t card_tp;
|
||||
/* 0024 */ le_uint32_t flags;
|
||||
// The only difference between this structure and ActionChainWithConds is that
|
||||
// these two fields are in the opposite order.
|
||||
/* 0028 */ parray<Condition, 9> conditions;
|
||||
/* 00B8 */ parray<le_uint16_t, 4 * 9> target_card_refs;
|
||||
/* 0100 */
|
||||
|
||||
ActionChainWithCondsTrial() = default;
|
||||
ActionChainWithCondsTrial(const ActionChainWithConds& src);
|
||||
operator ActionChainWithConds() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct ActionMetadata {
|
||||
le_uint16_t card_ref;
|
||||
uint8_t target_card_ref_count;
|
||||
uint8_t defense_card_ref_count;
|
||||
ActionSubphase action_subphase;
|
||||
int8_t defense_power;
|
||||
int8_t defense_bonus;
|
||||
int8_t attack_bonus;
|
||||
le_uint32_t flags;
|
||||
parray<le_uint16_t, 4 * 9> target_card_refs;
|
||||
parray<le_uint16_t, 8> defense_card_refs;
|
||||
parray<le_uint16_t, 8> original_attacker_card_refs;
|
||||
/* 00 */ le_uint16_t card_ref;
|
||||
/* 02 */ uint8_t target_card_ref_count;
|
||||
/* 03 */ uint8_t defense_card_ref_count;
|
||||
/* 04 */ ActionSubphase action_subphase;
|
||||
/* 05 */ int8_t defense_power;
|
||||
/* 06 */ int8_t defense_bonus;
|
||||
/* 07 */ int8_t attack_bonus;
|
||||
/* 08 */ le_uint32_t flags;
|
||||
/* 0C */ parray<le_uint16_t, 4 * 9> target_card_refs;
|
||||
/* 54 */ parray<le_uint16_t, 8> defense_card_refs;
|
||||
/* 64 */ parray<le_uint16_t, 8> original_attacker_card_refs;
|
||||
/* 74 */
|
||||
|
||||
ActionMetadata();
|
||||
bool operator==(const ActionMetadata& other) const;
|
||||
bool operator!=(const ActionMetadata& other) const;
|
||||
|
||||
std::string str() const;
|
||||
|
||||
void clear();
|
||||
void clear_FF();
|
||||
|
||||
@@ -197,31 +239,34 @@ struct ActionMetadata {
|
||||
uint16_t defense_card_ref,
|
||||
std::shared_ptr<Card> card,
|
||||
uint16_t original_attacker_card_ref);
|
||||
|
||||
std::string str(std::shared_ptr<const Server> s) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct HandAndEquipState {
|
||||
parray<uint8_t, 2> dice_results;
|
||||
uint8_t atk_points;
|
||||
uint8_t def_points;
|
||||
uint8_t atk_points2; // TODO: rename this to something more appropriate
|
||||
uint8_t unknown_a1;
|
||||
uint8_t total_set_cards_cost;
|
||||
uint8_t is_cpu_player;
|
||||
le_uint32_t assist_flags;
|
||||
parray<le_uint16_t, 6> hand_card_refs;
|
||||
le_uint16_t assist_card_ref;
|
||||
parray<le_uint16_t, 8> set_card_refs;
|
||||
le_uint16_t sc_card_ref;
|
||||
parray<le_uint16_t, 6> hand_card_refs2;
|
||||
parray<le_uint16_t, 8> set_card_refs2;
|
||||
le_uint16_t assist_card_ref2;
|
||||
le_uint16_t assist_card_set_number;
|
||||
le_uint16_t assist_card_id;
|
||||
uint8_t assist_remaining_turns;
|
||||
uint8_t assist_delay_turns;
|
||||
uint8_t atk_bonuses;
|
||||
uint8_t def_bonuses;
|
||||
parray<uint8_t, 2> unused2;
|
||||
/* 00 */ parray<uint8_t, 2> dice_results;
|
||||
/* 02 */ uint8_t atk_points;
|
||||
/* 03 */ uint8_t def_points;
|
||||
/* 04 */ uint8_t atk_points2; // TODO: rename this to something more appropriate
|
||||
/* 05 */ uint8_t unknown_a1;
|
||||
/* 06 */ uint8_t total_set_cards_cost;
|
||||
/* 07 */ uint8_t is_cpu_player;
|
||||
/* 08 */ le_uint32_t assist_flags;
|
||||
/* 0C */ parray<le_uint16_t, 6> hand_card_refs;
|
||||
/* 18 */ le_uint16_t assist_card_ref;
|
||||
/* 1A */ parray<le_uint16_t, 8> set_card_refs;
|
||||
/* 2A */ le_uint16_t sc_card_ref;
|
||||
/* 2C */ parray<le_uint16_t, 6> hand_card_refs2;
|
||||
/* 38 */ parray<le_uint16_t, 8> set_card_refs2;
|
||||
/* 48 */ le_uint16_t assist_card_ref2;
|
||||
/* 4A */ le_uint16_t assist_card_set_number;
|
||||
/* 4C */ le_uint16_t assist_card_id;
|
||||
/* 4E */ uint8_t assist_remaining_turns;
|
||||
/* 4F */ uint8_t assist_delay_turns;
|
||||
/* 50 */ uint8_t atk_bonuses;
|
||||
/* 51 */ uint8_t def_bonuses;
|
||||
/* 52 */ parray<uint8_t, 2> unused2;
|
||||
/* 54 */
|
||||
|
||||
HandAndEquipState();
|
||||
bool operator==(const HandAndEquipState& other) const;
|
||||
@@ -230,30 +275,31 @@ struct HandAndEquipState {
|
||||
void clear();
|
||||
void clear_FF();
|
||||
|
||||
std::string str() const;
|
||||
std::string str(std::shared_ptr<const Server> s) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerBattleStats {
|
||||
le_uint16_t damage_given;
|
||||
le_uint16_t damage_taken;
|
||||
le_uint16_t num_opponent_cards_destroyed;
|
||||
le_uint16_t num_owned_cards_destroyed;
|
||||
le_uint16_t total_move_distance;
|
||||
le_uint16_t num_cards_set;
|
||||
le_uint16_t num_item_or_creature_cards_set;
|
||||
le_uint16_t num_attack_actions_set;
|
||||
le_uint16_t num_tech_cards_set;
|
||||
le_uint16_t num_assist_cards_set;
|
||||
le_uint16_t defense_actions_set_on_self;
|
||||
le_uint16_t defense_actions_set_on_ally;
|
||||
le_uint16_t num_cards_drawn;
|
||||
le_uint16_t max_attack_damage;
|
||||
le_uint16_t max_attack_combo_size;
|
||||
le_uint16_t num_attacks_given;
|
||||
le_uint16_t num_attacks_taken;
|
||||
le_uint16_t sc_damage_taken;
|
||||
le_uint16_t action_card_negated_damage;
|
||||
le_uint16_t unused;
|
||||
/* 00 */ le_uint16_t damage_given;
|
||||
/* 02 */ le_uint16_t damage_taken;
|
||||
/* 04 */ le_uint16_t num_opponent_cards_destroyed;
|
||||
/* 06 */ le_uint16_t num_owned_cards_destroyed;
|
||||
/* 08 */ le_uint16_t total_move_distance;
|
||||
/* 0A */ le_uint16_t num_cards_set;
|
||||
/* 0C */ le_uint16_t num_item_or_creature_cards_set;
|
||||
/* 0E */ le_uint16_t num_attack_actions_set;
|
||||
/* 10 */ le_uint16_t num_tech_cards_set;
|
||||
/* 12 */ le_uint16_t num_assist_cards_set;
|
||||
/* 14 */ le_uint16_t defense_actions_set_on_self;
|
||||
/* 16 */ le_uint16_t defense_actions_set_on_ally;
|
||||
/* 18 */ le_uint16_t num_cards_drawn;
|
||||
/* 1A */ le_uint16_t max_attack_damage;
|
||||
/* 1C */ le_uint16_t max_attack_combo_size;
|
||||
/* 1E */ le_uint16_t num_attacks_given;
|
||||
/* 20 */ le_uint16_t num_attacks_taken;
|
||||
/* 22 */ le_uint16_t sc_damage_taken;
|
||||
/* 24 */ le_uint16_t action_card_negated_damage;
|
||||
/* 26 */ le_uint16_t unused;
|
||||
/* 28 */
|
||||
|
||||
PlayerBattleStats();
|
||||
void clear();
|
||||
@@ -266,6 +312,19 @@ struct PlayerBattleStats {
|
||||
static const char* name_for_rank(uint8_t rank);
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerBattleStatsTrial {
|
||||
/* 00 */ le_uint32_t damage_given = 0;
|
||||
/* 04 */ le_uint32_t damage_taken = 0;
|
||||
/* 08 */ le_uint32_t num_opponent_cards_destroyed = 0;
|
||||
/* 0C */ le_uint32_t num_owned_cards_destroyed = 0;
|
||||
/* 10 */ le_uint32_t total_move_distance = 0;
|
||||
/* 14 */
|
||||
|
||||
PlayerBattleStatsTrial() = default;
|
||||
PlayerBattleStatsTrial(const PlayerBattleStats& data);
|
||||
operator PlayerBattleStats() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
std::vector<uint16_t> get_card_refs_within_range(
|
||||
const parray<uint8_t, 9 * 9>& range,
|
||||
const Location& loc,
|
||||
|
||||
+270
-155
@@ -1,5 +1,7 @@
|
||||
#include "RulerServer.hh"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "Server.hh"
|
||||
|
||||
using namespace std;
|
||||
@@ -204,6 +206,8 @@ const ActionChainWithConds* RulerServer::action_chain_with_conds_for_card_ref(
|
||||
uint16_t card_ref) const {
|
||||
uint8_t client_id = client_id_for_card_ref(card_ref);
|
||||
if (client_id != 0xFF) {
|
||||
// There appears to be a bug in Trial Edition: the bound on this loop is
|
||||
// 0x10, not 9.
|
||||
for (size_t z = 0; z < 9; z++) {
|
||||
const auto* chain = &this->set_card_action_chains[client_id]->at(z);
|
||||
if (card_ref == chain->chain.acting_card_ref) {
|
||||
@@ -235,19 +239,25 @@ bool RulerServer::card_has_pierce_or_rampage(
|
||||
uint16_t action_card_ref,
|
||||
uint8_t def_effect_index,
|
||||
AttackMedium attack_medium) const {
|
||||
auto short_statuses = (client_id != 0xFF) ? this->short_statuses[client_id] : nullptr;
|
||||
auto short_statuses = (client_id < 4) ? this->short_statuses[client_id] : nullptr;
|
||||
*out_has_rampage = false;
|
||||
|
||||
if (cond_type == ConditionType::NONE) {
|
||||
return false;
|
||||
bool ret;
|
||||
bool is_nte = this->server()->options.is_nte();
|
||||
if (is_nte) {
|
||||
ret = true;
|
||||
} else {
|
||||
if (cond_type == ConditionType::NONE) {
|
||||
return false;
|
||||
}
|
||||
ret = this->check_usability_or_apply_condition_for_card_refs(
|
||||
action_card_ref,
|
||||
attacker_card_ref,
|
||||
// Original code omitted this null check and presumably could crash here
|
||||
short_statuses ? short_statuses->at(0).card_ref.load() : 0xFFFF,
|
||||
def_effect_index,
|
||||
attack_medium);
|
||||
}
|
||||
bool ret = this->check_usability_or_apply_condition_for_card_refs(
|
||||
action_card_ref,
|
||||
attacker_card_ref,
|
||||
// Original code omitted this null check and presumably could crash here
|
||||
short_statuses ? short_statuses->at(0).card_ref.load() : 0xFFFF,
|
||||
def_effect_index,
|
||||
attack_medium);
|
||||
|
||||
switch (cond_type) {
|
||||
case ConditionType::RAMPAGE:
|
||||
@@ -280,7 +290,10 @@ bool RulerServer::card_has_pierce_or_rampage(
|
||||
if (short_statuses) {
|
||||
const auto& sc_status = short_statuses->at(0);
|
||||
auto ce = this->definition_for_card_ref(sc_status.card_ref);
|
||||
if (ce && (this->get_card_ref_max_hp(sc_status.card_ref) <= sc_status.current_hp * 2)) {
|
||||
// This appears to be an NTE bug: Major Pierce doesn't work on Arkz SCs.
|
||||
if (ce &&
|
||||
(!is_nte || (ce->def.type == CardType::HUNTERS_SC)) &&
|
||||
(this->get_card_ref_max_hp(sc_status.card_ref) <= sc_status.current_hp * 2)) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -290,8 +303,7 @@ bool RulerServer::card_has_pierce_or_rampage(
|
||||
}
|
||||
}
|
||||
|
||||
bool RulerServer::attack_action_has_rampage_and_not_pierce(
|
||||
const ActionState& pa, uint16_t card_ref) const {
|
||||
bool RulerServer::attack_action_has_rampage_and_not_pierce(const ActionState& pa, uint16_t card_ref) const {
|
||||
uint16_t orig_card_ref;
|
||||
uint16_t effective_range_card_id;
|
||||
TargetMode effective_target_mode;
|
||||
@@ -365,15 +377,15 @@ bool RulerServer::attack_action_has_rampage_and_not_pierce(
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RulerServer::attack_action_has_pierce_and_not_rampage(
|
||||
const ActionState& pa, uint8_t client_id) {
|
||||
if ((client_id_for_card_ref(pa.attacker_card_ref) == 0xFF) || (client_id == 0xFF)) {
|
||||
bool RulerServer::attack_action_has_pierce_and_not_rampage(const ActionState& pa, uint8_t client_id) const {
|
||||
if ((client_id_for_card_ref(pa.attacker_card_ref) == 0xFF) || (client_id >= 4)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_nte = this->server()->options.is_nte();
|
||||
auto attack_medium = this->get_attack_medium(pa);
|
||||
auto stat = this->short_statuses[client_id];
|
||||
if (!stat || !this->card_exists_by_status(stat->at(0)) || (stat->at(0).card_ref == 0xFFFF)) {
|
||||
if (!stat || (!is_nte && !this->card_exists_by_status(stat->at(0))) || (stat->at(0).card_ref == 0xFFFF)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -392,6 +404,33 @@ bool RulerServer::attack_action_has_pierce_and_not_rampage(
|
||||
last_action_card_index = z;
|
||||
}
|
||||
|
||||
auto check_chain = [&]() -> optional<bool> {
|
||||
const auto* chain = this->action_chain_with_conds_for_card_ref(pa.attacker_card_ref);
|
||||
if (chain) {
|
||||
for (ssize_t cond_index = 8; cond_index >= 0; cond_index--) {
|
||||
bool has_rampage = false;
|
||||
if (this->card_has_pierce_or_rampage(
|
||||
client_id, chain->conditions[cond_index].type, &has_rampage,
|
||||
pa.attacker_card_ref, chain->conditions[cond_index].card_ref,
|
||||
chain->conditions[cond_index].card_definition_effect_index,
|
||||
attack_medium)) {
|
||||
return true;
|
||||
}
|
||||
if (has_rampage) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullopt;
|
||||
};
|
||||
|
||||
if (is_nte) {
|
||||
auto res = check_chain();
|
||||
if (res.has_value()) {
|
||||
return res.value();
|
||||
}
|
||||
}
|
||||
|
||||
for (; last_action_card_index >= 0; last_action_card_index--) {
|
||||
auto ce = this->definition_for_card_ref(
|
||||
pa.action_card_refs[last_action_card_index]);
|
||||
@@ -418,20 +457,10 @@ bool RulerServer::attack_action_has_pierce_and_not_rampage(
|
||||
}
|
||||
}
|
||||
|
||||
const auto* chain = this->action_chain_with_conds_for_card_ref(pa.attacker_card_ref);
|
||||
if (chain) {
|
||||
for (ssize_t cond_index = 8; cond_index >= 0; cond_index--) {
|
||||
bool has_rampage = false;
|
||||
if (this->card_has_pierce_or_rampage(
|
||||
client_id, chain->conditions[cond_index].type, &has_rampage,
|
||||
pa.attacker_card_ref, chain->conditions[cond_index].card_ref,
|
||||
chain->conditions[cond_index].card_definition_effect_index,
|
||||
attack_medium)) {
|
||||
return true;
|
||||
}
|
||||
if (has_rampage) {
|
||||
return false;
|
||||
}
|
||||
if (!is_nte) {
|
||||
auto res = check_chain();
|
||||
if (res.has_value()) {
|
||||
return res.value();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,9 +663,11 @@ bool RulerServer::card_ref_has_free_maneuver(uint16_t card_ref) const {
|
||||
}
|
||||
|
||||
bool RulerServer::card_ref_is_aerial(uint16_t card_ref) const {
|
||||
const auto* stat = this->short_status_for_card_ref(card_ref);
|
||||
if (!stat || !this->card_exists_by_status(*stat)) {
|
||||
return false;
|
||||
if (!this->server()->options.is_nte()) {
|
||||
const auto* stat = this->short_status_for_card_ref(card_ref);
|
||||
if (!stat || !this->card_exists_by_status(*stat)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t client_id = client_id_for_card_ref(card_ref);
|
||||
@@ -791,10 +822,13 @@ bool RulerServer::check_pierce_and_rampage(
|
||||
uint16_t action_card_ref,
|
||||
uint8_t def_effect_index,
|
||||
AttackMedium attack_medium) const {
|
||||
bool is_nte = this->server()->options.is_nte();
|
||||
|
||||
// Note: NTE doesn't set this to zero; it apparently expects the caller to.
|
||||
*out_has_pierce = false;
|
||||
|
||||
const auto* card_short_status = this->short_status_for_card_ref(card_ref);
|
||||
if (cond_type == ConditionType::NONE) {
|
||||
if (!is_nte && (cond_type == ConditionType::NONE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -816,8 +850,9 @@ bool RulerServer::check_pierce_and_rampage(
|
||||
client_short_statuses = nullptr;
|
||||
}
|
||||
|
||||
bool apply_check_result = this->check_usability_or_apply_condition_for_card_refs(
|
||||
action_card_ref, attacker_card_ref, card_ref, def_effect_index, attack_medium);
|
||||
bool apply_check_result = (is_nte ||
|
||||
this->check_usability_or_apply_condition_for_card_refs(
|
||||
action_card_ref, attacker_card_ref, card_ref, def_effect_index, attack_medium));
|
||||
|
||||
switch (cond_type) {
|
||||
case ConditionType::PIERCE:
|
||||
@@ -903,7 +938,9 @@ bool RulerServer::check_usability_or_condition_apply(
|
||||
uint8_t def_effect_index,
|
||||
bool is_item_usability_check,
|
||||
AttackMedium attack_medium) const {
|
||||
auto log = this->server()->log_stack(string_printf("check_usability_or_condition_apply(%02hhX, #%04hX, %02hhX, #%04hX, #%04hX, %02hhX, %s, %s): ", client_id1, card_id1, client_id2, card_id2, card_id3, def_effect_index, is_item_usability_check ? "true" : "false", name_for_attack_medium(attack_medium)));
|
||||
auto s = this->server();
|
||||
bool is_nte = s->options.is_nte();
|
||||
auto log = s->log_stack(string_printf("check_usability_or_condition_apply(%02hhX, #%04hX, %02hhX, #%04hX, #%04hX, %02hhX, %s, %s): ", client_id1, card_id1, client_id2, card_id2, card_id3, def_effect_index, is_item_usability_check ? "true" : "false", name_for_enum(attack_medium)));
|
||||
|
||||
if (static_cast<uint8_t>(attack_medium) & 0x80) {
|
||||
attack_medium = AttackMedium::UNKNOWN;
|
||||
@@ -916,7 +953,7 @@ bool RulerServer::check_usability_or_condition_apply(
|
||||
log.debug("ce1 missing");
|
||||
return false;
|
||||
}
|
||||
if ((ce1->def.type == CardType::ITEM) && this->card_id_is_boss_sc(card_id2)) {
|
||||
if (!is_nte && (ce1->def.type == CardType::ITEM) && this->card_id_is_boss_sc(card_id2)) {
|
||||
log.debug("ce1 is item and card_id2 is boss sc");
|
||||
return false;
|
||||
}
|
||||
@@ -931,7 +968,7 @@ bool RulerServer::check_usability_or_condition_apply(
|
||||
}
|
||||
criterion_code = ce1->def.effects[def_effect_index].apply_criterion;
|
||||
}
|
||||
log.debug("criterion_code=%s", name_for_criterion_code(criterion_code));
|
||||
log.debug("criterion_code=%s", name_for_enum(criterion_code));
|
||||
|
||||
// For item usability checks, prevent criteria that depend on player
|
||||
// positioning/team setup
|
||||
@@ -952,8 +989,8 @@ bool RulerServer::check_usability_or_condition_apply(
|
||||
// creature card is usable, the two client IDs should be the same or the
|
||||
// second should not be given, so we'd return true if the criterion passes. If
|
||||
// neither of these cases apply, we should return false as a failsafe even if
|
||||
// the criterion passes.
|
||||
bool ret = (!(def_effect_index & 0x80) || (client_id1 == client_id2)) || (client_id2 == 0xFF);
|
||||
// the criterion passes. NTE did not have such a check.
|
||||
bool ret = is_nte || (!(def_effect_index & 0x80) || (client_id1 == client_id2)) || (client_id2 == 0xFF);
|
||||
switch (criterion_code) {
|
||||
case CriterionCode::NONE:
|
||||
return ret;
|
||||
@@ -1361,6 +1398,7 @@ uint16_t RulerServer::compute_attack_or_defense_costs(
|
||||
cost_bias++;
|
||||
}
|
||||
|
||||
bool is_nte = this->server()->options.is_nte();
|
||||
if (pa.action_card_refs[0] == 0xFFFF) {
|
||||
total_cost = cost_bias + 1;
|
||||
} else {
|
||||
@@ -1369,26 +1407,29 @@ uint16_t RulerServer::compute_attack_or_defense_costs(
|
||||
tech_cost_bias = -1;
|
||||
}
|
||||
|
||||
auto s = this->server();
|
||||
for (size_t z = 0; pa.action_card_refs[z] != 0xFFFF; z++) {
|
||||
auto ce = this->definition_for_card_ref(pa.action_card_refs[z]);
|
||||
if (has_mighty_knuckle || !ce || (ce->def.type != CardType::ACTION)) {
|
||||
return 99;
|
||||
}
|
||||
total_cost += (ce->def.self_cost + cost_bias);
|
||||
if (card_class_is_tech_like(ce->def.card_class())) {
|
||||
if (card_class_is_tech_like(ce->def.card_class(), is_nte)) {
|
||||
total_cost += tech_cost_bias;
|
||||
}
|
||||
total_ally_cost += ce->def.ally_cost;
|
||||
if (this->card_has_mighty_knuckle(pa.action_card_refs[z])) {
|
||||
has_mighty_knuckle = true;
|
||||
}
|
||||
size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(pa.client_id);
|
||||
for (size_t w = 0; w < num_assists; w++) {
|
||||
auto assist_effect = this->assist_server->get_active_assist_by_index(w);
|
||||
if (assist_effect == AssistEffect::INFLATION) {
|
||||
assist_cost_bias++;
|
||||
} else if (assist_effect == AssistEffect::DEFLATION) {
|
||||
assist_cost_bias--;
|
||||
if (!is_nte) {
|
||||
size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(pa.client_id);
|
||||
for (size_t w = 0; w < num_assists; w++) {
|
||||
auto assist_effect = this->assist_server->get_active_assist_by_index(w);
|
||||
if (assist_effect == AssistEffect::INFLATION) {
|
||||
assist_cost_bias++;
|
||||
} else if (assist_effect == AssistEffect::DEFLATION) {
|
||||
assist_cost_bias--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1397,8 +1438,11 @@ uint16_t RulerServer::compute_attack_or_defense_costs(
|
||||
size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(pa.client_id);
|
||||
for (size_t w = 0; w < num_assists; w++) {
|
||||
auto assist_effect = this->assist_server->get_active_assist_by_index(w);
|
||||
if ((assist_effect == AssistEffect::BATTLE_ROYALE) &&
|
||||
(pa.action_card_refs[0] == 0xFFFF)) {
|
||||
if (is_nte && (assist_effect == AssistEffect::INFLATION)) {
|
||||
assist_cost_bias++;
|
||||
} else if (is_nte && (assist_effect == AssistEffect::DEFLATION)) {
|
||||
assist_cost_bias--;
|
||||
} else if ((assist_effect == AssistEffect::BATTLE_ROYALE) && (pa.action_card_refs[0] == 0xFFFF)) {
|
||||
total_cost = 0;
|
||||
final_cost = 0;
|
||||
}
|
||||
@@ -1406,7 +1450,9 @@ uint16_t RulerServer::compute_attack_or_defense_costs(
|
||||
|
||||
if (has_mighty_knuckle) {
|
||||
if (!allow_mighty_knuckle) {
|
||||
final_cost = 0;
|
||||
if (!is_nte) {
|
||||
final_cost = 0;
|
||||
}
|
||||
} else {
|
||||
final_cost = max<int16_t>(final_cost, this->hand_and_equip_states[pa.client_id]->atk_points);
|
||||
}
|
||||
@@ -1424,9 +1470,9 @@ bool RulerServer::compute_effective_range_and_target_mode_for_attack(
|
||||
TargetMode* out_effective_target_mode,
|
||||
uint16_t* out_orig_card_ref) const {
|
||||
size_t z;
|
||||
for (z = 0; (z < 9) && (pa.action_card_refs[z] != 0xFFFF); z++) {
|
||||
for (z = 0; (z < 8) && (pa.action_card_refs[z] != 0xFFFF); z++) {
|
||||
}
|
||||
if (z >= 9) {
|
||||
if (z >= 8) {
|
||||
return false;
|
||||
}
|
||||
uint16_t card_ref = (z == 0) ? pa.attacker_card_ref : pa.action_card_refs[z - 1];
|
||||
@@ -1449,9 +1495,11 @@ bool RulerServer::compute_effective_range_and_target_mode_for_attack(
|
||||
auto target_mode = ce->def.target_mode;
|
||||
if (this->card_ref_or_sc_has_fixed_range(pa.attacker_card_ref)) {
|
||||
card_id = this->card_id_for_card_ref(pa.attacker_card_ref);
|
||||
auto sc_ce = this->definition_for_card_id(card_id);
|
||||
if (sc_ce && (static_cast<uint8_t>(target_mode) < 6)) {
|
||||
target_mode = sc_ce->def.target_mode;
|
||||
if (!this->server()->options.is_nte()) {
|
||||
auto sc_ce = this->definition_for_card_id(card_id);
|
||||
if (sc_ce && (static_cast<uint8_t>(target_mode) < 6)) {
|
||||
target_mode = sc_ce->def.target_mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1474,8 +1522,7 @@ bool RulerServer::compute_effective_range_and_target_mode_for_attack(
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t RulerServer::count_rampage_targets_for_attack(
|
||||
const ActionState& pa, uint8_t client_id) const {
|
||||
size_t RulerServer::count_rampage_targets_for_attack(const ActionState& pa, uint8_t client_id) const {
|
||||
if (client_id == 0xFF) {
|
||||
return 0;
|
||||
}
|
||||
@@ -1583,8 +1630,7 @@ bool RulerServer::defense_card_can_apply_to_attack(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RulerServer::defense_card_matches_any_attack_card_top_color(
|
||||
const ActionState& pa) const {
|
||||
bool RulerServer::defense_card_matches_any_attack_card_top_color(const ActionState& pa) const {
|
||||
auto ce = this->definition_for_card_ref(pa.action_card_refs[0]);
|
||||
if (!ce) {
|
||||
throw runtime_error("defense card definition is missing");
|
||||
@@ -1632,7 +1678,8 @@ int32_t RulerServer::error_code_for_client_setting_card(
|
||||
return -0x76;
|
||||
}
|
||||
|
||||
if (!this->is_card_ref_in_hand(card_ref)) {
|
||||
bool is_nte = this->server()->options.is_nte();
|
||||
if (!is_nte && !this->is_card_ref_in_hand(card_ref)) {
|
||||
return -0x5E;
|
||||
}
|
||||
|
||||
@@ -1672,8 +1719,8 @@ int32_t RulerServer::error_code_for_client_setting_card(
|
||||
}
|
||||
|
||||
// Check for assists that can only be set on yourself
|
||||
auto eff = assist_effect_number_for_card_id(ce->def.card_id);
|
||||
if (((eff == AssistEffect::LEGACY) || (eff == AssistEffect::EXCHANGE)) &&
|
||||
auto eff = assist_effect_number_for_card_id(ce->def.card_id, is_nte);
|
||||
if (((eff == AssistEffect::LEGACY) || (!is_nte && (eff == AssistEffect::EXCHANGE))) &&
|
||||
(assist_target_client_id != 0xFF) &&
|
||||
(assist_target_client_id != client_id_for_card_ref(card_ref))) {
|
||||
return -0x75;
|
||||
@@ -1712,8 +1759,8 @@ int32_t RulerServer::error_code_for_client_setting_card(
|
||||
|
||||
if ((ce->def.type == CardType::ITEM) || (ce->def.type == CardType::CREATURE)) {
|
||||
int16_t existing_fcs_cost = 0;
|
||||
bool limit_summoning_by_count = this->find_condition_on_card_ref(
|
||||
short_statuses->at(0).card_ref, ConditionType::FC_LIMIT_BY_COUNT);
|
||||
bool limit_summoning_by_count = !is_nte &&
|
||||
this->find_condition_on_card_ref(short_statuses->at(0).card_ref, ConditionType::FC_LIMIT_BY_COUNT);
|
||||
for (size_t z = 7; z < 15; z++) {
|
||||
const auto& this_status = short_statuses->at(z);
|
||||
if ((this_status.card_ref != 0xFFFF) && this->card_exists_by_status(this_status)) {
|
||||
@@ -1752,71 +1799,91 @@ int32_t RulerServer::error_code_for_client_setting_card(
|
||||
return 0;
|
||||
}
|
||||
|
||||
Location summon_area_loc;
|
||||
uint8_t summon_area_size;
|
||||
if (!this->get_creature_summon_area(
|
||||
client_id, &summon_area_loc, &summon_area_size)) {
|
||||
if (team_id != 1) {
|
||||
if ((loc->x > 0) && (loc->x < this->map_and_rules->map.width - 1)) {
|
||||
if ((loc->y < this->map_and_rules->map.height - summon_cost - 1) &&
|
||||
(loc->y > 0)) {
|
||||
return 0;
|
||||
if (is_nte) {
|
||||
// It seems NTE assumes that teams always start on the same ends of the
|
||||
// map; non-NTE removes this restriction.
|
||||
if (team_id == 1) {
|
||||
if (((loc->x < 1) ||
|
||||
(loc->x >= this->map_and_rules->map.width - 1) ||
|
||||
(loc->y < summon_cost + 1) ||
|
||||
(loc->y >= this->map_and_rules->map.height - 1)) &&
|
||||
(loc->y != this->map_and_rules->map.height - 2)) {
|
||||
return -0x7E;
|
||||
}
|
||||
} else if (((loc->x < 1) ||
|
||||
(loc->x >= this->map_and_rules->map.width - 1) ||
|
||||
(loc->y < 1) ||
|
||||
(loc->y >= this->map_and_rules->map.height - summon_cost - 1)) &&
|
||||
(loc->y != 1)) {
|
||||
return -0x7E;
|
||||
}
|
||||
|
||||
} else {
|
||||
Location summon_area_loc;
|
||||
uint8_t summon_area_size;
|
||||
if (!this->get_creature_summon_area(client_id, &summon_area_loc, &summon_area_size)) {
|
||||
if (team_id != 1) {
|
||||
if ((loc->x > 0) && (loc->x < this->map_and_rules->map.width - 1)) {
|
||||
if ((loc->y < this->map_and_rules->map.height - summon_cost - 1) &&
|
||||
(loc->y > 0)) {
|
||||
return 0;
|
||||
}
|
||||
if (loc->y == 1) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (loc->y == 1) {
|
||||
return 0;
|
||||
} else {
|
||||
if ((loc->x > 0) &&
|
||||
(loc->x < this->map_and_rules->map.width - 1)) {
|
||||
if ((summon_cost + 1 <= loc->y) && (loc->y < this->map_and_rules->map.height - 1)) {
|
||||
return 0;
|
||||
}
|
||||
if (loc->y == this->map_and_rules->map.height - 2) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -0x7E;
|
||||
}
|
||||
|
||||
int32_t x_offset, y_offset;
|
||||
this->offsets_for_direction(summon_area_loc, &x_offset, &y_offset);
|
||||
if (x_offset == 0) {
|
||||
if ((loc->x < 1) && (loc->x >= this->map_and_rules->map.width - 1)) {
|
||||
return -0x7E;
|
||||
}
|
||||
} else {
|
||||
if ((loc->x > 0) &&
|
||||
(loc->x < this->map_and_rules->map.width - 1)) {
|
||||
if ((summon_cost + 1 <= loc->y) && (loc->y < this->map_and_rules->map.height - 1)) {
|
||||
return 0;
|
||||
int16_t diff = max<int16_t>(summon_area_size - summon_cost, 0);
|
||||
if (x_offset > 0) {
|
||||
if (loc->x < summon_area_loc.x) {
|
||||
return -0x7E;
|
||||
}
|
||||
if (loc->y == this->map_and_rules->map.height - 2) {
|
||||
return 0;
|
||||
if (loc->x > summon_area_loc.x + diff) {
|
||||
return -0x7E;
|
||||
}
|
||||
} else if (x_offset < 0) {
|
||||
if ((loc->x > summon_area_loc.x) || (loc->x < summon_area_loc.x - diff)) {
|
||||
return -0x7E;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -0x7E;
|
||||
}
|
||||
|
||||
int32_t x_offset, y_offset;
|
||||
this->offsets_for_direction(summon_area_loc, &x_offset, &y_offset);
|
||||
if (x_offset == 0) {
|
||||
if ((loc->x < 1) && (loc->x >= this->map_and_rules->map.width - 1)) {
|
||||
return -0x7E;
|
||||
}
|
||||
} else {
|
||||
int16_t diff = max<int16_t>(summon_area_size - summon_cost, 0);
|
||||
if (x_offset > 0) {
|
||||
if (loc->x < summon_area_loc.x) {
|
||||
if (y_offset == 0) {
|
||||
if ((loc->y < 1) && (loc->y >= this->map_and_rules->map.height - 1)) {
|
||||
return -0x7E;
|
||||
}
|
||||
if (loc->x > summon_area_loc.x + diff) {
|
||||
return -0x7E;
|
||||
}
|
||||
} else if (x_offset < 0) {
|
||||
if ((loc->x > summon_area_loc.x) || (loc->x < summon_area_loc.x - diff)) {
|
||||
return -0x7E;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (y_offset == 0) {
|
||||
if ((loc->y < 1) && (loc->y >= this->map_and_rules->map.height - 1)) {
|
||||
return -0x7E;
|
||||
}
|
||||
} else {
|
||||
int16_t diff = max<int16_t>(summon_area_size - summon_cost, 0);
|
||||
if (y_offset > 0) {
|
||||
if (loc->y < summon_area_loc.y) {
|
||||
return -0x7E;
|
||||
}
|
||||
if (loc->y > summon_area_loc.y + diff) {
|
||||
return -0x7E;
|
||||
}
|
||||
} else if (y_offset < 0) {
|
||||
if ((loc->y > summon_area_loc.y) || (loc->y < summon_area_loc.y - diff)) {
|
||||
return -0x7E;
|
||||
} else {
|
||||
int16_t diff = max<int16_t>(summon_area_size - summon_cost, 0);
|
||||
if (y_offset > 0) {
|
||||
if (loc->y < summon_area_loc.y) {
|
||||
return -0x7E;
|
||||
}
|
||||
if (loc->y > summon_area_loc.y + diff) {
|
||||
return -0x7E;
|
||||
}
|
||||
} else if (y_offset < 0) {
|
||||
if ((loc->y > summon_area_loc.y) || (loc->y < summon_area_loc.y - diff)) {
|
||||
return -0x7E;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2020,7 +2087,7 @@ uint8_t RulerServer::get_card_ref_max_hp(uint16_t card_ref) const {
|
||||
return 0;
|
||||
} else if (((ce->def.type == CardType::HUNTERS_SC) || (ce->def.type == CardType::ARKZ_SC)) &&
|
||||
(this->map_and_rules->rules.char_hp > 0) &&
|
||||
!this->card_ref_is_boss_sc(card_ref)) {
|
||||
(this->server()->options.is_nte() || !this->card_ref_is_boss_sc(card_ref))) {
|
||||
return this->map_and_rules->rules.char_hp;
|
||||
} else {
|
||||
return ce->def.hp.stat;
|
||||
@@ -2169,7 +2236,7 @@ bool RulerServer::is_attack_valid(const ActionState& pa) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (attacker_card_status->card_flags & 2) {
|
||||
if (!this->server()->options.is_nte() && (attacker_card_status->card_flags & 2)) {
|
||||
this->error_code3 = -0x60;
|
||||
return false;
|
||||
}
|
||||
@@ -2206,7 +2273,7 @@ bool RulerServer::is_attack_valid(const ActionState& pa) {
|
||||
|
||||
size_t conditional_card_count = 0;
|
||||
size_t z;
|
||||
for (z = 0; z < 9; z++) {
|
||||
for (z = 0; z < 8; z++) {
|
||||
uint16_t right_card_ref = pa.action_card_refs[z];
|
||||
if (right_card_ref == 0xFFFF) {
|
||||
break;
|
||||
@@ -2285,7 +2352,9 @@ bool RulerServer::is_attack_or_defense_valid(const ActionState& pa) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int16_t cost = this->compute_attack_or_defense_costs(pa, false, nullptr);
|
||||
// NTE apparently does not check the action's cost here
|
||||
bool is_nte = this->server()->options.is_nte();
|
||||
int16_t cost = is_nte ? 0 : this->compute_attack_or_defense_costs(pa, false, nullptr);
|
||||
|
||||
switch (this->get_pending_action_type(pa)) {
|
||||
case ActionType::ATTACK:
|
||||
@@ -2381,8 +2450,9 @@ bool RulerServer::is_defense_valid(const ActionState& pa) {
|
||||
}
|
||||
}
|
||||
|
||||
if (this->find_condition_on_card_ref(pa.target_card_refs[0], ConditionType::HOLD) ||
|
||||
this->find_condition_on_card_ref(pa.target_card_refs[0], ConditionType::CANNOT_DEFEND)) {
|
||||
if (!this->server()->options.is_nte() &&
|
||||
(this->find_condition_on_card_ref(pa.target_card_refs[0], ConditionType::HOLD) ||
|
||||
this->find_condition_on_card_ref(pa.target_card_refs[0], ConditionType::CANNOT_DEFEND))) {
|
||||
this->error_code3 = -0x63;
|
||||
return false;
|
||||
}
|
||||
@@ -2411,30 +2481,53 @@ size_t RulerServer::max_move_distance_for_card_ref(uint32_t card_ref) const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ssize_t ret = ce->def.mv.stat;
|
||||
|
||||
Condition cond;
|
||||
if (this->find_condition_on_card_ref(card_ref, ConditionType::MV_BONUS, &cond, nullptr, true)) {
|
||||
ret += cond.value;
|
||||
}
|
||||
if (this->find_condition_on_card_ref(card_ref, ConditionType::SET_MV, &cond, nullptr, true)) {
|
||||
ret = cond.value;
|
||||
}
|
||||
ret = max<ssize_t>(0, ret);
|
||||
|
||||
size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(client_id);
|
||||
bool has_stamina_effect = false;
|
||||
for (size_t z = 0; z < num_assists; z++) {
|
||||
auto eff = this->assist_server->get_active_assist_by_index(z);
|
||||
if (eff == AssistEffect::SNAIL_PACE) {
|
||||
return 1;
|
||||
if (this->server()->options.is_nte()) {
|
||||
if (ce->def.type == CardType::ITEM) {
|
||||
return ce->def.mv.stat;
|
||||
}
|
||||
if (eff == AssistEffect::STAMINA) {
|
||||
has_stamina_effect = true;
|
||||
}
|
||||
}
|
||||
|
||||
return (has_stamina_effect) ? 9 : min<ssize_t>(9, ret);
|
||||
Condition cond;
|
||||
if (this->find_condition_on_card_ref(card_ref, ConditionType::SET_MV, &cond)) {
|
||||
return cond.value;
|
||||
}
|
||||
|
||||
size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(client_id);
|
||||
bool has_stamina_effect = false;
|
||||
for (size_t z = 0; z < num_assists; z = z + 1) {
|
||||
auto assist = this->assist_server->get_active_assist_by_index(z);
|
||||
if (assist == AssistEffect::SNAIL_PACE) {
|
||||
return 1;
|
||||
} else if (assist == AssistEffect::STAMINA) {
|
||||
has_stamina_effect = true;
|
||||
}
|
||||
}
|
||||
return has_stamina_effect ? 99 : ce->def.mv.stat;
|
||||
|
||||
} else {
|
||||
ssize_t ret = ce->def.mv.stat;
|
||||
Condition cond;
|
||||
if (this->find_condition_on_card_ref(card_ref, ConditionType::MV_BONUS, &cond, nullptr, true)) {
|
||||
ret += cond.value;
|
||||
}
|
||||
if (this->find_condition_on_card_ref(card_ref, ConditionType::SET_MV, &cond, nullptr, true)) {
|
||||
ret = cond.value;
|
||||
}
|
||||
ret = max<ssize_t>(0, ret);
|
||||
|
||||
size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(client_id);
|
||||
bool has_stamina_effect = false;
|
||||
for (size_t z = 0; z < num_assists; z++) {
|
||||
auto eff = this->assist_server->get_active_assist_by_index(z);
|
||||
if (eff == AssistEffect::SNAIL_PACE) {
|
||||
return 1;
|
||||
}
|
||||
if (eff == AssistEffect::STAMINA) {
|
||||
has_stamina_effect = true;
|
||||
}
|
||||
}
|
||||
|
||||
return has_stamina_effect ? 9 : min<ssize_t>(9, ret);
|
||||
}
|
||||
}
|
||||
|
||||
RulerServer::MovePath::MovePath()
|
||||
@@ -2518,13 +2611,14 @@ void RulerServer::replace_D1_D2_rank_cards_with_Attack(
|
||||
}
|
||||
|
||||
AttackMedium RulerServer::get_attack_medium(const ActionState& pa) const {
|
||||
bool is_nte = this->server()->options.is_nte();
|
||||
for (size_t z = 0; z < 8; z++) {
|
||||
uint16_t card_ref = pa.action_card_refs[z];
|
||||
if (card_ref == 0xFFFF) {
|
||||
return AttackMedium::PHYSICAL;
|
||||
}
|
||||
auto ce = this->definition_for_card_ref(card_ref);
|
||||
if (ce && card_class_is_tech_like(ce->def.card_class())) {
|
||||
if (ce && card_class_is_tech_like(ce->def.card_class(), is_nte)) {
|
||||
return AttackMedium::TECH;
|
||||
}
|
||||
}
|
||||
@@ -2545,9 +2639,11 @@ int32_t RulerServer::set_cost_for_card(uint8_t client_id, uint16_t card_ref) con
|
||||
return -0x7D;
|
||||
}
|
||||
|
||||
bool is_nte = this->server()->options.is_nte();
|
||||
auto short_statuses = this->short_statuses[client_id];
|
||||
int32_t ret = ce->def.self_cost;
|
||||
if (short_statuses &&
|
||||
if (!is_nte &&
|
||||
short_statuses &&
|
||||
this->card_exists_by_status(short_statuses->at(0)) &&
|
||||
this->find_condition_on_card_ref(short_statuses->at(0).card_ref, ConditionType::UNKNOWN_69)) {
|
||||
ret = 0;
|
||||
@@ -2580,9 +2676,8 @@ int32_t RulerServer::set_cost_for_card(uint8_t client_id, uint16_t card_ref) con
|
||||
for (size_t z = 0; z < num_assists; z++) {
|
||||
auto eff = this->assist_server->get_active_assist_by_index(z);
|
||||
if (eff == AssistEffect::LAND_PRICE) {
|
||||
// Note: Original code had an extra addend (ret < 0 && (ret & 1) != 0),
|
||||
// but ret cannot be negatve here, so we omit it.
|
||||
ret += ret >> 1;
|
||||
// In NTE, Land Price is apparently 2x rather than 1.5x
|
||||
ret = is_nte ? (ret << 1) : (ret + (ret >> 1));
|
||||
} else if (eff == AssistEffect::DEFLATION) {
|
||||
ret = max<int32_t>(0, ret - 1);
|
||||
} else if (eff == AssistEffect::INFLATION) {
|
||||
@@ -2667,4 +2762,24 @@ int32_t RulerServer::verify_deck(
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t RulerServer::count_targets_with_rampage_and_not_pierce_nte(const ActionState& as) const {
|
||||
size_t ret = 0;
|
||||
for (size_t z = 0; (z < as.target_card_refs.size()) && (as.target_card_refs[z] != 0xFFFF); z++) {
|
||||
if (this->attack_action_has_rampage_and_not_pierce(as, as.target_card_refs[z])) {
|
||||
ret++;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
size_t RulerServer::count_targets_with_pierce_and_not_rampage_nte(const ActionState& as) const {
|
||||
size_t ret = 0;
|
||||
for (size_t z = 0; (z < as.target_card_refs.size()) && (as.target_card_refs[z] != 0xFFFF); z++) {
|
||||
if (this->attack_action_has_pierce_and_not_rampage(as, client_id_for_card_ref(as.target_card_refs[z]))) {
|
||||
ret++;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace Episode3
|
||||
|
||||
+22
-35
@@ -49,12 +49,9 @@ public:
|
||||
std::shared_ptr<Server> server();
|
||||
std::shared_ptr<const Server> server() const;
|
||||
|
||||
ActionChainWithConds* action_chain_with_conds_for_card_ref(
|
||||
uint16_t card_ref);
|
||||
const ActionChainWithConds* action_chain_with_conds_for_card_ref(
|
||||
uint16_t card_ref) const;
|
||||
bool any_attack_action_card_is_support_tech_or_support_pb(
|
||||
const ActionState& pa) const;
|
||||
ActionChainWithConds* action_chain_with_conds_for_card_ref(uint16_t card_ref);
|
||||
const ActionChainWithConds* action_chain_with_conds_for_card_ref(uint16_t card_ref) const;
|
||||
bool any_attack_action_card_is_support_tech_or_support_pb(const ActionState& pa) const;
|
||||
bool card_has_pierce_or_rampage(
|
||||
uint8_t client_id,
|
||||
ConditionType cond_type,
|
||||
@@ -63,27 +60,23 @@ public:
|
||||
uint16_t action_card_ref,
|
||||
uint8_t def_effect_index,
|
||||
AttackMedium attack_medium) const;
|
||||
bool attack_action_has_rampage_and_not_pierce(
|
||||
const ActionState& pa, uint16_t card_ref) const;
|
||||
bool attack_action_has_pierce_and_not_rampage(
|
||||
const ActionState& pa, uint8_t client_id);
|
||||
bool attack_action_has_rampage_and_not_pierce(const ActionState& pa, uint16_t card_ref) const;
|
||||
bool attack_action_has_pierce_and_not_rampage(const ActionState& pa, uint8_t client_id) const;
|
||||
size_t count_targets_with_rampage_and_not_pierce_nte(const ActionState& as) const;
|
||||
size_t count_targets_with_pierce_and_not_rampage_nte(const ActionState& as) const;
|
||||
bool card_exists_by_status(const CardShortStatus& stat) const;
|
||||
bool card_has_mighty_knuckle(uint32_t card_ref) const;
|
||||
uint16_t card_id_for_card_ref(uint16_t card_ref) const;
|
||||
static bool card_id_is_boss_sc(uint16_t card_id);
|
||||
static bool card_id_is_support_tech_or_support_pb(uint16_t card_id);
|
||||
bool card_ref_can_attack(uint16_t card_ref);
|
||||
bool card_ref_can_move(
|
||||
uint8_t client_id, uint16_t card_ref, bool ignore_atk_points) const;
|
||||
bool card_ref_has_class_usability_condition(
|
||||
uint16_t card_ref) const;
|
||||
bool card_ref_can_move(uint8_t client_id, uint16_t card_ref, bool ignore_atk_points) const;
|
||||
bool card_ref_has_class_usability_condition(uint16_t card_ref) const;
|
||||
bool card_ref_has_free_maneuver(uint16_t card_ref) const;
|
||||
bool card_ref_is_aerial(uint16_t card_ref) const;
|
||||
bool card_ref_is_aerial_or_has_free_maneuver(
|
||||
uint16_t card_ref) const;
|
||||
bool card_ref_is_aerial_or_has_free_maneuver(uint16_t card_ref) const;
|
||||
bool card_ref_is_boss_sc(uint32_t card_ref) const;
|
||||
bool card_ref_or_any_set_card_has_condition_46(
|
||||
uint16_t card_ref) const;
|
||||
bool card_ref_or_any_set_card_has_condition_46(uint16_t card_ref) const;
|
||||
bool card_ref_or_sc_has_fixed_range(uint16_t card_ref) const;
|
||||
bool check_move_path_and_get_cost(
|
||||
uint8_t client_id,
|
||||
@@ -123,14 +116,12 @@ public:
|
||||
uint16_t* out_effective_card_id,
|
||||
TargetMode* out_effective_target_mode,
|
||||
uint16_t* out_orig_card_ref) const;
|
||||
size_t count_rampage_targets_for_attack(
|
||||
const ActionState& pa, uint8_t client_id) const;
|
||||
size_t count_rampage_targets_for_attack(const ActionState& pa, uint8_t client_id) const;
|
||||
bool defense_card_can_apply_to_attack(
|
||||
uint16_t defense_card_ref,
|
||||
uint16_t attacker_card_ref,
|
||||
uint16_t attacker_sc_card_ref) const;
|
||||
bool defense_card_matches_any_attack_card_top_color(
|
||||
const ActionState& pa) const;
|
||||
bool defense_card_matches_any_attack_card_top_color(const ActionState& pa) const;
|
||||
std::shared_ptr<const CardIndex::CardEntry> definition_for_card_ref(uint16_t card_ref) const;
|
||||
int32_t error_code_for_client_setting_card(
|
||||
uint8_t client_id,
|
||||
@@ -157,17 +148,14 @@ public:
|
||||
size_t num_occupied_tiles,
|
||||
size_t num_vacant_tiles) const;
|
||||
uint16_t get_ally_sc_card_ref(uint16_t card_ref) const;
|
||||
std::shared_ptr<const CardIndex::CardEntry> definition_for_card_id(
|
||||
uint32_t card_id) const;
|
||||
std::shared_ptr<const CardIndex::CardEntry> definition_for_card_id(uint32_t card_id) const;
|
||||
uint32_t get_card_id_with_effective_range(
|
||||
uint16_t card_ref, uint16_t card_id_override, TargetMode* out_target_mode) const;
|
||||
uint8_t get_card_ref_max_hp(uint16_t card_ref) const;
|
||||
bool get_creature_summon_area(
|
||||
uint8_t client_id, Location* out_loc, uint8_t* out_region_size) const;
|
||||
std::shared_ptr<HandAndEquipState> get_hand_and_equip_state_for_client_id(
|
||||
uint8_t client_id);
|
||||
std::shared_ptr<const HandAndEquipState> get_hand_and_equip_state_for_client_id(
|
||||
uint8_t client_id) const;
|
||||
std::shared_ptr<HandAndEquipState> get_hand_and_equip_state_for_client_id(uint8_t client_id);
|
||||
std::shared_ptr<const HandAndEquipState> get_hand_and_equip_state_for_client_id(uint8_t client_id) const;
|
||||
bool get_move_path_length_and_cost(
|
||||
uint32_t client_id,
|
||||
uint32_t card_ref,
|
||||
@@ -188,8 +176,7 @@ public:
|
||||
std::shared_ptr<StateFlags> state_flags,
|
||||
std::shared_ptr<AssistServer> assist_server);
|
||||
size_t max_move_distance_for_card_ref(uint32_t card_ref) const;
|
||||
static void offsets_for_direction(
|
||||
const Location& loc, int32_t* out_x_offset, int32_t* out_y_offset);
|
||||
static void offsets_for_direction(const Location& loc, int32_t* out_x_offset, int32_t* out_y_offset);
|
||||
void register_player(
|
||||
uint8_t client_id,
|
||||
std::shared_ptr<HandAndEquipState> hes,
|
||||
@@ -211,11 +198,11 @@ private:
|
||||
std::weak_ptr<Server> w_server;
|
||||
|
||||
public:
|
||||
std::shared_ptr<HandAndEquipState> hand_and_equip_states[4];
|
||||
std::shared_ptr<parray<CardShortStatus, 0x10>> short_statuses[4];
|
||||
std::shared_ptr<DeckEntry> deck_entries[4];
|
||||
std::shared_ptr<parray<ActionChainWithConds, 9>> set_card_action_chains[4];
|
||||
std::shared_ptr<parray<ActionMetadata, 9>> set_card_action_metadatas[4];
|
||||
bcarray<std::shared_ptr<HandAndEquipState>, 4> hand_and_equip_states;
|
||||
bcarray<std::shared_ptr<parray<CardShortStatus, 0x10>>, 4> short_statuses;
|
||||
bcarray<std::shared_ptr<DeckEntry>, 4> deck_entries;
|
||||
bcarray<std::shared_ptr<parray<ActionChainWithConds, 9>>, 4> set_card_action_chains;
|
||||
bcarray<std::shared_ptr<parray<ActionMetadata, 9>>, 4> set_card_action_metadatas;
|
||||
std::shared_ptr<MapAndRulesState> map_and_rules;
|
||||
std::shared_ptr<StateFlags> state_flags;
|
||||
std::shared_ptr<AssistServer> assist_server;
|
||||
|
||||
+881
-531
File diff suppressed because it is too large
Load Diff
+64
-26
@@ -71,9 +71,13 @@ public:
|
||||
std::shared_ptr<const CardIndex> card_index;
|
||||
std::shared_ptr<const MapIndex> map_index;
|
||||
uint32_t behavior_flags;
|
||||
std::shared_ptr<PSOLFGEncryption> random_crypt;
|
||||
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt;
|
||||
std::shared_ptr<const Tournament> tournament;
|
||||
std::array<std::vector<uint16_t>, 5> trap_card_ids;
|
||||
|
||||
inline bool is_nte() const {
|
||||
return (this->behavior_flags & BehaviorFlag::IS_TRIAL_EDITION);
|
||||
}
|
||||
};
|
||||
Server(std::shared_ptr<Lobby> lobby, Options&& options);
|
||||
~Server() noexcept(false);
|
||||
@@ -95,26 +99,54 @@ public:
|
||||
StackLogger log_stack(const std::string& prefix) const;
|
||||
const StackLogger& log() const;
|
||||
|
||||
std::string debug_str_for_card_ref(uint16_t card_ref) const;
|
||||
std::string debug_str_for_card_id(uint16_t card_id) const;
|
||||
template <typename U16T>
|
||||
std::string debug_str_for_card_refs(const U16T* refs, size_t count) const {
|
||||
std::string ret = "[";
|
||||
for (size_t z = 0; z < count; z++) {
|
||||
if (refs[z] != 0xFFFF) {
|
||||
std::string ref_str = this->debug_str_for_card_ref(refs[z]);
|
||||
ret += string_printf("%zu:%s ", z, ref_str.c_str());
|
||||
}
|
||||
}
|
||||
if (ret.size() > 1) {
|
||||
ret.back() = ']'; // Replace the ' ' from the last added item
|
||||
} else {
|
||||
ret.push_back(']');
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
template <typename U16T>
|
||||
std::string debug_str_for_card_refs(const std::vector<U16T>& refs) const {
|
||||
return this->debug_str_for_card_refs(refs.data(), refs.size());
|
||||
}
|
||||
template <typename U16T, size_t Count>
|
||||
std::string debug_str_for_card_refs(const parray<U16T, Count>& refs) const {
|
||||
return this->debug_str_for_card_refs(refs.data(), refs.size());
|
||||
}
|
||||
|
||||
int8_t get_winner_team_id() const;
|
||||
|
||||
template <typename T>
|
||||
void send(const T& cmd) const {
|
||||
void send(const T& cmd, uint8_t command = 0xC9, bool enable_masking = true) const {
|
||||
if (cmd.header.size != sizeof(cmd) / 4) {
|
||||
throw std::logic_error("outbound command size field is incorrect");
|
||||
}
|
||||
if (cmd.header.subsubcommand == 0x06) {
|
||||
if (!this->options.is_nte() && (cmd.header.subsubcommand == 0x06)) {
|
||||
this->num_6xB4x06_commands_sent++;
|
||||
this->prev_num_6xB4x06_commands_sent = this->num_6xB4x06_commands_sent;
|
||||
if (this->num_6xB4x06_commands_sent > 0x100) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->send(&cmd, cmd.header.size * 4);
|
||||
this->send(&cmd, cmd.header.size * 4, command, enable_masking);
|
||||
}
|
||||
void send(const void* data, size_t size) const;
|
||||
void send(const void* data, size_t size, uint8_t command = 0xC9, bool enable_masking = true) const;
|
||||
void send_commands_for_joining_spectator(Channel& ch) const;
|
||||
|
||||
void force_battle_result(uint8_t surrendered_client_id, bool set_winner);
|
||||
void force_replace_assist_card(uint8_t client_id, uint16_t card_id);
|
||||
void force_destroy_field_character(uint8_t client_id, size_t set_index);
|
||||
|
||||
__attribute__((format(printf, 2, 3))) void send_debug_message_printf(const char* fmt, ...) const;
|
||||
@@ -142,7 +174,7 @@ public:
|
||||
bool check_presence_entry(uint8_t client_id) const;
|
||||
void clear_player_flags_after_dice_phase();
|
||||
void compute_all_map_occupied_bits();
|
||||
void compute_team_dice_boost(uint8_t team_id);
|
||||
void compute_team_dice_bonus(uint8_t team_id);
|
||||
void copy_player_states_to_prev_states();
|
||||
std::shared_ptr<const CardIndex::CardEntry> definition_for_card_id(uint16_t card_id) const;
|
||||
void destroy_cards_with_zero_hp();
|
||||
@@ -173,14 +205,14 @@ public:
|
||||
void send_set_card_updates_and_6xB4x04_if_needed();
|
||||
void set_battle_ended();
|
||||
void set_battle_started();
|
||||
void set_client_id_ready_to_advance_phase(uint8_t client_id);
|
||||
bool player_can_receive_dice_boost(uint8_t client_id) const;
|
||||
void set_client_id_ready_to_advance_phase(uint8_t client_id, BattlePhase battle_phase);
|
||||
void set_phase_after();
|
||||
void move_phase_before();
|
||||
void set_player_deck_valid(uint8_t client_id);
|
||||
void setup_and_start_battle();
|
||||
G_SetStateFlags_GC_Ep3_6xB4x03 prepare_6xB4x03() const;
|
||||
void update_battle_state_flags_and_send_6xB4x03_if_needed(
|
||||
bool always_send = false);
|
||||
G_SetStateFlags_Ep3_6xB4x03 prepare_6xB4x03() const;
|
||||
void update_battle_state_flags_and_send_6xB4x03_if_needed(bool always_send = false);
|
||||
bool update_registration_phase();
|
||||
void on_server_data_input(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx0B_mulligan_hand(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
@@ -191,6 +223,8 @@ public:
|
||||
void handle_CAx10_move_fc_to_location(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx11_enqueue_attack_or_defense(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx12_end_attack_list(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
template <typename CmdT>
|
||||
void handle_CAx13_update_map_during_setup_t(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx13_update_map_during_setup(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx14_update_deck_during_setup(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void handle_CAx15_unused_hard_reset_server_state(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
@@ -208,12 +242,11 @@ public:
|
||||
void handle_CAx49_card_counts(std::shared_ptr<Client> sender_c, const std::string& data);
|
||||
void compute_losing_team_id_and_add_winner_flags(uint32_t flags);
|
||||
uint32_t get_team_exp(uint8_t team_id) const;
|
||||
uint32_t send_6xB4x06_if_card_ref_invalid(
|
||||
uint16_t card_ref, int16_t negative_value);
|
||||
uint32_t send_6xB4x06_if_card_ref_invalid(uint16_t card_ref, int16_t negative_value);
|
||||
void unknown_8023EEF4();
|
||||
void execute_bomb_assist_effect();
|
||||
void replace_targets_due_to_destruction_or_conditions(
|
||||
ActionState* as);
|
||||
void replace_targets_due_to_destruction_nte(ActionState* as);
|
||||
void replace_targets_due_to_destruction_or_conditions(ActionState* as);
|
||||
bool any_target_exists_for_attack(const ActionState& as);
|
||||
uint8_t get_current_team_turn2() const;
|
||||
void unknown_8023EE48();
|
||||
@@ -224,12 +257,11 @@ public:
|
||||
void send_6xB4x02_for_all_players_if_needed(bool always_send = false);
|
||||
void send_6xB4x50_trap_tile_locations() const;
|
||||
|
||||
G_UpdateDecks_GC_Ep3_6xB4x07 prepare_6xB4x07_decks_update() const;
|
||||
G_SetPlayerNames_GC_Ep3_6xB4x1C prepare_6xB4x1C_names_update() const;
|
||||
static std::string prepare_6xB6x41_map_definition(
|
||||
std::shared_ptr<const MapIndex::Map> map, uint8_t language, bool is_trial);
|
||||
G_UpdateDecks_Ep3_6xB4x07 prepare_6xB4x07_decks_update() const;
|
||||
G_SetPlayerNames_Ep3_6xB4x1C prepare_6xB4x1C_names_update() const;
|
||||
static std::string prepare_6xB6x41_map_definition(std::shared_ptr<const MapIndex::Map> map, uint8_t language, bool is_nte);
|
||||
void send_6xB6x41_to_all_clients() const;
|
||||
G_SetTrapTileLocations_GC_Ep3_6xB4x50 prepare_6xB4x50_trap_tile_locations() const;
|
||||
G_SetTrapTileLocations_Ep3_6xB4x50 prepare_6xB4x50_trap_tile_locations() const;
|
||||
|
||||
std::vector<std::shared_ptr<Card>> const_cast_set_cards_v(
|
||||
const std::vector<std::shared_ptr<const Card>>& cards);
|
||||
@@ -241,10 +273,14 @@ private:
|
||||
public:
|
||||
// These fields are not part of the original implementation
|
||||
std::weak_ptr<Lobby> lobby;
|
||||
bool has_lobby;
|
||||
Options options;
|
||||
std::shared_ptr<const MapIndex::Map> last_chosen_map;
|
||||
bool tournament_match_result_sent;
|
||||
uint8_t override_environment_number;
|
||||
uint8_t def_dice_value_range_override;
|
||||
uint8_t atk_dice_value_range_2v1_override;
|
||||
uint8_t def_dice_value_range_2v1_override;
|
||||
mutable std::deque<StackLogger*> logger_stack;
|
||||
|
||||
// These fields were originally contained in the TCardServerBase object
|
||||
@@ -256,7 +292,7 @@ public:
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
std::shared_ptr<MapAndRulesState> map_and_rules;
|
||||
std::shared_ptr<DeckEntry> deck_entries[4];
|
||||
bcarray<std::shared_ptr<DeckEntry>, 4> deck_entries;
|
||||
parray<PresenceEntry, 4> presence_entries;
|
||||
uint8_t num_clients_present;
|
||||
parray<NameEntry, 4> name_entries;
|
||||
@@ -275,7 +311,7 @@ public:
|
||||
RegistrationPhase registration_phase;
|
||||
ActionSubphase action_subphase;
|
||||
uint8_t current_team_turn2;
|
||||
ActionState pending_attacks[0x20];
|
||||
bcarray<ActionState, 0x20> pending_attacks;
|
||||
uint32_t num_pending_attacks;
|
||||
parray<uint8_t, 4> client_done_enqueuing_attacks;
|
||||
parray<uint8_t, 4> player_ready_to_end_phase;
|
||||
@@ -288,11 +324,11 @@ public:
|
||||
uint32_t should_copy_prev_states_to_current_states;
|
||||
std::shared_ptr<CardSpecial> card_special;
|
||||
std::shared_ptr<StateFlags> state_flags;
|
||||
std::shared_ptr<PlayerState> player_states[4];
|
||||
std::array<std::shared_ptr<PlayerState>, 4> player_states;
|
||||
parray<uint32_t, 4> clients_done_in_mulligan_phase;
|
||||
uint32_t num_pending_attacks_with_cards;
|
||||
std::shared_ptr<Card> attack_cards[0x20];
|
||||
ActionState pending_attacks_with_cards[0x20];
|
||||
bcarray<std::shared_ptr<Card>, 0x20> attack_cards;
|
||||
bcarray<ActionState, 0x20> pending_attacks_with_cards;
|
||||
uint32_t unknown_a14;
|
||||
uint32_t unknown_a15;
|
||||
parray<uint32_t, 4> defense_list_ended_for_client;
|
||||
@@ -301,14 +337,16 @@ public:
|
||||
std::shared_ptr<RulerServer> ruler_server;
|
||||
parray<parray<parray<uint8_t, 2>, 2>, 5> warp_positions; // Array indexes are (type, end, x/y)
|
||||
parray<int16_t, 2> team_exp;
|
||||
parray<int16_t, 2> team_dice_boost;
|
||||
parray<int16_t, 2> team_dice_bonus;
|
||||
parray<uint32_t, 2> team_client_count;
|
||||
parray<uint32_t, 2> team_num_ally_fcs_destroyed;
|
||||
parray<uint32_t, 2> team_num_cards_destroyed;
|
||||
parray<uint8_t, 5> num_trap_tiles_of_type;
|
||||
parray<uint8_t, 5> chosen_trap_tile_index_of_type;
|
||||
parray<parray<parray<uint8_t, 2>, 8>, 5> trap_tile_locs;
|
||||
ActionState pb_action_states[4];
|
||||
parray<parray<uint8_t, 2>, 0x10> trap_tile_locs_nte;
|
||||
size_t num_trap_tiles_nte;
|
||||
bcarray<ActionState, 4> pb_action_states;
|
||||
parray<uint8_t, 4> has_done_pb;
|
||||
parray<parray<uint8_t, 4>, 4> has_done_pb_with_client;
|
||||
mutable uint32_t num_6xB4x06_commands_sent;
|
||||
|
||||
@@ -842,7 +842,7 @@ void TournamentIndex::save() const {
|
||||
for (const auto& it : this->name_to_tournament) {
|
||||
json.emplace(it.second->get_name(), it.second->json());
|
||||
}
|
||||
save_file(this->state_filename, json.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS));
|
||||
save_file(this->state_filename, json.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS | JSON::SerializeOption::ESCAPE_CONTROLS_ONLY));
|
||||
}
|
||||
|
||||
shared_ptr<Tournament> TournamentIndex::create_tournament(
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "EventUtils.hh"
|
||||
|
||||
#include <event2/event.h>
|
||||
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
|
||||
static void dispatch_forward_to_event_thread(evutil_socket_t, short, void* ctx) {
|
||||
auto* fn = reinterpret_cast<std::function<void()>*>(ctx);
|
||||
(*fn)();
|
||||
delete fn;
|
||||
}
|
||||
|
||||
void forward_to_event_thread(std::shared_ptr<struct event_base> base, std::function<void()>&& fn) {
|
||||
struct timeval tv = {0, 0};
|
||||
std::function<void()>* new_fn = new std::function<void()>(std::move(fn));
|
||||
event_base_once(base.get(), -1, EV_TIMEOUT, dispatch_forward_to_event_thread, new_fn, &tv);
|
||||
}
|
||||
|
||||
template <>
|
||||
void call_on_event_thread<void>(std::shared_ptr<struct event_base> base, std::function<void()>&& compute) {
|
||||
bool succeeded = false;
|
||||
std::string exc_what;
|
||||
std::mutex ret_lock;
|
||||
std::condition_variable ret_cv;
|
||||
std::unique_lock<std::mutex> g(ret_lock);
|
||||
forward_to_event_thread(base, [&]() -> void {
|
||||
std::lock_guard<std::mutex> g(ret_lock);
|
||||
try {
|
||||
compute();
|
||||
succeeded = true;
|
||||
} catch (const std::exception& e) {
|
||||
exc_what = e.what();
|
||||
}
|
||||
ret_cv.notify_one();
|
||||
});
|
||||
ret_cv.wait(g);
|
||||
if (!succeeded) {
|
||||
throw std::runtime_error(exc_what);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <event2/event.h>
|
||||
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
|
||||
void forward_to_event_thread(std::shared_ptr<struct event_base> base, std::function<void()>&& fn);
|
||||
|
||||
template <typename T>
|
||||
T call_on_event_thread(std::shared_ptr<struct event_base> base, std::function<T()>&& compute) {
|
||||
std::optional<T> ret;
|
||||
std::string exc_what;
|
||||
std::mutex ret_lock;
|
||||
std::condition_variable ret_cv;
|
||||
std::unique_lock<std::mutex> g(ret_lock);
|
||||
forward_to_event_thread(base, [&]() -> void {
|
||||
std::lock_guard<std::mutex> g(ret_lock);
|
||||
try {
|
||||
ret = compute();
|
||||
} catch (const std::exception& e) {
|
||||
exc_what = e.what();
|
||||
}
|
||||
ret_cv.notify_one();
|
||||
});
|
||||
ret_cv.wait(g);
|
||||
if (!ret.has_value()) {
|
||||
throw std::runtime_error(exc_what);
|
||||
}
|
||||
return ret.value();
|
||||
}
|
||||
|
||||
template <>
|
||||
void call_on_event_thread<void>(std::shared_ptr<struct event_base> base, std::function<void()>&& compute);
|
||||
@@ -74,3 +74,18 @@ FileContentsCache::GetResult FileContentsCache::get(const char* name,
|
||||
std::function<std::string(const std::string&)> generate) {
|
||||
return this->get(string(name), generate);
|
||||
}
|
||||
|
||||
shared_ptr<const string> ThreadSafeFileCache::get(
|
||||
const string& name, std::function<shared_ptr<const string>(const std::string&)> generate) {
|
||||
try {
|
||||
shared_lock g(this->lock);
|
||||
return this->name_to_file.at(name);
|
||||
} catch (const out_of_range&) {
|
||||
unique_lock g(this->lock);
|
||||
auto it = this->name_to_file.find(name);
|
||||
if (it == this->name_to_file.end()) {
|
||||
it = this->name_to_file.emplace(name, generate(name)).first;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -102,3 +104,21 @@ private:
|
||||
std::unordered_map<std::string, std::shared_ptr<File>> name_to_file;
|
||||
uint64_t ttl_usecs;
|
||||
};
|
||||
|
||||
class ThreadSafeFileCache {
|
||||
public:
|
||||
explicit ThreadSafeFileCache() = default;
|
||||
ThreadSafeFileCache(const ThreadSafeFileCache&) = delete;
|
||||
ThreadSafeFileCache(ThreadSafeFileCache&&) = delete;
|
||||
ThreadSafeFileCache& operator=(const ThreadSafeFileCache&) = delete;
|
||||
ThreadSafeFileCache& operator=(ThreadSafeFileCache&&) = delete;
|
||||
~ThreadSafeFileCache() = default;
|
||||
|
||||
// Warning: generate() is called while the lock is held for writing, so it
|
||||
// will block other threads.
|
||||
std::shared_ptr<const std::string> get(const std::string& name, std::function<std::shared_ptr<const std::string>(const std::string&)> generate);
|
||||
|
||||
private:
|
||||
std::shared_mutex lock;
|
||||
std::unordered_map<std::string, std::shared_ptr<const std::string>> name_to_file;
|
||||
};
|
||||
|
||||
+117
-43
@@ -10,6 +10,7 @@
|
||||
|
||||
#ifdef HAVE_RESOURCE_FILE
|
||||
#include <resource_file/Emulators/PPC32Emulator.hh>
|
||||
#include <resource_file/Emulators/X86Emulator.hh>
|
||||
#endif
|
||||
|
||||
#include "CommandFormats.hh"
|
||||
@@ -133,28 +134,74 @@ shared_ptr<CompiledFunctionCode> compile_function_code(
|
||||
ret->index = 0;
|
||||
ret->hide_from_patches_menu = false;
|
||||
|
||||
if (arch == CompiledFunctionCode::Architecture::POWERPC) {
|
||||
auto assembled = PPC32Emulator::assemble(text, {directory});
|
||||
ret->code = std::move(assembled.code);
|
||||
ret->label_offsets = std::move(assembled.label_offsets);
|
||||
for (const auto& it : assembled.metadata_keys) {
|
||||
if (it.first == "hide_from_patches_menu") {
|
||||
ret->hide_from_patches_menu = true;
|
||||
} else if (it.first == "index") {
|
||||
if (it.second.size() != 1) {
|
||||
throw runtime_error("invalid index value in .meta directive");
|
||||
}
|
||||
ret->index = it.second[0];
|
||||
} else if (it.first == "name") {
|
||||
ret->long_name = it.second;
|
||||
} else if (it.first == "description") {
|
||||
ret->description = it.second;
|
||||
} else {
|
||||
throw runtime_error("unknown metadata key: " + it.first);
|
||||
}
|
||||
unordered_set<string> get_include_stack;
|
||||
function<string(const string&)> get_include = [&](const string& name) -> string {
|
||||
const char* arch_name_token;
|
||||
switch (arch) {
|
||||
case CompiledFunctionCode::Architecture::POWERPC:
|
||||
arch_name_token = "ppc";
|
||||
break;
|
||||
case CompiledFunctionCode::Architecture::X86:
|
||||
arch_name_token = "x86";
|
||||
break;
|
||||
case CompiledFunctionCode::Architecture::SH4:
|
||||
arch_name_token = "sh4";
|
||||
break;
|
||||
default:
|
||||
throw runtime_error("unknown architecture");
|
||||
}
|
||||
|
||||
string asm_filename = string_printf("%s/%s.%s.inc.s", directory.c_str(), name.c_str(), arch_name_token);
|
||||
if (isfile(asm_filename)) {
|
||||
if (!get_include_stack.emplace(name).second) {
|
||||
throw runtime_error("mutual recursion between includes: " + name);
|
||||
}
|
||||
EmulatorBase::AssembleResult ret;
|
||||
switch (arch) {
|
||||
case CompiledFunctionCode::Architecture::POWERPC:
|
||||
ret = PPC32Emulator::assemble(load_file(asm_filename), get_include);
|
||||
break;
|
||||
case CompiledFunctionCode::Architecture::X86:
|
||||
ret = X86Emulator::assemble(load_file(asm_filename), get_include);
|
||||
break;
|
||||
case CompiledFunctionCode::Architecture::SH4:
|
||||
throw runtime_error("cannot compuile SH-4 assembly");
|
||||
default:
|
||||
throw runtime_error("unknown architecture");
|
||||
}
|
||||
get_include_stack.erase(name);
|
||||
return ret.code;
|
||||
}
|
||||
string bin_filename = directory + "/" + name + ".inc.bin";
|
||||
if (isfile(bin_filename)) {
|
||||
return load_file(bin_filename);
|
||||
}
|
||||
throw runtime_error("data not found for include: " + name + " (from " + asm_filename + " or " + bin_filename + ")");
|
||||
};
|
||||
|
||||
EmulatorBase::AssembleResult assembled;
|
||||
if (arch == CompiledFunctionCode::Architecture::POWERPC) {
|
||||
assembled = PPC32Emulator::assemble(text, get_include);
|
||||
} else if (arch == CompiledFunctionCode::Architecture::X86) {
|
||||
throw runtime_error("x86 assembler is not implemented");
|
||||
assembled = X86Emulator::assemble(text, get_include);
|
||||
}
|
||||
ret->code = std::move(assembled.code);
|
||||
ret->label_offsets = std::move(assembled.label_offsets);
|
||||
for (const auto& it : assembled.metadata_keys) {
|
||||
if (it.first == "hide_from_patches_menu") {
|
||||
ret->hide_from_patches_menu = true;
|
||||
} else if (it.first == "index") {
|
||||
if (it.second.size() != 1) {
|
||||
throw runtime_error("invalid index value in .meta directive");
|
||||
}
|
||||
ret->index = it.second[0];
|
||||
} else if (it.first == "name") {
|
||||
ret->long_name = it.second;
|
||||
} else if (it.first == "description") {
|
||||
ret->description = it.second;
|
||||
} else {
|
||||
throw runtime_error("unknown metadata key: " + it.first);
|
||||
}
|
||||
}
|
||||
|
||||
set<uint32_t> reloc_indexes;
|
||||
@@ -191,31 +238,57 @@ FunctionCodeIndex::FunctionCodeIndex(const string& directory) {
|
||||
}
|
||||
|
||||
uint32_t next_menu_item_id = 0;
|
||||
for (const auto& filename : list_directory(directory)) {
|
||||
if (!ends_with(filename, ".s") || ends_with(filename, ".inc.s")) {
|
||||
continue;
|
||||
}
|
||||
bool is_patch = ends_with(filename, ".patch.s");
|
||||
string name = filename.substr(0, filename.size() - (is_patch ? 8 : 2));
|
||||
|
||||
// Check for specific_version token
|
||||
uint32_t specific_version = 0;
|
||||
string short_name = name;
|
||||
if (is_patch &&
|
||||
(filename.size() >= 13) &&
|
||||
(filename[filename.size() - 13] == '.') &&
|
||||
isdigit(filename[filename.size() - 12]) &&
|
||||
(filename[filename.size() - 11] == 'O' || filename[filename.size() - 11] == 'S') &&
|
||||
(filename[filename.size() - 10] == 'E' || filename[filename.size() - 10] == 'J' || filename[filename.size() - 10] == 'P') &&
|
||||
(isdigit(filename[filename.size() - 9]) || filename[filename.size() - 9] == 'T')) {
|
||||
specific_version = 0x33000000 | (filename[filename.size() - 11] << 16) | (filename[filename.size() - 10] << 8) | filename[filename.size() - 9];
|
||||
short_name = filename.substr(0, filename.size() - 13);
|
||||
}
|
||||
|
||||
for (const auto& filename : list_directory_sorted(directory)) {
|
||||
try {
|
||||
if (!ends_with(filename, ".s")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
string name = filename.substr(0, filename.size() - 2);
|
||||
if (ends_with(name, ".inc")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool is_patch = ends_with(name, ".patch");
|
||||
if (is_patch) {
|
||||
name.resize(name.size() - 6);
|
||||
}
|
||||
|
||||
// Figure out the version or specific_version
|
||||
CompiledFunctionCode::Architecture arch = CompiledFunctionCode::Architecture::UNKNOWN;
|
||||
uint32_t specific_version = 0;
|
||||
string short_name = name;
|
||||
if (ends_with(name, ".ppc")) {
|
||||
arch = CompiledFunctionCode::Architecture::POWERPC;
|
||||
name.resize(name.size() - 4);
|
||||
short_name = name;
|
||||
} else if (ends_with(name, ".x86")) {
|
||||
arch = CompiledFunctionCode::Architecture::X86;
|
||||
name.resize(name.size() - 4);
|
||||
short_name = name;
|
||||
} else if (ends_with(name, ".sh4")) {
|
||||
arch = CompiledFunctionCode::Architecture::SH4;
|
||||
name.resize(name.size() - 4);
|
||||
short_name = name;
|
||||
} else if (is_patch && (name.size() >= 5) && (name[name.size() - 5] == '.')) {
|
||||
specific_version = (name[name.size() - 4] << 24) | (name[name.size() - 3] << 16) | (name[name.size() - 2] << 8) | name[name.size() - 1];
|
||||
if (specific_version_is_gc(specific_version)) {
|
||||
arch = CompiledFunctionCode::Architecture::POWERPC;
|
||||
} else if (specific_version_is_xb(specific_version) || specific_version_is_bb(specific_version)) {
|
||||
arch = CompiledFunctionCode::Architecture::X86;
|
||||
} else {
|
||||
throw runtime_error("unable to determine architecture from specific_version");
|
||||
}
|
||||
short_name = name.substr(0, name.size() - 5);
|
||||
}
|
||||
|
||||
if (arch == CompiledFunctionCode::Architecture::UNKNOWN) {
|
||||
throw runtime_error("unable to determine architecture");
|
||||
}
|
||||
|
||||
string path = directory + "/" + filename;
|
||||
string text = load_file(path);
|
||||
auto code = compile_function_code(CompiledFunctionCode::Architecture::POWERPC, directory, name, text);
|
||||
auto code = compile_function_code(arch, directory, name, text);
|
||||
if (code->index != 0) {
|
||||
if (!this->index_to_function.emplace(code->index, code).second) {
|
||||
throw runtime_error(string_printf(
|
||||
@@ -223,6 +296,7 @@ FunctionCodeIndex::FunctionCodeIndex(const string& directory) {
|
||||
}
|
||||
}
|
||||
code->specific_version = specific_version;
|
||||
code->source_path = path;
|
||||
code->short_name = short_name;
|
||||
this->name_to_function.emplace(name, code);
|
||||
if (is_patch) {
|
||||
@@ -239,7 +313,7 @@ FunctionCodeIndex::FunctionCodeIndex(const string& directory) {
|
||||
index_prefix.c_str(), patch_prefix.c_str(), name.c_str(), name_for_architecture(code->arch));
|
||||
|
||||
} catch (const exception& e) {
|
||||
function_compiler_log.warning("Failed to compile function %s: %s", name.c_str(), e.what());
|
||||
function_compiler_log.warning("Failed to compile function %s: %s", filename.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ void set_function_compiler_available(bool is_available);
|
||||
|
||||
struct CompiledFunctionCode {
|
||||
enum class Architecture {
|
||||
POWERPC = 0, // GC
|
||||
UNKNOWN = 0,
|
||||
POWERPC, // GC
|
||||
X86, // PC, XB, BB
|
||||
SH4, // Dreamcast
|
||||
};
|
||||
@@ -27,6 +28,7 @@ struct CompiledFunctionCode {
|
||||
std::vector<uint16_t> relocation_deltas;
|
||||
std::unordered_map<std::string, uint32_t> label_offsets;
|
||||
uint32_t entrypoint_offset_offset;
|
||||
std::string source_path; // Path to source file from newserv root
|
||||
std::string short_name; // Based on filename
|
||||
std::string long_name; // From .meta name directive
|
||||
std::string description; // From .meta description directive
|
||||
|
||||
+16
-16
@@ -37,23 +37,23 @@ constexpr uint32_t encode_argb8888(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
|
||||
return (a << 24) | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
constexpr uint16_t encode_xrgb8888_to_xrgb1555(uint32_t xrgb8888) {
|
||||
constexpr uint16_t encode_argb8888_to_argb1555(uint32_t argb8888) {
|
||||
// In: aaaaaaaarrrrrrrrggggggggbbbbbbbb
|
||||
// Out: arrrrrgggggbbbbb
|
||||
return ((argb8888 >> 9) & 0x7C00) | ((argb8888 >> 6) & 0x03E0) | ((argb8888 >> 3) & 0x001F) | ((argb8888 >> 16) & 0x8000);
|
||||
}
|
||||
|
||||
constexpr uint16_t encode_rgba8888_to_argb1555(uint32_t rgba8888) {
|
||||
// In: rrrrrrrrggggggggbbbbbbbbaaaaaaaa
|
||||
// Out: -rrrrrgggggbbbbb
|
||||
return ((xrgb8888 >> 9) & 0x7C00) | ((xrgb8888 >> 6) & 0x03E0) | ((xrgb8888 >> 3) & 0x001F);
|
||||
// Out: arrrrrgggggbbbbb
|
||||
return ((rgba8888 >> 17) & 0x7C00) | ((rgba8888 >> 14) & 0x03E0) | ((rgba8888 >> 11) & 0x001F) | ((rgba8888 << 8) & 0x8000);
|
||||
}
|
||||
|
||||
constexpr uint16_t encode_rgbx8888_to_xrgb1555(uint32_t rgbx8888) {
|
||||
// In: rrrrrrrrggggggggbbbbbbbbxxxxxxxx
|
||||
// Out: -rrrrrgggggbbbbb
|
||||
return ((rgbx8888 >> 17) & 0x7C00) | ((rgbx8888 >> 14) & 0x03E0) | ((rgbx8888 >> 11) & 0x001F);
|
||||
}
|
||||
|
||||
constexpr uint32_t decode_xrgb1555_to_rgba8888(uint16_t xrgb1555) {
|
||||
// In: -rrrrrgggggbbbbb
|
||||
// Out: rrrrrrrrggggggggbbbbbbbbaaaaaaaa (a is always FF)
|
||||
return ((xrgb1555 << 17) & 0xF8000000) | ((xrgb1555 << 12) & 0x07000000) |
|
||||
((xrgb1555 << 14) & 0x00F80000) | ((xrgb1555 << 9) & 0x00070000) |
|
||||
((xrgb1555 << 11) & 0x0000F800) | ((xrgb1555 << 6) & 0x00000700) |
|
||||
0x000000FF;
|
||||
constexpr uint32_t decode_argb1555_to_rgba8888(uint16_t argb1555) {
|
||||
// In: arrrrrgggggbbbbb
|
||||
// Out: rrrrrrrrggggggggbbbbbbbbaaaaaaaa
|
||||
return ((argb1555 << 17) & 0xF8000000) | ((argb1555 << 12) & 0x07000000) |
|
||||
((argb1555 << 14) & 0x00F80000) | ((argb1555 << 9) & 0x00070000) |
|
||||
((argb1555 << 11) & 0x0000F800) | ((argb1555 << 6) & 0x00000700) |
|
||||
((argb1555 & 0x8000) ? 0x000000FF : 0x00000000);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,992 @@
|
||||
#include "HTTPServer.hh"
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/event.h>
|
||||
#include <event2/http.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <phosg/Network.hh>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "EventUtils.hh"
|
||||
#include "Loggers.hh"
|
||||
#include "ProxyServer.hh"
|
||||
#include "Server.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
const unordered_map<int, const char*> HTTPServer::explanation_for_response_code({
|
||||
{100, "Continue"},
|
||||
{101, "Switching Protocols"},
|
||||
{102, "Processing"},
|
||||
{200, "OK"},
|
||||
{201, "Created"},
|
||||
{202, "Accepted"},
|
||||
{203, "Non-Authoritative Information"},
|
||||
{204, "No Content"},
|
||||
{205, "Reset Content"},
|
||||
{206, "Partial Content"},
|
||||
{207, "Multi-Status"},
|
||||
{208, "Already Reported"},
|
||||
{226, "IM Used"},
|
||||
{300, "Multiple Choices"},
|
||||
{301, "Moved Permanently"},
|
||||
{302, "Found"},
|
||||
{303, "See Other"},
|
||||
{304, "Not Modified"},
|
||||
{305, "Use Proxy"},
|
||||
{307, "Temporary Redirect"},
|
||||
{308, "Permanent Redirect"},
|
||||
{400, "Bad Request"},
|
||||
{401, "Unathorized"},
|
||||
{402, "Payment Required"},
|
||||
{403, "Forbidden"},
|
||||
{404, "Not Found"},
|
||||
{405, "Method Not Allowed"},
|
||||
{406, "Not Acceptable"},
|
||||
{407, "Proxy Authentication Required"},
|
||||
{408, "Request Timeout"},
|
||||
{409, "Conflict"},
|
||||
{410, "Gone"},
|
||||
{411, "Length Required"},
|
||||
{412, "Precondition Failed"},
|
||||
{413, "Request Entity Too Large"},
|
||||
{414, "Request-URI Too Long"},
|
||||
{415, "Unsupported Media Type"},
|
||||
{416, "Requested Range Not Satisfiable"},
|
||||
{417, "Expectation Failed"},
|
||||
{418, "I\'m a Teapot"},
|
||||
{420, "Enhance Your Calm"},
|
||||
{422, "Unprocessable Entity"},
|
||||
{423, "Locked"},
|
||||
{424, "Failed Dependency"},
|
||||
{426, "Upgrade Required"},
|
||||
{428, "Precondition Required"},
|
||||
{429, "Too Many Requests"},
|
||||
{431, "Request Header Fields Too Large"},
|
||||
{444, "No Response"},
|
||||
{449, "Retry With"},
|
||||
{451, "Unavailable For Legal Reasons"},
|
||||
{500, "Internal Server Error"},
|
||||
{501, "Not Implemented"},
|
||||
{502, "Bad Gateway"},
|
||||
{503, "Service Unavailable"},
|
||||
{504, "Gateway Timeout"},
|
||||
{505, "HTTP Version Not Supported"},
|
||||
{506, "Variant Also Negotiates"},
|
||||
{507, "Insufficient Storage"},
|
||||
{508, "Loop Detected"},
|
||||
{509, "Bandwidth Limit Exceeded"},
|
||||
{510, "Not Extended"},
|
||||
{511, "Network Authentication Required"},
|
||||
{598, "Network Read Timeout Error"},
|
||||
{599, "Network Connect Timeout Error"},
|
||||
});
|
||||
|
||||
HTTPServer::http_error::http_error(int code, const string& what)
|
||||
: runtime_error(what),
|
||||
code(code) {}
|
||||
|
||||
void HTTPServer::send_response(struct evhttp_request* req, int code, const char* content_type, struct evbuffer* b) {
|
||||
struct evkeyvalq* headers = evhttp_request_get_output_headers(req);
|
||||
evhttp_add_header(headers, "Content-Type", content_type);
|
||||
evhttp_add_header(headers, "Server", "newserv");
|
||||
evhttp_send_reply(req, code, explanation_for_response_code.at(code), b);
|
||||
}
|
||||
|
||||
void HTTPServer::send_response(struct evhttp_request* req, int code, const char* content_type, const char* fmt, ...) {
|
||||
unique_ptr<struct evbuffer, void (*)(struct evbuffer*)> out_buffer(evbuffer_new(), evbuffer_free);
|
||||
va_list va;
|
||||
va_start(va, fmt);
|
||||
evbuffer_add_vprintf(out_buffer.get(), fmt, va);
|
||||
va_end(va);
|
||||
HTTPServer::send_response(req, code, content_type, out_buffer.get());
|
||||
}
|
||||
|
||||
unordered_multimap<string, string> HTTPServer::parse_url_params(const string& query) {
|
||||
unordered_multimap<string, string> params;
|
||||
if (query.empty()) {
|
||||
return params;
|
||||
}
|
||||
for (auto it : split(query, '&')) {
|
||||
size_t first_equals = it.find('=');
|
||||
if (first_equals != string::npos) {
|
||||
string value(it, first_equals + 1);
|
||||
|
||||
size_t write_offset = 0, read_offset = 0;
|
||||
for (; read_offset < value.size(); write_offset++) {
|
||||
if ((value[read_offset] == '%') && (read_offset < value.size() - 2)) {
|
||||
value[write_offset] =
|
||||
static_cast<char>(value_for_hex_char(value[read_offset + 1]) << 4) |
|
||||
static_cast<char>(value_for_hex_char(value[read_offset + 2]));
|
||||
read_offset += 3;
|
||||
} else if (value[write_offset] == '+') {
|
||||
value[write_offset] = ' ';
|
||||
read_offset++;
|
||||
} else {
|
||||
value[write_offset] = value[read_offset];
|
||||
read_offset++;
|
||||
}
|
||||
}
|
||||
value.resize(write_offset);
|
||||
|
||||
params.emplace(piecewise_construct, forward_as_tuple(it, 0, first_equals),
|
||||
forward_as_tuple(value));
|
||||
} else {
|
||||
params.emplace(it, "");
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
unordered_map<string, string> HTTPServer::parse_url_params_unique(const string& query) {
|
||||
unordered_map<string, string> ret;
|
||||
for (const auto& it : HTTPServer::parse_url_params(query)) {
|
||||
ret.emplace(it.first, std::move(it.second));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
const string& HTTPServer::get_url_param(
|
||||
const unordered_multimap<string, string>& params, const string& key, const string* _default) {
|
||||
|
||||
auto range = params.equal_range(key);
|
||||
if (range.first == range.second) {
|
||||
if (!_default) {
|
||||
throw out_of_range("URL parameter " + key + " not present");
|
||||
}
|
||||
return *_default;
|
||||
}
|
||||
|
||||
return range.first->second;
|
||||
}
|
||||
|
||||
HTTPServer::HTTPServer(shared_ptr<ServerState> state)
|
||||
: state(state),
|
||||
base(event_base_new(), event_base_free),
|
||||
http(evhttp_new(this->base.get()), evhttp_free),
|
||||
th(&HTTPServer::thread_fn, this) {
|
||||
evhttp_set_gencb(this->http.get(), this->dispatch_handle_request, this);
|
||||
}
|
||||
|
||||
void HTTPServer::listen(const string& socket_path) {
|
||||
int fd = ::listen(socket_path, 0, SOMAXCONN);
|
||||
server_log.info("Listening on Unix socket %s on fd %d (HTTP)", socket_path.c_str(), fd);
|
||||
this->add_socket(fd);
|
||||
}
|
||||
|
||||
void HTTPServer::listen(const string& addr, int port) {
|
||||
if (port == 0) {
|
||||
this->listen(addr);
|
||||
} else {
|
||||
int fd = ::listen(addr, port, SOMAXCONN);
|
||||
string netloc_str = render_netloc(addr, port);
|
||||
server_log.info("Listening on TCP interface %s on fd %d (HTTP)", netloc_str.c_str(), fd);
|
||||
this->add_socket(fd);
|
||||
}
|
||||
}
|
||||
|
||||
void HTTPServer::listen(int port) {
|
||||
this->listen("", port);
|
||||
}
|
||||
|
||||
void HTTPServer::add_socket(int fd) {
|
||||
evhttp_accept_socket(this->http.get(), fd);
|
||||
}
|
||||
|
||||
void HTTPServer::schedule_stop() {
|
||||
event_base_loopexit(this->base.get(), nullptr);
|
||||
}
|
||||
|
||||
void HTTPServer::wait_for_stop() {
|
||||
this->th.join();
|
||||
}
|
||||
|
||||
void HTTPServer::dispatch_handle_request(struct evhttp_request* req, void* ctx) {
|
||||
reinterpret_cast<HTTPServer*>(ctx)->handle_request(req);
|
||||
}
|
||||
|
||||
JSON HTTPServer::generate_quest_json_st(shared_ptr<const Quest> q) {
|
||||
if (!q) {
|
||||
return nullptr;
|
||||
}
|
||||
auto battle_rules_json = q->battle_rules ? q->battle_rules->json() : nullptr;
|
||||
auto challenge_template_index_json = (q->challenge_template_index >= 0)
|
||||
? q->challenge_template_index
|
||||
: JSON(nullptr);
|
||||
return JSON::dict({
|
||||
{"Number", q->quest_number},
|
||||
{"Episode", name_for_episode(q->episode)},
|
||||
{"Joinable", q->joinable},
|
||||
{"Name", q->name},
|
||||
{"BattleRules", std::move(battle_rules_json)},
|
||||
{"ChallengeTemplateIndex", std::move(challenge_template_index_json)},
|
||||
});
|
||||
}
|
||||
|
||||
JSON HTTPServer::generate_client_config_json_st(const Client::Config& config) {
|
||||
const char* drop_notifications_mode = "unknown";
|
||||
switch (config.get_drop_notification_mode()) {
|
||||
case Client::ItemDropNotificationMode::NOTHING:
|
||||
drop_notifications_mode = "off";
|
||||
break;
|
||||
case Client::ItemDropNotificationMode::RARES_ONLY:
|
||||
drop_notifications_mode = "rare";
|
||||
break;
|
||||
case Client::ItemDropNotificationMode::ALL_ITEMS:
|
||||
drop_notifications_mode = "on";
|
||||
break;
|
||||
case Client::ItemDropNotificationMode::ALL_ITEMS_INCLUDING_MESETA:
|
||||
drop_notifications_mode = "every";
|
||||
break;
|
||||
}
|
||||
|
||||
auto ret = JSON::dict({
|
||||
{"SpecificVersion", config.specific_version},
|
||||
{"SwitchAssistEnabled", (config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED) ? true : false)},
|
||||
{"InfiniteHPEnabled", (config.check_flag(Client::Flag::INFINITE_HP_ENABLED) ? true : false)},
|
||||
{"InfiniteTPEnabled", (config.check_flag(Client::Flag::INFINITE_TP_ENABLED) ? true : false)},
|
||||
{"DropNotificationMode", drop_notifications_mode},
|
||||
{"DebugEnabled", (config.check_flag(Client::Flag::DEBUG_ENABLED) ? true : false)},
|
||||
{"ProxySaveFilesEnabled", (config.check_flag(Client::Flag::PROXY_SAVE_FILES) ? true : false)},
|
||||
{"ProxyChatCommandsEnabled", (config.check_flag(Client::Flag::PROXY_CHAT_COMMANDS_ENABLED) ? true : false)},
|
||||
{"ProxyPlayerNotificationsEnabled", (config.check_flag(Client::Flag::PROXY_PLAYER_NOTIFICATIONS_ENABLED) ? true : false)},
|
||||
{"ProxySuppressClientPings", (config.check_flag(Client::Flag::PROXY_SUPPRESS_CLIENT_PINGS) ? true : false)},
|
||||
{"ProxyEp3InfiniteMesetaEnabled", (config.check_flag(Client::Flag::PROXY_EP3_INFINITE_MESETA_ENABLED) ? true : false)},
|
||||
{"ProxyEp3InfiniteTimeEnabled", (config.check_flag(Client::Flag::PROXY_EP3_INFINITE_TIME_ENABLED) ? true : false)},
|
||||
{"ProxyBlockFunctionCalls", (config.check_flag(Client::Flag::PROXY_BLOCK_FUNCTION_CALLS) ? true : false)},
|
||||
{"ProxyEp3UnmaskWhispers", (config.check_flag(Client::Flag::PROXY_EP3_UNMASK_WHISPERS) ? true : false)},
|
||||
});
|
||||
ret.emplace("OverrideRandomSeed", config.check_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED) ? config.override_random_seed : JSON(nullptr));
|
||||
ret.emplace("OverrideSectionID", (config.override_section_id != 0xFF) ? config.override_section_id : JSON(nullptr));
|
||||
ret.emplace("OverrideLobbyEvent", (config.override_lobby_event != 0xFF) ? config.override_lobby_event : JSON(nullptr));
|
||||
ret.emplace("OverrideLobbyNumber", (config.override_lobby_number != 0x80) ? config.override_lobby_number : JSON(nullptr));
|
||||
return ret;
|
||||
}
|
||||
|
||||
JSON HTTPServer::generate_license_json_st(shared_ptr<const License> l) {
|
||||
auto ret = JSON::dict({
|
||||
{"SerialNumber", l->serial_number},
|
||||
{"Flags", l->flags},
|
||||
{"Ep3CurrentMeseta", l->ep3_current_meseta},
|
||||
{"Ep3TotalMesetaEarned", l->ep3_total_meseta_earned},
|
||||
{"BBTeamID", l->bb_team_id},
|
||||
});
|
||||
ret.emplace("BanEndTime", l->ban_end_time ? l->ban_end_time : JSON(nullptr));
|
||||
ret.emplace("XBGamertag", !l->xb_gamertag.empty() ? l->xb_gamertag : JSON(nullptr));
|
||||
ret.emplace("XBUserID", l->xb_user_id ? l->xb_user_id : JSON(nullptr));
|
||||
ret.emplace("BBUsername", !l->bb_username.empty() ? l->bb_username : JSON(nullptr));
|
||||
return ret;
|
||||
};
|
||||
|
||||
JSON HTTPServer::generate_game_client_json_st(shared_ptr<const Client> c, shared_ptr<const ItemNameIndex> item_name_index) {
|
||||
auto ret = JSON::dict({
|
||||
{"ID", c->id},
|
||||
{"RemoteAddress", render_sockaddr_storage(c->channel.remote_addr)},
|
||||
{"Version", name_for_enum(c->version())},
|
||||
{"SubVersion", c->sub_version},
|
||||
{"Config", HTTPServer::generate_client_config_json_st(c->config)},
|
||||
{"Language", name_for_language_code(c->language())},
|
||||
{"LocationX", c->x},
|
||||
{"LocationZ", c->z},
|
||||
{"LocationFloor", c->floor},
|
||||
{"CanChat", c->can_chat},
|
||||
});
|
||||
ret.emplace("license", c->license ? HTTPServer::generate_license_json_st(c->license) : JSON(nullptr));
|
||||
auto l = c->lobby.lock();
|
||||
if (l) {
|
||||
ret.emplace("LobbyID", l->lobby_id);
|
||||
ret.emplace("LobbyClientID", c->lobby_client_id);
|
||||
}
|
||||
if (c->version() == Version::BB_V4) {
|
||||
ret.emplace("BBCharacterIndex", c->bb_character_index);
|
||||
}
|
||||
auto p = c->character(false, false);
|
||||
if (p) {
|
||||
if (!is_ep3(c->version())) {
|
||||
ret.emplace("InventoryItems", p->inventory.num_items);
|
||||
if (c->version() != Version::DC_NTE) {
|
||||
ret.emplace("InventoryLanguage", p->inventory.language);
|
||||
ret.emplace("NumHPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::HP));
|
||||
ret.emplace("NumTPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::TP));
|
||||
if (!is_v1_or_v2(c->version())) {
|
||||
ret.emplace("NumPowerMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::POWER));
|
||||
ret.emplace("NumDefMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::DEF));
|
||||
ret.emplace("NumMindMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::MIND));
|
||||
ret.emplace("NumEvadeMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::EVADE));
|
||||
ret.emplace("NumLuckMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::LUCK));
|
||||
}
|
||||
}
|
||||
JSON items_json = JSON::list();
|
||||
for (size_t z = 0; z < p->inventory.num_items; z++) {
|
||||
const auto& item = p->inventory.items[z];
|
||||
auto item_dict = JSON::dict({
|
||||
{"Flags", item.flags.load()},
|
||||
{"Data", item.data.hex()},
|
||||
{"ItemID", item.data.id.load()},
|
||||
});
|
||||
if (item_name_index) {
|
||||
item_dict.emplace("Description", item_name_index->describe_item(item.data, false));
|
||||
}
|
||||
items_json.emplace_back(std::move(item_dict));
|
||||
}
|
||||
ret.emplace("ATP", p->disp.stats.char_stats.atp.load());
|
||||
ret.emplace("MST", p->disp.stats.char_stats.mst.load());
|
||||
ret.emplace("EVP", p->disp.stats.char_stats.evp.load());
|
||||
ret.emplace("HP", p->disp.stats.char_stats.hp.load());
|
||||
ret.emplace("DFP", p->disp.stats.char_stats.dfp.load());
|
||||
ret.emplace("ATA", p->disp.stats.char_stats.ata.load());
|
||||
ret.emplace("LCK", p->disp.stats.char_stats.lck.load());
|
||||
ret.emplace("EXP", p->disp.stats.experience.load());
|
||||
ret.emplace("Meseta", p->disp.stats.meseta.load());
|
||||
auto tech_levels_json = JSON::dict();
|
||||
for (size_t z = 0; z < 0x13; z++) {
|
||||
auto level = p->get_technique_level(z);
|
||||
tech_levels_json.emplace(name_for_technique(z), (level != 0xFF) ? level : JSON(nullptr));
|
||||
}
|
||||
ret.emplace("TechniqueLevels", std::move(tech_levels_json));
|
||||
}
|
||||
ret.emplace("Height", p->disp.stats.height.load());
|
||||
ret.emplace("Level", p->disp.stats.level.load());
|
||||
ret.emplace("NameColor", p->disp.visual.name_color.load());
|
||||
ret.emplace("ExtraModel", (p->disp.visual.validation_flags & 2) ? p->disp.visual.extra_model : JSON(nullptr));
|
||||
ret.emplace("SectionID", name_for_section_id(p->disp.visual.section_id));
|
||||
ret.emplace("CharClass", name_for_char_class(p->disp.visual.section_id));
|
||||
ret.emplace("Costume", p->disp.visual.costume.load());
|
||||
ret.emplace("Skin", p->disp.visual.skin.load());
|
||||
ret.emplace("Face", p->disp.visual.face.load());
|
||||
ret.emplace("Head", p->disp.visual.head.load());
|
||||
ret.emplace("Hair", p->disp.visual.hair.load());
|
||||
ret.emplace("HairR", p->disp.visual.hair_r.load());
|
||||
ret.emplace("HairG", p->disp.visual.hair_g.load());
|
||||
ret.emplace("HairB", p->disp.visual.hair_b.load());
|
||||
ret.emplace("ProportionX", p->disp.visual.proportion_x.load());
|
||||
ret.emplace("ProportionY", p->disp.visual.proportion_y.load());
|
||||
|
||||
ret.emplace("Name", p->disp.name.decode(c->language()));
|
||||
ret.emplace("PlayTimeSeconds", p->play_time_seconds.load());
|
||||
|
||||
ret.emplace("AutoReply", p->auto_reply.decode(c->language()));
|
||||
ret.emplace("InfoBoard", p->info_board.decode(c->language()));
|
||||
auto battle_place_counts = JSON::list({
|
||||
p->battle_records.place_counts[0].load(),
|
||||
p->battle_records.place_counts[1].load(),
|
||||
p->battle_records.place_counts[2].load(),
|
||||
p->battle_records.place_counts[3].load(),
|
||||
});
|
||||
ret.emplace("BattlePlaceCounts", std::move(battle_place_counts));
|
||||
ret.emplace("BattleDisconnectCount", p->battle_records.disconnect_count.load());
|
||||
|
||||
if (!is_ep3(c->version())) {
|
||||
auto json_for_challenge_times = []<size_t Count>(const parray<ChallengeTime<false>, Count>& times) -> JSON {
|
||||
auto times_json = JSON::list();
|
||||
for (size_t z = 0; z < times.size(); z++) {
|
||||
times_json.emplace_back(times[z].load());
|
||||
}
|
||||
return times_json;
|
||||
};
|
||||
ret.emplace("ChallengeTitleColorXRGB1555", p->challenge_records.title_color.load());
|
||||
ret.emplace("ChallengeTimesEp1Online", json_for_challenge_times(p->challenge_records.times_ep1_online));
|
||||
ret.emplace("ChallengeTimesEp2Online", json_for_challenge_times(p->challenge_records.times_ep2_online));
|
||||
ret.emplace("ChallengeTimesEp1Offline", json_for_challenge_times(p->challenge_records.times_ep1_offline));
|
||||
ret.emplace("ChallengeGraveIsEp2", p->challenge_records.grave_is_ep2 ? true : false);
|
||||
ret.emplace("ChallengeGraveStageNum", p->challenge_records.grave_stage_num);
|
||||
ret.emplace("ChallengeGraveFloor", p->challenge_records.grave_floor);
|
||||
ret.emplace("ChallengeGraveDeaths", p->challenge_records.grave_deaths.load());
|
||||
{
|
||||
uint16_t year = 2000 + ((p->challenge_records.grave_time >> 28) & 0x0F);
|
||||
uint8_t month = (p->challenge_records.grave_time >> 24) & 0x0F;
|
||||
uint8_t day = (p->challenge_records.grave_time >> 16) & 0xFF;
|
||||
uint8_t hour = (p->challenge_records.grave_time >> 8) & 0xFF;
|
||||
uint8_t minute = p->challenge_records.grave_time & 0xFF;
|
||||
ret.emplace("ChallengeGraveTime", string_printf("%04hu-%02hhu-%02hhu %02hhu:%02hhu:00", year, month, day, hour, minute));
|
||||
}
|
||||
string grave_enemy_types;
|
||||
if (p->challenge_records.grave_defeated_by_enemy_rt_index) {
|
||||
for (EnemyType type : enemy_types_for_rare_table_index(p->challenge_records.grave_is_ep2 ? Episode::EP2 : Episode::EP1, p->challenge_records.grave_defeated_by_enemy_rt_index)) {
|
||||
if (!grave_enemy_types.empty()) {
|
||||
grave_enemy_types += "/";
|
||||
}
|
||||
grave_enemy_types += name_for_enum(type);
|
||||
}
|
||||
}
|
||||
ret.emplace("ChallengeGraveDefeatedByEnemy", std::move(grave_enemy_types));
|
||||
ret.emplace("ChallengeGraveX", p->challenge_records.grave_x.load());
|
||||
ret.emplace("ChallengeGraveY", p->challenge_records.grave_y.load());
|
||||
ret.emplace("ChallengeGraveZ", p->challenge_records.grave_z.load());
|
||||
ret.emplace("ChallengeGraveTeam", p->challenge_records.grave_team.decode());
|
||||
ret.emplace("ChallengeGraveMessage", p->challenge_records.grave_message.decode());
|
||||
ret.emplace("ChallengeAwardStateEp1OnlineFlags", p->challenge_records.ep1_online_award_state.rank_award_flags.load());
|
||||
ret.emplace("ChallengeAwardStateEp1OnlineMaxRank", p->challenge_records.ep1_online_award_state.maximum_rank.load());
|
||||
ret.emplace("ChallengeAwardStateEp2OnlineFlags", p->challenge_records.ep2_online_award_state.rank_award_flags.load());
|
||||
ret.emplace("ChallengeAwardStateEp2OnlineMaxRank", p->challenge_records.ep2_online_award_state.maximum_rank.load());
|
||||
ret.emplace("ChallengeAwardStateEp1OfflineFlags", p->challenge_records.ep1_offline_award_state.rank_award_flags.load());
|
||||
ret.emplace("ChallengeAwardStateEp1OfflineMaxRank", p->challenge_records.ep1_offline_award_state.maximum_rank.load());
|
||||
ret.emplace("ChallengeRankTitle", p->challenge_records.rank_title.decode());
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
JSON HTTPServer::generate_proxy_client_json_st(shared_ptr<const ProxyServer::LinkedSession> ses) {
|
||||
struct LobbyPlayer {
|
||||
uint32_t guild_card_number = 0;
|
||||
uint64_t xb_user_id = 0;
|
||||
std::string name;
|
||||
uint8_t language = 0;
|
||||
uint8_t section_id = 0;
|
||||
uint8_t char_class = 0;
|
||||
};
|
||||
std::vector<LobbyPlayer> lobby_players;
|
||||
|
||||
auto lobby_players_json = JSON::list();
|
||||
for (size_t z = 0; z < ses->lobby_players.size(); z++) {
|
||||
const auto& p = ses->lobby_players[z];
|
||||
if (p.guild_card_number) {
|
||||
lobby_players_json.emplace_back(JSON::dict({
|
||||
{"GuildCardNumber", p.guild_card_number},
|
||||
{"Name", p.name},
|
||||
{"Language", name_for_language_code(p.language)},
|
||||
{"SectionID", name_for_section_id(p.section_id)},
|
||||
{"CharClass", name_for_char_class(p.char_class)},
|
||||
}));
|
||||
lobby_players_json.back().emplace("XBUserID", p.xb_user_id ? p.xb_user_id : JSON(nullptr));
|
||||
} else {
|
||||
lobby_players_json.emplace_back(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
auto ret = JSON::dict({
|
||||
{"ID", ses->id},
|
||||
{"RemoteClientAddress", render_sockaddr_storage(ses->client_channel.remote_addr)},
|
||||
{"RemoteServerAddress", render_sockaddr_storage(ses->server_channel.remote_addr)},
|
||||
{"LocalPort", ses->local_port},
|
||||
{"NextDestination", render_sockaddr_storage(ses->next_destination)},
|
||||
{"Version", name_for_enum(ses->version())},
|
||||
{"SubVersion", ses->sub_version},
|
||||
{"Name", ses->character_name},
|
||||
{"DCHardwareID", ses->hardware_id},
|
||||
{"RemoteGuildCardNumber", ses->remote_guild_card_number},
|
||||
{"RemoteClientConfigData", format_data_string(&ses->remote_client_config_data[0], ses->remote_client_config_data.size())},
|
||||
{"Config", HTTPServer::generate_client_config_json_st(ses->config)},
|
||||
{"Language", name_for_language_code(ses->language())},
|
||||
{"LobbyClientID", ses->lobby_client_id},
|
||||
{"LeaderClientID", ses->leader_client_id},
|
||||
{"LocationX", ses->x},
|
||||
{"LocationZ", ses->z},
|
||||
{"LocationFloor", ses->floor},
|
||||
{"IsInGame", ses->is_in_game},
|
||||
{"IsInQuest", ses->is_in_quest},
|
||||
{"LobbyEvent", ses->lobby_event},
|
||||
{"LobbyDifficulty", name_for_difficulty(ses->lobby_difficulty)},
|
||||
{"LobbySectionID", name_for_section_id(ses->lobby_section_id)},
|
||||
{"LobbyMode", name_for_mode(ses->lobby_mode)},
|
||||
{"LobbyEpisode", name_for_episode(ses->lobby_episode)},
|
||||
{"LobbyRandomSeed", ses->lobby_random_seed},
|
||||
{"LobbyPlayers", std::move(lobby_players_json)},
|
||||
});
|
||||
switch (ses->drop_mode) {
|
||||
case ProxyServer::LinkedSession::DropMode::DISABLED:
|
||||
ret.emplace("DropMode", "none");
|
||||
break;
|
||||
case ProxyServer::LinkedSession::DropMode::PASSTHROUGH:
|
||||
ret.emplace("DropMode", "default");
|
||||
break;
|
||||
case ProxyServer::LinkedSession::DropMode::INTERCEPT:
|
||||
ret.emplace("DropMode", "proxy");
|
||||
break;
|
||||
}
|
||||
ret.emplace("License", ses->license ? HTTPServer::generate_license_json_st(ses->license) : JSON(nullptr));
|
||||
return ret;
|
||||
}
|
||||
|
||||
JSON HTTPServer::generate_lobby_json_st(shared_ptr<const Lobby> l, shared_ptr<const ItemNameIndex> item_name_index) {
|
||||
std::array<std::shared_ptr<Client>, 12> clients;
|
||||
|
||||
auto client_ids_json = JSON::list();
|
||||
for (size_t z = 0; z < l->max_clients; z++) {
|
||||
client_ids_json.emplace_back(l->clients[z] ? l->clients[z]->id : JSON(nullptr));
|
||||
}
|
||||
|
||||
auto ret = JSON::dict({
|
||||
{"ID", l->lobby_id},
|
||||
{"AllowedVersions", l->allowed_versions},
|
||||
{"Event", l->event},
|
||||
{"LeaderClientID", l->leader_id},
|
||||
{"MaxClients", l->max_clients},
|
||||
{"IdleTimeoutUsecs", l->idle_timeout_usecs},
|
||||
{"ClientIDs", std::move(client_ids_json)},
|
||||
{"IsGame", l->is_game()},
|
||||
{"IsPersistent", l->check_flag(Lobby::Flag::PERSISTENT)},
|
||||
});
|
||||
|
||||
if (l->is_game()) {
|
||||
ret.emplace("CheatsEnabled", l->check_flag(Lobby::Flag::CHEATS_ENABLED));
|
||||
ret.emplace("MinLevel", l->min_level + 1);
|
||||
ret.emplace("MaxLevel", l->max_level + 1);
|
||||
ret.emplace("BaseVersion", l->base_version);
|
||||
ret.emplace("Episode", name_for_episode(l->episode));
|
||||
ret.emplace("HasPassword", !l->password.empty());
|
||||
ret.emplace("Name", l->name);
|
||||
ret.emplace("RandomSeed", l->random_seed);
|
||||
if (l->episode != Episode::EP3) {
|
||||
ret.emplace("QuestInProgress", l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS));
|
||||
ret.emplace("JoinableQuestInProgress", l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS));
|
||||
auto variations_json = JSON::list();
|
||||
for (size_t z = 0; z < l->variations.size(); z++) {
|
||||
variations_json.emplace_back(l->variations[z].load());
|
||||
}
|
||||
ret.emplace("Variations", std::move(variations_json));
|
||||
ret.emplace("SectionID", name_for_section_id(l->effective_section_id()));
|
||||
ret.emplace("Mode", name_for_mode(l->mode));
|
||||
ret.emplace("Difficulty", name_for_difficulty(l->difficulty));
|
||||
ret.emplace("BaseEXPMultiplier", l->base_exp_multiplier);
|
||||
ret.emplace("AllowedDropModes", l->allowed_drop_modes);
|
||||
switch (l->drop_mode) {
|
||||
case Lobby::DropMode::DISABLED:
|
||||
ret.emplace("DropMode", "none");
|
||||
break;
|
||||
case Lobby::DropMode::CLIENT:
|
||||
ret.emplace("DropMode", "client");
|
||||
break;
|
||||
case Lobby::DropMode::SERVER_SHARED:
|
||||
ret.emplace("DropMode", "shared");
|
||||
break;
|
||||
case Lobby::DropMode::SERVER_PRIVATE:
|
||||
ret.emplace("DropMode", "private");
|
||||
break;
|
||||
case Lobby::DropMode::SERVER_DUPLICATE:
|
||||
ret.emplace("DropMode", "duplicate");
|
||||
break;
|
||||
}
|
||||
if (l->mode == GameMode::CHALLENGE) {
|
||||
ret.emplace("ChallengeEXPMultiplier", l->challenge_exp_multiplier);
|
||||
if (l->challenge_params) {
|
||||
ret.emplace("ChallengeStageNumber", l->challenge_params->stage_number);
|
||||
ret.emplace("ChallengeRankColor", l->challenge_params->rank_color);
|
||||
ret.emplace("ChallengeRankText", l->challenge_params->rank_text);
|
||||
ret.emplace("ChallengeRank0ThresholdBitmask", l->challenge_params->rank_thresholds[0].bitmask);
|
||||
ret.emplace("ChallengeRank0ThresholdSeconds", l->challenge_params->rank_thresholds[0].seconds);
|
||||
ret.emplace("ChallengeRank1ThresholdBitmask", l->challenge_params->rank_thresholds[1].bitmask);
|
||||
ret.emplace("ChallengeRank1ThresholdSeconds", l->challenge_params->rank_thresholds[1].seconds);
|
||||
ret.emplace("ChallengeRank2ThresholdBitmask", l->challenge_params->rank_thresholds[2].bitmask);
|
||||
ret.emplace("ChallengeRank2ThresholdSeconds", l->challenge_params->rank_thresholds[2].seconds);
|
||||
}
|
||||
}
|
||||
|
||||
auto floor_items_json = JSON::list();
|
||||
for (size_t floor = 0; floor < l->floor_item_managers.size(); floor++) {
|
||||
for (const auto& it : l->floor_item_managers[floor].items) {
|
||||
const auto& item = it.second;
|
||||
auto item_dict = JSON::dict({
|
||||
{"LocationFloor", floor},
|
||||
{"LocationX", item->x},
|
||||
{"LocationZ", item->z},
|
||||
{"DropNumber", item->drop_number},
|
||||
{"VisibilityFlags", item->visibility_flags},
|
||||
{"Data", item->data.hex()},
|
||||
{"ItemID", item->data.id.load()},
|
||||
});
|
||||
if (item_name_index) {
|
||||
item_dict.emplace("Description", item_name_index->describe_item(item->data, false));
|
||||
}
|
||||
floor_items_json.emplace_back(std::move(item_dict));
|
||||
}
|
||||
}
|
||||
ret.emplace("FloorItems", std::move(floor_items_json));
|
||||
ret.emplace("Quest", HTTPServer::generate_quest_json_st(l->quest));
|
||||
|
||||
} else {
|
||||
ret.emplace("BattleInProgress", l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS));
|
||||
ret.emplace("IsSpectatorTeam", l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM));
|
||||
ret.emplace("SpectatorsForbidden", l->check_flag(Lobby::Flag::SPECTATORS_FORBIDDEN));
|
||||
|
||||
auto ep3s = l->ep3_server;
|
||||
if (ep3s) {
|
||||
auto players_json = JSON::list();
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
if (!ep3s->name_entries[z].present) {
|
||||
players_json.emplace_back(nullptr);
|
||||
} else {
|
||||
auto lc = l->clients[z];
|
||||
|
||||
auto deck_entry = ep3s->deck_entries[z];
|
||||
JSON deck_json = nullptr;
|
||||
if (deck_entry) {
|
||||
auto cards_json = JSON::list();
|
||||
for (size_t w = 0; w < deck_entry->card_ids.size(); w++) {
|
||||
try {
|
||||
const auto& ce = ep3s->options.card_index->definition_for_id(deck_entry->card_ids[w]);
|
||||
auto name = ce->def.en_name.decode();
|
||||
if (name.empty()) {
|
||||
name = ce->def.en_short_name.decode();
|
||||
}
|
||||
if (name.empty()) {
|
||||
name = ce->def.jp_name.decode();
|
||||
}
|
||||
if (name.empty()) {
|
||||
name = ce->def.jp_short_name.decode();
|
||||
}
|
||||
cards_json.emplace_back(name);
|
||||
} catch (const out_of_range&) {
|
||||
cards_json.emplace_back(deck_entry->card_ids[w].load());
|
||||
}
|
||||
}
|
||||
deck_json = JSON::dict({
|
||||
{"Name", deck_entry->name.decode(lc ? lc->language() : 1)},
|
||||
{"TeamID", deck_entry->team_id.load()},
|
||||
{"Cards", std::move(cards_json)},
|
||||
{"GodWhimFlag", deck_entry->god_whim_flag},
|
||||
{"PlayerLevel", deck_entry->player_level.load()},
|
||||
});
|
||||
}
|
||||
|
||||
auto player_json = JSON::dict({
|
||||
{"PlayerName", ep3s->name_entries[z].name.decode(lc ? lc->language() : 1)},
|
||||
{"ClientID", ep3s->name_entries[z].client_id},
|
||||
{"IsCOM", !!ep3s->name_entries[z].is_cpu_player},
|
||||
{"Deck", std::move(deck_json)},
|
||||
});
|
||||
players_json.emplace_back(std::move(player_json));
|
||||
}
|
||||
}
|
||||
auto battle_state_json = JSON::dict({
|
||||
{"BehaviorFlags", ep3s->options.behavior_flags},
|
||||
{"RandomSeed", ep3s->options.opt_rand_crypt ? ep3s->options.opt_rand_crypt->seed() : JSON(nullptr)},
|
||||
{"RandomOffset", ep3s->options.opt_rand_crypt ? ep3s->options.opt_rand_crypt->absolute_offset() : JSON(nullptr)},
|
||||
{"Tournament", ep3s->options.tournament ? ep3s->options.tournament->json() : nullptr},
|
||||
{"MapNumber", ep3s->last_chosen_map ? ep3s->last_chosen_map->map_number : JSON(nullptr)},
|
||||
{"EnvironmentNumber", ep3s->map_and_rules ? ep3s->map_and_rules->environment_number : JSON(nullptr)},
|
||||
{"Rules", ep3s->map_and_rules ? ep3s->map_and_rules->rules.json() : nullptr},
|
||||
{"Players", std::move(players_json)},
|
||||
{"IsBattleFinished", ep3s->battle_finished},
|
||||
{"IsBattleInprogress", ep3s->battle_in_progress},
|
||||
{"RoundNumber", ep3s->round_num},
|
||||
{"FirstTeamTurn", ep3s->first_team_turn},
|
||||
{"CurrentTeamTurn", ep3s->current_team_turn1},
|
||||
{"BattlePhase", name_for_enum(ep3s->battle_phase)},
|
||||
{"SetupPhase", ep3s->setup_phase},
|
||||
{"RegistrationPhase", ep3s->registration_phase},
|
||||
{"ActionSubphase", ep3s->action_subphase},
|
||||
{"BattleStartTimeUsecs", ep3s->battle_start_usecs},
|
||||
{"TeamEXP", JSON::list({ep3s->team_exp[0], ep3s->team_exp[1]})},
|
||||
{"TeamDiceBonus", JSON::list({ep3s->team_dice_bonus[0], ep3s->team_dice_bonus[1]})},
|
||||
});
|
||||
// std::shared_ptr<StateFlags> state_flags;
|
||||
// std::array<std::shared_ptr<PlayerState>, 4> player_states;
|
||||
ret.emplace("Episode3BattleState", std::move(battle_state_json));
|
||||
} else {
|
||||
ret.emplace("Episode3BattleState", nullptr);
|
||||
}
|
||||
auto watched_lobby = l->watched_lobby.lock();
|
||||
if (watched_lobby) {
|
||||
ret.emplace("WatchedLobbyID", watched_lobby->lobby_id);
|
||||
}
|
||||
auto watcher_lobby_ids_json = JSON::list();
|
||||
for (const auto& watcher_lobby : l->watcher_lobbies) {
|
||||
watcher_lobby_ids_json.emplace_back(watcher_lobby->lobby_id);
|
||||
}
|
||||
ret.emplace("WatcherLobbyIDs", std::move(watcher_lobby_ids_json));
|
||||
ret.emplace("IsReplayLobby", !!l->battle_player);
|
||||
}
|
||||
|
||||
} else { // Not game
|
||||
ret.emplace("IsPublic", l->check_flag(Lobby::Flag::PUBLIC));
|
||||
ret.emplace("IsDefault", l->check_flag(Lobby::Flag::DEFAULT));
|
||||
ret.emplace("IsOverflow", l->check_flag(Lobby::Flag::IS_OVERFLOW));
|
||||
ret.emplace("Block", l->block);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
JSON HTTPServer::generate_game_server_clients_json() const {
|
||||
return call_on_event_thread<JSON>(this->state->base, [&]() {
|
||||
auto res = JSON::list();
|
||||
for (const auto& it : this->state->channel_to_client) {
|
||||
res.emplace_back(this->generate_game_client_json_st(it.second, this->state->item_name_index_opt(it.second->version())));
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
JSON HTTPServer::generate_proxy_server_clients_json() const {
|
||||
return call_on_event_thread<JSON>(this->state->base, [&]() {
|
||||
JSON res = JSON::list();
|
||||
for (const auto& it : this->state->proxy_server->all_sessions()) {
|
||||
res.emplace_back(this->generate_proxy_client_json_st(it.second));
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
JSON HTTPServer::generate_server_info_json() const {
|
||||
return call_on_event_thread<JSON>(this->state->base, [&]() {
|
||||
size_t game_count = 0;
|
||||
size_t lobby_count = 0;
|
||||
for (const auto& it : this->state->id_to_lobby) {
|
||||
if (it.second->is_game()) {
|
||||
game_count++;
|
||||
} else {
|
||||
lobby_count++;
|
||||
}
|
||||
}
|
||||
uint64_t uptime_usecs = now() - this->state->creation_time;
|
||||
return JSON::dict({
|
||||
{"StartTimeUsecs", this->state->creation_time},
|
||||
{"StartTime", format_time(this->state->creation_time)},
|
||||
{"UptimeUsecs", uptime_usecs},
|
||||
{"Uptime", format_duration(uptime_usecs)},
|
||||
{"LobbyCount", lobby_count},
|
||||
{"GameCount", game_count},
|
||||
{"ClientCount", this->state->channel_to_client.size()},
|
||||
{"ProxySessionCount", this->state->proxy_server->num_sessions()},
|
||||
{"ServerName", this->state->name},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
JSON HTTPServer::generate_lobbies_json() const {
|
||||
return call_on_event_thread<JSON>(this->state->base, [&]() {
|
||||
JSON res = JSON::list();
|
||||
for (const auto& it : this->state->id_to_lobby) {
|
||||
res.emplace_back(this->generate_lobby_json_st(it.second, this->state->item_name_index_opt(it.second->base_version)));
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
JSON HTTPServer::generate_summary_json() const {
|
||||
auto ret = call_on_event_thread<JSON>(this->state->base, [&]() {
|
||||
auto clients_json = JSON::list();
|
||||
for (const auto& it : this->state->channel_to_client) {
|
||||
auto c = it.second;
|
||||
auto p = c->character(false, false);
|
||||
auto l = c->lobby.lock();
|
||||
clients_json.emplace_back(JSON::dict({
|
||||
{"ID", c->id},
|
||||
{"SerialNumber", c->license ? c->license->serial_number : JSON(nullptr)},
|
||||
{"Name", p ? p->disp.name.decode(it.second->language()) : JSON(nullptr)},
|
||||
{"Version", name_for_enum(it.second->version())},
|
||||
{"Language", name_for_language_code(it.second->language())},
|
||||
{"Level", p ? p->disp.stats.level + 1 : JSON(nullptr)},
|
||||
{"Class", p ? name_for_char_class(p->disp.visual.char_class) : JSON(nullptr)},
|
||||
{"SectionID", p ? name_for_section_id(p->disp.visual.section_id) : JSON(nullptr)},
|
||||
{"LobbyID", l ? l->lobby_id : JSON(nullptr)},
|
||||
}));
|
||||
}
|
||||
|
||||
auto proxy_clients_json = JSON::list();
|
||||
for (const auto& it : this->state->proxy_server->all_sessions()) {
|
||||
proxy_clients_json.emplace_back(JSON::dict({
|
||||
{"SerialNumber", it.second->license ? it.second->license->serial_number : JSON(nullptr)},
|
||||
{"Name", it.second->character_name},
|
||||
{"Version", name_for_enum(it.second->version())},
|
||||
{"Language", name_for_language_code(it.second->language())},
|
||||
}));
|
||||
}
|
||||
|
||||
auto games_json = JSON::list();
|
||||
for (const auto& it : this->state->id_to_lobby) {
|
||||
auto l = it.second;
|
||||
if (l->is_game()) {
|
||||
auto game_json = JSON::dict({
|
||||
{"ID", l->lobby_id},
|
||||
{"Name", l->name},
|
||||
{"BaseVersion", name_for_enum(l->base_version)},
|
||||
{"Players", l->count_clients()},
|
||||
{"CheatsEnabled", l->check_flag(Lobby::Flag::CHEATS_ENABLED)},
|
||||
{"Episode", name_for_episode(l->episode)},
|
||||
{"HasPassword", !l->password.empty()},
|
||||
});
|
||||
if (l->episode == Episode::EP3) {
|
||||
auto ep3s = l->ep3_server;
|
||||
game_json.emplace("BattleInProgress", l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS));
|
||||
game_json.emplace("IsSpectatorTeam", l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM));
|
||||
game_json.emplace("MapNumber", (ep3s && ep3s->last_chosen_map) ? ep3s->last_chosen_map->map_number : JSON(nullptr));
|
||||
game_json.emplace("Rules", (ep3s && ep3s->map_and_rules) ? ep3s->map_and_rules->rules.json() : nullptr);
|
||||
} else {
|
||||
game_json.emplace("QuestInProgress", l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS));
|
||||
game_json.emplace("JoinableQuestInProgress", l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS));
|
||||
game_json.emplace("SectionID", name_for_section_id(l->effective_section_id()));
|
||||
game_json.emplace("Mode", name_for_mode(l->mode));
|
||||
game_json.emplace("Difficulty", name_for_difficulty(l->difficulty));
|
||||
game_json.emplace("Quest", this->generate_quest_json_st(l->quest));
|
||||
}
|
||||
games_json.emplace_back(std::move(game_json));
|
||||
}
|
||||
}
|
||||
|
||||
return JSON::dict({
|
||||
{"Clients", std::move(clients_json)},
|
||||
{"ProxyClients", std::move(proxy_clients_json)},
|
||||
{"Games", std::move(games_json)},
|
||||
});
|
||||
});
|
||||
ret.emplace("Server", this->generate_server_info_json());
|
||||
return ret;
|
||||
}
|
||||
|
||||
JSON HTTPServer::generate_all_json() const {
|
||||
return JSON::dict({
|
||||
{"Clients", this->generate_game_server_clients_json()},
|
||||
{"ProxyClients", this->generate_proxy_server_clients_json()},
|
||||
{"Lobbies", this->generate_lobbies_json()},
|
||||
{"Server", this->generate_server_info_json()},
|
||||
});
|
||||
}
|
||||
|
||||
JSON HTTPServer::generate_ep3_cards_json(bool trial) const {
|
||||
auto index = call_on_event_thread<shared_ptr<const Episode3::CardIndex>>(this->state->base, [&]() {
|
||||
return trial ? this->state->ep3_card_index_trial : this->state->ep3_card_index;
|
||||
});
|
||||
return index->definitions_json();
|
||||
}
|
||||
|
||||
JSON HTTPServer::generate_common_tables_json() const {
|
||||
auto [set_v2, set_v3_v4] = call_on_event_thread<pair<shared_ptr<const CommonItemSet>, shared_ptr<const CommonItemSet>>>(this->state->base, [&]() {
|
||||
return make_pair(this->state->common_item_set_v2, this->state->common_item_set_v3_v4);
|
||||
});
|
||||
return JSON::dict({
|
||||
{"v1_v2", set_v2->json()},
|
||||
{"v3_v4", set_v3_v4->json()},
|
||||
});
|
||||
}
|
||||
|
||||
JSON HTTPServer::generate_rare_tables_json() const {
|
||||
auto sets = call_on_event_thread<unordered_map<string, shared_ptr<const RareItemSet>>>(this->state->base, [&]() {
|
||||
return this->state->rare_item_sets;
|
||||
});
|
||||
JSON ret = JSON::list();
|
||||
for (const auto& it : sets) {
|
||||
ret.emplace_back(it.first);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
JSON HTTPServer::generate_rare_table_json(const std::string& table_name) const {
|
||||
try {
|
||||
auto colls = call_on_event_thread<pair<shared_ptr<const RareItemSet>, shared_ptr<const ItemNameIndex>>>(this->state->base, [&]() {
|
||||
const auto& table = this->state->rare_item_sets.at(table_name);
|
||||
shared_ptr<const ItemNameIndex> name_index;
|
||||
if (ends_with(table_name, "-v1")) {
|
||||
name_index = this->state->item_name_index_opt(Version::DC_V1);
|
||||
} else if (ends_with(table_name, "-v2")) {
|
||||
name_index = this->state->item_name_index_opt(Version::PC_V2);
|
||||
} else if (ends_with(table_name, "-v3")) {
|
||||
name_index = this->state->item_name_index_opt(Version::GC_V3);
|
||||
} else if (ends_with(table_name, "-v4")) {
|
||||
name_index = this->state->item_name_index_opt(Version::BB_V4);
|
||||
}
|
||||
return make_pair(table, name_index);
|
||||
});
|
||||
return colls.first->json(colls.second);
|
||||
} catch (const out_of_range&) {
|
||||
throw http_error(404, "table does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
void HTTPServer::handle_request(struct evhttp_request* req) {
|
||||
shared_ptr<const JSON> ret;
|
||||
uint32_t serialize_options = 0;
|
||||
uint64_t start_time = now();
|
||||
string uri = evhttp_request_get_uri(req);
|
||||
|
||||
try {
|
||||
std::unordered_multimap<std::string, std::string> query;
|
||||
size_t query_pos = uri.find('?');
|
||||
if (query_pos != string::npos) {
|
||||
query = this->parse_url_params(uri.substr(query_pos + 1));
|
||||
uri.resize(query_pos);
|
||||
}
|
||||
|
||||
static const string default_format_option = "false";
|
||||
if (this->get_url_param(query, "format", &default_format_option) == "true") {
|
||||
serialize_options |= JSON::SerializeOption::FORMAT | JSON::SerializeOption::SORT_DICT_KEYS;
|
||||
}
|
||||
if (this->get_url_param(query, "hex", &default_format_option) == "true") {
|
||||
serialize_options |= JSON::SerializeOption::HEX_INTEGERS;
|
||||
}
|
||||
|
||||
if (uri == "/") {
|
||||
auto endpoints_json = JSON::list({
|
||||
"/y/data/ep3-cards",
|
||||
"/y/data/ep3-cards-trial",
|
||||
"/y/data/common-tables",
|
||||
"/y/data/rare-tables",
|
||||
"/y/data/rare-tables/<TABLE-NAME>",
|
||||
"/y/data/config",
|
||||
"/y/clients",
|
||||
"/y/proxy-clients",
|
||||
"/y/lobbies",
|
||||
"/y/server",
|
||||
"/y/summary",
|
||||
"/y/all",
|
||||
});
|
||||
ret = make_shared<JSON>(JSON::dict({{"endpoints", std::move(endpoints_json)}}));
|
||||
|
||||
} else if (uri == "/y/data/ep3-cards") {
|
||||
ret = make_shared<JSON>(this->generate_ep3_cards_json(false));
|
||||
} else if (uri == "/y/data/ep3-cards-trial") {
|
||||
ret = make_shared<JSON>(this->generate_ep3_cards_json(true));
|
||||
} else if (uri == "/y/data/common-tables") {
|
||||
ret = make_shared<JSON>(this->generate_common_tables_json());
|
||||
} else if (uri == "/y/data/rare-tables") {
|
||||
ret = make_shared<JSON>(this->generate_rare_tables_json());
|
||||
} else if (!strncmp(uri.c_str(), "/y/data/rare-tables/", 20)) {
|
||||
ret = make_shared<JSON>(this->generate_rare_table_json(uri.substr(20)));
|
||||
} else if (uri == "/y/data/config") {
|
||||
ret = call_on_event_thread<shared_ptr<const JSON>>(this->state->base, [this]() { return this->state->config_json; });
|
||||
} else if (uri == "/y/clients") {
|
||||
ret = make_shared<JSON>(this->generate_game_server_clients_json());
|
||||
} else if (uri == "/y/proxy-clients") {
|
||||
ret = make_shared<JSON>(this->generate_proxy_server_clients_json());
|
||||
} else if (uri == "/y/lobbies") {
|
||||
ret = make_shared<JSON>(this->generate_lobbies_json());
|
||||
} else if (uri == "/y/server") {
|
||||
ret = make_shared<JSON>(this->generate_server_info_json());
|
||||
} else if (uri == "/y/summary") {
|
||||
ret = make_shared<JSON>(this->generate_summary_json());
|
||||
} else if (uri == "/y/all") {
|
||||
ret = make_shared<JSON>(this->generate_all_json());
|
||||
|
||||
} else {
|
||||
throw http_error(404, "unknown action");
|
||||
}
|
||||
|
||||
} catch (const http_error& e) {
|
||||
unique_ptr<struct evbuffer, void (*)(struct evbuffer*)> out_buffer(evbuffer_new(), evbuffer_free);
|
||||
evbuffer_add_printf(out_buffer.get(), "%s", e.what());
|
||||
this->send_response(req, e.code, "text/plain", out_buffer.get());
|
||||
return;
|
||||
|
||||
} catch (const exception& e) {
|
||||
unique_ptr<struct evbuffer, void (*)(struct evbuffer*)> out_buffer(evbuffer_new(), evbuffer_free);
|
||||
evbuffer_add_printf(out_buffer.get(), "Error during request: %s", e.what());
|
||||
this->send_response(req, 500, "text/plain", out_buffer.get());
|
||||
server_log.warning("internal server error during http request: %s", e.what());
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t handler_end = now();
|
||||
unique_ptr<struct evbuffer, void (*)(struct evbuffer*)> out_buffer(evbuffer_new(), evbuffer_free);
|
||||
string* serialized = new string(ret->serialize(JSON::SerializeOption::ESCAPE_CONTROLS_ONLY | serialize_options));
|
||||
size_t size = serialized->size();
|
||||
uint64_t serialize_end = now();
|
||||
auto cleanup = +[](const void*, size_t, void* s) -> void {
|
||||
delete reinterpret_cast<string*>(s);
|
||||
};
|
||||
evbuffer_add_reference(out_buffer.get(), serialized->data(), serialized->size(), cleanup, serialized);
|
||||
this->send_response(req, 200, "application/json", out_buffer.get());
|
||||
|
||||
string handler_time = format_duration(handler_end - start_time);
|
||||
string serialize_time = format_duration(serialize_end - handler_end);
|
||||
string size_str = format_size(size);
|
||||
server_log.info("[HTTPServer] %s in [handler: %s, serialize: %s, size: %s]",
|
||||
uri.c_str(), handler_time.c_str(), serialize_time.c_str(), size_str.c_str());
|
||||
}
|
||||
|
||||
void HTTPServer::thread_fn() {
|
||||
event_base_loop(this->base.get(), EVLOOP_NO_EXIT_ON_EMPTY);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/event.h>
|
||||
#include <event2/http.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "ProxyServer.hh"
|
||||
#include "ServerState.hh"
|
||||
|
||||
class HTTPServer {
|
||||
public:
|
||||
HTTPServer(std::shared_ptr<ServerState> state);
|
||||
HTTPServer(const HTTPServer&) = delete;
|
||||
HTTPServer(HTTPServer&&) = delete;
|
||||
HTTPServer& operator=(const HTTPServer&) = delete;
|
||||
HTTPServer& operator=(HTTPServer&&) = delete;
|
||||
virtual ~HTTPServer() = default;
|
||||
|
||||
void listen(const std::string& socket_path);
|
||||
void listen(const std::string& addr, int port);
|
||||
void listen(int port);
|
||||
void add_socket(int fd);
|
||||
|
||||
void schedule_stop();
|
||||
void wait_for_stop();
|
||||
|
||||
protected:
|
||||
class http_error : public std::runtime_error {
|
||||
public:
|
||||
http_error(int code, const std::string& what);
|
||||
int code;
|
||||
};
|
||||
|
||||
std::shared_ptr<ServerState> state;
|
||||
std::shared_ptr<struct event_base> base;
|
||||
std::shared_ptr<struct evhttp> http;
|
||||
std::thread th;
|
||||
|
||||
void thread_fn();
|
||||
|
||||
static void dispatch_handle_request(struct evhttp_request* req, void* ctx);
|
||||
void handle_request(struct evhttp_request* req);
|
||||
|
||||
static const std::unordered_map<int, const char*> explanation_for_response_code;
|
||||
static void send_response(struct evhttp_request* req, int code, const char* content_type, struct evbuffer* b);
|
||||
static void send_response(struct evhttp_request* req, int code, const char* content_type, const char* fmt, ...);
|
||||
|
||||
static std::unordered_multimap<std::string, std::string> parse_url_params(const std::string& query);
|
||||
static std::unordered_map<std::string, std::string> parse_url_params_unique(const std::string& query);
|
||||
static const std::string& get_url_param(
|
||||
const std::unordered_multimap<std::string, std::string>& params,
|
||||
const std::string& key,
|
||||
const std::string* _default = nullptr);
|
||||
|
||||
static JSON generate_quest_json_st(std::shared_ptr<const Quest> q);
|
||||
static JSON generate_client_config_json_st(const Client::Config& config);
|
||||
static JSON generate_license_json_st(std::shared_ptr<const License> l);
|
||||
static JSON generate_game_client_json_st(std::shared_ptr<const Client> c, std::shared_ptr<const ItemNameIndex> item_name_index);
|
||||
static JSON generate_proxy_client_json_st(std::shared_ptr<const ProxyServer::LinkedSession> ses);
|
||||
static JSON generate_lobby_json_st(std::shared_ptr<const Lobby> l, std::shared_ptr<const ItemNameIndex> item_name_index);
|
||||
JSON generate_game_server_clients_json() const;
|
||||
JSON generate_proxy_server_clients_json() const;
|
||||
JSON generate_server_info_json() const;
|
||||
JSON generate_lobbies_json() const;
|
||||
JSON generate_summary_json() const;
|
||||
JSON generate_all_json() const;
|
||||
|
||||
JSON generate_ep3_cards_json(bool trial) const;
|
||||
JSON generate_common_tables_json() const;
|
||||
JSON generate_rare_tables_json() const;
|
||||
JSON generate_rare_table_json(const std::string& table_name) const;
|
||||
};
|
||||
+108
-50
@@ -135,28 +135,28 @@ IPStackSimulator::~IPStackSimulator() {
|
||||
}
|
||||
}
|
||||
|
||||
void IPStackSimulator::listen(const string& name, const string& socket_path, FrameInfo::LinkType link_type) {
|
||||
void IPStackSimulator::listen(const string& name, const string& socket_path, Protocol proto) {
|
||||
int fd = ::listen(socket_path, 0, SOMAXCONN);
|
||||
ip_stack_simulator_log.info("Listening on Unix socket %s on fd %d as %s", socket_path.c_str(), fd, name.c_str());
|
||||
this->add_socket(name, fd, link_type);
|
||||
this->add_socket(name, fd, proto);
|
||||
}
|
||||
|
||||
void IPStackSimulator::listen(const string& name, const string& addr, int port, FrameInfo::LinkType link_type) {
|
||||
void IPStackSimulator::listen(const string& name, const string& addr, int port, Protocol proto) {
|
||||
if (port == 0) {
|
||||
this->listen(name, addr, link_type);
|
||||
this->listen(name, addr, proto);
|
||||
} else {
|
||||
int fd = ::listen(addr, port, SOMAXCONN);
|
||||
string netloc_str = render_netloc(addr, port);
|
||||
ip_stack_simulator_log.info("Listening on TCP interface %s on fd %d as %s", netloc_str.c_str(), fd, name.c_str());
|
||||
this->add_socket(name, fd, link_type);
|
||||
this->add_socket(name, fd, proto);
|
||||
}
|
||||
}
|
||||
|
||||
void IPStackSimulator::listen(const string& name, int port, FrameInfo::LinkType link_type) {
|
||||
this->listen(name, "", port, link_type);
|
||||
void IPStackSimulator::listen(const string& name, int port, Protocol proto) {
|
||||
this->listen(name, "", port, proto);
|
||||
}
|
||||
|
||||
void IPStackSimulator::add_socket(const string& name, int fd, FrameInfo::LinkType link_type) {
|
||||
void IPStackSimulator::add_socket(const string& name, int fd, Protocol proto) {
|
||||
unique_listener l(
|
||||
evconnlistener_new(
|
||||
this->base.get(),
|
||||
@@ -166,7 +166,7 @@ void IPStackSimulator::add_socket(const string& name, int fd, FrameInfo::LinkTyp
|
||||
0,
|
||||
fd),
|
||||
evconnlistener_free);
|
||||
this->listening_sockets.emplace(piecewise_construct, forward_as_tuple(fd), forward_as_tuple(name, link_type, std::move(l)));
|
||||
this->listening_sockets.emplace(piecewise_construct, forward_as_tuple(fd), forward_as_tuple(name, proto, std::move(l)));
|
||||
}
|
||||
|
||||
uint32_t IPStackSimulator::connect_address_for_remote_address(uint32_t remote_addr) {
|
||||
@@ -180,14 +180,15 @@ uint32_t IPStackSimulator::connect_address_for_remote_address(uint32_t remote_ad
|
||||
}
|
||||
}
|
||||
|
||||
IPStackSimulator::IPClient::IPClient(shared_ptr<IPStackSimulator> sim, FrameInfo::LinkType link_type, struct bufferevent* bev)
|
||||
IPStackSimulator::IPClient::IPClient(shared_ptr<IPStackSimulator> sim, Protocol protocol, struct bufferevent* bev)
|
||||
: sim(sim),
|
||||
bev(bev, bufferevent_free),
|
||||
link_type(link_type),
|
||||
protocol(protocol),
|
||||
mac_addr(0),
|
||||
ipv4_addr(0),
|
||||
idle_timeout_event(event_new(sim->base.get(), -1, EV_TIMEOUT, &IPStackSimulator::IPClient::dispatch_on_idle_timeout, this), event_free) {
|
||||
struct timeval tv = usecs_to_timeval(60 * 1000 * 1000);
|
||||
uint64_t idle_timeout_usecs = sim->state->client_idle_timeout_usecs;
|
||||
struct timeval tv = usecs_to_timeval(idle_timeout_usecs);
|
||||
event_add(this->idle_timeout_event.get(), &tv);
|
||||
}
|
||||
|
||||
@@ -255,7 +256,7 @@ void IPStackSimulator::on_listen_accept(struct evconnlistener* listener,
|
||||
|
||||
struct bufferevent* bev = bufferevent_socket_new(this->base.get(), fd,
|
||||
BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);
|
||||
auto c = make_shared<IPClient>(this->shared_from_this(), listening_socket->link_type, bev);
|
||||
auto c = make_shared<IPClient>(this->shared_from_this(), listening_socket->protocol, bev);
|
||||
this->bev_to_client.emplace(make_pair(bev, c));
|
||||
|
||||
bufferevent_setcb(bev, &IPStackSimulator::dispatch_on_client_input, nullptr,
|
||||
@@ -294,27 +295,69 @@ void IPStackSimulator::on_client_input(struct bufferevent* bev) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct timeval tv = usecs_to_timeval(60 * 1000 * 1000);
|
||||
auto sim = c->sim.lock();
|
||||
uint64_t idle_timeout_usecs = sim ? sim->state->client_idle_timeout_usecs : 60000000;
|
||||
struct timeval tv = usecs_to_timeval(idle_timeout_usecs);
|
||||
event_add(c->idle_timeout_event.get(), &tv);
|
||||
|
||||
while (evbuffer_get_length(buf) >= 2) {
|
||||
uint16_t frame_size;
|
||||
evbuffer_copyout(buf, &frame_size, 2);
|
||||
if (evbuffer_get_length(buf) < static_cast<size_t>(frame_size + 2)) {
|
||||
break; // No complete frame available; done for now
|
||||
}
|
||||
switch (c->protocol) {
|
||||
case Protocol::ETHERNET_TAPSERVER:
|
||||
case Protocol::HDLC_TAPSERVER:
|
||||
while (evbuffer_get_length(buf) >= 2) {
|
||||
uint16_t frame_size;
|
||||
evbuffer_copyout(buf, &frame_size, 2);
|
||||
if (evbuffer_get_length(buf) < static_cast<size_t>(frame_size + 2)) {
|
||||
break; // No complete frame available; done for now
|
||||
}
|
||||
|
||||
evbuffer_drain(buf, 2);
|
||||
string frame(frame_size, '\0');
|
||||
evbuffer_remove(buf, frame.data(), frame.size());
|
||||
evbuffer_drain(buf, 2);
|
||||
string frame(frame_size, '\0');
|
||||
evbuffer_remove(buf, frame.data(), frame.size());
|
||||
|
||||
try {
|
||||
this->on_client_frame(c, frame);
|
||||
} catch (const exception& e) {
|
||||
if (ip_stack_simulator_log.warning("Failed to process frame: %s", e.what())) {
|
||||
print_data(stderr, frame);
|
||||
try {
|
||||
this->on_client_frame(c, frame);
|
||||
} catch (const exception& e) {
|
||||
if (ip_stack_simulator_log.warning("Failed to process frame: %s", e.what())) {
|
||||
print_data(stderr, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Protocol::HDLC_RAW:
|
||||
while (evbuffer_get_length(buf) >= 2) {
|
||||
struct evbuffer_ptr res = evbuffer_search(buf, "\x7E", 1, nullptr);
|
||||
if (res.pos < 0) {
|
||||
break;
|
||||
}
|
||||
size_t start_offset = res.pos;
|
||||
|
||||
if (evbuffer_ptr_set(buf, &res, 1, EVBUFFER_PTR_ADD)) {
|
||||
ip_stack_simulator_log.warning("Cannot advance search for end of frame");
|
||||
break;
|
||||
}
|
||||
|
||||
struct evbuffer_ptr end_res = evbuffer_search(buf, "\x7E", 1, &res);
|
||||
if (end_res.pos < 0) {
|
||||
break;
|
||||
}
|
||||
size_t frame_size = end_res.pos + 1 - start_offset;
|
||||
|
||||
if (start_offset) {
|
||||
evbuffer_drain(buf, start_offset);
|
||||
}
|
||||
|
||||
string frame(frame_size, '\0');
|
||||
evbuffer_remove(buf, frame.data(), frame.size());
|
||||
|
||||
try {
|
||||
this->on_client_frame(c, frame);
|
||||
} catch (const exception& e) {
|
||||
if (ip_stack_simulator_log.warning("Failed to process frame: %s", e.what())) {
|
||||
print_data(stderr, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,8 +383,8 @@ void IPStackSimulator::send_layer3_frame(shared_ptr<IPClient> c, FrameInfo::Prot
|
||||
void IPStackSimulator::send_layer3_frame(shared_ptr<IPClient> c, FrameInfo::Protocol proto, const void* data, size_t size) const {
|
||||
struct evbuffer* out_buf = bufferevent_get_output(c->bev.get());
|
||||
|
||||
switch (c->link_type) {
|
||||
case FrameInfo::LinkType::ETHERNET: {
|
||||
switch (c->protocol) {
|
||||
case Protocol::ETHERNET_TAPSERVER: {
|
||||
EthernetHeader ether;
|
||||
ether.dest_mac = c->mac_addr;
|
||||
ether.src_mac = this->host_mac_address_bytes;
|
||||
@@ -373,7 +416,8 @@ void IPStackSimulator::send_layer3_frame(shared_ptr<IPClient> c, FrameInfo::Prot
|
||||
break;
|
||||
}
|
||||
|
||||
case FrameInfo::LinkType::HDLC: {
|
||||
case Protocol::HDLC_TAPSERVER:
|
||||
case Protocol::HDLC_RAW: {
|
||||
HDLCHeader hdlc;
|
||||
hdlc.start_sentinel1 = 0x7E;
|
||||
hdlc.address = 0xFF;
|
||||
@@ -410,8 +454,10 @@ void IPStackSimulator::send_layer3_frame(shared_ptr<IPClient> c, FrameInfo::Prot
|
||||
print_data(stderr, w.str());
|
||||
}
|
||||
|
||||
le_uint16_t frame_size = escaped.size();
|
||||
evbuffer_add(out_buf, &frame_size, 2);
|
||||
if (c->protocol == Protocol::HDLC_TAPSERVER) {
|
||||
le_uint16_t frame_size = escaped.size();
|
||||
evbuffer_add(out_buf, &frame_size, 2);
|
||||
}
|
||||
evbuffer_add(out_buf, escaped.data(), escaped.size());
|
||||
if (this->pcap_text_log_file) {
|
||||
this->log_frame(escaped);
|
||||
@@ -425,9 +471,13 @@ void IPStackSimulator::send_layer3_frame(shared_ptr<IPClient> c, FrameInfo::Prot
|
||||
}
|
||||
|
||||
void IPStackSimulator::on_client_frame(shared_ptr<IPClient> c, const string& frame) {
|
||||
FrameInfo::LinkType link_type = (c->protocol == Protocol::ETHERNET_TAPSERVER)
|
||||
? FrameInfo::LinkType::ETHERNET
|
||||
: FrameInfo::LinkType::HDLC;
|
||||
|
||||
const string* effective_data = &frame;
|
||||
string hdlc_unescaped_data;
|
||||
if (c->link_type == FrameInfo::LinkType::HDLC) {
|
||||
if (link_type == FrameInfo::LinkType::HDLC) {
|
||||
hdlc_unescaped_data = unescape_hdlc_frame(frame);
|
||||
effective_data = &hdlc_unescaped_data;
|
||||
}
|
||||
@@ -436,7 +486,7 @@ void IPStackSimulator::on_client_frame(shared_ptr<IPClient> c, const string& fra
|
||||
}
|
||||
this->log_frame(*effective_data);
|
||||
|
||||
FrameInfo fi(c->link_type, *effective_data);
|
||||
FrameInfo fi(link_type, *effective_data);
|
||||
if (ip_stack_simulator_log.should_log(LogLevel::DEBUG)) {
|
||||
string fi_header = fi.header_str();
|
||||
ip_stack_simulator_log.debug("Frame header: %s", fi_header.c_str());
|
||||
@@ -1116,6 +1166,7 @@ void IPStackSimulator::on_client_tcp_frame(
|
||||
throw runtime_error("non-SYN frame does not correspond to any open TCP connection");
|
||||
}
|
||||
bool conn_valid = true;
|
||||
bool acked_seq_changed = false;
|
||||
|
||||
if (fi.tcp->flags & TCPHeader::Flag::ACK) {
|
||||
ip_stack_simulator_log.debug("Client sent ACK %08" PRIX32, fi.tcp->ack_num.load());
|
||||
@@ -1139,6 +1190,7 @@ void IPStackSimulator::on_client_tcp_frame(
|
||||
conn->acked_server_seq += ack_delta;
|
||||
conn->resend_push_usecs = DEFAULT_RESEND_PUSH_USECS;
|
||||
conn->next_push_max_frame_size = conn->max_frame_size;
|
||||
acked_seq_changed = true;
|
||||
|
||||
ip_stack_simulator_log.debug("Removed %08" PRIX32 " bytes from pending buffer and advanced acked_server_seq to %08" PRIX32,
|
||||
ack_delta, conn->acked_server_seq);
|
||||
@@ -1248,9 +1300,9 @@ void IPStackSimulator::on_client_tcp_frame(
|
||||
conn_str.c_str(), conn->acked_server_seq, conn->next_client_seq, conn->bytes_received);
|
||||
}
|
||||
|
||||
if (conn_valid) {
|
||||
if (conn_valid && acked_seq_changed) {
|
||||
// Try to send some more data if the client is waiting on it
|
||||
this->send_pending_push_frame(c, *conn);
|
||||
this->send_pending_push_frame(c, *conn, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1304,19 +1356,28 @@ void IPStackSimulator::open_server_connection(shared_ptr<IPClient> c, IPClient::
|
||||
}
|
||||
}
|
||||
|
||||
void IPStackSimulator::send_pending_push_frame(shared_ptr<IPClient> c, IPClient::TCPConnection& conn) {
|
||||
void IPStackSimulator::send_pending_push_frame(
|
||||
shared_ptr<IPClient> c, IPClient::TCPConnection& conn, bool always_send) {
|
||||
size_t pending_bytes = evbuffer_get_length(conn.pending_data.get());
|
||||
if (!pending_bytes) {
|
||||
event_del(conn.resend_push_event.get());
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're waiting to receive an ACK from the client, don't send another PSH
|
||||
// until we get the ACK (unless this is a resend of a previous PSH due to a
|
||||
// timeout)
|
||||
if (!always_send && event_pending(conn.resend_push_event.get(), EV_TIMEOUT, nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t bytes_to_send = min<size_t>(pending_bytes, conn.next_push_max_frame_size);
|
||||
if ((c->link_type == FrameInfo::LinkType::HDLC) && (bytes_to_send > 200)) {
|
||||
if (c->protocol == Protocol::HDLC_TAPSERVER) {
|
||||
// There is a bug in Dolphin's modem implementation (which I wrote, so it's
|
||||
// my fault) that causes commands to be dropped when too much data is sent
|
||||
// at once. To work around this, we only send up to 200 bytes in each push
|
||||
// frame.
|
||||
bytes_to_send = 200;
|
||||
bytes_to_send = min<size_t>(bytes_to_send, 200);
|
||||
}
|
||||
|
||||
ip_stack_simulator_log.debug("Sending PSH frame with seq_num %08" PRIX32 ", 0x%zX/0x%zX data bytes",
|
||||
@@ -1335,8 +1396,7 @@ void IPStackSimulator::send_pending_push_frame(shared_ptr<IPClient> c, IPClient:
|
||||
if (conn.resend_push_usecs > 5000000) {
|
||||
conn.resend_push_usecs = 5000000;
|
||||
}
|
||||
conn.next_push_max_frame_size = max<size_t>(
|
||||
0x100, conn.next_push_max_frame_size - 0x100);
|
||||
conn.next_push_max_frame_size = max<size_t>(0x100, conn.next_push_max_frame_size - 0x100);
|
||||
}
|
||||
|
||||
void IPStackSimulator::send_tcp_frame(
|
||||
@@ -1397,15 +1457,11 @@ void IPStackSimulator::dispatch_on_resend_push(evutil_socket_t, short, void* ctx
|
||||
if (!sim) {
|
||||
ip_stack_simulator_log.warning("Resend push event triggered for client on deleted simulator; ignoring");
|
||||
} else {
|
||||
sim->on_resend_push(c, *conn);
|
||||
sim->send_pending_push_frame(c, *conn, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IPStackSimulator::on_resend_push(shared_ptr<IPClient> c, IPClient::TCPConnection& conn) {
|
||||
this->send_pending_push_frame(c, conn);
|
||||
}
|
||||
|
||||
void IPStackSimulator::dispatch_on_server_input(struct bufferevent*, void* ctx) {
|
||||
auto* conn = reinterpret_cast<IPClient::TCPConnection*>(ctx);
|
||||
auto c = conn->client.lock();
|
||||
@@ -1426,11 +1482,13 @@ void IPStackSimulator::on_server_input(shared_ptr<IPClient> c, IPClient::TCPConn
|
||||
ip_stack_simulator_log.debug("Server input event: 0x%zX bytes to read",
|
||||
evbuffer_get_length(buf));
|
||||
|
||||
struct timeval tv = usecs_to_timeval(60 * 1000 * 1000);
|
||||
auto sim = c->sim.lock();
|
||||
uint64_t idle_timeout_usecs = sim ? sim->state->client_idle_timeout_usecs : 60000000;
|
||||
struct timeval tv = usecs_to_timeval(idle_timeout_usecs);
|
||||
event_add(c->idle_timeout_event.get(), &tv);
|
||||
|
||||
evbuffer_add_buffer(conn.pending_data.get(), buf);
|
||||
this->send_pending_push_frame(c, conn);
|
||||
this->send_pending_push_frame(c, conn, false);
|
||||
}
|
||||
|
||||
void IPStackSimulator::dispatch_on_server_error(
|
||||
|
||||
+17
-13
@@ -16,15 +16,21 @@
|
||||
|
||||
class IPStackSimulator : public std::enable_shared_from_this<IPStackSimulator> {
|
||||
public:
|
||||
enum class Protocol {
|
||||
ETHERNET_TAPSERVER = 0,
|
||||
HDLC_TAPSERVER,
|
||||
HDLC_RAW,
|
||||
};
|
||||
|
||||
IPStackSimulator(
|
||||
std::shared_ptr<struct event_base> base,
|
||||
std::shared_ptr<ServerState> state);
|
||||
~IPStackSimulator();
|
||||
|
||||
void listen(const std::string& name, const std::string& socket_path, FrameInfo::LinkType link_type);
|
||||
void listen(const std::string& name, const std::string& addr, int port, FrameInfo::LinkType link_type);
|
||||
void listen(const std::string& name, int port, FrameInfo::LinkType link_type);
|
||||
void add_socket(const std::string& name, int fd, FrameInfo::LinkType link_type);
|
||||
void listen(const std::string& name, const std::string& socket_path, Protocol protocol);
|
||||
void listen(const std::string& name, const std::string& addr, int port, Protocol protocol);
|
||||
void listen(const std::string& name, int port, Protocol protocol);
|
||||
void add_socket(const std::string& name, int fd, Protocol protocol);
|
||||
|
||||
static uint32_t connect_address_for_remote_address(uint32_t remote_addr);
|
||||
|
||||
@@ -41,7 +47,7 @@ private:
|
||||
std::weak_ptr<IPStackSimulator> sim;
|
||||
|
||||
unique_bufferevent bev;
|
||||
FrameInfo::LinkType link_type;
|
||||
Protocol protocol;
|
||||
uint32_t hdlc_escape_control_character_flags = 0xFFFFFFFF;
|
||||
uint32_t hdlc_remote_magic_number = 0;
|
||||
parray<uint8_t, 6> mac_addr; // Only used for LinkType::ETHERNET
|
||||
@@ -80,7 +86,7 @@ private:
|
||||
|
||||
unique_event idle_timeout_event;
|
||||
|
||||
IPClient(std::shared_ptr<IPStackSimulator> sim, FrameInfo::LinkType link_type, struct bufferevent* bev);
|
||||
IPClient(std::shared_ptr<IPStackSimulator> sim, Protocol protocol, struct bufferevent* bev);
|
||||
|
||||
static void dispatch_on_idle_timeout(evutil_socket_t fd, short events, void* ctx);
|
||||
void on_idle_timeout();
|
||||
@@ -88,12 +94,12 @@ private:
|
||||
|
||||
struct ListeningSocket {
|
||||
std::string name;
|
||||
FrameInfo::LinkType link_type;
|
||||
Protocol protocol;
|
||||
unique_listener listener;
|
||||
|
||||
ListeningSocket(const std::string& name, FrameInfo::LinkType link_type, unique_listener&& l)
|
||||
ListeningSocket(const std::string& name, Protocol protocol, unique_listener&& l)
|
||||
: name(name),
|
||||
link_type(link_type),
|
||||
protocol(protocol),
|
||||
listener(std::move(l)) {}
|
||||
};
|
||||
|
||||
@@ -138,15 +144,13 @@ private:
|
||||
void on_client_udp_frame(std::shared_ptr<IPClient> c, const FrameInfo& fi);
|
||||
void on_client_tcp_frame(std::shared_ptr<IPClient> c, const FrameInfo& fi);
|
||||
|
||||
static void dispatch_on_resend_push(evutil_socket_t fd, short events, void* ctx);
|
||||
void on_resend_push(std::shared_ptr<IPClient> c, IPClient::TCPConnection& conn);
|
||||
|
||||
static void dispatch_on_server_input(struct bufferevent* bev, void* ctx);
|
||||
void on_server_input(std::shared_ptr<IPClient> c, IPClient::TCPConnection& conn);
|
||||
static void dispatch_on_server_error(struct bufferevent* bev, short events, void* ctx);
|
||||
void on_server_error(std::shared_ptr<IPClient> c, IPClient::TCPConnection& conn, short events);
|
||||
|
||||
void send_pending_push_frame(std::shared_ptr<IPClient> c, IPClient::TCPConnection& conn);
|
||||
static void dispatch_on_resend_push(evutil_socket_t, short, void* ctx);
|
||||
void send_pending_push_frame(std::shared_ptr<IPClient> c, IPClient::TCPConnection& conn, bool always_send);
|
||||
void send_tcp_frame(
|
||||
std::shared_ptr<IPClient> c,
|
||||
IPClient::TCPConnection& conn,
|
||||
|
||||
+206
-168
@@ -18,15 +18,16 @@ ItemCreator::ItemCreator(
|
||||
shared_ptr<const WeaponRandomSet> weapon_random_set,
|
||||
shared_ptr<const TekkerAdjustmentSet> tekker_adjustment_set,
|
||||
shared_ptr<const ItemParameterTable> item_parameter_table,
|
||||
Version version,
|
||||
std::shared_ptr<const ItemData::StackLimits> stack_limits,
|
||||
Episode episode,
|
||||
GameMode mode,
|
||||
uint8_t difficulty,
|
||||
uint8_t section_id,
|
||||
uint32_t random_seed,
|
||||
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt,
|
||||
shared_ptr<const BattleRules> restrictions)
|
||||
: log(string_printf("[ItemCreator:%s/%s/%s/%c/%hhu] ", name_for_enum(version), abbreviation_for_episode(episode), abbreviation_for_mode(mode), abbreviation_for_difficulty(difficulty), section_id), lobby_log.min_level),
|
||||
version(version),
|
||||
: log(string_printf("[ItemCreator:%s/%s/%s/%c/%hhu] ", name_for_enum(stack_limits->version), abbreviation_for_episode(episode), abbreviation_for_mode(mode), abbreviation_for_difficulty(difficulty), section_id), lobby_log.min_level),
|
||||
logic_version(stack_limits->version),
|
||||
stack_limits(stack_limits),
|
||||
episode(episode),
|
||||
mode(mode),
|
||||
difficulty(difficulty),
|
||||
@@ -37,32 +38,28 @@ ItemCreator::ItemCreator(
|
||||
weapon_random_set(weapon_random_set),
|
||||
tekker_adjustment_set(tekker_adjustment_set),
|
||||
item_parameter_table(item_parameter_table),
|
||||
common_item_set(common_item_set),
|
||||
pt(common_item_set->get_table(this->episode, this->mode, this->difficulty, this->section_id)),
|
||||
restrictions(restrictions),
|
||||
random_crypt(random_seed) {
|
||||
opt_rand_crypt(opt_rand_crypt ? make_shared<PSOV2Encryption>(opt_rand_crypt->seed()) : nullptr) {
|
||||
this->generate_unit_stars_tables();
|
||||
}
|
||||
|
||||
void ItemCreator::set_random_state(uint32_t seed, uint32_t absolute_offset) {
|
||||
if ((this->random_crypt.seed() != seed) || (this->random_crypt.absolute_offset() > absolute_offset)) {
|
||||
this->random_crypt = PSOV2Encryption(seed);
|
||||
void ItemCreator::set_random_crypt(shared_ptr<PSOLFGEncryption> new_random_crypt) {
|
||||
this->opt_rand_crypt = new_random_crypt;
|
||||
}
|
||||
|
||||
void ItemCreator::set_section_id(uint8_t new_section_id) {
|
||||
if (this->section_id != new_section_id) {
|
||||
this->section_id = new_section_id;
|
||||
this->log.prefix = string_printf("[ItemCreator:%s/%s/%s/%c/%hhu] ",
|
||||
name_for_enum(stack_limits->version),
|
||||
abbreviation_for_episode(episode),
|
||||
abbreviation_for_mode(mode),
|
||||
abbreviation_for_difficulty(difficulty),
|
||||
this->section_id);
|
||||
this->pt = common_item_set->get_table(this->episode, this->mode, this->difficulty, this->section_id);
|
||||
}
|
||||
while (this->random_crypt.absolute_offset() < absolute_offset) {
|
||||
this->random_crypt.next();
|
||||
}
|
||||
}
|
||||
|
||||
void ItemCreator::set_box_destroyed(uint16_t entity_id) {
|
||||
this->destroyed_boxes.emplace(entity_id);
|
||||
}
|
||||
|
||||
void ItemCreator::set_monster_destroyed(uint16_t entity_id) {
|
||||
this->destroyed_monsters.emplace(entity_id);
|
||||
}
|
||||
|
||||
void ItemCreator::clear_destroyed_entities() {
|
||||
this->destroyed_monsters.clear();
|
||||
this->destroyed_boxes.clear();
|
||||
}
|
||||
|
||||
bool ItemCreator::are_rare_drops_allowed() const {
|
||||
@@ -130,50 +127,62 @@ uint8_t ItemCreator::normalize_area_number(uint8_t area) const {
|
||||
}
|
||||
|
||||
} else {
|
||||
return this->restrictions->box_drop_area;
|
||||
return this->restrictions->box_drop_area - 1;
|
||||
}
|
||||
}
|
||||
|
||||
ItemData ItemCreator::on_box_item_drop(uint16_t entity_id, uint8_t area) {
|
||||
return this->destroyed_boxes.count(entity_id)
|
||||
? ItemData()
|
||||
: this->on_box_item_drop_with_area_norm(this->normalize_area_number(area));
|
||||
ItemCreator::DropResult ItemCreator::on_box_item_drop(uint8_t area) {
|
||||
try {
|
||||
return this->on_box_item_drop_with_area_norm(this->normalize_area_number(area));
|
||||
} catch (const exception& e) {
|
||||
this->log.error("Exception in item creation: %s", e.what());
|
||||
return DropResult();
|
||||
}
|
||||
}
|
||||
|
||||
ItemData ItemCreator::on_monster_item_drop(uint16_t entity_id, uint32_t enemy_type, uint8_t area) {
|
||||
return this->destroyed_monsters.count(entity_id)
|
||||
? ItemData()
|
||||
: this->on_monster_item_drop_with_area_norm(enemy_type, this->normalize_area_number(area));
|
||||
ItemCreator::DropResult ItemCreator::on_monster_item_drop(uint32_t enemy_type, uint8_t area) {
|
||||
try {
|
||||
return this->on_monster_item_drop_with_area_norm(enemy_type, this->normalize_area_number(area));
|
||||
} catch (const exception& e) {
|
||||
this->log.error("Exception in item creation: %s", e.what());
|
||||
return DropResult();
|
||||
}
|
||||
}
|
||||
|
||||
ItemData ItemCreator::on_box_item_drop_with_area_norm(uint8_t area_norm) {
|
||||
this->log.info("Box drop checks for area_norm %02hhX; random state: %08" PRIX32 " %08" PRIX32,
|
||||
area_norm, this->random_crypt.seed(), this->random_crypt.absolute_offset());
|
||||
ItemData item = this->check_rare_specs_and_create_rare_box_item(area_norm);
|
||||
if (item.empty()) {
|
||||
ItemCreator::DropResult ItemCreator::on_box_item_drop_with_area_norm(uint8_t area_norm) {
|
||||
this->log.info("Box drop checks for area_norm %02hhX", area_norm);
|
||||
if (this->opt_rand_crypt) {
|
||||
this->log.info("Random state: %08" PRIX32 " %08" PRIX32,
|
||||
this->opt_rand_crypt->seed(), this->opt_rand_crypt->absolute_offset());
|
||||
}
|
||||
DropResult res;
|
||||
res.item = this->check_rare_specs_and_create_rare_box_item(area_norm);
|
||||
if (!res.item.empty()) {
|
||||
res.is_from_rare_table = true;
|
||||
} else {
|
||||
uint8_t item_class = this->get_rand_from_weighted_tables_2d_vertical(this->pt->box_item_class_prob_table, area_norm);
|
||||
this->log.info("Item class is %02hhX", item_class);
|
||||
switch (item_class) {
|
||||
case 0: // Weapon
|
||||
item.data1[0] = 0;
|
||||
res.item.data1[0] = 0;
|
||||
break;
|
||||
case 1: // Armor
|
||||
item.data1[0] = 1;
|
||||
item.data1[1] = 1;
|
||||
res.item.data1[0] = 1;
|
||||
res.item.data1[1] = 1;
|
||||
break;
|
||||
case 2: // Shield
|
||||
item.data1[0] = 1;
|
||||
item.data1[1] = 2;
|
||||
res.item.data1[0] = 1;
|
||||
res.item.data1[1] = 2;
|
||||
break;
|
||||
case 3: // Unit
|
||||
item.data1[0] = 1;
|
||||
item.data1[1] = 3;
|
||||
res.item.data1[0] = 1;
|
||||
res.item.data1[1] = 3;
|
||||
break;
|
||||
case 4: // Tool
|
||||
item.data1[0] = 3;
|
||||
res.item.data1[0] = 3;
|
||||
break;
|
||||
case 5: // Meseta
|
||||
item.data1[0] = 4;
|
||||
res.item.data1[0] = 4;
|
||||
break;
|
||||
case 6: // Nothing
|
||||
break;
|
||||
@@ -181,30 +190,37 @@ ItemData ItemCreator::on_box_item_drop_with_area_norm(uint8_t area_norm) {
|
||||
throw logic_error("this should be impossible");
|
||||
}
|
||||
if (item_class < 6) {
|
||||
this->generate_common_item_variances(area_norm, item);
|
||||
this->generate_common_item_variances(area_norm, res.item);
|
||||
}
|
||||
}
|
||||
return item;
|
||||
return res;
|
||||
}
|
||||
|
||||
ItemData ItemCreator::on_monster_item_drop_with_area_norm(uint32_t enemy_type, uint8_t area_norm) {
|
||||
ItemCreator::DropResult ItemCreator::on_monster_item_drop_with_area_norm(uint32_t enemy_type, uint8_t area_norm) {
|
||||
if (enemy_type > 0x58) {
|
||||
this->log.warning("Invalid enemy type: %" PRIX32, enemy_type);
|
||||
return ItemData();
|
||||
return DropResult();
|
||||
}
|
||||
this->log.info("Enemy type: %" PRIX32 "", enemy_type);
|
||||
if (this->opt_rand_crypt) {
|
||||
this->log.info("Random state: %08" PRIX32 " %08" PRIX32,
|
||||
this->opt_rand_crypt->seed(), this->opt_rand_crypt->absolute_offset());
|
||||
}
|
||||
this->log.info("Enemy type: %" PRIX32 "; random state: %08" PRIX32 " %08" PRIX32, enemy_type, this->random_crypt.seed(), this->random_crypt.absolute_offset());
|
||||
|
||||
uint8_t type_drop_prob = this->pt->enemy_type_drop_probs.at(enemy_type);
|
||||
uint8_t drop_sample = this->rand_int(100);
|
||||
if (drop_sample >= type_drop_prob) {
|
||||
this->log.info("Drop not chosen (%hhu >= %hhu)", drop_sample, type_drop_prob);
|
||||
return ItemData();
|
||||
return DropResult();
|
||||
} else {
|
||||
this->log.info("Drop chosen (%hhu < %hhu)", drop_sample, type_drop_prob);
|
||||
}
|
||||
|
||||
ItemData item = this->check_rare_spec_and_create_rare_enemy_item(enemy_type, area_norm);
|
||||
if (item.empty()) {
|
||||
DropResult res;
|
||||
res.item = this->check_rare_spec_and_create_rare_enemy_item(enemy_type, area_norm);
|
||||
if (!res.item.empty()) {
|
||||
res.is_from_rare_table = true;
|
||||
} else {
|
||||
uint32_t item_class_determinant =
|
||||
this->should_allow_meseta_drops()
|
||||
? this->rand_int(3)
|
||||
@@ -229,34 +245,34 @@ ItemData ItemCreator::on_monster_item_drop_with_area_norm(uint32_t enemy_type, u
|
||||
|
||||
switch (item_class) {
|
||||
case 0: // Weapon
|
||||
item.data1[0] = 0x00;
|
||||
res.item.data1[0] = 0x00;
|
||||
break;
|
||||
case 1: // Armor
|
||||
item.data1w[0] = 0x0101;
|
||||
res.item.data1w[0] = 0x0101;
|
||||
break;
|
||||
case 2: // Shield
|
||||
item.data1w[0] = 0x0201;
|
||||
res.item.data1w[0] = 0x0201;
|
||||
break;
|
||||
case 3: // Unit
|
||||
item.data1w[0] = 0x0301;
|
||||
res.item.data1w[0] = 0x0301;
|
||||
break;
|
||||
case 4: // Tool
|
||||
item.data1[0] = 0x03;
|
||||
res.item.data1[0] = 0x03;
|
||||
break;
|
||||
case 5: // Meseta
|
||||
item.data1[0] = 0x04;
|
||||
item.data2d = this->choose_meseta_amount(this->pt->enemy_meseta_ranges, enemy_type) & 0xFFFF;
|
||||
res.item.data1[0] = 0x04;
|
||||
res.item.data2d = this->choose_meseta_amount(this->pt->enemy_meseta_ranges, enemy_type) & 0xFFFF;
|
||||
break;
|
||||
default:
|
||||
return item;
|
||||
return res;
|
||||
}
|
||||
|
||||
if (item.data1[0] != 0x04) {
|
||||
this->generate_common_item_variances(area_norm, item);
|
||||
if (res.item.data1[0] != 0x04) {
|
||||
this->generate_common_item_variances(area_norm, res.item);
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
return res;
|
||||
}
|
||||
|
||||
ItemData ItemCreator::check_rare_specs_and_create_rare_box_item(uint8_t area_norm) {
|
||||
@@ -270,23 +286,27 @@ ItemData ItemCreator::check_rare_specs_and_create_rare_box_item(uint8_t area_nor
|
||||
for (const auto& spec : rare_specs) {
|
||||
item = this->check_rate_and_create_rare_item(spec, area_norm);
|
||||
if (!item.empty()) {
|
||||
this->log.info("Box spec %08" PRIX32 " produced item %02hhX%02hhX%02hhX",
|
||||
spec.probability, spec.item_code[0], spec.item_code[1], spec.item_code[2]);
|
||||
if (this->log.should_log(LogLevel::INFO)) {
|
||||
auto hex = spec.data.hex();
|
||||
this->log.info("Box spec %08" PRIX32 " produced item %s", spec.probability, hex.c_str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
this->log.info("Box spec %08" PRIX32 " did not produce item %02hhX%02hhX%02hhX",
|
||||
spec.probability, spec.item_code[0], spec.item_code[1], spec.item_code[2]);
|
||||
if (this->log.should_log(LogLevel::INFO)) {
|
||||
auto hex = spec.data.hex();
|
||||
this->log.info("Box spec %08" PRIX32 " did not produce item %s", spec.probability, hex.c_str());
|
||||
}
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
uint32_t ItemCreator::rand_int(uint64_t max) {
|
||||
return this->random_crypt.next() % max;
|
||||
return random_from_optional_crypt(this->opt_rand_crypt) % max;
|
||||
}
|
||||
|
||||
float ItemCreator::rand_float_0_1_from_crypt() {
|
||||
// This lacks some precision, but matches the original implementation.
|
||||
return (static_cast<double>(this->random_crypt.next() >> 16) / 65536.0);
|
||||
return (static_cast<double>(random_from_optional_crypt(this->opt_rand_crypt) >> 16) / 65536.0);
|
||||
}
|
||||
|
||||
template <size_t NumRanges>
|
||||
@@ -296,16 +316,17 @@ uint32_t ItemCreator::choose_meseta_amount(
|
||||
uint16_t min = ranges[table_index].min;
|
||||
uint16_t max = ranges[table_index].max;
|
||||
|
||||
// Note: The original code seems like it has a bug here: it compares to 0xFF
|
||||
// instead of 0xFFFF (and returns 0xFF if either limit matches 0xFF).
|
||||
uint32_t ret = 0;
|
||||
if (((min == 0xFFFF) || (max == 0xFFFF)) || (max < min)) {
|
||||
ret = 0xFFFF;
|
||||
} else if (min != max) {
|
||||
ret = this->rand_int((max - min) + 1) + min;
|
||||
} else {
|
||||
// Note: The original code returns 0xFF here if either limit is equal to 0xFF
|
||||
// (despite them being 16-bit integers!)
|
||||
uint16_t ret;
|
||||
if (min == max) {
|
||||
ret = min;
|
||||
} else if (max < min) {
|
||||
ret = this->rand_int((min - max) + 1) + max;
|
||||
} else {
|
||||
ret = this->rand_int((max - min) + 1) + min;
|
||||
}
|
||||
|
||||
this->log.info("Chose %" PRIu32 " Meseta from range [%hu, %hu]", ret, min, max);
|
||||
return ret;
|
||||
}
|
||||
@@ -326,12 +347,16 @@ ItemData ItemCreator::check_rare_spec_and_create_rare_enemy_item(uint32_t enemy_
|
||||
for (const auto& spec : rare_specs) {
|
||||
item = this->check_rate_and_create_rare_item(spec, area_norm);
|
||||
if (!item.empty()) {
|
||||
this->log.info("Enemy spec %08" PRIX32 " produced item %02hhX%02hhX%02hhX",
|
||||
spec.probability, spec.item_code[0], spec.item_code[1], spec.item_code[2]);
|
||||
if (this->log.should_log(LogLevel::INFO)) {
|
||||
auto hex = spec.data.hex();
|
||||
this->log.info("Enemy spec %08" PRIX32 " produced item %s", spec.probability, hex.c_str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
this->log.info("Enemy spec %08" PRIX32 " did not produce item %02hhX%02hhX%02hhX",
|
||||
spec.probability, spec.item_code[0], spec.item_code[1], spec.item_code[2]);
|
||||
if (this->log.should_log(LogLevel::INFO)) {
|
||||
auto hex = spec.data.hex();
|
||||
this->log.info("Enemy spec %08" PRIX32 " did not produce item %s", spec.probability, hex.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
return item;
|
||||
@@ -348,37 +373,36 @@ ItemData ItemCreator::check_rate_and_create_rare_item(const RareItemSet::Expande
|
||||
return ItemData();
|
||||
}
|
||||
|
||||
ItemData item;
|
||||
item.data1[0] = drop.item_code[0];
|
||||
item.data1[1] = drop.item_code[1];
|
||||
item.data1[2] = drop.item_code[2];
|
||||
switch (item.data1[0]) {
|
||||
case 0:
|
||||
if (this->pt->has_rare_bonus_value_prob_table) {
|
||||
this->generate_rare_weapon_bonuses(item, this->rand_int(10));
|
||||
} else {
|
||||
this->generate_common_weapon_bonuses(item, area_norm);
|
||||
}
|
||||
this->set_item_unidentified_flag_if_not_challenge(item);
|
||||
break;
|
||||
case 1:
|
||||
this->generate_common_armor_slots_and_bonuses(item);
|
||||
break;
|
||||
case 2:
|
||||
this->generate_common_mag_variances(item);
|
||||
break;
|
||||
case 3:
|
||||
this->clear_tool_item_if_invalid(item);
|
||||
this->set_tool_item_amount_to_1(item);
|
||||
break;
|
||||
case 4:
|
||||
break;
|
||||
default:
|
||||
throw logic_error("invalid item class");
|
||||
ItemData item = drop.data;
|
||||
if (item.can_be_encoded_in_rel_rare_table()) {
|
||||
switch (item.data1[0]) {
|
||||
case 0:
|
||||
if (this->pt->has_rare_bonus_value_prob_table) {
|
||||
this->generate_rare_weapon_bonuses(item, this->rand_int(10));
|
||||
} else {
|
||||
this->generate_common_weapon_bonuses(item, area_norm);
|
||||
}
|
||||
this->set_item_unidentified_flag_if_not_challenge(item);
|
||||
break;
|
||||
case 1:
|
||||
this->generate_common_armor_slots_and_bonuses(item);
|
||||
break;
|
||||
case 2:
|
||||
this->generate_common_mag_variances(item);
|
||||
break;
|
||||
case 3:
|
||||
this->clear_tool_item_if_invalid(item);
|
||||
this->set_tool_item_amount_to_1(item);
|
||||
break;
|
||||
case 4:
|
||||
break;
|
||||
default:
|
||||
throw logic_error("invalid item class");
|
||||
}
|
||||
this->set_item_kill_count_if_unsealable(item);
|
||||
}
|
||||
|
||||
this->clear_item_if_restricted(item);
|
||||
this->set_item_kill_count_if_unsealable(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -458,23 +482,21 @@ void ItemCreator::set_item_unidentified_flag_if_not_challenge(ItemData& item) co
|
||||
if (item.data1[0] != 0x00) {
|
||||
return;
|
||||
}
|
||||
// On V3, all rare weapons and weapons with specials are untekked when
|
||||
// created; on V2, only rares that are not in the standard item classes are
|
||||
// untekked when created.
|
||||
if (this->is_v3()) {
|
||||
if (this->item_parameter_table->is_item_rare(item) || (item.data1[4] != 0)) {
|
||||
item.data1[4] |= 0x80;
|
||||
}
|
||||
} else {
|
||||
if (this->item_parameter_table->is_item_rare(item) ? (item.data1[1] > 0x0C) : (item.data1[4] != 0)) {
|
||||
item.data1[4] |= 0x80;
|
||||
}
|
||||
// On V1, V3, and V4, all rare weapons and weapons with specials are untekked
|
||||
// when created; on V2, only rares that are not in the standard item classes
|
||||
// are untekked when created.
|
||||
bool is_rare = this->item_parameter_table->is_item_rare(item);
|
||||
bool use_v2_logic = is_v2(this->logic_version) && (this->logic_version != Version::GC_NTE);
|
||||
if (use_v2_logic
|
||||
? (is_rare ? (item.data1[1] > 0x0C) : (item.data1[4] != 0))
|
||||
: (is_rare || (item.data1[4] != 0))) {
|
||||
item.data1[4] |= 0x80;
|
||||
}
|
||||
}
|
||||
|
||||
void ItemCreator::set_tool_item_amount_to_1(ItemData& item) const {
|
||||
if (item.data1[0] == 0x03) {
|
||||
item.set_tool_item_amount(1);
|
||||
item.set_tool_item_amount(*this->stack_limits, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,7 +676,7 @@ void ItemCreator::generate_common_tool_variances(uint32_t area_norm, ItemData& i
|
||||
item.clear();
|
||||
|
||||
uint8_t tool_class = this->get_rand_from_weighted_tables_2d_vertical(this->pt->tool_class_prob_table, area_norm);
|
||||
if (this->is_v3() && (tool_class == 0x1A)) {
|
||||
if ((!is_v1_or_v2(this->logic_version) || (this->logic_version == Version::GC_NTE)) && (tool_class == 0x1A)) {
|
||||
tool_class = 0x73;
|
||||
}
|
||||
this->log.info("Generating tool with class %02hhX", tool_class);
|
||||
@@ -695,10 +717,20 @@ uint8_t ItemCreator::generate_tech_disk_level(uint32_t tech_num, uint32_t area_n
|
||||
return range.min;
|
||||
}
|
||||
|
||||
void ItemCreator::generate_common_mag_variances(ItemData& item) const {
|
||||
void ItemCreator::generate_common_mag_variances(ItemData& item) {
|
||||
if (item.data1[0] == 0x02) {
|
||||
item.data1[1] = 0x00;
|
||||
item.assign_mag_stats(ItemMagStats());
|
||||
|
||||
// The original code (on PSO GC) assigns the mag color as 0x0E. We assign
|
||||
// a random color instead.
|
||||
if (is_pre_v1(this->logic_version)) {
|
||||
item.data2[3] = 0x00;
|
||||
} else if (is_v1_or_v2(this->logic_version)) {
|
||||
item.data2[3] = random_from_optional_crypt(this->opt_rand_crypt) % 0x0E;
|
||||
} else {
|
||||
item.data2[3] = random_from_optional_crypt(this->opt_rand_crypt) % 0x12;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -810,23 +842,31 @@ void ItemCreator::generate_unit_stars_tables() {
|
||||
|
||||
size_t star_base_index;
|
||||
uint8_t num_units;
|
||||
switch (this->version) {
|
||||
switch (this->logic_version) {
|
||||
case Version::PC_PATCH:
|
||||
case Version::BB_PATCH:
|
||||
case Version::GC_NTE:
|
||||
throw logic_error("unknown parameters for version");
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
throw logic_error("ItemCreator cannot be created for Episode 3 games");
|
||||
case Version::DC_NTE:
|
||||
star_base_index = 0x124;
|
||||
num_units = 0x43;
|
||||
break;
|
||||
case Version::DC_V1_11_2000_PROTOTYPE:
|
||||
case Version::DC_V1:
|
||||
star_base_index = 0x128;
|
||||
num_units = 0x44;
|
||||
break;
|
||||
case Version::DC_V2:
|
||||
case Version::PC_NTE:
|
||||
case Version::PC_V2:
|
||||
star_base_index = 0x1D1;
|
||||
num_units = 0x44;
|
||||
break;
|
||||
case Version::GC_NTE:
|
||||
star_base_index = 0x251;
|
||||
num_units = 0x47;
|
||||
break;
|
||||
case Version::GC_V3:
|
||||
case Version::XB_V3:
|
||||
star_base_index = 0x2AF;
|
||||
@@ -987,7 +1027,7 @@ bool ItemCreator::shop_does_not_contain_duplicate_or_too_many_similar_weapons(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ItemCreator::shop_does_not_contain_duplicate_item_by_primary_identifier(
|
||||
bool ItemCreator::shop_does_not_contain_duplicate_item_by_data1_0_1_2(
|
||||
const vector<ItemData>& shop, const ItemData& item) {
|
||||
for (const auto& shop_item : shop) {
|
||||
if ((shop_item.data1[0] == item.data1[0]) &&
|
||||
@@ -999,8 +1039,7 @@ bool ItemCreator::shop_does_not_contain_duplicate_item_by_primary_identifier(
|
||||
return true;
|
||||
}
|
||||
|
||||
void ItemCreator::generate_armor_shop_armors(
|
||||
vector<ItemData>& shop, size_t player_level) {
|
||||
void ItemCreator::generate_armor_shop_armors(vector<ItemData>& shop, size_t player_level) {
|
||||
size_t num_items;
|
||||
if (player_level < 11) {
|
||||
num_items = 4;
|
||||
@@ -1020,7 +1059,7 @@ void ItemCreator::generate_armor_shop_armors(
|
||||
pt.push(src_table.first[z].value);
|
||||
}
|
||||
}
|
||||
pt.shuffle(this->random_crypt);
|
||||
pt.shuffle(this->opt_rand_crypt);
|
||||
|
||||
for (size_t items_generated = 0; items_generated < num_items;) {
|
||||
ItemData item;
|
||||
@@ -1064,7 +1103,7 @@ void ItemCreator::generate_armor_shop_shields(vector<ItemData>& shop, size_t pla
|
||||
pt.push(src_table.first[z].value);
|
||||
}
|
||||
}
|
||||
pt.shuffle(this->random_crypt);
|
||||
pt.shuffle(this->opt_rand_crypt);
|
||||
|
||||
for (size_t items_generated = 0; items_generated < num_items;) {
|
||||
ItemData item;
|
||||
@@ -1080,7 +1119,7 @@ void ItemCreator::generate_armor_shop_shields(vector<ItemData>& shop, size_t pla
|
||||
}
|
||||
}
|
||||
|
||||
if (this->shop_does_not_contain_duplicate_item_by_primary_identifier(shop, item)) {
|
||||
if (this->shop_does_not_contain_duplicate_item_by_data1_0_1_2(shop, item)) {
|
||||
shop.emplace_back(std::move(item));
|
||||
items_generated++;
|
||||
}
|
||||
@@ -1107,14 +1146,14 @@ void ItemCreator::generate_armor_shop_units(vector<ItemData>& shop, size_t playe
|
||||
pt.push(src_table.first[z].value);
|
||||
}
|
||||
}
|
||||
pt.shuffle(this->random_crypt);
|
||||
pt.shuffle(this->opt_rand_crypt);
|
||||
|
||||
for (size_t items_generated = 0; items_generated < num_items;) {
|
||||
ItemData item;
|
||||
item.data1[0] = 1;
|
||||
item.data1[1] = 3;
|
||||
item.data1[2] = pt.pop();
|
||||
if (this->shop_does_not_contain_duplicate_item_by_primary_identifier(shop, item)) {
|
||||
if (this->shop_does_not_contain_duplicate_item_by_data1_0_1_2(shop, item)) {
|
||||
shop.emplace_back(std::move(item));
|
||||
items_generated++;
|
||||
}
|
||||
@@ -1210,7 +1249,7 @@ void ItemCreator::generate_rare_tool_shop_recovery_items(
|
||||
pt.push(e.value);
|
||||
}
|
||||
}
|
||||
pt.shuffle(this->random_crypt);
|
||||
pt.shuffle(this->opt_rand_crypt);
|
||||
|
||||
size_t effective_num_items = num_items;
|
||||
size_t items_generated = 0;
|
||||
@@ -1225,7 +1264,7 @@ void ItemCreator::generate_rare_tool_shop_recovery_items(
|
||||
item.data1[0] = 3;
|
||||
item.data1[1] = tool_item_defs[type].first;
|
||||
item.data1[2] = tool_item_defs[type].second;
|
||||
if (this->shop_does_not_contain_duplicate_item_by_primary_identifier(shop, item)) {
|
||||
if (this->shop_does_not_contain_duplicate_item_by_data1_0_1_2(shop, item)) {
|
||||
shop.emplace_back(std::move(item));
|
||||
items_generated++;
|
||||
}
|
||||
@@ -1253,7 +1292,7 @@ void ItemCreator::generate_tool_shop_tech_disks(vector<ItemData>& shop, size_t p
|
||||
pt.push(e.value);
|
||||
}
|
||||
}
|
||||
pt.shuffle(this->random_crypt);
|
||||
pt.shuffle(this->opt_rand_crypt);
|
||||
|
||||
static const array<uint8_t, 0x13> tech_num_map = {
|
||||
0x00, 0x03, 0x06, 0x0F, 0x10, 0x0D, 0x0A, 0x0B, 0x0C, 0x01, 0x04, 0x07,
|
||||
@@ -1354,7 +1393,7 @@ vector<ItemData> ItemCreator::generate_weapon_shop_contents(size_t player_level)
|
||||
pt.push(e.value);
|
||||
}
|
||||
}
|
||||
pt.shuffle(this->random_crypt);
|
||||
pt.shuffle(this->opt_rand_crypt);
|
||||
|
||||
vector<ItemData> shop;
|
||||
while (shop.size() < num_items) {
|
||||
@@ -1554,7 +1593,7 @@ void ItemCreator::generate_weapon_shop_item_special(ItemData& item, size_t playe
|
||||
|
||||
// Note: The original code shuffles pt and then pops a single value from it.
|
||||
// For simplicity, we just sample a single value (and don't pop it) instead.
|
||||
switch (pt.sample(this->random_crypt)) {
|
||||
switch (pt.sample(this->opt_rand_crypt)) {
|
||||
case 0:
|
||||
item.data1[4] = 0;
|
||||
break;
|
||||
@@ -1606,7 +1645,7 @@ void ItemCreator::generate_weapon_shop_item_bonus1(
|
||||
|
||||
// Note: The original code shuffles pt and then pops a single value from it.
|
||||
// For simplicity, we just sample a single value (and don't pop it) instead.
|
||||
item.data1[6] = pt.sample(this->random_crypt);
|
||||
item.data1[6] = pt.sample(this->opt_rand_crypt);
|
||||
if (item.data1[6] == 0) {
|
||||
item.data1[7] = 0;
|
||||
|
||||
@@ -1647,7 +1686,7 @@ void ItemCreator::generate_weapon_shop_item_bonus2(ItemData& item, size_t player
|
||||
pt.push(e.value);
|
||||
}
|
||||
}
|
||||
pt.shuffle(this->random_crypt);
|
||||
pt.shuffle(this->opt_rand_crypt);
|
||||
|
||||
do {
|
||||
item.data1[8] = pt.pop();
|
||||
@@ -1663,23 +1702,20 @@ void ItemCreator::generate_weapon_shop_item_bonus2(ItemData& item, size_t player
|
||||
}
|
||||
}
|
||||
|
||||
ItemData ItemCreator::on_specialized_box_item_drop(
|
||||
uint16_t entity_id, uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2) {
|
||||
if (this->destroyed_boxes.count(entity_id)) {
|
||||
return ItemData();
|
||||
}
|
||||
|
||||
ItemData item = this->base_item_for_specialized_box(def0, def1, def2);
|
||||
ItemCreator::DropResult ItemCreator::on_specialized_box_item_drop(
|
||||
uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2) {
|
||||
DropResult res;
|
||||
res.item = this->base_item_for_specialized_box(def0, def1, def2);
|
||||
if (def_z == 0.0f) {
|
||||
uint16_t type = item.data1w[0];
|
||||
item.clear();
|
||||
item.data1w[0] = type;
|
||||
this->generate_common_item_variances(this->normalize_area_number(area), item);
|
||||
uint16_t type = res.item.data1w[0];
|
||||
res.item.clear();
|
||||
res.item.data1w[0] = type;
|
||||
this->generate_common_item_variances(this->normalize_area_number(area), res.item);
|
||||
}
|
||||
return item;
|
||||
return res;
|
||||
}
|
||||
|
||||
ItemData ItemCreator::base_item_for_specialized_box(uint32_t def0, uint32_t def1, uint32_t def2) {
|
||||
ItemData ItemCreator::base_item_for_specialized_box(uint32_t def0, uint32_t def1, uint32_t def2) const {
|
||||
ItemData item;
|
||||
item.data1[0] = (def0 >> 0x18) & 0x0F;
|
||||
item.data1[1] = (def0 >> 0x10) + ((item.data1[0] == 0x00) || (item.data1[0] == 0x01));
|
||||
@@ -1708,7 +1744,7 @@ ItemData ItemCreator::base_item_for_specialized_box(uint32_t def0, uint32_t def1
|
||||
if (item.data1[1] == 0x02) {
|
||||
item.data1[4] = def0 & 0xFF;
|
||||
}
|
||||
item.set_tool_item_amount(1);
|
||||
item.set_tool_item_amount(*this->stack_limits, 1);
|
||||
break;
|
||||
case 0x04:
|
||||
item.data2d = ((def1 >> 0x10) & 0xFFFF) * 10;
|
||||
@@ -1736,7 +1772,7 @@ ssize_t ItemCreator::apply_tekker_deltas(ItemData& item, uint8_t section_id) {
|
||||
// Adjust the weapon's special
|
||||
{
|
||||
const auto& prob_table = this->tekker_adjustment_set->get_special_upgrade_prob_table(section_id, favored);
|
||||
uint8_t delta_index = prob_table.sample(this->random_crypt);
|
||||
uint8_t delta_index = prob_table.sample(this->opt_rand_crypt);
|
||||
int8_t delta = delta_table.at(delta_index);
|
||||
this->log.info("(Special) Delta index %hhu, delta %hhd", delta_index, delta);
|
||||
// Note: The original code checks specifically for -1 and +1 here, but the
|
||||
@@ -1753,13 +1789,15 @@ ssize_t ItemCreator::apply_tekker_deltas(ItemData& item, uint8_t section_id) {
|
||||
} else {
|
||||
new_special = item.data1[4];
|
||||
}
|
||||
if ((new_special != item.data1[4]) &&
|
||||
(this->item_parameter_table->get_special(item.data1[4]).type ==
|
||||
this->item_parameter_table->get_special(new_special).type)) {
|
||||
this->log.info("(Special) Delta canceled because it would change special category");
|
||||
item.data1[4] = new_special;
|
||||
if (new_special != item.data1[4]) {
|
||||
if (this->item_parameter_table->get_special(item.data1[4]).type ==
|
||||
this->item_parameter_table->get_special(new_special).type) {
|
||||
item.data1[4] = new_special;
|
||||
} else {
|
||||
this->log.info("(Special) Delta canceled because it would change special category");
|
||||
}
|
||||
}
|
||||
} catch (const runtime_error&) {
|
||||
} catch (const out_of_range&) {
|
||||
// Invalid special number passed to get_special; just ignore it
|
||||
}
|
||||
luck += this->tekker_adjustment_set->get_luck_for_special_upgrade(delta_index);
|
||||
@@ -1770,7 +1808,7 @@ ssize_t ItemCreator::apply_tekker_deltas(ItemData& item, uint8_t section_id) {
|
||||
if (!this->item_parameter_table->is_item_rare(item)) {
|
||||
const auto& weapon_def = this->item_parameter_table->get_weapon(item.data1[1], item.data1[2]);
|
||||
const auto& prob_table = this->tekker_adjustment_set->get_grind_delta_prob_table(section_id, favored);
|
||||
uint8_t delta_index = prob_table.sample(this->random_crypt);
|
||||
uint8_t delta_index = prob_table.sample(this->opt_rand_crypt);
|
||||
int8_t delta = delta_table.at(delta_index);
|
||||
this->log.info("(Grind) Delta index %hhu, delta %hhd", delta_index, delta);
|
||||
int16_t new_grind = static_cast<int16_t>(item.data1[3]) + static_cast<int16_t>(delta);
|
||||
@@ -1786,7 +1824,7 @@ ssize_t ItemCreator::apply_tekker_deltas(ItemData& item, uint8_t section_id) {
|
||||
const auto& prob_table = this->tekker_adjustment_set->get_bonus_delta_prob_table(section_id, favored);
|
||||
// Note: The original code really does use the same delta for all three
|
||||
// bonuses.
|
||||
uint8_t delta_index = prob_table.sample(this->random_crypt);
|
||||
uint8_t delta_index = prob_table.sample(this->opt_rand_crypt);
|
||||
int8_t delta = delta_table.at(delta_index);
|
||||
this->log.info("(Bonuses) Delta index %hhu, delta %hhd", delta_index, delta);
|
||||
// Note: The original code doesn't check if there's actually a bonus in each
|
||||
|
||||
+23
-22
@@ -19,26 +19,27 @@ public:
|
||||
std::shared_ptr<const WeaponRandomSet> weapon_random_set,
|
||||
std::shared_ptr<const TekkerAdjustmentSet> tekker_adjustment_set,
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table,
|
||||
Version version,
|
||||
std::shared_ptr<const ItemData::StackLimits> stack_limits,
|
||||
Episode episode,
|
||||
GameMode mode,
|
||||
uint8_t difficulty,
|
||||
uint8_t section_id,
|
||||
uint32_t random_seed,
|
||||
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt,
|
||||
std::shared_ptr<const BattleRules> restrictions = nullptr);
|
||||
~ItemCreator() = default;
|
||||
|
||||
void set_random_state(uint32_t seed, uint32_t absolute_offset);
|
||||
void clear_destroyed_entities();
|
||||
void set_random_crypt(std::shared_ptr<PSOLFGEncryption> new_random_crypt);
|
||||
|
||||
ItemData on_monster_item_drop(uint16_t entity_id, uint32_t enemy_type, uint8_t area);
|
||||
ItemData on_box_item_drop(uint16_t entity_id, uint8_t area);
|
||||
ItemData on_specialized_box_item_drop(uint16_t entity_id, uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2);
|
||||
struct DropResult {
|
||||
ItemData item;
|
||||
bool is_from_rare_table = false;
|
||||
};
|
||||
|
||||
void set_monster_destroyed(uint16_t entity_id);
|
||||
void set_box_destroyed(uint16_t entity_id);
|
||||
DropResult on_monster_item_drop(uint32_t enemy_type, uint8_t area);
|
||||
DropResult on_box_item_drop(uint8_t area);
|
||||
DropResult on_specialized_box_item_drop(uint8_t area, float def_z, uint32_t def0, uint32_t def1, uint32_t def2);
|
||||
|
||||
static ItemData base_item_for_specialized_box(uint32_t def0, uint32_t def1, uint32_t def2);
|
||||
ItemData base_item_for_specialized_box(uint32_t def0, uint32_t def1, uint32_t def2) const;
|
||||
|
||||
std::vector<ItemData> generate_armor_shop_contents(size_t player_level);
|
||||
std::vector<ItemData> generate_tool_shop_contents(size_t player_level);
|
||||
@@ -51,10 +52,15 @@ public:
|
||||
inline void set_restrictions(std::shared_ptr<const BattleRules> restrictions) {
|
||||
this->restrictions = restrictions;
|
||||
}
|
||||
inline uint8_t get_section_id() const {
|
||||
return this->section_id;
|
||||
}
|
||||
void set_section_id(uint8_t new_section_id);
|
||||
|
||||
private:
|
||||
PrefixedLogger log;
|
||||
Version version;
|
||||
Version logic_version;
|
||||
std::shared_ptr<const ItemData::StackLimits> stack_limits;
|
||||
Episode episode;
|
||||
GameMode mode;
|
||||
uint8_t difficulty;
|
||||
@@ -65,6 +71,7 @@ private:
|
||||
std::shared_ptr<const WeaponRandomSet> weapon_random_set;
|
||||
std::shared_ptr<const TekkerAdjustmentSet> tekker_adjustment_set;
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table;
|
||||
std::shared_ptr<const CommonItemSet> common_item_set;
|
||||
std::shared_ptr<const CommonItemSet::Table> pt;
|
||||
std::shared_ptr<const BattleRules> restrictions;
|
||||
|
||||
@@ -76,19 +83,13 @@ private:
|
||||
|
||||
// Note: The original implementation uses 17 different random states for some
|
||||
// reason. We forego that and use only one for simplicity.
|
||||
PSOV2Encryption random_crypt;
|
||||
std::unordered_set<uint16_t> destroyed_monsters;
|
||||
std::unordered_set<uint16_t> destroyed_boxes;
|
||||
|
||||
inline bool is_v3() const {
|
||||
return !is_v1_or_v2(this->version);
|
||||
}
|
||||
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt;
|
||||
|
||||
bool are_rare_drops_allowed() const;
|
||||
uint8_t normalize_area_number(uint8_t area) const;
|
||||
|
||||
ItemData on_monster_item_drop_with_area_norm(uint32_t enemy_type, uint8_t area_norm);
|
||||
ItemData on_box_item_drop_with_area_norm(uint8_t area_norm);
|
||||
DropResult on_monster_item_drop_with_area_norm(uint32_t enemy_type, uint8_t area_norm);
|
||||
DropResult on_box_item_drop_with_area_norm(uint8_t area_norm);
|
||||
|
||||
uint32_t rand_int(uint64_t max);
|
||||
float rand_float_0_1_from_crypt();
|
||||
@@ -116,7 +117,7 @@ private:
|
||||
void generate_common_armor_or_shield_type_and_variances(char area_norm, ItemData& item);
|
||||
void generate_common_tool_variances(uint32_t area_norm, ItemData& item);
|
||||
uint8_t generate_tech_disk_level(uint32_t tech_num, uint32_t area_norm);
|
||||
void generate_common_mag_variances(ItemData& item) const;
|
||||
void generate_common_mag_variances(ItemData& item);
|
||||
void generate_common_weapon_variances(uint8_t area_norm, ItemData& item);
|
||||
void generate_common_weapon_grind(ItemData& item, uint8_t offset_within_subtype_range);
|
||||
void generate_common_weapon_bonuses(ItemData& item, uint8_t area_norm);
|
||||
@@ -135,7 +136,7 @@ private:
|
||||
const std::vector<ItemData>& shop, const ItemData& item);
|
||||
static bool shop_does_not_contain_duplicate_or_too_many_similar_weapons(
|
||||
const std::vector<ItemData>& shop, const ItemData& item);
|
||||
static bool shop_does_not_contain_duplicate_item_by_primary_identifier(
|
||||
static bool shop_does_not_contain_duplicate_item_by_data1_0_1_2(
|
||||
const std::vector<ItemData>& shop, const ItemData& item);
|
||||
void generate_armor_shop_armors(
|
||||
std::vector<ItemData>& shop, size_t player_level);
|
||||
|
||||
+119
-46
@@ -8,6 +8,39 @@
|
||||
|
||||
using namespace std;
|
||||
|
||||
const vector<uint8_t> ItemData::StackLimits::DEFAULT_TOOL_LIMITS_DC_11_2000(
|
||||
{10});
|
||||
const vector<uint8_t> ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V1_V2(
|
||||
{10, 10, 1, 10, 10, 10, 10, 10, 10, 1});
|
||||
const vector<uint8_t> ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V3_V4(
|
||||
{10, 10, 1, 10, 10, 10, 10, 10, 10, 1, 1, 1, 1, 1, 1, 1, 99, 1});
|
||||
|
||||
ItemData::StackLimits::StackLimits(
|
||||
Version version, const vector<uint8_t>& max_tool_stack_sizes_by_data1_1, uint32_t max_meseta_stack_size)
|
||||
: version(version),
|
||||
max_tool_stack_sizes_by_data1_1(max_tool_stack_sizes_by_data1_1),
|
||||
max_meseta_stack_size(max_meseta_stack_size) {}
|
||||
|
||||
ItemData::StackLimits::StackLimits(Version version, const JSON& json)
|
||||
: version(version) {
|
||||
this->max_tool_stack_sizes_by_data1_1.clear();
|
||||
for (const auto& limit_json : json.at("ToolLimits").as_list()) {
|
||||
this->max_tool_stack_sizes_by_data1_1.emplace_back(limit_json->as_int());
|
||||
}
|
||||
this->max_meseta_stack_size = json.at("MesetaLimit").as_int();
|
||||
}
|
||||
|
||||
uint8_t ItemData::StackLimits::get(uint8_t data1_0, uint8_t data1_1) const {
|
||||
if (data1_0 == 4) {
|
||||
return this->max_meseta_stack_size;
|
||||
}
|
||||
if (data1_0 == 3) {
|
||||
const auto& vec = this->max_tool_stack_sizes_by_data1_1;
|
||||
return vec.at(min<size_t>(data1_1, vec.size() - 1));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
ItemData::ItemData() {
|
||||
this->clear();
|
||||
}
|
||||
@@ -71,20 +104,21 @@ uint32_t ItemData::primary_identifier() const {
|
||||
// The game treats any item starting with 04 as Meseta, and ignores the rest
|
||||
// of data1 (the value is in data2)
|
||||
if (this->data1[0] == 0x04) {
|
||||
return 0x040000;
|
||||
return 0x04000000;
|
||||
}
|
||||
if (this->data1[0] == 0x03 && this->data1[1] == 0x02) {
|
||||
return 0x030200; // Tech disk (data1[2] is level, so omit it)
|
||||
// Tech disk (tech ID is data1[4], not [2])
|
||||
return 0x03020000 | (this->data1[4] << 8) | this->data1[2];
|
||||
} else if (this->data1[0] == 0x02) {
|
||||
return 0x020000 | (this->data1[1] << 8); // Mag
|
||||
return 0x02000000 | (this->data1[1] << 16); // Mag
|
||||
} else if (this->is_s_rank_weapon()) {
|
||||
return (this->data1[0] << 16) | (this->data1[1] << 8);
|
||||
return (this->data1[0] << 24) | (this->data1[1] << 16);
|
||||
} else {
|
||||
return (this->data1[0] << 16) | (this->data1[1] << 8) | this->data1[2];
|
||||
return (this->data1[0] << 24) | (this->data1[1] << 16) | (this->data1[2] << 8);
|
||||
}
|
||||
}
|
||||
|
||||
bool ItemData::is_wrapped() const {
|
||||
bool ItemData::is_wrapped(const StackLimits& limits) const {
|
||||
switch (this->data1[0]) {
|
||||
case 0:
|
||||
case 1:
|
||||
@@ -92,7 +126,7 @@ bool ItemData::is_wrapped() const {
|
||||
case 2:
|
||||
return this->data2[2] & 0x40;
|
||||
case 3:
|
||||
return !this->is_stackable() && (this->data1[3] & 0x40);
|
||||
return !this->is_stackable(limits) && (this->data1[3] & 0x40);
|
||||
case 4:
|
||||
return false;
|
||||
default:
|
||||
@@ -100,7 +134,7 @@ bool ItemData::is_wrapped() const {
|
||||
}
|
||||
}
|
||||
|
||||
void ItemData::wrap() {
|
||||
void ItemData::wrap(const StackLimits& limits) {
|
||||
switch (this->data1[0]) {
|
||||
case 0:
|
||||
case 1:
|
||||
@@ -110,7 +144,7 @@ void ItemData::wrap() {
|
||||
this->data2[2] |= 0x40;
|
||||
break;
|
||||
case 3:
|
||||
if (!this->is_stackable()) {
|
||||
if (!this->is_stackable(limits)) {
|
||||
this->data1[3] |= 0x40;
|
||||
}
|
||||
break;
|
||||
@@ -121,7 +155,7 @@ void ItemData::wrap() {
|
||||
}
|
||||
}
|
||||
|
||||
void ItemData::unwrap() {
|
||||
void ItemData::unwrap(const StackLimits& limits) {
|
||||
switch (this->data1[0]) {
|
||||
case 0:
|
||||
case 1:
|
||||
@@ -131,7 +165,7 @@ void ItemData::unwrap() {
|
||||
this->data2[2] &= 0xBF;
|
||||
break;
|
||||
case 3:
|
||||
if (!this->is_stackable()) {
|
||||
if (!this->is_stackable(limits)) {
|
||||
this->data1[3] &= 0xBF;
|
||||
}
|
||||
break;
|
||||
@@ -142,32 +176,31 @@ void ItemData::unwrap() {
|
||||
}
|
||||
}
|
||||
|
||||
bool ItemData::is_stackable() const {
|
||||
return this->max_stack_size() > 1;
|
||||
bool ItemData::is_stackable(const StackLimits& limits) const {
|
||||
return this->max_stack_size(limits) > 1;
|
||||
}
|
||||
|
||||
size_t ItemData::stack_size() const {
|
||||
if (max_stack_size_for_item(this->data1[0], this->data1[1]) > 1) {
|
||||
size_t ItemData::stack_size(const StackLimits& limits) const {
|
||||
if (this->max_stack_size(limits) > 1) {
|
||||
return this->data1[5];
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t ItemData::max_stack_size() const {
|
||||
return max_stack_size_for_item(this->data1[0], this->data1[1]);
|
||||
size_t ItemData::max_stack_size(const StackLimits& limits) const {
|
||||
return limits.get(this->data1[0], this->data1[1]);
|
||||
}
|
||||
|
||||
void ItemData::enforce_min_stack_size() {
|
||||
if (this->stack_size() == 0) {
|
||||
void ItemData::enforce_min_stack_size(const StackLimits& limits) {
|
||||
if (this->stack_size(limits) == 0) {
|
||||
this->data1[5] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
bool ItemData::is_common_consumable(uint32_t primary_identifier) {
|
||||
if (primary_identifier == 0x030200) {
|
||||
return false;
|
||||
}
|
||||
return (primary_identifier >= 0x030000) && (primary_identifier < 0x030A00);
|
||||
return (primary_identifier >= 0x03000000) &&
|
||||
(primary_identifier < 0x030A0000) &&
|
||||
((primary_identifier & 0xFFFF0000) != 0x03020000);
|
||||
}
|
||||
|
||||
bool ItemData::is_common_consumable() const {
|
||||
@@ -298,15 +331,15 @@ void ItemData::add_mag_photon_blast(uint8_t pb_num) {
|
||||
}
|
||||
if (pb_num >= 4) {
|
||||
throw runtime_error("left photon blast number is too high");
|
||||
pb_nums |= (pb_num << 6);
|
||||
}
|
||||
pb_nums |= (pb_num << 6);
|
||||
flags |= 4;
|
||||
}
|
||||
}
|
||||
|
||||
void ItemData::decode_for_version(Version from_version) {
|
||||
uint8_t encoded_v2_data = this->get_encoded_v2_data();
|
||||
bool should_decode_v2_data = (is_v1(from_version) || is_v2(from_version)) &&
|
||||
bool should_decode_v2_data = (is_v1(from_version) || is_v2(from_version)) && (from_version != Version::GC_NTE) &&
|
||||
(encoded_v2_data != 0x00) && this->has_encoded_v2_data();
|
||||
|
||||
switch (this->data1[0]) {
|
||||
@@ -330,13 +363,9 @@ void ItemData::decode_for_version(Version from_version) {
|
||||
this->data1[1] = encoded_v2_data + 0x2B;
|
||||
}
|
||||
|
||||
if (is_big_endian(from_version)) {
|
||||
// PSO GC erroneously byteswaps the data2d field, even though it's actually
|
||||
// just four individual bytes, so we correct for that here.
|
||||
this->data2d = bswap32(this->data2d);
|
||||
|
||||
} else if (is_v1(from_version) || is_v2(from_version)) {
|
||||
// PSO PC encodes mags in a tediously annoying manner. The first four bytes are the same, but then...
|
||||
if (is_v1(from_version) || is_v2(from_version)) {
|
||||
// PSO PC and GC NTE encode mags in a tediously annoying manner. The
|
||||
// first four bytes are the same, but then...
|
||||
// V2: pHHHHHHHHHHHHHHc pIIIIIIIIIIIIIIc JJJJJJJJJJJJJJJc KKKKKKKKKKKKKKKc QQQQQQQQ QQQQQQQQ YYYYYYYY pYYYYYYY
|
||||
// V3: HHHHHHHHHHHHHHHH IIIIIIIIIIIIIIII JJJJJJJJJJJJJJJJ KKKKKKKKKKKKKKKK YYYYYYYY QQQQQQQQ PPPPPPPP CCCCCCCC
|
||||
// c = color in V2 (4 bits; low bit first)
|
||||
@@ -352,10 +381,18 @@ void ItemData::decode_for_version(Version from_version) {
|
||||
this->data2[0] = this->data2w[1] & 0x7FFF; // Synchro
|
||||
this->data2[2] = ((this->data2[3] >> 7) & 1) | ((this->data1w[2] >> 14) & 2) | ((this->data1w[3] >> 13) & 4); // PB flags
|
||||
this->data2[3] = (this->data1w[2] & 1) | ((this->data1w[3] & 1) << 1) | ((this->data1w[4] & 1) << 2) | ((this->data1w[5] & 1) << 3); // Color
|
||||
// 01000080
|
||||
this->data1w[2] &= 0x7FFE;
|
||||
this->data1w[3] &= 0x7FFE;
|
||||
this->data1w[4] &= 0xFFFE;
|
||||
this->data1w[5] &= 0xFFFE;
|
||||
|
||||
} else if (is_big_endian(from_version)) {
|
||||
// PSO GC (but not GC NTE, which uses the above logic) byteswaps the
|
||||
// data2d field, since internally it's actually a uint32_t. We treat it
|
||||
// as individual bytes instead, so we correct for the client's
|
||||
// byteswapping here.
|
||||
this->data2d = bswap32(this->data2d);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -386,7 +423,7 @@ void ItemData::decode_for_version(Version from_version) {
|
||||
}
|
||||
|
||||
void ItemData::encode_for_version(Version to_version, shared_ptr<const ItemParameterTable> item_parameter_table) {
|
||||
bool should_encode_v2_data = (is_v1(to_version) || is_v2(to_version)) && !this->has_encoded_v2_data();
|
||||
bool should_encode_v2_data = (is_v1(to_version) || is_v2(to_version)) && (to_version != Version::GC_NTE) && !this->has_encoded_v2_data();
|
||||
|
||||
switch (this->data1[0]) {
|
||||
case 0x00:
|
||||
@@ -425,9 +462,7 @@ void ItemData::encode_for_version(Version to_version, shared_ptr<const ItemParam
|
||||
// This logic is the inverse of the corresponding logic in
|
||||
// decode_for_version; see that function for a description of what's
|
||||
// going on here.
|
||||
if (is_big_endian(to_version)) {
|
||||
this->data2d = bswap32(this->data2d);
|
||||
} else if (is_v1(to_version) || is_v2(to_version)) {
|
||||
if (is_v1(to_version) || is_v2(to_version)) {
|
||||
this->data1w[2] = (this->data1w[2] & 0x7FFE) | ((this->data2[2] << 14) & 0x8000) | (this->data2[3] & 1);
|
||||
this->data1w[3] = (this->data1w[3] & 0x7FFE) | ((this->data2[2] << 13) & 0x8000) | ((this->data2[3] >> 1) & 1);
|
||||
this->data1w[4] = (this->data1w[4] & 0xFFFE) | ((this->data2[3] >> 2) & 1);
|
||||
@@ -435,6 +470,8 @@ void ItemData::encode_for_version(Version to_version, shared_ptr<const ItemParam
|
||||
// Order is important; data2w[0] must not be written before data2[0] is read
|
||||
this->data2w[1] = this->data2[0] | ((this->data2[2] << 15) & 0x8000);
|
||||
this->data2w[0] = this->data2[1];
|
||||
} else if (is_big_endian(to_version)) {
|
||||
this->data2d = bswap32(this->data2d);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -502,12 +539,12 @@ void ItemData::set_sealed_item_kill_count(uint16_t v) {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t ItemData::get_tool_item_amount() const {
|
||||
return this->is_stackable() ? this->data1[5] : 1;
|
||||
uint8_t ItemData::get_tool_item_amount(const StackLimits& limits) const {
|
||||
return this->is_stackable(limits) ? this->data1[5] : 1;
|
||||
}
|
||||
|
||||
void ItemData::set_tool_item_amount(uint8_t amount) {
|
||||
if (this->is_stackable()) {
|
||||
void ItemData::set_tool_item_amount(const StackLimits& limits, uint8_t amount) {
|
||||
if (this->is_stackable(limits)) {
|
||||
this->data1[5] = amount;
|
||||
} else if (this->data1[0] == 0x03) {
|
||||
this->data1[5] = 0x00;
|
||||
@@ -628,6 +665,10 @@ bool ItemData::can_be_equipped_in_slot(EquipSlot slot) const {
|
||||
}
|
||||
}
|
||||
|
||||
bool ItemData::can_be_encoded_in_rel_rare_table() const {
|
||||
return !(this->data1[3] || this->data1d[1] || this->data1d[2] || this->data2d);
|
||||
}
|
||||
|
||||
bool ItemData::compare_for_sort(const ItemData& a, const ItemData& b) {
|
||||
for (size_t z = 0; z < 12; z++) {
|
||||
if (a.data1[z] < b.data1[z]) {
|
||||
@@ -647,6 +688,9 @@ bool ItemData::compare_for_sort(const ItemData& a, const ItemData& b) {
|
||||
}
|
||||
|
||||
ItemData ItemData::from_data(const string& data) {
|
||||
if (data.size() < 2) {
|
||||
throw runtime_error("data is too short");
|
||||
}
|
||||
if (data.size() > 0x10) {
|
||||
throw runtime_error("data is too long");
|
||||
}
|
||||
@@ -658,14 +702,43 @@ ItemData ItemData::from_data(const string& data) {
|
||||
for (size_t z = 12; z < min<size_t>(data.size(), 16); z++) {
|
||||
ret.data2[z - 12] = data[z];
|
||||
}
|
||||
if (ret.data1[0] > 4) {
|
||||
throw runtime_error("invalid item class");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
ItemData ItemData::from_primary_identifier(const StackLimits& limits, uint32_t primary_identifier) {
|
||||
ItemData ret;
|
||||
if (primary_identifier > 0x04000000) {
|
||||
throw runtime_error("invalid item class");
|
||||
}
|
||||
ret.data1[0] = (primary_identifier >> 24) & 0xFF;
|
||||
ret.data1[1] = (primary_identifier >> 16) & 0xFF;
|
||||
if ((primary_identifier & 0xFFFF0000) == 0x03020000) {
|
||||
ret.data1[4] = (primary_identifier >> 8) & 0xFF;
|
||||
ret.data1[2] = primary_identifier & 0xFF;
|
||||
} else {
|
||||
ret.data1[2] = (primary_identifier >> 8) & 0xFF;
|
||||
}
|
||||
ret.set_tool_item_amount(limits, 1);
|
||||
return ret;
|
||||
}
|
||||
|
||||
string ItemData::hex() const {
|
||||
return string_printf("%02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX (%08" PRIX32 ") %02hhX%02hhX%02hhX%02hhX",
|
||||
this->data1[0], this->data1[1], this->data1[2], this->data1[3],
|
||||
this->data1[4], this->data1[5], this->data1[6], this->data1[7],
|
||||
this->data1[8], this->data1[9], this->data1[10], this->data1[11],
|
||||
this->id.load(),
|
||||
this->data2[0], this->data2[1], this->data2[2], this->data2[3]);
|
||||
return string_printf("%08" PRIX32 " %08" PRIX32 " %08" PRIX32 " (%08" PRIX32 ") %08" PRIX32,
|
||||
this->data1db[0].load(), this->data1db[1].load(), this->data1db[2].load(), this->id.load(), this->data2db.load());
|
||||
}
|
||||
|
||||
string ItemData::short_hex() const {
|
||||
auto ret = string_printf("%08" PRIX32 "%08" PRIX32 "%08" PRIX32 "%08" PRIX32,
|
||||
this->data1db[0].load(), this->data1db[1].load(), this->data1db[2].load(), this->data2db.load());
|
||||
size_t offset = ret.find_last_not_of('0');
|
||||
if (offset != string::npos) {
|
||||
offset += (offset & 1) ? 1 : 2;
|
||||
if (offset < ret.size()) {
|
||||
ret.resize(offset);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
+43
-32
@@ -1,13 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/JSON.hh>
|
||||
#include <string>
|
||||
|
||||
#include "Text.hh"
|
||||
#include "Version.hh"
|
||||
|
||||
constexpr uint32_t MESETA_IDENTIFIER = 0x040000;
|
||||
|
||||
class ItemParameterTable;
|
||||
|
||||
enum class EquipSlot {
|
||||
@@ -29,26 +28,15 @@ enum class EquipSlot {
|
||||
};
|
||||
|
||||
struct ItemMagStats {
|
||||
uint16_t iq;
|
||||
uint16_t synchro;
|
||||
uint16_t def;
|
||||
uint16_t pow;
|
||||
uint16_t dex;
|
||||
uint16_t mind;
|
||||
uint8_t flags;
|
||||
uint8_t photon_blasts;
|
||||
uint8_t color;
|
||||
|
||||
ItemMagStats()
|
||||
: iq(0),
|
||||
synchro(40),
|
||||
def(500),
|
||||
pow(0),
|
||||
dex(0),
|
||||
mind(0),
|
||||
flags(0),
|
||||
photon_blasts(0),
|
||||
color(14) {}
|
||||
uint16_t iq = 0;
|
||||
uint16_t synchro = 40;
|
||||
uint16_t def = 500;
|
||||
uint16_t pow = 0;
|
||||
uint16_t dex = 0;
|
||||
uint16_t mind = 0;
|
||||
uint8_t flags = 0;
|
||||
uint8_t photon_blasts = 0;
|
||||
uint8_t color = 14;
|
||||
|
||||
inline uint16_t def_level() const {
|
||||
return this->def / 100;
|
||||
@@ -67,7 +55,26 @@ struct ItemMagStats {
|
||||
}
|
||||
};
|
||||
|
||||
struct ItemData { // 0x14 bytes
|
||||
struct ItemData {
|
||||
struct StackLimits {
|
||||
Version version;
|
||||
std::vector<uint8_t> max_tool_stack_sizes_by_data1_1;
|
||||
uint32_t max_meseta_stack_size;
|
||||
|
||||
StackLimits(Version version, const std::vector<uint8_t>& max_tool_stack_sizes_by_data1_1, uint32_t max_meseta_stack_size);
|
||||
StackLimits(Version version, const JSON& json);
|
||||
StackLimits(const StackLimits& other) = default;
|
||||
StackLimits(StackLimits&& other) = default;
|
||||
StackLimits& operator=(const StackLimits& other) = default;
|
||||
StackLimits& operator=(StackLimits&& other) = default;
|
||||
|
||||
uint8_t get(uint8_t data1_0, uint8_t data1_1) const;
|
||||
|
||||
static const std::vector<uint8_t> DEFAULT_TOOL_LIMITS_DC_11_2000;
|
||||
static const std::vector<uint8_t> DEFAULT_TOOL_LIMITS_V1_V2;
|
||||
static const std::vector<uint8_t> DEFAULT_TOOL_LIMITS_V3_V4;
|
||||
};
|
||||
|
||||
// QUICK ITEM FORMAT REFERENCE
|
||||
// data1/0 data1/4 data1/8 data2
|
||||
// Weapon: 00ZZZZGG SS00AABB AABBAABB 00000000
|
||||
@@ -137,17 +144,19 @@ struct ItemData { // 0x14 bytes
|
||||
void clear();
|
||||
|
||||
static ItemData from_data(const std::string& data);
|
||||
static ItemData from_primary_identifier(const StackLimits& limits, uint32_t primary_identifier);
|
||||
std::string hex() const;
|
||||
std::string short_hex() const;
|
||||
uint32_t primary_identifier() const;
|
||||
|
||||
bool is_wrapped() const;
|
||||
void wrap();
|
||||
void unwrap();
|
||||
bool is_wrapped(const StackLimits& limits) const;
|
||||
void wrap(const StackLimits& limits);
|
||||
void unwrap(const StackLimits& limits);
|
||||
|
||||
bool is_stackable() const;
|
||||
size_t stack_size() const;
|
||||
size_t max_stack_size() const;
|
||||
void enforce_min_stack_size();
|
||||
bool is_stackable(const StackLimits& limits) const;
|
||||
size_t stack_size(const StackLimits& limits) const;
|
||||
size_t max_stack_size(const StackLimits& limits) const;
|
||||
void enforce_min_stack_size(const StackLimits& limits);
|
||||
|
||||
static bool is_common_consumable(uint32_t primary_identifier);
|
||||
bool is_common_consumable() const;
|
||||
@@ -166,8 +175,8 @@ struct ItemData { // 0x14 bytes
|
||||
|
||||
uint16_t get_sealed_item_kill_count() const;
|
||||
void set_sealed_item_kill_count(uint16_t v);
|
||||
uint8_t get_tool_item_amount() const;
|
||||
void set_tool_item_amount(uint8_t amount);
|
||||
uint8_t get_tool_item_amount(const StackLimits& limits) const;
|
||||
void set_tool_item_amount(const StackLimits& limits, uint8_t amount);
|
||||
int16_t get_armor_or_shield_defense_bonus() const;
|
||||
void set_armor_or_shield_defense_bonus(int16_t bonus);
|
||||
int16_t get_common_armor_evasion_bonus() const;
|
||||
@@ -181,6 +190,8 @@ struct ItemData { // 0x14 bytes
|
||||
EquipSlot default_equip_slot() const;
|
||||
bool can_be_equipped_in_slot(EquipSlot slot) const;
|
||||
|
||||
bool can_be_encoded_in_rel_rare_table() const;
|
||||
|
||||
bool empty() const;
|
||||
|
||||
static bool compare_for_sort(const ItemData& a, const ItemData& b);
|
||||
|
||||
+376
-105
@@ -4,36 +4,28 @@
|
||||
|
||||
using namespace std;
|
||||
|
||||
ItemNameIndex::ItemNameIndex(JSON&& v2_names, JSON&& v3_names, JSON&& v4_names) {
|
||||
auto get_or_create_meta = [&](uint32_t primary_identifier) {
|
||||
shared_ptr<ItemMetadata> meta;
|
||||
ItemNameIndex::ItemNameIndex(
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table,
|
||||
std::shared_ptr<const ItemData::StackLimits> limits,
|
||||
const std::vector<std::string>& name_coll)
|
||||
: item_parameter_table(item_parameter_table),
|
||||
limits(limits) {
|
||||
|
||||
for (uint32_t primary_identifier : item_parameter_table->compute_all_valid_primary_identifiers()) {
|
||||
const string* name = nullptr;
|
||||
try {
|
||||
return this->primary_identifier_index.at(primary_identifier);
|
||||
ItemData item = ItemData::from_primary_identifier(*this->limits, primary_identifier);
|
||||
name = &name_coll.at(item_parameter_table->get_item_id(item));
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
|
||||
if (name) {
|
||||
auto meta = make_shared<ItemMetadata>();
|
||||
meta->primary_identifier = primary_identifier;
|
||||
this->primary_identifier_index.emplace(primary_identifier, meta);
|
||||
return meta;
|
||||
meta->name = *name;
|
||||
this->primary_identifier_index.emplace(meta->primary_identifier, meta);
|
||||
this->name_index.emplace(tolower(meta->name), meta);
|
||||
}
|
||||
};
|
||||
|
||||
for (const auto& it : v2_names.as_dict()) {
|
||||
uint32_t primary_identifier = stoul(it.first, nullptr, 16);
|
||||
auto meta = get_or_create_meta(primary_identifier);
|
||||
meta->v2_name = std::move(it.second->as_string());
|
||||
this->v2_name_index.emplace(tolower(meta->v2_name), meta);
|
||||
}
|
||||
for (const auto& it : v3_names.as_dict()) {
|
||||
uint32_t primary_identifier = stoul(it.first, nullptr, 16);
|
||||
auto meta = get_or_create_meta(primary_identifier);
|
||||
meta->v3_name = std::move(it.second->as_string());
|
||||
this->v3_name_index.emplace(tolower(meta->v3_name), meta);
|
||||
}
|
||||
for (const auto& it : v4_names.as_dict()) {
|
||||
uint32_t primary_identifier = stoul(it.first, nullptr, 16);
|
||||
auto meta = get_or_create_meta(primary_identifier);
|
||||
meta->v4_name = std::move(it.second->as_string());
|
||||
this->v4_name_index.emplace(tolower(meta->v4_name), meta);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,19 +97,17 @@ const array<const char*, 0x11> name_for_s_rank_special = {
|
||||
"King\'s",
|
||||
};
|
||||
|
||||
std::string ItemNameIndex::describe_item(
|
||||
Version version,
|
||||
const ItemData& item,
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table) const {
|
||||
std::string ItemNameIndex::describe_item(const ItemData& item, bool include_color_escapes) const {
|
||||
if (item.data1[0] == 0x04) {
|
||||
return string_printf("%s%" PRIu32 " Meseta", item_parameter_table ? "$C7" : "", item.data2d.load());
|
||||
return string_printf("%s%" PRIu32 " Meseta", include_color_escapes ? "$C7" : "", item.data2d.load());
|
||||
}
|
||||
|
||||
vector<string> ret_tokens;
|
||||
|
||||
// For weapons, specials appear before the weapon name
|
||||
bool is_unidentified = false;
|
||||
if ((item.data1[0] == 0x00) && (item.data1[4] != 0x00) && !item.is_s_rank_weapon()) {
|
||||
bool is_unidentified = item.data1[4] & 0x80;
|
||||
is_unidentified = item.data1[4] & 0x80;
|
||||
bool is_present = item.data1[4] & 0x40;
|
||||
uint8_t special_id = item.data1[4] & 0x3F;
|
||||
if (is_present) {
|
||||
@@ -147,21 +137,19 @@ std::string ItemNameIndex::describe_item(
|
||||
// flags in a different location.
|
||||
if (((item.data1[1] == 0x01) && (item.data1[4] & 0x40)) ||
|
||||
((item.data1[0] == 0x02) && (item.data2[2] & 0x40)) ||
|
||||
((item.data1[0] == 0x03) && !item.is_stackable() && (item.data1[3] & 0x40))) {
|
||||
((item.data1[0] == 0x03) && !item.is_stackable(*this->limits) && (item.data1[3] & 0x40))) {
|
||||
ret_tokens.emplace_back("Wrapped");
|
||||
}
|
||||
|
||||
// Add the item name. Technique disks are special because the level is part of
|
||||
// the primary identifier, so we manually generate the name instead of looking
|
||||
// it up.
|
||||
// Add the item name
|
||||
uint32_t primary_identifier = item.primary_identifier();
|
||||
if ((primary_identifier & 0xFFFFFF00) == 0x00030200) {
|
||||
if ((primary_identifier & 0xFFFF0000) == 0x03020000) {
|
||||
string technique_name;
|
||||
try {
|
||||
technique_name = tech_id_to_name.at(item.data1[4]);
|
||||
technique_name[0] = toupper(technique_name[0]);
|
||||
} catch (const out_of_range&) {
|
||||
technique_name = string_printf("!TECH:%02hhX", item.data1[4]);
|
||||
technique_name = string_printf("!TD:%02hhX", item.data1[4]);
|
||||
}
|
||||
// Hide the level for Reverser and Ryuker, unless the level isn't 1
|
||||
if ((item.data1[2] == 0) && ((item.data1[4] == 0x0E) || (item.data1[4] == 0x11))) {
|
||||
@@ -172,21 +160,9 @@ std::string ItemNameIndex::describe_item(
|
||||
} else {
|
||||
try {
|
||||
auto meta = this->primary_identifier_index.at(primary_identifier);
|
||||
const string* name;
|
||||
if (is_v4(version)) {
|
||||
name = &meta->v4_name;
|
||||
} else if (is_v3(version)) {
|
||||
name = &meta->v3_name;
|
||||
} else {
|
||||
name = &meta->v2_name;
|
||||
}
|
||||
if (name->empty()) {
|
||||
throw out_of_range("item does not exist");
|
||||
}
|
||||
ret_tokens.emplace_back(*name);
|
||||
|
||||
ret_tokens.emplace_back(meta->name);
|
||||
} catch (const out_of_range&) {
|
||||
ret_tokens.emplace_back(string_printf("!ID:%06" PRIX32, primary_identifier));
|
||||
ret_tokens.emplace_back(string_printf("!ID:%08" PRIX32, primary_identifier));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,16 +223,16 @@ std::string ItemNameIndex::describe_item(
|
||||
// For armors, add the slots, unit modifiers, and/or DEF/EVP bonuses
|
||||
} else if (item.data1[0] == 0x01) {
|
||||
if (item.data1[1] == 0x03) { // Units
|
||||
uint16_t modifier = item.data1w[3];
|
||||
if (modifier == 0x0001 || modifier == 0x0002) {
|
||||
int16_t modifier = item.data1w[3];
|
||||
if (modifier == 1 || modifier == 2) {
|
||||
ret_tokens.back().append("+");
|
||||
} else if (modifier == 0x0003 || modifier == 0x0004) {
|
||||
} else if (modifier >= 3) {
|
||||
ret_tokens.back().append("++");
|
||||
} else if (modifier == 0xFFFF || modifier == 0xFFFE) {
|
||||
} else if (modifier == -1 || modifier == -2) {
|
||||
ret_tokens.back().append("-");
|
||||
} else if (modifier == 0xFFFD || modifier == 0xFFFC) {
|
||||
} else if (modifier <= -3) {
|
||||
ret_tokens.back().append("--");
|
||||
} else if (modifier != 0x0000) {
|
||||
} else if (modifier != 0) {
|
||||
ret_tokens.emplace_back(string_printf("!MD:%04hX", modifier));
|
||||
}
|
||||
|
||||
@@ -341,16 +317,18 @@ std::string ItemNameIndex::describe_item(
|
||||
|
||||
// For tools, add the amount (if applicable)
|
||||
} else if (item.data1[0] == 0x03) {
|
||||
if (item.max_stack_size() > 1) {
|
||||
if (item.max_stack_size(*this->limits) > 1) {
|
||||
ret_tokens.emplace_back(string_printf("x%hhu", item.data1[5]));
|
||||
}
|
||||
}
|
||||
|
||||
string ret = join(ret_tokens, " ");
|
||||
if (item_parameter_table) {
|
||||
if (item.is_s_rank_weapon()) {
|
||||
if (include_color_escapes) {
|
||||
if (is_unidentified) {
|
||||
return "$C3" + ret;
|
||||
} else if (item.is_s_rank_weapon()) {
|
||||
return "$C4" + ret;
|
||||
} else if (item_parameter_table->is_item_rare(item)) {
|
||||
} else if (this->item_parameter_table->is_item_rare(item)) {
|
||||
return "$C6" + ret;
|
||||
} else if (item.has_bonuses()) {
|
||||
return "$C2" + ret;
|
||||
@@ -362,48 +340,32 @@ std::string ItemNameIndex::describe_item(
|
||||
}
|
||||
}
|
||||
|
||||
ItemData ItemNameIndex::parse_item_description(Version version, const std::string& desc) const {
|
||||
ItemData ItemNameIndex::parse_item_description(const std::string& desc) const {
|
||||
ItemData ret;
|
||||
try {
|
||||
ret = this->parse_item_description_phase(version, desc, false);
|
||||
ret = this->parse_item_description_phase(desc, false);
|
||||
} catch (const exception& e1) {
|
||||
try {
|
||||
ret = this->parse_item_description_phase(version, desc, true);
|
||||
ret = this->parse_item_description_phase(desc, true);
|
||||
} catch (const exception& e2) {
|
||||
try {
|
||||
string data = parse_data_string(desc);
|
||||
if (data.size() < 2) {
|
||||
throw runtime_error("item code too short");
|
||||
}
|
||||
if (data[0] > 4) {
|
||||
throw runtime_error("invalid item class");
|
||||
}
|
||||
if (data.size() > 16) {
|
||||
throw runtime_error("item code too long");
|
||||
}
|
||||
|
||||
if (data.size() <= 12) {
|
||||
memcpy(ret.data1.data(), data.data(), data.size());
|
||||
} else {
|
||||
memcpy(ret.data1.data(), data.data(), 12);
|
||||
memcpy(ret.data2.data(), data.data() + 12, data.size() - 12);
|
||||
}
|
||||
ret = ItemData::from_data(parse_data_string(desc));
|
||||
} catch (const exception& ed) {
|
||||
if (strcmp(e1.what(), e2.what())) {
|
||||
throw runtime_error(string_printf("cannot parse item description \"%s\" in %s (as text 1: %s) (as text 2: %s) (as data: %s)",
|
||||
desc.c_str(), name_for_enum(version), e1.what(), e2.what(), ed.what()));
|
||||
throw runtime_error(string_printf("cannot parse item description \"%s\" (as text 1: %s) (as text 2: %s) (as data: %s)",
|
||||
desc.c_str(), e1.what(), e2.what(), ed.what()));
|
||||
} else {
|
||||
throw runtime_error(string_printf("cannot parse item description \"%s\" in %s (as text: %s) (as data: %s)",
|
||||
desc.c_str(), name_for_enum(version), e1.what(), ed.what()));
|
||||
throw runtime_error(string_printf("cannot parse item description \"%s\" (as text: %s) (as data: %s)",
|
||||
desc.c_str(), e1.what(), ed.what()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ret.enforce_min_stack_size();
|
||||
ret.enforce_min_stack_size(*this->limits);
|
||||
return ret;
|
||||
}
|
||||
|
||||
ItemData ItemNameIndex::parse_item_description_phase(Version version, const std::string& description, bool skip_special) const {
|
||||
ItemData ItemNameIndex::parse_item_description_phase(const std::string& description, bool skip_special) const {
|
||||
ItemData ret;
|
||||
ret.data1d.clear(0);
|
||||
ret.id = 0xFFFFFFFF;
|
||||
@@ -446,6 +408,16 @@ ItemData ItemNameIndex::parse_item_description_phase(Version version, const std:
|
||||
if (is_wrapped) {
|
||||
desc = desc.substr(8);
|
||||
}
|
||||
bool is_unidentified = starts_with(desc, "?");
|
||||
if (is_unidentified) {
|
||||
size_t z;
|
||||
for (z = 1; z < desc.size(); z++) {
|
||||
if (desc[z] != ' ' && desc[z] != '?') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
desc = desc.substr(z);
|
||||
}
|
||||
|
||||
// TODO: It'd be nice to be able to parse S-rank weapon specials here too.
|
||||
uint8_t weapon_special = 0;
|
||||
@@ -464,24 +436,15 @@ ItemData ItemNameIndex::parse_item_description_phase(Version version, const std:
|
||||
}
|
||||
}
|
||||
|
||||
const map<string, shared_ptr<ItemMetadata>>* name_index;
|
||||
if (is_v4(version)) {
|
||||
name_index = &this->v4_name_index;
|
||||
} else if (is_v3(version)) {
|
||||
name_index = &this->v3_name_index;
|
||||
} else {
|
||||
name_index = &this->v2_name_index;
|
||||
}
|
||||
|
||||
auto name_it = name_index->lower_bound(desc);
|
||||
auto name_it = this->name_index.lower_bound(desc);
|
||||
// Look up to 3 places before the lower bound. We have to do this to catch
|
||||
// cases like Sange vs. Sange & Yasha - if the input is like "Sange 0/...",
|
||||
// then we'll see Sange & Yasha first, which we should skip.
|
||||
size_t lookback = 0;
|
||||
while (lookback < 4) {
|
||||
if (name_it != name_index->end() && desc.starts_with(name_it->first)) {
|
||||
if (name_it != this->name_index.end() && desc.starts_with(name_it->first)) {
|
||||
break;
|
||||
} else if (name_it == name_index->begin()) {
|
||||
} else if (name_it == this->name_index.begin()) {
|
||||
throw runtime_error("no such item");
|
||||
} else {
|
||||
name_it--;
|
||||
@@ -497,14 +460,16 @@ ItemData ItemNameIndex::parse_item_description_phase(Version version, const std:
|
||||
desc = desc.substr(1);
|
||||
}
|
||||
|
||||
// Tech disks should have already been handled above, so we don't need to
|
||||
// special-case 0302xxxx identifiers here.
|
||||
uint32_t primary_identifier = name_it->second->primary_identifier;
|
||||
ret.data1[0] = (primary_identifier >> 16) & 0xFF;
|
||||
ret.data1[1] = (primary_identifier >> 8) & 0xFF;
|
||||
ret.data1[2] = primary_identifier & 0xFF;
|
||||
ret.data1[0] = (primary_identifier >> 24) & 0xFF;
|
||||
ret.data1[1] = (primary_identifier >> 16) & 0xFF;
|
||||
ret.data1[2] = (primary_identifier >> 8) & 0xFF;
|
||||
|
||||
if (ret.data1[0] == 0x00) {
|
||||
// Weapons: add special, grind and percentages (or name, if S-rank)
|
||||
ret.data1[4] = weapon_special | (is_wrapped ? 0x40 : 0x00);
|
||||
ret.data1[4] = weapon_special | (is_wrapped ? 0x40 : 0x00) | (is_unidentified ? 0x80 : 0x00);
|
||||
|
||||
auto tokens = split(desc, ' ');
|
||||
for (auto& token : tokens) {
|
||||
@@ -642,7 +607,7 @@ ItemData ItemNameIndex::parse_item_description_phase(Version version, const std:
|
||||
ret.data2[2] |= 0x40;
|
||||
}
|
||||
} else if (ret.data1[0] == 0x03) {
|
||||
if (ret.max_stack_size() > 1) {
|
||||
if (ret.max_stack_size(*this->limits) > 1) {
|
||||
if (starts_with(desc, "x")) {
|
||||
ret.data1[5] = stoul(desc.substr(1), nullptr, 10);
|
||||
} else {
|
||||
@@ -653,7 +618,7 @@ ItemData ItemNameIndex::parse_item_description_phase(Version version, const std:
|
||||
}
|
||||
|
||||
if (is_wrapped) {
|
||||
if (ret.is_stackable()) {
|
||||
if (ret.is_stackable(*this->limits)) {
|
||||
throw runtime_error("stackable items cannot be wrapped");
|
||||
} else {
|
||||
ret.data1[3] |= 0x40;
|
||||
@@ -665,3 +630,309 @@ ItemData ItemNameIndex::parse_item_description_phase(Version version, const std:
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void ItemNameIndex::print_table(FILE* stream) const {
|
||||
auto pmt = this->item_parameter_table;
|
||||
|
||||
fprintf(stream, "WEAPON => ---ID--- TYPE SKIN POINTS FLAG ATPLO ATPHI ATPRQ MSTRQ ATARQ -MST- GND PH SP ATA SB PJ 1X 1Y 2X 2Y CL A1 A2 A3 A4 A5 TB CT V1 ST* USL ---DIVISOR--- NAME\n");
|
||||
for (size_t data1_1 = 0; data1_1 < pmt->num_weapon_classes; data1_1++) {
|
||||
uint8_t v1_replacement = pmt->get_weapon_v1_replacement(data1_1);
|
||||
float sale_divisor = pmt->get_sale_divisor(0x00, data1_1);
|
||||
string divisor_str = string_printf("%g", sale_divisor);
|
||||
divisor_str.resize(13, ' ');
|
||||
|
||||
size_t data1_2_limit = pmt->num_weapons_in_class(data1_1);
|
||||
for (size_t data1_2 = 0; data1_2 < data1_2_limit; data1_2++) {
|
||||
const auto& w = pmt->get_weapon(data1_1, data1_2);
|
||||
uint8_t stars = pmt->get_item_stars(w.base.id);
|
||||
bool is_unsealable = pmt->is_unsealable_item(0x00, data1_1, data1_2);
|
||||
|
||||
ItemData item;
|
||||
item.data1[0] = 0x00;
|
||||
item.data1[1] = data1_1;
|
||||
item.data1[2] = data1_2;
|
||||
string name = this->describe_item(item);
|
||||
|
||||
fprintf(stream, "00%02zX%02zX => %08" PRIX32 " %04hX %04hX %6" PRIu32 " %04hX %5hu %5hu %5hu %5hu %5hu %5hu %3hhu %02hhX %02hhX %3hhu %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %2hhu* %s %s %s\n",
|
||||
data1_1,
|
||||
data1_2,
|
||||
w.base.id.load(),
|
||||
w.base.type.load(),
|
||||
w.base.skin.load(),
|
||||
w.base.team_points.load(),
|
||||
w.class_flags.load(),
|
||||
w.atp_min.load(),
|
||||
w.atp_max.load(),
|
||||
w.atp_required.load(),
|
||||
w.mst_required.load(),
|
||||
w.ata_required.load(),
|
||||
w.mst.load(),
|
||||
w.max_grind,
|
||||
w.photon,
|
||||
w.special,
|
||||
w.ata,
|
||||
w.stat_boost,
|
||||
w.projectile,
|
||||
w.trail1_x,
|
||||
w.trail1_y,
|
||||
w.trail2_x,
|
||||
w.trail2_y,
|
||||
w.color,
|
||||
w.unknown_a1,
|
||||
w.unknown_a2,
|
||||
w.unknown_a3,
|
||||
w.unknown_a4,
|
||||
w.unknown_a5,
|
||||
w.tech_boost,
|
||||
w.combo_type,
|
||||
v1_replacement,
|
||||
stars,
|
||||
is_unsealable ? "YES" : " no",
|
||||
divisor_str.c_str(),
|
||||
name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stream, "ARMOR => ---ID--- TYPE SKIN POINTS -DFP- -EVP- BP BE FLAG LVL EFR ETH EIC EDK ELT DFR EVR SB TB -A2- ST* ---DIVISOR--- NAME\n");
|
||||
for (size_t data1_1 = 1; data1_1 < 3; data1_1++) {
|
||||
float sale_divisor = pmt->get_sale_divisor(0x01, data1_1);
|
||||
string divisor_str = string_printf("%g", sale_divisor);
|
||||
divisor_str.resize(13, ' ');
|
||||
|
||||
size_t data1_2_limit = pmt->num_armors_or_shields_in_class(data1_1);
|
||||
for (size_t data1_2 = 0; data1_2 < data1_2_limit; data1_2++) {
|
||||
const auto& a = pmt->get_armor_or_shield(data1_1, data1_2);
|
||||
uint8_t stars = pmt->get_item_stars(a.base.id);
|
||||
|
||||
ItemData item;
|
||||
item.data1[0] = 0x01;
|
||||
item.data1[1] = data1_1;
|
||||
item.data1[2] = data1_2;
|
||||
string name = this->describe_item(item);
|
||||
|
||||
fprintf(stream, "01%02zX%02zX => %08" PRIX32 " %04hX %04hX %6" PRIu32 " %5hu %5hu %02hhX %02hhX %04hX %3hhu %3hhu %3hhu %3hhu %3hhu %3hhu %3hhu %3hhu %02hhX %02hhX %04hX %2hhu* %s %s\n",
|
||||
data1_1,
|
||||
data1_2,
|
||||
a.base.id.load(),
|
||||
a.base.type.load(),
|
||||
a.base.skin.load(),
|
||||
a.base.team_points.load(),
|
||||
a.dfp.load(),
|
||||
a.evp.load(),
|
||||
a.block_particle,
|
||||
a.block_effect,
|
||||
a.class_flags.load(),
|
||||
static_cast<uint8_t>(a.required_level + 1),
|
||||
a.efr,
|
||||
a.eth,
|
||||
a.eic,
|
||||
a.edk,
|
||||
a.elt,
|
||||
a.dfp_range,
|
||||
a.evp_range,
|
||||
a.stat_boost,
|
||||
a.tech_boost,
|
||||
a.unknown_a2.load(),
|
||||
stars,
|
||||
divisor_str.c_str(),
|
||||
name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stream, "UNIT => ---ID--- TYPE SKIN POINTS STAT COUNT ST-MOD ST* ---DIVISOR--- NAME\n");
|
||||
{
|
||||
float sale_divisor = pmt->get_sale_divisor(0x01, 0x03);
|
||||
string divisor_str = string_printf("%g", sale_divisor);
|
||||
divisor_str.resize(13, ' ');
|
||||
|
||||
size_t data1_2_limit = pmt->num_units();
|
||||
for (size_t data1_2 = 0; data1_2 < data1_2_limit; data1_2++) {
|
||||
const auto& u = pmt->get_unit(data1_2);
|
||||
uint8_t stars = pmt->get_item_stars(u.base.id);
|
||||
|
||||
ItemData item;
|
||||
item.data1[0] = 0x01;
|
||||
item.data1[1] = 0x03;
|
||||
item.data1[2] = data1_2;
|
||||
string name = this->describe_item(item);
|
||||
|
||||
fprintf(stream, "0103%02zX => %08" PRIX32 " %04hX %04hX %6" PRIu32 " %04hX %5hu %6hd %2hhu* %s %s\n",
|
||||
data1_2,
|
||||
u.base.id.load(),
|
||||
u.base.type.load(),
|
||||
u.base.skin.load(),
|
||||
u.base.team_points.load(),
|
||||
u.stat.load(),
|
||||
u.stat_amount.load(),
|
||||
u.modifier_amount.load(),
|
||||
stars,
|
||||
divisor_str.c_str(),
|
||||
name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stream, "MAG => ---ID--- TYPE SKIN POINTS FTBL PB AC E1 E2 E3 E4 C1 C2 C3 C4 FLAG ST* ---DIVISOR--- NAME\n");
|
||||
{
|
||||
size_t data1_1_limit = pmt->num_mags();
|
||||
for (size_t data1_1 = 0; data1_1 < data1_1_limit; data1_1++) {
|
||||
const auto& m = pmt->get_mag(data1_1);
|
||||
uint8_t stars = pmt->get_item_stars(m.base.id);
|
||||
|
||||
float sale_divisor = pmt->get_sale_divisor(0x02, data1_1);
|
||||
string divisor_str = string_printf("%g", sale_divisor);
|
||||
divisor_str.resize(13, ' ');
|
||||
|
||||
ItemData item;
|
||||
item.data1[0] = 0x02;
|
||||
item.data1[1] = data1_1;
|
||||
item.data1[2] = 0x00;
|
||||
string name = this->describe_item(item);
|
||||
|
||||
fprintf(stream, "02%02zX00 => %08" PRIX32 " %04hX %04hX %6" PRIu32 " %04hX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %04hX %2hhu* %s %s\n",
|
||||
data1_1,
|
||||
m.base.id.load(),
|
||||
m.base.type.load(),
|
||||
m.base.skin.load(),
|
||||
m.base.team_points.load(),
|
||||
m.feed_table.load(),
|
||||
m.photon_blast,
|
||||
m.activation,
|
||||
m.on_pb_full,
|
||||
m.on_low_hp,
|
||||
m.on_death,
|
||||
m.on_boss,
|
||||
m.on_pb_full_flag,
|
||||
m.on_low_hp_flag,
|
||||
m.on_death_flag,
|
||||
m.on_boss_flag,
|
||||
m.class_flags.load(),
|
||||
stars,
|
||||
divisor_str.c_str(),
|
||||
name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stream, "TOOL => ---ID--- TYPE SKIN POINTS COUNT TECH -COST- ITEMFLAG ST* ---DIVISOR--- NAME\n");
|
||||
for (size_t data1_1 = 0; data1_1 < pmt->num_tool_classes; data1_1++) {
|
||||
float sale_divisor = pmt->get_sale_divisor(0x03, data1_1);
|
||||
string divisor_str = string_printf("%g", sale_divisor);
|
||||
divisor_str.resize(13, ' ');
|
||||
|
||||
size_t data1_2_limit = pmt->num_tools_in_class(data1_1);
|
||||
for (size_t data1_2 = 0; data1_2 < data1_2_limit; data1_2++) {
|
||||
const auto& t = pmt->get_tool(data1_1, data1_2);
|
||||
uint8_t stars = pmt->get_item_stars(t.base.id);
|
||||
|
||||
ItemData item;
|
||||
item.data1[0] = 0x03;
|
||||
item.data1[1] = data1_1;
|
||||
item.data1[(data1_1 == 0x02) ? 4 : 2] = data1_2;
|
||||
item.set_tool_item_amount(*this->limits, 1);
|
||||
string name = this->describe_item(item);
|
||||
|
||||
fprintf(stream, "03%02zX%02zX => %08" PRIX32 " %04hX %04hX %6" PRIu32 " %5hu %04hX %6" PRId32 " %08" PRIX32 " %2hhu* %s %s\n",
|
||||
data1_1,
|
||||
data1_2,
|
||||
t.base.id.load(),
|
||||
t.base.type.load(),
|
||||
t.base.skin.load(),
|
||||
t.base.team_points.load(),
|
||||
t.amount.load(),
|
||||
t.tech.load(),
|
||||
t.cost.load(),
|
||||
t.item_flag.load(),
|
||||
stars,
|
||||
divisor_str.c_str(),
|
||||
name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stream, "CLASS => F GF RF B GB RB Z GZ RZ GR DB JL ZL SH RY RS AT RV MG\n");
|
||||
for (size_t char_class = 0; char_class < 12; char_class++) {
|
||||
fprintf(stream, "%9s =>", name_for_char_class(char_class));
|
||||
for (size_t tech_num = 0; tech_num < 0x13; tech_num++) {
|
||||
uint8_t max_level = pmt->get_max_tech_level(char_class, tech_num) + 1;
|
||||
if (max_level == 0x00) {
|
||||
fprintf(stream, " ");
|
||||
} else {
|
||||
fprintf(stream, " %2hhu", max_level);
|
||||
}
|
||||
}
|
||||
fprintf(stream, "\n");
|
||||
}
|
||||
|
||||
fprintf(stream, "CLASS => F GF RF B GB RB Z GZ RZ GR DB JL ZL SH RY RS AT RV MG\n");
|
||||
for (size_t char_class = 0; char_class < 12; char_class++) {
|
||||
fprintf(stream, "%9s =>", name_for_char_class(char_class));
|
||||
for (size_t tech_num = 0; tech_num < 0x13; tech_num++) {
|
||||
uint8_t max_level = pmt->get_max_tech_level(char_class, tech_num) + 1;
|
||||
if (max_level == 0x00) {
|
||||
fprintf(stream, " ");
|
||||
} else {
|
||||
fprintf(stream, " %2hhu", max_level);
|
||||
}
|
||||
}
|
||||
fprintf(stream, "\n");
|
||||
}
|
||||
|
||||
for (size_t table_index = 0; table_index < 8; table_index++) {
|
||||
static const char* names[11] = {
|
||||
"Monomate", "Dimate", "Trimate", "Monofluid",
|
||||
"Difluid", "Trifluid", "Antidote", "Antiparalysis",
|
||||
"Sol Atomizer", "Moon Atomizer", "Star Atomizer"};
|
||||
fprintf(stream, "TABLE %02zX => -DEF -POW -DEX MIND -IQ- SYNC\n", table_index);
|
||||
for (size_t which = 0; which < 11; which++) {
|
||||
const auto& res = pmt->get_mag_feed_result(table_index, which);
|
||||
fprintf(stream, "%14s => %4hhd %4hhd %4hhd %4hhd %4hhd %4hhd\n",
|
||||
names[which], res.def, res.pow, res.dex, res.mind, res.iq, res.synchro);
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stream, "SPECIAL => TYPE COUNT ST*\n");
|
||||
for (size_t index = 0; index < pmt->num_specials; index++) {
|
||||
const auto& sp = pmt->get_special(index);
|
||||
uint8_t stars = pmt->get_special_stars(index);
|
||||
fprintf(stream, " %02zX => %04hX %5hu %2hu*\n", index, sp.type.load(), sp.amount.load(), stars);
|
||||
}
|
||||
|
||||
fprintf(stream, "---USE + -EQUIP => RESULT MLV GND LVL CLS\n");
|
||||
for (const auto& combo_list_it : pmt->get_all_item_combinations()) {
|
||||
for (const auto& combo : combo_list_it.second) {
|
||||
fprintf(stream, "%02hhX%02hhX%02hhX + %02hhX%02hhX%02hhX => %02hhX%02hhX%02hhX",
|
||||
combo.used_item[0], combo.used_item[1], combo.used_item[2],
|
||||
combo.equipped_item[0], combo.equipped_item[1], combo.equipped_item[2],
|
||||
combo.result_item[0], combo.result_item[1], combo.result_item[2]);
|
||||
if (combo.mag_level != 0xFF) {
|
||||
fprintf(stream, " %3hu", combo.mag_level);
|
||||
} else {
|
||||
fprintf(stream, " ");
|
||||
}
|
||||
if (combo.grind != 0xFF) {
|
||||
fprintf(stream, " %3hu", combo.grind);
|
||||
} else {
|
||||
fprintf(stream, " ");
|
||||
}
|
||||
if (combo.level != 0xFF) {
|
||||
fprintf(stream, " %3hu", combo.level);
|
||||
} else {
|
||||
fprintf(stream, " ");
|
||||
}
|
||||
if (combo.char_class != 0xFF) {
|
||||
fprintf(stream, " %3hu\n", combo.char_class);
|
||||
} else {
|
||||
fprintf(stream, " \n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t num_events = pmt->num_events();
|
||||
for (size_t event_number = 0; event_number < num_events; event_number++) {
|
||||
fprintf(stream, "EV %3zu => PRB\n", event_number);
|
||||
auto events_list = pmt->get_event_items(event_number);
|
||||
for (size_t z = 0; z < events_list.second; z++) {
|
||||
const auto& event_item = events_list.first[z];
|
||||
fprintf(stream, "%02hhX%02hhX%02hhX => %3hhu\n",
|
||||
event_item.item[0], event_item.item[1], event_item.item[2], event_item.probability);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-18
@@ -14,26 +14,38 @@
|
||||
|
||||
class ItemNameIndex {
|
||||
public:
|
||||
ItemNameIndex(JSON&& v2_names, JSON&& v3_names, JSON&& v4_names);
|
||||
|
||||
std::string describe_item(
|
||||
Version version,
|
||||
const ItemData& item,
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table = nullptr) const;
|
||||
ItemData parse_item_description(Version version, const std::string& description) const;
|
||||
|
||||
private:
|
||||
ItemData parse_item_description_phase(Version version, const std::string& description, bool skip_special) const;
|
||||
|
||||
struct ItemMetadata {
|
||||
uint32_t primary_identifier;
|
||||
std::string v2_name;
|
||||
std::string v3_name;
|
||||
std::string v4_name;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
std::unordered_map<uint32_t, std::shared_ptr<ItemMetadata>> primary_identifier_index;
|
||||
std::map<std::string, std::shared_ptr<ItemMetadata>> v2_name_index;
|
||||
std::map<std::string, std::shared_ptr<ItemMetadata>> v3_name_index;
|
||||
std::map<std::string, std::shared_ptr<ItemMetadata>> v4_name_index;
|
||||
ItemNameIndex(
|
||||
std::shared_ptr<const ItemParameterTable> pmt,
|
||||
std::shared_ptr<const ItemData::StackLimits> limits,
|
||||
const std::vector<std::string>& name_coll);
|
||||
|
||||
inline size_t entry_count() const {
|
||||
return this->primary_identifier_index.size();
|
||||
}
|
||||
|
||||
inline const std::unordered_map<uint32_t, std::shared_ptr<const ItemMetadata>>& all_by_primary_identifier() const {
|
||||
return this->primary_identifier_index;
|
||||
}
|
||||
inline const std::map<std::string, std::shared_ptr<const ItemMetadata>>& all_by_name() const {
|
||||
return this->name_index;
|
||||
}
|
||||
|
||||
std::string describe_item(const ItemData& item, bool include_color_escapes = false) const;
|
||||
ItemData parse_item_description(const std::string& description) const;
|
||||
|
||||
void print_table(FILE* stream) const;
|
||||
|
||||
private:
|
||||
ItemData parse_item_description_phase(const std::string& description, bool skip_special) const;
|
||||
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table;
|
||||
std::shared_ptr<const ItemData::StackLimits> limits;
|
||||
|
||||
std::unordered_map<uint32_t, std::shared_ptr<const ItemMetadata>> primary_identifier_index;
|
||||
std::map<std::string, std::shared_ptr<const ItemMetadata>> name_index;
|
||||
};
|
||||
|
||||
+719
-320
File diff suppressed because it is too large
Load Diff
+329
-150
@@ -5,6 +5,7 @@
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -13,11 +14,16 @@
|
||||
|
||||
class ItemParameterTable {
|
||||
public:
|
||||
// TODO: This implementation is ugly. We should use real classes and virtual
|
||||
// functions instead of manually branching on various offset table pointers
|
||||
// being null or not in each public function. Rewrite this and make it better.
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct ArrayRef {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
U32T count;
|
||||
U32T offset;
|
||||
/* 00 */ U32T count;
|
||||
/* 04 */ U32T offset;
|
||||
/* 08 */
|
||||
} __attribute__((packed));
|
||||
struct ArrayRefLE : ArrayRef<false> {
|
||||
} __attribute__((packed));
|
||||
@@ -29,151 +35,240 @@ public:
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
// id specifies several things; notably, it doubles as the index of the
|
||||
// item's name in the text archive (e.g. TextEnglish) collection 0.
|
||||
U32T id = 0xFFFFFFFF;
|
||||
/* 00 */ U32T id = 0xFFFFFFFF;
|
||||
/* 04 */
|
||||
} __attribute__((packed));
|
||||
template <bool IsBigEndian>
|
||||
struct ItemBaseV3 : ItemBaseV2<IsBigEndian> {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
U16T type = 0;
|
||||
U16T skin = 0;
|
||||
/* 04 */ U16T type = 0;
|
||||
/* 06 */ U16T skin = 0;
|
||||
/* 08 */
|
||||
} __attribute__((packed));
|
||||
template <bool IsBigEndian>
|
||||
struct ItemBaseV4 : ItemBaseV3<IsBigEndian> {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
U32T team_points = 0;
|
||||
/* 08 */ U32T team_points = 0;
|
||||
/* 0C */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct WeaponV2 {
|
||||
ItemBaseV2<false> base;
|
||||
le_uint16_t class_flags = 0;
|
||||
le_uint16_t atp_min = 0;
|
||||
le_uint16_t atp_max = 0;
|
||||
le_uint16_t atp_required = 0;
|
||||
le_uint16_t mst_required = 0;
|
||||
le_uint16_t ata_required = 0;
|
||||
uint8_t max_grind = 0;
|
||||
uint8_t photon = 0;
|
||||
uint8_t special = 0;
|
||||
uint8_t ata = 0;
|
||||
parray<uint8_t, 4> unknown_a9;
|
||||
struct WeaponV4;
|
||||
struct WeaponDCProtos {
|
||||
/* 00 */ ItemBaseV2<false> base;
|
||||
/* 04 */ le_uint16_t class_flags = 0;
|
||||
/* 06 */ le_uint16_t atp_min = 0;
|
||||
/* 08 */ le_uint16_t atp_max = 0;
|
||||
/* 0A */ le_uint16_t atp_required = 0;
|
||||
/* 0C */ le_uint16_t mst_required = 0;
|
||||
/* 0E */ le_uint16_t ata_required = 0;
|
||||
/* 10 */ uint8_t max_grind = 0;
|
||||
/* 11 */ uint8_t photon = 0;
|
||||
/* 12 */ uint8_t special = 0;
|
||||
/* 13 */ uint8_t ata = 0;
|
||||
/* 14 */
|
||||
|
||||
WeaponV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct WeaponV1V2 {
|
||||
/* 00 */ ItemBaseV2<false> base;
|
||||
/* 04 */ le_uint16_t class_flags = 0;
|
||||
/* 06 */ le_uint16_t atp_min = 0;
|
||||
/* 08 */ le_uint16_t atp_max = 0;
|
||||
/* 0A */ le_uint16_t atp_required = 0;
|
||||
/* 0C */ le_uint16_t mst_required = 0;
|
||||
/* 0E */ le_uint16_t ata_required = 0;
|
||||
/* 10 */ uint8_t max_grind = 0;
|
||||
/* 11 */ uint8_t photon = 0;
|
||||
/* 12 */ uint8_t special = 0;
|
||||
/* 13 */ uint8_t ata = 0;
|
||||
/* 14 */ uint8_t stat_boost = 0; // TODO: This could be larger (16 or 32 bits)
|
||||
/* 15 */ parray<uint8_t, 3> unknown_a9;
|
||||
/* 18 */
|
||||
|
||||
WeaponV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct WeaponGCNTE {
|
||||
/* 00 */ ItemBaseV3<true> base;
|
||||
/* 08 */ be_uint16_t class_flags = 0;
|
||||
/* 0A */ be_uint16_t atp_min = 0;
|
||||
/* 0C */ be_uint16_t atp_max = 0;
|
||||
/* 0E */ be_uint16_t atp_required = 0;
|
||||
/* 10 */ be_uint16_t mst_required = 0;
|
||||
/* 12 */ be_uint16_t ata_required = 0;
|
||||
/* 14 */ be_uint16_t mst = 0;
|
||||
/* 16 */ uint8_t max_grind = 0;
|
||||
/* 17 */ uint8_t photon = 0;
|
||||
/* 18 */ uint8_t special = 0;
|
||||
/* 19 */ uint8_t ata = 0;
|
||||
/* 1A */ uint8_t stat_boost = 0;
|
||||
/* 1B */ uint8_t projectile = 0;
|
||||
/* 1C */ int8_t trail1_x = 0;
|
||||
/* 1D */ int8_t trail1_y = 0;
|
||||
/* 1E */ int8_t trail2_x = 0;
|
||||
/* 1F */ int8_t trail2_y = 0;
|
||||
/* 20 */ int8_t color = 0;
|
||||
/* 21 */ uint8_t unknown_a1 = 0;
|
||||
/* 22 */ uint8_t unknown_a2 = 0;
|
||||
/* 23 */ uint8_t unknown_a3 = 0;
|
||||
/* 24 */
|
||||
|
||||
WeaponV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
template <bool IsBigEndian>
|
||||
struct WeaponV3 {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
ItemBaseV3<IsBigEndian> base;
|
||||
U16T class_flags = 0;
|
||||
U16T atp_min = 0;
|
||||
U16T atp_max = 0;
|
||||
U16T atp_required = 0;
|
||||
U16T mst_required = 0;
|
||||
U16T ata_required = 0;
|
||||
U16T mst = 0;
|
||||
uint8_t max_grind = 0;
|
||||
uint8_t photon = 0;
|
||||
uint8_t special = 0;
|
||||
uint8_t ata = 0;
|
||||
uint8_t stat_boost = 0;
|
||||
uint8_t projectile = 0;
|
||||
int8_t trail1_x = 0;
|
||||
int8_t trail1_y = 0;
|
||||
int8_t trail2_x = 0;
|
||||
int8_t trail2_y = 0;
|
||||
int8_t color = 0;
|
||||
uint8_t unknown_a1 = 0;
|
||||
uint8_t unknown_a2 = 0;
|
||||
uint8_t unknown_a3 = 0;
|
||||
uint8_t unknown_a4 = 0;
|
||||
uint8_t unknown_a5 = 0;
|
||||
uint8_t tech_boost = 0;
|
||||
uint8_t combo_type = 0;
|
||||
/* 00 */ ItemBaseV3<IsBigEndian> base;
|
||||
/* 08 */ U16T class_flags = 0;
|
||||
/* 0A */ U16T atp_min = 0;
|
||||
/* 0C */ U16T atp_max = 0;
|
||||
/* 0E */ U16T atp_required = 0;
|
||||
/* 10 */ U16T mst_required = 0;
|
||||
/* 12 */ U16T ata_required = 0;
|
||||
/* 14 */ U16T mst = 0;
|
||||
/* 16 */ uint8_t max_grind = 0;
|
||||
/* 17 */ uint8_t photon = 0;
|
||||
/* 18 */ uint8_t special = 0;
|
||||
/* 19 */ uint8_t ata = 0;
|
||||
/* 1A */ uint8_t stat_boost = 0;
|
||||
/* 1B */ uint8_t projectile = 0;
|
||||
/* 1C */ int8_t trail1_x = 0;
|
||||
/* 1D */ int8_t trail1_y = 0;
|
||||
/* 1E */ int8_t trail2_x = 0;
|
||||
/* 1F */ int8_t trail2_y = 0;
|
||||
/* 20 */ int8_t color = 0;
|
||||
/* 21 */ uint8_t unknown_a1 = 0;
|
||||
/* 22 */ uint8_t unknown_a2 = 0;
|
||||
/* 23 */ uint8_t unknown_a3 = 0;
|
||||
/* 24 */ uint8_t unknown_a4 = 0;
|
||||
/* 25 */ uint8_t unknown_a5 = 0;
|
||||
/* 26 */ uint8_t tech_boost = 0;
|
||||
/* 27 */ uint8_t combo_type = 0;
|
||||
/* 28 */
|
||||
|
||||
WeaponV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct WeaponV4 {
|
||||
ItemBaseV4<false> base;
|
||||
le_uint16_t class_flags = 0x00FF;
|
||||
le_uint16_t atp_min = 0;
|
||||
le_uint16_t atp_max = 0;
|
||||
le_uint16_t atp_required = 0;
|
||||
le_uint16_t mst_required = 0;
|
||||
le_uint16_t ata_required = 0;
|
||||
le_uint16_t mst = 0;
|
||||
uint8_t max_grind = 0;
|
||||
uint8_t photon = 0;
|
||||
uint8_t special = 0;
|
||||
uint8_t ata = 0;
|
||||
uint8_t stat_boost = 0;
|
||||
uint8_t projectile = 0;
|
||||
int8_t trail1_x = 0;
|
||||
int8_t trail1_y = 0;
|
||||
int8_t trail2_x = 0;
|
||||
int8_t trail2_y = 0;
|
||||
int8_t color = 0;
|
||||
uint8_t unknown_a1 = 0;
|
||||
uint8_t unknown_a2 = 0;
|
||||
uint8_t unknown_a3 = 0;
|
||||
uint8_t unknown_a4 = 0;
|
||||
uint8_t unknown_a5 = 0;
|
||||
uint8_t tech_boost = 0;
|
||||
uint8_t combo_type = 0;
|
||||
/* 00 */ ItemBaseV4<false> base;
|
||||
/* 0C */ le_uint16_t class_flags = 0x00FF;
|
||||
/* 0E */ le_uint16_t atp_min = 0;
|
||||
/* 10 */ le_uint16_t atp_max = 0;
|
||||
/* 12 */ le_uint16_t atp_required = 0;
|
||||
/* 14 */ le_uint16_t mst_required = 0;
|
||||
/* 16 */ le_uint16_t ata_required = 0;
|
||||
/* 18 */ le_uint16_t mst = 0;
|
||||
/* 1A */ uint8_t max_grind = 0;
|
||||
/* 1B */ uint8_t photon = 0;
|
||||
/* 1C */ uint8_t special = 0;
|
||||
/* 1D */ uint8_t ata = 0;
|
||||
/* 1E */ uint8_t stat_boost = 0;
|
||||
/* 1F */ uint8_t projectile = 0;
|
||||
/* 20 */ int8_t trail1_x = 0;
|
||||
/* 21 */ int8_t trail1_y = 0;
|
||||
/* 22 */ int8_t trail2_x = 0;
|
||||
/* 23 */ int8_t trail2_y = 0;
|
||||
/* 24 */ int8_t color = 0;
|
||||
/* 25 */ uint8_t unknown_a1 = 0;
|
||||
/* 26 */ uint8_t unknown_a2 = 0;
|
||||
/* 27 */ uint8_t unknown_a3 = 0;
|
||||
/* 28 */ uint8_t unknown_a4 = 0;
|
||||
/* 29 */ uint8_t unknown_a5 = 0;
|
||||
/* 2A */ uint8_t tech_boost = 0;
|
||||
/* 2B */ uint8_t combo_type = 0;
|
||||
/* 2C */
|
||||
} __attribute__((packed));
|
||||
|
||||
template <typename BaseT, bool IsBigEndian>
|
||||
struct ArmorOrShield {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
BaseT base;
|
||||
U16T dfp = 0;
|
||||
U16T evp = 0;
|
||||
uint8_t block_particle = 0;
|
||||
uint8_t block_effect = 0;
|
||||
U16T class_flags = 0x00FF;
|
||||
uint8_t required_level = 0;
|
||||
uint8_t efr = 0;
|
||||
uint8_t eth = 0;
|
||||
uint8_t eic = 0;
|
||||
uint8_t edk = 0;
|
||||
uint8_t elt = 0;
|
||||
uint8_t dfp_range = 0;
|
||||
uint8_t evp_range = 0;
|
||||
uint8_t stat_boost = 0;
|
||||
uint8_t tech_boost = 0;
|
||||
U16T unknown_a2 = 0;
|
||||
/* V1/V2 offsets */
|
||||
/* 00 */ BaseT base;
|
||||
/* 04 */ U16T dfp = 0;
|
||||
/* 06 */ U16T evp = 0;
|
||||
/* 08 */ uint8_t block_particle = 0;
|
||||
/* 09 */ uint8_t block_effect = 0;
|
||||
/* 0A */ U16T class_flags = 0x00FF;
|
||||
/* 0C */ uint8_t required_level = 0;
|
||||
/* 0D */ uint8_t efr = 0;
|
||||
/* 0E */ uint8_t eth = 0;
|
||||
/* 0F */ uint8_t eic = 0;
|
||||
/* 10 */ uint8_t edk = 0;
|
||||
/* 11 */ uint8_t elt = 0;
|
||||
/* 12 */ uint8_t dfp_range = 0;
|
||||
/* 13 */ uint8_t evp_range = 0;
|
||||
/* 14 */
|
||||
} __attribute__((packed));
|
||||
struct ArmorOrShieldV2 : ArmorOrShield<ItemBaseV2<false>, false> {
|
||||
|
||||
struct ArmorOrShieldV4;
|
||||
struct ArmorOrShieldDCProtos : ArmorOrShield<ItemBaseV2<false>, false> {
|
||||
ArmorOrShieldV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct ArmorOrShieldV3 : ArmorOrShield<ItemBaseV3<true>, true> {
|
||||
template <typename BaseT, bool IsBigEndian>
|
||||
struct ArmorOrShieldFinal : ArmorOrShield<BaseT, IsBigEndian> {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
/* 14 */ uint8_t stat_boost = 0;
|
||||
/* 15 */ uint8_t tech_boost = 0;
|
||||
/* 16 */ U16T unknown_a2 = 0;
|
||||
/* 18 */
|
||||
} __attribute__((packed));
|
||||
struct ArmorOrShieldV4 : ArmorOrShield<ItemBaseV4<false>, false> {
|
||||
|
||||
struct ArmorOrShieldV1V2 : ArmorOrShieldFinal<ItemBaseV2<false>, false> {
|
||||
ArmorOrShieldV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
template <bool IsBigEndian>
|
||||
struct ArmorOrShieldV3 : ArmorOrShieldFinal<ItemBaseV3<IsBigEndian>, IsBigEndian> {
|
||||
ArmorOrShieldV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct ArmorOrShieldV4 : ArmorOrShieldFinal<ItemBaseV4<false>, false> {
|
||||
} __attribute__((packed));
|
||||
|
||||
template <typename BaseT, bool IsBigEndian>
|
||||
struct Unit {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
using S16T = typename std::conditional<IsBigEndian, be_int16_t, le_int16_t>::type;
|
||||
BaseT base;
|
||||
U16T stat = 0;
|
||||
U16T stat_amount = 0;
|
||||
S16T modifier_amount = 0;
|
||||
parray<uint8_t, 2> unused;
|
||||
/* V1/V2 offsets */
|
||||
/* 00 */ BaseT base;
|
||||
/* 04 */ U16T stat = 0;
|
||||
/* 06 */ U16T stat_amount = 0;
|
||||
/* 08 */
|
||||
} __attribute__((packed));
|
||||
struct UnitV2 : Unit<ItemBaseV2<false>, false> {
|
||||
|
||||
struct UnitV4;
|
||||
struct UnitDCProtos : Unit<ItemBaseV2<false>, false> {
|
||||
UnitV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct UnitV3 : Unit<ItemBaseV3<true>, true> {
|
||||
template <typename BaseT, bool IsBigEndian>
|
||||
struct UnitFinal : Unit<BaseT, IsBigEndian> {
|
||||
using S16T = typename std::conditional<IsBigEndian, be_int16_t, le_int16_t>::type;
|
||||
/* 08 */ S16T modifier_amount = 0;
|
||||
/* 0A */ parray<uint8_t, 2> unused;
|
||||
/* 0C */
|
||||
} __attribute__((packed));
|
||||
struct UnitV4 : Unit<ItemBaseV4<false>, false> {
|
||||
struct UnitV1V2 : UnitFinal<ItemBaseV2<false>, false> {
|
||||
UnitV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
template <bool IsBigEndian>
|
||||
struct UnitV3 : UnitFinal<ItemBaseV3<IsBigEndian>, IsBigEndian> {
|
||||
UnitV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct UnitV4 : UnitFinal<ItemBaseV4<false>, false> {
|
||||
} __attribute__((packed));
|
||||
|
||||
template <typename BaseT, bool IsBigEndian>
|
||||
struct Mag {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
BaseT base;
|
||||
U16T feed_table = 0;
|
||||
uint8_t photon_blast = 0;
|
||||
uint8_t activation = 0;
|
||||
uint8_t on_pb_full = 0;
|
||||
uint8_t on_low_hp = 0;
|
||||
uint8_t on_death = 0;
|
||||
uint8_t on_boss = 0;
|
||||
/* V1/V2 offsets */
|
||||
/* 00 */ BaseT base;
|
||||
/* 04 */ U16T feed_table = 0;
|
||||
/* 06 */ uint8_t photon_blast = 0;
|
||||
/* 07 */ uint8_t activation = 0;
|
||||
/* 08 */ uint8_t on_pb_full = 0;
|
||||
/* 09 */ uint8_t on_low_hp = 0;
|
||||
/* 0A */ uint8_t on_death = 0;
|
||||
/* 0B */ uint8_t on_boss = 0;
|
||||
// These flags control how likely each effect is to activate. First, the
|
||||
// game computes step_synchro as follows:
|
||||
// if synchro in [0, 30], step_synchro = 0
|
||||
@@ -189,18 +284,37 @@ public:
|
||||
// flag == 3 => activation - 10
|
||||
// flag == 4 => step_synchro - 10
|
||||
// anything else => 0 (effect never occurs)
|
||||
uint8_t on_pb_full_flag = 0;
|
||||
uint8_t on_low_hp_flag = 0;
|
||||
uint8_t on_death_flag = 0;
|
||||
uint8_t on_boss_flag = 0;
|
||||
U16T class_flags = 0x00FF;
|
||||
parray<uint8_t, 2> unused;
|
||||
/* 0C */ uint8_t on_pb_full_flag = 0;
|
||||
/* 0D */ uint8_t on_low_hp_flag = 0;
|
||||
/* 0E */ uint8_t on_death_flag = 0;
|
||||
/* 0F */ uint8_t on_boss_flag = 0;
|
||||
/* 10 */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct MagV4;
|
||||
struct MagV1 : Mag<ItemBaseV2<false>, false> {
|
||||
MagV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct MagV2 : Mag<ItemBaseV2<false>, false> {
|
||||
/* 10 */ le_uint16_t class_flags = 0x00FF;
|
||||
/* 12 */ parray<uint8_t, 2> unused;
|
||||
/* 14 */
|
||||
|
||||
MagV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct MagV3 : Mag<ItemBaseV3<true>, true> {
|
||||
template <bool IsBigEndian>
|
||||
struct MagV3 : Mag<ItemBaseV3<IsBigEndian>, IsBigEndian> {
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
/* 10 */ U16T class_flags = 0x00FF;
|
||||
/* 12 */ parray<uint8_t, 2> unused;
|
||||
/* 14 */
|
||||
|
||||
MagV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct MagV4 : Mag<ItemBaseV4<false>, false> {
|
||||
/* 10 */ le_uint16_t class_flags = 0x00FF;
|
||||
/* 12 */ parray<uint8_t, 2> unused;
|
||||
/* 14 */
|
||||
} __attribute__((packed));
|
||||
|
||||
template <typename BaseT, bool IsBigEndian>
|
||||
@@ -208,15 +322,22 @@ public:
|
||||
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
|
||||
using S32T = typename std::conditional<IsBigEndian, be_int32_t, le_int32_t>::type;
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
BaseT base;
|
||||
U16T amount = 0;
|
||||
U16T tech = 0;
|
||||
S32T cost = 0;
|
||||
U32T item_flag = 0;
|
||||
/* V1/V2 offsets */
|
||||
/* 00 */ BaseT base;
|
||||
/* 04 */ U16T amount = 0;
|
||||
/* 06 */ U16T tech = 0;
|
||||
/* 08 */ S32T cost = 0;
|
||||
/* 0C */ U32T item_flag = 0;
|
||||
/* 10 */
|
||||
} __attribute__((packed));
|
||||
struct ToolV2 : Tool<ItemBaseV2<false>, false> {
|
||||
|
||||
struct ToolV4;
|
||||
struct ToolV1V2 : Tool<ItemBaseV2<false>, false> {
|
||||
ToolV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct ToolV3 : Tool<ItemBaseV3<true>, true> {
|
||||
template <bool IsBigEndian>
|
||||
struct ToolV3 : Tool<ItemBaseV3<IsBigEndian>, IsBigEndian> {
|
||||
ToolV4 to_v4() const;
|
||||
} __attribute__((packed));
|
||||
struct ToolV4 : Tool<ItemBaseV4<false>, false> {
|
||||
} __attribute__((packed));
|
||||
@@ -300,25 +421,28 @@ public:
|
||||
FloatT mag_divisor = 0.0f;
|
||||
} __attribute__((packed));
|
||||
|
||||
enum class Version {
|
||||
V2,
|
||||
V3,
|
||||
V4,
|
||||
};
|
||||
|
||||
ItemParameterTable(std::shared_ptr<const std::string> data, Version version);
|
||||
~ItemParameterTable() = default;
|
||||
|
||||
void print(FILE* stream) const;
|
||||
std::set<uint32_t> compute_all_valid_primary_identifiers() const;
|
||||
|
||||
size_t num_weapons_in_class(uint8_t data1_1) const;
|
||||
const WeaponV4& get_weapon(uint8_t data1_1, uint8_t data1_2) const;
|
||||
size_t num_armors_or_shields_in_class(uint8_t data1_1) const;
|
||||
const ArmorOrShieldV4& get_armor_or_shield(uint8_t data1_1, uint8_t data1_2) const;
|
||||
size_t num_units() const;
|
||||
const UnitV4& get_unit(uint8_t data1_2) const;
|
||||
size_t num_mags() const;
|
||||
const MagV4& get_mag(uint8_t data1_1) const;
|
||||
size_t num_tools_in_class(uint8_t data1_1) const;
|
||||
const ToolV4& get_tool(uint8_t data1_1, uint8_t data1_2) const;
|
||||
std::pair<uint8_t, uint8_t> find_tool_by_id(uint32_t id) const;
|
||||
|
||||
float get_sale_divisor(uint8_t data1_0, uint8_t data1_1) const;
|
||||
const MagFeedResult& get_mag_feed_result(uint8_t table_index, uint8_t which) const;
|
||||
uint8_t get_item_stars(uint32_t id) const;
|
||||
uint8_t get_special_stars(uint8_t det) const;
|
||||
uint8_t get_special_stars(uint8_t special) const;
|
||||
const Special<false>& get_special(uint8_t special) const;
|
||||
uint8_t get_max_tech_level(uint8_t char_class, uint8_t tech_num) const;
|
||||
uint8_t get_weapon_v1_replacement(uint8_t data1_1) const;
|
||||
@@ -328,10 +452,12 @@ public:
|
||||
uint8_t get_item_base_stars(const ItemData& item) const;
|
||||
uint8_t get_item_adjusted_stars(const ItemData& item) const;
|
||||
bool is_item_rare(const ItemData& item) const;
|
||||
bool is_unsealable_item(uint8_t data1_0, uint8_t data1_1, uint8_t data1_2) const;
|
||||
bool is_unsealable_item(const ItemData& param_1) const;
|
||||
const ItemCombination& get_item_combination(const ItemData& used_item, const ItemData& equipped_item) const;
|
||||
const std::vector<ItemCombination>& get_all_combinations_for_used_item(const ItemData& used_item) const;
|
||||
const std::map<uint32_t, std::vector<ItemCombination>>& get_all_item_combinations() const;
|
||||
size_t num_events() const;
|
||||
std::pair<const EventItem*, size_t> get_event_items(uint8_t event_number) const;
|
||||
|
||||
size_t price_for_item(const ItemData& item) const;
|
||||
@@ -343,29 +469,76 @@ public:
|
||||
size_t special_stars_begin_index;
|
||||
size_t num_specials;
|
||||
size_t first_rare_mag_index;
|
||||
size_t star_value_table_size;
|
||||
|
||||
private:
|
||||
struct TableOffsetsV2 {
|
||||
struct TableOffsetsDCProtos {
|
||||
/* ## / NTE / 11/2000 */
|
||||
/* 00 / 0013 / 0013 */ le_uint32_t unknown_a0;
|
||||
/* 04 / 2E1C / 2EB8 */ le_uint32_t weapon_table;
|
||||
/* 08 / 2D94 / 2E28 */ le_uint32_t armor_table;
|
||||
/* 0C / 2DA4 / 2E38 */ le_uint32_t unit_table;
|
||||
/* 10 / 2DB4 / 2E48 */ le_uint32_t tool_table;
|
||||
/* 14 / 2DAC / 2E40 */ le_uint32_t mag_table;
|
||||
/* 18 / 1F98 / 202C */ le_uint32_t v1_replacement_table;
|
||||
/* 1C / 1994 / 1A28 */ le_uint32_t photon_color_table;
|
||||
/* 20 / 1C64 / 1CF8 */ le_uint32_t weapon_range_table;
|
||||
/* 24 / 1FBF / 2053 */ le_uint32_t weapon_sale_divisor_table;
|
||||
/* 28 / 1FE6 / 207A */ le_uint32_t sale_divisor_table;
|
||||
/* 2C / 2F54 / 2FF0 */ le_uint32_t mag_feed_table;
|
||||
/* 30 / 22A9 / 233D */ le_uint32_t star_value_table;
|
||||
/* 34 / 23EE / 2484 */ le_uint32_t unknown_a1;
|
||||
/* 38 / 275E / 27F4 */ le_uint32_t special_data_table;
|
||||
/* 3C / 2804 / 2898 */ le_uint32_t stat_boost_table;
|
||||
/* 40 / 1908 / 199C */ le_uint32_t shield_effect_table;
|
||||
/* 44 / 0668 / 0668 */ le_uint32_t unknown_a2;
|
||||
/* 48 / 030C / 030C */ le_uint32_t unknown_a3;
|
||||
/* 4C / 2CE4 / 2D78 */ le_uint32_t unknown_a4;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct TableOffsetsV1V2 {
|
||||
// TODO: Is weapon count 0x89 or 0x8A? It could be that the last entry in
|
||||
// weapon_table is used for ???? items.
|
||||
/* 00 / 0013 */ le_uint32_t unknown_a0;
|
||||
/* 04 / 5AFC */ le_uint32_t weapon_table; // -> [{count, offset -> [WeaponV2]}](0x89)
|
||||
/* 08 / 5A5C */ le_uint32_t armor_table; // -> [{count, offset -> [ArmorOrShieldV2]}](2; armors and shields)
|
||||
/* 0C / 5A6C */ le_uint32_t unit_table; // -> {count, offset -> [UnitV2]} (last if out of range)
|
||||
/* 10 / 5A7C */ le_uint32_t tool_table; // -> [{count, offset -> [ToolV2]}](0x10) (last if out of range)
|
||||
/* 14 / 5A74 */ le_uint32_t mag_table; // -> {count, offset -> [MagV2]}
|
||||
/* 18 / 3DF8 */ le_uint32_t v1_replacement_table; // -> [uint8_t](0x89)
|
||||
/* 1C / 2E4C */ le_uint32_t photon_color_table; // -> [0x24-byte structs](0x20)
|
||||
/* 20 / 32CC */ le_uint32_t weapon_range_table; // -> ???
|
||||
/* 24 / 3E84 */ le_uint32_t weapon_sale_divisor_table; // -> [float](0x89)
|
||||
/* 28 / 40A8 */ le_uint32_t sale_divisor_table; // -> NonWeaponSaleDivisors
|
||||
/* 2C / 5F4C */ le_uint32_t mag_feed_table; // -> MagFeedResultsTable
|
||||
/* 30 / 4378 */ le_uint32_t star_value_table; // -> [uint8_t](0x1C7)
|
||||
/* 34 / 4540 */ le_uint32_t special_data_table; // -> [Special](0x29)
|
||||
/* 38 / 45E4 */ le_uint32_t weapon_effect_table; // -> [16-byte structs]
|
||||
/* 3C / 58DC */ le_uint32_t stat_boost_table; // -> [StatBoost]
|
||||
/* 40 / 5704 */ le_uint32_t shield_effect_table; // -> [8-byte structs]
|
||||
/* ## / V1 / V2*/
|
||||
/* 00 / 0013 / 0013 */ le_uint32_t unknown_a0;
|
||||
/* 04 / 32E8 / 5AFC */ le_uint32_t weapon_table; // -> [{count, offset -> [WeaponV2]}](0x89)
|
||||
/* 08 / 3258 / 5A5C */ le_uint32_t armor_table; // -> [{count, offset -> [ArmorOrShieldV2]}](2; armors and shields)
|
||||
/* 0C / 3268 / 5A6C */ le_uint32_t unit_table; // -> {count, offset -> [UnitV2]} (last if out of range)
|
||||
/* 10 / 3278 / 5A7C */ le_uint32_t tool_table; // -> [{count, offset -> [ToolV2]}](0x10) (last if out of range)
|
||||
/* 14 / 3270 / 5A74 */ le_uint32_t mag_table; // -> {count, offset -> [MagV2]}
|
||||
/* 18 / 23C8 / 3DF8 */ le_uint32_t v1_replacement_table; // -> [uint8_t](0x89)
|
||||
/* 1C / 1DB0 / 2E4C */ le_uint32_t photon_color_table; // -> [0x24-byte structs](0x20)
|
||||
/* 20 / 2080 / 32CC */ le_uint32_t weapon_range_table; // -> ???
|
||||
/* 24 / 23F0 / 3E84 */ le_uint32_t weapon_sale_divisor_table; // -> [float](0x89)
|
||||
/* 28 / 248C / 40A8 */ le_uint32_t sale_divisor_table; // -> NonWeaponSaleDivisors
|
||||
/* 2C / 3420 / 5F4C */ le_uint32_t mag_feed_table; // -> MagFeedResultsTable
|
||||
/* 30 / 275C / 4378 */ le_uint32_t star_value_table; // -> [uint8_t](0x1C7)
|
||||
/* 34 / 28A2 / 45E4 */ le_uint32_t unknown_a1;
|
||||
/* 38 / 2C12 / 4540 */ le_uint32_t special_data_table; // -> [Special](0x29)
|
||||
/* 3C / 2CB8 / 58DC */ le_uint32_t stat_boost_table; // -> [StatBoost]
|
||||
/* 40 / 3198 / 5704 */ le_uint32_t shield_effect_table; // -> [8-byte structs]
|
||||
} __attribute__((packed));
|
||||
|
||||
struct TableOffsetsGCNTE {
|
||||
/* 00 / 6F0C */ be_uint32_t weapon_table; // -> [{count, offset -> [WeaponV3/WeaponV4]}](0xED)
|
||||
/* 04 / 6E4C */ be_uint32_t armor_table; // -> [{count, offset -> [ArmorOrShieldV3/ArmorOrShieldV4]}](2; armors and shields)
|
||||
/* 08 / 6E5C */ be_uint32_t unit_table; // -> {count, offset -> [UnitV3/UnitV4]} (last if out of range)
|
||||
/* 0C / 6E6C */ be_uint32_t tool_table; // -> [{count, offset -> [ToolV3/ToolV4]}](0x1A) (last if out of range)
|
||||
/* 10 / 6E64 */ be_uint32_t mag_table; // -> {count, offset -> [MagV3/MagV4]}
|
||||
/* 14 / 47BC */ be_uint32_t v1_replacement_table; // -> [uint8_t](0xED)
|
||||
/* 18 / 37A4 */ be_uint32_t photon_color_table; // -> [0x24-byte structs](0x20)
|
||||
/* 1C / 3A74 */ be_uint32_t weapon_range_table; // -> ???
|
||||
/* 20 / 484C */ be_uint32_t weapon_sale_divisor_table; // -> [float](0xED)
|
||||
/* 24 / 4A80 */ be_uint32_t sale_divisor_table; // -> NonWeaponSaleDivisors
|
||||
/* 28 / 7384 */ be_uint32_t mag_feed_table; // -> MagFeedResultsTable
|
||||
/* 2C / 4D50 */ be_uint32_t star_value_table; // -> [uint8_t](0x330) (indexed by .id from weapon, armor, etc.)
|
||||
/* 30 / 4F72 */ be_uint32_t special_data_table; // -> [Special]
|
||||
/* 34 / 5018 */ be_uint32_t weapon_effect_table; // -> [16-byte structs]
|
||||
/* 38 / 68B8 */ be_uint32_t stat_boost_table; // -> [StatBoost]
|
||||
/* 3C / 61B8 */ be_uint32_t shield_effect_table; // -> [8-byte structs]
|
||||
/* 40 / 69D8 */ be_uint32_t max_tech_level_table; // -> MaxTechniqueLevels
|
||||
/* 44 / 737C */ be_uint32_t combination_table; // -> {count, offset -> [ItemCombination]}
|
||||
/* 48 / 68B0 */ be_uint32_t unknown_a1;
|
||||
/* 4C / 6B1C */ be_uint32_t tech_boost_table; // -> [TechniqueBoost] (always 0x2C of them? from counts struct?)
|
||||
} __attribute__((packed));
|
||||
|
||||
template <bool IsBigEndian>
|
||||
@@ -397,10 +570,14 @@ private:
|
||||
/* 58 / F600 / 15024 */ U32T ranged_special_table; // -> {count, offset -> [4-byte structs]}
|
||||
} __attribute__((packed));
|
||||
|
||||
Version version;
|
||||
std::shared_ptr<const std::string> data;
|
||||
StringReader r;
|
||||
const TableOffsetsV2* offsets_v2;
|
||||
const TableOffsetsV3V4<true>* offsets_v3;
|
||||
const TableOffsetsDCProtos* offsets_dc_protos;
|
||||
const TableOffsetsV1V2* offsets_v1_v2;
|
||||
const TableOffsetsGCNTE* offsets_gc_nte;
|
||||
const TableOffsetsV3V4<false>* offsets_v3_le;
|
||||
const TableOffsetsV3V4<true>* offsets_v3_be;
|
||||
const TableOffsetsV3V4<false>* offsets_v4;
|
||||
|
||||
// These are unused if offsets_v4 is not null (in that case, we just return
|
||||
@@ -419,8 +596,10 @@ private:
|
||||
|
||||
template <typename ToolT, bool IsBigEndian>
|
||||
std::pair<uint8_t, uint8_t> find_tool_by_id_t(uint32_t tool_table_offset, uint32_t id) const;
|
||||
template <bool IsBigEndian, typename OffsetsT>
|
||||
float get_sale_divisor_t(const OffsetsT* offsets, uint8_t data1_0, uint8_t data1_1) const;
|
||||
template <bool IsBigEndian>
|
||||
float get_sale_divisor_t(uint32_t weapon_table_offset, uint32_t non_weapon_table_offset, uint8_t data1_0, uint8_t data1_1) const;
|
||||
size_t num_events_t(uint32_t base_offset) const;
|
||||
template <bool IsBigEndian>
|
||||
std::pair<const ItemParameterTable::EventItem*, size_t> get_event_items_t(uint32_t base_offset, uint8_t event_number) const;
|
||||
};
|
||||
|
||||
+121
-108
@@ -2,13 +2,11 @@
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <phosg/Random.hh>
|
||||
|
||||
#include "SendCommands.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
void player_use_item(shared_ptr<Client> c, size_t item_index, shared_ptr<PSOLFGEncryption> opt_rand_crypt) {
|
||||
auto s = c->require_server_state();
|
||||
|
||||
// On PC (and presumably DC), the client sends a 6x29 after this to delete the
|
||||
@@ -19,20 +17,20 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
|
||||
auto player = c->character();
|
||||
auto& item = player->inventory.items[item_index];
|
||||
uint32_t item_identifier = item.data.primary_identifier();
|
||||
uint32_t primary_identifier = item.data.primary_identifier();
|
||||
|
||||
if (item.data.is_common_consumable()) { // Monomate, etc.
|
||||
// Nothing to do (it should be deleted)
|
||||
|
||||
} else if (item_identifier == 0x030200) { // Technique disk
|
||||
auto item_parameter_table = s->item_parameter_table_for_version(c->version());
|
||||
} else if ((primary_identifier & 0xFFFF0000) == 0x03020000) { // Technique disk
|
||||
auto item_parameter_table = s->item_parameter_table(c->version());
|
||||
uint8_t max_level = item_parameter_table->get_max_tech_level(player->disp.visual.char_class, item.data.data1[4]);
|
||||
if (item.data.data1[2] > max_level) {
|
||||
throw runtime_error("technique level too high");
|
||||
}
|
||||
player->set_technique_level(item.data.data1[4], item.data.data1[2]);
|
||||
|
||||
} else if ((item_identifier & 0xFFFF00) == 0x030A00) { // Grinder
|
||||
} else if ((primary_identifier & 0xFFFF0000) == 0x030A0000) { // Grinder
|
||||
if (item.data.data1[2] > 2) {
|
||||
throw runtime_error("incorrect grinder value");
|
||||
}
|
||||
@@ -45,7 +43,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
// get an accurate picture of what's actually in the player's inventory, so
|
||||
// there's no way to know if we would be enforcing the correct grind limit.
|
||||
if (is_v3_or_later) {
|
||||
auto item_parameter_table = s->item_parameter_table_for_version(c->version());
|
||||
auto item_parameter_table = s->item_parameter_table(c->version());
|
||||
auto weapon_def = item_parameter_table->get_weapon(weapon.data.data1[1], weapon.data.data1[2]);
|
||||
if (weapon.data.data1[3] >= weapon_def.max_grind) {
|
||||
throw runtime_error("weapon already at maximum grind");
|
||||
@@ -53,7 +51,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
}
|
||||
weapon.data.data1[3] += (item.data.data1[2] + 1);
|
||||
|
||||
} else if ((item_identifier & 0xFFFF00) == 0x030B00) { // Material
|
||||
} else if ((primary_identifier & 0xFFFF0000) == 0x030B0000) { // Material
|
||||
auto p = c->character();
|
||||
|
||||
using Type = PSOBBCharacterFile::MaterialType;
|
||||
@@ -83,7 +81,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
break;
|
||||
case 6: // Hit Material (v1/v2) or Luck Material (v3/v4)
|
||||
type = Type::LUCK;
|
||||
if (!is_v3_or_later) {
|
||||
if (!is_v3_or_later && (c->version() != Version::GC_NTE)) {
|
||||
// Hit material doesn't exist on v3/v4, but we'll ignore type anyway
|
||||
// in this case because track_non_hp_tp_materials is false
|
||||
p->disp.stats.char_stats.ata += 2;
|
||||
@@ -93,7 +91,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
break;
|
||||
case 7: // Luck Material (v1/v2)
|
||||
type = Type::LUCK;
|
||||
if (!is_v3_or_later) {
|
||||
if (!is_v3_or_later && (c->version() != Version::GC_NTE)) {
|
||||
p->disp.stats.char_stats.lck += 2;
|
||||
} else {
|
||||
throw runtime_error("unknown material used");
|
||||
@@ -106,71 +104,71 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
p->set_material_usage(type, p->get_material_usage(type) + 1);
|
||||
}
|
||||
|
||||
} else if ((item_identifier & 0xFFFF00) == 0x030F00) { // AddSlot
|
||||
} else if ((primary_identifier & 0xFFFF0000) == 0x030F0000) { // AddSlot
|
||||
auto& armor = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::ARMOR)];
|
||||
if (armor.data.data1[5] >= 4) {
|
||||
throw runtime_error("armor already at maximum slot count");
|
||||
}
|
||||
armor.data.data1[5]++;
|
||||
|
||||
} else if (item.data.is_wrapped()) {
|
||||
} else if (item.data.is_wrapped(*s->item_stack_limits(c->version()))) {
|
||||
// Unwrap present
|
||||
item.data.unwrap();
|
||||
item.data.unwrap(*s->item_stack_limits(c->version()));
|
||||
should_delete_item = false;
|
||||
|
||||
} else if (item_identifier == 0x003300) {
|
||||
} else if (primary_identifier == 0x00330000) {
|
||||
// Unseal Sealed J-Sword => Tsumikiri J-Sword
|
||||
item.data.data1[1] = 0x32;
|
||||
should_delete_item = false;
|
||||
|
||||
} else if (item_identifier == 0x00AB00) {
|
||||
} else if (primary_identifier == 0x00AB0000) {
|
||||
// Unseal Lame d'Argent => Excalibur
|
||||
item.data.data1[1] = 0xAC;
|
||||
should_delete_item = false;
|
||||
|
||||
} else if (item_identifier == 0x01034D) {
|
||||
} else if (primary_identifier == 0x01034D00) {
|
||||
// Unseal Limiter => Adept
|
||||
item.data.data1[2] = 0x4E;
|
||||
should_delete_item = false;
|
||||
|
||||
} else if (item_identifier == 0x01034F) {
|
||||
} else if (primary_identifier == 0x01034F00) {
|
||||
// Unseal Swordsman Lore => Proof of Sword-Saint
|
||||
item.data.data1[2] = 0x50;
|
||||
should_delete_item = false;
|
||||
|
||||
} else if (item_identifier == 0x030C00) {
|
||||
} else if (primary_identifier == 0x030C0000) {
|
||||
// Cell of MAG 502
|
||||
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
|
||||
mag.data.data1[1] = (player->disp.visual.section_id & 1) ? 0x1D : 0x21;
|
||||
|
||||
} else if (item_identifier == 0x030C01) {
|
||||
} else if (primary_identifier == 0x030C0100) {
|
||||
// Cell of MAG 213
|
||||
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
|
||||
mag.data.data1[1] = (player->disp.visual.section_id & 1) ? 0x27 : 0x22;
|
||||
|
||||
} else if (item_identifier == 0x030C02) {
|
||||
} else if (primary_identifier == 0x030C0200) {
|
||||
// Parts of RoboChao
|
||||
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
|
||||
mag.data.data1[1] = 0x28;
|
||||
|
||||
} else if (item_identifier == 0x030C03) {
|
||||
} else if (primary_identifier == 0x030C0300) {
|
||||
// Heart of Opa Opa
|
||||
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
|
||||
mag.data.data1[1] = 0x29;
|
||||
|
||||
} else if (item_identifier == 0x030C04) {
|
||||
} else if (primary_identifier == 0x030C0400) {
|
||||
// Heart of Pian
|
||||
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
|
||||
mag.data.data1[1] = 0x2A;
|
||||
|
||||
} else if (item_identifier == 0x030C05) {
|
||||
} else if (primary_identifier == 0x030C0500) {
|
||||
// Heart of Chao
|
||||
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
|
||||
mag.data.data1[1] = 0x2B;
|
||||
|
||||
} else if ((item_identifier & 0xFFFF00) == 0x031500) {
|
||||
} else if ((primary_identifier & 0xFFFF0000) == 0x03150000) {
|
||||
// Christmas Present, etc. - use unwrap_table + probabilities therein
|
||||
auto item_parameter_table = s->item_parameter_table_for_version(c->version());
|
||||
auto item_parameter_table = s->item_parameter_table(c->version());
|
||||
auto table = item_parameter_table->get_event_items(item.data.data1[2]);
|
||||
size_t sum = 0;
|
||||
for (size_t z = 0; z < table.second; z++) {
|
||||
@@ -179,7 +177,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
if (sum == 0) {
|
||||
throw runtime_error("no unwrap results available for event");
|
||||
}
|
||||
size_t det = random_object<size_t>() % sum;
|
||||
size_t det = random_from_optional_crypt(opt_rand_crypt) % sum;
|
||||
for (size_t z = 0; z < table.second; z++) {
|
||||
const auto& entry = table.first[z];
|
||||
if (det > entry.probability) {
|
||||
@@ -209,7 +207,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
auto item_parameter_table = s->item_parameter_table_for_version(c->version());
|
||||
auto item_parameter_table = s->item_parameter_table(c->version());
|
||||
const auto& combo = item_parameter_table->get_item_combination(item.data, inv_item.data);
|
||||
if (combo.char_class != 0xFF && combo.char_class != player->disp.visual.char_class) {
|
||||
throw runtime_error("item combination requires specific char_class");
|
||||
@@ -252,33 +250,35 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
if (should_delete_item) {
|
||||
// Allow overdrafting meseta if the client is not BB, since the server isn't
|
||||
// informed when meseta is added or removed from the bank.
|
||||
player->remove_item(item.data.id, 1, !is_v4(c->version()));
|
||||
player->remove_item(item.data.id, 1, *s->item_stack_limits(c->version()));
|
||||
}
|
||||
}
|
||||
|
||||
void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fed_item_index) {
|
||||
void apply_mag_feed_result(
|
||||
ItemData& mag_item,
|
||||
const ItemData& fed_item,
|
||||
shared_ptr<const ItemParameterTable> item_parameter_table,
|
||||
shared_ptr<const MagEvolutionTable> mag_evolution_table,
|
||||
uint8_t char_class,
|
||||
uint8_t section_id,
|
||||
bool version_has_rare_mags) {
|
||||
|
||||
static const unordered_map<uint32_t, size_t> result_index_for_fed_item({
|
||||
{0x030000, 0}, // Monomate
|
||||
{0x030001, 1}, // Dimate
|
||||
{0x030002, 2}, // Trimate
|
||||
{0x030100, 3}, // Monofluid
|
||||
{0x030101, 4}, // Difluid
|
||||
{0x030102, 5}, // Trifluid
|
||||
{0x030600, 6}, // Antidote
|
||||
{0x030601, 7}, // Antiparalysis
|
||||
{0x030300, 8}, // Sol Atomizer
|
||||
{0x030400, 9}, // Moon Atomizer
|
||||
{0x030500, 10}, // Star Atomizer
|
||||
{0x03000000, 0}, // Monomate
|
||||
{0x03000100, 1}, // Dimate
|
||||
{0x03000200, 2}, // Trimate
|
||||
{0x03010000, 3}, // Monofluid
|
||||
{0x03010100, 4}, // Difluid
|
||||
{0x03010200, 5}, // Trifluid
|
||||
{0x03060000, 6}, // Antidote
|
||||
{0x03060100, 7}, // Antiparalysis
|
||||
{0x03030000, 8}, // Sol Atomizer
|
||||
{0x03040000, 9}, // Moon Atomizer
|
||||
{0x03050000, 10}, // Star Atomizer
|
||||
});
|
||||
|
||||
auto s = c->require_server_state();
|
||||
auto player = c->character();
|
||||
auto& fed_item = player->inventory.items[fed_item_index];
|
||||
auto& mag_item = player->inventory.items[mag_item_index];
|
||||
|
||||
size_t result_index = result_index_for_fed_item.at(fed_item.data.primary_identifier());
|
||||
auto item_parameter_table = s->item_parameter_table_for_version(c->version());
|
||||
const auto& mag_def = item_parameter_table->get_mag(mag_item.data.data1[1]);
|
||||
size_t result_index = result_index_for_fed_item.at(fed_item.primary_identifier());
|
||||
const auto& mag_def = item_parameter_table->get_mag(mag_item.data1[1]);
|
||||
const auto& feed_result = item_parameter_table->get_mag_feed_result(mag_def.feed_table, result_index);
|
||||
|
||||
auto update_stat = +[](ItemData& data, size_t which, int8_t delta) -> void {
|
||||
@@ -295,17 +295,17 @@ void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fe
|
||||
}
|
||||
};
|
||||
|
||||
update_stat(mag_item.data, 2, feed_result.def);
|
||||
update_stat(mag_item.data, 3, feed_result.pow);
|
||||
update_stat(mag_item.data, 4, feed_result.dex);
|
||||
update_stat(mag_item.data, 5, feed_result.mind);
|
||||
mag_item.data.data2[0] = clamp<ssize_t>(static_cast<ssize_t>(mag_item.data.data2[0]) + feed_result.synchro, 0, 120);
|
||||
mag_item.data.data2[1] = clamp<ssize_t>(static_cast<ssize_t>(mag_item.data.data2[1]) + feed_result.iq, 0, 200);
|
||||
update_stat(mag_item, 2, feed_result.def);
|
||||
update_stat(mag_item, 3, feed_result.pow);
|
||||
update_stat(mag_item, 4, feed_result.dex);
|
||||
update_stat(mag_item, 5, feed_result.mind);
|
||||
mag_item.data2[0] = clamp<ssize_t>(static_cast<ssize_t>(mag_item.data2[0]) + feed_result.synchro, 0, 120);
|
||||
mag_item.data2[1] = clamp<ssize_t>(static_cast<ssize_t>(mag_item.data2[1]) + feed_result.iq, 0, 200);
|
||||
|
||||
uint8_t mag_level = mag_item.data.compute_mag_level();
|
||||
mag_item.data.data1[2] = mag_level;
|
||||
uint8_t evolution_number = s->mag_evolution_table->get_evolution_number(mag_item.data.data1[1]);
|
||||
uint8_t mag_number = mag_item.data.data1[1];
|
||||
uint8_t mag_level = mag_item.compute_mag_level();
|
||||
mag_item.data1[2] = mag_level;
|
||||
uint8_t evolution_number = mag_evolution_table->get_evolution_number(mag_item.data1[1]);
|
||||
uint8_t mag_number = mag_item.data1[1];
|
||||
|
||||
// Note: Sega really did just hardcode all these rules into the client. There
|
||||
// is no data file describing these evolutions, unfortunately.
|
||||
@@ -315,24 +315,24 @@ void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fe
|
||||
|
||||
} else if (mag_level < 35) { // Level 10 evolution
|
||||
if (evolution_number < 1) {
|
||||
switch (player->disp.visual.char_class) {
|
||||
switch (char_class) {
|
||||
case 0: // HUmar
|
||||
case 1: // HUnewearl
|
||||
case 2: // HUcast
|
||||
case 9: // HUcaseal
|
||||
mag_item.data.data1[1] = 0x01; // Varuna
|
||||
mag_item.data1[1] = 0x01; // Varuna
|
||||
break;
|
||||
case 3: // RAmar
|
||||
case 11: // RAmarl
|
||||
case 4: // RAcast
|
||||
case 5: // RAcaseal
|
||||
mag_item.data.data1[1] = 0x0D; // Kalki
|
||||
mag_item.data1[1] = 0x0D; // Kalki
|
||||
break;
|
||||
case 10: // FOmar
|
||||
case 6: // FOmarl
|
||||
case 7: // FOnewm
|
||||
case 8: // FOnewearl
|
||||
mag_item.data.data1[1] = 0x19; // Vritra
|
||||
mag_item.data1[1] = 0x19; // Vritra
|
||||
break;
|
||||
default:
|
||||
throw runtime_error("invalid character class");
|
||||
@@ -341,30 +341,30 @@ void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fe
|
||||
|
||||
} else if (mag_level < 50) { // Level 35 evolution
|
||||
if (evolution_number < 2) {
|
||||
uint16_t flags = mag_item.data.compute_mag_strength_flags();
|
||||
uint16_t flags = mag_item.compute_mag_strength_flags();
|
||||
if (mag_number == 0x0D) {
|
||||
if ((flags & 0x110) == 0) {
|
||||
mag_item.data.data1[1] = 0x02;
|
||||
mag_item.data1[1] = 0x02;
|
||||
} else if (flags & 8) {
|
||||
mag_item.data.data1[1] = 0x03;
|
||||
mag_item.data1[1] = 0x03;
|
||||
} else if (flags & 0x20) {
|
||||
mag_item.data.data1[1] = 0x0B;
|
||||
mag_item.data1[1] = 0x0B;
|
||||
}
|
||||
} else if (mag_number == 1) {
|
||||
if (flags & 0x108) {
|
||||
mag_item.data.data1[1] = 0x0E;
|
||||
mag_item.data1[1] = 0x0E;
|
||||
} else if (flags & 0x10) {
|
||||
mag_item.data.data1[1] = 0x0F;
|
||||
mag_item.data1[1] = 0x0F;
|
||||
} else if (flags & 0x20) {
|
||||
mag_item.data.data1[1] = 0x04;
|
||||
mag_item.data1[1] = 0x04;
|
||||
}
|
||||
} else if (mag_number == 0x19) {
|
||||
if (flags & 0x120) {
|
||||
mag_item.data.data1[1] = 0x1A;
|
||||
mag_item.data1[1] = 0x1A;
|
||||
} else if (flags & 8) {
|
||||
mag_item.data.data1[1] = 0x1B;
|
||||
mag_item.data1[1] = 0x1B;
|
||||
} else if (flags & 0x10) {
|
||||
mag_item.data.data1[1] = 0x14;
|
||||
mag_item.data1[1] = 0x14;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -372,18 +372,18 @@ void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fe
|
||||
} else if ((mag_level % 5) == 0) { // Level 50 (and beyond) evolutions
|
||||
if (evolution_number < 4) {
|
||||
|
||||
if ((mag_level >= 100) && !is_v1_or_v2(c->version())) {
|
||||
uint8_t section_id_group = player->disp.visual.section_id % 3;
|
||||
uint16_t def = mag_item.data.data1w[2] / 100;
|
||||
uint16_t pow = mag_item.data.data1w[3] / 100;
|
||||
uint16_t dex = mag_item.data.data1w[4] / 100;
|
||||
uint16_t mind = mag_item.data.data1w[5] / 100;
|
||||
bool is_male = char_class_is_male(player->disp.visual.char_class);
|
||||
if ((mag_level >= 100) && version_has_rare_mags) {
|
||||
uint8_t section_id_group = section_id % 3;
|
||||
uint16_t def = mag_item.data1w[2] / 100;
|
||||
uint16_t pow = mag_item.data1w[3] / 100;
|
||||
uint16_t dex = mag_item.data1w[4] / 100;
|
||||
uint16_t mind = mag_item.data1w[5] / 100;
|
||||
bool is_male = char_class_is_male(char_class);
|
||||
size_t table_index = (is_male ? 0 : 1) + section_id_group * 2;
|
||||
|
||||
bool is_hunter = char_class_is_hunter(player->disp.visual.char_class);
|
||||
bool is_ranger = char_class_is_ranger(player->disp.visual.char_class);
|
||||
bool is_force = char_class_is_force(player->disp.visual.char_class);
|
||||
bool is_hunter = char_class_is_hunter(char_class);
|
||||
bool is_ranger = char_class_is_ranger(char_class);
|
||||
bool is_force = char_class_is_force(char_class);
|
||||
if (is_force) {
|
||||
table_index += 12;
|
||||
} else if (is_ranger) {
|
||||
@@ -406,77 +406,77 @@ void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fe
|
||||
0x41, 0x3F, 0x41, 0x40, 0x41, 0x40, // Force
|
||||
};
|
||||
// clang-format on
|
||||
mag_item.data.data1[1] = result_table[table_index];
|
||||
mag_item.data1[1] = result_table[table_index];
|
||||
}
|
||||
}
|
||||
|
||||
// If a special evolution did not occur, do a normal level 50 evolution
|
||||
if (mag_number == mag_item.data.data1[1]) {
|
||||
uint16_t flags = mag_item.data.compute_mag_strength_flags();
|
||||
uint16_t def = mag_item.data.data1w[2] / 100;
|
||||
uint16_t pow = mag_item.data.data1w[3] / 100;
|
||||
uint16_t dex = mag_item.data.data1w[4] / 100;
|
||||
uint16_t mind = mag_item.data.data1w[5] / 100;
|
||||
if (mag_number == mag_item.data1[1]) {
|
||||
uint16_t flags = mag_item.compute_mag_strength_flags();
|
||||
uint16_t def = mag_item.data1w[2] / 100;
|
||||
uint16_t pow = mag_item.data1w[3] / 100;
|
||||
uint16_t dex = mag_item.data1w[4] / 100;
|
||||
uint16_t mind = mag_item.data1w[5] / 100;
|
||||
|
||||
bool is_hunter = char_class_is_hunter(player->disp.visual.char_class);
|
||||
bool is_ranger = char_class_is_ranger(player->disp.visual.char_class);
|
||||
bool is_force = char_class_is_force(player->disp.visual.char_class);
|
||||
bool is_hunter = char_class_is_hunter(char_class);
|
||||
bool is_ranger = char_class_is_ranger(char_class);
|
||||
bool is_force = char_class_is_force(char_class);
|
||||
if (is_hunter + is_ranger + is_force != 1) {
|
||||
throw logic_error("char class is not exactly one of the top-level classes");
|
||||
}
|
||||
|
||||
if (is_hunter) {
|
||||
if (flags & 0x108) {
|
||||
mag_item.data.data1[1] = (player->disp.visual.section_id & 1)
|
||||
mag_item.data1[1] = (section_id & 1)
|
||||
? ((dex < mind) ? 0x08 : 0x06)
|
||||
: ((dex < mind) ? 0x0C : 0x05);
|
||||
} else if (flags & 0x010) {
|
||||
mag_item.data.data1[1] = (player->disp.visual.section_id & 1)
|
||||
mag_item.data1[1] = (section_id & 1)
|
||||
? ((mind < pow) ? 0x12 : 0x10)
|
||||
: ((mind < pow) ? 0x17 : 0x13);
|
||||
} else if (flags & 0x020) {
|
||||
mag_item.data.data1[1] = (player->disp.visual.section_id & 1)
|
||||
mag_item.data1[1] = (section_id & 1)
|
||||
? ((pow < dex) ? 0x16 : 0x24)
|
||||
: ((pow < dex) ? 0x07 : 0x1E);
|
||||
}
|
||||
} else if (is_ranger) {
|
||||
if (flags & 0x110) {
|
||||
mag_item.data.data1[1] = (player->disp.visual.section_id & 1)
|
||||
mag_item.data1[1] = (section_id & 1)
|
||||
? ((mind < pow) ? 0x0A : 0x05)
|
||||
: ((mind < pow) ? 0x0C : 0x06);
|
||||
} else if (flags & 0x008) {
|
||||
mag_item.data.data1[1] = (player->disp.visual.section_id & 1)
|
||||
mag_item.data1[1] = (section_id & 1)
|
||||
? ((dex < mind) ? 0x0A : 0x26)
|
||||
: ((dex < mind) ? 0x0C : 0x06);
|
||||
} else if (flags & 0x020) {
|
||||
mag_item.data.data1[1] = (player->disp.visual.section_id & 1)
|
||||
mag_item.data1[1] = (section_id & 1)
|
||||
? ((pow < dex) ? 0x18 : 0x1E)
|
||||
: ((pow < dex) ? 0x08 : 0x05);
|
||||
}
|
||||
} else if (is_force) {
|
||||
if (flags & 0x120) {
|
||||
if (def < 45) {
|
||||
mag_item.data.data1[1] = (player->disp.visual.section_id & 1)
|
||||
mag_item.data1[1] = (section_id & 1)
|
||||
? ((pow < dex) ? 0x17 : 0x09)
|
||||
: ((pow < dex) ? 0x1E : 0x1C);
|
||||
} else {
|
||||
mag_item.data.data1[1] = 0x24;
|
||||
mag_item.data1[1] = 0x24;
|
||||
}
|
||||
} else if (flags & 0x008) {
|
||||
if (def < 45) {
|
||||
mag_item.data.data1[1] = (player->disp.visual.section_id & 1)
|
||||
mag_item.data1[1] = (section_id & 1)
|
||||
? ((dex < mind) ? 0x1C : 0x20)
|
||||
: ((dex < mind) ? 0x1F : 0x25);
|
||||
} else {
|
||||
mag_item.data.data1[1] = 0x23;
|
||||
mag_item.data1[1] = 0x23;
|
||||
}
|
||||
} else if (flags & 0x010) {
|
||||
if (def < 45) {
|
||||
mag_item.data.data1[1] = (player->disp.visual.section_id & 1)
|
||||
mag_item.data1[1] = (section_id & 1)
|
||||
? ((mind < pow) ? 0x12 : 0x0C)
|
||||
: ((mind < pow) ? 0x15 : 0x11);
|
||||
} else {
|
||||
mag_item.data.data1[1] = 0x24;
|
||||
mag_item.data1[1] = 0x24;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -485,8 +485,21 @@ void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fe
|
||||
}
|
||||
|
||||
// If the mag has evolved, add its new photon blast
|
||||
if (mag_number != mag_item.data.data1[1]) {
|
||||
const auto& new_mag_def = item_parameter_table->get_mag(mag_item.data.data1[1]);
|
||||
mag_item.data.add_mag_photon_blast(new_mag_def.photon_blast);
|
||||
if (mag_number != mag_item.data1[1]) {
|
||||
const auto& new_mag_def = item_parameter_table->get_mag(mag_item.data1[1]);
|
||||
mag_item.add_mag_photon_blast(new_mag_def.photon_blast);
|
||||
}
|
||||
}
|
||||
|
||||
void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fed_item_index) {
|
||||
auto s = c->require_server_state();
|
||||
auto player = c->character();
|
||||
apply_mag_feed_result(
|
||||
player->inventory.items[mag_item_index].data,
|
||||
player->inventory.items[fed_item_index].data,
|
||||
s->item_parameter_table(c->version()),
|
||||
s->mag_evolution_table,
|
||||
player->disp.visual.char_class,
|
||||
player->disp.visual.section_id,
|
||||
!is_v1_or_v2(c->version()));
|
||||
}
|
||||
|
||||
+13
-1
@@ -6,8 +6,20 @@
|
||||
#include <random>
|
||||
|
||||
#include "Client.hh"
|
||||
#include "ItemData.hh"
|
||||
#include "ItemParameterTable.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
#include "ServerState.hh"
|
||||
#include "StaticGameData.hh"
|
||||
|
||||
void player_use_item(std::shared_ptr<Client> c, size_t item_index);
|
||||
void player_use_item(std::shared_ptr<Client> c, size_t item_index, std::shared_ptr<PSOLFGEncryption> opt_rand_crypt);
|
||||
void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fed_item_index);
|
||||
|
||||
void apply_mag_feed_result(
|
||||
ItemData& mag_item,
|
||||
const ItemData& fed_item,
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table,
|
||||
std::shared_ptr<const MagEvolutionTable> mag_evolution_table,
|
||||
uint8_t char_class,
|
||||
uint8_t section_id,
|
||||
bool version_has_rare_mags);
|
||||
|
||||
+145
-28
@@ -5,6 +5,7 @@
|
||||
#include <phosg/Filesystem.hh>
|
||||
|
||||
#include "Compression.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
@@ -31,43 +32,159 @@ void PlayerStats::advance_to_level(uint8_t char_class, uint32_t level, shared_pt
|
||||
}
|
||||
}
|
||||
|
||||
LevelTable::LevelTable(shared_ptr<const string> data, bool compressed) {
|
||||
LevelTableV2::LevelTableV2(const string& data, bool compressed) {
|
||||
struct Offsets {
|
||||
// TODO: The overall format of this file on V2 has much more data than we
|
||||
// actually use. What's known of the structure so far:
|
||||
le_uint32_t level_deltas; // -> u32[9] -> LevelStatsDelta[200]
|
||||
le_uint32_t unknown_a1; // -> float[6]
|
||||
le_uint32_t max_stats; // -> PlayerStats[9]
|
||||
le_uint32_t level_100_stats; // -> Level100Entry[9]
|
||||
le_uint32_t base_stats; // -> u32[9] -> CharacterStats
|
||||
le_uint32_t unknown_a2; // -> (0x120 zero bytes)
|
||||
le_uint32_t attack_data; // -> AttackData[9]
|
||||
le_uint32_t unknown_a4; // -> (0x14-byte struct)[9]
|
||||
le_uint32_t unknown_a5; // -> float[9]
|
||||
le_uint32_t unknown_a6; // -> (0x30 bytes)
|
||||
le_uint32_t unknown_a7; // -> (0x2D bytes)
|
||||
le_uint32_t unknown_a8; // -> u32[3] -> float[0x2D]
|
||||
le_uint32_t unknown_a9; // -> (0x90 bytes)
|
||||
le_uint32_t unknown_a10; // -> u32[3] -> (0x10-byte struct)[0x0C]
|
||||
le_uint32_t unknown_a11; // -> u32[3] -> (0x30-bytes)
|
||||
le_uint32_t unknown_a12; // -> u32[3] -> (0x14-byte struct)[0x0F]
|
||||
} __attribute__((packed));
|
||||
|
||||
StringReader r;
|
||||
string decompressed_data;
|
||||
if (compressed) {
|
||||
this->data = make_shared<string>(prs_decompress(*data));
|
||||
decompressed_data = prs_decompress(data);
|
||||
r = StringReader(decompressed_data);
|
||||
} else {
|
||||
this->data = data;
|
||||
r = StringReader(data);
|
||||
}
|
||||
|
||||
if (this->data->size() < sizeof(Table)) {
|
||||
throw invalid_argument("level table size is incorrect");
|
||||
const auto& offsets = r.pget<Offsets>(r.pget_u32l(r.size() - 0x10));
|
||||
const auto& level_deltas_offsets = r.pget<parray<le_uint32_t, 9>>(offsets.level_deltas);
|
||||
const auto& base_stats_offsets = r.pget<parray<le_uint32_t, 9>>(offsets.base_stats);
|
||||
for (size_t char_class = 0; char_class < 9; char_class++) {
|
||||
const auto& src_level_deltas = r.pget<parray<LevelStatsDelta, 200>>(level_deltas_offsets[char_class]);
|
||||
for (size_t level = 0; level < 200; level++) {
|
||||
this->level_deltas[char_class][level] = src_level_deltas[level];
|
||||
}
|
||||
this->max_stats[char_class] = r.pget<PlayerStats>(offsets.max_stats + char_class * sizeof(PlayerStats));
|
||||
this->level_100_stats[char_class] = r.pget<Level100Entry>(offsets.level_100_stats + char_class * sizeof(Level100Entry));
|
||||
this->base_stats[char_class] = r.pget<CharacterStats>(base_stats_offsets[char_class]);
|
||||
}
|
||||
this->table = reinterpret_cast<const Table*>(this->data->data());
|
||||
}
|
||||
|
||||
const CharacterStats& LevelTable::base_stats_for_class(uint8_t char_class) const {
|
||||
if (char_class >= 12) {
|
||||
throw out_of_range("invalid character class");
|
||||
}
|
||||
return this->table->base_stats[char_class];
|
||||
const CharacterStats& LevelTableV2::base_stats_for_class(uint8_t char_class) const {
|
||||
return this->base_stats.at(char_class);
|
||||
}
|
||||
|
||||
const LevelTable::LevelStats& LevelTable::stats_delta_for_level(
|
||||
uint8_t char_class, uint8_t level) const {
|
||||
if (char_class >= 12) {
|
||||
throw invalid_argument("invalid character class");
|
||||
}
|
||||
if (level >= 200) {
|
||||
throw invalid_argument("invalid character level");
|
||||
}
|
||||
return this->table->levels[char_class][level];
|
||||
const LevelTableV2::Level100Entry& LevelTableV2::level_100_stats_for_class(uint8_t char_class) const {
|
||||
return this->level_100_stats.at(char_class);
|
||||
}
|
||||
|
||||
void LevelTable::LevelStats::apply(CharacterStats& ps) const {
|
||||
ps.ata += this->ata;
|
||||
ps.atp += this->atp;
|
||||
ps.dfp += this->dfp;
|
||||
ps.evp += this->evp;
|
||||
ps.hp += this->hp;
|
||||
ps.mst += this->mst;
|
||||
ps.lck += this->lck;
|
||||
const PlayerStats& LevelTableV2::max_stats_for_class(uint8_t char_class) const {
|
||||
return this->max_stats.at(char_class);
|
||||
}
|
||||
|
||||
const LevelStatsDelta& LevelTableV2::stats_delta_for_level(uint8_t char_class, uint8_t level) const {
|
||||
return this->level_deltas.at(char_class).at(level);
|
||||
}
|
||||
|
||||
LevelTableV3BE::LevelTableV3BE(const string& data, bool encrypted) {
|
||||
StringReader r;
|
||||
string decompressed_data;
|
||||
if (encrypted) {
|
||||
auto decrypted = decrypt_pr2_data<true>(data);
|
||||
decompressed_data = prs_decompress(decrypted.compressed_data);
|
||||
if (decompressed_data.size() != decrypted.decompressed_size) {
|
||||
throw runtime_error("decompressed data size does not match expected size");
|
||||
}
|
||||
r = StringReader(decompressed_data);
|
||||
} else {
|
||||
r = StringReader(data);
|
||||
}
|
||||
|
||||
// The GC format is very simple (but everything is big-endian):
|
||||
// root:
|
||||
// u32 offset:
|
||||
// u32[12] offsets:
|
||||
// LevelStatsDeltaBE[200] level_deltas
|
||||
const auto& offsets = r.pget<parray<be_uint32_t, 12>>(r.pget_u32b(r.pget_u32b(r.size() - 0x10)));
|
||||
for (size_t char_class = 0; char_class < 12; char_class++) {
|
||||
const auto& src_deltas = r.pget<parray<LevelStatsDeltaBE, 200>>(offsets[char_class]);
|
||||
for (size_t level = 0; level < 200; level++) {
|
||||
const auto& src_delta = src_deltas[level];
|
||||
auto& dest_delta = this->level_deltas[char_class][level];
|
||||
dest_delta.atp = src_delta.atp;
|
||||
dest_delta.mst = src_delta.mst;
|
||||
dest_delta.evp = src_delta.evp;
|
||||
dest_delta.hp = src_delta.hp;
|
||||
dest_delta.dfp = src_delta.dfp;
|
||||
dest_delta.ata = src_delta.ata;
|
||||
dest_delta.lck = src_delta.lck;
|
||||
dest_delta.tp = src_delta.tp;
|
||||
dest_delta.experience = src_delta.experience.load();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CharacterStats& LevelTableV3BE::base_stats_for_class(uint8_t char_class) const {
|
||||
static const array<CharacterStats, 12> data = {
|
||||
// ATP MST EVP HP DFP ATA LCK
|
||||
CharacterStats{0x0023, 0x001D, 0x002D, 0x0014, 0x0011, 0x001E, 0x000A},
|
||||
CharacterStats{0x001E, 0x0028, 0x003C, 0x0013, 0x0016, 0x0019, 0x000A},
|
||||
CharacterStats{0x0023, 0x0000, 0x0023, 0x0016, 0x0012, 0x0023, 0x000A},
|
||||
CharacterStats{0x0012, 0x0014, 0x0024, 0x0010, 0x000D, 0x0028, 0x000A},
|
||||
CharacterStats{0x0019, 0x0000, 0x001F, 0x0012, 0x0012, 0x002D, 0x000A},
|
||||
CharacterStats{0x0014, 0x0000, 0x001F, 0x0011, 0x0017, 0x002D, 0x000A},
|
||||
CharacterStats{0x000D, 0x0035, 0x0023, 0x0014, 0x000A, 0x000F, 0x000A},
|
||||
CharacterStats{0x000D, 0x003C, 0x0032, 0x0013, 0x0007, 0x000C, 0x000A},
|
||||
CharacterStats{0x000A, 0x003A, 0x0035, 0x0013, 0x000D, 0x000A, 0x000A},
|
||||
CharacterStats{0x0023, 0x0000, 0x0023, 0x0016, 0x0012, 0x0023, 0x000A},
|
||||
CharacterStats{0x000D, 0x0035, 0x0023, 0x0014, 0x000A, 0x000F, 0x000A},
|
||||
CharacterStats{0x0012, 0x0014, 0x0024, 0x0010, 0x000D, 0x0028, 0x000A},
|
||||
};
|
||||
return data.at(char_class);
|
||||
}
|
||||
|
||||
const LevelStatsDelta& LevelTableV3BE::stats_delta_for_level(uint8_t char_class, uint8_t level) const {
|
||||
return this->level_deltas.at(char_class).at(level);
|
||||
}
|
||||
|
||||
LevelTableV4::LevelTableV4(const string& data, bool compressed) {
|
||||
struct Offsets {
|
||||
le_uint32_t base_stats; // -> u32[12] -> CharacterStats
|
||||
le_uint32_t level_deltas; // -> u32[12] -> LevelStatsDelta[200]
|
||||
} __attribute__((packed));
|
||||
|
||||
StringReader r;
|
||||
string decompressed_data;
|
||||
if (compressed) {
|
||||
decompressed_data = prs_decompress(data);
|
||||
r = StringReader(decompressed_data);
|
||||
} else {
|
||||
r = StringReader(data);
|
||||
}
|
||||
|
||||
const auto& offsets = r.pget<Offsets>(r.pget_u32l(r.size() - 0x10));
|
||||
const auto& level_deltas_offsets = r.pget<parray<le_uint32_t, 12>>(offsets.level_deltas);
|
||||
const auto& base_stats_offsets = r.pget<parray<le_uint32_t, 12>>(offsets.base_stats);
|
||||
for (size_t char_class = 0; char_class < 12; char_class++) {
|
||||
const auto& src_level_deltas = r.pget<parray<LevelStatsDelta, 200>>(level_deltas_offsets[char_class]);
|
||||
for (size_t level = 0; level < 200; level++) {
|
||||
this->level_deltas[char_class][level] = src_level_deltas[level];
|
||||
}
|
||||
this->base_stats[char_class] = r.pget<CharacterStats>(base_stats_offsets[char_class]);
|
||||
}
|
||||
}
|
||||
|
||||
const CharacterStats& LevelTableV4::base_stats_for_class(uint8_t char_class) const {
|
||||
return this->base_stats.at(char_class);
|
||||
}
|
||||
|
||||
const LevelStatsDelta& LevelTableV4::stats_delta_for_level(uint8_t char_class, uint8_t level) const {
|
||||
return this->level_deltas.at(char_class).at(level);
|
||||
}
|
||||
|
||||
+98
-63
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <string>
|
||||
@@ -9,18 +10,19 @@
|
||||
class LevelTable;
|
||||
|
||||
struct CharacterStats {
|
||||
le_uint16_t atp = 0;
|
||||
le_uint16_t mst = 0;
|
||||
le_uint16_t evp = 0;
|
||||
le_uint16_t hp = 0;
|
||||
le_uint16_t dfp = 0;
|
||||
le_uint16_t ata = 0;
|
||||
le_uint16_t lck = 0;
|
||||
/* 00 */ le_uint16_t atp = 0;
|
||||
/* 02 */ le_uint16_t mst = 0;
|
||||
/* 04 */ le_uint16_t evp = 0;
|
||||
/* 06 */ le_uint16_t hp = 0;
|
||||
/* 08 */ le_uint16_t dfp = 0;
|
||||
/* 0A */ le_uint16_t ata = 0;
|
||||
/* 0C */ le_uint16_t lck = 0;
|
||||
/* 0E */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerStats {
|
||||
/* 00 */ CharacterStats char_stats;
|
||||
/* 0E */ le_uint16_t unknown_a1 = 0;
|
||||
/* 0E */ le_uint16_t esp = 0;
|
||||
/* 10 */ le_float height = 0.0;
|
||||
/* 14 */ le_float unknown_a3 = 0.0;
|
||||
/* 18 */ le_uint32_t level = 0;
|
||||
@@ -32,66 +34,99 @@ struct PlayerStats {
|
||||
void advance_to_level(uint8_t char_class, uint32_t level, std::shared_ptr<const LevelTable> level_table);
|
||||
} __attribute__((packed));
|
||||
|
||||
class LevelTable { // from PlyLevelTbl.prs
|
||||
template <bool IsBigEndian>
|
||||
struct LevelStatsDeltaBase {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
|
||||
/* 00 */ uint8_t atp;
|
||||
/* 01 */ uint8_t mst;
|
||||
/* 02 */ uint8_t evp;
|
||||
/* 03 */ uint8_t hp;
|
||||
/* 04 */ uint8_t dfp;
|
||||
/* 05 */ uint8_t ata;
|
||||
/* 06 */ uint8_t lck;
|
||||
/* 07 */ uint8_t tp;
|
||||
/* 08 */ U32T experience;
|
||||
/* 0C */
|
||||
|
||||
void apply(CharacterStats& ps) const {
|
||||
ps.ata += this->ata;
|
||||
ps.atp += this->atp;
|
||||
ps.dfp += this->dfp;
|
||||
ps.evp += this->evp;
|
||||
ps.hp += this->hp;
|
||||
ps.mst += this->mst;
|
||||
ps.lck += this->lck;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
|
||||
struct LevelStatsDelta : LevelStatsDeltaBase<false> {
|
||||
} __attribute__((packed));
|
||||
struct LevelStatsDeltaBE : LevelStatsDeltaBase<true> {
|
||||
} __attribute__((packed));
|
||||
|
||||
class LevelTable {
|
||||
// This is the base class for all the LevelTable implementations. The public
|
||||
// interface here only defines functions that the server needs to handle
|
||||
// requests, but some subclasses implement more functionality. See the
|
||||
// comments and Offsets structures inside the subclasses' constructor
|
||||
// implementations for more details on the file formats.
|
||||
public:
|
||||
struct LevelStats {
|
||||
uint8_t atp;
|
||||
uint8_t mst;
|
||||
uint8_t evp;
|
||||
uint8_t hp;
|
||||
uint8_t dfp;
|
||||
uint8_t ata;
|
||||
uint8_t lck;
|
||||
uint8_t tp;
|
||||
le_uint32_t experience;
|
||||
virtual ~LevelTable() = default;
|
||||
virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const = 0;
|
||||
virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const = 0;
|
||||
|
||||
void apply(CharacterStats& ps) const;
|
||||
protected:
|
||||
LevelTable() = default;
|
||||
};
|
||||
|
||||
class LevelTableV2 : public LevelTable { // from PlayerTable.prs (PC)
|
||||
public:
|
||||
struct Level100Entry {
|
||||
/* 00 */ CharacterStats char_stats;
|
||||
/* 0E */ le_uint16_t unknown_a1 = 0;
|
||||
/* 10 */ le_float height = 0.0;
|
||||
/* 14 */ le_float unknown_a3 = 0.0;
|
||||
/* 18 */ le_uint32_t level = 0;
|
||||
/* 1C */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct Table {
|
||||
CharacterStats base_stats[12];
|
||||
le_uint32_t unknown[12];
|
||||
LevelStats levels[12][200];
|
||||
} __attribute__((packed));
|
||||
LevelTableV2(const std::string& data, bool compressed);
|
||||
virtual ~LevelTableV2() = default;
|
||||
|
||||
LevelTable(std::shared_ptr<const std::string> data, bool compressed);
|
||||
|
||||
const CharacterStats& base_stats_for_class(uint8_t char_class) const;
|
||||
const LevelStats& stats_delta_for_level(uint8_t char_class, uint8_t level) const;
|
||||
virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const;
|
||||
const Level100Entry& level_100_stats_for_class(uint8_t char_class) const;
|
||||
const PlayerStats& max_stats_for_class(uint8_t char_class) const;
|
||||
virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const;
|
||||
|
||||
private:
|
||||
// TODO: Currently we only support the BB version of this file. It'd be nice
|
||||
// to support non-BB versions, but their formats are very different:
|
||||
//
|
||||
// BB:
|
||||
// root:
|
||||
// u32 offset:
|
||||
// u32[12] unknown
|
||||
// u32 offset:
|
||||
// u32[12] offsets:
|
||||
// LevelStats[200] level_stats
|
||||
// u32 offset:
|
||||
// CharacterStats[12] base_stats
|
||||
// GC:
|
||||
// root:
|
||||
// u32 offset:
|
||||
// u32[12] offsets:
|
||||
// LevelStats[200] level_stats
|
||||
// PC:
|
||||
// root:
|
||||
// u32 offset:
|
||||
// u32 offset[9]:
|
||||
// LevelStats[200] level_stats
|
||||
// u32 offset:
|
||||
// (0x18 bytes)
|
||||
// u32 offset:
|
||||
// PlayerStats[9] max_stats
|
||||
// u32 offset:
|
||||
// PlayerStats[9] level100_stats
|
||||
// u32 offset:
|
||||
// u32 offset[9]:
|
||||
// CharacterStats level1_stats
|
||||
// (11 more pointers)
|
||||
std::shared_ptr<const std::string> data;
|
||||
const Table* table;
|
||||
std::array<CharacterStats, 9> base_stats;
|
||||
std::array<Level100Entry, 9> level_100_stats;
|
||||
std::array<PlayerStats, 9> max_stats;
|
||||
std::array<std::array<LevelStatsDelta, 200>, 9> level_deltas;
|
||||
};
|
||||
|
||||
class LevelTableV3BE : public LevelTable { // from PlyLevelTbl.cpt (GC)
|
||||
public:
|
||||
LevelTableV3BE(const std::string& data, bool encrypted);
|
||||
virtual ~LevelTableV3BE() = default;
|
||||
|
||||
virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const;
|
||||
virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const;
|
||||
|
||||
private:
|
||||
std::array<std::array<LevelStatsDelta, 200>, 12> level_deltas;
|
||||
};
|
||||
|
||||
class LevelTableV4 : public LevelTable { // from PlyLevelTbl.prs (BB)
|
||||
public:
|
||||
LevelTableV4(const std::string& data, bool compressed);
|
||||
virtual ~LevelTableV4() = default;
|
||||
|
||||
virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const;
|
||||
virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const;
|
||||
|
||||
private:
|
||||
std::array<CharacterStats, 12> base_stats;
|
||||
std::array<std::array<LevelStatsDelta, 200>, 12> level_deltas;
|
||||
};
|
||||
|
||||
+106
-21
@@ -3,6 +3,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Hash.hh>
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "License.hh"
|
||||
@@ -18,6 +19,8 @@ License::License(const JSON& json)
|
||||
bb_team_id(0) {
|
||||
this->serial_number = json.get_int("SerialNumber");
|
||||
this->access_key = json.get_string("AccessKey", "");
|
||||
this->dc_nte_serial_number = json.get_string("DCNTESerialNumber", "");
|
||||
this->dc_nte_access_key = json.get_string("DCNTEAccessKey", "");
|
||||
this->gc_password = json.get_string("GCPassword", "");
|
||||
this->xb_gamertag = json.get_string("XBGamerTag", "");
|
||||
this->xb_user_id = json.get_int("XBUserID", 0);
|
||||
@@ -37,6 +40,8 @@ JSON License::json() const {
|
||||
return JSON::dict({
|
||||
{"SerialNumber", this->serial_number},
|
||||
{"AccessKey", this->access_key},
|
||||
{"DCNTESerialNumber", this->dc_nte_serial_number},
|
||||
{"DCNTEAccessKey", this->dc_nte_access_key},
|
||||
{"GCPassword", this->gc_password},
|
||||
{"XBGamerTag", this->xb_gamertag},
|
||||
{"XBUserID", this->xb_user_id},
|
||||
@@ -62,6 +67,12 @@ string License::str() const {
|
||||
if (!this->access_key.empty()) {
|
||||
tokens.emplace_back("access_key=" + this->access_key);
|
||||
}
|
||||
if (!this->dc_nte_serial_number.empty()) {
|
||||
tokens.emplace_back("dc_nte_serial_number=" + this->dc_nte_serial_number);
|
||||
}
|
||||
if (!this->dc_nte_access_key.empty()) {
|
||||
tokens.emplace_back("dc_nte_access_key=" + this->dc_nte_access_key);
|
||||
}
|
||||
if (!this->gc_password.empty()) {
|
||||
tokens.emplace_back("gc_password=" + this->gc_password);
|
||||
}
|
||||
@@ -127,6 +138,14 @@ shared_ptr<License> LicenseIndex::get(uint32_t serial_number) const {
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::get_by_bb_username(const string& bb_username) const {
|
||||
try {
|
||||
return this->bb_username_to_license.at(bb_username);
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
}
|
||||
}
|
||||
|
||||
vector<shared_ptr<License>> LicenseIndex::all() const {
|
||||
vector<shared_ptr<License>> ret;
|
||||
ret.reserve(this->serial_number_to_license.size());
|
||||
@@ -138,76 +157,119 @@ vector<shared_ptr<License>> LicenseIndex::all() const {
|
||||
|
||||
void LicenseIndex::add(shared_ptr<License> l) {
|
||||
this->serial_number_to_license[l->serial_number] = l;
|
||||
if (!l->bb_username.empty()) {
|
||||
this->bb_username_to_license[l->bb_username] = l;
|
||||
if (!l->dc_nte_serial_number.empty()) {
|
||||
this->dc_nte_serial_number_to_license[l->dc_nte_serial_number] = l;
|
||||
}
|
||||
if (!l->xb_gamertag.empty()) {
|
||||
this->xb_gamertag_to_license[l->xb_gamertag] = l;
|
||||
}
|
||||
if (!l->bb_username.empty()) {
|
||||
this->bb_username_to_license[l->bb_username] = l;
|
||||
}
|
||||
}
|
||||
|
||||
void LicenseIndex::remove(uint32_t serial_number) {
|
||||
auto l = this->serial_number_to_license.at(serial_number);
|
||||
this->serial_number_to_license.erase(l->serial_number);
|
||||
if (!l->bb_username.empty()) {
|
||||
this->bb_username_to_license.erase(l->bb_username);
|
||||
if (!l->dc_nte_serial_number.empty()) {
|
||||
this->dc_nte_serial_number_to_license.erase(l->dc_nte_serial_number);
|
||||
}
|
||||
if (!l->xb_gamertag.empty()) {
|
||||
this->xb_gamertag_to_license.erase(l->xb_gamertag);
|
||||
}
|
||||
if (!l->bb_username.empty()) {
|
||||
this->bb_username_to_license.erase(l->bb_username);
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::verify_v1_v2(uint32_t serial_number, const string& access_key) const {
|
||||
shared_ptr<License> LicenseIndex::verify_dc_nte(const string& serial_number, const string& access_key) const {
|
||||
if (serial_number.empty()) {
|
||||
throw no_username();
|
||||
}
|
||||
try {
|
||||
auto& license = this->dc_nte_serial_number_to_license.at(serial_number);
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
if (license->dc_nte_access_key != access_key) {
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
return license;
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::verify_v1_v2(
|
||||
uint32_t serial_number,
|
||||
const string& access_key,
|
||||
const string& character_name) const {
|
||||
if (serial_number == 0) {
|
||||
throw no_username();
|
||||
}
|
||||
try {
|
||||
auto& license = this->serial_number_to_license.at(serial_number);
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
if (license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
return this->create_temporary_license_for_shared_license(license->flags, serial_number, access_key, "", character_name);
|
||||
}
|
||||
if (license->access_key.compare(0, 8, access_key) != 0) {
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
return license;
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::verify_gc(uint32_t serial_number, const string& access_key) const {
|
||||
shared_ptr<License> LicenseIndex::verify_gc_no_password(
|
||||
uint32_t serial_number,
|
||||
const string& access_key,
|
||||
const string& character_name) const {
|
||||
if (serial_number == 0) {
|
||||
throw no_username();
|
||||
}
|
||||
try {
|
||||
auto& license = this->serial_number_to_license.at(serial_number);
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
if (license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
return this->create_temporary_license_for_shared_license(license->flags, serial_number, access_key, "", character_name);
|
||||
}
|
||||
if (license->access_key != access_key) {
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
return license;
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::verify_gc(uint32_t serial_number, const string& access_key, const string& password) const {
|
||||
shared_ptr<License> LicenseIndex::verify_gc_with_password(
|
||||
uint32_t serial_number,
|
||||
const string& access_key,
|
||||
const string& password,
|
||||
const string& character_name) const {
|
||||
if (serial_number == 0) {
|
||||
throw no_username();
|
||||
}
|
||||
try {
|
||||
auto& license = this->serial_number_to_license.at(serial_number);
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
if (license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
return this->create_temporary_license_for_shared_license(license->flags, serial_number, access_key, password, character_name);
|
||||
}
|
||||
if (license->access_key != access_key) {
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
if (license->gc_password != password) {
|
||||
throw incorrect_password();
|
||||
}
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
return license;
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
@@ -220,15 +282,18 @@ shared_ptr<License> LicenseIndex::verify_xb(const string& gamertag, uint64_t use
|
||||
}
|
||||
try {
|
||||
auto& license = this->xb_gamertag_to_license.at(gamertag);
|
||||
if (license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
throw missing_license(); // XB users cannot use shared serials
|
||||
}
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
if (license->xb_user_id && (license->xb_user_id != user_id)) {
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
if (license->xb_account_id && (license->xb_account_id != account_id)) {
|
||||
throw incorrect_access_key();
|
||||
}
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
return license;
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
@@ -241,18 +306,38 @@ shared_ptr<License> LicenseIndex::verify_bb(const string& username, const string
|
||||
}
|
||||
try {
|
||||
auto& license = this->bb_username_to_license.at(username);
|
||||
if (license->bb_password != password) {
|
||||
throw incorrect_password();
|
||||
if (license->check_flag(License::Flag::IS_SHARED_SERIAL)) {
|
||||
throw missing_license(); // BB users cannot use shared serials
|
||||
}
|
||||
if (license->ban_end_time && (license->ban_end_time >= now())) {
|
||||
throw invalid_argument("user is banned");
|
||||
}
|
||||
if (license->bb_password != password) {
|
||||
throw incorrect_password();
|
||||
}
|
||||
return license;
|
||||
} catch (const out_of_range&) {
|
||||
throw missing_license();
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<License> LicenseIndex::create_temporary_license_for_shared_license(
|
||||
uint32_t base_flags,
|
||||
uint32_t serial_number,
|
||||
const string& access_key,
|
||||
const string& password,
|
||||
const string& character_name) const {
|
||||
uint32_t temp_serial_number = fnv1a32(&serial_number, sizeof(serial_number));
|
||||
temp_serial_number = fnv1a32(access_key, temp_serial_number);
|
||||
temp_serial_number = fnv1a32(password, temp_serial_number);
|
||||
temp_serial_number = fnv1a32(character_name, temp_serial_number);
|
||||
auto ret = this->create_temporary_license();
|
||||
ret->serial_number = temp_serial_number & 0x7FFFFFFF;
|
||||
ret->flags = base_flags;
|
||||
ret->set_flag(License::Flag::IS_SHARED_SERIAL);
|
||||
return ret;
|
||||
}
|
||||
|
||||
DiskLicenseIndex::DiskLicenseIndex() {
|
||||
struct BinaryLicense {
|
||||
pstring<TextEncoding::ASCII, 0x14> username; // BB username (max. 16 chars; should technically be Unicode)
|
||||
|
||||
+60
-21
@@ -12,28 +12,31 @@ class LicenseIndex;
|
||||
|
||||
class License {
|
||||
public:
|
||||
enum Flag : uint32_t {
|
||||
enum class Flag : uint32_t {
|
||||
// clang-format off
|
||||
KICK_USER = 0x00000001,
|
||||
BAN_USER = 0x00000002,
|
||||
SILENCE_USER = 0x00000004,
|
||||
CHANGE_LOBBY_INFO = 0x00000008,
|
||||
CHANGE_EVENT = 0x00000010,
|
||||
ANNOUNCE = 0x00000020,
|
||||
FREE_JOIN_GAMES = 0x00000040,
|
||||
UNLOCK_GAMES = 0x00000080,
|
||||
DEBUG = 0x01000000,
|
||||
CHEAT_ANYWHERE = 0x02000000,
|
||||
DISABLE_QUEST_REQUIREMENTS = 0x04000000,
|
||||
MODERATOR = 0x00000007,
|
||||
ADMINISTRATOR = 0x000000FF,
|
||||
ROOT = 0x7FFFFFFF,
|
||||
|
||||
UNUSED_BITS = 0x78FFFF00,
|
||||
KICK_USER = 0x00000001,
|
||||
BAN_USER = 0x00000002,
|
||||
SILENCE_USER = 0x00000004,
|
||||
CHANGE_EVENT = 0x00000010,
|
||||
ANNOUNCE = 0x00000020,
|
||||
FREE_JOIN_GAMES = 0x00000040,
|
||||
DEBUG = 0x01000000,
|
||||
CHEAT_ANYWHERE = 0x02000000,
|
||||
DISABLE_QUEST_REQUIREMENTS = 0x04000000,
|
||||
ALWAYS_ENABLE_CHAT_COMMANDS = 0x08000000,
|
||||
MODERATOR = 0x00000007,
|
||||
ADMINISTRATOR = 0x000000FF,
|
||||
ROOT = 0x7FFFFFFF,
|
||||
IS_SHARED_SERIAL = 0x80000000,
|
||||
// NOTE: When adding or changing license flags, don't forget to change the
|
||||
// documentation in the shell's help text.
|
||||
UNUSED_BITS = 0x70FFFF00,
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
uint32_t serial_number = 0;
|
||||
std::string dc_nte_serial_number;
|
||||
std::string dc_nte_access_key;
|
||||
std::string access_key;
|
||||
std::string gc_password;
|
||||
std::string xb_gamertag;
|
||||
@@ -60,6 +63,22 @@ public:
|
||||
virtual void save() const;
|
||||
virtual void delete_file() const;
|
||||
|
||||
[[nodiscard]] inline bool check_flag(Flag flag) const {
|
||||
return !!(this->flags & static_cast<uint32_t>(flag));
|
||||
}
|
||||
inline void set_flag(Flag flag) {
|
||||
this->flags |= static_cast<uint32_t>(flag);
|
||||
}
|
||||
inline void clear_flag(Flag flag) {
|
||||
this->flags &= (~static_cast<uint32_t>(flag));
|
||||
}
|
||||
inline void toggle_flag(Flag flag) {
|
||||
this->flags ^= static_cast<uint32_t>(flag);
|
||||
}
|
||||
inline void replace_all_flags(Flag mask) {
|
||||
this->flags = static_cast<uint32_t>(mask);
|
||||
}
|
||||
|
||||
std::string str() const;
|
||||
};
|
||||
|
||||
@@ -100,21 +119,41 @@ public:
|
||||
|
||||
size_t count() const;
|
||||
std::shared_ptr<License> get(uint32_t serial_number) const;
|
||||
std::shared_ptr<License> get_by_bb_username(const std::string& bb_username) const;
|
||||
std::vector<std::shared_ptr<License>> all() const;
|
||||
|
||||
void add(std::shared_ptr<License> l);
|
||||
void remove(uint32_t serial_number);
|
||||
|
||||
std::shared_ptr<License> verify_v1_v2(uint32_t serial_number, const std::string& access_key) const;
|
||||
std::shared_ptr<License> verify_gc(uint32_t serial_number, const std::string& access_key) const;
|
||||
std::shared_ptr<License> verify_gc(uint32_t serial_number, const std::string& access_key, const std::string& password) const;
|
||||
std::shared_ptr<License> verify_dc_nte(const std::string& serial_number, const std::string& access_key) const;
|
||||
std::shared_ptr<License> verify_v1_v2(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& character_name) const;
|
||||
std::shared_ptr<License> verify_gc_no_password(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& character_name) const;
|
||||
std::shared_ptr<License> verify_gc_with_password(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& password,
|
||||
const std::string& character_name) const;
|
||||
std::shared_ptr<License> verify_xb(const std::string& gamertag, uint64_t user_id, uint64_t account_id) const;
|
||||
std::shared_ptr<License> verify_bb(const std::string& username, const std::string& password) const;
|
||||
|
||||
protected:
|
||||
std::unordered_map<std::string, std::shared_ptr<License>> bb_username_to_license;
|
||||
std::unordered_map<std::string, std::shared_ptr<License>> dc_nte_serial_number_to_license;
|
||||
std::unordered_map<std::string, std::shared_ptr<License>> xb_gamertag_to_license;
|
||||
std::unordered_map<std::string, std::shared_ptr<License>> bb_username_to_license;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<License>> serial_number_to_license;
|
||||
|
||||
std::shared_ptr<License> create_temporary_license_for_shared_license(
|
||||
uint32_t base_flags,
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& password,
|
||||
const std::string& character_name) const;
|
||||
};
|
||||
|
||||
class DiskLicenseIndex : public LicenseIndex {
|
||||
|
||||
+314
-131
@@ -123,7 +123,7 @@ void Lobby::FloorItemManager::clear() {
|
||||
}
|
||||
|
||||
uint32_t Lobby::FloorItemManager::reassign_all_item_ids(uint32_t next_item_id) {
|
||||
unordered_map<uint32_t, shared_ptr<FloorItem>> old_items;
|
||||
::map<uint32_t, shared_ptr<FloorItem>> old_items;
|
||||
old_items.swap(this->items);
|
||||
for (auto& queue : this->queue_for_client) {
|
||||
queue.clear();
|
||||
@@ -135,16 +135,16 @@ uint32_t Lobby::FloorItemManager::reassign_all_item_ids(uint32_t next_item_id) {
|
||||
return next_item_id;
|
||||
}
|
||||
|
||||
Lobby::Lobby(shared_ptr<ServerState> s, uint32_t id)
|
||||
Lobby::Lobby(shared_ptr<ServerState> s, uint32_t id, bool is_game)
|
||||
: server_state(s),
|
||||
log(string_printf("[Lobby:%" PRIX32 "] ", id), lobby_log.min_level),
|
||||
log(string_printf("[%s:%" PRIX32 "] ", is_game ? "Game" : "Lobby", id), lobby_log.min_level),
|
||||
lobby_id(id),
|
||||
min_level(0),
|
||||
max_level(0xFFFFFFFF),
|
||||
next_game_item_id(0xCC000000),
|
||||
base_version(Version::GC_V3),
|
||||
allowed_versions(0x0000),
|
||||
section_id(0),
|
||||
override_section_id(0xFF),
|
||||
episode(Episode::NONE),
|
||||
mode(GameMode::NORMAL),
|
||||
difficulty(0),
|
||||
@@ -162,15 +162,24 @@ Lobby::Lobby(shared_ptr<ServerState> s, uint32_t id)
|
||||
event_new(s->base.get(), -1, EV_TIMEOUT | EV_PERSIST, &Lobby::dispatch_on_idle_timeout, this),
|
||||
event_free) {
|
||||
this->log.info("Created");
|
||||
for (size_t x = 0; x < 12; x++) {
|
||||
this->next_item_id_for_client[x] = 0x00010000 + 0x00200000 * x;
|
||||
if (is_game) {
|
||||
this->set_flag(Flag::GAME);
|
||||
}
|
||||
this->reset_next_item_ids();
|
||||
}
|
||||
|
||||
Lobby::~Lobby() {
|
||||
this->log.info("Deleted");
|
||||
}
|
||||
|
||||
void Lobby::reset_next_item_ids() {
|
||||
uint32_t base_item_id = this->is_game() ? 0x00010000 : 0x10010000;
|
||||
for (size_t x = 0; x < 12; x++) {
|
||||
this->next_item_id_for_client[x] = base_item_id + 0x00200000 * x;
|
||||
}
|
||||
this->next_game_item_id = 0xCC000000;
|
||||
}
|
||||
|
||||
shared_ptr<ServerState> Lobby::require_server_state() const {
|
||||
auto s = this->server_state.lock();
|
||||
if (!s) {
|
||||
@@ -206,7 +215,7 @@ void Lobby::create_item_creator() {
|
||||
switch (this->base_version) {
|
||||
case Version::PC_PATCH:
|
||||
case Version::BB_PATCH:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
throw runtime_error("cannot create item creator for this base version");
|
||||
case Version::DC_NTE:
|
||||
@@ -242,19 +251,184 @@ void Lobby::create_item_creator() {
|
||||
s->tool_random_set,
|
||||
s->weapon_random_sets.at(this->difficulty),
|
||||
s->tekker_adjustment_set,
|
||||
s->item_parameter_table_for_version(this->base_version),
|
||||
this->base_version,
|
||||
s->item_parameter_table(this->base_version),
|
||||
s->item_stack_limits(this->base_version),
|
||||
this->episode,
|
||||
(this->mode == GameMode::SOLO) ? GameMode::NORMAL : this->mode,
|
||||
this->difficulty,
|
||||
this->section_id,
|
||||
this->random_seed,
|
||||
this->effective_section_id(),
|
||||
this->opt_rand_crypt,
|
||||
this->quest ? this->quest->battle_rules : nullptr);
|
||||
}
|
||||
|
||||
void Lobby::change_section_id() {
|
||||
if (this->item_creator) {
|
||||
uint8_t new_section_id = this->effective_section_id();
|
||||
if (this->item_creator->get_section_id() != new_section_id) {
|
||||
this->log.info("Changing section ID to %s", name_for_section_id(new_section_id));
|
||||
this->item_creator->set_section_id(new_section_id);
|
||||
for (const auto& c : this->clients) {
|
||||
if (c && c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
send_text_message_printf(c, "$C5Section ID changed\nto %s (%hhu)", name_for_section_id(new_section_id), new_section_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t Lobby::effective_section_id() const {
|
||||
if (this->override_section_id != 0xFF) {
|
||||
return this->override_section_id;
|
||||
}
|
||||
if (this->check_flag(Lobby::Flag::USE_CREATOR_SECTION_ID)) {
|
||||
return this->creator_section_id;
|
||||
}
|
||||
auto leader = this->clients.at(this->leader_id);
|
||||
if (leader) {
|
||||
return leader->character()->disp.visual.section_id;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
shared_ptr<Map> Lobby::load_maps(
|
||||
Version version,
|
||||
Episode episode,
|
||||
uint8_t difficulty,
|
||||
uint8_t event,
|
||||
uint32_t lobby_id,
|
||||
shared_ptr<const Map::RareEnemyRates> rare_rates,
|
||||
uint32_t random_seed,
|
||||
shared_ptr<PSOLFGEncryption> opt_rand_crypt,
|
||||
shared_ptr<const string> quest_dat_contents_decompressed) {
|
||||
auto map = make_shared<Map>(version, lobby_id, random_seed, opt_rand_crypt);
|
||||
map->add_entities_from_quest_data(
|
||||
episode,
|
||||
difficulty,
|
||||
event,
|
||||
quest_dat_contents_decompressed->data(),
|
||||
quest_dat_contents_decompressed->size(),
|
||||
rare_rates);
|
||||
return map;
|
||||
}
|
||||
|
||||
shared_ptr<Map> Lobby::load_maps(
|
||||
Version version,
|
||||
Episode episode,
|
||||
GameMode mode,
|
||||
uint8_t difficulty,
|
||||
uint8_t event,
|
||||
uint32_t lobby_id,
|
||||
shared_ptr<const SetDataTableBase> sdt,
|
||||
function<shared_ptr<const string>(Version, const string&)> get_file_data,
|
||||
shared_ptr<const Map::RareEnemyRates> rare_rates,
|
||||
uint32_t random_seed,
|
||||
shared_ptr<PSOLFGEncryption> opt_rand_crypt,
|
||||
const parray<le_uint32_t, 0x20>& variations,
|
||||
const PrefixedLogger* log) {
|
||||
auto enemy_filenames = sdt->map_filenames_for_variations(variations, episode, mode, SetDataTable::FilenameType::ENEMIES);
|
||||
auto object_filenames = sdt->map_filenames_for_variations(variations, episode, mode, SetDataTable::FilenameType::OBJECTS);
|
||||
auto event_filenames = sdt->map_filenames_for_variations(variations, episode, mode, SetDataTable::FilenameType::EVENTS);
|
||||
return Lobby::load_maps(
|
||||
enemy_filenames,
|
||||
object_filenames,
|
||||
event_filenames,
|
||||
version,
|
||||
episode,
|
||||
mode,
|
||||
difficulty,
|
||||
event,
|
||||
lobby_id,
|
||||
get_file_data,
|
||||
rare_rates,
|
||||
random_seed,
|
||||
opt_rand_crypt,
|
||||
log);
|
||||
}
|
||||
|
||||
shared_ptr<Map> Lobby::load_maps(
|
||||
const vector<string>& enemy_filenames,
|
||||
const vector<string>& object_filenames,
|
||||
const vector<string>& event_filenames,
|
||||
Version version,
|
||||
Episode episode,
|
||||
GameMode mode,
|
||||
uint8_t difficulty,
|
||||
uint8_t event,
|
||||
uint32_t lobby_id,
|
||||
function<shared_ptr<const string>(Version, const string&)> get_file_data,
|
||||
shared_ptr<const Map::RareEnemyRates> rare_rates,
|
||||
uint32_t rare_seed,
|
||||
shared_ptr<PSOLFGEncryption> opt_rand_crypt,
|
||||
const PrefixedLogger* log) {
|
||||
auto map = make_shared<Map>(version, lobby_id, rare_seed, opt_rand_crypt);
|
||||
|
||||
// Don't load free-roam maps in Challenge mode, since players can't go to
|
||||
// Ragol without a quest loaded
|
||||
if (mode == GameMode::CHALLENGE) {
|
||||
return map;
|
||||
}
|
||||
|
||||
for (size_t floor = 0; floor < 0x12; floor++) {
|
||||
const auto& floor_enemy_filename = enemy_filenames.at(floor);
|
||||
if (!floor_enemy_filename.empty()) {
|
||||
auto map_data = get_file_data(version, floor_enemy_filename);
|
||||
if (map_data) {
|
||||
map->add_enemies_from_map_data(
|
||||
episode,
|
||||
difficulty,
|
||||
event,
|
||||
floor,
|
||||
map_data->data(),
|
||||
map_data->size(),
|
||||
rare_rates);
|
||||
if (log) {
|
||||
log->info("Loaded enemies map %s for floor %02zX", floor_enemy_filename.c_str(), floor);
|
||||
}
|
||||
} else if (log) {
|
||||
log->info("Enemies map %s for floor %02zX cannot be used; skipping", floor_enemy_filename.c_str(), floor);
|
||||
}
|
||||
} else if (log) {
|
||||
log->info("No enemies to load for floor %02zX", floor);
|
||||
}
|
||||
|
||||
const auto& floor_object_filename = object_filenames.at(floor);
|
||||
if (!floor_object_filename.empty()) {
|
||||
auto map_data = get_file_data(version, floor_object_filename);
|
||||
if (map_data) {
|
||||
map->add_objects_from_map_data(floor, map_data->data(), map_data->size());
|
||||
if (log) {
|
||||
log->info("Loaded objects map %s for floor %02zX", floor_object_filename.c_str(), floor);
|
||||
}
|
||||
} else if (log) {
|
||||
log->info("Objects map %s for floor %02zX cannot be used; skipping", floor_object_filename.c_str(), floor);
|
||||
}
|
||||
} else if (log) {
|
||||
log->info("No objects to load for floor %02zX", floor);
|
||||
}
|
||||
|
||||
const auto& floor_event_filename = event_filenames.at(floor);
|
||||
if (!floor_event_filename.empty()) {
|
||||
auto map_data = get_file_data(version, floor_event_filename);
|
||||
if (map_data) {
|
||||
map->add_events_from_map_data(floor, map_data->data(), map_data->size());
|
||||
if (log) {
|
||||
log->info("Loaded events map %s for floor %02zX", floor_event_filename.c_str(), floor);
|
||||
}
|
||||
} else if (log) {
|
||||
log->info("Events map %s for floor %02zX cannot be used; skipping", floor_event_filename.c_str(), floor);
|
||||
}
|
||||
} else if (log) {
|
||||
log->info("No events to load for floor %02zX", floor);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
void Lobby::load_maps() {
|
||||
auto s = this->require_server_state();
|
||||
this->map = make_shared<Map>(this->lobby_id);
|
||||
auto rare_rates = ((this->base_version == Version::BB_V4) && this->rare_enemy_rates)
|
||||
? this->rare_enemy_rates
|
||||
: Map::DEFAULT_RARE_ENEMIES;
|
||||
|
||||
if (this->quest) {
|
||||
auto leader_c = this->clients.at(this->leader_id);
|
||||
@@ -262,87 +436,45 @@ void Lobby::load_maps() {
|
||||
throw logic_error("lobby leader is missing");
|
||||
}
|
||||
|
||||
auto vq = this->quest->version(Version::BB_V4, leader_c->language());
|
||||
auto dat_contents = prs_decompress(*vq->dat_contents);
|
||||
this->map->clear();
|
||||
this->map->add_enemies_and_objects_from_quest_data(
|
||||
auto vq = this->quest->version(this->base_version, leader_c->language());
|
||||
if (!vq->dat_contents_decompressed) {
|
||||
throw runtime_error("quest does not have DAT data");
|
||||
}
|
||||
this->map = this->load_maps(
|
||||
this->base_version,
|
||||
this->episode,
|
||||
this->difficulty,
|
||||
this->event,
|
||||
dat_contents.data(),
|
||||
dat_contents.size(),
|
||||
this->lobby_id,
|
||||
rare_rates,
|
||||
this->random_seed,
|
||||
this->rare_enemy_rates ? this->rare_enemy_rates : Map::NO_RARE_ENEMIES);
|
||||
this->opt_rand_crypt,
|
||||
vq->dat_contents_decompressed);
|
||||
|
||||
} else { // No quest loaded
|
||||
for (size_t floor = 0; floor < 0x10; floor++) {
|
||||
this->log.info("[Map/%zu] Using variations %" PRIX32 ", %" PRIX32,
|
||||
floor, this->variations[floor * 2].load(), this->variations[floor * 2 + 1].load());
|
||||
} else if (this->mode != GameMode::CHALLENGE) {
|
||||
auto s = this->require_server_state();
|
||||
this->map = this->load_maps(
|
||||
this->base_version,
|
||||
this->episode,
|
||||
this->mode,
|
||||
this->difficulty,
|
||||
this->event,
|
||||
this->lobby_id,
|
||||
s->set_data_table(this->base_version, this->episode, this->mode, this->difficulty),
|
||||
bind(&ServerState::load_map_file, s.get(), placeholders::_1, placeholders::_2),
|
||||
rare_rates,
|
||||
this->random_seed,
|
||||
this->opt_rand_crypt,
|
||||
this->variations,
|
||||
&this->log);
|
||||
|
||||
auto enemy_filenames = map_filenames_for_variation(
|
||||
this->episode,
|
||||
(this->mode == GameMode::SOLO),
|
||||
floor,
|
||||
this->variations[floor * 2],
|
||||
this->variations[floor * 2 + 1],
|
||||
true);
|
||||
if (enemy_filenames.empty()) {
|
||||
this->log.info("[Map/%zu:e] No file to load", floor);
|
||||
} else {
|
||||
bool any_map_loaded = false;
|
||||
for (const string& filename : enemy_filenames) {
|
||||
try {
|
||||
auto map_data = s->load_bb_file(filename, "", "map/" + filename);
|
||||
this->map->add_enemies_from_map_data(
|
||||
this->episode,
|
||||
this->difficulty,
|
||||
this->event,
|
||||
floor,
|
||||
map_data->data(),
|
||||
map_data->size(),
|
||||
this->rare_enemy_rates);
|
||||
any_map_loaded = true;
|
||||
break;
|
||||
} catch (const exception& e) {
|
||||
this->log.info("[Map/%zu:e] Failed to load %s: %s", floor, filename.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
if (!any_map_loaded) {
|
||||
throw runtime_error(string_printf("no enemy maps loaded for floor %zu", floor));
|
||||
}
|
||||
}
|
||||
|
||||
auto object_filenames = map_filenames_for_variation(
|
||||
this->episode,
|
||||
(this->mode == GameMode::SOLO),
|
||||
floor,
|
||||
this->variations[floor * 2],
|
||||
this->variations[floor * 2 + 1],
|
||||
false);
|
||||
if (object_filenames.empty()) {
|
||||
this->log.info("[Map/%zu:o] No file to load", floor);
|
||||
} else {
|
||||
bool any_map_loaded = false;
|
||||
for (const string& filename : object_filenames) {
|
||||
try {
|
||||
auto map_data = s->load_bb_file(filename, "", "map/" + filename);
|
||||
this->map->add_objects_from_map_data(floor, map_data->data(), map_data->size());
|
||||
any_map_loaded = true;
|
||||
break;
|
||||
} catch (const exception& e) {
|
||||
this->log.info("[Map/%zu:o] Failed to load %s: %s", floor, filename.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
if (!any_map_loaded) {
|
||||
throw runtime_error(string_printf("no object maps loaded for floor %zu", floor));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this->map = make_shared<Map>(this->base_version, this->lobby_id, this->random_seed, this->opt_rand_crypt);
|
||||
}
|
||||
|
||||
this->log.info("Generated objects list (%zu entries):", this->map->objects.size());
|
||||
for (size_t z = 0; z < this->map->objects.size(); z++) {
|
||||
string o_str = this->map->objects[z].str(s->item_name_index);
|
||||
string o_str = this->map->objects[z].str();
|
||||
this->log.info("(K-%zX) %s", z, o_str.c_str());
|
||||
}
|
||||
this->log.info("Generated enemies list (%zu entries):", this->map->enemies.size());
|
||||
@@ -350,12 +482,13 @@ void Lobby::load_maps() {
|
||||
string e_str = this->map->enemies[z].str();
|
||||
this->log.info("(E-%zX) %s", z, e_str.c_str());
|
||||
}
|
||||
this->log.info("Generated events list (%zu entries):", this->map->events.size());
|
||||
for (size_t z = 0; z < this->map->events.size(); z++) {
|
||||
string e_str = this->map->events[z].str();
|
||||
this->log.info("%s", e_str.c_str());
|
||||
}
|
||||
this->log.info("Loaded maps contain %zu object entries and %zu enemy entries overall (%zu as rares)",
|
||||
this->map->objects.size(), this->map->enemies.size(), this->map->rare_enemy_indexes.size());
|
||||
|
||||
if (this->item_creator) {
|
||||
this->item_creator->clear_destroyed_entities();
|
||||
}
|
||||
}
|
||||
|
||||
void Lobby::create_ep3_server() {
|
||||
@@ -366,15 +499,20 @@ void Lobby::create_ep3_server() {
|
||||
this->log.info("Recreating Episode 3 server state");
|
||||
}
|
||||
auto tourn = this->tournament_match ? this->tournament_match->tournament.lock() : nullptr;
|
||||
bool is_trial = this->base_version == Version::GC_EP3_TRIAL_EDITION;
|
||||
bool is_nte = this->base_version == Version::GC_EP3_NTE;
|
||||
Episode3::Server::Options options = {
|
||||
.card_index = is_trial ? s->ep3_card_index_trial : s->ep3_card_index,
|
||||
.card_index = is_nte ? s->ep3_card_index_trial : s->ep3_card_index,
|
||||
.map_index = s->ep3_map_index,
|
||||
.behavior_flags = s->ep3_behavior_flags,
|
||||
.random_crypt = this->random_crypt,
|
||||
.opt_rand_crypt = this->opt_rand_crypt,
|
||||
.tournament = tourn,
|
||||
.trap_card_ids = s->ep3_trap_card_ids,
|
||||
};
|
||||
if (this->base_version == Version::GC_EP3_NTE) {
|
||||
options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION;
|
||||
} else {
|
||||
options.behavior_flags &= (~Episode3::BehaviorFlag::IS_TRIAL_EDITION);
|
||||
}
|
||||
this->ep3_server = make_shared<Episode3::Server>(this->shared_from_this(), std::move(options));
|
||||
this->ep3_server->init();
|
||||
}
|
||||
@@ -384,8 +522,9 @@ void Lobby::reassign_leader_on_client_departure(size_t leaving_client_index) {
|
||||
if (x == leaving_client_index) {
|
||||
continue;
|
||||
}
|
||||
if (this->clients[x].get()) {
|
||||
if (this->clients[x]) {
|
||||
this->leader_id = x;
|
||||
this->change_section_id();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -410,13 +549,22 @@ bool Lobby::any_client_loading() const {
|
||||
size_t Lobby::count_clients() const {
|
||||
size_t ret = 0;
|
||||
for (size_t x = 0; x < this->max_clients; x++) {
|
||||
if (this->clients[x].get()) {
|
||||
if (this->clients[x]) {
|
||||
ret++;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool Lobby::any_v1_clients_present() const {
|
||||
for (size_t x = 0; x < this->max_clients; x++) {
|
||||
if (this->clients[x] && is_v1(this->clients[x]->version())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
|
||||
ssize_t index;
|
||||
ssize_t min_client_id = this->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM) ? 4 : 0;
|
||||
@@ -463,33 +611,33 @@ void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
|
||||
}
|
||||
if (leader_index >= this->max_clients) {
|
||||
this->leader_id = c->lobby_client_id;
|
||||
this->change_section_id();
|
||||
}
|
||||
|
||||
// If the lobby is a game and there was no one in it, reassign all the floor
|
||||
// If this is a lobby or no one was here before this, reassign all the floor
|
||||
// item IDs and reset the next item IDs
|
||||
if (this->is_game()) {
|
||||
if (leader_index >= this->max_clients) {
|
||||
for (size_t x = 0; x < 12; x++) {
|
||||
this->next_item_id_for_client[x] = 0x00010000 + 0x00200000 * x;
|
||||
}
|
||||
this->next_game_item_id = 0xCC000000;
|
||||
if (!this->is_game() || (leader_index >= this->max_clients)) {
|
||||
this->reset_next_item_ids();
|
||||
for (auto& m : this->floor_item_managers) {
|
||||
this->next_game_item_id = m.reassign_all_item_ids(this->next_game_item_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Reassign all floor item IDs so they won't conflict with any players'
|
||||
// item IDs
|
||||
for (auto& m : this->floor_item_managers) {
|
||||
this->next_game_item_id = m.reassign_all_item_ids(this->next_game_item_id);
|
||||
}
|
||||
}
|
||||
// On DC NTE and 11/2000, the game assigns item IDs immediately when a
|
||||
// player joins a game, then assigns them again after the 6x6D equivalent is
|
||||
// received. For this reason, we consume item IDs here only if the client is
|
||||
// NTE or 11/2000.
|
||||
this->assign_inventory_and_bank_item_ids(c, is_pre_v1(c->version()));
|
||||
// On BB, we send artificial flag state to fix an Episode 2 bug where the
|
||||
// CCA door lock state is overwritten by quests.
|
||||
if (c->version() == Version::BB_V4) {
|
||||
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE);
|
||||
}
|
||||
// If this is not a game or the joining client is the leader, they will assign
|
||||
// their item IDs BEFORE they process any inbound commands (therefore a 6x6D
|
||||
// command, which we will send during loading, should reflect the item state
|
||||
// AFTER their IDs are assigned). If the joining client is not the leader,
|
||||
// they will not assign their item IDs until they receive a 6x71 command,
|
||||
// which is sent AFTER the 6x6D command, so the 6x6D should reflect the item
|
||||
// state BEFORE their IDs are assigned. (In the latter case, we'll assign the
|
||||
// IDs for real when they send a 6F command, or 6x1F equivalent in the case of
|
||||
// DC NTE and 11/2000.)
|
||||
this->assign_inventory_and_bank_item_ids(c, (!this->is_game() || (c->lobby_client_id == this->leader_id)));
|
||||
|
||||
// On BB, we send artificial flag state to fix an Episode 2 bug where the
|
||||
// CCA door lock state is overwritten by quests.
|
||||
if (this->is_game() && (c->version() == Version::BB_V4)) {
|
||||
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE);
|
||||
}
|
||||
|
||||
// If the lobby is recording a battle record, add the player join event
|
||||
@@ -637,17 +785,50 @@ shared_ptr<Client> Lobby::find_client(const string* identifier, uint64_t serial_
|
||||
throw out_of_range("client not found");
|
||||
}
|
||||
|
||||
uint8_t Lobby::game_event_for_lobby_event(uint8_t lobby_event) {
|
||||
if (lobby_event > 7) {
|
||||
return 0;
|
||||
Lobby::JoinError Lobby::join_error_for_client(std::shared_ptr<Client> c, const std::string* password) const {
|
||||
if (this->count_clients() >= this->max_clients) {
|
||||
return JoinError::FULL;
|
||||
}
|
||||
if (lobby_event == 7) {
|
||||
return 2;
|
||||
if (!this->version_is_allowed(c->version()) && !c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
return JoinError::VERSION_CONFLICT;
|
||||
}
|
||||
if (lobby_event == 2) {
|
||||
return 0;
|
||||
if (this->is_game()) {
|
||||
if (this->check_flag(Flag::QUEST_IN_PROGRESS)) {
|
||||
return JoinError::QUEST_IN_PROGRESS;
|
||||
}
|
||||
if (this->check_flag(Flag::BATTLE_IN_PROGRESS)) {
|
||||
return JoinError::BATTLE_IN_PROGRESS;
|
||||
}
|
||||
if (this->mode == GameMode::SOLO) {
|
||||
return JoinError::SOLO;
|
||||
}
|
||||
if (!c->license->check_flag(License::Flag::FREE_JOIN_GAMES)) {
|
||||
if (password && !this->password.empty() && (*password != this->password)) {
|
||||
return JoinError::INCORRECT_PASSWORD;
|
||||
}
|
||||
auto p = c->character();
|
||||
if (p->disp.stats.level < this->min_level) {
|
||||
return JoinError::LEVEL_TOO_LOW;
|
||||
}
|
||||
if (p->disp.stats.level > this->max_level) {
|
||||
return JoinError::LEVEL_TOO_HIGH;
|
||||
}
|
||||
if (this->quest) {
|
||||
size_t num_clients = this->count_clients() + 1;
|
||||
bool v1_present = is_v1(c->version()) || this->any_v1_clients_present();
|
||||
if (!c->can_see_quest(this->quest, this->event, this->difficulty, num_clients, v1_present) ||
|
||||
!c->can_play_quest(this->quest, this->event, this->difficulty, num_clients, v1_present)) {
|
||||
return JoinError::NO_ACCESS_TO_QUEST;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only prevent joining during loading if the client is actually trying to
|
||||
// join (not just loading the game list)
|
||||
if (password && this->any_client_loading()) {
|
||||
return JoinError::LOADING;
|
||||
}
|
||||
}
|
||||
return lobby_event;
|
||||
return JoinError::ALLOWED;
|
||||
}
|
||||
|
||||
bool Lobby::item_exists(uint8_t floor, uint32_t item_id) const {
|
||||
@@ -713,19 +894,20 @@ void Lobby::on_item_id_generated_externally(uint32_t item_id) {
|
||||
|
||||
void Lobby::assign_inventory_and_bank_item_ids(shared_ptr<Client> c, bool consume_ids) {
|
||||
auto p = c->character();
|
||||
uint32_t start_item_id = this->next_item_id_for_client[c->lobby_client_id];
|
||||
uint32_t orig_next_item_id = this->next_item_id_for_client.at(c->lobby_client_id);
|
||||
for (size_t z = 0; z < p->inventory.num_items; z++) {
|
||||
p->inventory.items[z].data.id = this->generate_item_id(c->lobby_client_id);
|
||||
}
|
||||
if (!consume_ids) {
|
||||
this->next_item_id_for_client[c->lobby_client_id] = start_item_id;
|
||||
this->next_item_id_for_client[c->lobby_client_id] = orig_next_item_id;
|
||||
}
|
||||
|
||||
if (c->log.info("Assigned inventory item IDs%s", consume_ids ? "" : " but did not mark IDs as used")) {
|
||||
p->print_inventory(stderr, c->version(), c->require_server_state()->item_name_index);
|
||||
c->print_inventory(stderr);
|
||||
if (p->bank.num_items) {
|
||||
p->bank.assign_ids(0x99000000 + (c->lobby_client_id << 20));
|
||||
c->log.info("Assigned bank item IDs");
|
||||
p->print_bank(stderr, c->version(), c->require_server_state()->item_name_index);
|
||||
c->print_bank(stderr);
|
||||
} else {
|
||||
c->log.info("Bank is empty");
|
||||
}
|
||||
@@ -744,13 +926,14 @@ unordered_map<uint32_t, shared_ptr<Client>> Lobby::clients_by_serial_number() co
|
||||
|
||||
QuestIndex::IncludeCondition Lobby::quest_include_condition() const {
|
||||
size_t num_players = this->count_clients();
|
||||
return [this, num_players](shared_ptr<const Quest> q) -> QuestIndex::IncludeState {
|
||||
bool v1_present = this->any_v1_clients_present();
|
||||
return [this, num_players, v1_present](shared_ptr<const Quest> q) -> QuestIndex::IncludeState {
|
||||
bool is_enabled = true;
|
||||
for (const auto& lc : this->clients) {
|
||||
if (lc && !lc->can_see_quest(q, this->difficulty, num_players)) {
|
||||
if (lc && !lc->can_see_quest(q, this->event, this->difficulty, num_players, v1_present)) {
|
||||
return QuestIndex::IncludeState::HIDDEN;
|
||||
}
|
||||
if (lc && !lc->can_play_quest(q, this->difficulty, num_players)) {
|
||||
if (lc && !lc->can_play_quest(q, this->event, this->difficulty, num_players, v1_present)) {
|
||||
is_enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
+85
-24
@@ -36,7 +36,9 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
struct FloorItemManager {
|
||||
PrefixedLogger log;
|
||||
uint64_t next_drop_number;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<FloorItem>> items; // Keyed on item_id
|
||||
// It's important that this is a map and not an unordered_map. See the
|
||||
// comment in send_game_item_state for more details.
|
||||
std::map<uint32_t, std::shared_ptr<FloorItem>> items; // Keyed on item_id
|
||||
std::array<std::map<uint64_t, std::shared_ptr<FloorItem>>, 12> queue_for_client;
|
||||
|
||||
FloorItemManager(uint32_t lobby_id, uint8_t floor);
|
||||
@@ -54,23 +56,24 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
uint32_t reassign_all_item_ids(uint32_t next_item_id);
|
||||
};
|
||||
enum class Flag {
|
||||
GAME = 0x00000001,
|
||||
PERSISTENT = 0x00000002,
|
||||
|
||||
// clang-format off
|
||||
GAME = 0x00000001,
|
||||
PERSISTENT = 0x00000002,
|
||||
// Flags used only for games
|
||||
CHEATS_ENABLED = 0x00000100,
|
||||
QUEST_IN_PROGRESS = 0x00000200,
|
||||
BATTLE_IN_PROGRESS = 0x00000400,
|
||||
JOINABLE_QUEST_IN_PROGRESS = 0x00000800,
|
||||
IS_SPECTATOR_TEAM = 0x00002000, // episode must be EP3 also
|
||||
SPECTATORS_FORBIDDEN = 0x00004000,
|
||||
CHEATS_ENABLED = 0x00000100,
|
||||
QUEST_IN_PROGRESS = 0x00000200,
|
||||
BATTLE_IN_PROGRESS = 0x00000400,
|
||||
JOINABLE_QUEST_IN_PROGRESS = 0x00000800,
|
||||
IS_SPECTATOR_TEAM = 0x00002000, // episode must be EP3 also
|
||||
SPECTATORS_FORBIDDEN = 0x00004000,
|
||||
START_BATTLE_PLAYER_IMMEDIATELY = 0x00008000,
|
||||
CANNOT_CHANGE_CHEAT_MODE = 0x00010000,
|
||||
|
||||
CANNOT_CHANGE_CHEAT_MODE = 0x00010000,
|
||||
USE_CREATOR_SECTION_ID = 0x00020000,
|
||||
// Flags used only for lobbies
|
||||
PUBLIC = 0x01000000,
|
||||
DEFAULT = 0x02000000,
|
||||
IS_OVERFLOW = 0x08000000,
|
||||
PUBLIC = 0x01000000,
|
||||
DEFAULT = 0x02000000,
|
||||
IS_OVERFLOW = 0x08000000,
|
||||
// clang-format on
|
||||
};
|
||||
enum class DropMode {
|
||||
DISABLED = 0,
|
||||
@@ -89,15 +92,15 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
uint32_t min_level;
|
||||
uint32_t max_level;
|
||||
|
||||
// Item state
|
||||
// Game state
|
||||
std::array<uint32_t, 12> next_item_id_for_client;
|
||||
uint32_t next_game_item_id;
|
||||
std::vector<FloorItemManager> floor_item_managers;
|
||||
|
||||
// Map state
|
||||
std::shared_ptr<const Map::RareEnemyRates> rare_enemy_rates;
|
||||
std::shared_ptr<Map> map;
|
||||
parray<le_uint32_t, 0x20> variations;
|
||||
std::unique_ptr<QuestFlags> quest_flags_known; // If null, ALL quest flags are known
|
||||
std::unique_ptr<QuestFlags> quest_flag_values;
|
||||
|
||||
// Game config
|
||||
Version base_version;
|
||||
@@ -105,7 +108,8 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
// bits are indexed as (1 << version), where version is a value from the
|
||||
// Version enum.
|
||||
uint16_t allowed_versions;
|
||||
uint8_t section_id;
|
||||
uint8_t creator_section_id;
|
||||
uint8_t override_section_id;
|
||||
Episode episode;
|
||||
GameMode mode;
|
||||
uint8_t difficulty; // 0-3
|
||||
@@ -115,7 +119,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
std::string name;
|
||||
// This seed is also sent to the client for rare enemy generation
|
||||
uint32_t random_seed;
|
||||
std::shared_ptr<PSOLFGEncryption> random_crypt;
|
||||
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt;
|
||||
uint8_t allowed_drop_modes;
|
||||
DropMode drop_mode;
|
||||
std::shared_ptr<ItemCreator> item_creator;
|
||||
@@ -149,7 +153,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
std::shared_ptr<Episode3::BattleRecord> battle_record; // Not used in watcher games
|
||||
std::shared_ptr<Episode3::BattleRecordPlayer> battle_player; // Only used in replay games
|
||||
std::shared_ptr<Episode3::Tournament::Match> tournament_match;
|
||||
std::shared_ptr<const G_SetEXResultValues_GC_Ep3_6xB4x4B> ep3_ex_result_values;
|
||||
std::shared_ptr<const G_SetEXResultValues_Ep3_6xB4x4B> ep3_ex_result_values;
|
||||
|
||||
// Lobby stuff
|
||||
uint8_t event;
|
||||
@@ -167,13 +171,15 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
uint64_t idle_timeout_usecs;
|
||||
std::unique_ptr<struct event, void (*)(struct event*)> idle_timeout_event;
|
||||
|
||||
Lobby(std::shared_ptr<ServerState> s, uint32_t id);
|
||||
Lobby(std::shared_ptr<ServerState> s, uint32_t id, bool is_game);
|
||||
Lobby(const Lobby&) = delete;
|
||||
Lobby(Lobby&&) = delete;
|
||||
~Lobby();
|
||||
Lobby& operator=(const Lobby&) = delete;
|
||||
Lobby& operator=(Lobby&&) = delete;
|
||||
|
||||
void reset_next_item_ids();
|
||||
|
||||
[[nodiscard]] inline bool check_flag(Flag flag) const {
|
||||
return !!(this->enabled_flags & static_cast<uint32_t>(flag));
|
||||
}
|
||||
@@ -191,6 +197,47 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
std::shared_ptr<ChallengeParameters> require_challenge_params() const;
|
||||
void set_drop_mode(DropMode new_mode);
|
||||
void create_item_creator();
|
||||
void change_section_id();
|
||||
uint8_t effective_section_id() const;
|
||||
static std::shared_ptr<Map> load_maps(
|
||||
Version version,
|
||||
Episode episode,
|
||||
uint8_t difficulty,
|
||||
uint8_t event,
|
||||
uint32_t lobby_id,
|
||||
std::shared_ptr<const Map::RareEnemyRates> rare_rates,
|
||||
uint32_t random_seed,
|
||||
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt,
|
||||
std::shared_ptr<const std::string> quest_dat_contents_decompressed);
|
||||
static std::shared_ptr<Map> load_maps(
|
||||
Version version,
|
||||
Episode episode,
|
||||
GameMode mode,
|
||||
uint8_t difficulty,
|
||||
uint8_t event,
|
||||
uint32_t lobby_id,
|
||||
std::shared_ptr<const SetDataTableBase> sdt,
|
||||
std::function<std::shared_ptr<const std::string>(Version, const std::string&)> get_file_data,
|
||||
std::shared_ptr<const Map::RareEnemyRates> rare_rates,
|
||||
uint32_t random_seed,
|
||||
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt,
|
||||
const parray<le_uint32_t, 0x20>& variations,
|
||||
const PrefixedLogger* log = nullptr);
|
||||
static std::shared_ptr<Map> load_maps(
|
||||
const std::vector<std::string>& enemy_filenames,
|
||||
const std::vector<std::string>& object_filenames,
|
||||
const std::vector<std::string>& event_filenames,
|
||||
Version version,
|
||||
Episode episode,
|
||||
GameMode mode,
|
||||
uint8_t difficulty,
|
||||
uint8_t event,
|
||||
uint32_t lobby_id,
|
||||
std::function<std::shared_ptr<const std::string>(Version, const std::string&)> get_file_data,
|
||||
std::shared_ptr<const Map::RareEnemyRates> rare_rates,
|
||||
uint32_t random_seed,
|
||||
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt,
|
||||
const PrefixedLogger* log = nullptr);
|
||||
void load_maps();
|
||||
void create_ep3_server();
|
||||
|
||||
@@ -210,6 +257,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
|
||||
void reassign_leader_on_client_departure(size_t leaving_client_id);
|
||||
size_t count_clients() const;
|
||||
bool any_v1_clients_present() const;
|
||||
bool any_client_loading() const;
|
||||
|
||||
void add_client(std::shared_ptr<Client> c, ssize_t required_client_id = -1);
|
||||
@@ -224,6 +272,21 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
const std::string* identifier = nullptr,
|
||||
uint64_t serial_number = 0);
|
||||
|
||||
enum class JoinError {
|
||||
ALLOWED = 0,
|
||||
FULL,
|
||||
VERSION_CONFLICT,
|
||||
QUEST_IN_PROGRESS,
|
||||
BATTLE_IN_PROGRESS,
|
||||
LOADING,
|
||||
SOLO,
|
||||
INCORRECT_PASSWORD,
|
||||
LEVEL_TOO_LOW,
|
||||
LEVEL_TOO_HIGH,
|
||||
NO_ACCESS_TO_QUEST,
|
||||
};
|
||||
JoinError join_error_for_client(std::shared_ptr<Client> c, const std::string* password) const;
|
||||
|
||||
bool item_exists(uint8_t floor, uint32_t item_id) const;
|
||||
std::shared_ptr<FloorItem> find_item(uint8_t floor, uint32_t item_id) const;
|
||||
void add_item(uint8_t floor, const ItemData& item, float x, float z, uint16_t visibility_flags);
|
||||
@@ -237,8 +300,6 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
|
||||
QuestIndex::IncludeCondition quest_include_condition() const;
|
||||
|
||||
static uint8_t game_event_for_lobby_event(uint8_t lobby_event);
|
||||
|
||||
std::unordered_map<uint32_t, std::shared_ptr<Client>> clients_by_serial_number() const;
|
||||
|
||||
static void dispatch_on_idle_timeout(evutil_socket_t, short, void* ctx);
|
||||
|
||||
+839
-181
File diff suppressed because it is too large
Load Diff
+1209
-139
File diff suppressed because it is too large
Load Diff
+128
-32
@@ -10,12 +10,13 @@
|
||||
#include <vector>
|
||||
|
||||
#include "BattleParamsIndex.hh"
|
||||
#include "ItemNameIndex.hh"
|
||||
#include "PSOEncryption.hh"
|
||||
#include "StaticGameData.hh"
|
||||
#include "Text.hh"
|
||||
|
||||
struct Map {
|
||||
static const char* name_for_object_type(uint16_t type);
|
||||
|
||||
struct SectionHeader {
|
||||
enum class Type {
|
||||
END = 0,
|
||||
@@ -93,7 +94,7 @@ struct Map {
|
||||
} __attribute__((packed));
|
||||
|
||||
struct EventsSectionHeader { // Section type 3 (WAVE_EVENTS)
|
||||
/* 00 */ le_uint32_t footer_offset;
|
||||
/* 00 */ le_uint32_t action_stream_offset;
|
||||
/* 04 */ le_uint32_t entries_offset;
|
||||
/* 08 */ le_uint32_t entry_count;
|
||||
/* 0C */ be_uint32_t format; // 0 or 'evt2'
|
||||
@@ -103,18 +104,18 @@ struct Map {
|
||||
struct Event1Entry { // Section type 3 (WAVE_EVENTS) if format == 0
|
||||
/* 00 */ le_uint32_t event_id;
|
||||
/* 04 */ le_uint16_t flags;
|
||||
/* 06 */ le_uint16_t unknown_a2;
|
||||
/* 06 */ le_uint16_t event_type;
|
||||
/* 08 */ le_uint16_t section;
|
||||
/* 0A */ le_uint16_t wave_number;
|
||||
/* 0C */ le_uint32_t delay;
|
||||
/* 10 */ le_uint32_t clear_events_index;
|
||||
/* 10 */ le_uint32_t action_stream_offset;
|
||||
/* 14 */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct Event2Entry { // Section type 3 (WAVE_EVENTS) if format == 'evt2'
|
||||
/* 00 */ le_uint32_t event_id;
|
||||
/* 04 */ le_uint16_t flags;
|
||||
/* 06 */ le_uint16_t unknown_a2;
|
||||
/* 06 */ le_uint16_t event_type;
|
||||
/* 08 */ le_uint16_t section;
|
||||
/* 0A */ le_uint16_t wave_number;
|
||||
/* 0C */ le_uint16_t min_delay;
|
||||
@@ -122,7 +123,7 @@ struct Map {
|
||||
/* 10 */ uint8_t min_enemies;
|
||||
/* 11 */ uint8_t max_enemies;
|
||||
/* 12 */ le_uint16_t max_waves;
|
||||
/* 14 */ le_uint32_t clear_events_index;
|
||||
/* 14 */ le_uint32_t action_stream_offset;
|
||||
/* 18 */
|
||||
} __attribute__((packed));
|
||||
|
||||
@@ -207,6 +208,8 @@ struct Map {
|
||||
// TODO: Add more fields in here if we ever care about them. Currently we
|
||||
// only care about boxes with fixed item drops.
|
||||
size_t source_index;
|
||||
uint16_t object_id;
|
||||
uint8_t floor;
|
||||
uint16_t base_type;
|
||||
uint16_t section;
|
||||
float param1; // If <= 0, this is a specialized box, and the specialization is in param4/5/6
|
||||
@@ -214,10 +217,13 @@ struct Map {
|
||||
uint32_t param4;
|
||||
uint32_t param5;
|
||||
uint32_t param6;
|
||||
uint8_t floor;
|
||||
uint16_t game_flags;
|
||||
// Technically set_flags shouldn't be part of the Object struct, but all
|
||||
// object entries always generate exactly one object, so we store it here.
|
||||
uint16_t set_flags;
|
||||
bool item_drop_checked;
|
||||
|
||||
std::string str(std::shared_ptr<const ItemNameIndex> name_index) const;
|
||||
std::string str() const;
|
||||
};
|
||||
|
||||
struct Enemy {
|
||||
@@ -229,12 +235,24 @@ struct Map {
|
||||
ITEM_DROPPED = 0x10,
|
||||
};
|
||||
size_t source_index;
|
||||
size_t set_index;
|
||||
uint16_t enemy_id;
|
||||
uint16_t total_damage;
|
||||
uint32_t game_flags; // From 6x0A
|
||||
EnemyType type;
|
||||
uint8_t floor;
|
||||
uint8_t state_flags;
|
||||
uint8_t last_hit_by_client_id;
|
||||
|
||||
Enemy(size_t source_index, uint8_t floor, EnemyType type);
|
||||
Enemy(uint16_t enemy_id, size_t source_index, size_t set_index, uint8_t floor, EnemyType type);
|
||||
|
||||
std::string str() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct Event {
|
||||
uint32_t event_id;
|
||||
uint16_t flags;
|
||||
uint8_t floor;
|
||||
uint32_t action_stream_offset;
|
||||
|
||||
std::string str() const;
|
||||
} __attribute__((packed));
|
||||
@@ -253,7 +271,7 @@ struct Map {
|
||||
void generate_shuffled_location_table(const Map::RandomEnemyLocationsHeader& header, StringReader r, uint16_t section);
|
||||
};
|
||||
|
||||
explicit Map(uint32_t lobby_id);
|
||||
Map(Version version, uint32_t lobby_id, uint32_t rare_seed, std::shared_ptr<PSOLFGEncryption> opt_rand_crypt);
|
||||
~Map() = default;
|
||||
|
||||
void clear();
|
||||
@@ -288,6 +306,11 @@ struct Map {
|
||||
std::shared_ptr<DATParserRandomState> random_state,
|
||||
std::shared_ptr<const RareEnemyRates> rare_rates = DEFAULT_RARE_ENEMIES);
|
||||
|
||||
void add_event(uint32_t event_id, uint16_t flags, uint8_t floor, uint32_t action_stream_offset);
|
||||
Event& get_event(uint8_t floor, uint32_t event_id);
|
||||
const Event& get_event(uint8_t floor, uint32_t event_id) const;
|
||||
void add_events_from_map_data(uint8_t floor, const void* data, size_t size);
|
||||
|
||||
struct DATSectionsForFloor {
|
||||
uint32_t objects = 0xFFFFFFFF;
|
||||
uint32_t enemies = 0xFFFFFFFF;
|
||||
@@ -297,59 +320,132 @@ struct Map {
|
||||
};
|
||||
static std::vector<DATSectionsForFloor> collect_quest_map_data_sections(const void* data, size_t size);
|
||||
|
||||
void add_enemies_and_objects_from_quest_data(
|
||||
void add_entities_from_quest_data(
|
||||
Episode episode,
|
||||
uint8_t difficulty,
|
||||
uint8_t event,
|
||||
const void* data,
|
||||
size_t size,
|
||||
uint32_t rare_seed,
|
||||
std::shared_ptr<const RareEnemyRates> rare_rates = Map::DEFAULT_RARE_ENEMIES);
|
||||
|
||||
const Enemy& find_enemy(uint8_t floor, EnemyType type) const;
|
||||
Enemy& find_enemy(uint8_t floor, EnemyType type);
|
||||
|
||||
static std::string disassemble_quest_data(const void* data, size_t size);
|
||||
|
||||
PrefixedLogger log;
|
||||
Version version;
|
||||
uint32_t rare_seed;
|
||||
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt;
|
||||
std::vector<Object> objects;
|
||||
std::vector<Enemy> enemies;
|
||||
std::vector<uint16_t> enemy_set_flags;
|
||||
std::vector<size_t> rare_enemy_indexes;
|
||||
std::vector<Event> events;
|
||||
std::string event_action_stream;
|
||||
std::unordered_map<uint64_t, size_t> floor_and_event_id_to_index;
|
||||
};
|
||||
|
||||
// TODO: This class is currently unused. It would be nice if we could use this
|
||||
// to generate variations and link to the corresponding map filenames, but it
|
||||
// seems that SetDataTable.rel files link to map filenames that don't actually
|
||||
// exist in some cases, so we can't just directly use this data structure.
|
||||
class SetDataTable {
|
||||
class SetDataTableBase {
|
||||
public:
|
||||
virtual ~SetDataTableBase() = default;
|
||||
|
||||
parray<le_uint32_t, 0x20> generate_variations(
|
||||
Episode episode,
|
||||
bool is_solo,
|
||||
std::shared_ptr<PSOLFGEncryption> opt_rand_crypt = nullptr) const;
|
||||
virtual std::pair<uint32_t, uint32_t> num_available_variations_for_floor(Episode episode, uint8_t floor) const = 0;
|
||||
virtual std::pair<uint32_t, uint32_t> num_free_roam_variations_for_floor(Episode episode, bool is_solo, uint8_t floor) const = 0;
|
||||
|
||||
enum class FilenameType {
|
||||
OBJECTS = 0,
|
||||
ENEMIES,
|
||||
EVENTS,
|
||||
};
|
||||
|
||||
virtual std::string map_filename_for_variation(
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, FilenameType type) const = 0;
|
||||
std::vector<std::string> map_filenames_for_variations(
|
||||
const parray<le_uint32_t, 0x20>& variations, Episode episode, GameMode mode, FilenameType type) const;
|
||||
|
||||
uint8_t default_area_for_floor(Episode episode, uint8_t floor) const;
|
||||
|
||||
protected:
|
||||
explicit SetDataTableBase(Version version);
|
||||
|
||||
Version version;
|
||||
};
|
||||
|
||||
class SetDataTable : public SetDataTableBase {
|
||||
public:
|
||||
struct SetEntry {
|
||||
std::string object_list_basename;
|
||||
std::string enemy_list_basename;
|
||||
std::string event_list_basename;
|
||||
std::string enemy_and_event_list_basename;
|
||||
std::string area_setup_filename;
|
||||
};
|
||||
|
||||
SetDataTable(std::shared_ptr<const std::string> data, bool big_endian);
|
||||
SetDataTable(Version version, const std::string& data);
|
||||
virtual ~SetDataTable() = default;
|
||||
|
||||
inline const std::vector<std::vector<std::vector<SetEntry>>> get() const {
|
||||
return this->entries;
|
||||
}
|
||||
virtual std::pair<uint32_t, uint32_t> num_available_variations_for_floor(Episode episode, uint8_t floor) const;
|
||||
virtual std::pair<uint32_t, uint32_t> num_free_roam_variations_for_floor(Episode episode, bool is_solo, uint8_t floor) const;
|
||||
virtual std::string map_filename_for_variation(
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, FilenameType type) const;
|
||||
|
||||
void print(FILE* stream) const;
|
||||
std::string str() const;
|
||||
|
||||
private:
|
||||
template <bool IsBigEndian>
|
||||
void load_table_t(std::shared_ptr<const std::string> data);
|
||||
void load_table_t(const std::string& data);
|
||||
|
||||
// Indexes are [floor][variation1][variation2]
|
||||
// floor is cumulative per episode, so Ep2 starts at floor=18.
|
||||
std::vector<std::vector<std::vector<SetEntry>>> entries;
|
||||
};
|
||||
|
||||
void generate_variations(
|
||||
class SetDataTableDCNTE : public SetDataTableBase {
|
||||
public:
|
||||
SetDataTableDCNTE();
|
||||
virtual ~SetDataTableDCNTE() = default;
|
||||
|
||||
virtual std::pair<uint32_t, uint32_t> num_available_variations_for_floor(Episode episode, uint8_t floor) const;
|
||||
virtual std::pair<uint32_t, uint32_t> num_free_roam_variations_for_floor(Episode episode, bool is_solo, uint8_t floor) const;
|
||||
virtual std::string map_filename_for_variation(
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, FilenameType type) const;
|
||||
|
||||
private:
|
||||
static const std::array<std::vector<std::vector<std::string>>, 0x12> NAMES;
|
||||
};
|
||||
|
||||
class SetDataTableDC112000 : public SetDataTableBase {
|
||||
public:
|
||||
SetDataTableDC112000();
|
||||
virtual ~SetDataTableDC112000() = default;
|
||||
|
||||
virtual std::pair<uint32_t, uint32_t> num_available_variations_for_floor(Episode episode, uint8_t floor) const;
|
||||
virtual std::pair<uint32_t, uint32_t> num_free_roam_variations_for_floor(Episode episode, bool is_solo, uint8_t floor) const;
|
||||
virtual std::string map_filename_for_variation(
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Episode episode, GameMode mode, FilenameType type) const;
|
||||
|
||||
private:
|
||||
static const std::array<std::vector<std::vector<std::string>>, 0x12> NAMES;
|
||||
};
|
||||
|
||||
void generate_variations_deprecated(
|
||||
parray<le_uint32_t, 0x20>& variations,
|
||||
std::shared_ptr<PSOLFGEncryption> random,
|
||||
Version version,
|
||||
Episode episode,
|
||||
bool is_solo);
|
||||
void generate_variations_dc_nte(
|
||||
parray<le_uint32_t, 0x20>& variations,
|
||||
std::shared_ptr<PSOLFGEncryption> random);
|
||||
std::vector<std::string> map_filenames_for_variation(
|
||||
Episode episode, bool is_solo, uint8_t floor, uint32_t var1, uint32_t var2, bool is_enemies);
|
||||
|
||||
parray<le_uint32_t, 0x20> variation_maxes_deprecated(Version version, Episode episode, bool is_solo);
|
||||
bool next_variation_deprecated(parray<le_uint32_t, 0x20>& variations, Version version, Episode episode, bool is_solo);
|
||||
|
||||
std::vector<std::string> map_filenames_for_variation_deprecated(
|
||||
uint8_t floor, uint32_t var1, uint32_t var2, Version version, Episode episode, GameMode mode, bool is_enemies);
|
||||
std::vector<std::vector<std::string>> map_filenames_for_variations_deprecated(
|
||||
const parray<le_uint32_t, 0x20>& variations,
|
||||
Version version,
|
||||
Episode episode,
|
||||
GameMode mode,
|
||||
bool is_enemies);
|
||||
|
||||
+8
-2
@@ -14,6 +14,7 @@
|
||||
|
||||
namespace MenuID {
|
||||
constexpr uint32_t MAIN = 0x11000011;
|
||||
constexpr uint32_t CLEAR_LICENSE_CONFIRMATION = 0x11111111;
|
||||
constexpr uint32_t INFORMATION = 0x22000022;
|
||||
constexpr uint32_t LOBBY = 0x33000033;
|
||||
constexpr uint32_t GAME = 0x44000044;
|
||||
@@ -40,6 +41,11 @@ constexpr uint32_t DISCONNECT = 0x11888811;
|
||||
constexpr uint32_t CLEAR_LICENSE = 0x11999911;
|
||||
} // namespace MainMenuItemID
|
||||
|
||||
namespace ClearLicenseConfirmationMenuItemID {
|
||||
constexpr uint32_t CANCEL = 0x01010101;
|
||||
constexpr uint32_t CLEAR_LICENSE = 0x02020202;
|
||||
} // namespace ClearLicenseConfirmationMenuItemID
|
||||
|
||||
namespace InformationMenuItemID {
|
||||
constexpr uint32_t GO_BACK = 0x22FFFF22;
|
||||
}
|
||||
@@ -60,8 +66,8 @@ constexpr uint32_t GO_BACK = 0x99FFFF99;
|
||||
namespace ProxyOptionsMenuItemID {
|
||||
constexpr uint32_t GO_BACK = 0xAAFFFFAA;
|
||||
constexpr uint32_t CHAT_COMMANDS = 0xAA0101AA;
|
||||
constexpr uint32_t CHAT_FILTER = 0xAA0202AA;
|
||||
constexpr uint32_t PLAYER_NOTIFICATIONS = 0xAA0303AA;
|
||||
constexpr uint32_t PLAYER_NOTIFICATIONS = 0xAA0202AA;
|
||||
constexpr uint32_t DROP_NOTIFICATIONS = 0xAA0303AA;
|
||||
constexpr uint32_t BLOCK_PINGS = 0xAA0404AA;
|
||||
constexpr uint32_t INFINITE_HP = 0xAA0505AA;
|
||||
constexpr uint32_t INFINITE_TP = 0xAA0606AA;
|
||||
|
||||
@@ -88,3 +88,17 @@ string string_for_address(uint32_t address) {
|
||||
uint32_t address_for_string(const char* address) {
|
||||
return ntohl(inet_addr(address));
|
||||
}
|
||||
|
||||
uint64_t devolution_phone_number_for_netloc(uint32_t addr, uint16_t port) {
|
||||
// It seems the address part of the number is fixed-width, but the port is
|
||||
// not. Why did they do it this way?
|
||||
if (port & 0xF000) {
|
||||
return (static_cast<uint64_t>(addr) << 16) | port;
|
||||
} else if (port & 0x0F00) {
|
||||
return (static_cast<uint64_t>(addr) << 12) | port;
|
||||
} else if (port & 0x00F0) {
|
||||
return (static_cast<uint64_t>(addr) << 8) | port;
|
||||
} else {
|
||||
return (static_cast<uint64_t>(addr) << 4) | port;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,3 +17,5 @@ bool is_local_address(const sockaddr_storage& daddr);
|
||||
|
||||
std::string string_for_address(uint32_t address);
|
||||
uint32_t address_for_string(const char* address);
|
||||
|
||||
uint64_t devolution_phone_number_for_netloc(uint32_t addr, uint16_t port);
|
||||
|
||||
+25
-50
@@ -117,22 +117,14 @@ void PSOLFGEncryption::encrypt_both_endian(
|
||||
}
|
||||
|
||||
PSOV2Encryption::PSOV2Encryption(uint32_t seed)
|
||||
: PSOLFGEncryption(seed, this->STREAM_LENGTH + 1, this->STREAM_LENGTH) {
|
||||
uint32_t esi, ebx, edi, eax, edx, var1;
|
||||
esi = 1;
|
||||
ebx = this->initial_seed;
|
||||
edi = 0x15;
|
||||
this->stream[56] = ebx;
|
||||
this->stream[55] = ebx;
|
||||
while (edi <= 0x46E) {
|
||||
eax = edi;
|
||||
var1 = eax / 55;
|
||||
edx = eax - (var1 * 55);
|
||||
ebx = ebx - esi;
|
||||
edi = edi + 0x15;
|
||||
this->stream[edx] = esi;
|
||||
esi = ebx;
|
||||
ebx = this->stream[edx];
|
||||
: PSOLFGEncryption(seed, STREAM_LENGTH + 1, STREAM_LENGTH) {
|
||||
uint32_t a = 1, b = this->initial_seed;
|
||||
this->stream[0x37] = b;
|
||||
for (uint16_t virtual_index = 0x15; virtual_index <= 0x36 * 0x15; virtual_index += 0x15) {
|
||||
this->stream[virtual_index % 0x37] = a;
|
||||
uint32_t c = b - a;
|
||||
b = a;
|
||||
a = c;
|
||||
}
|
||||
for (size_t x = 0; x < 5; x++) {
|
||||
this->update_stream();
|
||||
@@ -141,26 +133,11 @@ PSOV2Encryption::PSOV2Encryption(uint32_t seed)
|
||||
}
|
||||
|
||||
void PSOV2Encryption::update_stream() {
|
||||
uint32_t esi, edi, eax, ebp, edx;
|
||||
edi = 1;
|
||||
edx = 0x18;
|
||||
eax = edi;
|
||||
while (edx > 0) {
|
||||
esi = this->stream[eax + 0x1F];
|
||||
ebp = this->stream[eax] - esi;
|
||||
this->stream[eax] = ebp;
|
||||
eax++;
|
||||
edx--;
|
||||
for (size_t z = 1; z < 0x19; z++) {
|
||||
this->stream[z] -= this->stream[z + 0x1F];
|
||||
}
|
||||
edi = 0x19;
|
||||
edx = 0x1F;
|
||||
eax = edi;
|
||||
while (edx > 0) {
|
||||
esi = this->stream[eax - 0x18];
|
||||
ebp = this->stream[eax] - esi;
|
||||
this->stream[eax] = ebp;
|
||||
eax++;
|
||||
edx--;
|
||||
for (size_t z = 0x19; z < 0x38; z++) {
|
||||
this->stream[z] -= this->stream[z - 0x18];
|
||||
}
|
||||
this->offset = 1;
|
||||
this->cycles++;
|
||||
@@ -171,7 +148,7 @@ PSOEncryption::Type PSOV2Encryption::type() const {
|
||||
}
|
||||
|
||||
PSOV3Encryption::PSOV3Encryption(uint32_t seed)
|
||||
: PSOLFGEncryption(seed, this->STREAM_LENGTH, this->STREAM_LENGTH) {
|
||||
: PSOLFGEncryption(seed, STREAM_LENGTH, STREAM_LENGTH) {
|
||||
uint32_t x, y, basekey, source1, source2, source3;
|
||||
basekey = 0;
|
||||
|
||||
@@ -194,7 +171,7 @@ PSOV3Encryption::PSOV3Encryption(uint32_t seed)
|
||||
source1 = 0;
|
||||
source2 = 1;
|
||||
source3 = this->offset - 1;
|
||||
while (this->offset != this->STREAM_LENGTH) {
|
||||
while (this->offset != STREAM_LENGTH) {
|
||||
this->stream[this->offset++] = (this->stream[source3++] ^ (((this->stream[source1++] << 23) & 0xFF800000) ^ ((this->stream[source2++] >> 9) & 0x007FFFFF)));
|
||||
}
|
||||
|
||||
@@ -205,19 +182,13 @@ PSOV3Encryption::PSOV3Encryption(uint32_t seed)
|
||||
}
|
||||
|
||||
void PSOV3Encryption::update_stream() {
|
||||
uint32_t r5, r6, r7;
|
||||
r5 = 0;
|
||||
r6 = 489;
|
||||
r7 = 0;
|
||||
|
||||
while (r6 != this->STREAM_LENGTH) {
|
||||
this->stream[r5++] ^= this->stream[r6++];
|
||||
static constexpr size_t PHASE2_OFFSET = STREAM_LENGTH - 489;
|
||||
for (size_t z = 489; z < STREAM_LENGTH; z++) {
|
||||
this->stream[z - 489] ^= this->stream[z];
|
||||
}
|
||||
|
||||
while (r5 != this->STREAM_LENGTH) {
|
||||
this->stream[r5++] ^= this->stream[r7++];
|
||||
for (size_t z = PHASE2_OFFSET; z < STREAM_LENGTH; z++) {
|
||||
this->stream[z] ^= this->stream[z - PHASE2_OFFSET];
|
||||
}
|
||||
|
||||
this->offset = 0;
|
||||
this->cycles++;
|
||||
}
|
||||
@@ -918,15 +889,19 @@ uint32_t encrypt_challenge_time(uint16_t value) {
|
||||
available_bits.erase(it);
|
||||
}
|
||||
|
||||
return (mask << 16) | (value ^ mask);
|
||||
uint32_t ret = (mask << 16) | (value ^ mask);
|
||||
fprintf(stderr, "encrypt_challenge_time %04hX => %08" PRIX32 "\n", value, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint16_t decrypt_challenge_time(uint32_t value) {
|
||||
uint16_t mask = (value >> 0x10);
|
||||
uint8_t mask_one_bits = count_one_bits(mask);
|
||||
return ((mask_one_bits < 4) || (mask_one_bits > 12))
|
||||
uint16_t ret = ((mask_one_bits < 4) || (mask_one_bits > 12))
|
||||
? 0xFFFF
|
||||
: ((mask ^ value) & 0xFFFF);
|
||||
fprintf(stderr, "decrypt_challenge_time %08" PRIX32 " => %04hX\n", value, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
string decrypt_v2_registry_value(const void* data, size_t size) {
|
||||
|
||||
+54
-2
@@ -5,10 +5,13 @@
|
||||
|
||||
#include <memory>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/Random.hh>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Text.hh" // for parray
|
||||
#include "Compression.hh"
|
||||
#include "Text.hh"
|
||||
|
||||
class PSOEncryption {
|
||||
public:
|
||||
@@ -79,7 +82,7 @@ public:
|
||||
protected:
|
||||
virtual void update_stream();
|
||||
|
||||
static constexpr size_t STREAM_LENGTH = 56;
|
||||
static constexpr size_t STREAM_LENGTH = 0x38;
|
||||
};
|
||||
|
||||
class PSOV3Encryption : public PSOLFGEncryption {
|
||||
@@ -250,6 +253,41 @@ void decrypt_trivial_gci_data(void* data, size_t size, uint8_t basis);
|
||||
uint32_t encrypt_challenge_time(uint16_t value);
|
||||
uint16_t decrypt_challenge_time(uint32_t value);
|
||||
|
||||
template <bool IsBigEndian>
|
||||
class ChallengeTime {
|
||||
private:
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
|
||||
U32T value;
|
||||
|
||||
public:
|
||||
ChallengeTime() = default;
|
||||
ChallengeTime(uint16_t v) {
|
||||
this->store(v);
|
||||
}
|
||||
ChallengeTime(const ChallengeTime& other) = default;
|
||||
ChallengeTime(ChallengeTime&& other) = default;
|
||||
ChallengeTime& operator=(const ChallengeTime& other) = default;
|
||||
ChallengeTime& operator=(ChallengeTime&& other) = default;
|
||||
|
||||
bool has_value() const {
|
||||
return this->value != 0;
|
||||
}
|
||||
|
||||
uint16_t load() const {
|
||||
return decrypt_challenge_time(this->value);
|
||||
}
|
||||
operator uint16_t() const {
|
||||
return this->load();
|
||||
}
|
||||
void store(uint16_t v) {
|
||||
this->value = ((v == 0) || (v == 0xFFFF)) ? 0 : encrypt_challenge_time(v);
|
||||
}
|
||||
ChallengeTime& operator=(uint16_t v) {
|
||||
this->store(v);
|
||||
return *this;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
|
||||
std::string decrypt_v2_registry_value(const void* data, size_t size);
|
||||
|
||||
struct DecryptedPR2 {
|
||||
@@ -277,6 +315,16 @@ DecryptedPR2 decrypt_pr2_data(const std::string& data) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <bool IsBigEndian>
|
||||
std::string decrypt_and_decompress_pr2_data(const std::string& data) {
|
||||
auto decrypted = decrypt_pr2_data<IsBigEndian>(data);
|
||||
std::string decompressed = prs_decompress(decrypted.compressed_data);
|
||||
if (decompressed.size() != decrypted.decompressed_size) {
|
||||
throw std::runtime_error("decompressed size does not match expected size");
|
||||
}
|
||||
return decompressed;
|
||||
}
|
||||
|
||||
template <bool IsBigEndian>
|
||||
std::string encrypt_pr2_data(const std::string& data, size_t decompressed_size, uint32_t seed) {
|
||||
using U32T = std::conditional_t<IsBigEndian, be_uint32_t, le_uint32_t>;
|
||||
@@ -295,3 +343,7 @@ std::string encrypt_pr2_data(const std::string& data, size_t decompressed_size,
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline uint32_t random_from_optional_crypt(std::shared_ptr<PSOLFGEncryption> random_crypt) {
|
||||
return random_crypt ? random_crypt->next() : random_object<uint32_t>();
|
||||
}
|
||||
|
||||
+7
-7
@@ -31,7 +31,7 @@ uint16_t PSOCommandHeader::command(Version version) const {
|
||||
return this->dc.command;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
return this->gc.command;
|
||||
case Version::XB_V3:
|
||||
@@ -59,7 +59,7 @@ void PSOCommandHeader::set_command(Version version, uint16_t command) {
|
||||
break;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
this->gc.command = command;
|
||||
break;
|
||||
@@ -88,7 +88,7 @@ uint16_t PSOCommandHeader::size(Version version) const {
|
||||
return this->dc.size;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
return this->gc.size;
|
||||
case Version::XB_V3:
|
||||
@@ -116,7 +116,7 @@ void PSOCommandHeader::set_size(Version version, uint32_t size) {
|
||||
break;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
this->gc.size = size;
|
||||
break;
|
||||
@@ -145,7 +145,7 @@ uint32_t PSOCommandHeader::flag(Version version) const {
|
||||
return this->dc.flag;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
return this->gc.flag;
|
||||
case Version::XB_V3:
|
||||
@@ -173,7 +173,7 @@ void PSOCommandHeader::set_flag(Version version, uint32_t flag) {
|
||||
break;
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
this->gc.flag = flag;
|
||||
break;
|
||||
@@ -218,7 +218,7 @@ std::string prepend_command_header(
|
||||
case Version::DC_V2:
|
||||
case Version::GC_NTE:
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3_TRIAL_EDITION:
|
||||
case Version::GC_EP3_NTE:
|
||||
case Version::GC_EP3:
|
||||
case Version::XB_V3: {
|
||||
PSOCommandHeaderDCV3 header;
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
#include "PatchServer.hh"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/bufferevent.h>
|
||||
#include <event2/event.h>
|
||||
#include <event2/listener.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/Network.hh>
|
||||
#include <phosg/Strings.hh>
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "EventUtils.hh"
|
||||
#include "Loggers.hh"
|
||||
#include "PSOProtocol.hh"
|
||||
#include "ReceiveCommands.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
static atomic<uint64_t> next_id(1);
|
||||
|
||||
PatchServer::Client::Client(
|
||||
shared_ptr<PatchServer> server,
|
||||
struct bufferevent* bev,
|
||||
Version version,
|
||||
uint64_t idle_timeout_usecs,
|
||||
bool hide_data_from_logs)
|
||||
: server(server),
|
||||
id(next_id++),
|
||||
log(string_printf("[C-%" PRIX64 "] ", this->id), client_log.min_level),
|
||||
channel(bev, version, 1, nullptr, nullptr, this, string_printf("C-%" PRIX64, this->id), TerminalFormat::FG_YELLOW, TerminalFormat::FG_GREEN),
|
||||
idle_timeout_usecs(idle_timeout_usecs),
|
||||
idle_timeout_event(
|
||||
event_new(bufferevent_get_base(bev), -1, EV_TIMEOUT, &PatchServer::Client::dispatch_idle_timeout, this),
|
||||
event_free) {
|
||||
this->reschedule_timeout_event();
|
||||
|
||||
// Don't print data sent to patch clients to the logs. The patch server
|
||||
// protocol is fully understood and data logs for patch clients are generally
|
||||
// more annoying than helpful at this point.
|
||||
if (hide_data_from_logs) {
|
||||
this->channel.terminal_recv_color = TerminalFormat::END;
|
||||
this->channel.terminal_send_color = TerminalFormat::END;
|
||||
}
|
||||
|
||||
this->log.info("Created");
|
||||
}
|
||||
|
||||
void PatchServer::Client::reschedule_timeout_event() {
|
||||
struct timeval idle_tv = usecs_to_timeval(this->idle_timeout_usecs);
|
||||
event_add(this->idle_timeout_event.get(), &idle_tv);
|
||||
}
|
||||
|
||||
void PatchServer::Client::dispatch_idle_timeout(evutil_socket_t, short, void* ctx) {
|
||||
reinterpret_cast<Client*>(ctx)->idle_timeout();
|
||||
}
|
||||
|
||||
void PatchServer::Client::idle_timeout() {
|
||||
this->log.info("Idle timeout expired");
|
||||
auto s = this->server.lock();
|
||||
if (s) {
|
||||
auto c = this->shared_from_this();
|
||||
s->disconnect_client(c);
|
||||
} else {
|
||||
this->channel.disconnect();
|
||||
this->log.info("Server is deleted; cannot disconnect client");
|
||||
}
|
||||
}
|
||||
|
||||
void PatchServer::send_server_init(shared_ptr<Client> c) const {
|
||||
uint32_t server_key = random_object<uint32_t>();
|
||||
uint32_t client_key = random_object<uint32_t>();
|
||||
|
||||
S_ServerInit_Patch_02 cmd;
|
||||
cmd.copyright.encode("Patch Server. Copyright SonicTeam, LTD. 2001");
|
||||
cmd.server_key = server_key;
|
||||
cmd.client_key = client_key;
|
||||
c->channel.send(0x02, 0x00, cmd);
|
||||
|
||||
c->channel.crypt_out = make_shared<PSOV2Encryption>(server_key);
|
||||
c->channel.crypt_in = make_shared<PSOV2Encryption>(client_key);
|
||||
}
|
||||
|
||||
void PatchServer::send_message_box(shared_ptr<Client> c, const string& text) const {
|
||||
StringWriter w;
|
||||
try {
|
||||
if (c->version() == Version::PC_PATCH) {
|
||||
w.write(tt_encode_marked_optional(text, c->channel.language, true));
|
||||
} else if (c->version() == Version::BB_PATCH) {
|
||||
w.write(tt_encode_marked_optional(add_color(text), c->channel.language, true));
|
||||
} else {
|
||||
throw logic_error("non-patch client on patch server");
|
||||
}
|
||||
} catch (const runtime_error& e) {
|
||||
log_warning("Failed to encode message for patch message box command: %s", e.what());
|
||||
return;
|
||||
}
|
||||
w.put_u16(0);
|
||||
while (w.str().size() & 3) {
|
||||
w.put_u8(0);
|
||||
}
|
||||
c->channel.send(0x13, 0x00, w.str());
|
||||
}
|
||||
|
||||
void PatchServer::send_enter_directory(shared_ptr<Client> c, const string& dir) const {
|
||||
S_EnterDirectory_Patch_09 cmd = {{dir, 1}};
|
||||
c->channel.send(0x09, 0x00, cmd);
|
||||
}
|
||||
|
||||
void PatchServer::on_02(shared_ptr<Client> c, string& data) {
|
||||
check_size_v(data.size(), 0);
|
||||
c->channel.send(0x04, 0x00); // This requests the user's login information
|
||||
}
|
||||
|
||||
void PatchServer::change_to_directory(
|
||||
shared_ptr<Client> c,
|
||||
vector<string>& client_path_directories,
|
||||
const vector<string>& file_path_directories) const {
|
||||
// First, exit all leaf directories that don't match the desired path
|
||||
while (!client_path_directories.empty() &&
|
||||
((client_path_directories.size() > file_path_directories.size()) ||
|
||||
(client_path_directories.back() != file_path_directories[client_path_directories.size() - 1]))) {
|
||||
c->channel.send(0x0A, 0x00);
|
||||
client_path_directories.pop_back();
|
||||
}
|
||||
|
||||
// At this point, client_path_directories should be a prefix of
|
||||
// file_path_directories (or should match exactly)
|
||||
if (client_path_directories.size() > file_path_directories.size()) {
|
||||
throw logic_error("did not exit all necessary directories");
|
||||
}
|
||||
for (size_t x = 0; x < client_path_directories.size(); x++) {
|
||||
if (client_path_directories[x] != file_path_directories[x]) {
|
||||
throw logic_error("intermediate path is not a prefix of final path");
|
||||
}
|
||||
}
|
||||
|
||||
// Second, enter all necessary leaf directories
|
||||
while (client_path_directories.size() < file_path_directories.size()) {
|
||||
const string& dir = file_path_directories[client_path_directories.size()];
|
||||
this->send_enter_directory(c, dir);
|
||||
client_path_directories.emplace_back(dir);
|
||||
}
|
||||
}
|
||||
|
||||
void PatchServer::on_04(shared_ptr<Client> c, string& data) {
|
||||
const auto& cmd = check_size_t<C_Login_Patch_04>(data);
|
||||
|
||||
string username = cmd.username.decode();
|
||||
string password = cmd.password.decode();
|
||||
|
||||
// There are 3 cases here:
|
||||
// - No login information at all: just proceed without checking license
|
||||
// - Username only: check that license exists if allow_unregistered_users is off
|
||||
// - Username and password: call verify_bb
|
||||
if (!username.empty() && !password.empty()) {
|
||||
try {
|
||||
this->config->license_index->verify_bb(username, password);
|
||||
|
||||
} catch (const LicenseIndex::incorrect_password& e) {
|
||||
this->send_message_box(c, string_printf("Login failed: %s", e.what()));
|
||||
this->disconnect_client(c);
|
||||
return;
|
||||
|
||||
} catch (const LicenseIndex::missing_license& e) {
|
||||
if (!this->config->allow_unregistered_users) {
|
||||
this->send_message_box(c, string_printf("Login failed: %s", e.what()));
|
||||
this->disconnect_client(c);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (!username.empty() && !this->config->allow_unregistered_users) {
|
||||
try {
|
||||
this->config->license_index->get_by_bb_username(username);
|
||||
} catch (const LicenseIndex::missing_license& e) {
|
||||
this->send_message_box(c, string_printf("Login failed: %s", e.what()));
|
||||
this->disconnect_client(c);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->config->message.empty()) {
|
||||
this->send_message_box(c, this->config->message.c_str());
|
||||
}
|
||||
|
||||
const auto& index = this->config->patch_file_index;
|
||||
if (index.get()) {
|
||||
c->channel.send(0x0B, 0x00); // Start patch session; go to root directory
|
||||
|
||||
vector<string> path_directories;
|
||||
for (const auto& file : index->all_files()) {
|
||||
this->change_to_directory(c, path_directories, file->path_directories);
|
||||
|
||||
S_FileChecksumRequest_Patch_0C req = {c->patch_file_checksum_requests.size(), {file->name, 1}};
|
||||
c->channel.send(0x0C, 0x00, req);
|
||||
c->patch_file_checksum_requests.emplace_back(file);
|
||||
}
|
||||
this->change_to_directory(c, path_directories, {});
|
||||
|
||||
c->channel.send(0x0D, 0x00); // End of checksum requests
|
||||
|
||||
} else {
|
||||
// No patch index present: just do something that will satisfy the client
|
||||
// without actually checking or downloading any files
|
||||
this->send_enter_directory(c, ".");
|
||||
this->send_enter_directory(c, "data");
|
||||
this->send_enter_directory(c, "scene");
|
||||
c->channel.send(0x0A, 0x00);
|
||||
c->channel.send(0x0A, 0x00);
|
||||
c->channel.send(0x0A, 0x00);
|
||||
c->channel.send(0x12, 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
void PatchServer::on_0F(shared_ptr<Client> c, string& data) {
|
||||
auto& cmd = check_size_t<C_FileInformation_Patch_0F>(data);
|
||||
auto& req = c->patch_file_checksum_requests.at(cmd.request_id);
|
||||
req.crc32 = cmd.checksum;
|
||||
req.size = cmd.size;
|
||||
req.response_received = true;
|
||||
}
|
||||
|
||||
void PatchServer::on_10(shared_ptr<Client> c, string&) {
|
||||
S_StartFileDownloads_Patch_11 start_cmd = {0, 0};
|
||||
for (const auto& req : c->patch_file_checksum_requests) {
|
||||
if (!req.response_received) {
|
||||
throw runtime_error("client did not respond to checksum request");
|
||||
}
|
||||
if (req.needs_update()) {
|
||||
c->log.info("File %s needs update (CRC: %08" PRIX32 "/%08" PRIX32 ", size: %" PRIu32 "/%" PRIu32 ")",
|
||||
req.file->name.c_str(), req.file->crc32, req.crc32, req.file->size, req.size);
|
||||
start_cmd.total_bytes += req.file->size;
|
||||
start_cmd.num_files++;
|
||||
} else {
|
||||
c->log.info("File %s is up to date", req.file->name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (start_cmd.num_files) {
|
||||
c->channel.send(0x11, 0x00, start_cmd);
|
||||
vector<string> path_directories;
|
||||
for (const auto& req : c->patch_file_checksum_requests) {
|
||||
if (req.needs_update()) {
|
||||
this->change_to_directory(c, path_directories, req.file->path_directories);
|
||||
|
||||
S_OpenFile_Patch_06 open_cmd = {0, req.file->size, {req.file->name, 1}};
|
||||
c->channel.send(0x06, 0x00, open_cmd);
|
||||
|
||||
for (size_t x = 0; x < req.file->chunk_crcs.size(); x++) {
|
||||
auto data = req.file->load_data();
|
||||
size_t chunk_size = min<uint32_t>(req.file->size - (x * 0x4000), 0x4000);
|
||||
|
||||
vector<pair<const void*, size_t>> blocks;
|
||||
S_WriteFileHeader_Patch_07 cmd_header = {x, req.file->chunk_crcs[x], chunk_size};
|
||||
blocks.emplace_back(&cmd_header, sizeof(cmd_header));
|
||||
blocks.emplace_back(data->data() + (x * 0x4000), chunk_size);
|
||||
c->channel.send(0x07, 0x00, blocks);
|
||||
}
|
||||
|
||||
S_CloseCurrentFile_Patch_08 close_cmd = {0};
|
||||
c->channel.send(0x08, 0x00, close_cmd);
|
||||
}
|
||||
}
|
||||
this->change_to_directory(c, path_directories, {});
|
||||
}
|
||||
|
||||
c->channel.send(0x12, 0x00);
|
||||
}
|
||||
|
||||
void PatchServer::disconnect_client(shared_ptr<Client> c) {
|
||||
if (c->channel.is_virtual_connection) {
|
||||
server_log.info("Client disconnected: C-%" PRIX64 " on virtual connection %p", c->id, c->channel.bev.get());
|
||||
} else if (c->channel.bev) {
|
||||
server_log.info("Client disconnected: C-%" PRIX64 " on fd %d", c->id, bufferevent_getfd(c->channel.bev.get()));
|
||||
} else {
|
||||
server_log.info("Client C-%" PRIX64 " removed from game server", c->id);
|
||||
}
|
||||
|
||||
this->channel_to_client.erase(&c->channel);
|
||||
c->channel.disconnect();
|
||||
|
||||
// We can't just let c be destroyed here, since disconnect_client can be
|
||||
// called from within the client's channel's receive handler. So, we instead
|
||||
// move it to another set, which we'll clear in an immediately-enqueued
|
||||
// callback after the current event. This will also call the client's
|
||||
// disconnect hooks (if any).
|
||||
this->clients_to_destroy.insert(std::move(c));
|
||||
this->enqueue_destroy_clients();
|
||||
}
|
||||
|
||||
void PatchServer::enqueue_destroy_clients() {
|
||||
auto tv = usecs_to_timeval(0);
|
||||
event_add(this->destroy_clients_ev.get(), &tv);
|
||||
}
|
||||
|
||||
void PatchServer::dispatch_destroy_clients(evutil_socket_t, short, void* ctx) {
|
||||
reinterpret_cast<PatchServer*>(ctx)->clients_to_destroy.clear();
|
||||
}
|
||||
|
||||
void PatchServer::dispatch_on_listen_accept(
|
||||
struct evconnlistener* listener, evutil_socket_t fd,
|
||||
struct sockaddr* address, int socklen, void* ctx) {
|
||||
reinterpret_cast<PatchServer*>(ctx)->on_listen_accept(listener, fd, address, socklen);
|
||||
}
|
||||
|
||||
void PatchServer::dispatch_on_listen_error(
|
||||
struct evconnlistener* listener, void* ctx) {
|
||||
reinterpret_cast<PatchServer*>(ctx)->on_listen_error(listener);
|
||||
}
|
||||
|
||||
void PatchServer::on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd, struct sockaddr*, int) {
|
||||
int listen_fd = evconnlistener_get_fd(listener);
|
||||
ListeningSocket* listening_socket;
|
||||
try {
|
||||
listening_socket = &this->listening_sockets.at(listen_fd);
|
||||
} catch (const out_of_range& e) {
|
||||
server_log.warning("Can\'t determine version for socket %d; disconnecting client", listen_fd);
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
struct bufferevent* bev = bufferevent_socket_new(this->base.get(), fd, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);
|
||||
auto c = make_shared<Client>(
|
||||
this->shared_from_this(),
|
||||
bev,
|
||||
listening_socket->version,
|
||||
this->config->idle_timeout_usecs,
|
||||
this->config->hide_data_from_logs);
|
||||
c->channel.on_command_received = PatchServer::on_client_input;
|
||||
c->channel.on_error = PatchServer::on_client_error;
|
||||
c->channel.context_obj = this;
|
||||
this->channel_to_client.emplace(&c->channel, c);
|
||||
|
||||
server_log.info("Patch client connected: U-%" PRIX64 " on fd %d via %d (%s)",
|
||||
c->id, fd, listen_fd, listening_socket->addr_str.c_str());
|
||||
|
||||
this->send_server_init(c);
|
||||
}
|
||||
|
||||
void PatchServer::on_listen_error(struct evconnlistener* listener) {
|
||||
int err = EVUTIL_SOCKET_ERROR();
|
||||
server_log.error("Failure on listening socket %d: %d (%s)",
|
||||
evconnlistener_get_fd(listener), err, evutil_socket_error_to_string(err));
|
||||
event_base_loopexit(this->base.get(), nullptr);
|
||||
}
|
||||
|
||||
void PatchServer::on_client_input(Channel& ch, uint16_t command, uint32_t, std::string& data) {
|
||||
PatchServer* server = reinterpret_cast<PatchServer*>(ch.context_obj);
|
||||
shared_ptr<Client> c = server->channel_to_client.at(&ch);
|
||||
|
||||
try {
|
||||
switch (command) {
|
||||
case 0x02:
|
||||
server->on_02(c, data);
|
||||
break;
|
||||
case 0x04:
|
||||
server->on_04(c, data);
|
||||
break;
|
||||
case 0x0F:
|
||||
server->on_0F(c, data);
|
||||
break;
|
||||
case 0x10:
|
||||
server->on_10(c, data);
|
||||
break;
|
||||
default:
|
||||
throw runtime_error("invalid command");
|
||||
}
|
||||
} catch (const exception& e) {
|
||||
server_log.warning("Error processing client command: %s", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void PatchServer::on_client_error(Channel& ch, short events) {
|
||||
PatchServer* server = reinterpret_cast<PatchServer*>(ch.context_obj);
|
||||
shared_ptr<Client> c = server->channel_to_client.at(&ch);
|
||||
|
||||
if (events & BEV_EVENT_ERROR) {
|
||||
int err = EVUTIL_SOCKET_ERROR();
|
||||
server_log.warning("Client caused error %d (%s)", err, evutil_socket_error_to_string(err));
|
||||
}
|
||||
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
|
||||
server->disconnect_client(c);
|
||||
}
|
||||
}
|
||||
|
||||
PatchServer::PatchServer(shared_ptr<const Config> config)
|
||||
: base(event_base_new(), event_base_free),
|
||||
config(config),
|
||||
destroy_clients_ev(event_new(this->base.get(), -1, EV_TIMEOUT, &PatchServer::dispatch_destroy_clients, this), event_free),
|
||||
th(&PatchServer::thread_fn, this) {}
|
||||
|
||||
void PatchServer::schedule_stop() {
|
||||
event_base_loopexit(this->base.get(), nullptr);
|
||||
}
|
||||
|
||||
void PatchServer::wait_for_stop() {
|
||||
this->th.join();
|
||||
}
|
||||
|
||||
void PatchServer::listen(const std::string& addr_str, const string& socket_path, Version version) {
|
||||
int fd = ::listen(socket_path, 0, SOMAXCONN);
|
||||
server_log.info("Listening on Unix socket %s on fd %d as %s", socket_path.c_str(), fd, addr_str.c_str());
|
||||
this->add_socket(addr_str, fd, version);
|
||||
}
|
||||
|
||||
void PatchServer::listen(const std::string& addr_str, const string& addr, int port, Version version) {
|
||||
if (port == 0) {
|
||||
this->listen(addr_str, addr, version);
|
||||
} else {
|
||||
int fd = ::listen(addr, port, SOMAXCONN);
|
||||
string netloc_str = render_netloc(addr, port);
|
||||
server_log.info("Listening on TCP interface %s on fd %d as %s", netloc_str.c_str(), fd, addr_str.c_str());
|
||||
this->add_socket(addr_str, fd, version);
|
||||
}
|
||||
}
|
||||
|
||||
void PatchServer::listen(const std::string& addr_str, int port, Version version) {
|
||||
this->listen(addr_str, "", port, version);
|
||||
}
|
||||
|
||||
PatchServer::ListeningSocket::ListeningSocket(PatchServer* s, const std::string& addr_str, int fd, Version version)
|
||||
: addr_str(addr_str),
|
||||
fd(fd),
|
||||
version(version),
|
||||
listener(evconnlistener_new(s->base.get(), PatchServer::dispatch_on_listen_accept, s, LEV_OPT_REUSEABLE, 0, this->fd),
|
||||
evconnlistener_free) {
|
||||
evconnlistener_set_error_cb(this->listener.get(), PatchServer::dispatch_on_listen_error);
|
||||
}
|
||||
|
||||
void PatchServer::add_socket(const std::string& addr_str, int fd, Version version) {
|
||||
this->listening_sockets.emplace(piecewise_construct, forward_as_tuple(fd), forward_as_tuple(this, addr_str, fd, version));
|
||||
}
|
||||
|
||||
void PatchServer::thread_fn() {
|
||||
event_base_loop(this->base.get(), EVLOOP_NO_EXIT_ON_EMPTY);
|
||||
}
|
||||
|
||||
void PatchServer::set_config(std::shared_ptr<const Config> config) {
|
||||
forward_to_event_thread(this->base, [s = this->shared_from_this(), config = std::move(config)]() {
|
||||
s->config = config;
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user