Compare commits

..

213 Commits

Author SHA1 Message Date
Loïc Hoguin
ceea04c3b4 Work in progress on egs_net (will be amended) 2012-05-16 13:31:37 +02:00
Loïc Hoguin
9adab0ea87 Move the PRS compression library into a separate application 2012-01-04 01:56:38 +01:00
Loïc Hoguin
6579c26f98 egs_patch: Rename the folder containing the patch files 2012-01-04 00:56:20 +01:00
Loïc Hoguin
368bb0f7b4 egs_patch: Use code:priv_dir/1 to get the location of the configuration 2012-01-04 00:52:41 +01:00
Loïc Hoguin
807510f669 Add custom crc.bin and SmutFilter_J.bin files to the patch server
These are safe to use and distribute as they were rebuilt from scratch.
2011-12-15 19:22:37 +01:00
Loïc Hoguin
c69513073d Move the patch server into its own separate application 2011-12-12 10:46:27 +01:00
Loïc Hoguin
35b86b58ba Fix paths temporarily to fix the compilation 2011-12-12 10:41:16 +01:00
Loïc Hoguin
ac0c3c551b Initial release layout
Moved files around to generate releases later on, but also to
separate the code into many different applications each with their
own purpose (patch server, game server, data storage, script compiler,
files library, and so on).
2011-12-12 10:02:34 +01:00
Loïc Hoguin
289ce855ad Use supervisor:child_spec() type where appropriate. 2011-06-08 15:36:14 +02:00
Loïc Hoguin
bc47fcb049 Fix a dialyzer warning in egs_app. 2011-06-08 12:30:13 +02:00
Loïc Hoguin
408e7e99e6 Convert egs_zones_sup to a simple_one_for_one supervisor. 2011-06-08 12:01:39 +02:00
Loïc Hoguin
c42b1a85f8 Convert egs_quests_sup to a simple_one_for_one supervisor. 2011-06-08 11:30:28 +02:00
Loïc Hoguin
e2274666b8 Rename Makefile target server into app. 2011-06-08 02:18:10 +02:00
Loïc Hoguin
6fd1119777 Add type specs and simplifications to egs_conf. 2011-06-08 02:17:49 +02:00
Loïc Hoguin
eab53bc3a7 Add type specs and simplifications to egs_sup.erl. 2011-06-08 02:17:24 +02:00
Loïc Hoguin
2325c7cf63 Move type definitions from an include file to egs.erl. 2011-06-08 01:44:31 +02:00
Loïc Hoguin
8889ca8332 Simplify the code returning the current unix time. 2011-06-08 01:23:32 +02:00
Loïc Hoguin
23e781f498 Add type specs to egs.erl. 2011-06-08 00:13:48 +02:00
Loïc Hoguin
60b8009382 Use a try .. after construct for handling disconnects. 2011-05-18 21:38:18 +02:00
Loïc Hoguin
325c1a4c10 Use Cowboy as a pool of acceptors. 2011-05-17 17:15:17 +02:00
Loïc Hoguin
8f3db6480a Only enable zone 0 on colony by default.
Will prevent the issue with other zones not being defined yet.
2011-04-19 13:00:06 +02:00
Loïc Hoguin
8363741b3a Initial work on 5th floor on colony zone 0. 2011-04-19 12:57:31 +02:00
Loïc Hoguin
935b490461 Update the copyright to 2010-2011 in a few files. 2011-04-19 12:47:08 +02:00
Loïc Hoguin
948873ddec Add dialyzer and compiler options to rebar.config. 2011-04-19 12:45:17 +02:00
Loïc Hoguin
72726bdf6c Remove rebar from the repository. Use the $PATH one by default. 2011-04-19 12:44:46 +02:00
Loïc Hoguin
0a399238f4 Fix a small issue in the patch server that could lead to a crash. 2011-03-29 01:58:26 +02:00
Loïc Hoguin
c957d9a8b9 Separate type definitions in their own header.
Also convert source files to utf8, update the copyright info and
other minor changes.
2011-02-27 23:13:09 +01:00
Loïc Hoguin
d9cde30b0b Remove the #level record. 2011-02-27 20:12:43 +01:00
Loïc Hoguin
c7cb5ab589 Remove the unused #users.se. 2011-02-27 19:49:27 +01:00
Loïc Hoguin
dc26bb82f7 Remove the unused #users.classlevels. 2011-02-27 19:46:37 +01:00
Loïc Hoguin
d8906226a5 Remove the unused #users.playtime. 2011-02-27 19:44:05 +01:00
Loïc Hoguin
730f47d837 Remove the unused #users.time. 2011-02-27 19:42:27 +01:00
Loïc Hoguin
d2f7c9e83f psu_characters: Remove validate_name as it's not doing anything yet. 2011-02-27 19:26:59 +01:00
Loïc Hoguin
6466f05728 psu_characters: Remove se_list_to_binary, it's doing it wrong. 2011-02-27 19:25:06 +01:00
Loïc Hoguin
3821a1e7bf egs_proto: Move the build_char_level function where it belongs. 2011-02-27 19:19:12 +01:00
Loïc Hoguin
a1bf3e43f3 Remove #characters and merge the data into #users directly. 2011-02-27 19:14:03 +01:00
Loïc Hoguin
3290aba95d egs: Use egs_users:broadcast to send the warp message to a single player too. 2011-02-27 14:27:58 +01:00
Loïc Hoguin
72989f5332 egs_game: Forgot a function export. 2011-02-27 14:06:31 +01:00
Loïc Hoguin
6cbb987ab0 Rename psu_proto into egs_proto since this module isn't temporary. 2011-02-27 14:03:04 +01:00
Loïc Hoguin
9c8ad80a07 egs_game: Move psu_game:char_load into egs_game and delete psu_game. 2011-02-27 13:48:10 +01:00
Loïc Hoguin
b5d6b3934b egs_game: Move psu_game:npc_load into egs_game. 2011-02-27 13:36:36 +01:00
Loïc Hoguin
cdc2c56d5c egs_game: Move the code from area_load inside the area_change event. 2011-02-27 13:33:18 +01:00
Loïc Hoguin
5b255b211b egs_login: Don't use the process dictionary anymore. 2011-02-27 02:26:37 +01:00
Loïc Hoguin
edb061662f psu_proto: Convert log calls to io:format to get rid of get(gid). 2011-02-27 02:24:49 +01:00
Loïc Hoguin
9268f3f7ae egs_game: Convert log calls to io:format to get rid of get(gid). 2011-02-27 02:16:15 +01:00
Loïc Hoguin
dd4a228b01 egs_login: Remove a warning on unused Socket variable. 2011-02-27 02:14:57 +01:00
Loïc Hoguin
34b4a21ce7 egs_game: Remove a few commented get(gid) that are polluting the search. 2011-02-27 00:28:57 +01:00
Loïc Hoguin
2a7383b9a1 psu_game: Remove psu_game:send in favor of psu_proto:packet_send.
This definitely gets rid of the process dictionary for storing the socket.
2011-02-27 00:10:28 +01:00
Loïc Hoguin
3f1d1d2fb0 psu_proto: Move send_0a0a to psu_proto without reviewing it. 2011-02-27 00:01:25 +01:00
Loïc Hoguin
18429b8d76 psu_proto: Move build_item_variables to psu_proto. 2011-02-26 23:56:51 +01:00
Loïc Hoguin
82aca844f2 psu_proto: Move build_item_constants to psu_proto. 2011-02-26 23:54:22 +01:00
Loïc Hoguin
1b36ff2589 psu_proto: Move send_010a to psu_proto without reviewing it. 2011-02-26 23:49:56 +01:00
Loïc Hoguin
4ff28e0939 psu_proto: Move send_1004 to psu_proto without reviewing it. 2011-02-26 23:32:43 +01:00
Loïc Hoguin
f21f23a92f psu_game: Delete send_1309 and its binary packet, now unused. 2011-02-26 23:21:15 +01:00
Loïc Hoguin
979e99f6a5 psu_game: Delete send_1332 and its binary packet, now unused. 2011-02-26 23:20:26 +01:00
Loïc Hoguin
58cfb8a61e psu_proto: Move send_0113 to psu_proto without reviewing it. 2011-02-26 23:16:31 +01:00
Loïc Hoguin
9aca48a697 psu_proto: Move send_0503 to psu_proto without reviewing it. 2011-02-26 23:14:12 +01:00
Loïc Hoguin
722bcf7c9e psu_proto: Move send_0a04 to psu_proto without reviewing it. 2011-02-26 23:11:31 +01:00
Loïc Hoguin
dc77b8e804 psu_proto: Move send_0d03 to psu_proto without reviewing it. 2011-02-26 23:07:52 +01:00
Loïc Hoguin
eaafdc213f psu_proto: Move send_1016 to psu_proto without reviewing it. 2011-02-26 23:03:19 +01:00
Loïc Hoguin
f8524ca9c4 psu_proto: Move send_1216 to psu_proto without reviewing it. 2011-02-26 23:01:08 +01:00
Loïc Hoguin
8c95aab709 psu_proto: Move send_1501 to psu_proto without reviewing it. 2011-02-26 22:58:23 +01:00
Loïc Hoguin
87c256edba psu_proto: Move send_1512 to psu_proto without reviewing it. 2011-02-26 22:55:55 +01:00
Loïc Hoguin
9c7f8f6eaa psu_proto: Move send_1602 to psu_proto without reviewing it. 2011-02-26 22:44:51 +01:00
Loïc Hoguin
5c234257dc psu_game: Remove the header function now unused. 2011-02-26 22:34:37 +01:00
Loïc Hoguin
a571b9a56b psu_proto: Move send_1a04 to psu_proto without reviewing it. 2011-02-26 22:34:05 +01:00
Loïc Hoguin
dfa5634adb psu_proto: Move send_170c to psu_proto without reviewing it. 2011-02-26 22:31:01 +01:00
Loïc Hoguin
53a4b3dbbe psu_proto: Move send_170a to psu_proto without reviewing it. 2011-02-26 22:27:47 +01:00
Loïc Hoguin
4faabbda8f psu_proto: Move send_1706 to psu_proto without reviewing it. 2011-02-26 22:25:06 +01:00
Loïc Hoguin
023214793c psu_proto: Move send_1601 to psu_proto without reviewing it. 2011-02-26 22:18:19 +01:00
Loïc Hoguin
250a22dea8 psu_proto: Move send_1215 to psu_proto without reviewing it. 2011-02-26 22:15:20 +01:00
Loïc Hoguin
f40ba44364 psu_proto: Move send_1213 to psu_proto without reviewing it. 2011-02-26 22:09:09 +01:00
Loïc Hoguin
d0e15316b8 psu_proto: Move send_1212 to psu_proto without reviewing it. 2011-02-26 22:00:08 +01:00
Loïc Hoguin
4848b3c218 psu_proto: Move send_1211 to psu_proto without reviewing it. 2011-02-26 21:57:30 +01:00
Loïc Hoguin
7415da1c89 psu_proto: Move send_1207 to psu_proto without reviewing it. 2011-02-26 21:32:35 +01:00
Loïc Hoguin
e8c1c98824 psu_proto: Move send_1206 to psu_proto without reviewing it. 2011-02-26 21:29:32 +01:00
Loïc Hoguin
fde0f8b3fe psu_proto: Move send_1205 to psu_proto without reviewing it. 2011-02-26 18:44:42 +01:00
Loïc Hoguin
82e82503fa psu_proto: Move send_1202 to psu_proto without reviewing it. 2011-02-26 18:39:42 +01:00
Loïc Hoguin
fde008c7a8 psu_proto: Move send_1113 to psu_proto without reviewing it. 2011-02-26 18:36:28 +01:00
Loïc Hoguin
564ab8749f psu_proto: Move send_110e to psu_proto without reviewing it. 2011-02-26 18:30:40 +01:00
Loïc Hoguin
6dc9fb52dc psu_proto: Move send_101a to psu_proto without reviewing it. 2011-02-26 18:27:31 +01:00
Loïc Hoguin
0b02718faa psu_proto: Move send_1015 to psu_proto without reviewing it. 2011-02-26 18:25:04 +01:00
Loïc Hoguin
333e898bb4 psu_proto: Move send_100f to psu_proto without reviewing it. 2011-02-26 18:15:53 +01:00
Loïc Hoguin
64d8bf7c25 psu_proto: Move send_0c09 to psu_proto without reviewing it. 2011-02-26 18:09:07 +01:00
Loïc Hoguin
a44fc4274f psu_proto: Move send_0c02 to psu_proto without reviewing it. 2011-02-26 18:03:55 +01:00
Loïc Hoguin
5f0bd73303 psu_proto: Move send_022c to psu_proto without reviewing it. 2011-02-26 17:38:07 +01:00
Loïc Hoguin
19350ba1ff egs_game: Stop using psu_game:header. 2011-02-26 17:25:31 +01:00
Loïc Hoguin
998263b417 egs_login: Use pattern matching for system_client_version_info. 2011-02-26 17:16:42 +01:00
Loïc Hoguin
b6c1bf277d egs_users: Rename stateu into state. 2011-02-26 17:03:25 +01:00
Loïc Hoguin
86bb5c81b3 Rename the client state record #state into #client for clarity. 2011-02-26 17:00:41 +01:00
Loïc Hoguin
d69fe073a8 egs_users: Introduce broadcast_all/1 to broadcast messages to all online users. 2011-02-21 02:30:39 +01:00
Loïc Hoguin
6c8b831fd2 egs_users: Replace read({pid, Pid}) calls by the new find_by_pid/1 function. 2011-02-21 01:34:54 +01:00
Loïc Hoguin
953da28a3e egs_game_server: Make sure the egs_users:read can't fail in on_exit. 2011-02-21 00:58:14 +01:00
Loïc Hoguin
344c534812 egs_login_server: Remove the unused on_exit/1. 2011-02-21 00:37:15 +01:00
Loïc Hoguin
3280e79743 egs_game: The egs_users:read/1 call can't fail. Simplify the cast/3 function. 2011-02-21 00:12:47 +01:00
Loïc Hoguin
6e922a7ec9 egs_users: Remove broadcast_unspawn/2 in favor of broadcast/2. 2011-02-21 00:07:37 +01:00
Loïc Hoguin
823ee73e7d egs_users: Remove broadcast_spawn/2 in favor of broadcast/2. 2011-02-21 00:04:23 +01:00
Loïc Hoguin
40d2eed01b egs_users: Remove select({neighbors, User}). Use egs_zones for broadcasting. 2011-02-20 23:39:56 +01:00
Loïc Hoguin
ac8d6858cd egs_users: Remove the unused select(all) function. 2011-02-20 23:06:32 +01:00
Loïc Hoguin
e409241a50 egs_game: Counter zone data is just many 0s. Don't use a file for that. 2011-02-20 20:24:21 +01:00
Loïc Hoguin
25c9548ec3 Merge a few .gitignore into the top-level one and update it. 2011-02-20 20:14:42 +01:00
Loïc Hoguin
0b8c4dbd85 egs_users: Remove mnesia from users handling. Convert to a gen_server.
Mnesia has now been fully removed from EGS. It will be replaced by Riak
at a later time, when we need to store permanent data to disk.
2011-02-20 20:00:04 +01:00
Loïc Hoguin
8eae404797 Fix dependency handling in start.sh. 2011-02-20 19:47:07 +01:00
Loïc Hoguin
840db6b7b3 egs_accounts: Fix the type spec for #state.accounts. 2011-02-20 17:39:02 +01:00
Loïc Hoguin
d5b5afa0a7 Use ex_reloader instead of reloader. Include it as a rebar dependency. 2011-02-20 15:30:35 +01:00
Loïc Hoguin
344b88eec4 egs_accounts: Add tmp_gid/0. Remove the table/record counters. 2011-02-20 02:42:40 +01:00
Loïc Hoguin
69a07dfad2 egs_accounts: Remove mnesia for accounts handling. Convert to a gen_server. 2011-02-20 02:01:16 +01:00
Loïc Hoguin
57e4e91187 Remove all references to the unused psu_object record/table. 2011-02-20 01:15:05 +01:00
Loïc Hoguin
722e0a53f4 reloader: Update to the latest HEAD version. 2011-02-20 00:40:56 +01:00
Loïc Hoguin
0f64bea72d egs_network: Match as binary instead of bits to avoid calculating the bit size. 2011-02-20 00:37:18 +01:00
Loïc Hoguin
b380fe9d23 egs_prs: Replace an exit call by the more appropriate erlang:nif_error/1. 2011-02-20 00:20:23 +01:00
Loïc Hoguin
ecee1226aa Cleanup: Replace a lot of 'little-unsigned-integer' by 'little' for binaries. 2011-02-20 00:18:14 +01:00
Loïc Hoguin
378e9a9927 egs_game_server: Properly leave the zone when the user is disconnecting. 2011-02-19 22:23:42 +01:00
Loïc Hoguin
2de4359c32 Create the user at character selection rather than login. 2011-02-19 21:21:35 +01:00
Loïc Hoguin
b728731830 Properly handle the LID per player and per zone. Remove related hacks. 2011-02-19 19:45:50 +01:00
Loïc Hoguin
1be3c4f5c6 Do not load the quest or zone nbl unless we actually need to send it. 2011-02-19 17:04:26 +01:00
Loïc Hoguin
e7246271ad users: Remove the setid property. Sets are fully handled by the zone now. 2011-02-19 16:58:09 +01:00
Loïc Hoguin
ad7071a61b psu_game: Load the players already in the zone on zone change. 2011-02-19 16:05:01 +01:00
Loïc Hoguin
50c2a2615f egs_zones: Broadcast spawn/unspawn directly from enter/leave. 2011-02-19 15:26:54 +01:00
Loïc Hoguin
afb888e566 egs_game: Leave the lobby's zone when entering a counter. 2011-02-19 13:31:38 +01:00
Loïc Hoguin
326b356d02 egs_zones: Add enter and leave functions for players entering/leaving the zone. 2011-02-18 02:52:25 +01:00
Loïc Hoguin
9d10c28504 egs_quests: Add the function zone_pid to retrieve a quest's zone pid. 2011-02-17 23:35:27 +01:00
Loïc Hoguin
06a032652a egs_universes: Add the function lobby_pid to retrieve a uni's lobby pid. 2011-02-17 23:17:11 +01:00
Loïc Hoguin
a4d9c5a35d egs_zones: Build a list of indexed and target objects, saved in the state. 2011-02-17 20:12:45 +01:00
Loïc Hoguin
526f281e2b egs_zones: Roughly initialize all the objects available so far. 2011-02-17 17:12:57 +01:00
Loïc Hoguin
bf7fa44897 egs_zones: Iterate through the set file to later create objects. 2011-02-16 23:08:28 +01:00
Loïc Hoguin
11a03f3a96 egs_quests_db: Fix crash when trying to load the set configuration file. 2011-02-16 21:59:33 +01:00
Loïc Hoguin
7e42b9b26a egs_zones: Load the set configuration at process initialization. 2011-02-16 00:09:46 +01:00
Loïc Hoguin
a1b8b1909e egs_zones: Determine which set to use at process initialization. 2011-02-15 23:26:48 +01:00
Loïc Hoguin
8f069e72d8 egs_sup: Delete the useless upgrade function. 2011-02-15 02:46:36 +01:00
Loïc Hoguin
9247e51fb7 Add egs_servers_sup to properly handle dependencies on egs_conf at startup. 2011-02-15 02:46:11 +01:00
Loïc Hoguin
ea25d1bd74 Universes start lobby quests automatically which in turn start their zones.
Universes are all stored in a single process; on the other hand, each quest
and each zone have their own process.
2011-02-15 01:15:28 +01:00
Loïc Hoguin
ffd27bda46 Rename egs_quests_db:quest and :zone into :quest_nbl and :zone_nbl. 2011-02-14 13:04:26 +01:00
Loïc Hoguin
fdfd49179f Remove psu_instance; to be replaced with egs_quests and egs_zones. 2011-02-13 19:26:08 +01:00
Loïc Hoguin
0989664035 Cleanup area loading code. Remove myroom, mission and spaceport support. 2011-02-13 18:06:25 +01:00
Loïc Hoguin
74f54e7e71 Move the server startup command into start.sh. 2011-02-13 12:20:23 +01:00
Loïc Hoguin
a484de77e0 egs_users: Rename id into gid inside the users record. 2011-02-13 00:35:05 +01:00
Loïc Hoguin
098ad5243a Get rid of psu_area; use a normal tuple instead (for now). 2011-02-12 23:28:56 +01:00
Loïc Hoguin
05149483c8 Use a normal tuple instead of a record for the pos values. 2011-02-12 20:12:48 +01:00
Loïc Hoguin
4dcaa79371 Add types to the users record and convert egs_users into a functions module.
The egs_user_model module was renamed into egs_users.
The egs_user_model record was renamed into users.
2011-02-12 19:58:53 +01:00
Loïc Hoguin
4171f2eba4 Convert egs_accounts from a gen_server to a functions module. 2011-02-12 17:19:39 +01:00
Loïc Hoguin
bd4a296b0a records: Define types for state and accounts. 2011-02-12 15:53:55 +01:00
Loïc Hoguin
d022584300 egs_files: Add a type_counter object. 2011-02-07 02:36:05 +01:00
Loïc Hoguin
1957ad0f1d rebar: Update to today's git. Fix clean issues. 2011-01-27 01:07:32 +01:00
Loïc Hoguin
3a2bb34cbe egs_prs: The Erlang include file was a full path preventing compilation on most systems. 2011-01-01 15:20:40 +01:00
Loïc Hoguin
cedc2f50d7 docs: Add command 1214: enable/disable season. 2011-01-01 15:20:17 +01:00
Loïc Hoguin
8526b5ab8f patch: Add a fully working patch server and replace the old hack with it. 2010-12-27 22:16:06 +01:00
Loïc Hoguin
d043ab4d3d egs_game_server: On exit, don't try to leave the uni if it wasn't entered. 2010-12-25 14:59:57 +01:00
Loïc Hoguin
d5178a313e quests: Oops. Club exit isn't a stair. 2010-12-12 17:08:52 +01:00
Loïc Hoguin
411763faf5 quests: Add the club (map 103) to zone 0 on the colony lobby. 2010-12-12 01:49:51 +01:00
Loïc Hoguin
f867dfbb72 egs_script_lexer: Add 4 syscalls for retrieving current questid, areaid, zoneid and mapid. 2010-12-12 01:18:52 +01:00
Loïc Hoguin
207b0b04cc egs_files: Make the standup move distance configurable for chairs. 2010-12-12 01:18:24 +01:00
Loïc Hoguin
c378397181 Makefile: Don't delete the generated lexer/parser after all. 2010-12-11 16:29:37 +01:00
Loïc Hoguin
3eb3d4dc4d quests: Add the missing static objects to colony zone 0 4th floor. 2010-12-11 16:23:17 +01:00
Loïc Hoguin
67b38a3316 maps: Use the EGS colony zone 0 by default. 2010-12-11 15:51:54 +01:00
Loïc Hoguin
92f3c2fbc5 egs_files: NPC data had an extra byte preventing following groups from being loaded. 2010-12-11 15:45:21 +01:00
Loïc Hoguin
6335c39403 quests: Add zone 0 to quest 1100000 (colony). 2010-12-11 15:44:37 +01:00
Loïc Hoguin
c161f8c48a scripts: Big update to the scripts lexer, parser and compilers.
Supports most syscalls, functions and opcodes. Includes a few bug fixes.
2010-12-11 14:38:13 +01:00
Loïc Hoguin
8504352ef7 egs_files: Add the sensor object. 2010-12-11 14:36:47 +01:00
Loïc Hoguin
9b501e998b quests: Cleanup the colony warps. 2010-12-11 14:35:49 +01:00
Loïc Hoguin
9f3f815be8 Remove useless rebar.config. 2010-12-05 10:40:58 +01:00
Loïc Hoguin
3a45b43354 egs_files: Compress the NBL data automatically when its size is >= 16#800. 2010-11-28 16:32:28 +01:00
Loïc Hoguin
3b05e4dfe3 egs_quests_db: Initial zone file support. Handle 1 set + script and text files. 2010-11-27 23:03:04 +01:00
Loïc Hoguin
401ffe5194 priv/*.hrl: Missing closing parenthesis. 2010-11-27 18:59:24 +01:00
Loïc Hoguin
48728337c7 egs_network: Stay on the old SSL implementation until the new one works better. 2010-11-17 23:36:35 +01:00
Loïc Hoguin
d7f41a8ee5 egs_prs: C module implementing PRS compression from fuzziqer. 2010-11-17 21:36:05 +01:00
Loïc Hoguin
bd6b5632f1 The server now requires Erlang R14B to run. 2010-11-17 21:31:33 +01:00
Loïc Hoguin
c23c7e56fa Makefile: Don't keep the generated lexer and parser files after compilation. 2010-11-17 02:23:04 +01:00
Loïc Hoguin
9efb734c63 quests: Convert the zones to a proplist of proplists rather than a list of proplists. 2010-11-17 02:19:10 +01:00
Loïc Hoguin
7fadf362b3 Initial script lexer, parser and compiler support. 2010-11-17 01:30:07 +01:00
Loïc Hoguin
71772a58fb quests: Convert the Counters to configuration files. 2010-11-08 22:03:16 +01:00
Loïc Hoguin
1e83a98d22 quests: Convert the Spaceport to configuration files. 2010-11-08 19:24:08 +01:00
Loïc Hoguin
681d61e73c quests: Convert the Moatoob lobbies to configuration files. 2010-11-08 05:05:38 +01:00
Loïc Hoguin
e9fc339bc2 quests: Convert the Neudaiz lobbies to configuration files. 2010-11-08 03:07:00 +01:00
Loïc Hoguin
ccf3c6f55e quests: Convert the Parum lobbies to configuration files. 2010-11-08 01:38:18 +01:00
Loïc Hoguin
2ce9ec0857 egs_char_select: Set back the entrance to colony 4th floor. 2010-11-08 01:37:00 +01:00
Loïc Hoguin
4e12f5bd5f quests: Remove the useless parameters. 2010-11-08 00:47:59 +01:00
Loïc Hoguin
f6819a2f95 Updated rebar. 2010-11-07 20:08:40 +01:00
Loïc Hoguin
eafeb744af Introduce egs_quests_db for quest building and handling. 2010-11-07 19:51:42 +01:00
Loïc Hoguin
fc6c424536 Rename egs_counters to egs_counters_db. 2010-11-06 03:24:55 +01:00
Loïc Hoguin
1e264d3e1b Update egs.app.src and let rebar fill in the modules. 2010-11-06 03:20:05 +01:00
Loïc Hoguin
35bcf4595e egs_files: Move the nbl packing functions to egs_files. 2010-11-06 01:27:16 +01:00
Loïc Hoguin
dc2968cbce egs_counters: Move the file loading functions into their own module, egs_files. 2010-11-06 01:20:14 +01:00
Loïc Hoguin
9528c521b3 Remove the counters folder from data/ now that they're all converted. 2010-10-30 01:43:16 +02:00
Loïc Hoguin
01c02b24ef counters: All undefined counters now have a dummy default. Remove old counter code. 2010-10-28 02:56:28 +02:00
Loïc Hoguin
3fa3d27915 Rename doc to docs because rebar cleans doc automatically. 2010-10-28 02:39:23 +02:00
Loïc Hoguin
0a14def994 counters: Add the Moatoob GUARDIANS counter. 2010-10-28 02:37:26 +02:00
Loïc Hoguin
1c8dbd0a69 counters: Add the two tutorial counters. 2010-10-28 02:13:47 +02:00
Loïc Hoguin
8c0683c07b egs_app: Return ok on db_init. 2010-10-28 01:34:52 +02:00
Loïc Hoguin
79e9e39fba maps: Fix the QuestID for 3 boss test missions. 2010-10-28 01:34:25 +02:00
Loïc Hoguin
6f13319790 doc: Update commands file. 2010-10-25 02:47:12 +02:00
Loïc Hoguin
03ec8a4562 counters: Add the two Linear Line counters. 2010-10-25 02:33:14 +02:00
Loïc Hoguin
3f97b24a1d counters: Add the Rykros counters. 2010-10-23 23:22:39 +02:00
Loïc Hoguin
9e36068382 counters: Add the two HIVE #3 counters. 2010-10-23 22:57:13 +02:00
Loïc Hoguin
103fe3f8a9 Bump to 0.8.0. 2010-10-23 04:02:58 +02:00
Loïc Hoguin
71d2c22028 doc: Add the documentation for most of the commands reviewed so far. 2010-10-23 04:02:15 +02:00
Loïc Hoguin
814d4348f5 psu_proto: Remove a useless @todo. 2010-10-23 04:01:47 +02:00
Loïc Hoguin
a0d6e67e0e egs_game: Properly handle the spawning of individual players instead of sending everything each time. 2010-10-22 04:04:21 +02:00
Loïc Hoguin
d6b4b24388 psu_proto: Review and move send_0233 to psu_proto. 2010-10-22 02:35:16 +02:00
Loïc Hoguin
22278250b9 egs_game: Handle counter_join_party_request and reply. No parties listed yet though. 2010-10-21 23:55:01 +02:00
Loïc Hoguin
26d5763df3 psu_proto: Review and move send_1500 to psu_proto. 2010-10-21 23:30:26 +02:00
Loïc Hoguin
a86e4caf0d psu_proto: Review and move send_0c08 to psu_proto. 2010-10-21 22:56:18 +02:00
Loïc Hoguin
31dc7ffb7f psu_proto: Review and move send_0204 to psu_proto. 2010-10-21 22:31:06 +02:00
Loïc Hoguin
94ce201941 psu_proto: Review and move send_0d01 to psu_proto. 2010-10-21 19:39:11 +02:00
Loïc Hoguin
4ff129224f psu_proto: Review and move send_1005 to psu_proto. 2010-10-21 18:20:43 +02:00
Loïc Hoguin
c91880be1f Introduce egs_universes for universe handling. Review and move send_021e and send_0222 to psu_proto. 2010-10-21 17:00:30 +02:00
Loïc Hoguin
a6563c7378 egs_npc_db: Remove count/0, it's never needed standalone. 2010-10-19 18:47:26 +02:00
Loïc Hoguin
94e902d07e psu_proto: Review and move send_0230 to psu_proto. 2010-10-19 18:37:05 +02:00
Loïc Hoguin
b9f407bb1e psu_proto: Review and move send_0304 to psu_proto. 2010-10-19 18:12:25 +02:00
Loïc Hoguin
8bcf1ba3c8 psu_proto: Review and move send_1711 to psu_proto. 2010-10-19 00:20:44 +02:00
Loïc Hoguin
393ee96012 maps: Fix the entry point for Cargo Train Rescue. 2010-10-19 00:14:59 +02:00
Loïc Hoguin
af29f8a460 egs_counters: 255 is the default background for most counters. 2010-10-19 00:13:58 +02:00
Loïc Hoguin
d99c5388b9 egs_login: Don't save accounts that logged in during the event anymore! 2010-10-18 15:23:09 +02:00
406 changed files with 17351 additions and 7154 deletions

7
.gitignore vendored
View File

@ -1 +1,6 @@
Mnesia*
apps/*/c_src/*.o
apps/*/ebin
apps/*/priv/*.so
apps/egs/src/egs_script_lexer.erl
apps/egs/src/egs_script_parser.erl
deps

View File

@ -1,5 +1,5 @@
# EGS: Erlang Game Server
# Copyright (C) 2010 Loic Hoguin
# Copyright (C) 2010-2011 Loic Hoguin
#
# This file is part of EGS.
#
@ -16,29 +16,23 @@
# You should have received a copy of the GNU Affero General Public License
# along with EGS. If not, see <http://www.gnu.org/licenses/>.
ERL ?= erl
ERLC ?= erlc
APP := egs
REBAR = rebar
all: clean missions server
all: app
server:
@./rebar compile
app: deps
@$(REBAR) compile
missions:
$(ERLC) src/psu/psu_parser.erl
$(ERL) -noshell -noinput -sname missions -pa ebin -run psu_parser run -run init stop
rm psu_parser.beam
deps:
@$(REBAR) get-deps
clean:
@./rebar clean
@$(REBAR) clean
rm -f erl_crash.dump
fclean: clean
rm -rf Mnesia.egs*
tests:
@$(REBAR) eunit
@$(REBAR) ct
run:
@echo "EGS is free software available under the GNU GPL version 3"
@echo "Copyright (C) 2010 Loic Hoguin"
@echo
$(ERL) -ssl protocol_version '{sslv3}' -sname egs -pa ebin -boot start_sasl -s reloader -s egs
dialyze:
@$(REBAR) dialyze

2
README
View File

@ -10,4 +10,6 @@ This is a very early work in progress. It does not include any data files
yet, meaning one cannot at this point run a server without using the
proprietary files from AOTI.
The server requires Erlang R14B to compile and run.
Use 'make' to compile, 'make run' to run and 'make fclean' to cleanup.

View File

@ -1,5 +1,5 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin.
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc Project-wide Erlang records.
%%
%% This file is part of EGS.
@ -17,13 +17,67 @@
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
%% @doc Per-process state used by the various EGS modules.
-record(state, {socket, gid, slot, lid=16#ffff, areanb=0}).
%% Records.
%% @doc Accounts. So far only used for storing temporary information.
%% @todo Hash the password.
%% @todo Add email, password_salt, is_ingame, register_time, last_login_time, etc.
-record(accounts, {gid, username, password, auth_state}).
%% @doc Client state. One per connected client.
-record(egs_net, {
socket :: ssl:sslsocket(),
transport :: module(),
handler :: module(),
buffer = <<>> :: binary(),
keepalive = false :: boolean(),
gid = 0 :: egs:gid(),
lid = 16#ffff :: egs:lid(),
slot = 0 :: 0..3,
areanb = 0 :: non_neg_integer()
}).
%% @doc Table containing the users currently logged in.
%% @todo Probably can use a "param" or "extra" field to store the game-specific information (for things that don't need to be queried).
-record(users, {
%% General information.
gid :: egs:gid(),
lid = 16#ffff :: egs:lid(),
pid :: pid(),
%% Character information.
%% @todo Specs it.
type = white,
slot,
npcid = 16#ffff,
name,
race,
gender,
class,
level = 1,
exp = 0,
currenthp = 100,
maxhp = 100,
stats = {stats, 1000, 2000, 3000, 4000, 5000, 6000, 7000},
money = 1000000,
blastbar = 0,
luck = 3,
appearance,
onlinestatus = 0,
options = {options, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0},
inventory = [],
%% Location/state related information.
uni :: integer(),
questpid :: pid(),
zonepid :: pid(),
partypid :: pid(),
areatype :: counter | mission | lobby | myroom,
area :: egs:area(),
entryid :: egs:entryid(),
pos = {0.0, 0.0, 0.0, 0.0} :: egs:position(),
shopid :: integer(),
prev_area = {0, 0, 0} :: egs:area(),
prev_entryid = 0 :: egs:entryid(),
%% To be moved or deleted later on.
instancepid :: pid(),
char
}).
%% Past this point needs to be reviewed.
%% @doc NPC configuration data.
%% @todo Add inventory, AI parameters.
@ -57,36 +111,6 @@
faceboxx=65535, faceboxy=65535
}).
%% Past this point needs to be reviewed.
%% @doc Table containing counters current values.
-record(counters, {name, id}).
%% @doc Character position data structure.
-record(pos, {x, y, z, dir}).
%% @doc Character area location data structure.
-record(psu_area, {questid, zoneid, mapid}).
%% @doc Table containing the users currently logged in.
%% @todo Probably can use a "param" or "extra" field to store the game-specific information (for things that don't need to be queried).
-record(egs_user_model, {
%% General information.
id, lid, pid, time, character,
%% Location/state related information.
instancepid, partypid, areatype, area, entryid, pos=#pos{x=0.0, y=0.0, z=0.0, dir=0.0}, shopid,
prev_area=#psu_area{questid=0, zoneid=0, mapid=0}, prev_entryid=0, %% universeid
%% To be moved or deleted later on.
setid=0 %% @todo Current area's set number. Move that to psu_instance probably.
}).
%% @doc Character main or class level data structure.
-record(level, {number, exp}).
%% @doc Character stats data structure.
-record(stats, {atp, ata, tp, dfp, evp, mst, sta}).
@ -97,38 +121,6 @@
cutindisplay, mainmenucursorposition, camera3y, camera3x, camera1y, camera1x, controller, weaponswap,
lockon, brightness, functionkeysetting, buttondetaildisplay}).
%% @doc Characters data structure.
%% @todo Make a disk table for storing characters permanently. Also keep the current character in #users.
-record(characters, {
gid,
type=white,
slot,
npcid=16#ffff,
name,
race,
gender,
class,
mainlevel={level, 1, 0},
classlevels,
currenthp=100,
maxhp=100,
stats={stats, 1000, 2000, 3000, 4000, 5000, 6000, 7000},
se=[],
money=1000000,
blastbar=0,
luck=3,
playtime=0,
appearance,
onlinestatus=0,
options={options, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0},
inventory=[]
}). % also: shortcuts partnercards blacklist npcs flags...
%% @doc Table containing all mission objects.
-record(psu_object, {id, instancepid, type, args}).
%% @doc Hit response data.
-record(hit_response, {type, user, exp, damage, targethp, targetse, events}).

6
apps/egs/rebar.config Normal file
View File

@ -0,0 +1,6 @@
{deps, [
{erlson, ".*", {git, "https://github.com/alavrik/erlson.git", "HEAD"}},
{ex_reloader, ".*", {git, "git://github.com/extend/ex_reloader.git", "HEAD"}},
{cowboy, ".*", {git, "git://github.com/extend/cowboy.git", "HEAD"}}
]}.
{plugins, [erlson_rebar_plugin]}.

16
apps/egs/src/egs.app.src Normal file
View File

@ -0,0 +1,16 @@
%%-*- mode: erlang -*-
{application, egs, [
{description, "EGS online action-RPG game server"},
{vsn, "0.14"},
{modules, []},
{registered, []},
{applications, [
kernel,
stdlib,
crypto,
ssl,
cowboy
]},
{mod, {egs_app, []}},
{env, []}
]}.

85
apps/egs/src/egs.erl Normal file
View File

@ -0,0 +1,85 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS startup code and utility functions.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs).
-export([start/0, stop/0, global/1, warp/4, warp/5]). %% API.
%% Player and account-related types.
-type gid() :: 0..16#ffffffff.
-type lid() :: 0..1023 | 16#ffff.
-type character_slot() :: 0..3.
-export_type([gid/0, lid/0, character_slot/0]).
%% Location related types.
-type uniid() :: 21 | 26..254 | 16#ffffffff.
-type questid() :: 0..16#ffffffff. %% @todo What's the real max?
-type zoneid() :: 0..16#ffff. %% @todo What's the real max?
-type mapid() :: 0..9999.
-type entryid() :: 0..16#ffff. %% @todo What's the real max?
-type area() :: {questid(), zoneid(), mapid()}. %% @todo Probably remove later.
-type position() :: {X::float(), Y::float(), Z::float(), Dir::float()}.
-export_type([uniid/0, questid/0, zoneid/0, mapid/0, entryid/0,
area/0, position/0]).
%% API.
-spec start() -> ok.
start() ->
ensure_started(crypto),
ensure_started(public_key),
ensure_started(ssl),
ensure_started(cowboy),
application:start(egs).
-spec stop() -> ok.
stop() ->
Res = application:stop(egs),
ok = application:stop(cowboy),
ok = application:stop(ssl),
ok = application:stop(public_key),
ok = application:stop(crypto),
Res.
%% @doc Send a global message.
-spec global(string()) -> ok.
global(Message) when length(Message) > 511 ->
io:format("global: message too long~n");
global(Message) ->
egs_users:broadcast_all({egs, notice, top, Message}).
%% @doc Warp all players to a new map.
-spec warp(questid(), zoneid(), mapid(), entryid()) -> ok.
warp(QuestID, ZoneID, MapID, EntryID) ->
egs_users:broadcast_all({egs, warp, QuestID, ZoneID, MapID, EntryID}).
%% @doc Warp one player to a new map.
-spec warp(gid(), questid(), zoneid(), mapid(), entryid()) -> ok.
warp(GID, QuestID, ZoneID, MapID, EntryID) ->
egs_users:broadcast({egs, warp, QuestID, ZoneID, MapID, EntryID}, [GID]).
%% Internal.
-spec ensure_started(module()) -> ok.
ensure_started(App) ->
case application:start(App) of
ok -> ok;
{error, {already_started, App}} -> ok
end.

View File

@ -0,0 +1,143 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc Accounts handling.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_accounts).
-behaviour(gen_server).
-export([start_link/0, stop/0, get_folder/1, key_auth/2, key_auth_init/1, key_auth_timeout/1, login_auth/2, tmp_gid/0]). %% API.
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
-define(SERVER, ?MODULE).
%% @todo Make accounts permanent.
%% @todo Hash the password.
%% @todo Add email, password_salt, is_ingame, register_time, last_login_time, etc.
-record(accounts, {
gid :: egs:gid(),
username :: string(),
password :: string(),
auth_state :: undefined | {wait_for_authentication, binary(), any()}
}).
-record(state, {
accounts = [] :: list({egs:gid(), #accounts{}}),
next_gid = 10000001 :: integer(),
tmp_gid = 16#ff000001 :: integer()
}).
%% API.
-spec start_link() -> {ok, Pid::pid()}.
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
-spec stop() -> stopped.
stop() ->
gen_server:call(?SERVER, stop).
%% @todo Temporary code until we properly save the player data.
get_folder(GID) ->
gen_server:call(?SERVER, {get_folder, GID}).
-spec key_auth(egs:gid(), AuthKey::binary()) -> ok | {error, badarg}.
%% @doc Authenticate using the given key.
key_auth(GID, AuthKey) ->
gen_server:call(?SERVER, {key_auth, GID, AuthKey}).
-spec key_auth_init(egs:gid()) -> {ok, AuthKey::binary()}.
%% @doc Initialize key authentication. Obtain a key for a subsequent re-authentication on a different connection.
key_auth_init(GID) ->
gen_server:call(?SERVER, {key_auth_init, GID}).
-spec key_auth_timeout(egs:gid()) -> ok.
%% @doc Key authentication timeout handling.
%% @todo Probably handle the authentication in a gen_fsm properly.
key_auth_timeout(GID) ->
gen_server:cast(?SERVER, {key_auth_timeout, GID}).
-spec login_auth(Username::binary(), Password::binary()) -> {ok, GID::integer()}.
%% @doc Authenticate using the given username and password.
%% @todo Properly handle login authentication when accounts are saved.
login_auth(Username, Password) ->
gen_server:call(?SERVER, {login_auth, Username, Password}).
-spec tmp_gid() -> egs:gid().
%% @doc Return an unused temporary GID for initial connection and APC characters.
tmp_gid() ->
gen_server:call(?SERVER, tmp_gid).
%% gen_server.
init([]) ->
{ok, #state{}}.
handle_call({get_folder, GID}, _From, State) ->
{_, #accounts{username=Username, password=Password}} = lists:keyfind(GID, 1, State#state.accounts),
{reply, << Username/binary, "-", Password/binary >>, State};
handle_call({key_auth, GID, AuthKey}, _From, State) ->
{_, Account = #accounts{auth_state=AuthState}} = lists:keyfind(GID, 1, State#state.accounts),
case AuthState of
{wait_for_authentication, AuthKey, TRef} ->
timer:cancel(TRef),
Accounts = lists:delete({GID, Account}, State#state.accounts),
{reply, ok, State#state{accounts=[{GID, Account#accounts{auth_state=undefined}}|Accounts]}};
undefined ->
{reply, {error, badarg}, State}
end;
handle_call({key_auth_init, GID}, _From, State) ->
AuthKey = crypto:rand_bytes(4),
TRef = timer:apply_after(10000, ?MODULE, key_auth_timeout, [GID]),
{_, Account} = lists:keyfind(GID, 1, State#state.accounts),
Accounts = lists:delete({GID, Account}, State#state.accounts),
{reply, {ok, AuthKey}, State#state{accounts=
[{GID, Account#accounts{auth_state={wait_for_authentication, AuthKey, TRef}}}|Accounts]}};
handle_call({login_auth, Username, Password}, _From, State) ->
GID = State#state.next_gid,
Account = #accounts{gid=GID, username=Username, password=Password},
{reply, {ok, GID}, State#state{next_gid=GID + 1, accounts=[{GID, Account}|State#state.accounts]}};
handle_call(tmp_gid, _From, State) ->
GID = State#state.tmp_gid,
{reply, GID, State#state{tmp_gid=GID + 1}};
handle_call(stop, _From, State) ->
{stop, normal, stopped, State};
handle_call(_Request, _From, State) ->
{reply, ignored, State}.
handle_cast({key_auth_timeout, GID}, State) ->
{_, Account} = lists:keyfind(GID, 1, State#state.accounts),
Accounts = lists:delete({GID, Account}, State#state.accounts),
{noreply, State#state{accounts= [{GID, Account#accounts{auth_state=undefined}}|Accounts]}};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.

58
apps/egs/src/egs_app.erl Normal file
View File

@ -0,0 +1,58 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc Callbacks for the egs application.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_app).
-behaviour(application).
-export([start/2, stop/1]). %% API.
-type application_start_type()
:: normal | {takeover, node()} | {failover, node()}.
-define(SSL_OPTIONS, [{certfile, "priv/ssl/servercert.pem"},
{keyfile, "priv/ssl/serverkey.pem"}, {password, "alpha"}]).
%% API.
-spec start(application_start_type(), term()) -> {ok, pid()}.
start(_Type, _StartArgs) ->
{ok, Pid} = egs_sup:start_link(),
application:set_env(egs_patch, patch_ports, egs_conf:read(patch_ports)),
application:start(egs_patch),
application:start(egs_store),
start_login_listeners(egs_conf:read(login_ports)),
{_ServerIP, GamePort} = egs_conf:read(game_server),
{ok, _GamePid} = cowboy:start_listener({game, GamePort}, 10,
cowboy_ssl_transport, [{port, GamePort}] ++ ?SSL_OPTIONS,
egs_game_protocol, []),
{ok, Pid}.
-spec stop(term()) -> ok.
stop(_State) ->
ok.
%% Internal.
-spec start_login_listeners([inet:ip_port()]) -> ok.
start_login_listeners([]) ->
ok;
start_login_listeners([Port|Tail]) ->
{ok, _Pid} = cowboy:start_listener({login, Port}, 10,
cowboy_ssl_transport, [{port, Port}] ++ ?SSL_OPTIONS,
egs_login_protocol, []),
start_login_listeners(Tail).

64
apps/egs/src/egs_char.erl Normal file
View File

@ -0,0 +1,64 @@
-module(egs_char).
-export([new/6]).
-include_lib("erlson/include/erlson.hrl").
%% @todo Add the current location for backtopreviousfield
new(Slot, Name, Race, Gender, Class, Appearance) ->
#{
type=player,
slot=Slot,
name=Name,
race=Race,
gender=Gender,
class=Class,
appearance=Appearance,
level=1,
exp=0,
hunter_level=1,
hunter_exp=0,
ranger_level=1,
ranger_exp=0,
force_level=1,
force_exp=0,
acro_level=1,
acro_level=0,
blast_bar=0,
luck=3,
playtime=0,
%% current_uni,
%% current location + entryid
%% previous location + entryid
%% pids for such
money=1000000,
inventory=[],
card_comment= <<>>,
options=#{
brightness=4,
buttonhelp=0,
cam1stx=0,
cam1sty=0,
cam3rdx=0,
cam3rdy=0,
controller=0,
cursorpos=0,
cutin=0,
fnkeys=0,
lockon=0,
musicvolume=0,
radarmap=0,
sfxvolume=0,
sound=0,
textspeed=0,
vibration=0,
weaponswap=0
}}.

View File

@ -0,0 +1,86 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc Character selection callback module.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_char_select).
-export([info/2, cast/3, event/2]).
-include_lib("erlson/include/erlson.hrl").
-include("include/records.hrl").
%% @doc We don't expect any message here.
info(_Msg, _Client) ->
ok.
%% @doc Nothing to broadcast.
cast(_Command, _Data, _Client) ->
ok.
%% Events.
%% @doc Character screen selection request and delivery.
event(account_characters_request, Client) ->
{ok, Characters}
= egs_store:load_characters(egs_net:get_gid(Client), [0, 1, 2, 3]),
egs_net:account_characters_response(Characters, Client);
%% @todo Don't forget to check for the character's name (in egs_net).
%% 00F7 is the RGBA color control character.
%% 03F7 is the RGB color control character.
event({account_create_character, Slot, Name, Race, Gender, Class, Appearance},
Client) ->
Character = egs_char:new(Slot, Name, Race, Gender, Class, Appearance),
egs_store:save_character(egs_net:get_gid(Client), Slot, Character);
%% @doc Load the selected character into the game's default universe.
%% @todo Isn't very pretty to call egs_game from here but that will do for now.
event({system_character_select, Slot, _BackToPreviousField}, Client) ->
GID = egs_net:get_gid(Client),
{ok, 1, Char} = egs_store:load_character(GID, Slot),
UniID = egs_universes:defaultid(),
egs_universes:enter(UniID),
%% @todo Handle users properly, just giving Character directly.
Name = Char.name,
NameBin = << Name/binary, 0:(512 - bit_size(Name)) >>,
Race = Char.race,
Gender = Char.gender,
Class = Char.class,
Appearance = Char.appearance,
Options = Char.options,
User = #users{gid=GID, pid=self(), uni=UniID, slot=Slot,
name=NameBin, race=Race, gender=Gender, class=Class,
appearance=Appearance, options=Options,
area={1100000, 0, 4}, entryid=0, char=Char},
egs_users:write(User),
egs_users:item_add(GID, 16#11010000, #psu_special_item_variables{}),
egs_users:item_add(GID, 16#11020000, #psu_special_item_variables{}),
egs_users:item_add(GID, 16#11020100, #psu_special_item_variables{}),
egs_users:item_add(GID, 16#11020200, #psu_special_item_variables{}),
egs_users:item_add(GID, 16#01010900, #psu_striking_weapon_item_variables{
current_pp=99, max_pp=100, element=#psu_element{type=1, percent=50}}),
egs_users:item_add(GID, 16#01010a00, #psu_striking_weapon_item_variables{
current_pp=99, max_pp=100, element=#psu_element{type=2, percent=50}}),
egs_users:item_add(GID, 16#01010b00, #psu_striking_weapon_item_variables{
current_pp=99, max_pp=100, element=#psu_element{type=3, percent=50}}),
{ok, User2} = egs_users:read(GID),
egs_game:char_load(User2, Client),
Client2 = egs_net:set_handler(egs_game, Client),
{ok, Client2};
event({client_hardware, GPU, CPU}, Client) ->
ok.

View File

@ -1,5 +1,5 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin.
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS configuration gen_server.
%%
%% This file is part of EGS.
@ -19,41 +19,35 @@
-module(egs_conf).
-behavior(gen_server).
-export([start_link/0, stop/0, read/1, reload/0]). %% API.
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
%% Use the module name for the server's name.
-export([start_link/0, stop/0, read/1, reload/0]). %% API.
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]). %% gen_server.
-define(SERVER, ?MODULE).
%% API.
%% @spec start_link() -> {ok,Pid::pid()}
-spec start_link() -> {ok, pid()}.
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%% @spec stop() -> stopped
-spec stop() -> stopped.
stop() ->
gen_server:call(?SERVER, stop).
%% @spec read(Key) -> Value | undefined
-spec read(atom()) -> undefined | any().
read(Key) ->
gen_server:call(?SERVER, {read, Key}).
%% @spec reload() -> ok
-spec reload() -> ok.
reload() ->
gen_server:cast(?SERVER, reload).
%% gen_server.
init([]) ->
case file:consult("priv/egs.conf") of
{ok, Terms} ->
error_logger:info_report("egs_conf started"),
{ok, Terms};
Error ->
error_logger:error_report(["An error occurred when trying to load the configuration file:", Error]),
Error
end.
{ok, _Terms} = file:consult("priv/egs.conf").
handle_call({read, Key}, _From, State) ->
{reply, proplists:get_value(Key, State), State};

View File

@ -0,0 +1,106 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS counters database and cache manager.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_counters_db).
-behavior(gen_server).
-export([start_link/0, stop/0, bg/1, opts/1, pack/1, reload/0]). %% API.
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
%% Use the module name for the server's name.
-define(SERVER, ?MODULE).
%% API.
%% @spec start_link() -> {ok,Pid::pid()}
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%% @spec stop() -> stopped
stop() ->
gen_server:call(?SERVER, stop).
%% @spec bg(CounterID) -> integer()
bg(CounterID) ->
gen_server:call(?SERVER, {bg, CounterID}).
%% @spec opts(CounterID) -> binary()
opts(CounterID) ->
gen_server:call(?SERVER, {opts, CounterID}).
%% @spec pack(CounterID) -> binary()
pack(CounterID) ->
gen_server:call(?SERVER, {pack, CounterID}).
%% @spec reload() -> ok
reload() ->
gen_server:cast(?SERVER, reload).
%% gen_server.
init([]) ->
{ok, []}.
%% @doc Possible keys: bg, opts, pack.
handle_call({Key, CounterID}, _From, State) ->
{Counter, State2} = get_counter(CounterID, State),
{reply, proplists:get_value(Key, Counter), State2};
handle_call(stop, _From, State) ->
{stop, normal, stopped, State};
handle_call(_Request, _From, State) ->
{reply, ignored, State}.
handle_cast(reload, _State) ->
{noreply, []};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% Internal.
%% @doc Return a counter information either from the cache or from the configuration file,
%% in which case it gets added to the cache for subsequent attempts.
get_counter(CounterID, Cache) ->
case proplists:get_value(CounterID, Cache) of
undefined ->
Dir = io_lib:format("priv/counters/~b/", [CounterID]),
ConfFilename = Dir ++ "counter.conf",
{TableRelData, TableRelPtrs} = egs_files:load_table_rel(ConfFilename),
TextBinData = egs_files:load_text_bin(Dir ++ "text.bin.en_US.txt"),
CounterNbl = egs_files:nbl_pack([{files, [
{data, "table.rel", TableRelData, TableRelPtrs},
{data, "text.bin", TextBinData, []}
]}]),
Counter = egs_files:load_counter_pack(ConfFilename, CounterNbl),
Cache2 = [{CounterID, Counter}|Cache],
{Counter, Cache2};
Counter ->
{Counter, Cache}
end.

559
apps/egs/src/egs_files.erl Normal file
View File

@ -0,0 +1,559 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS file creation functions.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_files).
-export([load_counter_pack/2, load_quest_xnr/1, load_script_bin/1, load_set_rel/4, load_table_rel/1,
load_text_bin/1, load_unit_title_table_rel/2, nbl_pack/1, nbl_padded_size/1]).
%% @doc Build a counter's pack file, options and return them along with the background value.
load_counter_pack(ConfFilename, CounterNbl) ->
{ok, Settings} = file:consult(ConfFilename),
Groups = proplists:get_value(groups, Settings),
{FilesBin, PosList, SizeList} = load_counter_pack_groups(Groups, [0], [byte_size(CounterNbl)]),
NbFiles = length(PosList),
PosBin = iolist_to_binary([<< P:32/little >> || P <- PosList]),
SizeBin = iolist_to_binary([<< S:32/little >> || S <- SizeList]),
Padding = 8 * (512 - byte_size(PosBin)),
Pack = << PosBin/binary, 0:Padding, SizeBin/binary, 0:Padding, NbFiles:32/little, CounterNbl/binary, FilesBin/binary >>,
Opts = case length(Groups) of
0 -> << >>;
L ->
OptsList = lists:seq(1, L + length(PosList)),
iolist_to_binary([<< 3:8 >> || _N <- OptsList])
end,
Opts2 = if byte_size(Opts) rem 2 =:= 0 -> Opts; true -> << Opts/binary, 0 >> end,
[{bg, proplists:get_value(bg, Settings, 255)}, {opts, Opts2}, {pack, Pack}].
load_counter_pack_groups(Groups, PosList, SizeList) ->
load_counter_pack_groups(Groups, PosList, SizeList, []).
load_counter_pack_groups([], PosList, SizeList, Acc) ->
GroupsBin = iolist_to_binary(lists:reverse(Acc)),
{GroupsBin, lists:reverse(PosList), lists:reverse(SizeList)};
load_counter_pack_groups([Group|Tail], PosList, SizeList, Acc) ->
Quests = proplists:get_value(quests, Group),
{QuestsBin, PosList2, SizeList2} = load_counter_pack_quests(Quests, PosList, SizeList),
load_counter_pack_groups(Tail, PosList2, SizeList2, [QuestsBin|Acc]).
load_counter_pack_quests(Quests, PosList, SizeList) ->
load_counter_pack_quests(Quests, PosList, SizeList, []).
load_counter_pack_quests([], PosList, SizeList, Acc) ->
QuestsBin = iolist_to_binary(lists:reverse(Acc)),
{QuestsBin, PosList, SizeList};
load_counter_pack_quests([{_QuestID, Filename}|Tail], PosList, SizeList, Acc) ->
{ok, File} = file:read_file("data/missions/" ++ Filename),
Pos = lists:sum(SizeList),
Size = byte_size(File),
load_counter_pack_quests(Tail, [Pos|PosList], [Size|SizeList], [File|Acc]).
%% @doc Load a quest configuration file and return a quest.xnr binary along with its pointers array.
load_quest_xnr(Settings) ->
QuestID = proplists:get_value(questid, Settings),
%% Temp flags.
TmpFlagsList = proplists:get_value(temp_flags, Settings),
TmpFlagsBin = load_quest_xnr_flags(TmpFlagsList),
%% Value flags.
ValFlagsList = proplists:get_value(value_flags, Settings),
ValFlagsBin = load_quest_xnr_flags(ValFlagsList),
%% Bool flags.
BoolFlagsList = proplists:get_value(bool_flags, Settings),
BoolFlagsBin = load_quest_xnr_flags(BoolFlagsList),
%% Items.
ItemsPos = 16 + byte_size(TmpFlagsBin) + byte_size(ValFlagsBin) + byte_size(BoolFlagsBin),
ItemsList = proplists:get_value(items, Settings),
ItemsBin = load_quest_xnr_items(ItemsList),
NbItems = length(ItemsList),
%% Item pointers.
ItemsPtrsPos = ItemsPos + byte_size(ItemsBin),
ItemsPtrs = << ItemsPos:32/little, NbItems:32/little >>,
%% Zones.
ZonesBasePos = ItemsPtrsPos + byte_size(ItemsPtrs),
ZonesList = proplists:get_value(zones, Settings),
{SetsBin, SetsPtrsList, ZonesBin} = load_quest_xnr_zones(ZonesList, ZonesBasePos),
ZonesPos = ZonesBasePos + byte_size(SetsBin),
NbZones = length(ZonesList),
%% Warps.
WarpsPos = ZonesPos + byte_size(ZonesBin),
WarpsList = proplists:get_value(warps, Settings),
WarpsBin = load_quest_xnr_warps(QuestID, WarpsList),
NbWarps = length(WarpsList),
%% Temp flag pointers.
TmpFlagsPtrsPos = WarpsPos + byte_size(WarpsBin),
{NbTmpFlags, TmpFlagsPtrs} = load_quest_xnr_flag_ptrs(TmpFlagsList, 0),
TmpFlagsPtrsPos2 = if NbTmpFlags =/= 0 -> TmpFlagsPtrsPos; true -> 0 end,
%% Value flag pointers.
ValFlagsPtrsPos = TmpFlagsPtrsPos + byte_size(TmpFlagsPtrs),
{NbValFlags, ValFlagsPtrs} = load_quest_xnr_flag_ptrs(ValFlagsList, NbTmpFlags),
ValFlagsPtrsPos2 = if NbValFlags =/= 0 -> ValFlagsPtrsPos; true -> 0 end,
%% Bool flag pointers.
BoolFlagsPtrsPos = ValFlagsPtrsPos + byte_size(ValFlagsPtrs),
{NbBoolFlags, BoolFlagsPtrs} = load_quest_xnr_flag_ptrs(BoolFlagsList, NbTmpFlags + NbValFlags),
BoolFlagsPtrsPos2 = if NbBoolFlags =/= 0 -> BoolFlagsPtrsPos; true -> 0 end,
%% Main pointers.
MainPos = BoolFlagsPtrsPos + byte_size(BoolFlagsPtrs),
NbNPCs = 0, %% @todo
NPCsPos = MainPos + 260, %% @todo if NbNPCs =/= 0 -> todo; true -> MainPos + 260 end,
NPCsBin = << 0:64 >>,
%% Main options.
EnterWarp = load_quest_xnr_warp(QuestID, proplists:get_value(enter_warp, Settings)),
ExitWarp = load_quest_xnr_warp(QuestID, proplists:get_value(exit_warp, Settings)),
FailWarp = load_quest_xnr_warp(QuestID, proplists:get_value(fail_warp, Settings)),
MissionOpts = 0, %% @todo
NbCustomNPCs = 0, %% @todo
Icon = proplists:get_value(icon, Settings),
{PartySizeMin, PartySizeMax} = proplists:get_value(party_size, Settings),
{CursorX, CursorY} = proplists:get_value(cursor, Settings),
UnixTime = calendar:datetime_to_gregorian_seconds(calendar:now_to_universal_time(now()))
- calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}),
MainBin = << 16#16000600:32, UnixTime:32/little, QuestID:32/little, ZonesPos:32/little, NbZones:32/little,
WarpsPos:32/little, NbWarps:32/little, EnterWarp/binary, ExitWarp/binary, MissionOpts:8, 16#040200:24, 0:288,
Icon:16/little, 0:16, PartySizeMin:8, PartySizeMax:8, CursorX:16/little, CursorY:16/little, 0:16, ItemsPtrsPos:32/little,
FailWarp/binary, 0:160, TmpFlagsPtrsPos2:32/little, ValFlagsPtrsPos2:32/little, BoolFlagsPtrsPos2:32/little, NbTmpFlags:8,
NbValFlags:8, NbBoolFlags:8, NbNPCs:8, NPCsPos:32/little, 16#00000100:32, 16#00000100:32, NbCustomNPCs:32/little, 0:704 >>,
%% Wrapping it up.
Data = << MainPos:32/little, 0:32, TmpFlagsBin/binary, ValFlagsBin/binary, BoolFlagsBin/binary,
ItemsBin/binary, ItemsPtrs/binary, SetsBin/binary, ZonesBin/binary, WarpsBin/binary,
TmpFlagsPtrs/binary, ValFlagsPtrs/binary, BoolFlagsPtrs/binary, MainBin/binary, NPCsBin/binary >>,
Size = 8 + byte_size(Data),
Data2 = << $N, $X, $R, 0, Size:32/little, Data/binary >>,
%% Calculate the pointers and return.
L0 = [ItemsPtrsPos] ++ SetsPtrsList,
L1 = L0 ++ lists:seq(ZonesPos + 16, ZonesPos + 16 + 64 * (NbZones - 1), 64),
L2 = L1 ++ lists:seq(TmpFlagsPtrsPos, TmpFlagsPtrsPos + 4 * (NbTmpFlags + NbValFlags + NbBoolFlags - 1), 4),
L3 = L2 ++ [MainPos + 12, MainPos + 20, MainPos + 104],
L4 = if TmpFlagsPtrsPos2 =/= 0 -> L3 ++ [MainPos + 140]; true -> L3 end,
L5 = if ValFlagsPtrsPos2 =/= 0 -> L4 ++ [MainPos + 144]; true -> L4 end,
L6 = if BoolFlagsPtrsPos2 =/= 0 -> L5 ++ [MainPos + 148]; true -> L5 end,
{Data2, L6 ++ [MainPos + 156]}.
load_quest_xnr_flag_ptrs([], _N) ->
{0, << >>};
load_quest_xnr_flag_ptrs(FlagsList, N) ->
NbFlags = length(FlagsList),
L1 = lists:seq(16 + N * 16, 16 + N * 16 + (NbFlags - 1) * 16, 16),
L2 = [<< X:32/little >> || X <- L1],
{NbFlags, iolist_to_binary(L2)}.
load_quest_xnr_flags(FlagsList) ->
load_quest_xnr_flags(FlagsList, []).
load_quest_xnr_flags([], Acc) ->
iolist_to_binary(lists:reverse(Acc));
load_quest_xnr_flags([Flag|Tail], Acc) ->
L = length(Flag),
Padding = 8 * (16 - L),
FlagBin = list_to_binary(Flag),
Bin = << FlagBin/binary, 0:Padding >>,
load_quest_xnr_flags(Tail, [Bin|Acc]).
load_quest_xnr_items(ItemsList) ->
load_quest_xnr_items(ItemsList, []).
load_quest_xnr_items([], Acc) ->
iolist_to_binary(lists:reverse(Acc));
load_quest_xnr_items([Item|Tail], Acc) ->
Index = proplists:get_value(index, Item),
ItemID = proplists:get_value(itemid, Item),
NbItems = proplists:get_value(nb_items, Item),
Type = proplists:get_value(type, Item),
Money = proplists:get_value(money, Item),
Bin = << Index:8, ItemID:32, 0:16, NbItems:8, Type:32/little, Money:32/little >>,
load_quest_xnr_items(Tail, [Bin|Acc]).
load_quest_xnr_warp(QuestID, {WarpQuestID, WarpZoneID, WarpMapID, WarpEntryID}) ->
WarpQuestID2 = if WarpQuestID =:= QuestID -> 16#ffffffff; true -> WarpQuestID end,
<< WarpQuestID2:32/little, WarpZoneID:16/little, WarpMapID:16/little, WarpEntryID:16/little, 0:16 >>.
load_quest_xnr_warps(QuestID, WarpsList) ->
load_quest_xnr_warps(QuestID, WarpsList, []).
load_quest_xnr_warps(_QuestID, [], Acc) ->
iolist_to_binary(lists:reverse(Acc));
load_quest_xnr_warps(QuestID, [Warp|Tail], Acc) ->
{CurrentWarp, NextWarp} = Warp,
Bin1 = load_quest_xnr_warp(QuestID, CurrentWarp),
Bin2 = load_quest_xnr_warp(QuestID, NextWarp),
load_quest_xnr_warps(QuestID, Tail, [<< Bin1/binary, Bin2/binary >>|Acc]).
%% @todo Counter(16#7fffffff) has ffff before EnemyLevel, why?
%% @todo Spaceport(1104000) and counter(16#7fffffff) has 04010000 be 00010000, why?
load_quest_xnr_zones(ZonesList, BasePos) ->
load_quest_xnr_zones(ZonesList, BasePos, [], [], []).
load_quest_xnr_zones([], _BasePos, SetsAcc, SetsPtrsAcc, ZonesAcc) ->
SetsBin = iolist_to_binary(lists:reverse(SetsAcc)),
SetsPtrsList = lists:flatten(lists:reverse(SetsPtrsAcc)),
ZonesBin = iolist_to_binary(lists:reverse(ZonesAcc)),
{SetsBin, SetsPtrsList, ZonesBin};
load_quest_xnr_zones([{ZoneID, ZoneParams}|Tail], BasePos, SetsAcc, SetsPtrsAcc, ZonesAcc) ->
AreaID = proplists:get_value(areaid, ZoneParams),
EnemyLevel = proplists:get_value(enemy_level, ZoneParams),
SetList = proplists:get_value(sets, ZoneParams),
NbSets = length(SetList),
SetsBin = iolist_to_binary([<< Set:32/little >> || Set <- SetList]),
SetsBin2 = << SetsBin/binary, BasePos:32/little, NbSets:32/little >>,
SetPos = BasePos + NbSets * 4,
ZoneBin = << ZoneID:16/little, AreaID:16/little, AreaID:32/little, 0:16, EnemyLevel:8, 16#ff:8, 16#04010000:32, SetPos:32/little, 0:352 >>,
load_quest_xnr_zones(Tail, BasePos + byte_size(SetsBin2), [SetsBin2|SetsAcc], [SetPos|SetsPtrsAcc], [ZoneBin|ZonesAcc]).
%% @doc Load a script file and compile it into the bytecode used by the game.
load_script_bin(ScriptFilename) ->
{ok, Script} = file:read_file(ScriptFilename),
{ok, Tokens, _NbLines} = egs_script_lexer:string(binary_to_list(Script)),
{ok, ParseTree} = egs_script_parser:parse(Tokens),
egs_script_compiler:compile(ParseTree).
%% @doc Load a set configuration file and return a set_r*.rel binary.
load_set_rel(ConfFilename, AreaID, Maps, FilePos) ->
{ok, Settings} = file:consult(ConfFilename),
NbMaps = length(Maps),
AreaPtr = FilePos + 16,
MapsPtr = FilePos + 24,
{MapsBin, Ptrs} = load_set_rel_maps(Settings, Maps, MapsPtr, MapsPtr + NbMaps * 12),
Ptrs2 = [FilePos + 20|Ptrs],
Size = byte_size(MapsBin) + 24,
{<< $N, $X, $R, 0, Size:32/little, AreaPtr:32/little, 0:32,
AreaID:16/little, NbMaps:16/little, MapsPtr:32/little,
MapsBin/binary >>, Ptrs2}.
load_set_rel_maps(Settings, Maps, MapsPos, GroupsPos) ->
load_set_rel_maps(Settings, Maps, MapsPos, GroupsPos, [], [], []).
load_set_rel_maps(_Settings, [], _MapsPos, _GroupsPos, Ptrs, GroupsAcc, MapsAcc) ->
ObjectsBin = iolist_to_binary(lists:reverse(GroupsAcc)),
MapsBin = iolist_to_binary(lists:reverse(MapsAcc)),
{<< MapsBin/binary, ObjectsBin/binary >>, lists:sort(Ptrs)};
load_set_rel_maps(Settings, [MapID|Tail], MapsPos, GroupsPos, Ptrs, GroupsAcc, MapsAcc) ->
Map = proplists:get_value({map, MapID}, Settings),
NbGroups = length(Map),
{GroupsBin, Ptrs2} = load_set_rel_groups(Map, GroupsPos, GroupsPos + NbGroups * 40, Ptrs),
MapBin = << MapID:16/little, NbGroups:16/little, GroupsPos:32/little, 0:32 >>,
load_set_rel_maps(Settings, Tail, MapsPos + 12, GroupsPos + byte_size(GroupsBin), [MapsPos + 4|Ptrs2], [GroupsBin|GroupsAcc], [MapBin|MapsAcc]).
%% @todo 0:144, 16#ffff:16 0:32 can have some values.
load_set_rel_groups(Groups, GroupsPos, ObjectsPos, Ptrs) ->
load_set_rel_groups(Groups, GroupsPos, ObjectsPos, Ptrs, 0, [], []).
load_set_rel_groups([], _GroupsPos, _ObjectsPos, Ptrs, _N, ObjectsAcc, GroupsAcc) ->
ObjectsBin = iolist_to_binary(lists:reverse(ObjectsAcc)),
GroupsBin = iolist_to_binary(lists:reverse(GroupsAcc)),
{<< GroupsBin/binary, ObjectsBin/binary >>, Ptrs};
load_set_rel_groups([Group|Tail], GroupsPos, ObjectsPos, Ptrs, N, ObjectsAcc, GroupsAcc) ->
NbObjects = length(Group),
{ObjectsBin, Ptrs2} = load_set_rel_objects(Group, ObjectsPos, ObjectsPos + NbObjects * 52, Ptrs),
GroupBin = << 16#ffffffff:32, 0:144, 16#ffff:16, 0:32, N:32/little, 0:16, NbObjects:16/little, ObjectsPos:32/little >>,
load_set_rel_groups(Tail, GroupsPos + 40, ObjectsPos + byte_size(ObjectsBin), [GroupsPos + 36|Ptrs2], N + 1, [ObjectsBin|ObjectsAcc], [GroupBin|GroupsAcc]).
load_set_rel_objects(Objects, StdPos, ParamsPos, Ptrs) ->
load_set_rel_objects(Objects, StdPos, ParamsPos, Ptrs, [], []).
load_set_rel_objects([], _StdPos, _ParamsPos, Ptrs, StdAcc, ParamsAcc) ->
StdBin = iolist_to_binary(lists:reverse(StdAcc)),
ParamsBin = iolist_to_binary(lists:reverse(ParamsAcc)),
{<< StdBin/binary, ParamsBin/binary >>, Ptrs};
load_set_rel_objects([{ObjType, ObjPos, ObjRot, ObjParams}|Tail], StdPos, ParamsPos, Ptrs, StdAcc, ParamsAcc) ->
ParamsBin = load_set_rel_object_params(ObjType, ObjParams),
ParamsSize = byte_size(ParamsBin),
{ClassID, TypeID} = load_set_rel_object_id(ObjType),
{PosX, PosY, PosZ} = ObjPos,
{RotX, RotY, RotZ} = ObjRot,
StdBin = << 16#ffffffff:32, ClassID:32/little, 16#ffffffff:32, 16#ffff:16, TypeID:16/little, 0:32,
PosX:32/little-float, PosY:32/little-float, PosZ:32/little-float,
RotX:32/little-float, RotY:32/little-float, RotZ:32/little-float,
ParamsSize:32/little, ParamsPos:32/little >>,
load_set_rel_objects(Tail, StdPos + 52, ParamsPos + ParamsSize, [StdPos + 48|Ptrs], [StdBin|StdAcc], [ParamsBin|ParamsAcc]).
load_set_rel_object_id(static_model) -> {4, 4};
load_set_rel_object_id(sensor) -> {4, 9};
load_set_rel_object_id(invisible_block) -> {1, 10};
load_set_rel_object_id(npc) -> {2, 18};
load_set_rel_object_id(door) -> {5, 20};
load_set_rel_object_id(entrance) -> {2, 26};
load_set_rel_object_id(exit) -> {6, 27};
load_set_rel_object_id(type_counter) -> {1, 47};
load_set_rel_object_id(label) -> {2, 53};
load_set_rel_object_id(chair) -> {2, 56};
load_set_rel_object_id(uni_cube) -> {0, 60};
load_set_rel_object_id(pp_cube) -> {0, 62};
load_set_rel_object_id({raw, ClassID, TypeID}) -> {ClassID, TypeID}.
load_set_rel_object_params(static_model, Params) ->
Model = proplists:get_value(model, Params),
Size = proplists:get_value(size, Params, 1.0),
<< Model:32/little, Size:32/little-float, 16#0000ff00:32,
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32,
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>;
%% @todo If 00010000 has a 01 at the end, the sensor doesn't have an associated function in the script.
%% @todo Not sure about the box. It's probably wrong.
load_set_rel_object_params(sensor, Params) ->
ID = proplists:get_value(id, Params),
{Rad, X, Y, Z} = proplists:get_value(box, Params),
<< Rad:32/little-float, X:32/little-float, Y:32/little-float, Z:32/little-float, 0:64, ID:32/little, 16#00010000:32, 0:96,
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>;
load_set_rel_object_params(invisible_block, Params) ->
{Width, Height, Depth} = proplists:get_value(dimension, Params),
<< Width:32/little-float, Height:32/little-float, Depth:32/little-float, 16#ffff0000:32,
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, %% First ffffffff can be events required to enable (each being a 16-bit integer).
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>; %% First ffffffff can be events required to disable.
load_set_rel_object_params(npc, Params) ->
Model = proplists:get_value(model, Params),
ID = proplists:get_value(id, Params),
{CanTalk, TalkRadius} = case proplists:get_value(talk_radius, Params) of undefined -> {0, 0.0}; T -> {1, T} end,
{CanWalk, WalkRadius} = case proplists:get_value(walk_radius, Params) of undefined -> {0, 0.0}; W -> {1, W} end,
<< Model:16/little, ID:16/little, TalkRadius:32/little-float, WalkRadius:32/little-float,
CanWalk:8, 0:24, CanTalk:8, 0:24, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>;
%% @todo The byte right after the first ffff can take the value 1.
%% @todo The 2 bytes starting the many ffffffff at the end can take an unknown value.
load_set_rel_object_params(door, Params) ->
Model = proplists:get_value(model, Params),
Variant = proplists:get_value(variant, Params, 255),
IsClosed = case proplists:get_value(closed, Params, false) of true -> 1; false -> 0 end,
<< Model:32/little, IsClosed:8, 0:8, 16#ffff:16, 0:16, Variant:8, 0:8,
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>;
%% @todo 16#2041 is unknown (10.0 float). Other unknown values following can be non-0. Probably entrance and camera movements.
load_set_rel_object_params(entrance, Params) ->
EntryID = proplists:get_value(entryid, Params),
<< EntryID:16/little, 0:32, 16#2041:16, 0:1088 >>;
%% @todo The 2 bytes after CameraMovZ can have unknown values.
load_set_rel_object_params(exit, Params) ->
EntryID = proplists:get_value(entryid, Params),
Animation = proplists:get_value(animation, Params),
AnimationID = case Animation of stop -> 0; walk -> 1; run -> 2 end,
{ExitBoxRad, ExitBoxW, ExitBoxH, ExitBoxD} = proplists:get_value(exit_box, Params),
{ExitMovX, ExitMovY, ExitMovZ} = proplists:get_value(exit_movement, Params),
{CameraBoxRad, CameraBoxW, CameraBoxH, CameraBoxD} = proplists:get_value(camera_box, Params),
{CameraMovX, CameraMovY, CameraMovZ} = proplists:get_value(camera_movement, Params),
CommonBin = << ExitBoxRad:32/little-float, ExitBoxW:32/little-float, ExitBoxH:32/little-float, ExitBoxD:32/little-float,
ExitMovX:32/little-float, ExitMovY:32/little-float, ExitMovZ:32/little-float, 0:192,
CameraBoxRad:32/little-float, CameraBoxW:32/little-float, CameraBoxH:32/little-float, CameraBoxD:32/little-float,
CameraMovX:32/little-float, CameraMovY:32/little-float, CameraMovZ:32/little-float,
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>,
case proplists:get_value(type, Params) of
counter ->
CounterID = proplists:get_value(counterid, Params),
<< CounterID:16/little, 1:16/little, 0:16, AnimationID:8, 1:8, EntryID:8, 0:16, 1:8, CommonBin/binary >>;
map ->
<< EntryID:16/little, 0:32, AnimationID:8, 1:8, 255:8, 0:16, 1:8, CommonBin/binary >>
end;
%% @todo Mostly unknown values!
load_set_rel_object_params(type_counter, Params) ->
TalkRadius = proplists:get_value(talk_radius, Params),
<< TalkRadius:32/little-float, 0:384, 16#ffffffff:32 >>;
%% @todo Not sure about the box. It's probably wrong.
%% @todo Can only have up to 10 different label ids.
load_set_rel_object_params(label, Params) ->
ID = proplists:get_value(id, Params),
{Rad, X, Y, Z} = proplists:get_value(box, Params),
<< Rad:32/little-float, X:32/little-float, Y:32/little-float, Z:32/little-float, ID:8, 1:8, 0:16,
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>;
%% @todo Many unknown values...
load_set_rel_object_params(chair, Params) ->
ID = proplists:get_value(id, Params),
ActionRadius = 1.0,
UnknownA = -20.0,
UnknownB = 30.0,
UnknownC = 110.0,
UnknownD = -6.0,
%% If it looks like the player is going to get stuck in an object, the game won't allow the player to leave its chair.
StandUpMoveDistance = proplists:get_value(stand_dist, Params, 13.0),
UnknownE = 94.0,
<< ID:32/little, ActionRadius:32/little-float, 0, 1, 1, 0, UnknownA:32/little-float, UnknownB:32/little-float,
UnknownC:32/little-float, 0:32, UnknownD:32/little-float, StandUpMoveDistance:32/little-float, UnknownE:32/little-float >>;
%% @todo First ffff sometimes is 0, find out why.
load_set_rel_object_params(uni_cube, Params) ->
I = proplists:get_value(i, Params),
EntryID = proplists:get_value(entryid, Params),
<< I:8, EntryID:16/little, 0:8,
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32,
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>;
%% @todo First ffff can be that same old unknown type of value.
%% @todo First byte set to 1 = hidden?
load_set_rel_object_params(pp_cube, _Params) ->
<< 0:32, 16#ffffffff:32, 16#ffffffff:32 >>;
%% @doc Raw binary, used for testing.
load_set_rel_object_params({raw, _ClassID, _TypeID}, ParamsBin) ->
ParamsBin.
%% @todo ghosts: NbGhosts:32, Width:32/float, Unknown:32/float
%% @doc Load a counter configuration file and return a table.rel binary along with its pointers array.
load_table_rel(ConfFilename) ->
{ok, Settings} = file:consult(ConfFilename),
TName = proplists:get_value(t_name, Settings),
TDesc = proplists:get_value(t_desc, Settings),
{CursorX, CursorY} = proplists:get_value(cursor, Settings),
{NbQuests, QuestsBin, NbGroups, GroupsBin} = load_table_rel_groups_to_bin(proplists:get_value(groups, Settings)),
QuestsPos = 16,
GroupsPos = 16 + byte_size(QuestsBin),
UnixTime = calendar:datetime_to_gregorian_seconds(calendar:now_to_universal_time(now()))
- calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}),
MainBin = << 16#00000100:32, UnixTime:32/little, GroupsPos:32/little, QuestsPos:32/little,
NbGroups:16/little, NbQuests:16/little, TName:16/little, TDesc:16/little, CursorX:16/little, CursorY:16/little,
0:32, 0:16, 16#ffff:16 >>,
MainPos = GroupsPos + byte_size(GroupsBin),
{CityPos, CityBin} = case proplists:get_value(city, Settings) of
undefined -> {0, << >>};
{QuestID, ZoneID, MapID, EntryID} ->
{MainPos + byte_size(MainBin) + 4, << QuestID:32/little, ZoneID:16/little, MapID:16/little, EntryID:16/little >>}
end,
Data = << MainPos:32/little, 0:32, QuestsBin/binary, GroupsBin/binary, MainBin/binary, CityPos:32/little, CityBin/binary >>,
Size = byte_size(Data),
Data2 = << $N, $X, $R, 0, Size:32/little, Data/binary >>,
case CityPos of
0 -> {Data2, [MainPos + 8, MainPos + 12]};
_ -> {Data2, [MainPos + 8, MainPos + 12, MainPos + 36]}
end.
%% @doc Convert groups of quests to their binary equivalent for load_table_rel.
load_table_rel_groups_to_bin(Groups) ->
load_table_rel_groups_to_bin(Groups, 0, [], []).
load_table_rel_groups_to_bin([], N, QAcc, GAcc) ->
NbGroups = length(GAcc),
Quests = iolist_to_binary(lists:reverse(QAcc)),
Groups = iolist_to_binary(lists:reverse(GAcc)),
{N, Quests, NbGroups, Groups};
load_table_rel_groups_to_bin([Settings|Tail], N, QAcc, GAcc) ->
TName = proplists:get_value(t_name, Settings),
TDesc = proplists:get_value(t_desc, Settings),
Quests = proplists:get_value(quests, Settings),
QuestsBin = [<< Q:32/little >> || {Q, _Filename} <- Quests],
L = length(Quests),
GroupBin = << N:16/little, L:16/little, TName:16/little, TDesc:16/little, 0:16, 16#ff03:16, 0:160 >>,
load_table_rel_groups_to_bin(Tail, N + L, [QuestsBin|QAcc], [GroupBin|GAcc]).
%% @doc Load a text.bin file from its UCS-2 txt file equivalent.
load_text_bin(TextFilename) ->
{ok, << 16#fffe:16, File/binary >>} = file:read_file(TextFilename),
Strings = re:split(File, "\n."),
TextBin = load_text_bin_strings(Strings),
Size = 12 + byte_size(TextBin),
<< Size:32/little, 8:32/little, 12:32/little, TextBin/binary >>.
load_text_bin_strings(Strings) ->
load_text_bin_strings(Strings, 0, [], []).
load_text_bin_strings([], _Pos, PosList, Acc) ->
L = length(PosList) * 4,
PosList2 = [P + L + 12 || P <- lists:reverse(PosList)],
PosBin = iolist_to_binary([<< P:32/little >> || P <- PosList2]),
StringsBin = iolist_to_binary(lists:reverse(Acc)),
<< PosBin/binary, StringsBin/binary >>;
%% empty line at the end of a text.bin.txt file.
load_text_bin_strings([<< >>], Pos, PosList, Acc) ->
load_text_bin_strings([], Pos, PosList, Acc);
load_text_bin_strings([String|Tail], Pos, PosList, Acc) ->
String2 = re:replace(String, "~.n.", "\n\0", [global, {return, binary}]),
String3 = << String2/binary, 0, 0 >>,
load_text_bin_strings(Tail, Pos + byte_size(String3), [Pos|PosList], [String3|Acc]).
%% @doc Create the unit_title_table.rel file based on a quest configuration file and a starting file position.
%% The file position depends on the previously built quest.xnr.
load_unit_title_table_rel(ConfFilename, FilePos) ->
{ok, Settings} = file:consult(ConfFilename),
{Titles, NbTitles} = load_unit_title_table_rel_zones(proplists:get_value(zones, Settings)),
MainPos = 16 + byte_size(Titles),
Size = 8 + MainPos,
TitlesPtr = FilePos + 16,
Data = << $N, $X, $R, 0, Size:32/little, MainPos:32/little, 0:32, Titles/binary, TitlesPtr:32/little, NbTitles:32/little >>,
{Data, [FilePos + 16 + byte_size(Titles)]}.
load_unit_title_table_rel_maps(ZoneID, Maps) ->
load_unit_title_table_rel_maps(ZoneID, Maps, 0, []).
load_unit_title_table_rel_maps(_ZoneID, [], N, Acc) ->
{iolist_to_binary(lists:reverse(Acc)), N};
load_unit_title_table_rel_maps(ZoneID, [MapID|Tail], N, Acc) ->
Bin = << ZoneID:16/little, MapID:16/little, N:32/little >>,
load_unit_title_table_rel_maps(ZoneID, Tail, N + 1, [Bin|Acc]).
load_unit_title_table_rel_zones(Zones) ->
load_unit_title_table_rel_zones(Zones, 0, []).
load_unit_title_table_rel_zones([], N, Acc) ->
{iolist_to_binary(lists:reverse(Acc)), N};
load_unit_title_table_rel_zones([{ZoneID, ZoneParams}|Tail], N, Acc) ->
Maps = proplists:get_value(maps, ZoneParams),
{Bin, N2} = load_unit_title_table_rel_maps(ZoneID, Maps),
load_unit_title_table_rel_zones(Tail, N + N2, [Bin|Acc]).
%% @doc Pack an nbl file according to the given Options.
%% Example usage: nbl:pack([{files, [{file, "table.rel", [16#184, 16#188, 16#1a0]}, {file, "text.bin", []}]}]).
%% The data will be automatically compressed if the file is big enough to make it worth it.
%% @todo The 0010 value is unknown. If it was too low it would crash the client when it cleans up the nbl.
nbl_pack(Options) ->
Files = proplists:get_value(files, Options),
{Header, Data, DataSize, CompressedDataSize, PtrArray, PtrArraySize} = nbl_pack_files(Files),
NbFiles = length(Files),
HeaderSize = 16#30 + 16#60 * NbFiles,
EncryptSeed = 0,
<< $N, $M, $L, $L, 2:16/little, 16#0010:16, HeaderSize:32/little, NbFiles:32/little,
DataSize:32/little, CompressedDataSize:32/little, PtrArraySize:32/little, EncryptSeed:32/little,
0:128, Header/binary, Data/binary, PtrArray/binary >>.
%% @doc Pack a list of files and return the header, data and pointer array parts.
nbl_pack_files(Files) ->
nbl_pack_files(Files, {[], [], [], 0, 0}).
nbl_pack_files([], {AccH, AccD, AccP, _FilePos, _PtrIndex}) ->
BinH = iolist_to_binary(lists:reverse(AccH)),
PaddingH = 8 * (16#7d0 - (byte_size(BinH) rem 16#800)),
PaddingH2 = if PaddingH =< 0 -> 16#800 + PaddingH; true -> PaddingH end,
BinD = iolist_to_binary(lists:reverse(AccD)),
BinDSize = byte_size(BinD),
{BinD3, CompressedDataSize} = if BinDSize < 16#800 ->
{BinD, 0};
true ->
BinD2 = prs:compress(BinD),
BinD2Size = byte_size(BinD2),
{BinD2, BinD2Size}
end,
PaddingD = 8 * (16#800 - (byte_size(BinD3) rem 16#800)),
PaddingD2 = if PaddingD =:= 8 * 16#800 -> 0; true -> PaddingD end,
BinP = iolist_to_binary(lists:reverse(AccP)),
PtrSize = byte_size(BinP),
PtrArray = case PtrSize of
0 -> << >>;
_ ->
PaddingP = 8 * (16#800 - (byte_size(BinP) rem 16#800)),
<< BinP/binary, 0:PaddingP >>
end,
{<< BinH/binary, 0:PaddingH2 >>, << BinD3/binary, 0:PaddingD2 >>, BinDSize, CompressedDataSize, PtrArray, PtrSize};
nbl_pack_files([{data, Filename, Data, PtrList}|Tail], {AccH, AccD, AccP, FilePos, PtrIndex}) ->
ID = case filename:extension(Filename) of
".bin" -> << $S, $T, $D, 0 >>;
[$.|String] -> list_to_binary(string:to_upper(String ++ [0]))
end,
FilenameBin = iolist_to_binary(Filename),
FilenamePaddingBits = 8 * (32 - byte_size(FilenameBin)),
DataSize = byte_size(Data),
PaddedSize = nbl_padded_size(DataSize),
DataPaddingBits = 8 * (PaddedSize - DataSize),
PtrSize = 4 * length(PtrList),
BinD = << Data/binary, 0:DataPaddingBits >>,
BinH = << ID/binary, 16#60000000:32, 0:64, FilenameBin/binary, 0:FilenamePaddingBits,
FilePos:32/little, DataSize:32/little, PtrIndex:32/little, PtrSize:32/little >>,
NXIF = case filename:extension(Filename) of
".bin" -> << 0:256 >>;
_ -> nbl_pack_nxif(DataSize, PtrSize)
end,
BinH2 = << BinH/binary, NXIF/binary >>,
BinP = iolist_to_binary([ << Ptr:32/little >> || Ptr <- PtrList]),
nbl_pack_files(Tail, {[BinH2|AccH], [BinD|AccD], [BinP|AccP], FilePos + PaddedSize, PtrIndex + PtrSize});
nbl_pack_files([{file, Filename, PtrList}|Tail], Acc) ->
io:format("~p~n", [Filename]),
{ok, Data} = file:read_file(Filename),
nbl_pack_files([{data, Filename, Data, PtrList}|Tail], Acc).
%% @doc Return an NXIF chunk data for a specific data and pointer array size.
nbl_pack_nxif(DataSize, PtrSize) ->
DataSize2 = DataSize + 16#20,
PtrSize2 = PtrSize + 16#20 - (PtrSize rem 16#20),
<< $N, $X, $I, $F, 16#18000000:32, 16#01000000:32, 16#20000000:32,
DataSize:32/little, DataSize2:32/little, PtrSize2:32/little, 16#01000000:32 >>.
%% @doc Return the padded size of a file to be packed in an nbl archive.
nbl_padded_size(Size) ->
Size + 16#20 - (Size rem 16#20).

857
apps/egs/src/egs_game.erl Normal file
View File

@ -0,0 +1,857 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc Game callback module.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_game).
-export([info/2, cast/2, raw/3, event/2]).
-export([char_load/2]). %% Hopefully temporary export.
-include("include/records.hrl").
%% @doc Forward the broadcasted command to the client.
info({egs, cast, Command}, Client=#egs_net{gid=GID}) ->
<< A:64/bits, _:32, B:96/bits, _:64, C/bits >> = Command,
egs_proto:packet_send(Client, << A/binary, 16#00011300:32, B/binary, 16#00011300:32, GID:32/little, C/binary >>);
%% @doc Forward the chat message to the client.
info({egs, chat, FromGID, ChatTypeID, ChatGID, ChatName, ChatModifiers, ChatMessage}, Client) ->
egs_proto:send_0304(FromGID, ChatTypeID, ChatGID, ChatName, ChatModifiers, ChatMessage, Client);
info({egs, notice, Type, Message}, Client) ->
egs_proto:send_0228(Type, 2, Message, Client);
%% @doc Inform the client that a player has spawn.
%% @todo Not sure what IsSeasonal or the AreaNb in 0205 should be for other spawns.
info({egs, player_spawn, Player}, Client) ->
egs_proto:send_0111(Player, 6, Client),
egs_proto:send_010d(Player, Client),
egs_proto:send_0205(Player, 0, Client),
egs_proto:send_0203(Player, Client),
egs_proto:send_0201(Player, Client);
%% @doc Inform the client that a player has unspawn.
info({egs, player_unspawn, Player}, Client) ->
egs_proto:send_0204(Player, Client);
%% @doc Warp the player to the given location.
info({egs, warp, QuestID, ZoneID, MapID, EntryID}, Client) ->
event({area_change, QuestID, ZoneID, MapID, EntryID}, Client).
%% Broadcasts.
%% @todo Handle broadcasting better than that. Review the commands at the same time.
%% @doc Position change. Save the position and then dispatch it.
cast({16#0503, Data}, Client=#egs_net{gid=GID}) ->
<< _:424, Dir:24/little, _PrevCoords:96, X:32/little-float, Y:32/little-float, Z:32/little-float,
QuestID:32/little, ZoneID:32/little, MapID:32/little, EntryID:32/little, _:32 >> = Data,
FloatDir = Dir / 46603.375,
{ok, User} = egs_users:read(GID),
NewUser = User#users{pos={X, Y, Z, FloatDir}, area={QuestID, ZoneID, MapID}, entryid=EntryID},
egs_users:write(NewUser),
cast({valid, Data}, Client);
%% @doc Stand still. Save the position and then dispatch it.
cast({16#0514, Data}, Client=#egs_net{gid=GID}) ->
<< _:424, Dir:24/little, X:32/little-float, Y:32/little-float, Z:32/little-float,
QuestID:32/little, ZoneID:32/little, MapID:32/little, EntryID:32/little, _/bits >> = Data,
FloatDir = Dir / 46603.375,
{ok, User} = egs_users:read(GID),
NewUser = User#users{pos={X, Y, Z, FloatDir}, area={QuestID, ZoneID, MapID}, entryid=EntryID},
egs_users:write(NewUser),
cast({valid, Data}, Client);
%% @doc Default broadcast handler. Dispatch the command to everyone.
%% We clean up the command and use the real GID and LID of the user, disregarding what was sent and possibly tampered with.
%% Only a handful of commands are allowed to broadcast. An user tampering with it would get disconnected instantly.
%% @todo Don't query the user data everytime! Keep the needed information in the Client.
cast({Command, Data}, #egs_net{gid=GID, lid=LID})
when Command =:= 16#0101;
Command =:= 16#0102;
Command =:= 16#0104;
Command =:= 16#0107;
Command =:= 16#010f;
Command =:= 16#050f;
Command =:= valid ->
<< _:32, A:64/bits, _:64, B:192/bits, _:64, C/bits >> = Data,
{ok, User} = egs_users:read(GID),
Packet = << A/binary, 16#00011300:32, GID:32/little, B/binary, GID:32/little, LID:32/little, C/binary >>,
egs_zones:broadcast(User#users.zonepid, GID, Packet).
%% Raw commands.
%% @todo Handle this packet properly.
%% @todo Spawn cleared response event shouldn't be handled following this packet but when we see the spawn actually dead HP-wise.
%% @todo Type shouldn't be :32 but it seems when the later 16 have something it's not a spawn event.
raw(16#0402, << _:352, Data/bits >>, Client=#egs_net{gid=GID}) ->
<< SpawnID:32/little, _:64, Type:32/little, _:64 >> = Data,
case Type of
7 -> % spawn cleared @todo 1201 sent back with same values apparently, but not always
io:format("~p: cleared spawn ~b~n", [GID, SpawnID]),
{ok, User} = egs_users:read(GID),
{BlockID, EventID} = psu_instance:spawn_cleared_event(User#users.instancepid, element(2, User#users.area), SpawnID),
if EventID =:= false -> ignore;
true -> egs_proto:send_1205(EventID, BlockID, 0, Client)
end;
_ ->
ignore
end;
%% @todo Handle this packet.
%% @todo 3rd Unsafe Passage C, EventID 10 BlockID 2 = mission cleared?
raw(16#0404, << _:352, Data/bits >>, Client) ->
<< EventID:8, BlockID:8, _:16, Value:8, _/bits >> = Data,
io:format("~p: unknown command 0404: eventid ~b blockid ~b value ~b~n", [Client#egs_net.gid, EventID, BlockID, Value]),
egs_proto:send_1205(EventID, BlockID, Value, Client);
%% @todo Used in the tutorial. Not sure what it does. Give an item (the PA) maybe?
%% @todo Probably should ignore that until more is known.
raw(16#0a09, _Data, Client=#egs_net{gid=GID}) ->
egs_proto:packet_send(Client, << 16#0a090300:32, 0:32, 16#00011300:32, GID:32/little, 0:64, 16#00011300:32, GID:32/little, 0:64, 16#00003300:32, 0:32 >>);
%% @todo Figure out this command.
raw(16#0c11, << _:352, A:32/little, B:32/little >>, Client=#egs_net{gid=GID}) ->
io:format("~p: 0c11 ~p ~p~n", [GID, A, B]),
egs_proto:packet_send(Client, << 16#0c120300:32, 0:160, 16#00011300:32, GID:32/little, 0:64, A:32/little, 1:32/little >>);
%% @doc Set flag handler. Associate a new flag with the character.
%% Just reply with a success value for now.
%% @todo God save the flags.
raw(16#0d04, << _:352, Data/bits >>, Client=#egs_net{gid=GID}) ->
<< Flag:128/bits, A:16/bits, _:8, B/bits >> = Data,
io:format("~p: flag handler for ~s~n", [GID, re:replace(Flag, "\\0+", "", [global, {return, binary}])]),
egs_proto:packet_send(Client, << 16#0d040300:32, 0:160, 16#00011300:32, GID:32/little, 0:64, Flag/binary, A/binary, 1, B/binary >>);
%% @doc Initialize a vehicle object.
%% @todo Find what are the many values, including the odd Whut value (and whether it's used in the reply).
%% @todo Separate the reply.
raw(16#0f00, << _:352, Data/bits >>, Client=#egs_net{gid=GID}) ->
<< A:32/little, 0:16, B:16/little, 0:16, C:16/little, 0, Whut:8, D:16/little, 0:16,
E:16/little, 0:16, F:16/little, G:16/little, H:16/little, I:32/little >> = Data,
io:format("~p: init vehicle: ~b ~b ~b ~b ~b ~b ~b ~b ~b ~b~n", [GID, A, B, C, Whut, D, E, F, G, H, I]),
egs_proto:packet_send(Client, << 16#12080300:32, 0:160, 16#00011300:32, GID:32/little, 0:64,
A:32/little, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32,
0:16, B:16/little, 0:16, C:16/little, 0:16, D:16/little, 0:112,
E:16/little, 0:16, F:16/little, H:16/little, 1, 0, 100, 0, 10, 0, G:16/little, 0:16 >>);
%% @doc Enter vehicle.
%% @todo Separate the reply.
raw(16#0f02, << _:352, Data/bits >>, Client=#egs_net{gid=GID}) ->
<< A:32/little, B:32/little, C:32/little >> = Data,
io:format("~p: enter vehicle: ~b ~b ~b~n", [GID, A, B, C]),
HP = 100,
egs_proto:packet_send(Client, << 16#120a0300:32, 0:160, 16#00011300:32, GID:32/little, 0:64, A:32/little, B:32/little, C:32/little, HP:32/little >>);
%% @doc Sent right after entering the vehicle. Can't move without it.
%% @todo Separate the reply.
raw(16#0f07, << _:352, Data/bits >>, Client=#egs_net{gid=GID}) ->
<< A:32/little, B:32/little >> = Data,
io:format("~p: after enter vehicle: ~b ~b~n", [GID, A, B]),
egs_proto:packet_send(Client, << 16#120f0300:32, 0:160, 16#00011300:32, GID:32/little, 0:64, A:32/little, B:32/little >>);
%% @todo Not sure yet.
raw(16#1019, _Data, _Client) ->
ignore;
%~ egs_proto:packet_send(Client, << 16#10190300:32, 0:160, 16#00011300:32, GID:32/little, 0:64, 0:192, 16#00200000:32, 0:32 >>);
%% @todo Not sure about that one though. Probably related to 1112 still.
raw(16#1106, << _:352, Data/bits >>, Client) ->
egs_proto:send_110e(Data, Client);
%% @doc Probably asking permission to start the video (used for syncing?).
raw(16#1112, << _:352, Data/bits >>, Client) ->
egs_proto:send_1113(Data, Client);
%% @todo Not sure yet. Value is probably a TargetID. Used in Airboard Rally. Replying with the same value starts the race.
raw(16#1216, << _:352, Data/bits >>, Client) ->
<< Value:32/little >> = Data,
io:format("~p: command 1216 with value ~b~n", [Client#egs_net.gid, Value]),
egs_proto:send_1216(Value, Client);
%% @doc Dismiss all unknown raw commands with a log notice.
%% @todo Have a log event handler instead.
raw(Command, _Data, Client) ->
io:format("~p (~p): dismissed command ~4.16.0b~n", [?MODULE, Client#egs_net.gid, Command]).
%% Events.
%% @doc Load the given map as a standard lobby.
%% @todo When changing lobby to the room, or room to lobby, we must perform an universe change.
%% @todo Handle area_change event for APCs in story missions (characters with PartyPos defined).
event({area_change, QuestID, ZoneID, MapID, EntryID}, Client) ->
event({area_change, QuestID, ZoneID, MapID, EntryID, 16#ffffffff}, Client);
event({area_change, QuestID, ZoneID, MapID, EntryID, PartyPos=16#ffffffff}, Client) ->
io:format("~p: area change (~b,~b,~b,~b,~b)~n", [Client#egs_net.gid, QuestID, ZoneID, MapID, EntryID, PartyPos]),
{ok, OldUser} = egs_users:read(Client#egs_net.gid),
{OldQuestID, OldZoneID, _OldMapID} = OldUser#users.area,
QuestChange = OldQuestID /= QuestID,
ZoneChange = if OldQuestID =:= QuestID, OldZoneID =:= ZoneID -> false; true -> true end,
AreaType = egs_quests_db:area_type(QuestID, ZoneID),
AreaShortName = "dammy", %% @todo Load the short name from egs_quests_db.
{IsSeasonal, SeasonID} = egs_seasons:read(QuestID),
User = OldUser#users{areatype=AreaType, area={QuestID, ZoneID, MapID}, entryid=EntryID},
egs_users:write(User), %% @todo Booh ugly! But temporary.
%% Load the quest.
User2 = if QuestChange ->
egs_proto:send_0c00(User, Client),
egs_proto:send_020e(egs_quests_db:quest_nbl(QuestID), Client),
User#users{questpid=egs_universes:lobby_pid(User#users.uni, QuestID)};
true -> User
end,
%% Load the zone.
Client1 = if ZoneChange ->
ZonePid = egs_quests:zone_pid(User2#users.questpid, ZoneID),
egs_zones:leave(User2#users.zonepid, User2#users.gid),
NewLID = egs_zones:enter(ZonePid, User2#users.gid),
NewClient = Client#egs_net{lid=NewLID},
{ok, User3} = egs_users:read(User2#users.gid),
egs_proto:send_0a05(NewClient),
egs_proto:send_0111(User3, 6, NewClient),
egs_proto:send_010d(User3, NewClient),
egs_proto:send_0200(ZoneID, AreaType, NewClient),
egs_proto:send_020f(egs_quests_db:zone_nbl(QuestID, ZoneID), egs_zones:setid(ZonePid), SeasonID, NewClient),
NewClient;
true ->
User3 = User2,
Client
end,
%% Save the user.
egs_users:write(User3),
%% Load the player location.
Client2 = Client1#egs_net{areanb=Client#egs_net.areanb + 1},
egs_proto:send_0205(User3, IsSeasonal, Client2),
egs_proto:send_100e(User3#users.area, User3#users.entryid, AreaShortName, Client2),
%% Load the zone objects.
if ZoneChange ->
egs_proto:send_1212(Client2); %% @todo Only sent if there is a set file.
true -> ignore
end,
%% Load the player.
egs_proto:send_0201(User3, Client2),
if ZoneChange ->
egs_proto:send_0a06(User3, Client2),
%% Load the other players in the zone.
OtherPlayersGID = egs_zones:get_all_players(User3#users.zonepid, User3#users.gid),
if OtherPlayersGID =:= [] -> ignore;
true ->
OtherPlayers = egs_users:select(OtherPlayersGID),
egs_proto:send_0233(OtherPlayers, Client)
end;
true -> ignore
end,
%% End of loading.
Client3 = Client2#egs_net{areanb=Client2#egs_net.areanb + 1},
egs_proto:send_0208(Client3),
egs_proto:send_0236(Client3),
%% @todo Load APC characters.
{ok, Client3};
event({area_change, QuestID, ZoneID, MapID, EntryID, PartyPos}, Client) ->
io:format("~p: area change (~b,~b,~b,~b,~b)~n", [Client#egs_net.gid, QuestID, ZoneID, MapID, EntryID, PartyPos]),
ignore;
%% @doc After the character has been (re)loaded, change the area he's in.
%% @todo The area_change event should probably not change the user's values.
%% @todo Remove that ugly code when the above is done.
event(system_character_load_complete, Client=#egs_net{gid=GID}) ->
{ok, User=#users{area={QuestID, ZoneID, MapID}, entryid=EntryID}} = egs_users:read(GID),
egs_users:write(User#users{area={0, 0, 0}, entryid=0}),
event({area_change, QuestID, ZoneID, MapID, EntryID}, Client);
%% @doc Chat broadcast handler. Dispatch the message to everyone (for now).
%% Disregard the name sent by the server. Use the name saved in memory instead, to prevent client-side editing.
%% @todo Only broadcast to people in the same map.
%% @todo In the case of NPC characters, when FromTypeID is 00001d00, check that the NPC is in the party and broadcast only to the party (probably).
%% @todo When the game doesn't find an NPC (probably) and forces it to talk like in the tutorial mission it seems FromTypeID, FromGID and Name are all 0.
%% @todo Make sure modifiers have correct values.
event({chat, _, FromGID, _, Modifiers, _, ChatMsg}, #egs_net{gid=UserGID}) ->
[BcastTypeID, BcastGID, BcastName] = case FromGID of
0 -> %% This probably shouldn't happen. Just make it crash on purpose.
io:format("~p: chat FromGID=0~n", [UserGID]),
ignore;
UserGID -> %% player chat: disregard whatever was sent except modifiers and message.
{ok, User} = egs_users:read(UserGID),
[16#00001200, User#users.gid, User#users.name];
NPCGID -> %% npc chat: @todo Check that the player is the party leader and this npc is in his party.
{ok, User} = egs_users:read(NPCGID),
[16#00001d00, FromGID, User#users.name]
end,
%% log the message as ascii to the console
[LogName|_] = re:split(BcastName, "\\0\\0", [{return, binary}]),
[TmpMessage|_] = re:split(ChatMsg, "\\0\\0", [{return, binary}]),
LogMessage = re:replace(TmpMessage, "\\n", " ", [global, {return, binary}]),
io:format("~p: chat from ~s: ~s~n", [UserGID, [re:replace(LogName, "\\0", "", [global, {return, binary}])], [re:replace(LogMessage, "\\0", "", [global, {return, binary}])]]),
egs_users:broadcast_all({egs, chat, UserGID, BcastTypeID, BcastGID, BcastName, Modifiers, ChatMsg});
%% @todo There's at least 9 different sets of locations. Handle all of them correctly.
event(counter_background_locations_request, Client) ->
egs_proto:send_170c(Client);
%% @todo Make sure non-mission counters follow the same loading process.
%% @todo Probably validate the From* values, to not send the player back inside a mission.
%% @todo Handle the LID change when entering counters.
event({counter_enter, CounterID, FromZoneID, FromMapID, FromEntryID}, Client=#egs_net{gid=GID}) ->
io:format("~p: counter load ~b~n", [GID, CounterID]),
{ok, OldUser} = egs_users:read(GID),
FromArea = {element(1, OldUser#users.area), FromZoneID, FromMapID},
egs_zones:leave(OldUser#users.zonepid, OldUser#users.gid),
User = OldUser#users{questpid=undefined, zonepid=undefined, areatype=counter,
area={16#7fffffff, 0, 0}, entryid=0, prev_area=FromArea, prev_entryid=FromEntryID},
egs_users:write(User),
QuestData = egs_quests_db:quest_nbl(0),
ZoneData = << 0:16000 >>, %% Doing like official just in case.
%% load counter
egs_proto:send_0c00(User, Client),
egs_proto:send_020e(QuestData, Client),
egs_proto:send_0a05(Client),
egs_proto:send_010d(User, Client),
egs_proto:send_0200(0, mission, Client),
egs_proto:send_020f(ZoneData, 0, 255, Client),
Client2 = Client#egs_net{areanb=Client#egs_net.areanb + 1},
egs_proto:send_0205(User, 0, Client2),
egs_proto:send_100e(CounterID, "Counter", Client2),
egs_proto:send_0215(0, Client2),
egs_proto:send_0215(0, Client2),
egs_proto:send_020c(Client2),
egs_proto:send_1202(Client2),
egs_proto:send_1204(Client2),
egs_proto:send_1206(Client2),
egs_proto:send_1207(Client2),
egs_proto:send_1212(Client2),
egs_proto:send_0201(User, Client2),
egs_proto:send_0a06(User, Client2),
case User#users.partypid of
undefined -> ignore;
_ -> egs_proto:send_022c(0, 16#12, Client)
end,
Client3 = Client2#egs_net{areanb=Client2#egs_net.areanb + 1},
egs_proto:send_0208(Client3),
egs_proto:send_0236(Client3),
{ok, Client3};
%% @todo Handle parties to join.
event(counter_join_party_request, Client) ->
egs_proto:send_1701(Client);
%% @doc Leave mission counter handler.
event(counter_leave, Client=#egs_net{gid=GID}) ->
{ok, User} = egs_users:read(GID),
PrevArea = User#users.prev_area,
event({area_change, element(1, PrevArea), element(2, PrevArea), element(3, PrevArea), User#users.prev_entryid}, Client);
%% @doc Send the code for the background image to use. But there's more that should be sent though.
%% @todo Apparently background values 1 2 3 are never used on official servers. Find out why.
%% @todo Rename to counter_bg_request.
event({counter_options_request, CounterID}, Client) ->
io:format("~p: counter options request ~p~n", [Client#egs_net.gid, CounterID]),
egs_proto:send_1711(egs_counters_db:bg(CounterID), Client);
%% @todo Handle when the party already exists! And stop doing it wrong.
event(counter_party_info_request, Client=#egs_net{gid=GID}) ->
{ok, User} = egs_users:read(GID),
egs_proto:send_1706(User#users.name, Client);
%% @todo Item distribution is always set to random for now.
event(counter_party_options_request, Client) ->
egs_proto:send_170a(Client);
%% @doc Request the counter's quest files.
event({counter_quest_files_request, CounterID}, Client) ->
io:format("~p: counter quest files request ~p~n", [Client#egs_net.gid, CounterID]),
egs_proto:send_0c06(egs_counters_db:pack(CounterID), Client);
%% @doc Counter available mission list request handler.
event({counter_quest_options_request, CounterID}, Client) ->
io:format("~p: counter quest options request ~p~n", [Client#egs_net.gid, CounterID]),
egs_proto:send_0c10(egs_counters_db:opts(CounterID), Client);
%% @todo A and B are mostly unknown. Like most of everything else from the command 0e00...
event({hit, FromTargetID, ToTargetID, A, B}, Client=#egs_net{gid=GID}) ->
{ok, User} = egs_users:read(GID),
%% hit!
#hit_response{type=Type, user=NewUser, exp=HasEXP, damage=Damage, targethp=TargetHP, targetse=TargetSE, events=Events} = psu_instance:hit(User, FromTargetID, ToTargetID),
case Type of
box ->
%% @todo also has a hit sent, we should send it too
events(Events, Client);
_ ->
PlayerHP = NewUser#users.currenthp,
case lists:member(death, TargetSE) of
true -> SE = 16#01000200;
false -> SE = 16#01000000
end,
egs_proto:packet_send(Client, << 16#0e070300:32, 0:160, 16#00011300:32, GID:32/little, 0:64,
1:32/little, 16#01050000:32, Damage:32/little,
A/binary, 0:64, PlayerHP:32/little, 0:32, SE:32,
0:32, TargetHP:32/little, 0:32, B/binary,
16#04320000:32, 16#80000000:32, 16#26030000:32, 16#89068d00:32, 16#0c1c0105:32, 0:64 >>)
% after TargetHP is SE-related too?
end,
%% exp
if HasEXP =:= true ->
egs_proto:send_0115(NewUser, ToTargetID, Client);
true -> ignore
end,
%% save
egs_users:write(NewUser);
event({hits, Hits}, Client) ->
events(Hits, Client);
event({item_description_request, ItemID}, Client) ->
egs_proto:send_0a11(ItemID, egs_items_db:desc(ItemID), Client);
%% @todo A and B are unknown.
%% Melee uses a format similar to: AAAA--BBCCCC----DDDDDDDDEE----FF with
%% AAAA the attack sound effect, BB the range, CCCC and DDDDDDDD unknown but related to angular range or similar, EE number of targets and FF the model.
%% Bullets and tech weapons formats are unknown but likely use a slightly different format.
%% @todo Others probably want to see that you changed your weapon.
%% @todo Apparently B is always ItemID+1. Not sure why.
%% @todo Currently use a separate file for the data sent for the weapons.
%% @todo TargetGID and TargetLID must be validated, they're either the player's or his NPC characters.
%% @todo Handle NPC characters properly.
event({item_equip, ItemIndex, TargetGID, TargetLID, A, B}, Client=#egs_net{gid=GID}) ->
case egs_users:item_nth(GID, ItemIndex) of
{ItemID, Variables} when element(1, Variables) =:= psu_special_item_variables ->
<< Category:8, _:24 >> = << ItemID:32 >>,
egs_proto:packet_send(Client, << 16#01050300:32, 0:64, TargetGID:32/little, 0:64, 16#00011300:32, GID:32/little, 0:64,
TargetGID:32/little, TargetLID:32/little, ItemIndex:8, 1:8, Category:8, A:8, B:32/little >>);
{ItemID, Variables} when element(1, Variables) =:= psu_striking_weapon_item_variables ->
#psu_item{data=Constants} = egs_items_db:read(ItemID),
#psu_striking_weapon_item{attack_sound=Sound, hitbox_a=HitboxA, hitbox_b=HitboxB,
hitbox_c=HitboxC, hitbox_d=HitboxD, nb_targets=NbTargets, effect=Effect, model=Model} = Constants,
<< Category:8, _:24 >> = << ItemID:32 >>,
{SoundInt, SoundType} = case Sound of
{default, Val} -> {Val, 0};
{custom, Val} -> {Val, 8}
end,
egs_proto:packet_send(Client, << 16#01050300:32, 0:64, TargetGID:32/little, 0:64, 16#00011300:32, GID:32/little, 0:64,
TargetGID:32/little, TargetLID:32/little, ItemIndex:8, 1:8, Category:8, A:8, B:32/little,
SoundInt:32/little, HitboxA:16, HitboxB:16, HitboxC:16, HitboxD:16, SoundType:4, NbTargets:4, 0:8, Effect:8, Model:8 >>);
{ItemID, Variables} when element(1, Variables) =:= psu_trap_item_variables ->
#psu_item{data=#psu_trap_item{effect=Effect, type=Type}} = egs_items_db:read(ItemID),
<< Category:8, _:24 >> = << ItemID:32 >>,
Bin = case Type of
damage -> << Effect:8, 16#0c0a05:24, 16#20140500:32, 16#0001c800:32, 16#10000000:32 >>;
damage_g -> << Effect:8, 16#2c0505:24, 16#0c000600:32, 16#00049001:32, 16#10000000:32 >>;
trap -> << Effect:8, 16#0d0a05:24, 16#61140000:32, 16#0001c800:32, 16#10000000:32 >>;
trap_g -> << Effect:8, 16#4d0505:24, 16#4d000000:32, 16#00049001:32, 16#10000000:32 >>;
trap_ex -> << Effect:8, 16#490a05:24, 16#4500000f:32, 16#4b055802:32, 16#10000000:32 >>
end,
egs_proto:packet_send(Client, << 16#01050300:32, 0:64, TargetGID:32/little, 0:64, 16#00011300:32, GID:32/little, 0:64,
TargetGID:32/little, TargetLID:32/little, ItemIndex:8, 1:8, Category:8, A:8, B:32/little, Bin/binary >>);
undefined ->
%% @todo Shouldn't be needed later when NPCs are handled correctly.
ignore
end;
event({item_set_trap, ItemIndex, TargetGID, TargetLID, A, B}, Client=#egs_net{gid=GID}) ->
{ItemID, _Variables} = egs_users:item_nth(GID, ItemIndex),
egs_users:item_qty_add(GID, ItemIndex, -1),
<< Category:8, _:24 >> = << ItemID:32 >>,
egs_proto:packet_send(Client, << 16#01050300:32, 0:64, TargetGID:32/little, 0:64, 16#00011300:32, GID:32/little, 0:64,
TargetGID:32/little, TargetLID:32/little, ItemIndex:8, 9:8, Category:8, A:8, B:32/little >>);
%% @todo A and B are unknown.
%% @see item_equip
event({item_unequip, ItemIndex, TargetGID, TargetLID, A, B}, Client=#egs_net{gid=GID}) ->
Category = case ItemIndex of
% units would be 8, traps would be 12
19 -> 2; % armor
Y when Y =:= 5; Y =:= 6; Y =:= 7 -> 0; % clothes
_ -> 1 % weapons
end,
egs_proto:packet_send(Client, << 16#01050300:32, 0:64, GID:32/little, 0:64, 16#00011300:32, GID:32/little,
0:64, TargetGID:32/little, TargetLID:32/little, ItemIndex, 2, Category, A, B:32/little >>);
%% @todo Just ignore the meseta price for now and send the player where he wanna be!
event(lobby_transport_request, Client) ->
egs_proto:send_0c08(Client);
event(lumilass_options_request, Client=#egs_net{gid=GID}) ->
{ok, User} = egs_users:read(GID),
egs_proto:send_1a03(User, Client);
%% @todo Probably replenish the player HP when entering a non-mission area rather than when aborting the mission?
event(mission_abort, Client=#egs_net{gid=GID}) ->
egs_proto:send_1006(11, Client),
{ok, User} = egs_users:read(GID),
%% delete the mission
if User#users.instancepid =:= undefined -> ignore;
true -> psu_instance:stop(User#users.instancepid)
end,
%% full hp
User2 = User#users{currenthp=User#users.maxhp, instancepid=undefined},
egs_users:write(User2),
%% map change
if User2#users.areatype =:= mission ->
PrevArea = User2#users.prev_area,
event({area_change, element(1, PrevArea), element(2, PrevArea), element(3, PrevArea), User2#users.prev_entryid}, Client);
true -> ignore
end;
%% @todo Forward the mission start to other players of the same party, whatever their location is.
event({mission_start, QuestID}, Client) ->
io:format("~p: mission start ~b~n", [Client#egs_net.gid, QuestID]),
egs_proto:send_1020(Client),
egs_proto:send_1015(QuestID, Client),
egs_proto:send_0c02(Client);
%% @doc Force the invite of an NPC character while inside a mission. Mostly used by story missions.
%% Note that the NPC is often removed and reinvited between block/cutscenes.
event({npc_force_invite, NPCid}, Client=#egs_net{gid=GID}) ->
{ok, User} = egs_users:read(GID),
%% Create NPC.
io:format("~p: npc force invite ~p~n", [GID, NPCid]),
TmpNPCUser = egs_npc_db:create(NPCid, User#users.level),
%% Create and join party.
case User#users.partypid of
undefined ->
{ok, PartyPid} = psu_party:start_link(GID);
PartyPid ->
ignore
end,
{ok, PartyPos} = psu_party:join(PartyPid, npc, TmpNPCUser#users.gid),
#users{instancepid=InstancePid, area=Area, entryid=EntryID, pos=Pos} = User,
NPCUser = TmpNPCUser#users{lid=PartyPos, partypid=PartyPid, instancepid=InstancePid, areatype=mission, area=Area, entryid=EntryID, pos=Pos},
egs_users:write(NPCUser),
egs_users:write(User#users{partypid=PartyPid}),
%% Send stuff.
egs_proto:send_010d(NPCUser, Client),
egs_proto:send_0201(NPCUser, Client),
egs_proto:send_0215(0, Client),
egs_proto:send_0a04(NPCUser#users.gid, Client),
egs_proto:send_022c(0, 16#12, Client),
egs_proto:send_1004(npc_mission, NPCUser, PartyPos, Client),
egs_proto:send_100f(NPCUser#users.npcid, PartyPos, Client),
egs_proto:send_1601(PartyPos, Client);
%% @todo Also at the end send a 101a (NPC:16, PartyPos:16, ffffffff). Not sure about PartyPos.
event({npc_invite, NPCid}, Client=#egs_net{gid=GID}) ->
{ok, User} = egs_users:read(GID),
%% Create NPC.
io:format("~p: invited npcid ~b~n", [GID, NPCid]),
TmpNPCUser = egs_npc_db:create(NPCid, User#users.level),
%% Create and join party.
case User#users.partypid of
undefined ->
{ok, PartyPid} = psu_party:start_link(GID),
egs_proto:send_022c(0, 16#12, Client);
PartyPid ->
ignore
end,
{ok, PartyPos} = psu_party:join(PartyPid, npc, TmpNPCUser#users.gid),
NPCUser = TmpNPCUser#users{lid=PartyPos, partypid=PartyPid},
egs_users:write(NPCUser),
egs_users:write(User#users{partypid=PartyPid}),
%% Send stuff.
egs_proto:send_1004(npc_invite, NPCUser, PartyPos, Client),
egs_proto:send_101a(NPCid, PartyPos, Client);
%% @todo Should be 0115(money) 010a03(confirm sale).
event({npc_shop_buy, ShopItemIndex, QuantityOrColor}, Client=#egs_net{gid=GID}) ->
ShopID = egs_users:shop_get(GID),
ItemID = egs_shops_db:nth(ShopID, ShopItemIndex + 1),
io:format("~p: npc shop ~p buy itemid ~8.16.0b quantity/color+1 ~p~n", [GID, ShopID, ItemID, QuantityOrColor]),
#psu_item{name=Name, rarity=Rarity, buy_price=BuyPrice, sell_price=SellPrice, data=Constants} = egs_items_db:read(ItemID),
{Quantity, Variables} = case element(1, Constants) of
psu_clothing_item ->
if QuantityOrColor >= 1, QuantityOrColor =< 10 ->
{1, #psu_clothing_item_variables{color=QuantityOrColor - 1}}
end;
psu_consumable_item ->
{QuantityOrColor, #psu_consumable_item_variables{quantity=QuantityOrColor}};
psu_parts_item ->
{1, #psu_parts_item_variables{}};
psu_special_item ->
{1, #psu_special_item_variables{}};
psu_striking_weapon_item ->
#psu_striking_weapon_item{pp=PP, shop_element=Element} = Constants,
{1, #psu_striking_weapon_item_variables{current_pp=PP, max_pp=PP, element=Element}};
psu_trap_item ->
{QuantityOrColor, #psu_trap_item_variables{quantity=QuantityOrColor}}
end,
egs_users:money_add(GID, -1 * BuyPrice * Quantity),
ItemUUID = egs_users:item_add(GID, ItemID, Variables),
{ok, User} = egs_users:read(GID),
egs_proto:send_0115(User, Client), %% @todo This one is apparently broadcast to everyone in the same zone.
%% @todo Following command isn't done 100% properly.
UCS2Name = << << X:8, 0:8 >> || X <- Name >>,
NamePadding = 8 * (46 - byte_size(UCS2Name)),
<< Category:8, _:24 >> = << ItemID:32 >>,
RarityInt = Rarity - 1,
egs_proto:packet_send(Client, << 16#010a0300:32, 0:64, GID:32/little, 0:64, 16#00011300:32, GID:32/little, 0:64,
GID:32/little, 0:32, 2:16/little, 0:16, (egs_proto:build_item_variables(ItemID, ItemUUID, Variables))/binary,
UCS2Name/binary, 0:NamePadding, RarityInt:8, Category:8, SellPrice:32/little, (egs_proto:build_item_constants(Constants))/binary >>);
%% @todo Currently send the normal items shop for all shops, differentiate.
event({npc_shop_enter, ShopID}, Client=#egs_net{gid=GID}) ->
io:format("~p: npc shop enter ~p~n", [GID, ShopID]),
egs_users:shop_enter(GID, ShopID),
egs_proto:send_010a(egs_shops_db:read(ShopID), Client);
event({npc_shop_leave, ShopID}, Client=#egs_net{gid=GID}) ->
io:format("~p: npc shop leave ~p~n", [GID, ShopID]),
egs_users:shop_leave(GID),
egs_proto:packet_send(Client, << 16#010a0300:32, 0:64, GID:32/little, 0:64, 16#00011300:32,
GID:32/little, 0:64, GID:32/little, 0:32 >>);
%% @todo Should be 0115(money) 010a03(confirm sale).
event({npc_shop_sell, InventoryItemIndex, Quantity}, Client) ->
io:format("~p: npc shop sell itemindex ~p quantity ~p~n", [Client#egs_net.gid, InventoryItemIndex, Quantity]);
%% @todo First 1a02 value should be non-0.
%% @todo Could the 2nd 1a02 parameter simply be the shop type or something?
%% @todo Although the values replied should be right, they seem mostly ignored by the client.
event({npc_shop_request, ShopID}, Client) ->
io:format("~p: npc shop request ~p~n", [Client#egs_net.gid, ShopID]),
case ShopID of
80 -> egs_proto:send_1a02(17, 17, 3, 9, Client); %% lumilass
90 -> egs_proto:send_1a02(5, 1, 4, 5, Client); %% parum weapon grinding
91 -> egs_proto:send_1a02(5, 5, 4, 7, Client); %% tenora weapon grinding
92 -> egs_proto:send_1a02(5, 8, 4, 0, Client); %% yohmei weapon grinding
93 -> egs_proto:send_1a02(5, 18, 4, 0, Client); %% kubara weapon grinding
_ -> egs_proto:send_1a02(0, 1, 0, 0, Client)
end;
%% @todo Not sure what are those hardcoded values.
event({object_boss_gate_activate, ObjectID}, Client) ->
egs_proto:send_1213(ObjectID, 0, Client),
egs_proto:send_1215(2, 16#7008, Client),
%% @todo Following sent after the warp?
egs_proto:send_1213(37, 0, Client),
%% @todo Why resend this?
egs_proto:send_1213(ObjectID, 0, Client);
event({object_boss_gate_enter, ObjectID}, Client) ->
egs_proto:send_1213(ObjectID, 1, Client);
%% @todo Do we need to send something back here?
event({object_boss_gate_leave, _ObjectID}, _Client) ->
ignore;
event({object_box_destroy, ObjectID}, Client) ->
egs_proto:send_1213(ObjectID, 3, Client);
%% @todo Second send_1211 argument should be User#users.lid. Fix when it's correctly handled.
event({object_chair_sit, ObjectTargetID}, Client) ->
egs_proto:send_1211(ObjectTargetID, 0, 8, 0, Client);
%% @todo Second send_1211 argument should be User#users.lid. Fix when it's correctly handled.
event({object_chair_stand, ObjectTargetID}, Client) ->
egs_proto:send_1211(ObjectTargetID, 0, 8, 2, Client);
event({object_crystal_activate, ObjectID}, Client) ->
egs_proto:send_1213(ObjectID, 1, Client);
%% @doc Server-side event.
event({object_event_trigger, BlockID, EventID}, Client) ->
egs_proto:send_1205(EventID, BlockID, 0, Client);
event({object_goggle_target_activate, ObjectID}, Client=#egs_net{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, EventID} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
egs_proto:send_1205(EventID, BlockID, 0, Client),
egs_proto:send_1213(ObjectID, 8, Client);
%% @todo Make NPC characters heal too.
event({object_healing_pad_tick, [_PartyPos]}, Client=#egs_net{gid=GID}) ->
{ok, User} = egs_users:read(GID),
if User#users.currenthp =:= User#users.maxhp -> ignore;
true ->
NewHP = User#users.currenthp + User#users.maxhp div 10,
NewHP2 = if NewHP > User#users.maxhp -> User#users.maxhp; true -> NewHP end,
User2 = User#users{currenthp=NewHP2},
egs_users:write(User2),
egs_proto:send_0117(User2, Client),
egs_proto:send_0111(User2, 4, Client)
end;
event({object_key_console_enable, ObjectID}, Client=#egs_net{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, [EventID|_]} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
egs_proto:send_1205(EventID, BlockID, 0, Client),
egs_proto:send_1213(ObjectID, 1, Client);
event({object_key_console_init, ObjectID}, Client=#egs_net{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, [_, EventID, _]} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
egs_proto:send_1205(EventID, BlockID, 0, Client);
event({object_key_console_open_gate, ObjectID}, Client=#egs_net{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, [_, _, EventID]} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
egs_proto:send_1205(EventID, BlockID, 0, Client),
egs_proto:send_1213(ObjectID, 1, Client);
%% @todo Now that it's separate from object_key_console_enable, handle it better than that, don't need a list of events.
event({object_key_enable, ObjectID}, Client=#egs_net{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, [EventID|_]} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
egs_proto:send_1205(EventID, BlockID, 0, Client),
egs_proto:send_1213(ObjectID, 1, Client);
%% @todo Some switch objects apparently work differently, like the light switch in Mines in MAG'.
event({object_switch_off, ObjectID}, Client=#egs_net{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, EventID} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
egs_proto:send_1205(EventID, BlockID, 1, Client),
egs_proto:send_1213(ObjectID, 0, Client);
event({object_switch_on, ObjectID}, Client=#egs_net{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, EventID} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
egs_proto:send_1205(EventID, BlockID, 0, Client),
egs_proto:send_1213(ObjectID, 1, Client);
event({object_vehicle_boost_enable, ObjectID}, Client) ->
egs_proto:send_1213(ObjectID, 1, Client);
event({object_vehicle_boost_respawn, ObjectID}, Client) ->
egs_proto:send_1213(ObjectID, 0, Client);
%% @todo Second send_1211 argument should be User#users.lid. Fix when it's correctly handled.
event({object_warp_take, BlockID, ListNb, ObjectNb}, Client=#egs_net{gid=GID}) ->
{ok, User} = egs_users:read(GID),
Pos = psu_instance:warp_event(User#users.instancepid, element(2, User#users.area), BlockID, ListNb, ObjectNb),
NewUser = User#users{pos=Pos},
egs_users:write(NewUser),
egs_proto:send_0503(User#users.pos, Client),
egs_proto:send_1211(16#ffffffff, 0, 14, 0, Client);
%% @todo Don't send_0204 if the player is removed from the party while in the lobby I guess.
event({party_remove_member, PartyPos}, Client=#egs_net{gid=GID}) ->
io:format("~p: party remove member ~b~n", [GID, PartyPos]),
{ok, DestUser} = egs_users:read(GID),
{ok, RemovedGID} = psu_party:get_member(DestUser#users.partypid, PartyPos),
psu_party:remove_member(DestUser#users.partypid, PartyPos),
{ok, RemovedUser} = egs_users:read(RemovedGID),
case RemovedUser#users.type of
npc -> egs_users:delete(RemovedGID);
_ -> ignore
end,
egs_proto:send_1006(8, PartyPos, Client),
egs_proto:send_0204(RemovedUser, Client),
egs_proto:send_0215(0, Client);
event({player_options_change, Options}, #egs_net{gid=GID, slot=Slot}) ->
Folder = egs_accounts:get_folder(GID),
file:write_file(io_lib:format("save/~s/~b-character.options", [Folder, Slot]), Options);
%% @todo If the player has a scape, use it! Otherwise red screen.
%% @todo Right now we force revive with a dummy HP value.
event(player_death, Client=#egs_net{gid=GID}) ->
% @todo send_0115(GID, 16#ffffffff, LV=1, EXP=idk, Money=1000), % apparently sent everytime you die...
%% use scape:
NewHP = 10,
{ok, User} = egs_users:read(GID),
User2 = User#users{currenthp=NewHP},
egs_users:write(User2),
egs_proto:send_0117(User2, Client),
egs_proto:send_1022(User2, Client);
%% red screen with return to lobby choice:
%~ egs_proto:send_0111(User2, 3, 1, Client);
%% @todo Refill the player's HP to maximum, remove SEs etc.
event(player_death_return_to_lobby, Client=#egs_net{gid=GID}) ->
{ok, User} = egs_users:read(GID),
PrevArea = User#users.prev_area,
event({area_change, element(1, PrevArea), element(2, PrevArea), element(3, PrevArea), User#users.prev_entryid}, Client);
event(player_type_availability_request, Client) ->
egs_proto:send_1a07(Client);
event(character_type_capabilities_request, Client) ->
egs_proto:send_0113(Client);
event(ppcube_request, Client) ->
egs_proto:send_1a04(Client);
event(unicube_request, Client) ->
egs_proto:send_021e(egs_universes:all(), Client);
%% @todo When selecting 'Your room', don't load a default room that's not yours.
event({unicube_select, cancel, _EntryID}, _Client) ->
ignore;
event({unicube_select, Selection, EntryID}, Client=#egs_net{gid=GID}) ->
{ok, User} = egs_users:read(GID),
case Selection of
16#ffffffff ->
UniID = egs_universes:myroomid(),
User2 = User#users{uni=UniID, area={1120000, 0, 100}, entryid=0};
_ ->
UniID = Selection,
User2 = User#users{uni=UniID, entryid=EntryID}
end,
egs_proto:send_0230(Client),
%% 0220
case User#users.partypid of
undefined -> ignore;
PartyPid ->
%% @todo Replace stop by leave when leaving stops the party correctly when nobody's there anymore.
%~ psu_party:leave(User#users.partypid, User#users.gid)
{ok, NPCList} = psu_party:get_npc(PartyPid),
[egs_users:delete(NPCGID) || {_Spot, NPCGID} <- NPCList],
psu_party:stop(PartyPid)
end,
egs_users:write(User2),
egs_universes:leave(User#users.uni),
egs_universes:enter(UniID),
char_load(User2, Client).
%% Internal.
%% @doc Trigger many events.
events(Events, Client) ->
[event(Event, Client) || Event <- Events],
ok.
%% @doc Load and send the character information to the client.
char_load(User, Client) ->
egs_net:account_character(User#users.char, Client),
%% 0246
egs_proto:send_0a0a(User#users.inventory, Client),
egs_proto:send_1006(5, 0, Client), %% @todo The 0 here is PartyPos, save it in User.
egs_proto:send_1005(User, Client),
egs_proto:send_1006(12, Client),
egs_proto:send_0210(Client),
egs_proto:send_0222(User#users.uni, Client),
egs_net:comm_own_card(User#users.char, Client),
egs_proto:send_1501(Client),
egs_proto:send_1512(Client),
%% 0303
egs_proto:send_1602(Client),
egs_proto:send_021b(Client).
%% @todo Don't change the NPC info unless you are the leader!
npc_load(_Leader, [], _Client) ->
ok;
npc_load(Leader, [{PartyPos, NPCGID}|NPCList], Client) ->
{ok, OldNPCUser} = egs_users:read(NPCGID),
#users{instancepid=InstancePid, area=Area, entryid=EntryID, pos=Pos} = Leader,
NPCUser = OldNPCUser#users{lid=PartyPos, instancepid=InstancePid, areatype=mission, area=Area, entryid=EntryID, pos=Pos},
%% @todo This one on mission end/abort?
%~ OldNPCUser#users{lid=PartyPos, instancepid=undefined, areatype=AreaType, area={0, 0, 0}, entryid=0, pos={0.0, 0.0, 0.0, 0}}
egs_users:write(NPCUser),
egs_proto:send_010d(NPCUser, Client),
egs_proto:send_0201(NPCUser, Client),
egs_proto:send_0215(0, Client),
egs_proto:send_0a04(NPCUser#users.gid, Client),
egs_proto:send_1004(npc_mission, NPCUser, PartyPos, Client),
egs_proto:send_100f(NPCUser#users.npcid, PartyPos, Client),
egs_proto:send_1601(PartyPos, Client),
egs_proto:send_1016(PartyPos, Client),
npc_load(Leader, NPCList, Client).

View File

@ -0,0 +1,62 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc Cowboy protocol module for the game server.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_game_protocol).
-export([start_link/4, init/2]).
-include("include/records.hrl").
-spec start_link(pid(), ssl:sslsocket(), module(), []) -> {ok, pid()}.
start_link(_ListenerPid, Socket, Transport, []) ->
Pid = spawn_link(?MODULE, init, [Socket, Transport]),
{ok, Pid}.
-spec init(ssl:sslsocket(), module()) -> ok.
%% @todo Handle keepalive messages globally?
init(Socket, Transport) ->
{ok, _TRef} = timer:send_interval(5000, {egs, keepalive}),
Client = egs_net:init(Socket, Transport, egs_login,
egs_accounts:tmp_gid()),
egs_net:system_hello(Client),
catch egs_net:loop(Client),
terminate().
-spec terminate() -> ok.
%% @todo Just use monitors to handle cleanups.
%% @todo Cleanup the instance process if there's nobody in it anymore.
%% @todo Leave party instead of stopping it.
%% @todo Fix the crash when user isn't in egs_users yet.
terminate() ->
case egs_users:find_by_pid(self()) of
undefined -> ok;
User ->
case User#users.partypid of
undefined ->
ignore;
PartyPid ->
{ok, NPCList} = psu_party:get_npc(PartyPid),
lists:foreach(fun({_Spot, NPCGID}) ->
egs_users:delete(NPCGID) end, NPCList),
psu_party:stop(PartyPid)
end,
egs_zones:leave(User#users.zonepid, User#users.gid),
egs_universes:leave(User#users.uni),
egs_users:delete(User#users.gid),
io:format("game (~p): quit~n", [User#users.gid])
end.

View File

@ -1,5 +1,5 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin.
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS items database.
%%
%% This file is part of EGS.
@ -19,6 +19,7 @@
-module(egs_items_db).
-behavior(gen_server).
-export([start_link/0, stop/0, desc/1, read/1, reload/0]). %% API.
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
@ -26,7 +27,7 @@
-define(SERVER, ?MODULE).
-include("include/records.hrl").
-include("priv/items.hrl").
-include("../../priv/items.hrl").
%% API.

View File

@ -0,0 +1,81 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc Log in and authentication callback module.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_login).
-export([info/2, cast/3, event/2]).
-include("include/records.hrl").
%% @doc We don't expect any message here.
info(_Msg, _Client) ->
ok.
%% @doc Nothing to broadcast.
cast(_Command, _Data, _Client) ->
ok.
%% Events.
%% @doc Reject version < 2.0009.2.
%% @todo Reject wrong platforms too.
%% @todo Put the URL in a configuration file.
event({client_version, _Entrance, _Language, _Platform, Version}, Client)
when Version < 2009002 ->
egs_net:system_open_url(<<"http://psumods.co.uk/forums/comments.php?DiscussionID=40#Item_1">>, Client),
{ok, Error} = file:read_file("priv/login/error_version.txt"),
egs_net:system_auth_error(Error, Client),
egs_net:terminate(Client),
closed;
event({client_version, _Entrance, _Language, _Platform, _Version}, _Client) ->
ok;
%% @doc Game server info request handler.
event(system_game_server_request, Client) ->
{ServerIP, ServerPort} = egs_conf:read(game_server),
egs_net:system_game_server_response(ServerIP, ServerPort, Client),
egs_net:terminate(Client),
closed;
%% @doc Authenticate the user by pattern matching its saved state against the key received.
%% If the user is authenticated, send him the character flags list.
event({system_key_auth, AuthGID, AuthKey}, Client) ->
egs_accounts:key_auth(AuthGID, AuthKey),
Client2 = egs_net:set_gid(AuthGID, Client),
ValueFlags = egs_conf:read(value_flags),
BoolFlags = egs_conf:read(bool_flags),
TempFlags = egs_conf:read(temp_flags),
egs_net:account_flags(ValueFlags, BoolFlags, TempFlags, Client2),
Client3 = egs_net:set_handler(egs_char_select, Client2),
Client4 = egs_net:set_keepalive(Client3),
{ok, Client4};
%% @doc Authentication request handler. Currently always succeed.
%% @todo Handle real GIDs whenever there's real authentication. GID is the second SessionID in the reply.
%% @todo Apparently it's possible to ask a question in the reply here. Used for free course on JP.
event({system_login_auth, Username, Password}, Client) ->
{ok, AuthGID} = egs_accounts:login_auth(Username, Password),
{ok, AuthKey} = egs_accounts:key_auth_init(AuthGID),
io:format("auth success for ~s ~s~n", [Username, Password]),
egs_net:system_key_auth_info(AuthGID, AuthKey, Client);
%% @doc MOTD request handler. Page number starts at 0.
%% @todo Currently ignore the language and send the same MOTD file to everyone.
event({system_motd_request, Page, _Language}, Client) ->
{ok, MOTD} = file:read_file("priv/login/motd.txt"),
egs_net:system_motd_response(MOTD, Page, Client).

View File

@ -1,6 +1,6 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin.
%% @doc Login server module.
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc Cowboy protocol module for the login server.
%%
%% This file is part of EGS.
%%
@ -17,25 +17,19 @@
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_login_server).
-export([start_link/1, on_exit/1, init/1]).
-module(egs_login_protocol).
-export([start_link/4, init/2]).
-include("include/records.hrl").
%% @spec start_link(Port) -> {ok,Pid::pid()}
%% @doc Start the login server.
start_link(Port) ->
Pid = spawn(egs_network, listen, [Port, ?MODULE]),
-spec start_link(pid(), ssl:sslsocket(), module(), []) -> {ok, pid()}.
start_link(_ListenerPid, Socket, Transport, []) ->
Pid = spawn_link(?MODULE, init, [Socket, Transport]),
{ok, Pid}.
%% @spec on_exit(Pid) -> ok
%% @doc Nothing to do for the login server.
on_exit(_Pid) ->
ok.
%% @doc Initialize the game state and start receiving messages.
init(Socket) ->
TmpGID = 16#ff000000 + mnesia:dirty_update_counter(counters, tmpgid, 1),
State = #state{socket=Socket, gid=TmpGID},
psu_proto:send_0202(State),
egs_network:recv(<< >>, egs_login, State).
-spec init(ssl:sslsocket(), module()) -> ok | closed.
init(Socket, Transport) ->
Client = egs_net:init(Socket, Transport, egs_login,
egs_accounts:tmp_gid()),
egs_net:system_hello(Client),
egs_net:loop(Client).

View File

@ -1,5 +1,5 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin.
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS NPC database.
%%
%% This file is part of EGS.
@ -19,14 +19,15 @@
-module(egs_npc_db).
-behavior(gen_server).
-export([start_link/0, stop/0, all/0, count/0, create/2, reload/0]). %% API.
-export([start_link/0, stop/0, all/0, create/2, reload/0]). %% API.
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
%% Use the module name for the server's name.
-define(SERVER, ?MODULE).
-include("include/records.hrl").
-include("priv/npc.hrl").
-include("../../priv/npc.hrl").
%% API.
@ -42,10 +43,6 @@ stop() ->
all() ->
gen_server:call(?SERVER, all).
%% @spec count() -> integer()
count() ->
gen_server:call(?SERVER, count).
%% @spec read(NPCid, BaseLevel) -> term()
create(NPCid, BaseLevel) ->
gen_server:call(?SERVER, {create, NPCid, BaseLevel}).
@ -63,19 +60,17 @@ init([]) ->
handle_call(all, _From, State) ->
{reply, ?NPC, State};
handle_call(count, _From, State) ->
{reply, length(?NPC), State};
%% @todo Handle stats, experience, based on level.
handle_call({create, NPCid, BaseLevel}, _From, State) ->
NPCGID = 16#ff000000 + mnesia:dirty_update_counter(counters, tmpgid, 1),
NPCGID = egs_accounts:tmp_gid(),
#npc{name=Name, race=Race, gender=Gender, class=Class, level_diff=LevelDiff, appearance=Appearance} = proplists:get_value(NPCid, ?NPC),
TmpUCS2Name = << << X:8, 0:8 >> || X <- Name >>,
Padding = 8 * (64 - byte_size(TmpUCS2Name)),
UCS2Name = << TmpUCS2Name/binary, 0:Padding >>,
Character = #characters{gid=NPCGID, slot=0, type=npc, npcid=NPCid, name=UCS2Name, race=Race, gender=Gender, class=Class, appearance=Appearance,
mainlevel={level, calc_level(BaseLevel, LevelDiff), 0}, blastbar=0, luck=2, money=0, playtime=0, stats={stats, 0, 0, 0, 0, 0, 0, 0}, se=[], currenthp=100, maxhp=100},
User = #egs_user_model{id=NPCGID, character=Character, areatype=lobby, area={psu_area, 0, 0, 0}, entryid=0},
User = #users{gid=NPCGID, slot=0, type=npc, npcid=NPCid, name=UCS2Name, race=Race, gender=Gender, class=Class, appearance=Appearance,
level=calc_level(BaseLevel, LevelDiff), blastbar=0, luck=2, money=0,
stats={stats, 0, 0, 0, 0, 0, 0, 0}, currenthp=100, maxhp=100,
areatype=lobby, area={0, 0, 0}, entryid=0},
{reply, User, State};
handle_call(stop, _From, State) ->

726
apps/egs/src/egs_proto.erl Normal file
View File

@ -0,0 +1,726 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc Independent implementation of the PSU protocol.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_proto).
-compile(export_all).
-include("include/records.hrl").
%% @spec assert() -> ok
%% @doc Log a detailed message when the function is called.
-define(ASSERT(), io:format("assert error in module ~p on line ~p~n", [?MODULE, ?LINE])).
%% @spec assert(A, B) -> ok
%% @doc Log a detailed message when the assertion A =:= B fails.
-define(ASSERT_EQ(A, B), if A =:= B -> ok; true -> io:format("assert error in module ~p on line ~p~n", [?MODULE, ?LINE]) end).
%% @doc Send a shop listing.
%% @todo This packet (and its build_010a_list function) hasn't been reviewed at all yet.
send_010a(ItemsList, Client=#egs_net{gid=DestGID}) ->
NbItems = length(ItemsList),
ItemsBin = build_010a_list(ItemsList, []),
packet_send(Client, << 16#010a0300:32, 0:64, DestGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64,
DestGID:32/little, 0:32, 1:16/little, NbItems:8, 2:8, 0:32, ItemsBin/binary >>).
%% @todo The values set to 0 are unknown.
build_010a_list([], Acc) ->
iolist_to_binary(lists:reverse(Acc));
build_010a_list([ItemID|Tail], Acc) ->
#psu_item{name=Name, rarity=Rarity, buy_price=SellPrice, data=Data} = egs_items_db:read(ItemID),
UCS2Name = << << X:8, 0:8 >> || X <- Name >>,
NamePadding = 8 * (46 - byte_size(UCS2Name)),
RarityBin = Rarity - 1,
DataBin = build_item_constants(Data),
BinItemID = case element(1, Data) of
psu_clothing_item -> %% Change the ItemID to enable all colors.
<< A:8, _:4, B:12, _:8 >> = << ItemID:32 >>,
<< A:8, 3:4, B:12, 16#ff:8 >>;
_Any ->
<< ItemID:32 >>
end,
Bin = << UCS2Name/binary, 0:NamePadding, RarityBin:8, 0:8, BinItemID/binary, SellPrice:32/little, DataBin/binary >>,
build_010a_list(Tail, [Bin|Acc]).
%% @doc Send character appearance and other information.
%% @todo Probably don't pattern match the data like this...
send_010d(CharUser, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
CharGID = CharUser#users.gid,
CharLID = CharUser#users.lid,
<< _:640, CharBin/bits >> = psu_characters:character_user_to_binary(CharUser),
packet_send(Client, << 16#010d0300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little,
0:64, 1:32/little, 0:32, 16#00000300:32, 16#ffff0000:32, 0:32, CharGID:32/little,
0:192, CharGID:32/little, CharLID:32/little, 16#ffffffff:32, CharBin/binary >>).
%% @doc Trigger a character-related event.
send_0111(CharUser, EventID, Client) ->
send_0111(CharUser, EventID, 0, Client).
send_0111(#users{gid=CharGID, lid=CharLID}, EventID, Param, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
packet_send(Client, << 16#01110300:32, DestLID:16/little, 0:48, CharGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64,
CharGID:32/little, CharLID:32/little, EventID:32/little, Param:32/little >>).
%% @todo Types capability list.
%% @todo This packet hasn't been reviewed at all yet.
send_0113(Client=#egs_net{gid=DestGID}) ->
{ok, File} = file:read_file("p/typesinfo.bin"),
packet_send(Client, << 16#01130300:32, 0:64, DestGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64, DestGID:32/little, File/binary >>).
%% @doc Update the character level, blastbar, luck and money information.
send_0115(User, Client) ->
send_0115(User, 16#ffffffff, Client).
send_0115(User=#users{gid=CharGID, lid=CharLID}, EnemyTargetID, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
packet_send(Client, << 16#01150300:32, DestLID:16/little, 0:48, CharGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64,
CharGID:32/little, CharLID:32/little, EnemyTargetID:32/little, (build_char_level(User))/binary >>).
%% @doc Revive player with optional SEs.
%% @todo SEs.
send_0117(#users{gid=CharGID, lid=CharLID, currenthp=HP}, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
SE = << 0:64 >>,
packet_send(Client, << 16#01170300:32, DestLID:16/little, 0:48, CharGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64,
CharGID:32/little, CharLID:32/little, SE/binary, HP:32/little, 0:32 >>).
%% @doc Send the zone initialization command.
%% @todo Handle NbPlayers properly. There's more than 1 player!
send_0200(ZoneID, ZoneType, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
Var = case ZoneType of
mission -> << 16#06000500:32, 16#01000000:32, 0:64, 16#00040000:32, 16#00010000:32, 16#00140000:32 >>;
myroom -> << 16#06000000:32, 16#02000000:32, 0:64, 16#40000000:32, 16#00010000:32, 16#00010000:32 >>;
_ -> << 16#00040000:32, 0:160, 16#00140000:32 >>
end,
packet_send(Client, << 16#02000300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
DestLID:16/little, ZoneID:16/little, 1:32/little, 16#ffffffff:32, Var/binary, 16#ffffffff:32, 16#ffffffff:32 >>).
%% @doc Send character location, appearance and other information.
send_0201(CharUser, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
[CharTypeID, GameVersion] = case CharUser#users.type of
npc -> [16#00001d00, 255];
_ -> [16#00001200, 0]
end,
CharGID = CharUser#users.gid,
CharBin = psu_characters:character_user_to_binary(CharUser),
IsGM = 0,
OnlineStatus = 0,
packet_send(Client, << 16#02010300:32, DestLID:16/little, 0:16, CharTypeID:32, CharGID:32/little,
0:64, 16#00011300:32, DestGID:32/little, 0:64, CharBin/binary, IsGM:8, 0:8, OnlineStatus:8, GameVersion:8, 0:608 >>).
%% @doc Spawn a player with the given GID and LID.
send_0203(#users{gid=CharGID, lid=CharLID}, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
packet_send(Client, << 16#02030300:32, DestLID:16/little, 0:144, 16#00011300:32,
DestGID:32/little, 0:64, CharGID:32/little, CharLID:32/little >>).
%% @doc Unspawn the given character.
%% @todo The last 4 bytes are probably the number of players remaining in the zone.
send_0204(User, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
CharTypeID = case User#users.type of
npc -> 16#00001d00;
_ -> 16#00001200
end,
#users{gid=CharGID, lid=CharLID} = User,
packet_send(Client, << 16#02040300:32, DestLID:16/little, 0:16, CharTypeID:32, CharGID:32/little, 0:64,
16#00011300:32, DestGID:32/little, 0:64, CharGID:32/little, CharLID:32/little, 100:32/little >>).
%% @doc Make the client load a new map.
send_0205(CharUser, IsSeasonal, Client=#egs_net{gid=DestGID, lid=DestLID, areanb=AreaNb}) ->
#users{lid=CharLID, area={_QuestID, ZoneID, MapID}, entryid=EntryID} = CharUser,
packet_send(Client, << 16#02050300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
16#ffffffff:32, ZoneID:32/little, MapID:32/little, EntryID:32/little, AreaNb:32/little, CharLID:16/little, 0:8, IsSeasonal:8 >>).
%% @doc Indicate to the client that loading should finish.
send_0208(Client=#egs_net{gid=DestGID, lid=DestLID, areanb=AreaNb}) ->
packet_send(Client, << 16#02080300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64, AreaNb:32/little >>).
%% @todo No idea what this one does. For unknown reasons it uses channel 2.
%% @todo Handle the DestLID properly?
send_020c(Client) ->
packet_send(Client, << 16#020c0200:32, 16#ffff0000:32, 0:256 >>).
%% @doc Send the quest file to be loaded by the client.
%% @todo Handle the DestLID properly?
send_020e(QuestData, Client) ->
Size = byte_size(QuestData),
packet_send(Client, << 16#020e0300:32, 16#ffff:16, 0:272, Size:32/little, 0:32, QuestData/binary, 0:32 >>).
%% @doc Send the zone file to be loaded.
send_020f(ZoneData, SetID, SeasonID, Client) ->
Size = byte_size(ZoneData),
packet_send(Client, << 16#020f0300:32, 16#ffff:16, 0:272, SetID, SeasonID, 0:16, Size:32/little, ZoneData/binary >>).
%% @doc Send the current UNIX time.
send_0210(Client=#egs_net{gid=DestGID, lid=DestLID}) ->
{M, S, _} = erlang:now(),
UnixTime = M * 1000000 + S,
packet_send(Client, << 16#02100300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:96, UnixTime:32/little >>).
%% @todo No idea what this is doing.
send_0215(UnknownValue, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
packet_send(Client, << 16#02150300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64, UnknownValue:32/little >>).
%% @doc End of character loading.
send_021b(Client=#egs_net{gid=DestGID, lid=DestLID}) ->
packet_send(Client, << 16#021b0300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64 >>).
%% @doc Send the list of available universes.
send_021e(Universes, Client) ->
NbUnis = length(Universes),
UnisBin = build_021e_uni(Universes, []),
packet_send(Client, << 16#021e0300:32, 0:288, NbUnis:32/little, UnisBin/binary >>).
build_021e_uni([], Acc) ->
iolist_to_binary(lists:reverse(Acc));
build_021e_uni([{_UniID, {myroom, Name, NbPlayers, _MaxPlayers}}|Tail], Acc) ->
Padding = 8 * (44 - byte_size(Name)),
Bin = << 16#ffffffff:32, NbPlayers:16/little, 0:16, Name/binary, 0:Padding >>,
build_021e_uni(Tail, [Bin|Acc]);
build_021e_uni([{UniID, {universe, Name, NbPlayers, _MaxPlayers}}|Tail], Acc) ->
Padding = 8 * (32 - byte_size(Name)),
PopString = lists:flatten(io_lib:format("~5b", [NbPlayers])),
PopString2 = << << X:8, 0:8 >> || X <- PopString >>,
Bin = << UniID:32/little, NbPlayers:16/little, 643:16/little, Name/binary, 0:Padding, PopString2/binary, 0:16 >>,
build_021e_uni(Tail, [Bin|Acc]).
%% @doc Send the current universe info along with the current level cap.
send_0222(UniID, Client=#egs_net{gid=DestGID}) ->
{_Type, Name, NbPlayers, MaxPlayers} = egs_universes:read(UniID),
Padding = 8 * (44 - byte_size(Name)),
LevelCap = egs_conf:read(level_cap),
packet_send(Client, << 16#02220300:32, 16#ffff:16, 0:16, 16#00001200:32, DestGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64,
UniID:32/little, NbPlayers:16/little, MaxPlayers:16/little, Name/binary, 0:Padding, LevelCap:32/little >>).
%% @doc Display a notice on the player's screen.
%% There are four types of notices: dialog, top, scroll and timeout.
%% * dialog: A dialog in the center of the screen, which can be OK'd by players.
%% * top: Horizontal scroll on top of the screen, traditionally used for server-wide messages.
%% * scroll: Vertical scroll on the right of the screen, traditionally used for rare missions obtention messages.
%% * timeout: A dialog in the center of the screen that disappears after Duration seconds.
send_0228(Type, Duration, Message, Client=#egs_net{gid=DestGID}) ->
TypeInt = case Type of dialog -> 0; top -> 1; scroll -> 2; timeout -> 3 end,
UCS2Message = << << X:8, 0:8 >> || X <- Message >>,
packet_send(Client, << 16#02280300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
TypeInt:32/little, Duration:32/little, UCS2Message/binary, 0:16 >>).
%% @todo No idea!
%% @todo This packet hasn't been reviewed at all yet.
send_022c(A, B, Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#022c0300:32, 0:160, 16#00011300:32, DestGID:32/little, 0:64, A:16/little, B:16/little >>).
%% @todo Not sure. Sent when going to or from room. Possibly when changing universes too?
send_0230(Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#02300300:32, 16#ffff:16, 0:16, 16#00011300:32, DestGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64 >>).
%% @doc Send the list of players already spawned in the zone when entering it.
send_0233(Users, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
NbUsers = length(Users),
Bin = build_0233_users(Users, []),
packet_send(Client, << 16#02330300:32, DestLID:16/little, 0:16, 16#00001200:32, DestGID:32/little, 0:64,
16#00011300:32, DestGID:32/little, 0:64, NbUsers:32/little, Bin/binary, 0:608 >>).
build_0233_users([], Acc) ->
iolist_to_binary(lists:reverse(Acc));
build_0233_users([User|Tail], Acc) ->
Bin = psu_characters:character_user_to_binary(User),
build_0233_users(Tail, [<< Bin/binary, 0:32 >>|Acc]).
%% @doc Start the zone handling: load the zone file and the objects sent separately.
send_0236(Client=#egs_net{gid=DestGID, lid=DestLID}) ->
packet_send(Client, << 16#02360300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64 >>).
%% @doc Chat message.
send_0304(FromGID, ChatTypeID, ChatGID, ChatName, ChatModifiers, ChatMessage, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
{chat_modifiers, ChatType, ChatCutIn, ChatCutInAngle, ChatMsgLength, ChatChannel, ChatCharacterType} = ChatModifiers,
packet_send(Client, << 16#03040300:32, DestLID:16/little, 0:16, 16#00011300:32, FromGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64,
ChatTypeID:32, ChatGID:32/little, 0:64, ChatType:8, ChatCutIn:8, ChatCutInAngle:8, ChatMsgLength:8,
ChatChannel:8, ChatCharacterType:8, 0:16, ChatName/binary, ChatMessage/binary >>).
%% @todo Force send a new player location. Used for warps.
%% @todo The value before IntDir seems to be the player's current animation. 01 stand up, 08 ?, 17 normal sit
%% @todo This packet hasn't been reviewed at all yet.
send_0503({PrevX, PrevY, PrevZ, _AnyDir}, Client=#egs_net{gid=DestGID}) ->
{ok, User} = egs_users:read(DestGID),
#users{pos={X, Y, Z, Dir}, area={QuestID, ZoneID, MapID}, entryid=EntryID} = User,
IntDir = trunc(Dir * 182.0416),
packet_send(Client, << 16#05030300:32, 0:64, DestGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64, DestGID:32/little, 0:32,
16#1000:16, IntDir:16/little, PrevX:32/little-float, PrevY:32/little-float, PrevZ:32/little-float, X:32/little-float, Y:32/little-float, Z:32/little-float,
QuestID:32/little, ZoneID:32/little, MapID:32/little, EntryID:32/little, 1:32/little >>).
%% @todo NPC inventory. Guessing it's only for NPC characters...
%% @todo This packet hasn't been reviewed at all yet.
send_0a04(NPCGID, Client=#egs_net{gid=DestGID}) ->
{ok, Bin} = file:read_file("p/packet0a04.bin"),
packet_send(Client, << 16#0a040300:32, 0:32, 16#00001d00:32, NPCGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64, Bin/binary >>).
%% @todo Inventory related. Doesn't seem to do anything.
send_0a05(Client=#egs_net{gid=DestGID, lid=DestLID}) ->
packet_send(Client, << 16#0a050300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64 >>).
%% @doc Send the list of ItemUUID for the items in the inventory.
send_0a06(CharUser, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
Len = length(CharUser#users.inventory),
UUIDs = lists:seq(1, Len),
Bin = iolist_to_binary([ << N:32/little >> || N <- UUIDs]),
Blanks = lists:seq(1, 60 - Len),
Bin2 = iolist_to_binary([ << 16#ffffffff:32 >> || _N <- Blanks]),
packet_send(Client, << 16#0a060300:32, DestLID:16/little, 0:48, DestGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64, Bin/binary, Bin2/binary >>).
%% @todo Handle more than just goggles.
%% @todo This packet hasn't been reviewed at all yet.
send_0a0a(Inventory, Client=#egs_net{gid=DestGID}) ->
{ok, << _:68608/bits, Rest/bits >>} = file:read_file("p/packet0a0a.bin"),
NbItems = length(Inventory),
ItemVariables = build_0a0a_item_variables(Inventory, 1, []),
ItemConstants = build_0a0a_item_constants(Inventory, []),
packet_send(Client, << 16#0a0a0300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
NbItems:8, 0:8, 6:8, 0:72, 0:192, 0:2304, ItemVariables/binary, ItemConstants/binary, 0:13824, Rest/binary >>).
build_0a0a_item_variables([], _N, Acc) ->
Bin = iolist_to_binary(lists:reverse(Acc)),
Padding = 17280 - 8 * byte_size(Bin),
<< Bin/binary, 0:Padding >>;
build_0a0a_item_variables([{ItemID, Variables}|Tail], N, Acc) ->
build_0a0a_item_variables(Tail, N + 1, [build_item_variables(ItemID, N, Variables)|Acc]).
build_0a0a_item_constants([], Acc) ->
Bin = iolist_to_binary(lists:reverse(Acc)),
Padding = 34560 - 8 * byte_size(Bin),
<< Bin/binary, 0:Padding >>;
build_0a0a_item_constants([{ItemID, _Variables}|Tail], Acc) ->
#psu_item{name=Name, rarity=Rarity, sell_price=SellPrice, data=Data} = egs_items_db:read(ItemID),
UCS2Name = << << X:8, 0:8 >> || X <- Name >>,
NamePadding = 8 * (46 - byte_size(UCS2Name)),
<< Category:8, _:24 >> = << ItemID:32 >>,
DataBin = build_item_constants(Data),
RarityInt = Rarity - 1,
Bin = << UCS2Name/binary, 0:NamePadding, RarityInt:8, Category:8, SellPrice:32/little, DataBin/binary >>,
build_0a0a_item_constants(Tail, [Bin|Acc]).
%% @doc Send an item's description.
send_0a11(ItemID, ItemDesc, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
Length = 1 + byte_size(ItemDesc) div 2,
packet_send(Client, << 16#0a110300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
ItemID:32, Length:32/little, ItemDesc/binary, 0:16 >>).
%% @doc Quest init.
%% @todo When first entering a zone it seems LID should be set to ffff apparently.
send_0c00(CharUser, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
#users{area={QuestID, _ZoneID, _MapID}} = CharUser,
packet_send(Client, << 16#0c000300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64, QuestID:32/little,
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32,
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32,
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32,
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>).
%% @todo Figure out last 4 bytes!
%% @todo This packet hasn't been reviewed at all yet.
send_0c02(Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#0c020300:32, 0:160, 16#00011300:32, DestGID:32/little, 0:64, 0:32 >>).
%% @doc Send the huge pack of quest files available in the counter.
send_0c06(Pack, Client) ->
packet_send(Client, << 16#0c060300:32, 0:288, 1:32/little, Pack/binary >>).
%% @doc Reply that the player is allowed to use the lobby transport. Always allow.
send_0c08(Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#0c080300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:96 >>).
%% @doc Send the trial start notification.
%% @todo This packet hasn't been reviewed at all yet.
send_0c09(Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#0c090300:32, 0:160, 16#00011300:32, DestGID:32/little, 0:64, 0:64 >>).
%% @doc Send the counter's mission options (0 = invisible, 2 = disabled, 3 = available).
send_0c10(Options, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
Size = byte_size(Options),
packet_send(Client, << 16#0c100300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64, 1, 0, Size:16/little, Options/binary >>).
%% @todo Add a character (NPC or real) to the party members on the right of the screen.
%% @todo NPCid is 65535 for normal characters.
%% @todo Apparently the 4 location ids are set to 0 when inviting an NPC in the lobby - NPCs have their location set to 0 when in lobby; also odd value before PartyPos related to missions
%% @todo Not sure about LID. But seems like it.
%% @todo This packet hasn't been reviewed at all yet.
send_1004(Type, User, PartyPos, Client=#egs_net{gid=DestGID}) ->
[TypeID, LID, SomeFlag] = case Type of
npc_mission -> [16#00001d00, PartyPos, 2];
npc_invite -> [0, 16#ffffffff, 3];
_ -> 1 %% seems to be for players
end,
#users{gid=GID, npcid=NPCid, name=Name, level=Level, area={QuestID, ZoneID, MapID}, entryid=EntryID} = User,
packet_send(Client, << 16#10040300:32, 16#ffff0000:32, 0:128, 16#00011300:32, DestGID:32/little, 0:64,
TypeID:32, GID:32/little, 0:64, Name/binary,
Level:16/little, 16#ffff:16,
SomeFlag, 1, PartyPos:8, 1,
NPCid:16/little, 0:16,
%% Odd unknown values. PA related? No idea. Values on invite, 0 in-mission.
%~ 16#00001f08:32, 0:32, 16#07000000:32,
%~ 16#04e41f08:32, 0:32, 16#01000000:32,
%~ 16#64e41f08:32, 0:32, 16#02000000:32,
%~ 16#64e41f08:32, 0:32, 16#03000000:32,
%~ 16#64e41f08:32, 0:32, 16#12000000:32,
%~ 16#24e41f08:32,
0:512,
QuestID:32/little, ZoneID:32/little, MapID:32/little, EntryID:32/little,
LID:32/little,
0:64,
16#01000000:32, 16#01000000:32, %% @todo first is current hp, second is max hp
0:608 >>).
%% @doc Send the client's own player's party information, on the bottom left of the screen.
%% @todo Location and the 20 bytes following sometimes have values, not sure why; when joining a party maybe?
send_1005(User, Client=#egs_net{gid=DestGID}) ->
#users{name=Name, level=Level, currenthp=CurrentHP, maxhp=MaxHP} = User,
Location = << 0:512 >>,
packet_send(Client, << 16#10050300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
16#00000100:32, 0:32, 16#ffffffff:32, 0:32, 16#00011200:32, DestGID:32/little, 0:64,
Name/binary, Level:8, 0:16, 1:8, 16#01010000:32, 0:32, Location/binary,
16#ffffffff:32, 0:96, 16#ffffffff:32, 0:64, CurrentHP:32/little, MaxHP:32/little, 0:640,
16#0100ffff:32, 16#0000ff00:32, 16#ffff0000:32, 0:640, 16#ffffffff:32, 0:768,
16#0100ffff:32, 16#0000ff00:32, 16#ffff0000:32, 0:640, 16#ffffffff:32, 0:768,
16#0100ffff:32, 16#0000ff00:32, 16#ffff0000:32, 0:640, 16#ffffffff:32, 0:768,
16#0100ffff:32, 16#0000ff00:32, 16#ffff0000:32, 0:640, 16#ffffffff:32, 0:768,
16#0100ffff:32, 16#0000ff00:32, 16#ffff0000:32, 0:640, 16#ffffffff:32, 0:448,
16#ffffffff:32, 0:32, 16#ff020000:32, 16#ffff0000:32, 16#ffff0000:32, 16#ffff0000:32,
16#ffff0000:32, 16#ffff0000:32, 16#ffff0000:32, 0:3680 >>).
%% @doc Party-related events.
send_1006(EventID, Client) ->
send_1006(EventID, 0, Client).
send_1006(EventID, PartyPos, Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#10060300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, EventID:8, PartyPos:8, 0:16 >>).
%% @doc Send the player's current location.
%% @todo Handle PartyPos.
%% @todo Receive the AreaName as UCS2 directly to allow for color codes and the like.
%% @todo Handle TargetLID probably (right after the padding).
%% @todo Do counters even have a name?
send_100e(CounterID, AreaName, Client=#egs_net{gid=DestGID}) ->
PartyPos = 0,
UCS2Name = << << X:8, 0:8 >> || X <- AreaName >>,
Padding = 8 * (64 - byte_size(UCS2Name)),
CounterType = if CounterID =:= 16#ffffffff -> 2; true -> 1 end,
packet_send(Client, << 16#100e0300:32, 16#ffffffbf:32, 0:128, 16#00011300:32, DestGID:32, 0:64,
1, PartyPos, 0:48, 16#ffffff7f:32, UCS2Name/binary, 0:Padding, 0:32, CounterID:32/little, CounterType:32/little >>).
send_100e({QuestID, ZoneID, MapID}, EntryID, AreaName, Client=#egs_net{gid=DestGID}) ->
PartyPos = 0,
UCS2Name = << << X:8, 0:8 >> || X <- AreaName >>,
Padding = 8 * (64 - byte_size(UCS2Name)),
packet_send(Client, << 16#100e0300:32, 16#ffffffbf:32, 0:128, 16#00011300:32, DestGID:32, 0:64,
1, PartyPos, ZoneID:16/little, MapID:16/little, EntryID:16/little, QuestID:32/little,
UCS2Name/binary, 0:Padding, 0:32, 16#ffffffff:32, 0:32 >>).
%% @todo No idea. Also the 2 PartyPos in the built packet more often than not match, but sometimes don't? That's probably because one is PartyPos and the other is LID or something.
%% @todo This packet hasn't been reviewed at all yet.
send_100f(NPCid, PartyPos, Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#100f0300:32, 0:160, 16#00011300:32, DestGID:32/little, 0:64, NPCid:16/little, 1, PartyPos:8, PartyPos:32/little >>).
%% @doc Send the mission's quest file when starting a new mission.
%% @todo Handle correctly. 0:32 is actually a missing value. Value before that is unknown too.
%% @todo This packet hasn't been reviewed at all yet.
send_1015(QuestID, Client=#egs_net{gid=DestGID}) ->
QuestData = egs_quests_db:quest_nbl(QuestID),
Size = byte_size(QuestData),
packet_send(Client, << 16#10150300:32, 0:160, 16#00011300:32, DestGID:32/little, 0:64, QuestID:32/little, 16#01010000:32, 0:32, Size:32/little, QuestData/binary >>).
%% @todo No idea.
%% @todo This packet hasn't been reviewed at all yet.
send_1016(PartyPos, Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#10160300:32, 16#ffff0000:32, 0:128, 16#00011300:32, DestGID:32/little, 0:64, PartyPos:32/little >>).
%% @todo No idea.
%% @todo This packet hasn't been reviewed at all yet.
send_101a(NPCid, PartyPos, Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#101a0300:32, 0:160, 16#00011300:32, DestGID:32/little, 0:64, NPCid:16/little, PartyPos:16/little, 16#ffffffff:32 >>).
%% @doc Mission start related.
send_1020(Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#10200300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64 >>).
%% @doc Update HP in the party members information on the left.
%% @todo Handle PartyPos. Probably only pass HP later.
send_1022(#users{currenthp=HP}, Client=#egs_net{gid=DestGID}) ->
PartyPos = 0,
packet_send(Client, << 16#10220300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, HP:32/little, PartyPos:32/little >>).
%% @todo Boss related command.
%% @todo This packet hasn't been reviewed at all yet.
send_110e(Data, Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#110e0300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, Data/binary, 0:32, 5:16/little, 12:16/little, 0:32, 260:32/little >>).
%% @todo Boss related command.
%% @todo This packet hasn't been reviewed at all yet.
send_1113(Data, Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#11130300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, Data/binary >>).
%% @todo Figure out what this packet does. Sane values for counter and missions for now.
%% @todo This packet hasn't been reviewed at all yet.
send_1202(Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#12020300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, 0:32, 16#10000000:32, 0:64, 16#14000000:32, 0:32 >>).
%% @todo Always the same value, no idea what it's for.
send_1204(Client=#egs_net{gid=DestGID, lid=DestLID}) ->
packet_send(Client, << 16#12040300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:96, 16#20000000:32, 0:256 >>).
%% @doc Object events response?
%% @todo Not sure what Value does exactly. It's either 0 or 1.
%% @todo This packet hasn't been reviewed at all yet.
send_1205(EventID, BlockID, Value, Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#12050300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, EventID, BlockID, 0:16, Value, 0:24 >>).
%% @todo Figure out what this packet does. Sane values for counter and missions for now.
%% @todo This packet hasn't been reviewed at all yet.
send_1206(Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#12060300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, 0:32, 16#80020000:32, 0:5120 >>).
%% @todo Figure out what this packet does. Sane values for counter and missions for now.
%% @todo This packet hasn't been reviewed at all yet.
send_1207(Client=#egs_net{gid=DestGID}) ->
Chunk = << 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 0:224, 16#0000ffff:32, 16#ff000000:32, 16#64000a00:32 >>,
packet_send(Client, << 16#12070300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
Chunk/binary, Chunk/binary, Chunk/binary, Chunk/binary, Chunk/binary, Chunk/binary >>).
%% @todo Object interaction? Figure out. C probably the interaction type.
%% @todo Apparently A would be TargetID/ffffffff, B would be the player LID, C would be the object type? D still completely unknown.
%% @todo This packet hasn't been reviewed at all yet.
send_1211(A, B, C, D, Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#12110300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, A:32/little, B:32/little, C:32/little, D:32/little >>).
%% @doc Make the client load the quest previously sent.
%% @todo This packet hasn't been reviewed at all yet.
send_1212(Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#12120300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, 0:19200 >>).
%% @todo Not sure. Related to keys.
%% @todo This packet hasn't been reviewed at all yet.
send_1213(A, B, Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#12130300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, A:32/little, B:32/little >>).
%% @todo Related to boss gates.
%% @todo This packet hasn't been reviewed at all yet.
send_1215(A, B, Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#12150300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, A:32/little, 0:16, B:16/little >>).
%% @todo Not sure yet. Value is probably a TargetID. Used in Airboard Rally. Replying with the same value starts the race.
%% @todo This packet hasn't been reviewed at all yet.
send_1216(Value, Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#12160300:32, 0:32, 16#00011300:32, DestGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64, Value:32/little >>).
%% @todo Send an empty partner card list.
%% @todo This packet hasn't been reviewed at all yet.
send_1501(Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#15010300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:96 >>).
%% @todo Send an empty blacklist.
%% @todo This packet hasn't been reviewed at all yet.
send_1512(Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#15120300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:46144 >>).
%% @todo NPC related packet, sent when there's an NPC in the area.
%% @todo This packet hasn't been reviewed at all yet.
send_1601(PartyPos, Client=#egs_net{gid=DestGID}) ->
{ok, << _:32, Bin/bits >>} = file:read_file("p/packet1601.bin"),
packet_send(Client, << 16#16010300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, PartyPos:32/little, Bin/binary >>).
%% @doc Send the player's NPC and PM information.
%% @todo The value 4 is the card priority. Find what 3 is. When sending, the first 0 is an unknown value.
%% @todo This packet hasn't been reviewed at all yet.
send_1602(Client=#egs_net{gid=DestGID}) ->
NPCList = egs_npc_db:all(),
NbNPC = length(NPCList),
Bin = iolist_to_binary([<< NPCid:8, 0, 4, 0, 3, 0:24 >> || {NPCid, _Data} <- NPCList]),
MiddlePaddingSize = 8 * (344 - byte_size(Bin)),
PMName = "My PM",
UCS2PMName = << << X:8, 0:8 >> || X <- PMName >>,
EndPaddingSize = 8 * (64 - byte_size(UCS2PMName)),
packet_send(Client, << 16#16020300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:96,
Bin/binary, 0:MiddlePaddingSize, NbNPC, 0:24, UCS2PMName/binary, 0:EndPaddingSize, 0:32 >>).
%% @doc Send the list of parties to join.
%% @todo Handle lists of parties.
%% @todo Probably has to handle a LID here, although it should always be 0.
send_1701(Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#17010300:32, 0:160, 16#00011300:32, DestGID:32/little, 0:96 >>).
%% @doc Party information.
%% @todo Handle existing parties.
%% @todo This packet hasn't been reviewed at all yet.
send_1706(CharName, Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#17060300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
16#00000300:32, 16#d5c0faff:32, 0:64, CharName/binary,
16#78000000:32, 16#01010000:32, 0:1536, 16#0100c800:32, 16#0601010a:32, 16#ffffffff:32, 0:32 >>).
%% @doc Party settings. Item distribution is random for now.
%% @todo Handle correctly.
%% @todo This packet hasn't been reviewed at all yet.
send_170a(Client=#egs_net{gid=DestGID}) ->
packet_send(Client, << 16#170a0300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, 16#01010c08:32 >>).
%% @todo Find what the heck this packet is.
%% @todo This packet hasn't been reviewed at all yet.
send_170c(Client=#egs_net{gid=DestGID}) ->
{ok, File} = file:read_file("p/packet170c.bin"),
packet_send(Client, << 16#170c0300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, File/binary >>).
%% @doc Send the background to use for the counter.
send_1711(Bg, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
packet_send(Client, << 16#17110300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64, Bg:8, 0:24 >>).
%% @doc NPC shop request reply.
send_1a02(A, B, C, D, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
packet_send(Client, << 16#1a020300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:96,
A:16/little, B:16/little, C:16/little, D:16/little >>).
%% @doc Lumilass available hairstyles/headtypes handler.
send_1a03(User, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
{ok, Conf} = file:consult("priv/lumilass.conf"),
NbHeadtypes = proplists:get_value({headtypes, User#users.gender, User#users.race}, Conf, 0),
HairstylesList = proplists:get_value({hairstyles, User#users.gender}, Conf),
NbHairstyles = length(HairstylesList),
HairstylesBin = iolist_to_binary([ << N:32 >> || N <- HairstylesList]),
packet_send(Client, << 16#1a030300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:96,
NbHairstyles:32/little, NbHeadtypes:32/little, 0:416, HairstylesBin/binary, 0:32 >>).
%% @doc PP cube handler.
%% @todo The 4 bytes before the file may vary. Everything past that is the same. Figure things out.
%% @todo This packet hasn't been reviewed at all yet.
send_1a04(Client=#egs_net{gid=DestGID}) ->
{ok, File} = file:read_file("p/ppcube.bin"),
packet_send(Client, << 16#1a040300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, 0:32, File/binary >>).
%% @doc Available types handler. Enable all 16 types.
send_1a07(Client=#egs_net{gid=DestGID, lid=DestLID}) ->
packet_send(Client, << 16#1a070300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:160,
16#01010101:32, 16#01010101:32, 16#01010101:32, 16#01010101:32 >>).
%% Common binary building functions.
%% @todo Handle class levels.
build_char_level(#users{type=Type, level=Level, exp=EXP, blastbar=BlastBar, luck=Luck, money=Money}) ->
ClassesBin = case Type of
npc ->
<< 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32,
16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32,
16#4e4f4630:32, 16#08000000:32, 0:32, 0:32, 16#4e454e44:32 >>;
_ ->
<< 0:160,
16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32,
16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32 >>
end,
PlayTime = 0, %% @todo
<< Level:32/little, BlastBar:16/little, Luck:8, 0:40, EXP:32/little, 0:32, Money:32/little, PlayTime:32/little, ClassesBin/binary >>.
build_item_constants(#psu_clothing_item{appearance=Appearance, manufacturer=Manufacturer, type=Type, overlap=Overlap, gender=Gender, colors=Colors}) ->
GenderInt = case Gender of male -> 16#1b; female -> 16#2b end,
<< Appearance:16, Type:4, Manufacturer:4, Overlap:8, GenderInt:8, Colors/binary, 0:40 >>;
build_item_constants(#psu_consumable_item{max_quantity=MaxQuantity, pt_diff=PointsDiff,
status_effect=StatusEffect, target=Target, use_condition=UseCondition, item_effect=ItemEffect}) ->
<< 0:8, MaxQuantity:8, Target:8, UseCondition:8, PointsDiff:16/little, StatusEffect:8, ItemEffect:8, 0:96 >>;
build_item_constants(#psu_parts_item{appearance=Appearance, manufacturer=Manufacturer, type=Type, overlap=Overlap, gender=Gender}) ->
GenderInt = case Gender of male -> 16#14; female -> 16#24 end,
<< Appearance:16, Type:4, Manufacturer:4, Overlap:8, GenderInt:8, 0:120 >>;
%% @todo Handle rank properly.
build_item_constants(#psu_striking_weapon_item{pp=PP, atp=ATP, ata=ATA, atp_req=Req, shop_element=#psu_element{type=EleType, percent=ElePercent},
hand=Hand, max_upgrades=MaxUpgrades, attack_label=AttackLabel}) ->
Rank = 4,
HandInt = case Hand of
both -> 0;
_ -> error
end,
<< PP:16/little, ATP:16/little, ATA:16/little, Req:16/little, 16#ffffff:24,
EleType:8, ElePercent:8, HandInt:8, 0:8, Rank:8, 0:8, MaxUpgrades:8, AttackLabel:8, 0:8 >>;
build_item_constants(#psu_trap_item{max_quantity=MaxQuantity}) ->
<< 2:32/little, 16#ffffff:24, MaxQuantity:8, 0:96 >>;
build_item_constants(#psu_special_item{}) ->
<< 0:160 >>.
build_item_variables(ItemID, ItemUUID, #psu_clothing_item_variables{color=ColorNb}) ->
#psu_item{rarity=Rarity, data=#psu_clothing_item{colors=ColorsBin}} = egs_items_db:read(ItemID),
RarityInt = Rarity - 1,
ColorInt = if ColorNb < 5 -> ColorNb; true -> 16#10 + ColorNb - 5 end,
Bits = ColorNb * 8,
<< _Before:Bits, ColorA:4, ColorB:4, _After/bits >> = ColorsBin,
<< 0:32, ItemUUID:32/little, ItemID:32, 0:88, RarityInt:8, ColorA:8, ColorB:8, ColorInt:8, 0:72 >>;
build_item_variables(ItemID, ItemUUID, #psu_consumable_item_variables{quantity=Quantity}) ->
#psu_item{rarity=Rarity, data=#psu_consumable_item{max_quantity=MaxQuantity, action=Action}} = egs_items_db:read(ItemID),
RarityInt = Rarity - 1,
<< 0:32, ItemUUID:32/little, ItemID:32, Quantity:32/little, MaxQuantity:32/little, 0:24, RarityInt:8, Action:8, 0:88 >>;
build_item_variables(ItemID, ItemUUID, #psu_parts_item_variables{}) ->
#psu_item{rarity=Rarity} = egs_items_db:read(ItemID),
RarityInt = Rarity - 1,
<< 0:32, ItemUUID:32/little, ItemID:32, 0:88, RarityInt:8, 0:96 >>;
%% @todo Handle rank, rarity and hands properly.
build_item_variables(ItemID, ItemUUID, Variables) when element(1, Variables) =:= psu_striking_weapon_item_variables ->
#psu_striking_weapon_item_variables{is_active=IsActive, slot=Slot, current_pp=CurrentPP, max_pp=MaxPP,
element=#psu_element{type=EleType, percent=ElePercent}, pa=#psu_pa{type=PAType, level=PALevel}} = Variables,
Rank = 4,
Grind = 0,
Rarity = 14, %% Rarity - 1
Hand = both,
<< _:8, WeaponType:8, _:16 >> = << ItemID:32 >>,
HandBin = case Hand of
both -> << 16#0000:16 >>;
_ -> error
end,
<< IsActive:8, Slot:8, 0:16, ItemUUID:32/little, ItemID:32, 0:32, CurrentPP:16/little, MaxPP:16/little, 0:16, %% @todo What's this 0:16?
Grind:4, Rank:4, Rarity:8, EleType:8, ElePercent:8, HandBin/binary, WeaponType:8, PAType:8, PALevel:8, 0:40 >>;
build_item_variables(ItemID, ItemUUID, #psu_special_item_variables{}) ->
Action = case ItemID of
16#11010000 -> << 16#12020100:32 >>;
16#11020000 -> << 16#15000000:32 >>;
16#11020100 -> << 0:32 >>;
16#11020200 -> << 0:32 >>
end,
<< 0:32, ItemUUID:32/little, ItemID:32, 0:24, 16#80:8, 0:56, 16#80:8, 0:32, Action/binary, 0:32 >>;
build_item_variables(ItemID, ItemUUID, #psu_trap_item_variables{quantity=Quantity}) ->
#psu_item{rarity=Rarity, data=#psu_trap_item{max_quantity=MaxQuantity}} = egs_items_db:read(ItemID),
RarityInt = Rarity - 1,
<< 0:32, ItemUUID:32/little, ItemID:32, Quantity:32/little, MaxQuantity:32/little, 0:24, RarityInt:8, 0:96 >>.
%% Utility functions.
%% @doc Prepare a packet. Return the real size and padding at the end.
packet_prepare(Packet) ->
Size = 4 + byte_size(Packet),
case Size rem 4 of
0 -> {ok, Size, <<>>};
2 -> {ok, Size + 2, << 0:16 >>};
_ -> {error, badarg}
end.
%% @doc Send a packet. The packet argument must not contain the size field.
packet_send(Client, Packet) ->
{ok, Size, Padding} = packet_prepare(Packet),
packet_send(Client, << Size:32/little, Packet/binary, Padding/binary >>, Size).
%% Send a normal command.
packet_send(#egs_net{socket=Socket, transport=Transport}, Packet, Size)
when Size =< 16#4000 ->
Transport:send(Socket, Packet);
%% Send a fragmented command when size is too big.
packet_send(Client, Packet, Size) ->
packet_fragment_send(Client, Packet, Size, 0).
%% Send the last chunk of a fragmented command.
packet_fragment_send(#egs_net{socket=Socket, transport=Transport}, Packet,
Size, Current) when Size - Current =< 16#4000 ->
FragmentSize = 16#10 + byte_size(Packet),
Fragment = << FragmentSize:32/little, 16#0b030000:32, Size:32/little, Current:32/little, Packet/binary >>,
Transport:send(Socket, Fragment);
%% Send another chunk of a fragmented command.
packet_fragment_send(Client=#egs_net{socket=Socket, transport=Transport}, Packet,
Size, Current) ->
<< Chunk:131072/bits, Rest/bits >> = Packet,
Fragment = << 16#10400000:32, 16#0b030000:32, Size:32/little, Current:32/little, Chunk/binary >>,
Transport:send(Socket, Fragment),
packet_fragment_send(Client, Rest, Size, Current + 16#4000).

View File

@ -0,0 +1,71 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin.
%% @doc Quest handler.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_quests).
-behaviour(gen_server).
-export([start_link/2, stop/1, zone_pid/2]). %% API.
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
-record(state, {zones}).
%% API.
%% @spec start_link(UniID, QuestID) -> {ok,Pid::pid()}
start_link(UniID, QuestID) ->
gen_server:start_link(?MODULE, [UniID, QuestID], []).
%% @spec stop(Pid) -> stopped
stop(Pid) ->
gen_server:call(Pid, stop).
zone_pid(Pid, ZoneID) ->
gen_server:call(Pid, {zone_pid, ZoneID}).
%% gen_server.
init([UniID, QuestID]) ->
Zones = egs_quests_db:quest_zones(QuestID),
ZonesPids = lists:map(fun({ZoneID, ZoneData}) ->
{ok, Pid} = egs_zones_sup:start_zone(UniID, QuestID, ZoneID, ZoneData),
{ZoneID, Pid}
end, Zones),
{ok, #state{zones=ZonesPids}}.
handle_call({zone_pid, ZoneID}, _From, State) ->
{_, Pid} = lists:keyfind(ZoneID, 1, State#state.zones),
{reply, Pid, State};
handle_call(stop, _From, State) ->
{stop, normal, stopped, State};
handle_call(_Request, _From, State) ->
{reply, ignored, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.

View File

@ -0,0 +1,190 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS quests database and cache manager.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_quests_db).
-behavior(gen_server).
-export([start_link/0, stop/0, quest_nbl/1, zone_nbl/2, area_type/2, quest_zones/1, set/3, reload/0]). %% API.
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
%% Use the module name for the server's name.
-define(SERVER, ?MODULE).
-record(state, {quests=[], quests_bin=[], zones_bin=[], sets=[]}).
%% API.
%% @spec start_link() -> {ok,Pid::pid()}
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%% @spec stop() -> stopped
stop() ->
gen_server:call(?SERVER, stop).
%% @spec quest_nbl(QuestID) -> binary()
quest_nbl(QuestID) ->
gen_server:call(?SERVER, {quest_nbl, QuestID}).
%% @spec zone_nbl(QuestID, ZoneID) -> binary()
zone_nbl(QuestID, ZoneID) ->
gen_server:call(?SERVER, {zone_nbl, QuestID, ZoneID}).
area_type(QuestID, ZoneID) ->
gen_server:call(?SERVER, {area_type, QuestID, ZoneID}).
quest_zones(QuestID) ->
gen_server:call(?SERVER, {quest_zones, QuestID}).
set(QuestID, ZoneID, SetID) ->
gen_server:call(?SERVER, {set, QuestID, ZoneID, SetID}).
%% @spec reload() -> ok
reload() ->
gen_server:cast(?SERVER, reload).
%% gen_server.
init([]) ->
{ok, #state{}}.
%% @doc Return a quest information either from the cache or from the configuration file,
%% in which case it gets added to the cache for subsequent attempts.
handle_call({quest_nbl, QuestID}, _From, State=#state{quests=Cache, quests_bin=BinCache}) ->
case proplists:get_value(QuestID, BinCache) of
undefined ->
Dir = io_lib:format("priv/quests/~b/", [QuestID]),
ConfFilename = Dir ++ "quest.conf",
{ok, Settings} = file:consult(ConfFilename),
{QuestXnrData, QuestXnrPtrs} = egs_files:load_quest_xnr(Settings),
UnitTitleBinFiles = load_unit_title_bin_files(Dir, Settings),
Files = [{data, "quest.xnr", QuestXnrData, QuestXnrPtrs}],
Files2 = Files ++ case UnitTitleBinFiles of
ignore -> [];
_Any ->
TablePos = egs_files:nbl_padded_size(byte_size(QuestXnrData)),
TextSize = lists:sum([egs_files:nbl_padded_size(byte_size(D)) || {data, _F, D, _P} <- UnitTitleBinFiles]),
TablePos2 = TablePos + TextSize,
{UnitTitleTableRelData, UnitTitleTableRelPtrs} = egs_files:load_unit_title_table_rel(ConfFilename, TablePos2),
UnitTitleBinFiles ++ [{data, "unit_title_table.rel", UnitTitleTableRelData, UnitTitleTableRelPtrs}]
end,
QuestNbl = egs_files:nbl_pack([{files, Files2}]),
{reply, QuestNbl, State#state{quests=[{QuestID, Settings}|Cache], quests_bin=[{QuestID, QuestNbl}|BinCache]}};
QuestNbl ->
{reply, QuestNbl, State}
end;
%% @doc Return a zone information either from the cache or from the configuration files.
%% @todo FilePos, text.bin, other sets, enemies.
handle_call({zone_nbl, QuestID, ZoneID}, _From, State=#state{quests=QuestsCache, zones_bin=BinCache}) ->
case proplists:get_value({QuestID, ZoneID}, BinCache) of
undefined ->
Dir = io_lib:format("priv/quests/~b/", [QuestID]),
ZoneDir = Dir ++ io_lib:format("zone-~b/", [ZoneID]),
QuestSettings = proplists:get_value(QuestID, QuestsCache),
Zones = proplists:get_value(zones, QuestSettings),
Zone = proplists:get_value(ZoneID, Zones),
AreaID = proplists:get_value(areaid, Zone),
Maps = proplists:get_value(maps, Zone),
FilePos = 0, %% @todo
{Set0, SetPtrs} = egs_files:load_set_rel(ZoneDir ++ io_lib:format("set_r~b.conf", [0]), AreaID, Maps, FilePos),
ScriptBin = egs_files:load_script_bin(ZoneDir ++ "script.es"),
ScriptBinSize = byte_size(ScriptBin),
ScriptBin2 = prs:compress(ScriptBin),
ScriptBinSize2 = byte_size(ScriptBin2),
ScriptBin3 = << ScriptBinSize:32/little, ScriptBinSize2:32/little, 0:32, 1:32/little, 0:96, ScriptBin2/binary >>,
TextBin = egs_files:load_text_bin(ZoneDir ++ "text.bin.en_US.txt"),
ZoneNbl = egs_files:nbl_pack([{files, [
{data, "set_r0.rel", Set0, SetPtrs},
{data, "script.bin", ScriptBin3, []},
{data, "text.bin", TextBin, []}
]}]),
{reply, ZoneNbl, State#state{zones_bin=[{{QuestID, ZoneID}, ZoneNbl}|BinCache]}};
ZoneNbl ->
{reply, ZoneNbl, State}
end;
handle_call({area_type, QuestID, ZoneID}, _From, State=#state{quests=QuestsCache}) ->
{_, Quest} = lists:keyfind(QuestID, 1, QuestsCache),
{_, Zones} = lists:keyfind(zones, 1, Quest),
{_, Zone} = lists:keyfind(ZoneID, 1, Zones),
{_, AreaID} = lists:keyfind(areaid, 1, Zone),
AreaType = case AreaID of
0 -> lobby;
2 -> lobby;
3 -> lobby;
4 -> lobby;
5 -> lobby;
22 -> myroom;
_Any -> mission
end,
{reply, AreaType, State};
handle_call({quest_zones, QuestID}, _From, State=#state{quests=QuestsCache}) ->
{_, Quest} = lists:keyfind(QuestID, 1, QuestsCache),
{_, Zones} = lists:keyfind(zones, 1, Quest),
{reply, Zones, State};
%% @todo The set file is loaded both here and in zone_nbl. Thinking about it zone_nbl should call this function.
%% @todo Same for quest_nbl loading quest files and binaries, there should be a function for the file itself called only when needed.
handle_call({set, QuestID, ZoneID, SetID}, _From, State=#state{sets=SetsCache}) ->
case proplists:get_value({QuestID, ZoneID, SetID}, SetsCache) of
undefined ->
SetFilename = io_lib:format("priv/quests/~b/zone-~b/set_r~b.conf", [QuestID, ZoneID, SetID]),
{ok, Set} = file:consult(SetFilename),
{reply, Set, State#state{sets=[{{QuestID, ZoneID, SetID}, Set}|SetsCache]}};
CachedSet ->
{reply, CachedSet, State}
end;
handle_call(stop, _From, State) ->
{stop, normal, stopped, State};
handle_call(_Request, _From, State) ->
{reply, ignored, State}.
handle_cast(reload, _State) ->
{noreply, #state{}};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% Internal.
load_unit_title_bin_files(Dir, Settings) ->
case proplists:get_value(notitles, Settings) of
true -> ignore;
_Any ->
Zones = proplists:get_value(zones, Settings),
[load_unit_title_bin(Dir, Zone) || Zone <- Zones]
end.
load_unit_title_bin(Dir, {ZoneID, _ZoneParams}) ->
Filename = io_lib:format("unit_title_~2.10.0b.bin", [ZoneID]),
TxtFilename = io_lib:format("~s~s.en_US.txt", [Dir, Filename]),
{data, Filename, egs_files:load_text_bin(TxtFilename), []}.

View File

@ -0,0 +1,46 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin.
%% @doc Supervisor for the egs_quests gen_server.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_quests_sup).
-behaviour(supervisor).
-export([start_link/0, start_quest/2]). %% API.
-export([init/1]). %% supervisor.
-define(SUPERVISOR, ?MODULE).
%% API.
-spec start_link() -> {ok, pid()}.
start_link() ->
supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []).
-spec start_quest(egs:uniid(), egs:questid()) -> {ok, pid()}.
start_quest(UniID, QuestID) ->
supervisor:start_child(?SUPERVISOR, [UniID, QuestID]).
%% supervisor.
-spec init([]) -> {ok, {{simple_one_for_one, 0, 1}, [{egs_quests,
{egs_quests, start_link, []}, temporary, brutal_kill,
worker, [egs_quests]}]}}.
init([]) ->
{ok, {{simple_one_for_one, 0, 1}, [{egs_quests,
{egs_quests, start_link, []}, temporary, brutal_kill,
worker, [egs_quests]}]}}.

View File

@ -0,0 +1,221 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS script compiler.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_script_compiler).
-export([compile/1]).
%% @doc Compile a script parsed using egs_script_lexer and egs_script_parser.
compile(ParseTree) ->
{RootBin, Funcs} = root(ParseTree),
FuncsPos = byte_size(RootBin),
FuncsBin = funcs(Funcs),
FuncsSize = byte_size(FuncsBin),
<< $T, $S, $B, $2, FuncsPos:32/little, FuncsSize:32/little, RootBin/binary, FuncsBin/binary >>.
root(Routines) ->
root(Routines, 0, [], []).
root(nil, _Pos, Funcs, Acc) ->
{iolist_to_binary(lists:reverse(Acc)), Funcs};
root({Routine, Next}, Pos, Funcs, Acc) ->
{Bin, Funcs2} = routine(Routine, Funcs),
Pos2 = case Next of nil -> 0; _ -> Pos + byte_size(Bin) + 4 end,
root(Next, Pos2, Funcs2, [<< Pos2:32/little, Bin/binary >>|Acc]).
routine({event_def, {Name, _Type}, Instrs}, Funcs) ->
NameBin = routine_name_to_binary([$E, $V, $E, $N, $T, $.|Name]),
{BodyBin, Funcs2} = routine_body_to_binary(Instrs, Funcs),
{<< NameBin/binary, BodyBin/binary >>, Funcs2};
routine({function_def, {Name, _Type}, Instrs}, Funcs) ->
NameBin = routine_name_to_binary(Name),
{BodyBin, Funcs2} = routine_body_to_binary(Instrs, Funcs),
{<< NameBin/binary, BodyBin/binary >>, [Name|Funcs2]};
routine({num_var, {Name, _Type}}, Funcs) ->
NameBin = routine_name_to_binary(Name),
{<< NameBin/binary, 0:32/little, 16#3c:32/little, 0:32 >>, [Name|Funcs]};
routine({str_var, {Name, _Type}, Length}, Funcs) ->
NameBin = routine_name_to_binary(Name),
{<< NameBin/binary, 0:32/little, 16#49:32/little, Length:32/little >>, [Name|Funcs]}.
routine_body_to_binary(Instrs, Funcs) ->
{_Inc, InstrsBin, VarsList, Funcs2} = instructions(Instrs, Funcs),
InstrsSize = byte_size(InstrsBin) + 4,
NbVars = length(VarsList),
VarsBin = iolist_to_binary([<< VarN:32/little, VarPos:32/little >> || {VarN, VarPos} <- VarsList]),
{<< InstrsSize:32/little, 16#4c:32/little, NbVars:32/little, InstrsBin/binary, 0:32, VarsBin/binary >>, Funcs2}.
routine_name_to_binary(Name) ->
NameBin = list_to_binary(Name),
Padding = 8 * (32 - byte_size(NameBin)),
<< NameBin/binary, 0:Padding >>.
instructions(Instrs, Funcs) ->
instructions(Instrs, Funcs, 0, [], []).
instructions(Instrs, Funcs, Pos) ->
instructions(Instrs, Funcs, Pos, [], []).
instructions(nil, Funcs, Pos, Acc, VarsAcc) ->
{Pos, iolist_to_binary(lists:reverse(Acc)), lists:reverse(lists:flatten(VarsAcc)), Funcs};
instructions({Instr, Next}, Funcs, Pos, Acc, VarsAcc) ->
{Inc, Bin, Vars, Funcs2} = instruction(Instr, Funcs, Pos),
instructions(Next, Funcs2, Pos + Inc, [Bin|Acc], [Vars|VarsAcc]).
%% High level constructs.
instruction({'case', Tests}, Funcs, Pos) ->
{Pos2, Bin, Vars, Funcs2} = case_tests(Tests, Funcs, Pos),
{Pos2 - Pos, Bin, Vars, Funcs2};
%% Functions and syscalls.
instruction({function, {Name, Type}}, Funcs, Pos) ->
{N2, Funcs2} = case find_func(Name, Funcs) of
{{error, undefined}, N} -> {N, [Name|Funcs]};
{ok, N} -> {N, Funcs}
end,
SyncBin = case Type of sync -> << 16#56:32/little >>; _ -> << >> end,
{2 + byte_size(SyncBin) div 4, << 16#60:32/little, 16#ffffffff:32, SyncBin/binary >>, [{N2, Pos + 1}], Funcs2};
instruction({syscall, {N, async}}, Funcs, _Pos) ->
{2, << 16#61:32/little, N:32/little >>, [], Funcs};
instruction({syscall, {N, sync}}, Funcs, _Pos) ->
{3, << 16#61:32/little, N:32/little, 16#56:32/little >>, [], Funcs};
%% Low level instructions.
instruction('abs', Funcs, _Pos) ->
{1, << 16#11:32/little >>, [], Funcs};
instruction(add, Funcs, _Pos) ->
{1, << 16#04:32/little >>, [], Funcs};
instruction('band', Funcs, _Pos) ->
{1, << 16#0b:32/little >>, [], Funcs};
instruction('bor', Funcs, _Pos) ->
{1, << 16#0c:32/little >>, [], Funcs};
instruction('bxor', Funcs, _Pos) ->
{1, << 16#0d:32/little >>, [], Funcs};
instruction(dec, Funcs, _Pos) ->
{1, << 16#0f:32/little >>, [], Funcs};
instruction('div', Funcs, _Pos) ->
{1, << 16#07:32/little >>, [], Funcs};
instruction(inc, Funcs, _Pos) ->
{1, << 16#0e:32/little >>, [], Funcs};
instruction(is_eq, Funcs, _Pos) ->
{1, << 16#12:32/little >>, [], Funcs};
instruction(is_gt, Funcs, _Pos) ->
{1, << 16#15:32/little >>, [], Funcs};
instruction(is_gteq, Funcs, _Pos) ->
{1, << 16#14:32/little >>, [], Funcs};
instruction(is_lt, Funcs, _Pos) ->
{1, << 16#17:32/little >>, [], Funcs};
instruction(is_lteq, Funcs, _Pos) ->
{1, << 16#16:32/little >>, [], Funcs};
instruction(is_neq, Funcs, _Pos) ->
{1, << 16#13:32/little >>, [], Funcs};
instruction({jmp, N}, Funcs, _Pos) ->
{2, << 16#2c:32/little, N:32/little-signed >>, [], Funcs};
instruction({jnz, N}, Funcs, _Pos) ->
{2, << 16#2e:32/little, N:32/little-signed >>, [], Funcs};
instruction({jz, N}, Funcs, _Pos) ->
{2, << 16#2d:32/little, N:32/little-signed >>, [], Funcs};
instruction(land, Funcs, _Pos) ->
{1, << 16#18:32/little >>, [], Funcs};
instruction(lor, Funcs, _Pos) ->
{1, << 16#19:32/little >>, [], Funcs};
instruction(lshift, Funcs, _Pos) ->
{1, << 16#09:32/little >>, [], Funcs};
instruction(mod, Funcs, _Pos) ->
{1, << 16#08:32/little >>, [], Funcs};
instruction(mul, Funcs, _Pos) ->
{1, << 16#06:32/little >>, [], Funcs};
instruction(neg, Funcs, _Pos) ->
{1, << 16#10:32/little >>, [], Funcs};
instruction(nop, Funcs, _Pos) ->
{1, << 16#01:32/little >>, [], Funcs};
instruction({num_get, {Name, _Type}}, Funcs, Pos) ->
{ok, N} = find_func(Name, Funcs),
{2, << 16#3c:32/little, 16#ffffffff:32 >>, [{N, Pos + 1}], Funcs};
instruction({num_set, {Name, _Type}}, Funcs, Pos) ->
{ok, N} = find_func(Name, Funcs),
{2, << 16#3d:32/little, 16#ffffffff:32 >>, [{N, Pos + 1}], Funcs};
instruction({push, I}, Funcs, _Pos) when is_integer(I) ->
{2, << 16#02:32/little, I:32/little-signed >>, [], Funcs};
instruction({push, F}, Funcs, _Pos) when is_float(F) ->
{2, << 16#03:32/little, F:32/little-signed-float >>, [], Funcs};
instruction({push, Str}, Funcs, _Pos) when is_list(Str) ->
StrBin = list_to_binary(Str),
L = length(Str),
Padding = 8 * (4 - L rem 4),
StrBin2 = << StrBin/binary, 0:Padding >>,
Size = byte_size(StrBin2) div 4,
{2 + Size, << 16#46:32/little, Size:32/little, StrBin2/binary >>, [], Funcs};
instruction(restore, Funcs, _Pos) ->
{1, << 16#1b:32/little >>, [], Funcs};
instruction(return, Funcs, _Pos) ->
{1, << 16#00:32/little >>, [], Funcs};
instruction(rshift, Funcs, _Pos) ->
{1, << 16#0a:32/little >>, [], Funcs};
instruction(save, Funcs, _Pos) ->
{1, << 16#1a:32/little >>, [], Funcs};
instruction(savep, Funcs, _Pos) ->
{1, << 16#1d:32/little >>, [], Funcs};
instruction({str_get, {Name, _Type}}, Funcs, Pos) ->
{ok, N} = find_func(Name, Funcs),
{2, << 16#49:32/little, 16#ffffffff:32 >>, [{N, Pos + 1}], Funcs};
instruction({str_set, {Name, _Type}}, Funcs, Pos) ->
{ok, N} = find_func(Name, Funcs),
{2, << 16#4a:32/little, 16#ffffffff:32 >>, [{N, Pos + 1}], Funcs};
instruction(sub, Funcs, _Pos) ->
{1, << 16#05:32/little >>, [], Funcs};
%% Debug instruction. Do nothing by default. Change it to whatever needs testing.
instruction(debug, Funcs, _Pos) ->
Bin = << 16#01:32/little >>,
{byte_size(Bin) div 4, Bin, [], Funcs}.
case_tests(Tests, Funcs, Pos) ->
case_tests(Tests, Funcs, Pos, [], []).
case_tests(nil, Funcs, Pos, Acc, VarsAcc) ->
{Pos, case_tests_end(lists:reverse(Acc)), VarsAcc, Funcs};
case_tests({{case_default, Instrs}, Next}, Funcs, Pos, Acc, VarsAcc) ->
{Pos2, InstrsBin, VarsList, Funcs2} = instructions(Instrs, Funcs, Pos),
case_tests(Next, Funcs2, Pos2, [InstrsBin|Acc], [VarsList|VarsAcc]);
case_tests({{case_test, N, Instrs}, Next}, Funcs, Pos, Acc, VarsAcc) ->
{Pos2, InstrsBin, VarsList, Funcs2} = instructions(Instrs, Funcs, Pos + 7),
Jump = Pos2 - Pos - 4,
TestBin = << 16#02:32/little, N:32/little-signed, 16#1d:32/little, 16#12:32/little,
16#2d:32/little, Jump:32/little, 16#1b:32/little, InstrsBin/binary >>,
case_tests(Next, Funcs2, Pos2 + 2, [TestBin|Acc], [VarsList|VarsAcc]).
case_tests_end(Tests) ->
case_tests_end(Tests, []).
case_tests_end([], Acc) ->
iolist_to_binary(lists:reverse(Acc));
case_tests_end([TestBin|Tail], Acc) ->
TailBin = iolist_to_binary(Tail),
Jump = byte_size(TailBin) div 4 + 2 * length(Tail),
case_tests_end(Tail, [<< TestBin/binary, 16#2c:32/little, Jump:32/little >>|Acc]).
funcs(Funcs) ->
funcs(Funcs, []).
funcs([], Acc) ->
iolist_to_binary(Acc);
funcs([Name|Tail], Acc) ->
NameBin = list_to_binary(Name),
Padding = 8 * (32 - byte_size(NameBin)),
funcs(Tail, [<< NameBin/binary, 0:Padding >>|Acc]).
find_func(Name, Funcs) ->
find_func(Name, lists:reverse(Funcs), 0).
find_func(_Name, [], N) ->
{{error, undefined}, N};
find_func(Name, [Func|_Tail], N) when Name =:= Func ->
{ok, N};
find_func(Name, [_Func|Tail], N) ->
find_func(Name, Tail, N + 1).

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,509 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS script lexer.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
Definitions.
D = [0-9]
L = [a-zA-Z]
N = ({L}|{D}|_)
WS = ([\000-\s]|%.*)
Rules.
-*{D}+ : {token, {integer, TokenLine, list_to_integer(TokenChars)}}.
-*{D}+\.{D}+ : {token, {float, TokenLine, list_to_float(TokenChars)}}.
{N}+\.*{N}+ : {token, case reserved_word(TokenChars) of
false -> case syscall(TokenChars) of
false -> {function, TokenLine, function(TokenChars)};
Syscall -> {syscall, TokenLine, Syscall}
end;
KeyWord -> {KeyWord, TokenLine}
end}.
"(\\\^.|\\.|[^"])*" : %% Strip quotes.
S = lists:sublist(TokenChars, 2, TokenLen - 2),
{token, {string, TokenLine, string_gen(S)}}.
-> : {token, {'->', TokenLine}}.
[}{,;] : {token, {list_to_atom(TokenChars), TokenLine}}.
\.{WS} : {end_token, {dot, TokenLine}}.
{WS}+ : skip_token.
Erlang code.
%% Reserved words.
reserved_word("debug") -> debug;
%% Definitions.
reserved_word("event") -> event_def;
reserved_word("function") -> function_def;
reserved_word("num_var") -> num_var;
reserved_word("str_var") -> str_var;
%% Low level opcodes.
reserved_word("abs") -> abs;
reserved_word("add") -> add;
reserved_word("band") -> 'band';
reserved_word("bor") -> 'bor';
reserved_word("bxor") -> 'bxor';
reserved_word("dec") -> dec;
reserved_word("div") -> 'div';
reserved_word("inc") -> inc;
reserved_word("is_eq") -> is_eq;
reserved_word("is_gt") -> is_gt;
reserved_word("is_gteq") -> is_gteq;
reserved_word("is_lt") -> is_lt;
reserved_word("is_lteq") -> is_lteq;
reserved_word("is_neq") -> is_neq;
reserved_word("jmp") -> jmp;
reserved_word("jnz") -> jnz;
reserved_word("jz") -> jz;
reserved_word("land") -> land;
reserved_word("lor") -> lor;
reserved_word("lshift") -> lshift;
reserved_word("mod") -> mod;
reserved_word("mul") -> mul;
reserved_word("neg") -> neg;
reserved_word("nop") -> nop;
reserved_word("num_get") -> num_get;
reserved_word("num_set") -> num_set;
reserved_word("push") -> push;
reserved_word("restore") -> restore;
reserved_word("return") -> return;
reserved_word("rshift") -> rshift;
reserved_word("save") -> save;
reserved_word("savep") -> savep;
reserved_word("str_get") -> str_get;
reserved_word("str_set") -> str_set;
reserved_word("sub") -> sub;
%% Case statement.
reserved_word("case") -> 'case';
reserved_word("default") -> default;
reserved_word("end") -> 'end';
%% Otherwise isn't a reserved word.
reserved_word(_) -> false.
%% Syscalls.
syscall("mes.fukidasi_pc") -> {100, sync};
syscall("mes.fukidasi_apc") -> {101, sync};
syscall("mes.fukidasi_npc") -> {102, sync};
syscall("mes.fukidasi_pc_type") -> {103, sync};
syscall("mes.fukidasi_apc_type") -> {104, sync};
syscall("mes.fukidasi_npc_type") -> {105, sync};
syscall("mes.fukidasi_custom") -> {106, sync};
syscall("mes.erase_fukidasi") -> {107, async};
syscall("mes.chat_pc") -> {108, async};
syscall("mes.cut_in_chat") -> {109, async};
syscall("mes.cut_in_chat_custom") -> {110, async};
syscall("mes.system_win") -> {111, sync};
syscall("mes.custom_win") -> {112, sync};
syscall("mes.scroll_win") -> {113, sync};
syscall("mes.win_move") -> {114, async};
syscall("mes.erase_system_win") -> {115, async};
syscall("mes.select_win") -> {116, sync};
syscall("mes.select_win_set") -> {117, sync};
syscall("mes.select_win_single") -> {118, sync};
syscall("mes.select_win_custom") -> {119, sync};
syscall("mes.select_win_fukidasi") -> {120, sync};
syscall("mes.select_win_b") -> {121, sync};
syscall("mes.select_win_set_b") -> {122, sync};
syscall("mes.select_win_single_b") -> {123, sync};
syscall("mes.select_win_custom_b") -> {124, sync};
syscall("mes.select_win_fukidasi_b") -> {125, sync};
syscall("mes.erase_select_win") -> {126, sync};
syscall("mes.message_end") -> {127, async};
syscall("mes.set_name_npc") -> {128, async};
syscall("mes.set_color_npc") -> {129, async};
syscall("mes.line_window") -> {130, async};
syscall("adv.cut_in") -> {131, async};
syscall("adv.cut_in_custom") -> {132, async};
syscall("adv.window_disable") -> {133, async};
syscall("adv.window_enable") -> {134, async};
syscall("adv.fukidasi_disable") -> {135, async};
syscall("adv.fukidasi_enable") -> {136, async};
syscall("adv.player_disable") -> {137, async};
syscall("adv.player_enable") -> {138, async};
syscall("adv.chat_on") -> {139, async};
syscall("adv.chat_off") -> {140, async};
syscall("adv.mainmenu_disable") -> {141, async};
syscall("adv.mainmenu_enable") -> {142, async};
syscall("unit_flag.on") -> {143, async};
syscall("unit_flag.off") -> {144, async};
syscall("unit_flag.reverse") -> {145, async};
syscall("unit_flag.delay_on") -> {146, async};
syscall("unit_flag.delay_off") -> {147, async};
syscall("unit_flag.delay_reverse") -> {148, async};
syscall("unit_flag.chk") -> {149, async};
syscall("player.get_my_id") -> {150, async};
syscall("player.set_pos") -> {151, async};
syscall("player.change_unit") -> {152, async};
syscall("player.turn") -> {153, async};
syscall("player.walk") -> {154, async};
syscall("player.run") -> {155, async};
syscall("player.pad_off") -> {158, async};
syscall("player.pad_on") -> {159, async};
syscall("player.get_pos") -> {160, async};
syscall("player.get_ang") -> {161, async};
syscall("player.turn_coord") -> {162, async};
syscall("player.turn_member") -> {163, async};
syscall("plymotion.item_take_off") -> {173, async};
syscall("plymotion.set_pack") -> {174, sync};
syscall("plymotion.release_pack") -> {176, async};
syscall("plymotion.set_loop") -> {177, async};
syscall("plymotion.play_one_shot") -> {178, sync};
syscall("plymotion.cancel_one_shot") -> {179, async};
syscall("plymotion.restart_one_shot") -> {180, async};
syscall("apc.create") -> {183, sync};
syscall("apc.delete") -> {184, async};
syscall("apc.delete_inx") -> {185, async};
syscall("apc.team_into") -> {186, async};
syscall("apc.team_remove") -> {187, async};
syscall("apc.set_pos") -> {188, async};
syscall("apc.turn") -> {189, async};
syscall("apc.walk") -> {190, async};
syscall("apc.run") -> {191, async};
syscall("apc.think_off") -> {194, async};
syscall("apc.think_on") -> {195, async};
syscall("apc.get_pos") -> {196, async};
syscall("apc.get_ang") -> {197, async};
syscall("apc.reserve") -> {205, async};
syscall("apc.reserve_cancel") -> {206, async};
syscall("apc.is_reserve") -> {207, async};
syscall("apc.create_lv") -> {208, sync};
syscall("apc.reserve_lv") -> {209, async};
syscall("apc.target_guard") -> {210, async};
syscall("apc.target_guard_pid") -> {211, async};
syscall("apc.target_guard_aid") -> {212, async};
syscall("apc.cancel_target_guard") -> {213, async};
syscall("apc.set_move_out_party") -> {214, async};
syscall("apc.apc_set_ban") -> {215, async};
syscall("apc.get_aid_from_pid") -> {216, async};
syscall("apc.is_apc2") -> {217, async};
syscall("apc.is_alive") -> {218, async};
syscall("camera.set") -> {219, async};
syscall("camera.move") -> {220, async};
syscall("camera.release") -> {221, async};
syscall("camera.get_pos") -> {222, async};
syscall("camera.cam_rot") -> {223, async};
syscall("camera.target_rot") -> {224, async};
syscall("camera.dist_move") -> {225, async};
syscall("camera.get_cam_angle") -> {226, async};
syscall("camera.get_target_angle") -> {227, async};
syscall("camera.get_dist") -> {228, async};
syscall("camera.set_quake") -> {229, async};
syscall("camera.stop_quake") -> {230, async};
syscall("direction.fade_in") -> {232, async};
syscall("direction.fade_out") -> {233, async};
syscall("direction.fade_out_rgba") -> {234, async};
syscall("direction.cinema_on") -> {235, async};
syscall("direction.cinema_off") -> {236, async};
syscall("direction.vib") -> {237, async};
syscall("wait.abort_cancel") -> {238, async};
syscall("temp.exit_game") -> {239, async};
syscall("movie.play_prm") -> {240, sync};
syscall("movie.play_rtm") -> {242, sync};
syscall("movie.play_event") -> {245, sync};
syscall("movie.play_sub_title") -> {246, sync};
syscall("movie.telop") -> {247, sync};
syscall("sound.play_se") -> {251, async};
syscall("sound.play_bgm") -> {253, async}; %% Unofficial name.
syscall("sound.stop_bgm") -> {256, async};
syscall("render.scene_off") -> {257, async};
syscall("render.scene_on") -> {258, async};
syscall("render.npc_all_off") -> {259, async};
syscall("render.npc_all_on") -> {260, async};
syscall("render.npc_off") -> {261, async};
syscall("render.npc_on") -> {262, async};
syscall("render.player_off") -> {263, async};
syscall("render.player_on") -> {264, async};
syscall("render.enemy_off") -> {265, async};
syscall("render.enemy_on") -> {266, async};
syscall("render.obj_off") -> {267, async};
syscall("render.obj_on") -> {268, async};
syscall("seq.get_quest") -> {271, async};
syscall("seq.is_online") -> {272, async};
syscall("seq.save_qst") -> {273, sync};
syscall("seq.get_area") -> {274, async};
syscall("seq.get_zone") -> {275, async};
syscall("seq.get_unit") -> {276, async};
syscall("seq.get_item") -> {280, sync};
syscall("work.accountwork_get") -> {282, sync};
syscall("work.accountwork_set") -> {283, sync};
syscall("work.chrwork_get") -> {284, sync};
syscall("work.chrwork_set") -> {285, sync};
syscall("work.chrflag_get") -> {286, sync};
syscall("work.chrflag_set") -> {287, sync};
syscall("work.partyflag_on") -> {288, sync};
syscall("work.partyflag_off") -> {289, sync};
syscall("work.qstwork_get") -> {290, async};
syscall("work.qstwork_get_member") -> {291, async};
syscall("work.qstwork_set") -> {292, async};
syscall("work.partyflag_get") -> {297, async};
syscall("work.partywork_get") -> {298, async};
syscall("work.partywork_set") -> {299, async};
syscall("work.zone_cndflag_get") -> {300, async};
syscall("work.zone_cndflag_on") -> {301, async};
syscall("work.zone_cndflag_off") -> {302, async};
syscall("work.zone_sendwork_get") -> {303, async};
syscall("work.zone_sendwork_set") -> {304, async};
syscall("work.party_rand_get") -> {305, sync};
syscall("party.get_member") -> {306, async};
syscall("party.get_leader") -> {307, async};
syscall("party.chk_exist") -> {308, async};
syscall("party.sync_wait") -> {309, sync};
syscall("party.join_on") -> {310, async};
syscall("party.join_off") -> {311, async};
syscall("party.get_enemy_kill") -> {312, async};
syscall("ptcl.create") -> {315, async};
syscall("ptcl.move") -> {316, async};
syscall("ptcl.delete") -> {317, async};
syscall("col.create") -> {318, async};
syscall("col.create_lock_on") -> {319, async};
syscall("col.create_attack") -> {320, async};
syscall("col.move") -> {321, async};
syscall("col.scale") -> {322, async};
syscall("col.delete") -> {323, async};
syscall("npc.talk_on") -> {324, async};
syscall("npc.talk_off") -> {325, async};
syscall("npc.think_on") -> {326, async};
syscall("npc.think_off") -> {327, async};
syscall("npc.set_pos") -> {328, async};
syscall("npc.turn") -> {329, async};
syscall("npc.gaze") -> {330, async};
syscall("npc.walk") -> {331, async};
syscall("npc.run") -> {332, async};
syscall("npc.walk_path") -> {335, async};
syscall("npc.run_path") -> {336, async};
syscall("npc.get_pos") -> {338, async};
syscall("npc.get_ang") -> {339, async};
syscall("npc.cng_motion") -> {340, async};
syscall("npc.cancel_motion") -> {341, async};
syscall("npc.get_motion") -> {342, async};
syscall("npc.coli_end") -> {343, async};
syscall("obj.coli_end") -> {347, async}; %% Unofficial name.
syscall("status.get_posflag") -> {354, async};
syscall("lobby.start") -> {356, sync};
syscall("lobby.end") -> {357, async};
syscall("lobby.select_planet") -> {358, sync};
syscall("camera.def_cng") -> {359, async};
syscall("camera.def_remove") -> {360, async};
syscall("camera.def_move_back") -> {361, async};
syscall("camera.def_set_back") -> {362, async};
syscall("camera.def_set_quake") -> {364, async};
syscall("camera.def_stop_qake") -> {365, async};
syscall("calc.get_rot") -> {373, async};
syscall("sjis.fukidasi_pc") -> {389, sync};
syscall("sjis.fukidasi_apc") -> {390, sync};
syscall("sjis.fukidasi_npc") -> {391, sync};
syscall("sjis.fukidasi_pc_type") -> {392, sync};
syscall("sjis.fukidasi_apc_type") -> {393, sync};
syscall("sjis.fukidasi_npc_type") -> {394, sync};
syscall("sjis.fukidasi_custom") -> {395, sync};
syscall("sjis.chat_pc") -> {396, async};
syscall("sjis.cut_in_chat") -> {397, async};
syscall("sjis.cut_in_chat_custom") -> {398, async};
syscall("sjis.system_win") -> {399, sync};
syscall("sjis.custom_win") -> {400, sync};
syscall("sjis.scroll_win") -> {401, sync};
syscall("sjis.select_win") -> {402, sync};
syscall("sjis.select_win_set") -> {403, sync};
syscall("sjis.select_win_single") -> {404, sync};
syscall("sjis.select_win_custom") -> {405, sync};
syscall("sjis.select_win_fukidasi") -> {406, sync};
syscall("sjis.select_win_b") -> {407, sync};
syscall("sjis.select_win_set_b") -> {408, sync};
syscall("sjis.select_win_single_b") -> {409, sync};
syscall("sjis.select_win_custom_b") -> {410, sync};
syscall("sjis.select_win_fukidasi_b") -> {411, sync};
syscall("sjis.line_window") -> {412, async};
syscall("temp.line") -> {413, async};
syscall("temp.sline") -> {414, async};
syscall(_) -> false.
%% Functions.
function("adv.cut_in_load") -> {"q.adv.cut_in_load", async};
function("adv.cut_in_noise") -> {"q.adv.cut_in_noise", async};
function("adv.cut_in_release") -> {"q.adv.cut_in_release", async};
function("adv.cut_in_stop") -> {"q.adv.cut_in_stop", async};
%% @todo apc.apc_sp_exec
%% @todo q.apc.create_ex
function("apc.get_reserve_aid_all") -> {"apc.get_reserve_aid_all", async};
function("apc.get_reserve_count") -> {"apc.get_reserve_count", async};
function("apc.guildcard_disable") -> {"apc.guildcard_disable", async};
function("apc.guildcard_enable") -> {"apc.guildcard_enable", async};
function("apc.impossible_join_pt") -> {"apc.impossible_join_pt", async};
function("apc.is_set_ban") -> {"apc.is_set_ban", async};
%% @todo apc.move
%% @todo apc.move_xz
function("apc.possible_join_pt") -> {"apc.possible_join_pt", async};
%% @todo apc.set_goal_out_party
function("apc.set_sp_ban") -> {"apc.set_sp_ban", async};
function("apc.set_talk_ban") -> {"apc.set_talk_ban", async};
function("counter.create_dc_map") -> {"counter.create_dc_map", async};
function("counter.delete_dc_map") -> {"counter.delete_dc_map", async};
function("counter.set_arrow_pos") -> {"counter.set_arrow_pos", async};
function("counter.set_lookon_pos") -> {"counter.set_lookon_pos", async};
function("direction.fade_out_nowloading") -> {"direction.fade_out_nowloading", async};
function("font.set_party_member_id") -> {"font.set_party_member_id", async};
function("item.exchange") -> {"q.seq.exchange_item", sync};
function("item.get_num") -> {"q.seq.get_item_num", sync};
function("item.get_slot_num_req") -> {"q.item.get_slot_num_req", sync};
function("item.get_slot_num_ret") -> {"q.item.get_slot_num_ret", async};
%% @todo item.set_ability
function("mes.mes_broadcast_clear") -> {"mes.mes_broadcast_clear", async};
%% @todo mes.mes_broadcast_clear_all
function("mes.mes_broadcast_set") -> {"mes.mes_broadcast_set", async};
function("mes.line_window_time") -> {"q.mes.line_window_line_time", async};
function("mes.select_win_number") -> {"q.mes.select_win_number", sync};
function("movie.text_prm_delay") -> {"movie.text_prm_delay", async};
function("myroom.check_data_load") -> {"myroom.check_data_load", async};
function("myroom.disable_obj_ex") -> {"myroom.disable_obj_ex", async};
function("myroom.get_move_planet") -> {"myroom.get_move_planet", async};
function("myroom.get_probo_ang") -> {"myroom.get_probo_ang", async};
function("myroom.get_probo_pos") -> {"myroom.get_probo_pos", async};
function("myroom.get_probo_rank") -> {"myroom.get_probo_rank", async};
function("myroom.news_enable") -> {"myroom.news_enable", async};
%% @todo myroom.get_probo_type
function("npc.gaze_cancel") -> {"npc.gaze_cancel", async};
%% @todo npc.get_head_y
function("npc.is_load_comp") -> {"npc.is_load_comp", async};
%% @todo q.npc.run_fast
%% @todo npc.run_xz_fast
%% @todo q.npc.walk_fast
%% @todo npc.walk_xz_fast
%% @todo obj.chair_disable
%% @todo obj.chair_enable
function("obj.get_eventflag") -> {"obj.get_eventflag", async};
function("obj.set_caption") -> {"obj.set_caption", async};
function("obj.vehicle_resetpos") -> {"obj.vehicle_resetpos", async};
function("player.display") -> {"player.display", async};
function("player.disp_off_except_party") -> {"player.disp_off_except_party", async};
function("player.disp_on_except_party") -> {"player.disp_on_except_party", async};
function("player.face_set") -> {"player.face_set", async};
%% @todo player.gaze_body
%% @todo player.get_action
function("player.get_level") -> {"player.get_level", async};
function("player.get_my_pmid") -> {"q.player.get_my_pmid", async};
function("player.get_occupation") -> {"player.get_occupation", async};
function("player.get_off_vehicle") -> {"player.get_off_vehicle", async};
%% @todo player.get_on_vehicle
function("player.get_race") -> {"player.get_race", async};
function("player.get_sex") -> {"player.get_sex", async};
function("player.hide") -> {"player.hide", async};
function("player.is_dead") -> {"player.is_dead", async};
%% @todo player.is_equipping_item
function("player.is_in_sp") -> {"player.is_in_sp", async};
function("player.is_on_vehicle") -> {"player.is_on_vehicle", async};
%% @todo player.move
%% @todo player.move_xz
%% @todo player.set_vehicle_breakpos
function("player.sp_cancel") -> {"player.sp_cancel", async};
%% @todo player.turn_round
%% @todo player.turn_round_coord_xz
function("plymotion.can_i_set_event_pack") -> {"plymotion.can_i_set_event_pack", async};
%% @todo plymotion.get_pack
function("plymotion.is_ready") -> {"plymotion.is_ready", async};
function("plymotion.item_re_equip") -> {"plymotion.item_re_equip", async};
%% @todo radar.display
%% @todo radar.fullclose
function("radar.fullopen") -> {"radar.fullopen", async};
%% @todo radar.hide
function("seq.endroll_play") -> {"q.seq.endroll_play", sync};
%% @todo seq.get_death_count
%% @todo seq.get_death_count_apc
function("seq.get_destroy_enemy_count") -> {"seq.get_destroy_enemy_count", async};
function("seq.get_elapsed_time") -> {"seq.get_elapsed_time", async};
function("seq.get_judgement") -> {"seq.get_judgement", async};
function("seq.get_mission_step") -> {"seq.get_mission_step", async};
function("seq.get_rank") -> {"seq.get_rank", async};
%% @todo seq.get_remaining_time
%% @todo seq.get_setdata_inx
%% @todo seq.goto_title
function("seq.has_finished_result") -> {"seq.has_finished_result", async};
function("seq.open_ex_mode") -> {"seq.open_ex_mode", async};
function("seq.preview_network") -> {"q.seq.preview_network", sync};
function("seq.preview_play") -> {"q.seq.preview_play", sync};
function("seq.result") -> {"q.seq.result", async};
function("seq.save_menu") -> {"q.seq.save_menu", sync};
%% @todo seq.set_chapter_no
function("seq.set_ex_chapter_no") -> {"seq.set_ex_chapter_no", async};
%% @todo seq.set_story_end_flag
function("seq.staffroll_play") -> {"q.seq.staffroll_play", sync};
%% @todo q.seq.unify_counter
function("shop.open") -> {"shop.open", async};
%% @todo shop.req_load_data
function("sjis.line_window_time") -> {"q.sjis.line_window_time", async};
function("sound.check_jingle") -> {"sound.check_jingle", async};
%% @todo sound.get_player_pack
function("sound.load_enemy_pack") -> {"sound.load_enemy_pack", async};
%% @todo sound.load_enemy_port_pack
function("sound.load_event_pack") -> {"sound.load_event_pack", async};
function("sound.load_player_pack") -> {"sound.load_player_pack", async};
%% @todo sound.pause_adx_voice
function("sound.pause_bgm") -> {"sound.pause_bgm", async};
function("sound.play_jingle") -> {"sound.play_jingle", async};
function("sound.play_se3d_loop_stop") -> {"sound.play_se3d_loop_stop", async};
function("sound.play_se3d_loop_vol") -> {"sound.play_se3d_loop_vol", async};
function("sound.play_player") -> {"q.sound.play_player", sync};
function("sound.play_se3d") -> {"q.sound.play_se3d", sync};
%% @todo sound.play_se3d_loop
function("sound.play_se3d_vol") -> {"q.sound.play_se3d_vol", sync};
function("sound.play_technique") -> {"sound.play_technique", async};
function("sound.play_vehicle") -> {"sound.play_vehicle", async};
function("sound.stop_adx_voice") -> {"sound.stop_adx_voice", async};
function("system.frame_adjust") -> {"system.frame_adjust", async};
%% @todo system.frame_default
function("system.frame_get") -> {"system.frame_get", async};
%% @todo system.frame_set
function("system.get_episode") -> {"system.get_episode", async};
%% @todo system.get_language
function("system.get_platform") -> {"system.get_platform", async};
function("system.get_resolution") -> {"system.get_resolution", async};
function("work.get_stamp_bonus") -> {"q.work.get_stamp_bonus", sync};
function("work.get_stamp_num") -> {"q.work.get_stamp_num", sync};
function("work.get_stamp_quest") -> {"q.work.get_stamp_quest", sync};
%% @todo q.work.on_accountwork_get
function("work.partyworknum_get") -> {"q.work.partyworknum_get", async};
function("work.partyworknum_set") -> {"q.work.partyworknum_set", async};
%% @todo q.work.stamp_command
function(Name) -> {Name, local}.
%% Strings.
string_gen([$\\|Cs]) ->
string_escape(Cs);
string_gen([C|Cs]) ->
[C|string_gen(Cs)];
string_gen([]) ->
[].
string_escape([O1,O2,O3|S]) when O1 >= $0, O1 =< $7, O2 >= $0, O2 =< $7, O3 >= $0, O3 =< $7 ->
[(O1*8 + O2)*8 + O3 - 73*$0|string_gen(S)];
string_escape([$^,C|Cs]) ->
[C band 31|string_gen(Cs)];
string_escape([C|Cs]) when C >= $\000, C =< $\s ->
string_gen(Cs);
string_escape([C|Cs]) ->
[escape_char(C)|string_gen(Cs)].
escape_char($n) -> $\n; %% \n = LF
escape_char($r) -> $\r; %% \r = CR
escape_char($t) -> $\t; %% \t = TAB
escape_char($v) -> $\v; %% \v = VT
escape_char($b) -> $\b; %% \b = BS
escape_char($f) -> $\f; %% \f = FF
escape_char($e) -> $\e; %% \e = ESC
escape_char($s) -> $\s; %% \s = SPC
escape_char($d) -> $\d; %% \d = DEL
escape_char(C) -> C.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,90 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS script parser.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
Nonterminals declarations declaration instructions instruction case_tests case_test case_default.
Terminals
integer float string function syscall debug event_def function_def num_var str_var
abs add band bor bxor dec div inc is_eq is_gt is_gteq is_lt is_lteq is_neq
jmp jnz jz land lor lshift mod mul neg nop num_get num_set push restore return
rshift save savep str_get str_set sub case default end '->' ',' ';' dot.
Rootsymbol declarations.
declarations -> declaration declarations : {'$1', '$2'}.
declarations -> '$empty' : nil.
declaration -> event_def function '->' instructions dot : {event_def, unwrap('$2'), '$4'}.
declaration -> function_def function '->' instructions dot : {function_def, unwrap('$2'), '$4'}.
declaration -> num_var function dot : {num_var, unwrap('$2')}.
declaration -> str_var function integer dot : {str_var, unwrap('$2'), unwrap('$3')}.
instructions -> instruction ',' instructions : {'$1', '$3'}.
instructions -> instruction : {'$1', nil}.
instruction -> abs : abs.
instruction -> add : add.
instruction -> band : 'band'.
instruction -> bor : 'bor'.
instruction -> bxor : 'bxor'.
instruction -> dec : dec.
instruction -> div : 'div'.
instruction -> inc : inc.
instruction -> is_eq : is_eq.
instruction -> is_gt : is_gt.
instruction -> is_gteq : is_gteq.
instruction -> is_lt : is_lt.
instruction -> is_lteq : is_lteq.
instruction -> is_neq : is_neq.
instruction -> jmp integer : {jmp, unwrap('$2')}.
instruction -> jnz integer : {jnz, unwrap('$2')}.
instruction -> jz integer : {jz, unwrap('$2')}.
instruction -> land : land.
instruction -> lor : lor.
instruction -> lshift : lshift.
instruction -> mod : mod.
instruction -> mul : mul.
instruction -> neg : neg.
instruction -> nop : nop.
instruction -> num_get function : {num_get, unwrap('$2')}.
instruction -> num_set function : {num_set, unwrap('$2')}.
instruction -> push integer : {push, unwrap('$2')}.
instruction -> push float : {push, unwrap('$2')}.
instruction -> push string : {push, unwrap('$2')}.
instruction -> restore : restore.
instruction -> return : return.
instruction -> rshift : rshift.
instruction -> save : save.
instruction -> savep : savep.
instruction -> str_get function : {str_get, unwrap('$2')}.
instruction -> str_set function : {str_set, unwrap('$2')}.
instruction -> sub : sub.
instruction -> debug : debug.
instruction -> function : {function, unwrap('$1')}.
instruction -> syscall : {syscall, unwrap('$1')}.
instruction -> 'case' case_tests 'end' : {'case', '$2'}.
case_tests -> case_test ';' case_tests : {'$1', '$3'}.
case_tests -> case_test ';' case_default : {'$1', {'$3', nil}}.
case_tests -> case_test : {'$1', nil}.
case_test -> integer '->' instructions : {case_test, unwrap('$1'), '$3'}.
case_default -> default '->' instructions : {case_default, '$3'}.
Erlang code.
unwrap({_,_,V}) -> V.

View File

@ -1,5 +1,5 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin.
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS seasons management.
%% @todo When we know how to do it we should change the lobby automatically to the next season.
%%
@ -20,6 +20,7 @@
-module(egs_seasons).
-behavior(gen_server).
-export([start_link/0, stop/0, read/1]). %% API.
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.

View File

@ -1,5 +1,5 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin.
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS shops database.
%%
%% This file is part of EGS.
@ -19,6 +19,7 @@
-module(egs_shops_db).
-behavior(gen_server).
-export([start_link/0, stop/0, nth/2, read/1, reload/0]). %% API.
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.

54
apps/egs/src/egs_sup.erl Normal file
View File

@ -0,0 +1,54 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc Top-level supervisor for the egs application.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_sup).
-behaviour(supervisor).
-export([start_link/0]). %% API.
-export([init/1]). %% Supervisor.
-spec start_link() -> {ok, pid()}.
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
-spec init([]) -> {ok, {{one_for_one, 10, 10}, [supervisor:child_spec(), ...]}}.
init([]) ->
Procs = procs([egs_conf, {sup, egs_quests_sup}, {sup, egs_zones_sup},
egs_accounts, egs_users, egs_seasons, egs_counters_db, egs_items_db,
egs_npc_db, egs_quests_db, egs_shops_db, egs_universes], []),
{ok, {{one_for_one, 10, 10}, Procs}}.
%% Internal.
-spec procs([module()|{sup, module()}], [supervisor:child_spec()])
-> [supervisor:child_spec()].
procs([], Acc) ->
lists:reverse(Acc);
procs([{sup, Module}|Tail], Acc) ->
procs(Tail, [sup(Module)|Acc]);
procs([Module|Tail], Acc) ->
procs(Tail, [worker(Module)|Acc]).
-spec worker(M) -> {M, {M, start_link, []}, permanent, 5000, worker, dynamic}.
worker(Module) ->
{Module, {Module, start_link, []}, permanent, 5000, worker, dynamic}.
-spec sup(M) -> {M, {M, start_link, []}, permanent, 5000, supervisor, [M]}.
sup(Module) ->
{Module, {Module, start_link, []}, permanent, 5000, supervisor, [Module]}.

View File

@ -0,0 +1,157 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS universes handler.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_universes).
-behavior(gen_server).
-export([start_link/0, stop/0, all/0, defaultid/0, enter/1, leave/1, myroomid/0, read/1, lobby_pid/2, reload/0]). %% API.
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
%% Use the module name for the server's name.
-define(SERVER, ?MODULE).
-record(state, {unis=[], lobbies=[]}).
%% Default universe IDs.
-define(MYROOM_ID, 21).
-define(DEFAULT_ID, 26).
%% Lobbies: permanent quests to start with the universe.
-define(LOBBIES, [1100000]).
%% API.
%% @spec start_link() -> {ok,Pid::pid()}
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%% @spec stop() -> stopped
stop() ->
gen_server:call(?SERVER, stop).
%% @spec all() -> term()
all() ->
gen_server:call(?SERVER, all).
%% @spec defaultid() -> 26
%% @doc Return the default universe, Uni 01, with ID 26.
defaultid() ->
?DEFAULT_ID.
%% @spec enter(UniID) -> term()
enter(UniID) ->
gen_server:cast(?SERVER, {enter, UniID}).
%% @spec leave(UniID) -> term()
leave(UniID) ->
gen_server:cast(?SERVER, {leave, UniID}).
%% @spec myroomid() -> 21
%% @doc Return the ID for the myroom universe.
myroomid() ->
?MYROOM_ID.
%% @spec read(UniID) -> term()
read(UniID) ->
gen_server:call(?SERVER, {read, UniID}).
lobby_pid(UniID, QuestID) ->
gen_server:call(?SERVER, {lobby_pid, UniID, QuestID}).
%% @spec reload() -> ok
reload() ->
gen_server:cast(?SERVER, reload).
%% gen_server.
%% @doc Create the unis, then load all the permanent quests nbl files, then create processes for all the needed quests.
init([]) ->
State = #state{unis=[create_myroom()|create_unis()]},
[egs_quests_db:quest_nbl(QuestID) || QuestID <- ?LOBBIES],
Lobbies = lists:flatten([init_lobbies(UniID) || {UniID, {Type, _Name, _NbPlayers, _MaxPlayers}} <- State#state.unis, Type =:= universe]),
{ok, State#state{lobbies=Lobbies}}.
handle_call(all, _From, State) ->
{reply, State#state.unis, State};
handle_call({read, UniID}, _From, State) ->
{reply, proplists:get_value(UniID, State#state.unis), State};
handle_call({lobby_pid, UniID, QuestID}, _From, State) ->
{_, Pid} = lists:keyfind({UniID, QuestID}, 1, State#state.lobbies),
{reply, Pid, State};
handle_call(stop, _From, State) ->
{stop, normal, stopped, State};
handle_call(_Request, _From, State) ->
{reply, ignored, State}.
handle_cast({enter, UniID}, State) ->
{Type, Name, NbPlayers, MaxPlayers} = proplists:get_value(UniID, State#state.unis),
Unis = proplists:delete(UniID, State#state.unis),
Unis2 = [{UniID, {Type, Name, NbPlayers + 1, MaxPlayers}}|Unis],
Unis3 = lists:keysort(1, Unis2),
{noreply, State#state{unis=Unis3}};
handle_cast({leave, UniID}, State) ->
{Type, Name, NbPlayers, MaxPlayers} = proplists:get_value(UniID, State#state.unis),
Unis = proplists:delete(UniID, State#state.unis),
Unis2 = [{UniID, {Type, Name, NbPlayers - 1, MaxPlayers}}|Unis],
Unis3 = lists:keysort(1, Unis2),
{noreply, State#state{unis=Unis3}};
handle_cast(reload, _State) ->
{noreply, #state{unis=[create_myroom()|create_unis()]}};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% Internal.
%% @doc Max players defaults to 5000 for now.
create_myroom() ->
{ok, << 16#fffe:16, MyRoomName/binary >>} = file:read_file("priv/universes/myroom.en_US.txt"),
{?MYROOM_ID, {myroom, MyRoomName, 0, 5000}}.
%% @doc Max players defaults to 1000 for now.
create_unis() ->
{ok, << 16#fffe:16, Universes/binary >>} = file:read_file("priv/universes/universes.en_US.txt"),
Universes2 = re:split(Universes, "\n."),
create_unis(Universes2, ?DEFAULT_ID, []).
create_unis([], _UniID, Acc) ->
lists:reverse(Acc);
create_unis([Name|Tail], UniID, Acc) ->
create_unis(Tail, UniID + 2, [{UniID, {universe, Name, 0, 1000}}|Acc]).
%% @doc Start lobbies for the given universe.
init_lobbies(UniID) ->
lists:map(fun(QuestID) ->
{ok, Pid} = egs_quests_sup:start_quest(UniID, QuestID),
{{UniID, QuestID}, Pid}
end, ?LOBBIES).

239
apps/egs/src/egs_users.erl Normal file
View File

@ -0,0 +1,239 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc Users handling.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_users).
-behaviour(gen_server).
-export([start_link/0, stop/0, broadcast/2, broadcast_all/1, find_by_pid/1, set_zone/3]). %% API.
-export([read/1, select/1, write/1, delete/1, item_nth/2, item_add/3, item_qty_add/3,
shop_enter/2, shop_leave/1, shop_get/1, money_add/2]). %% Deprecated API.
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
-define(SERVER, ?MODULE).
-include("include/records.hrl").
-record(state, {
users = [] :: list({egs:gid(), #users{}})
}).
%% API.
-spec start_link() -> {ok, Pid::pid()}.
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
-spec stop() -> stopped.
stop() ->
gen_server:call(?SERVER, stop).
broadcast(Message, PlayersGID) ->
gen_server:cast(?SERVER, {broadcast, Message, PlayersGID}).
broadcast_all(Message) ->
gen_server:cast(?SERVER, {broadcast_all, Message}).
find_by_pid(Pid) ->
gen_server:call(?SERVER, {find_by_pid, Pid}).
set_zone(GID, ZonePid, LID) ->
gen_server:call(?SERVER, {set_zone, GID, ZonePid, LID}).
%% Deprecated API.
%% @spec read(ID) -> {ok, User} | {error, badarg}
read(ID) ->
gen_server:call(?SERVER, {read, ID}).
select(GIDsList) ->
gen_server:call(?SERVER, {select, GIDsList}).
%% @spec write(User) -> ok
write(User) ->
gen_server:call(?SERVER, {write, User}).
%% @spec delete(GID) -> ok
delete(GID) ->
gen_server:call(?SERVER, {delete, GID}).
item_nth(GID, ItemIndex) ->
gen_server:call(?SERVER, {item_nth, GID, ItemIndex}).
item_add(GID, ItemID, Variables) ->
gen_server:call(?SERVER, {item_add, GID, ItemID, Variables}).
%% @todo Consumable items.
item_qty_add(GID, ItemIndex, QuantityDiff) ->
gen_server:call(?SERVER, {item_qty_add, GID, ItemIndex, QuantityDiff}).
shop_enter(GID, ShopID) ->
gen_server:call(?SERVER, {shop_enter, GID, ShopID}).
shop_leave(GID) ->
gen_server:call(?SERVER, {shop_leave, GID}).
shop_get(GID) ->
gen_server:call(?SERVER, {shop_get, GID}).
money_add(GID, MoneyDiff) ->
gen_server:call(?SERVER, {money_add, GID, MoneyDiff}).
%% gen_server.
init([]) ->
{ok, #state{}}.
handle_call({find_by_pid, Pid}, _From, State) ->
L = [User || {_GID, User} <- State#state.users, User#users.pid =:= Pid],
case L of
[] -> {reply, undefined, State};
[User] -> {reply, User, State}
end;
handle_call({set_zone, GID, ZonePid, LID}, _From, State) ->
{GID, User} = lists:keyfind(GID, 1, State#state.users),
Users2 = lists:delete({GID, User}, State#state.users),
{reply, ok, State#state{users=[{GID, User#users{zonepid=ZonePid, lid=LID}}|Users2]}};
handle_call({read, GID}, _From, State) ->
{GID, User} = lists:keyfind(GID, 1, State#state.users),
{reply, {ok, User}, State};
handle_call({select, UsersGID}, _From, State) ->
Users = [begin
{GID, User} = lists:keyfind(GID, 1, State#state.users),
User
end || GID <- UsersGID],
{reply, Users, State};
handle_call({write, User}, _From, State) ->
Users2 = lists:keydelete(User#users.gid, 1, State#state.users),
{reply, ok, State#state{users=[{User#users.gid, User}|Users2]}};
handle_call({delete, GID}, _From, State) ->
Users2 = lists:keydelete(GID, 1, State#state.users),
{reply, ok, State#state{users=Users2}};
handle_call({item_nth, GID, ItemIndex}, _From, State) ->
{GID, User} = lists:keyfind(GID, 1, State#state.users),
Item = lists:nth(ItemIndex + 1, User#users.inventory),
{reply, Item, State};
handle_call({item_add, GID, ItemID, Variables}, _From, State) ->
{GID, User} = lists:keyfind(GID, 1, State#state.users),
Inventory = case Variables of
#psu_consumable_item_variables{quantity=Quantity} ->
#psu_item{data=#psu_consumable_item{max_quantity=MaxQuantity}} = egs_items_db:read(ItemID),
{ItemID, #psu_consumable_item_variables{quantity=Quantity2}} = case lists:keyfind(ItemID, 1, User#users.inventory) of
false -> New = true, {ItemID, #psu_consumable_item_variables{quantity=0}};
Tuple -> New = false, Tuple
end,
Quantity3 = Quantity + Quantity2,
if Quantity3 =< MaxQuantity ->
lists:keystore(ItemID, 1, User#users.inventory, {ItemID, #psu_consumable_item_variables{quantity=Quantity3}})
end;
#psu_trap_item_variables{quantity=Quantity} ->
#psu_item{data=#psu_trap_item{max_quantity=MaxQuantity}} = egs_items_db:read(ItemID),
{ItemID, #psu_trap_item_variables{quantity=Quantity2}} = case lists:keyfind(ItemID, 1, User#users.inventory) of
false -> New = true, {ItemID, #psu_trap_item_variables{quantity=0}};
Tuple -> New = false, Tuple
end,
Quantity3 = Quantity + Quantity2,
if Quantity3 =< MaxQuantity ->
lists:keystore(ItemID, 1, User#users.inventory, {ItemID, #psu_trap_item_variables{quantity=Quantity3}})
end;
_ ->
New = true,
if length(User#users.inventory) < 60 ->
User#users.inventory ++ [{ItemID, Variables}]
end
end,
Users2 = lists:keydelete(User#users.gid, 1, State#state.users),
State2 = State#state{users=[{GID, User#users{inventory=Inventory}}|Users2]},
case New of
false -> {reply, 16#ffffffff, State2};
true -> {reply, length(Inventory), State2}
end;
handle_call({item_qty_add, GID, ItemIndex, QuantityDiff}, _From, State) ->
{GID, User} = lists:keyfind(GID, 1, State#state.users),
{ItemID, Variables} = lists:nth(ItemIndex + 1, User#users.inventory),
Inventory = case Variables of
#psu_trap_item_variables{quantity=Quantity} ->
#psu_item{data=#psu_trap_item{max_quantity=MaxQuantity}} = egs_items_db:read(ItemID),
Quantity2 = Quantity + QuantityDiff,
if Quantity2 =:= 0 ->
string:substr(User#users.inventory, 1, ItemIndex) ++ string:substr(User#users.inventory, ItemIndex + 2);
Quantity2 > 0, Quantity2 =< MaxQuantity ->
Variables2 = Variables#psu_trap_item_variables{quantity=Quantity2},
string:substr(User#users.inventory, 1, ItemIndex) ++ [{ItemID, Variables2}] ++ string:substr(User#users.inventory, ItemIndex + 2)
end
end,
Users2 = lists:keydelete(User#users.gid, 1, State#state.users),
{reply, ok, State#state{users=[{GID, User#users{inventory=Inventory}}|Users2]}};
handle_call({shop_enter, GID, ShopID}, _From, State) ->
{GID, User} = lists:keyfind(GID, 1, State#state.users),
Users2 = lists:delete({GID, User}, State#state.users),
{reply, ok, State#state{users=[{GID, User#users{shopid=ShopID}}|Users2]}};
handle_call({shop_leave, GID}, _From, State) ->
{GID, User} = lists:keyfind(GID, 1, State#state.users),
Users2 = lists:delete({GID, User}, State#state.users),
{reply, ok, State#state{users=[{GID, User#users{shopid=undefined}}|Users2]}};
handle_call({shop_get, GID}, _From, State) ->
{GID, User} = lists:keyfind(GID, 1, State#state.users),
{reply, User#users.shopid, State};
handle_call({money_add, GID, MoneyDiff}, _From, State) ->
{GID, User} = lists:keyfind(GID, 1, State#state.users),
Money = User#users.money + MoneyDiff,
if Money >= 0 ->
Users2 = lists:delete({GID, User}, State#state.users),
{reply, ok, [{GID, User#users{money=Money}}|Users2]}
end;
handle_call(stop, _From, State) ->
{stop, normal, stopped, State};
handle_call(_Request, _From, State) ->
{reply, ignored, State}.
handle_cast({broadcast, Message, PlayersGID}, State) ->
[begin {GID, #users{pid=Pid}} = lists:keyfind(GID, 1, State#state.users),
Pid ! Message
end || GID <- PlayersGID],
{noreply, State};
handle_cast({broadcast_all, Message}, State) ->
[Pid ! Message || {_GID, #users{pid=Pid}} <- State#state.users],
{noreply, State};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.

182
apps/egs/src/egs_zones.erl Normal file
View File

@ -0,0 +1,182 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin.
%% @doc Zone handler.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_zones).
-behaviour(gen_server).
-export([start_link/4, stop/1, setid/1, enter/2, leave/2, get_all_players/2, broadcast/3]). %% API.
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
-record(state, {
setid = 0 :: integer(),
objects = [] :: list(),
indexes = [] :: list(),
targets = [] :: list(),
players = [] :: list(),
freelids = [] :: list()
}).
%% API.
%% @spec start_link(UniID, QuestID, ZoneID, ZoneData) -> {ok,Pid::pid()}
start_link(UniID, QuestID, ZoneID, ZoneData) ->
gen_server:start_link(?MODULE, [UniID, QuestID, ZoneID, ZoneData], []).
%% @spec stop(Pid) -> stopped
stop(Pid) ->
gen_server:call(Pid, stop).
setid(Pid) ->
gen_server:call(Pid, setid).
enter(Pid, GID) ->
gen_server:call(Pid, {enter, GID}).
leave(undefined, _GID) ->
ok;
leave(Pid, GID) ->
gen_server:call(Pid, {leave, GID}).
get_all_players(Pid, ExcludeGID) ->
gen_server:call(Pid, {get_all_players, ExcludeGID}).
broadcast(Pid, FromGID, Packet) ->
gen_server:cast(Pid, {broadcast, FromGID, Packet}).
%% gen_server.
init([UniID, QuestID, ZoneID, ZoneData]) ->
SetID = rand_setid(proplists:get_value(sets, ZoneData, [100])),
Set = egs_quests_db:set(QuestID, ZoneID, SetID),
Objects = create_units(Set),
{Indexes, Targets} = index_objects(Objects),
FreeLIDs = lists:seq(0, 1023),
{ok, #state{setid=SetID, objects=Objects, indexes=Indexes, targets=Targets, freelids=FreeLIDs}}.
handle_call(setid, _From, State) ->
{reply, State#state.setid, State};
handle_call({enter, GID}, _From, State) ->
[LID|FreeLIDs] = State#state.freelids,
egs_users:set_zone(GID, self(), LID),
{ok, Spawn} = egs_users:read(GID),
egs_users:broadcast({egs, player_spawn, Spawn}, players_gid(State#state.players)),
{reply, LID, State#state{players=[{GID, LID}|State#state.players], freelids=FreeLIDs}};
handle_call({leave, GID}, _From, State) ->
{_, LID} = lists:keyfind(GID, 1, State#state.players),
Players = lists:delete({GID, LID}, State#state.players),
{ok, Spawn} = egs_users:read(GID),
egs_users:broadcast({egs, player_unspawn, Spawn}, players_gid(Players)),
{reply, ok, State#state{players=Players, freelids=[LID|State#state.freelids]}};
handle_call({get_all_players, ExcludeGID}, _From, State) ->
{reply, lists:delete(ExcludeGID, players_gid(State#state.players)), State};
handle_call(stop, _From, State) ->
{stop, normal, stopped, State};
handle_call(_Request, _From, State) ->
{reply, ignored, State}.
handle_cast({broadcast, FromGID, Packet}, State) ->
PlayersGID = lists:delete(FromGID, players_gid(State#state.players)),
egs_users:broadcast({egs, cast, Packet}, PlayersGID),
{noreply, State};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% Internal.
%% @doc Return a random setid from a list of chances per set.
rand_setid(Sets) ->
N = crypto:rand_uniform(1, lists:sum(Sets)),
rand_setid(N, Sets, 0).
rand_setid(N, [Set|_Tail], I) when N < Set ->
I;
rand_setid(N, [Set|Tail], I) ->
rand_setid(N - Set, Tail, I + 1).
%% @doc Create the objects for all units in a set.
create_units(Set) ->
create_units(Set, 0, []).
create_units([], _MapNb, Acc) ->
lists:flatten(lists:reverse(Acc));
create_units([{{map, _MapID}, Groups}|Tail], MapNb, Acc) ->
MapObjects = create_groups(Groups, MapNb),
create_units(Tail, MapNb + 1, [MapObjects|Acc]).
%% @doc Create the objects for all groups in a unit.
create_groups(Groups, MapNb) ->
create_groups(Groups, MapNb, 0, []).
create_groups([], _MapNb, _GroupNb, Acc) ->
lists:flatten(lists:reverse(Acc));
create_groups([Objects|Tail], MapNb, GroupNb, Acc) ->
GroupObjects = create_objects(Objects, MapNb, GroupNb),
create_groups(Tail, MapNb, GroupNb + 1, [GroupObjects|Acc]).
%% @doc Create the given objects.
create_objects(Objects, MapNb, GroupNb) ->
create_objects(Objects, MapNb, GroupNb, 0, []).
create_objects([], _MapNb, _GroupNb, _ObjectNb, Acc) ->
lists:reverse(Acc);
create_objects([{ObjType, ObjPos, ObjRot, ObjParams}|Tail], MapNb, GroupNb, ObjectNb, Acc) ->
Object = create_object(ObjType, ObjPos, ObjRot, ObjParams),
create_objects(Tail, MapNb, GroupNb, ObjectNb + 1, [{{MapNb, GroupNb, ObjectNb}, Object}|Acc]).
%% @doc Create the given object.
create_object(Type, Pos, Rot, Params) ->
M = list_to_existing_atom(lists:flatten(["egs_obj_", atom_to_list(Type)])),
M:init(Pos, Rot, Params).
%% @doc Build a list of object indexes and targets based on the list of objects.
index_objects(Objects) ->
index_objects(Objects, 0, [], 1024, []).
index_objects([], _Index, IndexesAcc, _Target, TargetsAcc) ->
{lists:reverse(IndexesAcc), lists:reverse(TargetsAcc)};
index_objects([{Key, Object}|Tail], Index, IndexesAcc, Target, TargetsAcc) ->
M = element(1, Object),
Attrs = M:module_info(attributes),
{Index2, IndexesAcc2} = case lists:keyfind(is_indexed, 1, Attrs) of
{_, [true]} -> {Index + 1, [{Index, Key}|IndexesAcc]};
{_, [false]} -> {Index, IndexesAcc}
end,
{Target2, TargetsAcc2} = case lists:keyfind(is_target, 1, Attrs) of
{_, [true]} -> {Target + 1, [{Target, Key}|TargetsAcc]};
{_, [false]} -> {Target, TargetsAcc}
end,
index_objects(Tail, Index2, IndexesAcc2, Target2, TargetsAcc2).
%% @doc Return a list of GID from a list of {GID, LID}.
players_gid(Players) ->
players_gid(Players, []).
players_gid([], Acc) ->
Acc;
players_gid([{GID, _LID}|Tail], Acc) ->
players_gid(Tail, [GID|Acc]).

View File

@ -0,0 +1,47 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin.
%% @doc Supervisor for the egs_zones gen_server.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_zones_sup).
-behaviour(supervisor).
-export([start_link/0, start_zone/4]). %% API.
-export([init/1]). %% supervisor.
-define(SUPERVISOR, ?MODULE).
%% API.
-spec start_link() -> {ok, pid()}.
start_link() ->
supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []).
-spec start_zone(egs:uniid(), egs:questid(), egs:zoneid(), tuple())
-> {ok, pid()}.
start_zone(UniID, QuestID, ZoneID, ZoneData) ->
supervisor:start_child(?SUPERVISOR, [UniID, QuestID, ZoneID, ZoneData]).
%% supervisor.
-spec init([]) -> {ok, {{simple_one_for_one, 0, 1}, [{egs_zones,
{egs_zones, start_link, []}, temporary, brutal_kill,
worker, [egs_zones]}]}}.
init([]) ->
{ok, {{simple_one_for_one, 0, 1}, [{egs_zones,
{egs_zones, start_link, []}, temporary, brutal_kill,
worker, [egs_zones]}]}}.

View File

@ -0,0 +1,33 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin.
%% @doc Chair object.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_obj_chair).
-export([init/3]).
%% @todo Real values.
-is_indexed(true).
-is_target(true).
-record(egs_obj_chair, {
pos :: {X :: float(), Y :: float(), Z :: float()},
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
}).
init(Pos, Rot, Params) ->
#egs_obj_chair{pos=Pos, rot=Rot}.

View File

@ -0,0 +1,33 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin.
%% @doc Door object.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_obj_door).
-export([init/3]).
%% @todo Real values.
-is_indexed(true).
-is_target(false).
-record(egs_obj_door, {
pos :: {X :: float(), Y :: float(), Z :: float()},
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
}).
init(Pos, Rot, Params) ->
#egs_obj_door{pos=Pos, rot=Rot}.

View File

@ -0,0 +1,33 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin.
%% @doc Entrance object.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_obj_entrance).
-export([init/3]).
%% @todo Real values.
-is_indexed(false).
-is_target(false).
-record(egs_obj_entrance, {
pos :: {X :: float(), Y :: float(), Z :: float()},
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
}).
init(Pos, Rot, Params) ->
#egs_obj_entrance{pos=Pos, rot=Rot}.

View File

@ -0,0 +1,33 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin.
%% @doc Exit object.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_obj_exit).
-export([init/3]).
%% @todo Real values.
-is_indexed(false).
-is_target(true).
-record(egs_obj_exit, {
pos :: {X :: float(), Y :: float(), Z :: float()},
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
}).
init(Pos, Rot, Params) ->
#egs_obj_exit{pos=Pos, rot=Rot}.

View File

@ -0,0 +1,33 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin.
%% @doc Invisible block object.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_obj_invisible_block).
-export([init/3]).
%% @todo Real values.
-is_indexed(false).
-is_target(false).
-record(egs_obj_invisible_block, {
pos :: {X :: float(), Y :: float(), Z :: float()},
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
}).
init(Pos, Rot, Params) ->
#egs_obj_invisible_block{pos=Pos, rot=Rot}.

View File

@ -0,0 +1,33 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin.
%% @doc Label object.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_obj_label).
-export([init/3]).
%% @todo Real values.
-is_indexed(false).
-is_target(false).
-record(egs_obj_label, {
pos :: {X :: float(), Y :: float(), Z :: float()},
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
}).
init(Pos, Rot, Params) ->
#egs_obj_label{pos=Pos, rot=Rot}.

View File

@ -0,0 +1,33 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin.
%% @doc NPC object.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_obj_npc).
-export([init/3]).
%% @todo Real values.
-is_indexed(true).
-is_target(true).
-record(egs_obj_npc, {
pos :: {X :: float(), Y :: float(), Z :: float()},
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
}).
init(Pos, Rot, Params) ->
#egs_obj_npc{pos=Pos, rot=Rot}.

View File

@ -0,0 +1,33 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin.
%% @doc PP cube object.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_obj_pp_cube).
-export([init/3]).
%% @todo Real values.
-is_indexed(false).
-is_target(false).
-record(egs_obj_pp_cube, {
pos :: {X :: float(), Y :: float(), Z :: float()},
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
}).
init(Pos, Rot, Params) ->
#egs_obj_pp_cube{pos=Pos, rot=Rot}.

View File

@ -0,0 +1,33 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin.
%% @doc Sensor object.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_obj_sensor).
-export([init/3]).
%% @todo Real values.
-is_indexed(true).
-is_target(true).
-record(egs_obj_sensor, {
pos :: {X :: float(), Y :: float(), Z :: float()},
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
}).
init(Pos, Rot, Params) ->
#egs_obj_sensor{pos=Pos, rot=Rot}.

View File

@ -0,0 +1,33 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin.
%% @doc Static model object.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_obj_static_model).
-export([init/3]).
%% @todo Real values.
-is_indexed(false).
-is_target(false).
-record(egs_obj_static_model, {
pos :: {X :: float(), Y :: float(), Z :: float()},
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
}).
init(Pos, Rot, Params) ->
#egs_obj_static_model{pos=Pos, rot=Rot}.

View File

@ -0,0 +1,33 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin.
%% @doc Type counter object.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_obj_type_counter).
-export([init/3]).
%% @todo Real values.
-is_indexed(true).
-is_target(true).
-record(egs_obj_type_counter, {
pos :: {X :: float(), Y :: float(), Z :: float()},
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
}).
init(Pos, Rot, Params) ->
#egs_obj_type_counter{pos=Pos, rot=Rot}.

View File

@ -0,0 +1,33 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin.
%% @doc Uni cube object.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_obj_uni_cube).
-export([init/3]).
%% @todo Real values.
-is_indexed(true).
-is_target(true).
-record(egs_obj_uni_cube, {
pos :: {X :: float(), Y :: float(), Z :: float()},
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
}).
init(Pos, Rot, Params) ->
#egs_obj_uni_cube{pos=Pos, rot=Rot}.

View File

@ -0,0 +1,96 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc General character functions.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(psu_characters).
-export([
character_user_to_binary/1
]).
-include("include/records.hrl").
%% @doc Convert a character tuple into a binary to be sent to clients.
%% Only contains the actually saved data, not the stats and related information.
%% @todo The name isn't very good anymore now that I switched #characters to #users.
character_tuple_to_binary(Tuple) ->
#users{name=Name, race=Race, gender=Gender, class=Class, appearance=Appearance} = Tuple,
RaceBin = race_atom_to_binary(Race),
GenderBin = gender_atom_to_binary(Gender),
ClassBin = class_atom_to_binary(Class),
AppearanceBin = egs_net:character_appearance_to_binary(Race, Appearance),
LevelsBin = egs_proto:build_char_level(Tuple),
<< Name/binary, RaceBin:8, GenderBin:8, ClassBin:8, AppearanceBin/binary, LevelsBin/binary >>.
%% @doc Convert a character tuple into a binary to be sent to clients.
%% Contains everything from character_tuple_to_binary/1 along with location, stats, SE and more.
%% @todo The second StatsBin seems unused. Not sure what it's for.
%% @todo Find out what the big block of 0 is at the end.
%% @todo The value before IntDir seems to be the player's current animation. 01 stand up, 08 ?, 17 normal sit
character_user_to_binary(User) ->
#users{gid=CharGID, lid=CharLID, npcid=NPCid, type=Type, level=Level, stats=Stats, currenthp=CurrentHP, maxhp=MaxHP,
pos={X, Y, Z, Dir}, area={QuestID, ZoneID, MapID}, entryid=EntryID, prev_area={PrevQuestID, PrevZoneID, PrevMapID}, prev_entryid=PrevEntryID} = User,
CharBin = character_tuple_to_binary(User),
StatsBin = stats_tuple_to_binary(Stats),
EXPNextLevel = 100,
EXPCurrentLevel = 0,
IntDir = trunc(Dir * 182.0416),
TypeID = case Type of npc -> 16#00001d00; _ -> 16#00001200 end,
NPCStuff = case Type of npc -> 16#01ff; _ -> 16#0000 end,
<< TypeID:32, CharGID:32/little, 0:64, CharLID:16/little, 0:16, NPCStuff:16, NPCid:16/little, QuestID:32/little,
ZoneID:32/little, MapID:32/little, EntryID:16/little, 0:16,
16#0100:16, IntDir:16/little, X:32/little-float, Y:32/little-float, Z:32/little-float, 0:64,
PrevQuestID:32/little, PrevZoneID:32/little, PrevMapID:32/little, PrevEntryID:32/little,
CharBin/binary, EXPNextLevel:32/little, EXPCurrentLevel:32/little, MaxHP:32/little,
StatsBin/binary, 0:96, Level:32/little, StatsBin/binary, CurrentHP:32/little, MaxHP:32/little,
0:1344, 16#0000803f:32, 0:64, 16#0000803f:32, 0:64, 16#0000803f:32, 0:64, 16#0000803f:32, 0:64, 16#0000803f:32, 0:160, 16#0000803f:32, 0:352 >>.
%% @doc Convert a class atom into a binary to be sent to clients.
class_atom_to_binary(Class) ->
case Class of
hunter -> 12;
ranger -> 13;
force -> 14;
acro -> 15
end.
%% @doc Convert a gender atom into a binary to be sent to clients.
gender_atom_to_binary(Gender) ->
case Gender of
male -> 0;
female -> 1
end.
%% @doc Convert a race atom into a binary to be sent to clients.
race_atom_to_binary(Race) ->
case Race of
human -> 0;
newman -> 1;
cast -> 2;
beast -> 3
end.
%% @doc Convert the tuple of stats data into a binary to be sent to clients.
stats_tuple_to_binary(Tuple) ->
{stats, ATP, ATA, TP, DFP, EVP, MST, STA} = Tuple,
<< ATP:16/little, DFP:16/little, ATA:16/little, EVP:16/little,
STA:16/little, 0:16, TP:16/little, MST:16/little >>.

View File

@ -1,5 +1,5 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin.
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc Party gen_server.
%%
%% This file is part of EGS.

View File

@ -0,0 +1,4 @@
{deps, [
{erlson, ".*", {git, "https://github.com/alavrik/erlson.git", "HEAD"}}
]}.
{plugins, [erlson_rebar_plugin]}.

View File

@ -0,0 +1,12 @@
%%-*- mode: erlang -*-
{application, egs_net, [
{description, "EGS network layer."},
{vsn, "0.1.0"},
{modules, []},
{registered, []},
{applications, [
kernel,
stdlib,
cowboy
]}
]}.

1576
apps/egs_net/src/egs_net.erl Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
%% @doc List of folders where patched files are located.
{folders, [root, "DATA"]}.
%% @doc Files in the root folder.
{{folder, root}, [
]}.
%% @doc Files in the DATA folder.
{{folder, "DATA"}, [
"1ffd0db3e0b54048caff394e9c09eda8", %% crc.bin
"bb04cc8e1727288bd2a336d60040eff1" %% SmutFilter_J.bin
]}.

View File

@ -0,0 +1,3 @@
{deps, [
{cowboy, ".*", {git, "git://github.com/extend/cowboy.git", "HEAD"}}
]}.

View File

@ -0,0 +1,14 @@
%%-*- mode: erlang -*-
{application, egs_patch, [
{description, "EGS patch server"},
{vsn, "0.1.0"},
{modules, []},
{registered, []},
{applications, [
kernel,
stdlib,
cowboy
]},
{mod, {egs_patch_app, []}},
{env, []}
]}.

View File

@ -0,0 +1,47 @@
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_patch_app).
-behaviour(application).
-export([start/2, stop/1]). %% API.
-type application_start_type()
:: normal | {takeover, node()} | {failover, node()}.
%% API.
-spec start(application_start_type(), term()) -> {ok, pid()}.
start(_Type, _StartArgs) ->
{ok, PatchPorts} = application:get_env(patch_ports),
start_listeners(PatchPorts),
egs_patch_sup:start_link().
-spec stop(term()) -> ok.
stop(_State) ->
ok.
%% Internal.
-spec start_listeners([inet:ip_port()]) -> ok.
start_listeners([]) ->
ok;
start_listeners([Port|Tail]) ->
{ok, _Pid} = cowboy:start_listener({patch, Port}, 10,
cowboy_tcp_transport, [{port, Port}],
egs_patch_protocol, []),
start_listeners(Tail).

View File

@ -0,0 +1,175 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS patch files database and cache manager.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_patch_files_db).
-behavior(gen_server).
-export([start_link/0, stop/0, list/0, check/3, get_size/1, get_info/1, reload/0]). %% API.
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
%% Use the module name for the server's name.
-define(SERVER, ?MODULE).
-include_lib("kernel/include/file.hrl").
-record(state, {list_bin=[], files=[]}).
-record(file, {crc, size, folder, filename_bin, full_filename}).
%% API.
%% @spec start_link() -> {ok,Pid::pid()}
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%% @spec stop() -> stopped
stop() ->
gen_server:call(?SERVER, stop).
%% @spec list() -> binary()
list() ->
gen_server:call(?SERVER, list).
%% @spec check(FileNumber, CRC, Size) -> ok | invalid
check(FileNumber, CRC, Size) ->
gen_server:call(?SERVER, {check, FileNumber, CRC, Size}).
%% @spec get_size(FileNumber) -> Size
get_size(FileNumber) ->
gen_server:call(?SERVER, {get_size, FileNumber}).
%% @spec get_info(FileNumber) -> {CRC, Size, FilenameBin, FullFilename}
get_info(FileNumber) ->
gen_server:call(?SERVER, {get_info, FileNumber}).
%% @spec reload() -> ok
reload() ->
gen_server:cast(?SERVER, reload).
%% gen_server.
init([]) ->
{ok, build_state()}.
handle_call(list, _From, State=#state{list_bin=Bin}) ->
{reply, Bin, State};
handle_call({check, FileNumber, CRC, Size}, _From, State=#state{files=Files}) ->
File = proplists:get_value(FileNumber, Files),
case File of
#file{crc=CRC, size=Size} -> {reply, ok, State};
_Any -> {reply, invalid, State}
end;
handle_call({get_size, FileNumber}, _From, State=#state{files=Files}) ->
File = proplists:get_value(FileNumber, Files),
{reply, File#file.size, State};
handle_call({get_info, FileNumber}, _From, State=#state{files=Files}) ->
{reply, proplists:get_value(FileNumber, Files), State};
handle_call(stop, _From, State) ->
{stop, normal, stopped, State};
handle_call(_Request, _From, State) ->
{reply, ignored, State}.
handle_cast(reload, _State) ->
{noreply, build_state()};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% Internal.
build_state() ->
{ok, App} = application:get_application(),
PrivDir = code:priv_dir(App),
{ok, Terms} = file:consult([PrivDir, "/patch.conf"]),
Folders = proplists:get_value(folders, Terms),
{ListBin, Files} = build_list_bin(Folders, Terms, [PrivDir, "/files/"]),
#state{list_bin=ListBin, files=Files}.
%% The file number must start at 0.
build_list_bin(Folders, Terms, PatchDir) ->
build_list_bin(Folders, Terms, PatchDir, 0, [], []).
build_list_bin([], _Terms, _PatchDir, _N, Acc, FilesAcc) ->
Bin = list_to_binary(lists:reverse(Acc)),
Bin2 = << 16#08:32/little, 16#06:32/little, Bin/binary, 16#08:32/little, 16#08:32/little >>,
{Bin2, lists:flatten(FilesAcc)};
build_list_bin([Folder|Tail], Terms, PatchDir, N, Acc, FilesAcc) ->
Filenames = proplists:get_value({folder, Folder}, Terms),
{BinFiles, Files, N2} = build_files_bin(Folder, Filenames, PatchDir, N),
BinFiles2 = case Folder of
root -> BinFiles;
_Any ->
FolderBin = list_to_binary(Folder),
Padding = 8 * (64 - length(Folder)),
<< 16#48:32/little, 16#09:32/little, FolderBin/binary, 0:Padding,
BinFiles/binary, 16#08:32/little, 16#0a:32/little >>
end,
build_list_bin(Tail, Terms, PatchDir, N2, [BinFiles2|Acc], [Files|FilesAcc]).
build_files_bin(Folder, Filenames, PatchDir, N) ->
build_files_bin(Folder, Filenames, PatchDir, N, [], []).
build_files_bin(_Folder, [], _PatchDir, N, Acc, FilesAcc) ->
Bin = list_to_binary(lists:reverse(Acc)),
{Bin, FilesAcc, N};
build_files_bin(Folder, [Filename|Tail], PatchDir, N, Acc, FilesAcc) ->
FullFilename = case Folder of
root -> [PatchDir|Filename];
_Any -> [PatchDir,Folder,"/"|Filename]
end,
Size = file_get_size(FullFilename),
CRC = file_get_crc(FullFilename),
FilenameBin = list_to_binary(Filename),
Padding = 8 * (64 - length(Filename)),
FilenameBin2 = << FilenameBin/binary, 0:Padding >>,
Bin = << 16#4c:32/little, 16#07:32/little, N:32/little, FilenameBin2/binary >>,
build_files_bin(Folder, Tail, PatchDir, N + 1, [Bin|Acc], [{N, #file{crc=CRC, size=Size, folder=Folder, filename_bin=FilenameBin2, full_filename=FullFilename}}|FilesAcc]).
file_get_size(Filename) ->
{ok, FileInfo} = file:read_file_info(Filename),
FileInfo#file_info.size.
file_get_crc(Filename) ->
{ok, IoDevice} = file:open(Filename, [read, raw, binary]),
case file:read(IoDevice, 524288) of
eof -> 0;
{ok, Data} ->
CRC = erlang:crc32(Data),
file_get_crc(IoDevice, CRC)
end.
file_get_crc(IoDevice, CRC) ->
case file:read(IoDevice, 524288) of
{ok, Data} ->
CRC2 = erlang:crc32(CRC, Data),
file_get_crc(IoDevice, CRC2);
eof ->
file:close(IoDevice),
CRC
end.

View File

@ -0,0 +1,218 @@
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
%% @doc Cowboy protocol module for the patch server.
-module(egs_patch_protocol).
-export([start_link/4, init/2]).
%% @todo Move that in a configuration file.
-define(TIMEOUT, 5000).
-record(state, {
socket :: inet:socket(),
transport :: module(),
buffer = <<>> :: binary(),
files = [] :: list(integer())
}).
-type state() :: #state{}.
-type cmd() :: 0..16#14.
-type cmd_size() :: 0..16#ffffffff.
-spec start_link(pid(), inet:socket(), module(), []) -> {ok, pid()}.
start_link(_ListenerPid, Socket, Transport, []) ->
Pid = spawn_link(?MODULE, init, [Socket, Transport]),
{ok, Pid}.
-spec init(inet:socket(), module()) -> ok | closed.
init(Socket, Transport) ->
State = #state{socket=Socket, transport=Transport},
send_01(State),
wait_hello(State).
-spec next(state()) -> {ok, cmd(), cmd_size(), binary(), state()} | closed.
next(State=#state{buffer= << Size:32/little, Cmd:16/little, _:16, Rest/bits >>})
when byte_size(Rest) + 8 >= Size ->
Size2 = Size - 8,
<< Data:Size2/binary, Buffer/bits >> = Rest,
{ok, Cmd, Size, Data, State#state{buffer=Buffer}};
next(State=#state{socket=Socket, transport=Transport, buffer=Buffer}) ->
Transport:setopts(Socket, [{active, once}]),
{OK, Closed, Error} = Transport:messages(),
receive
{OK, Socket, Data} -> next(State#state{
buffer= << Buffer/binary, Data/binary >>});
{Closed, Socket} -> closed;
{Error, Socket, _Reason} -> closed
after ?TIMEOUT ->
closed
end.
-spec wait_hello(state()) -> ok | closed.
wait_hello(State) ->
case next(State) of
{ok, 16#14, 52, Data, State2} -> handle_hello(State2, Data);
closed -> closed
end.
-spec handle_hello(state(), binary()) -> ok | closed.
handle_hello(State=#state{socket=Socket, transport=Transport}, Data) ->
<< 16#e44c0915:32, UnknownA:32/little,
UnknownB:32/little, UnknownC:32/little, UnknownD:32/little,
_GameVersion:32/little, UnknownE:32/little, 0:128 >> = Data,
io:format("patch hello: ~p ~p ~p ~p ~p~n",
[UnknownA, UnknownB, UnknownC, UnknownD, UnknownE]),
ListBin = egs_patch_files_db:list(),
Transport:send(Socket, ListBin),
wait_fileinfo_begin(State).
-spec wait_fileinfo_begin(state()) -> ok | closed.
wait_fileinfo_begin(State) ->
case next(State) of
{ok, 16#0c, 8, <<>>, State2} -> wait_fileinfo(State2);
closed -> closed
end.
-spec wait_fileinfo(state()) -> ok | closed.
wait_fileinfo(State) ->
case next(State) of
{ok, 16#0d, 20, Data, State2} -> handle_fileinfo(State2, Data);
{ok, 16#0e, 8, <<>>, State2} -> handle_fileinfo_end(State2);
closed -> closed
end.
-spec handle_fileinfo(state(), binary()) -> ok | closed.
handle_fileinfo(State=#state{files=Files}, Data) ->
<< FileNumber:32/little, CRC:32/little, Size:32/little >> = Data,
case egs_patch_files_db:check(FileNumber, CRC, Size) of
ok -> wait_fileinfo(State);
invalid -> wait_fileinfo(State#state{files=[FileNumber|Files]})
end.
-spec handle_fileinfo_end(state()) -> ok.
handle_fileinfo_end(State=#state{files=[]}) ->
handle_update_complete(State);
handle_fileinfo_end(State=#state{files=Files}) ->
Files2 = lists:reverse(Files),
State2 = State#state{files=Files2},
send_0f(State2),
handle_update(State2, root, Files2).
-spec handle_update(state(), root | string(), list(integer())) -> ok.
handle_update(State, _CurrentDir, []) ->
handle_update_complete(State);
handle_update(State=#state{}, CurrentDir, [FileNumber|Tail]) ->
{file, _CRC, Size, Dir, FilenameBin, FullFilename}
= egs_patch_files_db:get_info(FileNumber),
change_directory(State, CurrentDir, Dir),
send_10(State, Size, FilenameBin),
sendfile(State, FullFilename),
send_12(State),
handle_update(State, Dir, Tail).
-spec change_directory(state(), root | string(), root | string()) -> ok.
change_directory(_State, CurrentDir, CurrentDir) ->
ok;
change_directory(State, _CurrentDir, root) ->
send_0a(State);
change_directory(State, root, Dir) ->
send_09(State, Dir).
-spec sendfile(state(), string()) -> ok.
sendfile(State, Filename) ->
{ok, IoDevice} = file:open(Filename, [read, raw, binary]),
sendfile(State, IoDevice, 0).
sendfile(State, IoDevice, N) ->
case file:read(IoDevice, 24576) of
{ok, Data} ->
send_11(State, Data, N),
sendfile(State, IoDevice, N + 1);
eof ->
ok = file:close(IoDevice)
end.
-spec handle_update_complete(state()) -> ok.
handle_update_complete(State=#state{socket=Socket, transport=Transport}) ->
send_13(State),
ok = Transport:close(Socket).
-spec send_01(state()) -> ok.
%% @doc Hello command sent on connect. Encryption is disabled.
send_01(#state{socket=Socket, transport=Transport}) ->
Bin = << 16#28:32/little, 16#01:32/little,
16#8b9f2dfa:32, 0:96, 1:32/little, 0:96 >>,
ok = Transport:send(Socket, Bin).
-spec send_09(state(), string()) -> ok.
%% @doc Change folder command.
send_09(#state{socket=Socket, transport=Transport}, Folder) ->
FolderBin = list_to_binary(Folder),
Padding = 8 * (64 - length(Folder)),
Bin = << 16#48:32/little, 16#09:32/little, FolderBin/binary, 0:Padding >>,
ok = Transport:send(Socket, Bin).
-spec send_0a(state()) -> ok.
%% @doc Back to root folder command.
send_0a(#state{socket=Socket, transport=Transport}) ->
Bin = << 16#8:32/little, 16#0a:32/little >>,
ok = Transport:send(Socket, Bin).
-spec send_0f(state()) -> ok.
%% @doc General update information command. Prepare the update screen.
send_0f(#state{socket=Socket, transport=Transport, files=Files}) ->
Size = lists:foldl(
fun(N, Acc) -> Acc + egs_patch_files_db:get_size(N) end, 0, Files),
NbFiles = length(Files),
Bin = << 16#10:32/little, 16#0f:32/little,
Size:32/little, NbFiles:32/little >>,
ok = Transport:send(Socket, Bin).
-spec send_10(state(), non_neg_integer(), binary()) -> ok.
%% @doc File update begin command. Prepare sending an individual file.
send_10(#state{socket=Socket, transport=Transport}, Size, FilenameBin) ->
Bin = << 16#50:32/little, 16#10:32/little, 0:32,
Size:32/little, FilenameBin/binary >>,
ok = Transport:send(Socket, Bin).
-spec send_11(state(), binary(), non_neg_integer()) -> ok.
%% @doc Command to send a file fragment.
send_11(#state{socket=Socket, transport=Transport}, Data, N) ->
DataSize = byte_size(Data),
Padding = case DataSize rem 4 of
0 -> 0;
Rem -> 8 * (4 - Rem)
end,
Data2 = << Data/binary, 0:Padding >>,
DataSize2 = DataSize + Padding div 8,
Size = DataSize2 + 16#14,
CRC = erlang:crc32(Data2),
Bin = << Size:32/little, 16#11:32/little, N:32/little,
CRC:32/little, DataSize:32/little, Data2/binary >>,
ok = Transport:send(Socket, Bin).
-spec send_12(state()) -> ok.
%% @doc File update end command.
send_12(#state{socket=Socket, transport=Transport}) ->
Bin = << 16#8:32/little, 16#12:32/little >>,
ok = Transport:send(Socket, Bin).
-spec send_13(state()) -> ok.
%% @doc Update complete command. Followed by the server closing the connection.
send_13(#state{socket=Socket, transport=Transport}) ->
Bin = << 16#8:32/little, 16#13:32/little >>,
ok = Transport:send(Socket, Bin).

View File

@ -0,0 +1,32 @@
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_patch_sup).
-behaviour(supervisor).
-export([start_link/0]). %% API.
-export([init/1]). %% Supervisor.
-spec start_link() -> {ok, pid()}.
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
-spec init([]) -> {ok, {{one_for_one, 10, 10}, [supervisor:child_spec(), ...]}}.
init([]) ->
Procs = [{egs_patch_files_db, {egs_patch_files_db, start_link, []},
permanent, 5000, worker, [egs_patch_files_db]}],
{ok, {{one_for_one, 10, 10}, Procs}}.

0
apps/egs_store/priv/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,13 @@
%%-*- mode: erlang -*-
{application, egs_store, [
{description, "EGS storage layer."},
{vsn, "0.1.0"},
{modules, []},
{registered, []},
{applications, [
kernel,
stdlib
]},
{mod, {egs_store_app, []}},
{env, []}
]}.

View File

@ -0,0 +1,92 @@
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_store).
-behaviour(gen_server).
-export([start_link/0,
load_character/2, load_characters/2, save_character/3]). %% API.
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]). %% gen_server.
-define(SERVER, ?MODULE).
-define(ACCOUNTS_TBL, accounts_tbl).
-define(ACCOUNTS_VSN, 1).
-define(CHARACTERS_TBL, characters_tbl).
-define(CHARACTERS_VSN, 1).
%% API.
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
load_character(GID, Slot) ->
gen_server:call(?SERVER, {load_character, GID, Slot}).
load_characters(GID, Slots) ->
gen_server:call(?SERVER, {load_characters, GID, Slots}).
save_character(GID, Slot, Data) ->
gen_server:call(?SERVER, {save_character, GID, Slot, Data}).
%% gen_server.
init([]) ->
{ok, App} = application:get_application(),
PrivDir = code:priv_dir(App),
AccountsFile = PrivDir ++ "/accounts.tbl",
CharactersFile = PrivDir ++ "/characters.tbl",
{ok, ?ACCOUNTS_TBL} = dets:open_file(?ACCOUNTS_TBL,
[{file, AccountsFile}]),
io:format("accounts tbl:~n~p~n~n", [dets:info(?ACCOUNTS_TBL)]),
{ok, ?CHARACTERS_TBL} = dets:open_file(?CHARACTERS_TBL,
[{file, CharactersFile}]),
io:format("characters tbl:~n~p~n~n", [dets:info(?CHARACTERS_TBL)]),
{ok, undefined}.
handle_call({load_character, GID, Slot}, _From, State) ->
case dets:lookup(?CHARACTERS_TBL, {GID, Slot}) of
[{{GID, Slot}, Version, Data}] ->
{reply, {ok, Version, Data}, State};
[] ->
{reply, {error, notfound}, State}
end;
handle_call({load_characters, GID, Slots}, _From, State) ->
Characters = lists:map(fun(Slot) ->
case dets:lookup(?CHARACTERS_TBL, {GID, Slot}) of
[{{GID, Slot}, Version, Data}] ->
{Version, Data};
[] ->
notfound
end
end, Slots),
{reply, {ok, Characters}, State};
handle_call({save_character, GID, Slot, Data}, _From, State) ->
ok = dets:insert(?CHARACTERS_TBL, {{GID, Slot}, ?CHARACTERS_VSN, Data}),
{reply, ok, State};
handle_call(_Request, _From, State) ->
{reply, ignored, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.

View File

@ -0,0 +1,31 @@
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_store_app).
-behaviour(application).
-export([start/2, stop/1]). %% API.
-type application_start_type()
:: normal | {takeover, node()} | {failover, node()}.
%% API.
-spec start(application_start_type(), any()) -> {ok, pid()}.
start(_Type, _StartArgs) ->
egs_store_sup:start_link().
-spec stop(any()) -> ok.
stop(_State) ->
ok.

View File

@ -0,0 +1,31 @@
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_store_sup).
-behaviour(supervisor).
-export([start_link/0]). %% API.
-export([init/1]). %% Supervisor.
-spec start_link() -> {ok, pid()}.
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
-spec init([]) -> {ok, {{one_for_one, 10, 10}, [supervisor:child_spec(), ...]}}.
init([]) ->
{ok, {{one_for_one, 10, 10}, [
{egs_store, {egs_store, start_link, []},
permanent, 5000, worker, [egs_store]}
]}}.

173
apps/prs/c_src/prs.c Normal file
View File

@ -0,0 +1,173 @@
/*
Original code from the PRSutil project.
http://www.fuzziqersoftware.com/projects.php
*/
#include <string.h>
typedef struct {
unsigned char bitpos;
unsigned char* controlbyteptr;
unsigned char* srcptr_orig;
unsigned char* dstptr_orig;
unsigned char* srcptr;
unsigned char* dstptr; } PRS_COMPRESSOR;
void prs_put_control_bit(PRS_COMPRESSOR* pc,unsigned char bit)
{
*pc->controlbyteptr = *pc->controlbyteptr >> 1;
*pc->controlbyteptr |= ((!!bit) << 7);
pc->bitpos++;
if (pc->bitpos >= 8)
{
pc->bitpos = 0;
pc->controlbyteptr = pc->dstptr;
pc->dstptr++;
}
}
void prs_put_control_bit_nosave(PRS_COMPRESSOR* pc,unsigned char bit)
{
*pc->controlbyteptr = *pc->controlbyteptr >> 1;
*pc->controlbyteptr |= ((!!bit) << 7);
pc->bitpos++;
}
void prs_put_control_save(PRS_COMPRESSOR* pc)
{
if (pc->bitpos >= 8)
{
pc->bitpos = 0;
pc->controlbyteptr = pc->dstptr;
pc->dstptr++;
}
}
void prs_put_static_data(PRS_COMPRESSOR* pc,unsigned char data)
{
*pc->dstptr = data;
pc->dstptr++;
}
unsigned char prs_get_static_data(PRS_COMPRESSOR* pc)
{
unsigned char data = *pc->srcptr;
pc->srcptr++;
return data;
}
/* **************************************************** */
void prs_init(PRS_COMPRESSOR* pc,void* src,void* dst)
{
pc->bitpos = 0;
pc->srcptr = (unsigned char*)src;
pc->srcptr_orig = (unsigned char*)src;
pc->dstptr = (unsigned char*)dst;
pc->dstptr_orig = (unsigned char*)dst;
pc->controlbyteptr = pc->dstptr;
pc->dstptr++;
}
void prs_finish(PRS_COMPRESSOR* pc)
{
prs_put_control_bit(pc,0);
prs_put_control_bit(pc,1);
if (pc->bitpos != 0)
{
*pc->controlbyteptr = ((*pc->controlbyteptr << pc->bitpos) >> 8);
}
prs_put_static_data(pc,0);
prs_put_static_data(pc,0);
}
void prs_rawbyte(PRS_COMPRESSOR* pc)
{
prs_put_control_bit_nosave(pc,1);
prs_put_static_data(pc,prs_get_static_data(pc));
prs_put_control_save(pc);
}
void prs_shortcopy(PRS_COMPRESSOR* pc,int offset,unsigned char size)
{
size -= 2;
prs_put_control_bit(pc,0);
prs_put_control_bit(pc,0);
prs_put_control_bit(pc,(size >> 1) & 1);
prs_put_control_bit_nosave(pc,size & 1);
prs_put_static_data(pc,offset & 0xFF);
prs_put_control_save(pc);
}
void prs_longcopy(PRS_COMPRESSOR* pc,int offset,unsigned char size)
{
if (size <= 9)
{
prs_put_control_bit(pc,0);
prs_put_control_bit_nosave(pc,1);
prs_put_static_data(pc,((offset << 3) & 0xF8) | ((size - 2) & 0x07));
prs_put_static_data(pc,(offset >> 5) & 0xFF);
prs_put_control_save(pc);
} else {
prs_put_control_bit(pc,0);
prs_put_control_bit_nosave(pc,1);
prs_put_static_data(pc,(offset << 3) & 0xF8);
prs_put_static_data(pc,(offset >> 5) & 0xFF);
prs_put_static_data(pc,size - 1);
prs_put_control_save(pc);
}
}
void prs_copy(PRS_COMPRESSOR* pc,int offset,unsigned char size)
{
if ((offset > -0x100) && (size <= 5))
prs_shortcopy(pc,offset,size);
else
prs_longcopy(pc,offset,size);
pc->srcptr += size;
}
/* **************************************************** */
unsigned long prs_compress(void* source,void* dest,unsigned long size)
{
PRS_COMPRESSOR pc;
int x,y;
unsigned long xsize;
int lsoffset,lssize;
prs_init(&pc,source,dest);
for (x = 0; x < size; x++)
{
lsoffset = lssize = xsize = 0;
for (y = x - 3; (y > 0) && (y > (x - 0x1FF0)) && (xsize < 255); y--)
{
xsize = 3;
if (!memcmp((void*)((unsigned long)source + y),(void*)((unsigned long)source + x),xsize))
{
do xsize++;
while (!memcmp((void*)((unsigned long)source + y),
(void*)((unsigned long)source + x),
xsize) &&
(xsize < 256) &&
((y + xsize) < x) &&
((x + xsize) <= size)
);
xsize--;
if (xsize > lssize)
{
lsoffset = -(x - y);
lssize = xsize;
}
}
}
if (lssize == 0)
{
prs_rawbyte(&pc);
} else {
prs_copy(&pc,lsoffset,lssize);
x += (lssize - 1);
}
}
prs_finish(&pc);
return pc.dstptr - pc.dstptr_orig;
}

74
apps/prs/c_src/prs_drv.c Normal file
View File

@ -0,0 +1,74 @@
/*
@author Loïc Hoguin <essen@dev-extend.eu>
@copyright 2010-2011 Loïc Hoguin.
@doc PRS Erlang driver for EGS.
This file is part of EGS.
EGS is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
EGS is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with EGS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <erl_nif.h>
extern unsigned long prs_compress(unsigned char* source, unsigned char* dest, unsigned long size);
static ERL_NIF_TERM compress_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
ErlNifBinary srcbin;
ErlNifBinary destbin;
unsigned int size;
if (argc != 1)
return enif_make_badarg(env);
if (!enif_is_binary(env, argv[0]))
return enif_make_badarg(env);
if (!enif_inspect_binary(env, argv[0], &srcbin))
return enif_make_badarg(env);
if (!enif_alloc_binary((9 * srcbin.size) / 8 + 2, &destbin))
return enif_make_badarg(env);
size = prs_compress(srcbin.data, destbin.data, srcbin.size);
enif_realloc_binary(&destbin, size);
return enif_make_binary(env, &destbin);
}
static int load(ErlNifEnv* env, void** priv, ERL_NIF_TERM load_info)
{
return 0;
}
static int reload(ErlNifEnv* env, void** priv, ERL_NIF_TERM load_info)
{
return 0;
}
static int upgrade(ErlNifEnv* env, void** priv, void** old_priv, ERL_NIF_TERM load_info)
{
return 0;
}
static void unload(ErlNifEnv* env, void* priv)
{
return;
}
static ErlNifFunc nif_funcs[] = {
{"compress", 1, compress_nif}
};
ERL_NIF_INIT(prs, nif_funcs, load, reload, upgrade, unload)

11
apps/prs/src/prs.app.src Normal file
View File

@ -0,0 +1,11 @@
%%-*- mode: erlang -*-
{application, prs, [
{description, "PRS compression library."},
{vsn, "0.1.0"},
{modules, []},
{registered, []},
{applications, [
kernel,
stdlib
]}
]}.

29
apps/prs/src/prs.erl Normal file
View File

@ -0,0 +1,29 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS file creation functions.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation, either version 3 of the
%% License, or (at your option) any later version.
%%
%% EGS is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(prs).
-export([init/0, compress/1]).
-on_load(init/0).
init() ->
PrivDir = code:priv_dir(prs),
erlang:load_nif(PrivDir ++ "/prs_drv", 0).
compress(_SrcBin) ->
erlang:nif_error(not_loaded).

View File

@ -1 +0,0 @@
*

View File

@ -1 +0,0 @@
*

View File

@ -1 +0,0 @@
*

View File

@ -1 +0,0 @@
*

View File

@ -1 +0,0 @@
*

1
docs/protocol/README Normal file
View File

@ -0,0 +1 @@
All the files in this folder are research released in the public domain.

419
docs/protocol/commands.txt Normal file
View File

@ -0,0 +1,419 @@
[ Research Status / Importance / Occurences / Command / Channels / Description ]
Characters
!* C 6 0101 B--
WIP C 7 0102 BCS animation state change (movement, sit, stand up, trigger unicube, talk to npc...)
!* C 6 0104 B-- sit position change (floor and chairs); goggle position change
WIP A 7 0105 -CS weapon/armor equip, item use/drop and more; request/reply
? 6 0106 -CS
!* C 4 0107 B-- sit on chair, get on vehicle
? 6 0108 --S
! ? 4 0109 -C- weapon/item palette change
WIP B 5 010a -CS shop listing request/reply
OK ? 5 010b -C- player enter area (probably broadcast spawn here)
WIP A 6 010d --S character details (excluding location); similar to 0201
!* C 7 010f B-- talk to npc; set trap, boom trap
WIP A 5 0110 -C- triggered event (death, type change, online status change...)
OK A 6 0111 --S trigger event
C 3 0112 --S
!* B 4 0113 --S types capabilities list
OK A 7 0115 --S update EXP/money...
OK A 5 0117 --S revive
? 5 0119 -CS (probably make the RCSM shoot)
System
OK A 5 0200 --S zone init
WIP A 6 0201 --S character details (including location); similar to 010d
OK A 5 0202 --S hello
OK A 5 0203 --S spawn character
OK B 5 0204 --S unspawn character
OK A 6 0205 --S load map (related to 0806)
OK A 5 0208 --S end of loading (related to 0808)
OK A 4 020b -C- character selection: select slot
OK ? 5 020c -C- (despite the channel, sent by the server; channel 2)
OK A 4 020d -C- game auth: token authentication request
OK A 5 020e --S quest file transfer
OK A 5 020f --S zone file transfer
OK C 5 0210 --S current unix time
OK C 5 0215 --S
OK A 5 0216 --S login: forward to game server
OK A 5 0217 -C- login: game server address request
OK ? 5 0219 -C- login: password authentication request
OK A 5 021b --S (character selection: end)
OK ? 5 021c -C- (character selection: end) reply
OK A 4 021d -C- uni cube request
OK A 4 021e --S uni cube reply
OK A 4 021f -C- uni cube selection
! ? 4 0220 -C- (uni cube related, sent by the client? thought it was sent by the server)
OK A 5 0222 --S current uni details
OK ? 5 0223 --S login: password authentication success
! ? 3 0224 --S disconnect user, reason given
OK ? 5 0225 --S login: motd reply
OK A 5 0226 -C- login: motd request (jp only): same as 023f (apparently uses channel 3)
OK ? 5 0227 -C- login: whether the motd (TOS on official) was accepted
OK ? 3 0228 --S display a notice: global message; rare mission get message
!* ? 5 022c --S (probably related to parties or NPCs)
-- - - 022d --S gameguard ping
-- - - 022e -C- gameguard pong
OK ? 4 0230 --S (universe change related, including room doors)
OK A 2 0231 --S send user to website (used on wrong password)
WIP A 5 0233 --S list of other characters in zone/lobby/area, whichever
! ? 3 0234 -C- (probably related to 0235; probably partner card related)
? 3 0235 --S (probably related to 0234; probably partner card related; sends 1 GID and 4 card-like character infos)
OK A 5 0236 --S start zone handler (load objects and enable them)
! ? 4 0237 -C-
! ? 4 0238 --S (probably related to 0237)
OK A 5 023f -C- login: motd request (us only): same as 0226
! ? 3 0245 --S
! ? 5 0246 --S add or replace text strings
? 5 024b -C-
? ? 024f -C- (gam/gas dialog request; jp only)
? ? 0250 --S (gam/gas dialog reply; jp only)
? ? 025e --S (lumilass-related; jp only)
Chat
! B 2 0302 -C- shortcuts change
! B 5 0303 --S shortcuts list
OK A 6 0304 -CS message
! C 4 0305 --S sound
Missions
!* C 7 0402 -C- enemy event (safely ignored?)
!* C 6 0404 -C- object event
Movements
!** A 8 0503 B-S position change
!* C 7 050f B-- camera change
!* C 7 0514 B-- stay still
Area/State change
OK ? 5 0806 -C- (related to 0808)
OK A 5 0807 -C- area change
OK ? 5 0808 -C- (related to 0806)
WIP ? 4 080c -C- npc force invite-- NO, more like invite npc specific for this quest (in the file), probably kills NPC on zone change too (but not map change apparently)
OK ? 5 080d -C- (area change successful notice?)
OK C 5 080e -C- client/platform/chosen entrance info
OK ? 4 080f -C- (npc related or story related)
OK A 4 0811 -C- enter counter
OK A 4 0812 -C- leave counter
OK A 3 0813 -C- npc invite
OK ? 4 0814 -C- (mission area change successful notice?)
OK ? 5 0815 -C- (non-mission area change successful notice?)
OK C 4 0818 -C- cpu/gpu info
Items
? 5 0a01 --S
? 5 0a02 --S
? 3 0a03 --S
* A 4 0a04 --S npc inventory
OK A 5 0a05 --S (player inventory related; the LID is changed right before this command, or right before the previous 0215 command in the case of missions)
OK A 5 0a06 --S player inventory: list of unique items id
? 4 0a07 -CS (view stats/equip)
? 3 0a08 --S
** C 5 0a09 -CS (probably: receive item)
!* A 5 0a0a --S player inventory
? 2 0a0b --S
? 4 0a0e --S
? 4 0a0f --S
OK C 5 0a10 -C- item description request
OK C 5 0a11 --S item description reply
Packet fragments
OK A 6 0b03 --- fragment (no channel)
OK A 6 0b05 --- reception confirmed (no channel)
Quests
OK A 5 0c00 --S quest init
OK A 4 0c01 -C- mission start request
!* A 4 0c02 --S mission start reply (no 1020 or 1015 when rejected)
OK A 4 0c05 -C- counter quest files pack request
OK A 4 0c06 --S counter quest files pack transfer
OK A 3 0c07 -C- lobby transport request
OK A 3 0c08 --S lobby transport reply
* A 4 0c09 --S trial start; end (contains reward)
? 5 0c0a --S
? 4 0c0b --S
? 4 0c0c --S
OK ? 4 0c0d -C- (mission start and mission area change related)
OK A 3 0c0e -C- mission abort
OK A 4 0c0f -C- counter options request
OK A 4 0c10 --S counter options reply
!* ? 3 0c11 -C- (story mission related; probably related to 0c12)
!* ? 3 0c12 --S (probably related to 0c11)
? 3 0c13 -C- (probably related to 0c14)
? 3 0c14 -C- (probably related to 0c13)
Account/Flags
WIP A 5 0d01 --S character selection: selected character details
OK A 2 0d02 -C- character selection: create character
!* A 4 0d03 --S character selection: character list reply
!** C 5 0d04 -CS set flag
OK C 4 0d05 --S system flags list
OK A 4 0d06 -C- character selection: character list request
OK A 3 0d07 -C- options change
! ? 1 0d08 -C- (gbr info request, 1 per dialog)
? 1 0d09 --S (gbr info reply 2, 4, 5; during GBR only)
! ? 1 0d0a --S (gbr info reply 1, 3; many GBR 0d04 sent by the server before that; also sent when not in GBR)
Hits
WIP A 6 0e00 -C- hit
! ? 3 0e01 -C-
* A 7 0e07 --S hit reply
? 4 0e08 --S
Zones
!* C 2 0f00 -C- vehicle init
!* C 2 0f02 -C- enter vehicle
? 4 0f03 -C- (vehicle move?)
! ? 1 0f04 -C-
! ? 1 0f05 -C- (vehicle related)
(0f06 vehicle related?)
!* C 2 0f07 -C- (enter vehicle related)
! ? 2 0f09 -C- (vehicle related)
WIP A 6 0f0a -C- object event
Parties
! ? 3 1002 -C- (leave party request)
? 3 1003 --S
* A 4 1004 --S join event (add character info to the left of the screen)
!* ? 5 1005 --S
OK A 5 1006 --S trigger party event
OK ? 4 1007 -C- remove member request
? 3 100a -CS (invite other player to party; received invite)
? 3 100c -CS
OK A 5 100e --S character location
* ? 4 100f --S
? 4 1011 --S
? 2 1013 -C-
!* A 4 1015 --S mission start: quest file transfer (for mission details window)
* ? 4 1016 -CS (probably related to NPCs)
? 6 1017 -CS (related to NPC removal from party in story mission)
* ? 6 1019 -CS
* ? 3 101a --S
? 3 101b -C-
? 3 101c --- (channel 5; accept party invite)
C 1 101d -C- (set invite block)
? 3 101e --S
? 6 101f --S character level and hp update (on the left of the screen); sometimes sent in other places
OK A 4 1020 --S (mission start related)
OK ? 6 1022 --S character hp update (on the left of the screen)
Bosses
! ? 4 1101 -C-
? 3 1102 -C-
? 4 1103 -C-
? 4 1104 --S
? 3 1105 -C-
* ? 3 1106 -C-
? 3 1107 -C-
? 3 1108 --S
? 3 1109 --S
? 2 110a --S
? 2 110b --S
? 4 110c --S
? 3 110d --S
* ? 4 110e --S
? 4 110f --S
? 4 1110 --S
? 4 1111 --S
* ? 3 1112 -C- (probably related to 1113)
* ? 3 1113 --S (probably related to 1112)
? 3 1114 --S
? 2 1116 --S
? 1 1117 --S
? 2 1118 --S
? 4 1119 --S
? 3 111b --S
? 3 111c --S
? 2 111d --S
? 2 111e --S
? 3 111f --S
? 4 1120 --S
? 4 1121 --S
? 4 1122 --S
? 4 1123 -C-
? 2 1124 --S
Instances state
? 7 1201 --S (related to 0402; probably this enemy synchronization thing)
* A 6 1202 --S
OK A 5 1204 --S
!* A 6 1205 --S block/map event
* A 5 1206 --S
* A 4 1207 --S
* C 3 1208 --S (vehicle init related)
* C 3 120a --S (enter vehicle related)
? 5 120b --S
? 1 120c --S
* C 3 120f --S (enter vehicle related; reply for 0f07)
? 1 1210 --S
!* A 6 1211 --S (object event related)
* A 5 1212 --S
!* A 6 1213 --S object state on/off
C 1 1214 --S season on/off
* A 3 1215 --S (boss gate related)
** C 3 1216 --- (airboard rally race start; channel 0)
Player rooms
C 4 1304 -C-
C 2 1305 -C-
* C 4 1309 --S
C 4 130a --S
C 2 130b -C-
C 2 130c --S
C 6 130d -C-
C 6 130e --S
C 5 130f -C-
C 3 1310 --S
C 3 1312 --S
C 3 1313 -C-
C 3 1314 --S
C 2 1316 --S
C 5 1318 --S
C 2 1319 -C-
C 2 131a -C-
C 6 131b --S
C 3 131c --S
C 5 131e --S
C 3 131f --S
C 3 1320 --S
C 2 1321 --S
C 4 1323 --S
C 5 1324 --S
C 3 1325 --S
C 3 1326 --S
C 5 1327 --S
C 4 1328 -C-
C 3 1329 --S
C 2 132a -C-
C 4 132b --S
C 3 132c --S
C 4 132d -C-
C 3 132e --S (probably related to 132f)
C 3 132f -C- (probably related to 132e)
C 4 1330 -C-
C 2 1331 --S
* C 4 1332 --S
C 3 1333 --S
C 4 1334 -C-
C 4 1335 --S
C 4 1336 --S
C 4 1337 -C- (probably related to 1338)
C 4 1338 --S (probably related to 1337)
C 3 1339 --S
???
? 3 1402 -C- (occurs in 1025012, Sakura Blast A 2nd route)
? 2 1403 -C-
? 3 1410 --S
Partner cards/Simple mail
OK A 5 1500 --S player's own partner card
!* B 5 1501 --S partner cards list
! ? 3 1503 --S (card received)
! ? 3 1504 -C- (card accepted)
! C 4 1506 -C- (partner cards online status request)
! C 4 1507 --S (partner cards online status reply; whether cards are online or offline)
! ? 3 1508 -C- (edit partner card; self only?)
! ? 3 1509 --S (add card to the list)
! ? 4 150b -C- send simple mail
! ? 4 150d -C- (mark simple mail as read?)
! ? 4 150f --S receive simple mail
!* B 5 1512 --S blacklist cards list
! ? 2 1513 -C- (add to blacklist)
! ? 3 1514 -C- (send partner card)
! ? 4 1515 -C- (probably related to 1516)
? 4 1516 --S (probably related to 1515)
NPC characters
* A 4 1601 --S (probably npc AI parameters)
!* A 5 1602 --S player's available NPC and PM info
! ? 3 1603 -C- (receive npc card related; see tutorial)
! ? 2 1604 -C- (sort priority related)
! ? 3 1605 -C- (receive npc card related; see tutorial; sort priority related)
Counters
WIP ? 4 1701 -CS parties list request/reply
? 3 1702 -CS (probably join party request/reply)
OK B 4 1705 -C- party info recap request
!* B 4 1706 -CS party info recap change(C) reply(S)
OK C 4 1707 -C- menu event; select mission
OK B 4 1709 -C- party settings request
!* B 4 170a -CS party settings change(C) reply(S)
OK C 4 170b -C- counter background location points request
WIP C 4 170c --S counter background location points reply (9 of them total)
! ? 3 170d -C- (probably related to 170e)
? 3 170e --S (probably related to 170d)
? 4 170f --S (probably reply to 1707, but not all the time?)
OK C 4 1710 -C- counter background request
OK C 4 1711 --S counter background reply
???
! ? 2 1803 -C- (occurs in the AOTI story mission 1231022)
Trade
? 3 1901 -CS (trade request)
? 3 1902 --S
? 3 1903 -C-
? 3 1904 --S
Dialogs
OK A 5 1a01 -C- dialog request
OK B 4 1a02 --S dialog reply: standard
OK B 3 1a03 --S dialog reply: lumilass options
!* A 4 1a04 --S dialog reply: pp cube
! ? 3 1a05 -C- (lumilass: bought something)
OK B 4 1a07 --S dialog reply: types
? 3 1a08 -C- (shop delivery request)
? 3 1a09 --S (shop delivery reply)
Casino/Temple
! C 4 1d01 -C- (casino shop/coins request; also on casino shop buy)
! C 3 1d04 --S (casino shop related)
! C 3 1d05 --S (casino shop buy related)
! C 4 1d06 -C- (casino roulette bet request)
! C 5 1d07 --S (casino coin request and shop related)
! C 2 1d08 -C- (luck lady request)
! C 2 1d09 --S (luck lady reply)
! C 3 1d0a -C- (probably related to 1d0b; offering box)
! C 3 1d0b --S (probably related to 1d0a)
! C 3 1d0c -C- (casino enter; slot machine request)
! C 3 1d0d -C-
! C 4 1d0e -C-
! C 3 1d0f --S (reply to 1d0c?)
C 4 1d10 --S
C 3 1d11 --S
C 4 1d12 --S
C 3 1d13 --S
C 3 1d16 --S
C 3 1d17 --S (casino coin request related)
C 3 1d18 --S (casino coin request related)
13 WIP 85 OK 337 Total
! means the command has already been analyzed, at least partially
* means the command is already found somewhere in the server but needs to be reviewed

View File

@ -0,0 +1,30 @@
010b
Size:32
Command:16 Channel:8 Garbage:8
FromLID:16 0:16
0:32
FromGID:32
0:32
0:32
0:32
0:32
0:32
0:32
FromGID:32
PartyPosOrLID:32
0:16 IntDir:16
PosX:32
PosY:32
PosZ:32
0:32
0:32
QuestID:32
ZoneID:32
MapID:32
EntryID:32
Size: 0x5c
Channel: 2
FromGID: should be the same in both the header and the body (even for NPC characters)
IntDir: character direction as a float converted to int:16

View File

@ -0,0 +1,32 @@
0111
Size:32
Command:16 Channel:8 Garbage:8
DestLID:16 Garbage:16
0:32
TargetGID:32
0:32
0:32
DestTypeID:32
DestGID:32
0:32
0:32
TargetGID:32
TargetLID:32
EventID:32
Param:32
Size: 60
Channel: 3
TargetGID: player or npc
DestTypeID: 00011300
EventID: 2 3 4 5 6 7
Param: only for EventID 3 and 7
Events:
2: ? ; Param=0
3: red screen of death; Param: whether to ask if the player wants to go back to lobby, 0 or 1
4: healing pad aura + sound; Param=0
5: ? ; Param=0
6: ? ; Param=0 ; sent for lobby area change just before 010d
7: EXP FULL message; Param is the enemy TargetID that triggered this

View File

@ -0,0 +1,40 @@
0115
Size:32
Command:16 Channel:8 Garbage:8
DestLID:16 Garbage:16
0:32
TargetGID:32
0:32
0:32
DestTypeID:32
DestGID:32
0:32
0:32
TargetGID:32
TargetLID:32
EnemyTargetID:32
Level:32
BlastBar:16 Luck:8 _UnknownJ:8
0:32
EXP:32
0:32
Money:32
PlayTime:32
for npc characters:
01000000:32 01000000:32 01000000:32 01000000:32 01000000:32 01000000:32 01000000:32 01000000:32
01000000:32 01000000:32 01000000:32 01000000:32 01000000:32 01000000:32 01000000:32 01000000:32
4e4f4630:32 08000000:32 00000000:32 00000000:32 4e454e44:32
for player characters:
_UnknownK:32 _UnknownL:32 0:32 0:32 0:32
ClassLevelAndEXP:32 ClassLevelAndEXP:32 ClassLevelAndEXP:32 ClassLevelAndEXP:32
ClassLevelAndEXP:32 ClassLevelAndEXP:32 ClassLevelAndEXP:32 ClassLevelAndEXP:32
ClassLevelAndEXP:32 ClassLevelAndEXP:32 ClassLevelAndEXP:32 ClassLevelAndEXP:32
ClassLevelAndEXP:32 ClassLevelAndEXP:32 ClassLevelAndEXP:32 ClassLevelAndEXP:32
Size: 168
Channel: 3
DestTypeID: 00011300
TargetGID: either player or npc
EnemyTargetID: presumably the TargetID of the enemy that got you leveled
Level and following: seems to take the same format as 0201, refer there for details

View File

@ -0,0 +1,26 @@
0117
Size:32
Command:16 Channel:8 Garbage:8
DestLID:16 Garbage:16
0:32
TargetGID:32
0:32
0:32
DestTypeID:32
DestGID:32
0:32
0:32
TargetGID:32
TargetLID:32
SE1:32 SE2:32
HP:32
0:32
Size: 68
Channel: 3
TargetGID: either player or npc
DestTypeID: 00011300
SE1: 00000000 or 00000200(stay down) or 00001000(no idea); SE part 1
SE2: various; SE part 2
HP: how many HP to revive the player with

View File

@ -0,0 +1,26 @@
0200
Size:32
Command:16 Channel:8 Garbage:8
LID:16 Garbage:16
0:32
0:32
0:32
0:32
TypeID:32
GID:32
0:32
0:32
LID:16 ZoneID:16
NbPlayers:32
Var:320
Size: always 5c
TypeID: always 00011300
LID: same as in both places; probably the player's new LID after entering this zone
ZoneID: 0 1 2 3 4 5 6 7 8 9 10 11 32; I now have doubt over whether this is a ZoneID
NbPlayers: many values from 0 to more than 255; number of players in this zone, starts at 1 (self) - LID is supposed to stay below that
Var: either of
ffffffff 00040000 00000000 00000000 00000000 00000000 00000000 00140000 ffffffff ffffffff
ffffffff 06000000 02000000 00000000 00000000 40000000 00010000 00010000 ffffffff ffffffff
ffffffff 06000500 01000000 00000000 00000000 00040000 00010000 00140000 ffffffff ffffffff

View File

@ -0,0 +1,25 @@
0202
Size:32
Command:16 Channel:8 Garbage:8
ffff:16 0:16
0:32
0:32
0:32
0:32
0:32
0:32
0:32
0:32
SessionID:32
0:1024
Size: 0xb0
Channel: 3
SessionID: like GID except typically only temporary; apparently official use the same counter for NPCs and for these SessionID
0:1024: can also contain an error message, though details are limited on that; it's similar to 0223 though
The following format will display an error:
0:32, 3:32/little, 0:48, Len:16/little, Error/binary, 0:Padding
The server should disconnect the client right after sending that command if an error has been sent.

View File

@ -0,0 +1,19 @@
0203
Size:32
Command:16 Channel:8 0:8
DestLID:16 0:16
0:32
0:32
0:32
0:32
DestTypeID:32
DestGID:32
0:32
0:32
TargetGID:32
TargetLID:32
Size: 52
Channel: 3
DestTypeID: 00011300

View File

@ -0,0 +1,23 @@
0204
Size:32
Command:16 Channel:8 Garbage:8
DestLID:16 Garbage:16
CharTypeID:32
CharGID:32
0:32
0:32
DestTypeID:32
DestGID:32
0:32
0:32
CharGID:32
CharLID:32
_UnknownC:32
Size: 0x38
Channel: 3
CharTypeID: 00001200 for players, 00001d00 for NPCs
CharGID: both NPC and players
DestTypeID: 00011300
_UnknownC: seems to be an LID that isn't either of DestLID or CharLID; should be the total number of players in the zone after the removal

View File

@ -0,0 +1,30 @@
0205
Size:32
Command:16 Channel:8 Garbage:8
LID:16 Garbage:16
0:32
0:32
0:32
0:32
TypeID:32
GID:32
0:32
0:32
ffffffff:32
ZoneID:32
MapID:32
EntryID:32
AreaNb:32
PlayerLID:16 _UnknownF:8 IsSeasonal:8
TypeID: always 00011300
ZoneID: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 20
MapID: 313 of them
EntryID: 0 1 2 3 4 5 6 7 8 10 11 12 13 14 20 21 22 23 32767
AreaNb: odd value, starts at 1, incremented after sending this command; replied by 0806
PlayerLID: LID of the related player
_UnknownF: sometimes 1
IsSeasonal: 0 otherwise
not sure how AreaNb works when 0205 is sent for spawning other players

View File

@ -0,0 +1,19 @@
0208
Size:32
Command:16 Channel:8 Garbage:8
DestLID:16 Garbage:16
0:32
0:32
0:32
0:32
DestTypeID:32
DestGID:32
0:32
0:32
AreaNb:32
Size: 48
Channel: 3
DestTypeID: 00011300
AreaNb: even value, starts at 2 following the first 0205 sent, incremented after sending this command; replied by 0808

View File

@ -0,0 +1,20 @@
020b
Size:32
Command:16 Channel:8 Garbage:8
ffff:16 0:16
0:32
0:32
0:32
0:32
0:32
0:32
0:32
0:32
Slot:32
0:8 BackToPreviousField:8 0:16
Size: 52
Channel: 2
Slot: character save slot
BackToPreviousField: if any, whether to go back to the previous field the character was in

View File

@ -0,0 +1,16 @@
020c
Size:32
Command:16 Channel:8 Garbage:8
ffff:16 Garbage:16
0:32
0:32
0:32
0:32
0:32
0:32
0:32
0:32
Size: 0x2c
Channel: 2, despite being sent by the server

View File

@ -0,0 +1,20 @@
020d
Size:32
Command:16 Channel:8 Garbage:8
ffff:16 0:16
0:32
0:32
0:32
0:32
0:32
0:32
0:32
0:32
AuthGID:32
AuthKey:32
0:32
0:32
Size: 60
Channel: 2

View File

@ -0,0 +1,21 @@
020e
Size:32
Command:16 Channel:8 0:8
ffff:16 0:16
0:32
0:32
0:32
0:32
0:32
0:32
0:32
0:32
FileSize:32
_UnknownA:32
File/binary
0:32
Size: variable, depends on File
Channel: 3
_UnknownA: odd value but always in the same region; maybe the addition of all values in the file or something?

View File

@ -0,0 +1,20 @@
020f
Size:32
Command:16 Channel:8 0:8
ffff:16 0:16
0:32
0:32
0:32
0:32
0:32
0:32
0:32
0:32
SetID:8 SeasonID:8 0:16
FileSize:32
File:N
Size: depends on FileSize
SetID: which set file to open (0 to N - 1)
SeasonID: what season to load, if any; 255 otherwise

View File

@ -0,0 +1,19 @@
0210
Size:32
Command:16 Channel:8 Garbage:8
ffff:16 Garbage:16
0:32
0:32
0:32
0:32
TypeID:32
GID:32
0:32
0:32
0:32
UnixTime:32
Size: 52
TypeID: 00011300
UnixTime: current unix time

View File

@ -0,0 +1,19 @@
0215
Size:32
Command:16 Channel:8 Garbage:8
LID:16 Garbage:16
0:32
0:32
0:32
0:32
TypeID:32
GID:32
0:32
0:32
_UnknownC:32
Size: 0x30
Channel: 3
TypeID: always 00011300
_UnknownC: 0 1 2 3 4 5 ffffffff (PartyPos? Leader's PartyPos?)

View File

@ -0,0 +1,22 @@
0216
Size:32
Command:16 Channel:8 0:8
ffff:16 0:16
0:32
0:32
0:32
0:32
TypeID:32
GID:32
0:32
0:32
IP:32
Port:16 0:16
Size: 52
Channel: 3
TypeID: 00000f00
GID: the temporary SessionID
IP: standard binary format, as four 8 bit integers
Port: standard binary format

View File

@ -0,0 +1,16 @@
0217
Size:32
Command:16 Channel:8 0:8
ffff:16 0:16
0:32
0:32
0:32
0:32
0:32
0:32
0:32
0:32
Size: 44
Channel: 2

View File

@ -0,0 +1,21 @@
0219
Size:32
Command:16 Channel:8 Garbage:8
ffff:16 0:16
0:32
0:32
0:32
0:32
0:32
0:32
0:32
0:32
Username:192
Password:192
_UnknownC:64
Size: 100
Channel: 2
Username, Password: ASCII
_UnknownC: always 29525a52 7510439e?

View File

@ -0,0 +1,17 @@
021b
Size:32
Command:16 Channel:8 Garbage:8
ffff:16 Garbage:16
0:32
0:32
0:32
0:32
TypeID:32
GID:32
0:32
0:32
Size: 44
Channel: 3
TypeID: 00011300

View File

@ -0,0 +1,17 @@
021c
Size:32
Command:16 Channel:8 Garbage:8
FromLID:16 0:16
0:32
0:32
0:32
0:32
0:32
0:32
0:32
0:32
Size: 0x2c
Channel: 2
FromLID: ffff or LID

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