Compare commits

...

114 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
106 changed files with 7382 additions and 4871 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 # EGS: Erlang Game Server
# Copyright (C) 2010 Loic Hoguin # Copyright (C) 2010-2011 Loic Hoguin
# #
# This file is part of EGS. # This file is part of EGS.
# #
@ -16,14 +16,23 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with EGS. If not, see <http://www.gnu.org/licenses/>. # along with EGS. If not, see <http://www.gnu.org/licenses/>.
all: server REBAR = rebar
server: all: app
@./rebar compile
app: deps
@$(REBAR) compile
deps:
@$(REBAR) get-deps
clean: clean:
@./rebar clean @$(REBAR) clean
rm -f erl_crash.dump rm -f erl_crash.dump
fclean: clean tests:
rm -rf Mnesia.egs* @$(REBAR) eunit
@$(REBAR) ct
dialyze:
@$(REBAR) dialyze

View File

@ -1,5 +1,5 @@
%% @author Loïc Hoguin <essen@dev-extend.eu> %% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin. %% @copyright 2010-2011 Loïc Hoguin.
%% @doc Project-wide Erlang records. %% @doc Project-wide Erlang records.
%% %%
%% This file is part of EGS. %% This file is part of EGS.
@ -17,64 +17,64 @@
%% You should have received a copy of the GNU Affero General Public License %% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>. %% along with EGS. If not, see <http://www.gnu.org/licenses/>.
%% Standard library types.
-opaque sslsocket() :: any().
%% EGS types.
-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()}.
-type position() :: {X :: float(), Y :: float(), Z :: float(), Dir :: float()}.
%% Records. %% Records.
%% @doc Per-process state used by the various EGS modules. %% @doc Client state. One per connected client.
-record(state, { -record(egs_net, {
socket :: sslsocket(), socket :: ssl:sslsocket(),
gid :: integer(), transport :: module(),
slot :: 0..3, handler :: module(),
lid = 16#ffff :: 0..16#ffff, buffer = <<>> :: binary(),
keepalive = false :: boolean(),
gid = 0 :: egs:gid(),
lid = 16#ffff :: egs:lid(),
slot = 0 :: 0..3,
areanb = 0 :: non_neg_integer() areanb = 0 :: non_neg_integer()
}). }).
%% @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 :: integer(),
username :: string(),
password :: string(),
auth_state :: undefined | {wait_for_authentication, binary(), any()}
}).
%% @doc Table containing the users currently logged in. %% @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). %% @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, { -record(users, {
%% General information. %% General information.
gid :: integer(), gid :: egs:gid(),
lid = 16#ffff :: 0..16#ffff, lid = 16#ffff :: egs:lid(),
pid :: pid(), pid :: pid(),
time :: integer(), %% Character information.
character :: tuple(), %% @todo Details. %% @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. %% Location/state related information.
uni :: integer(), uni :: integer(),
questpid :: pid(), questpid :: pid(),
zonepid :: pid(), zonepid :: pid(),
partypid :: pid(), partypid :: pid(),
areatype :: counter | mission | lobby | myroom, areatype :: counter | mission | lobby | myroom,
area :: area(), area :: egs:area(),
entryid :: entryid(), entryid :: egs:entryid(),
pos = {0.0, 0.0, 0.0, 0.0} :: position(), pos = {0.0, 0.0, 0.0, 0.0} :: egs:position(),
shopid :: integer(), shopid :: integer(),
prev_area = {0, 0, 0} :: area(), prev_area = {0, 0, 0} :: egs:area(),
prev_entryid = 0 :: entryid(), prev_entryid = 0 :: egs:entryid(),
%% To be moved or deleted later on. %% To be moved or deleted later on.
instancepid :: pid() instancepid :: pid(),
char
}). }).
%% Past this point needs to be reviewed. %% Past this point needs to be reviewed.
@ -111,13 +111,6 @@
faceboxx=65535, faceboxy=65535 faceboxx=65535, faceboxy=65535
}). }).
%% @doc Table containing counters current values.
-record(counters, {name, id}).
%% @doc Character main or class level data structure.
-record(level, {number, exp}).
%% @doc Character stats data structure. %% @doc Character stats data structure.
-record(stats, {atp, ata, tp, dfp, evp, mst, sta}). -record(stats, {atp, ata, tp, dfp, evp, mst, sta}).
@ -128,38 +121,6 @@
cutindisplay, mainmenucursorposition, camera3y, camera3x, camera1y, camera1x, controller, weaponswap, cutindisplay, mainmenucursorposition, camera3y, camera3x, camera1y, camera1x, controller, weaponswap,
lockon, brightness, functionkeysetting, buttondetaildisplay}). 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. %% @doc Hit response data.
-record(hit_response, {type, user, exp, damage, targethp, targetse, events}). -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]}.

View File

@ -1,7 +1,7 @@
%%-*- mode: erlang -*- %%-*- mode: erlang -*-
{application, egs, [ {application, egs, [
{description, "EGS online action-RPG game server"}, {description, "EGS online action-RPG game server"},
{vsn, "0.9"}, {vsn, "0.14"},
{modules, []}, {modules, []},
{registered, []}, {registered, []},
{applications, [ {applications, [
@ -9,7 +9,7 @@
stdlib, stdlib,
crypto, crypto,
ssl, ssl,
mnesia cowboy
]}, ]},
{mod, {egs_app, []}}, {mod, {egs_app, []}},
{env, []} {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> %% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin. %% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS configuration gen_server. %% @doc EGS configuration gen_server.
%% %%
%% This file is part of EGS. %% This file is part of EGS.
@ -19,41 +19,35 @@
-module(egs_conf). -module(egs_conf).
-behavior(gen_server). -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). -define(SERVER, ?MODULE).
%% API. %% API.
%% @spec start_link() -> {ok,Pid::pid()} -spec start_link() -> {ok, pid()}.
start_link() -> start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%% @spec stop() -> stopped -spec stop() -> stopped.
stop() -> stop() ->
gen_server:call(?SERVER, stop). gen_server:call(?SERVER, stop).
%% @spec read(Key) -> Value | undefined -spec read(atom()) -> undefined | any().
read(Key) -> read(Key) ->
gen_server:call(?SERVER, {read, Key}). gen_server:call(?SERVER, {read, Key}).
%% @spec reload() -> ok -spec reload() -> ok.
reload() -> reload() ->
gen_server:cast(?SERVER, reload). gen_server:cast(?SERVER, reload).
%% gen_server. %% gen_server.
init([]) -> init([]) ->
case file:consult("priv/egs.conf") of {ok, _Terms} = file:consult("priv/egs.conf").
{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.
handle_call({read, Key}, _From, State) -> handle_call({read, Key}, _From, State) ->
{reply, proplists:get_value(Key, State), State}; {reply, proplists:get_value(Key, State), State};

View File

@ -1,5 +1,5 @@
%% @author Loïc Hoguin <essen@dev-extend.eu> %% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin. %% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS counters database and cache manager. %% @doc EGS counters database and cache manager.
%% %%
%% This file is part of EGS. %% This file is part of EGS.
@ -19,6 +19,7 @@
-module(egs_counters_db). -module(egs_counters_db).
-behavior(gen_server). -behavior(gen_server).
-export([start_link/0, stop/0, bg/1, opts/1, pack/1, reload/0]). %% API. -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. -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> %% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin. %% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS file creation functions. %% @doc EGS file creation functions.
%% %%
%% This file is part of EGS. %% This file is part of EGS.
@ -506,7 +506,7 @@ nbl_pack_files([], {AccH, AccD, AccP, _FilePos, _PtrIndex}) ->
{BinD3, CompressedDataSize} = if BinDSize < 16#800 -> {BinD3, CompressedDataSize} = if BinDSize < 16#800 ->
{BinD, 0}; {BinD, 0};
true -> true ->
BinD2 = egs_prs:compress(BinD), BinD2 = prs:compress(BinD),
BinD2Size = byte_size(BinD2), BinD2Size = byte_size(BinD2),
{BinD2, BinD2Size} {BinD2, BinD2Size}
end, end,

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> %% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin. %% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS items database. %% @doc EGS items database.
%% %%
%% This file is part of EGS. %% This file is part of EGS.
@ -19,6 +19,7 @@
-module(egs_items_db). -module(egs_items_db).
-behavior(gen_server). -behavior(gen_server).
-export([start_link/0, stop/0, desc/1, read/1, reload/0]). %% API. -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. -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). -define(SERVER, ?MODULE).
-include("include/records.hrl"). -include("include/records.hrl").
-include("priv/items.hrl"). -include("../../priv/items.hrl").
%% API. %% API.

View File

@ -1,5 +1,5 @@
%% @author Loïc Hoguin <essen@dev-extend.eu> %% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin. %% @copyright 2010-2011 Loïc Hoguin.
%% @doc Log in and authentication callback module. %% @doc Log in and authentication callback module.
%% %%
%% This file is part of EGS. %% This file is part of EGS.
@ -18,72 +18,64 @@
%% along with EGS. If not, see <http://www.gnu.org/licenses/>. %% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_login). -module(egs_login).
-export([keepalive/1, info/2, cast/3, raw/3, event/2]). -export([info/2, cast/3, event/2]).
-include("include/records.hrl"). -include("include/records.hrl").
%% @doc Don't keep alive here, authentication should go fast.
keepalive(_State) ->
ok.
%% @doc We don't expect any message here. %% @doc We don't expect any message here.
info(_Msg, _State) -> info(_Msg, _Client) ->
ok. ok.
%% @doc Nothing to broadcast. %% @doc Nothing to broadcast.
cast(_Command, _Data, _State) -> cast(_Command, _Data, _Client) ->
ok. ok.
%% Raw commands.
%% @doc Dismiss all raw commands with a log notice.
%% @todo Have a log event handler instead.
raw(Command, _Data, _State) ->
io:format("~p: dismissed command ~4.16.0b~n", [?MODULE, Command]).
%% Events. %% Events.
%% @doc Reject version < 2.0009.2. %% @doc Reject version < 2.0009.2.
%% @todo Reject wrong platforms too. %% @todo Reject wrong platforms too.
event({system_client_version_info, _Entrance, _Language, _Platform, Version}, State=#state{socket=Socket}) -> %% @todo Put the URL in a configuration file.
if Version >= 2009002 -> ignore; true -> event({client_version, _Entrance, _Language, _Platform, Version}, Client)
psu_proto:send_0231("http://psumods.co.uk/forums/comments.php?DiscussionID=40#Item_1", State), when Version < 2009002 ->
{ok, ErrorMsg} = file:read_file("priv/login/error_version.txt"), egs_net:system_open_url(<<"http://psumods.co.uk/forums/comments.php?DiscussionID=40#Item_1">>, Client),
psu_proto:send_0223(ErrorMsg, State), {ok, Error} = file:read_file("priv/login/error_version.txt"),
ssl:close(Socket), egs_net:system_auth_error(Error, Client),
closed egs_net:terminate(Client),
end; closed;
event({client_version, _Entrance, _Language, _Platform, _Version}, _Client) ->
ok;
%% @doc Game server info request handler. %% @doc Game server info request handler.
event(system_game_server_request, State=#state{socket=Socket}) -> event(system_game_server_request, Client) ->
{ServerIP, ServerPort} = egs_conf:read(game_server), {ServerIP, ServerPort} = egs_conf:read(game_server),
psu_proto:send_0216(ServerIP, ServerPort, State), egs_net:system_game_server_response(ServerIP, ServerPort, Client),
ssl:close(Socket), egs_net:terminate(Client),
closed; closed;
%% @doc Authenticate the user by pattern matching its saved state against the key received. %% @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. %% If the user is authenticated, send him the character flags list.
%% @todo Remove the put calls when all the send_xxxx are moved out of psu_game and into psu_proto. event({system_key_auth, AuthGID, AuthKey}, Client) ->
event({system_key_auth_request, AuthGID, AuthKey}, State=#state{socket=Socket}) ->
egs_accounts:key_auth(AuthGID, AuthKey), egs_accounts:key_auth(AuthGID, AuthKey),
egs_users:write(#users{gid=AuthGID, pid=self()}), Client2 = egs_net:set_gid(AuthGID, Client),
put(socket, Socket), ValueFlags = egs_conf:read(value_flags),
put(gid, AuthGID), BoolFlags = egs_conf:read(bool_flags),
State2 = State#state{gid=AuthGID}, TempFlags = egs_conf:read(temp_flags),
psu_proto:send_0d05(State2), egs_net:account_flags(ValueFlags, BoolFlags, TempFlags, Client2),
{ok, egs_char_select, State2}; Client3 = egs_net:set_handler(egs_char_select, Client2),
Client4 = egs_net:set_keepalive(Client3),
{ok, Client4};
%% @doc Authentication request handler. Currently always succeed. %% @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 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. %% @todo Apparently it's possible to ask a question in the reply here. Used for free course on JP.
event({system_login_auth_request, Username, Password}, State) -> event({system_login_auth, Username, Password}, Client) ->
{ok, GID} = egs_accounts:login_auth(Username, Password), {ok, AuthGID} = egs_accounts:login_auth(Username, Password),
{ok, AuthKey} = egs_accounts:key_auth_init(GID), {ok, AuthKey} = egs_accounts:key_auth_init(AuthGID),
io:format("auth success for ~s ~s~n", [Username, Password]), io:format("auth success for ~s ~s~n", [Username, Password]),
psu_proto:send_0223(GID, AuthKey, State); egs_net:system_key_auth_info(AuthGID, AuthKey, Client);
%% @doc MOTD request handler. Page number starts at 0. %% @doc MOTD request handler. Page number starts at 0.
%% @todo Currently ignore the language and send the same MOTD file to everyone. %% @todo Currently ignore the language and send the same MOTD file to everyone.
event({system_motd_request, Page, _Language}, State) -> event({system_motd_request, Page, _Language}, Client) ->
{ok, MOTD} = file:read_file("priv/login/motd.txt"), {ok, MOTD} = file:read_file("priv/login/motd.txt"),
psu_proto:send_0225(MOTD, Page, State). egs_net:system_motd_response(MOTD, Page, Client).

View File

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

View File

@ -1,5 +1,5 @@
%% @author Loïc Hoguin <essen@dev-extend.eu> %% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin. %% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS NPC database. %% @doc EGS NPC database.
%% %%
%% This file is part of EGS. %% This file is part of EGS.
@ -19,6 +19,7 @@
-module(egs_npc_db). -module(egs_npc_db).
-behavior(gen_server). -behavior(gen_server).
-export([start_link/0, stop/0, all/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. -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). -define(SERVER, ?MODULE).
-include("include/records.hrl"). -include("include/records.hrl").
-include("priv/npc.hrl"). -include("../../priv/npc.hrl").
%% API. %% API.
@ -61,14 +62,15 @@ handle_call(all, _From, State) ->
%% @todo Handle stats, experience, based on level. %% @todo Handle stats, experience, based on level.
handle_call({create, NPCid, BaseLevel}, _From, State) -> 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), #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 >>, TmpUCS2Name = << << X:8, 0:8 >> || X <- Name >>,
Padding = 8 * (64 - byte_size(TmpUCS2Name)), Padding = 8 * (64 - byte_size(TmpUCS2Name)),
UCS2Name = << TmpUCS2Name/binary, 0:Padding >>, 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, User = #users{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}, level=calc_level(BaseLevel, LevelDiff), blastbar=0, luck=2, money=0,
User = #users{gid=NPCGID, character=Character, areatype=lobby, area={0, 0, 0}, entryid=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}; {reply, User, State};
handle_call(stop, _From, 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

@ -1,5 +1,5 @@
%% @author Loïc Hoguin <essen@dev-extend.eu> %% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin. %% @copyright 2011 Loïc Hoguin.
%% @doc Quest handler. %% @doc Quest handler.
%% %%
%% This file is part of EGS. %% This file is part of EGS.
@ -43,7 +43,7 @@ zone_pid(Pid, ZoneID) ->
init([UniID, QuestID]) -> init([UniID, QuestID]) ->
Zones = egs_quests_db:quest_zones(QuestID), Zones = egs_quests_db:quest_zones(QuestID),
ZonesPids = lists:map(fun({ZoneID, ZoneData}) -> ZonesPids = lists:map(fun({ZoneID, ZoneData}) ->
{ok, Pid} = supervisor:start_child(egs_zones_sup, {{zone, UniID, QuestID, ZoneID}, {egs_zones, start_link, [UniID, QuestID, ZoneID, ZoneData]}, permanent, 5000, worker, dynamic}), {ok, Pid} = egs_zones_sup:start_zone(UniID, QuestID, ZoneID, ZoneData),
{ZoneID, Pid} {ZoneID, Pid}
end, Zones), end, Zones),
{ok, #state{zones=ZonesPids}}. {ok, #state{zones=ZonesPids}}.

View File

@ -1,5 +1,5 @@
%% @author Loïc Hoguin <essen@dev-extend.eu> %% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin. %% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS quests database and cache manager. %% @doc EGS quests database and cache manager.
%% %%
%% This file is part of EGS. %% This file is part of EGS.
@ -19,14 +19,15 @@
-module(egs_quests_db). -module(egs_quests_db).
-behavior(gen_server). -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([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. -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
-record(state, {quests=[], quests_bin=[], zones_bin=[], sets=[]}).
%% Use the module name for the server's name. %% Use the module name for the server's name.
-define(SERVER, ?MODULE). -define(SERVER, ?MODULE).
-record(state, {quests=[], quests_bin=[], zones_bin=[], sets=[]}).
%% API. %% API.
%% @spec start_link() -> {ok,Pid::pid()} %% @spec start_link() -> {ok,Pid::pid()}
@ -105,7 +106,7 @@ handle_call({zone_nbl, QuestID, ZoneID}, _From, State=#state{quests=QuestsCache,
{Set0, SetPtrs} = egs_files:load_set_rel(ZoneDir ++ io_lib:format("set_r~b.conf", [0]), AreaID, Maps, FilePos), {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"), ScriptBin = egs_files:load_script_bin(ZoneDir ++ "script.es"),
ScriptBinSize = byte_size(ScriptBin), ScriptBinSize = byte_size(ScriptBin),
ScriptBin2 = egs_prs:compress(ScriptBin), ScriptBin2 = prs:compress(ScriptBin),
ScriptBinSize2 = byte_size(ScriptBin2), ScriptBinSize2 = byte_size(ScriptBin2),
ScriptBin3 = << ScriptBinSize:32/little, ScriptBinSize2:32/little, 0:32, 1:32/little, 0:96, ScriptBin2/binary >>, 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"), TextBin = egs_files:load_text_bin(ZoneDir ++ "text.bin.en_US.txt"),

View File

@ -1,6 +1,6 @@
%% @author Loïc Hoguin <essen@dev-extend.eu> %% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin. %% @copyright 2011 Loïc Hoguin.
%% @doc Supervisor for the patch, login and game listener processes. %% @doc Supervisor for the egs_quests gen_server.
%% %%
%% This file is part of EGS. %% This file is part of EGS.
%% %%
@ -17,27 +17,30 @@
%% You should have received a copy of the GNU Affero General Public License %% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>. %% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_servers_sup). -module(egs_quests_sup).
-behaviour(supervisor). -behaviour(supervisor).
-export([start_link/0]). %% API. -export([start_link/0, start_quest/2]). %% API.
-export([init/1]). %% supervisor. -export([init/1]). %% supervisor.
-define(SUPERVISOR, ?MODULE). -define(SUPERVISOR, ?MODULE).
%% API. %% API.
-spec start_link() -> {ok, Pid::pid()}. -spec start_link() -> {ok, pid()}.
start_link() -> start_link() ->
supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []). 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. %% supervisor.
-spec init([]) -> {ok, {{simple_one_for_one, 0, 1}, [{egs_quests,
{egs_quests, start_link, []}, temporary, brutal_kill,
worker, [egs_quests]}]}}.
init([]) -> init([]) ->
PatchPorts = egs_conf:read(patch_ports), {ok, {{simple_one_for_one, 0, 1}, [{egs_quests,
LoginPorts = egs_conf:read(login_ports), {egs_quests, start_link, []}, temporary, brutal_kill,
{_ServerIP, GamePort} = egs_conf:read(game_server), worker, [egs_quests]}]}}.
PatchProcs = [{{egs_patch_server, Port}, {egs_patch_server, start_link, [Port]}, permanent, 5000, worker, dynamic} || Port <- PatchPorts],
LoginProcs = [{{egs_login_server, Port}, {egs_login_server, start_link, [Port]}, permanent, 5000, worker, dynamic} || Port <- LoginPorts],
Procs = lists:flatten([PatchProcs, LoginProcs, {egs_game_server, {egs_game_server, start_link, [GamePort]}, permanent, 5000, worker, dynamic}]),
{ok, {{one_for_one, 10, 10}, Procs}}.

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,5 +1,5 @@
%% @author Loïc Hoguin <essen@dev-extend.eu> %% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin. %% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS seasons management. %% @doc EGS seasons management.
%% @todo When we know how to do it we should change the lobby automatically to the next season. %% @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). -module(egs_seasons).
-behavior(gen_server). -behavior(gen_server).
-export([start_link/0, stop/0, read/1]). %% API. -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. -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> %% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin. %% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS shops database. %% @doc EGS shops database.
%% %%
%% This file is part of EGS. %% This file is part of EGS.
@ -19,6 +19,7 @@
-module(egs_shops_db). -module(egs_shops_db).
-behavior(gen_server). -behavior(gen_server).
-export([start_link/0, stop/0, nth/2, read/1, reload/0]). %% API. -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. -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

@ -1,5 +1,5 @@
%% @author Loïc Hoguin <essen@dev-extend.eu> %% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin. %% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS universes handler. %% @doc EGS universes handler.
%% %%
%% This file is part of EGS. %% This file is part of EGS.
@ -19,14 +19,15 @@
-module(egs_universes). -module(egs_universes).
-behavior(gen_server). -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([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. -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
-record(state, {unis=[], lobbies=[]}).
%% Use the module name for the server's name. %% Use the module name for the server's name.
-define(SERVER, ?MODULE). -define(SERVER, ?MODULE).
-record(state, {unis=[], lobbies=[]}).
%% Default universe IDs. %% Default universe IDs.
-define(MYROOM_ID, 21). -define(MYROOM_ID, 21).
-define(DEFAULT_ID, 26). -define(DEFAULT_ID, 26).
@ -151,6 +152,6 @@ create_unis([Name|Tail], UniID, Acc) ->
%% @doc Start lobbies for the given universe. %% @doc Start lobbies for the given universe.
init_lobbies(UniID) -> init_lobbies(UniID) ->
lists:map(fun(QuestID) -> lists:map(fun(QuestID) ->
{ok, Pid} = supervisor:start_child(egs_quests_sup, {{quest, UniID, QuestID}, {egs_quests, start_link, [UniID, QuestID]}, permanent, 5000, worker, dynamic}), {ok, Pid} = egs_quests_sup:start_quest(UniID, QuestID),
{{UniID, QuestID}, Pid} {{UniID, QuestID}, Pid}
end, ?LOBBIES). 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}.

View File

@ -1,5 +1,5 @@
%% @author Loïc Hoguin <essen@dev-extend.eu> %% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2011 Loïc Hoguin. %% @copyright 2011 Loïc Hoguin.
%% @doc Zone handler. %% @doc Zone handler.
%% %%
%% This file is part of EGS. %% This file is part of EGS.
@ -20,7 +20,7 @@
-module(egs_zones). -module(egs_zones).
-behaviour(gen_server). -behaviour(gen_server).
-export([start_link/4, stop/1, setid/1, enter/2, leave/2, get_all_players/2]). %% API. -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. -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
-record(state, { -record(state, {
@ -48,12 +48,17 @@ setid(Pid) ->
enter(Pid, GID) -> enter(Pid, GID) ->
gen_server:call(Pid, {enter, GID}). gen_server:call(Pid, {enter, GID}).
leave(undefined, _GID) ->
ok;
leave(Pid, GID) -> leave(Pid, GID) ->
gen_server:cast(Pid, {leave, GID}). gen_server:call(Pid, {leave, GID}).
get_all_players(Pid, ExcludeGID) -> get_all_players(Pid, ExcludeGID) ->
gen_server:call(Pid, {get_all_players, ExcludeGID}). gen_server:call(Pid, {get_all_players, ExcludeGID}).
broadcast(Pid, FromGID, Packet) ->
gen_server:cast(Pid, {broadcast, FromGID, Packet}).
%% gen_server. %% gen_server.
init([UniID, QuestID, ZoneID, ZoneData]) -> init([UniID, QuestID, ZoneID, ZoneData]) ->
@ -70,10 +75,16 @@ handle_call(setid, _From, State) ->
handle_call({enter, GID}, _From, State) -> handle_call({enter, GID}, _From, State) ->
[LID|FreeLIDs] = State#state.freelids, [LID|FreeLIDs] = State#state.freelids,
egs_users:set_zone(GID, self(), LID), egs_users:set_zone(GID, self(), LID),
Players = State#state.players, {ok, Spawn} = egs_users:read(GID),
PlayersGID = players_gid(Players), egs_users:broadcast({egs, player_spawn, Spawn}, players_gid(State#state.players)),
egs_users:broadcast_spawn(GID, PlayersGID), {reply, LID, State#state{players=[{GID, LID}|State#state.players], freelids=FreeLIDs}};
{reply, LID, State#state{players=[{GID, LID}|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) -> handle_call({get_all_players, ExcludeGID}, _From, State) ->
{reply, lists:delete(ExcludeGID, players_gid(State#state.players)), State}; {reply, lists:delete(ExcludeGID, players_gid(State#state.players)), State};
@ -84,13 +95,10 @@ handle_call(stop, _From, State) ->
handle_call(_Request, _From, State) -> handle_call(_Request, _From, State) ->
{reply, ignored, State}. {reply, ignored, State}.
handle_cast({leave, GID}, State) -> handle_cast({broadcast, FromGID, Packet}, State) ->
{_, LID} = lists:keyfind(GID, 1, State#state.players), PlayersGID = lists:delete(FromGID, players_gid(State#state.players)),
Players = lists:delete({GID, LID}, State#state.players), egs_users:broadcast({egs, cast, Packet}, PlayersGID),
PlayersGID = players_gid(Players), {noreply, State};
FreeLIDs = State#state.freelids,
egs_users:broadcast_unspawn(GID, PlayersGID),
{noreply, State#state{players=Players, freelids=[LID|FreeLIDs]}};
handle_cast(_Msg, State) -> handle_cast(_Msg, State) ->
{noreply, State}. {noreply, State}.

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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> %% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin. %% @copyright 2010-2011 Loïc Hoguin.
%% @doc Party gen_server. %% @doc Party gen_server.
%% %%
%% This file is part of EGS. %% 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

@ -22,4 +22,6 @@
%% @doc Files in the DATA folder. %% @doc Files in the DATA folder.
{{folder, "DATA"}, [ {{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

@ -1,5 +1,5 @@
%% @author Loïc Hoguin <essen@dev-extend.eu> %% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin. %% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS patch files database and cache manager. %% @doc EGS patch files database and cache manager.
%% %%
%% This file is part of EGS. %% This file is part of EGS.
@ -19,17 +19,18 @@
-module(egs_patch_files_db). -module(egs_patch_files_db).
-behavior(gen_server). -behavior(gen_server).
-export([start_link/0, stop/0, list/0, check/3, get_size/1, get_info/1, reload/0]). %% API. -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. -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"). -include_lib("kernel/include/file.hrl").
-record(state, {list_bin=[], files=[]}). -record(state, {list_bin=[], files=[]}).
-record(file, {crc, size, folder, filename_bin, full_filename}). -record(file, {crc, size, folder, filename_bin, full_filename}).
%% Use the module name for the server's name.
-define(SERVER, ?MODULE).
%% API. %% API.
%% @spec start_link() -> {ok,Pid::pid()} %% @spec start_link() -> {ok,Pid::pid()}
@ -106,21 +107,23 @@ code_change(_OldVsn, State, _Extra) ->
%% Internal. %% Internal.
build_state() -> build_state() ->
{ok, Terms} = file:consult("priv/patch.conf"), {ok, App} = application:get_application(),
PrivDir = code:priv_dir(App),
{ok, Terms} = file:consult([PrivDir, "/patch.conf"]),
Folders = proplists:get_value(folders, Terms), Folders = proplists:get_value(folders, Terms),
{ListBin, Files} = build_list_bin(Folders, Terms), {ListBin, Files} = build_list_bin(Folders, Terms, [PrivDir, "/files/"]),
#state{list_bin=ListBin, files=Files}. #state{list_bin=ListBin, files=Files}.
%% The file number must start at 0. %% The file number must start at 0.
build_list_bin(Folders, Terms) -> build_list_bin(Folders, Terms, PatchDir) ->
build_list_bin(Folders, Terms, 0, [], []). build_list_bin(Folders, Terms, PatchDir, 0, [], []).
build_list_bin([], _Terms, _N, Acc, FilesAcc) -> build_list_bin([], _Terms, _PatchDir, _N, Acc, FilesAcc) ->
Bin = list_to_binary(lists:reverse(Acc)), 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 = << 16#08:32/little, 16#06:32/little, Bin/binary, 16#08:32/little, 16#08:32/little >>,
{Bin2, lists:flatten(FilesAcc)}; {Bin2, lists:flatten(FilesAcc)};
build_list_bin([Folder|Tail], Terms, N, Acc, FilesAcc) -> build_list_bin([Folder|Tail], Terms, PatchDir, N, Acc, FilesAcc) ->
Filenames = proplists:get_value({folder, Folder}, Terms), Filenames = proplists:get_value({folder, Folder}, Terms),
{BinFiles, Files, N2} = build_files_bin(Folder, Filenames, N), {BinFiles, Files, N2} = build_files_bin(Folder, Filenames, PatchDir, N),
BinFiles2 = case Folder of BinFiles2 = case Folder of
root -> BinFiles; root -> BinFiles;
_Any -> _Any ->
@ -129,17 +132,17 @@ build_list_bin([Folder|Tail], Terms, N, Acc, FilesAcc) ->
<< 16#48:32/little, 16#09:32/little, FolderBin/binary, 0:Padding, << 16#48:32/little, 16#09:32/little, FolderBin/binary, 0:Padding,
BinFiles/binary, 16#08:32/little, 16#0a:32/little >> BinFiles/binary, 16#08:32/little, 16#0a:32/little >>
end, end,
build_list_bin(Tail, Terms, N2, [BinFiles2|Acc], [Files|FilesAcc]). build_list_bin(Tail, Terms, PatchDir, N2, [BinFiles2|Acc], [Files|FilesAcc]).
build_files_bin(Folder, Filenames, N) -> build_files_bin(Folder, Filenames, PatchDir, N) ->
build_files_bin(Folder, Filenames, N, [], []). build_files_bin(Folder, Filenames, PatchDir, N, [], []).
build_files_bin(_Folder, [], N, Acc, FilesAcc) -> build_files_bin(_Folder, [], _PatchDir, N, Acc, FilesAcc) ->
Bin = list_to_binary(lists:reverse(Acc)), Bin = list_to_binary(lists:reverse(Acc)),
{Bin, FilesAcc, N}; {Bin, FilesAcc, N};
build_files_bin(Folder, [Filename|Tail], N, Acc, FilesAcc) -> build_files_bin(Folder, [Filename|Tail], PatchDir, N, Acc, FilesAcc) ->
FullFilename = case Folder of FullFilename = case Folder of
root -> ["priv/patch/"|Filename]; root -> [PatchDir|Filename];
_Any -> ["priv/patch/",Folder,"/"|Filename] _Any -> [PatchDir,Folder,"/"|Filename]
end, end,
Size = file_get_size(FullFilename), Size = file_get_size(FullFilename),
CRC = file_get_crc(FullFilename), CRC = file_get_crc(FullFilename),
@ -147,7 +150,7 @@ build_files_bin(Folder, [Filename|Tail], N, Acc, FilesAcc) ->
Padding = 8 * (64 - length(Filename)), Padding = 8 * (64 - length(Filename)),
FilenameBin2 = << FilenameBin/binary, 0:Padding >>, FilenameBin2 = << FilenameBin/binary, 0:Padding >>,
Bin = << 16#4c:32/little, 16#07:32/little, N:32/little, FilenameBin2/binary >>, Bin = << 16#4c:32/little, 16#07:32/little, N:32/little, FilenameBin2/binary >>,
build_files_bin(Folder, Tail, N + 1, [Bin|Acc], [{N, #file{crc=CRC, size=Size, folder=Folder, filename_bin=FilenameBin2, full_filename=FullFilename}}|FilesAcc]). 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) -> file_get_size(Filename) ->
{ok, FileInfo} = file:read_file_info(Filename), {ok, FileInfo} = file:read_file_info(Filename),

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

@ -1,6 +1,4 @@
%% @author Loïc Hoguin <essen@dev-extend.eu> %% Copyright (c) 2011, 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. %% This file is part of EGS.
%% %%
@ -17,22 +15,18 @@
%% You should have received a copy of the GNU Affero General Public License %% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>. %% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_zones_sup). -module(egs_patch_sup).
-behaviour(supervisor). -behaviour(supervisor).
-export([start_link/0]). %% API. -export([start_link/0]). %% API.
-export([init/1]). %% supervisor. -export([init/1]). %% Supervisor.
-define(SUPERVISOR, ?MODULE). -spec start_link() -> {ok, pid()}.
%% API.
-spec start_link() -> {ok, Pid::pid()}.
start_link() -> start_link() ->
supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []). supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%% supervisor.
-spec init([]) -> {ok, {{one_for_one, 10, 10}, [supervisor:child_spec(), ...]}}.
init([]) -> init([]) ->
Procs = [], 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}}. {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

@ -1,7 +1,3 @@
%% @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. %% This file is part of EGS.
%% %%
%% EGS is free software: you can redistribute it and/or modify %% EGS is free software: you can redistribute it and/or modify
@ -17,22 +13,19 @@
%% You should have received a copy of the GNU Affero General Public License %% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>. %% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_quests_sup). -module(egs_store_sup).
-behaviour(supervisor). -behaviour(supervisor).
-export([start_link/0]). %% API. -export([start_link/0]). %% API.
-export([init/1]). %% supervisor. -export([init/1]). %% Supervisor.
-define(SUPERVISOR, ?MODULE). -spec start_link() -> {ok, pid()}.
%% API.
-spec start_link() -> {ok, Pid::pid()}.
start_link() -> start_link() ->
supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []). supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%% supervisor.
-spec init([]) -> {ok, {{one_for_one, 10, 10}, [supervisor:child_spec(), ...]}}.
init([]) -> init([]) ->
Procs = [], {ok, {{one_for_one, 10, 10}, [
{ok, {{one_for_one, 10, 10}, Procs}}. {egs_store, {egs_store, start_link, []},
permanent, 5000, worker, [egs_store]}
]}}.

View File

@ -1,6 +1,6 @@
/* /*
@author Loïc Hoguin <essen@dev-extend.eu> @author Loïc Hoguin <essen@dev-extend.eu>
@copyright 2010 Loïc Hoguin. @copyright 2010-2011 Loïc Hoguin.
@doc PRS Erlang driver for EGS. @doc PRS Erlang driver for EGS.
This file is part of EGS. This file is part of EGS.
@ -71,4 +71,4 @@ static ErlNifFunc nif_funcs[] = {
{"compress", 1, compress_nif} {"compress", 1, compress_nif}
}; };
ERL_NIF_INIT(egs_prs, nif_funcs, load, reload, upgrade, unload) 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
]}
]}.

View File

@ -1,5 +1,5 @@
%% @author Loïc Hoguin <essen@dev-extend.eu> %% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin. %% @copyright 2010-2011 Loïc Hoguin.
%% @doc EGS file creation functions. %% @doc EGS file creation functions.
%% %%
%% This file is part of EGS. %% This file is part of EGS.
@ -17,12 +17,13 @@
%% You should have received a copy of the GNU Affero General Public License %% You should have received a copy of the GNU Affero General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>. %% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_prs). -module(prs).
-export([init/0, compress/1]). -export([init/0, compress/1]).
-on_load(init/0). -on_load(init/0).
init() -> init() ->
erlang:load_nif("priv/egs_drv", 0). PrivDir = code:priv_dir(prs),
erlang:load_nif(PrivDir ++ "/prs_drv", 0).
compress(_SrcBin) -> compress(_SrcBin) ->
exit(nif_library_not_loaded). erlang:nif_error(not_loaded).

1
c_src/.gitignore vendored
View File

@ -1 +0,0 @@
*.o

View File

@ -1 +0,0 @@
*

View File

@ -1 +0,0 @@
*

View File

@ -1 +0,0 @@
*

View File

@ -1 +0,0 @@
*

2
ebin/.gitignore vendored
View File

@ -1,2 +0,0 @@
*.beam
egs.app

Binary file not shown.

Binary file not shown.

Binary file not shown.

2
priv/.gitignore vendored
View File

@ -1,2 +0,0 @@
egs_drv.so
patch.conf

View File

@ -27,9 +27,26 @@
%% @doc Game server IP address and port. %% @doc Game server IP address and port.
%% They can be modified freely without problem. %% They can be modified freely without problem.
%% Note that the port should be available and above 1024. %% Note that the port should be available and above 1024.
{game_server, {<< 127, 0, 0, 1 >>, 12061}}. {game_server, {<< 91, 121, 75, 204 >>, 12061}}.
%% Caps and limitations. %% Caps and limitations.
%% @doc Maximum level players can reach. %% @doc Maximum level players can reach.
{level_cap, 200}. {level_cap, 200}.
%% Flags.
%% @todo doc
{value_flags, [
"EGS_VALUE_FLAG"
]}.
%% @todo doc
{bool_flags, [
"EGS_BOOL_FLAG"
]}.
%% @todo doc
{temp_flags, [
"EGS_TEMP_FLAG"
]}.

View File

@ -1 +0,0 @@
*

View File

@ -27,15 +27,15 @@
%% @todo Default enemy_level to 1 if unspecified (lobbies). %% @todo Default enemy_level to 1 if unspecified (lobbies).
%% @todo Default sets to [100] if unspecified (lobbies). %% @todo Default sets to [100] if unspecified (lobbies).
{zones, [ {zones, [
{ 0, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [1, 2, 3, 4, 103]}]}, { 0, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [1, 2, 3, 4, 5, 103]}]}
{ 1, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9001]}]}, %% { 1, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9001]}]},
{ 2, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9000]}]}, %% { 2, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9000]}]},
{ 3, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9102]}]}, %% { 3, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9102]}]},
{ 4, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9010]}]}, %% { 4, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9010]}]},
{ 7, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9200, 9202]}]}, %% { 7, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9200, 9202]}]},
{11, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [5, 100, 101, 102, 110]}]}, %% {11, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [100, 101, 102, 110]}]},
{12, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [100, 101, 102]}]}, %% {12, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [100, 101, 102]}]},
{13, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [100, 101, 102]}]} %% {13, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [100, 101, 102]}]}
]}. ]}.
{temp_flags, []}. {temp_flags, []}.
@ -100,14 +100,13 @@
{{1100000, 0, 3, 5}, {1100000, 0, 103, 0}}, {{1100000, 0, 3, 5}, {1100000, 0, 103, 0}},
%% @todo Exit 6. %% @todo Exit 6.
%% 4rd floor. %% 4rd floor.
{{1100000, 0, 4, 0}, {1100000, 0, 3, 1}}, {{1100000, 0, 4, 0}, {1100000, 0, 3, 1}},
{{1100000, 0, 4, 1}, {1100000, 11, 5, 0}}, {{1100000, 0, 4, 1}, {1100000, 0, 5, 0}},
{{1100000, 0, 4, 2}, {1104000, 0, 900, 0}}, {{1100000, 0, 4, 2}, {1104000, 0, 900, 0}},
{{1100000, 0, 4, 3}, {1104000, 0, 900, 0}}, {{1100000, 0, 4, 3}, {1104000, 0, 900, 0}},
{{1100000, 0, 4, 4}, {1104000, 0, 900, 0}}, {{1100000, 0, 4, 4}, {1104000, 0, 900, 0}},
%% 5th floor. %% 5th floor.
%% @todo Add 5th floor to zone 0. {{1100000, 0, 5, 0}, {1100000, 0, 4, 1}},
{{1100000, 11, 5, 0}, {1100000, 0, 4, 1}},
%% 2nd floor shops. %% 2nd floor shops.
{{1100000, 11, 101, 0}, {1100000, 0, 2, 3}}, {{1100000, 11, 101, 0}, {1100000, 0, 2, 3}},
{{1100000, 12, 101, 1}, {1100000, 0, 2, 4}}, {{1100000, 12, 101, 1}, {1100000, 0, 2, 4}},

View File

@ -83,6 +83,10 @@ event entr_unit0004 ->
push 0, npc.talk_on, %% Linear Line NPC. push 0, npc.talk_on, %% Linear Line NPC.
push 1, npc.talk_on. %% Space Docks NPC. push 1, npc.talk_on. %% Space Docks NPC.
%% @doc Enter map 5 event. Initialize the labels.
event entr_unit0005 ->
push 6, push 0, obj.set_caption. %% Elevators.
%% @doc Enter map 103 event. Initialize the label. %% @doc Enter map 103 event. Initialize the label.
event entr_unit0103 -> event entr_unit0103 ->
push 3, push 0, obj.set_caption. %% Exits to 3rd floor. push 3, push 0, obj.set_caption. %% Exits to 3rd floor.
@ -118,7 +122,7 @@ function coli_unit0001_elevator ->
push 4, %% selected option: 1st floor push 4, %% selected option: 1st floor
mes.select_win_b, mes.select_win_b,
case case
0 -> push 11, push 5, push 0, player.change_unit; 0 -> push 0, push 5, num_get nElevatorEntry, player.change_unit;
1 -> push 0, push 4, num_get nElevatorEntry, player.change_unit; 1 -> push 0, push 4, num_get nElevatorEntry, player.change_unit;
2 -> push 0, push 3, num_get nElevatorEntry, player.change_unit; 2 -> push 0, push 3, num_get nElevatorEntry, player.change_unit;
3 -> push 0, push 2, num_get nElevatorEntry, player.change_unit 3 -> push 0, push 2, num_get nElevatorEntry, player.change_unit
@ -154,7 +158,7 @@ function coli_unit0002_elevator ->
push 3, %% selected option: 2nd floor push 3, %% selected option: 2nd floor
mes.select_win_b, mes.select_win_b,
case case
0 -> push 11, push 5, push 0, player.change_unit; 0 -> push 0, push 5, num_get nElevatorEntry, player.change_unit;
1 -> push 0, push 4, num_get nElevatorEntry, player.change_unit; 1 -> push 0, push 4, num_get nElevatorEntry, player.change_unit;
2 -> push 0, push 3, num_get nElevatorEntry, player.change_unit; 2 -> push 0, push 3, num_get nElevatorEntry, player.change_unit;
4 -> push 0, push 1, num_get nElevatorEntry, player.change_unit 4 -> push 0, push 1, num_get nElevatorEntry, player.change_unit
@ -204,7 +208,7 @@ function coli_unit0003_elevator ->
push 2, %% selected option: 3rd floor push 2, %% selected option: 3rd floor
mes.select_win_b, mes.select_win_b,
case case
0 -> push 11, push 5, push 0, player.change_unit; 0 -> push 0, push 5, num_get nElevatorEntry, player.change_unit;
1 -> push 0, push 4, num_get nElevatorEntry, player.change_unit; 1 -> push 0, push 4, num_get nElevatorEntry, player.change_unit;
3 -> push 0, push 2, num_get nElevatorEntry, player.change_unit; 3 -> push 0, push 2, num_get nElevatorEntry, player.change_unit;
4 -> push 0, push 1, num_get nElevatorEntry, player.change_unit 4 -> push 0, push 1, num_get nElevatorEntry, player.change_unit
@ -254,7 +258,7 @@ function coli_unit0004_elevator ->
push 1, %% selected option: 4th floor push 1, %% selected option: 4th floor
mes.select_win_b, mes.select_win_b,
case case
0 -> push 11, push 5, push 0, player.change_unit; 0 -> push 0, push 5, num_get nElevatorEntry, player.change_unit;
2 -> push 0, push 3, num_get nElevatorEntry, player.change_unit; 2 -> push 0, push 3, num_get nElevatorEntry, player.change_unit;
3 -> push 0, push 2, num_get nElevatorEntry, player.change_unit; 3 -> push 0, push 2, num_get nElevatorEntry, player.change_unit;
4 -> push 0, push 1, num_get nElevatorEntry, player.change_unit 4 -> push 0, push 1, num_get nElevatorEntry, player.change_unit
@ -289,6 +293,42 @@ event coli_unit0004_obje023 ->
num_get nElevatorEntry, num_get nElevatorEntry,
obj.coli_end. obj.coli_end.
%% Map 5.
function coli_unit0005_elevator ->
player.pad_off,
push 39, %% return
push 29, %% 1st floor
push 31, %% 2nd floor
push 33, %% 3rd floor
push 35, %% 4th floor
push 38, %% 5th floor
push 27, %% stringid question
push 6, %% number of options
push 0, %% selected option: 5th floor
mes.select_win_b,
case
1 -> push 0, push 4, num_get nElevatorEntry, player.change_unit;
2 -> push 0, push 3, num_get nElevatorEntry, player.change_unit;
3 -> push 0, push 2, num_get nElevatorEntry, player.change_unit;
4 -> push 0, push 1, num_get nElevatorEntry, player.change_unit
end,
player.pad_on.
event coli_unit0005_obje022 ->
push 22,
num_set nElevatorEntry,
coli_unit0005_elevator,
num_get nElevatorEntry,
obj.coli_end.
event coli_unit0005_obje023 ->
push 23,
num_set nElevatorEntry,
coli_unit0005_elevator,
num_get nElevatorEntry,
obj.coli_end.
%% NPCs. %% NPCs.
num_var nTransportNPC. num_var nTransportNPC.

View File

@ -1881,6 +1881,53 @@
] ]
]}. ]}.
{{map, 5}, [
[ %% Always available.
{chair, { 90.0, 16.0, 197.0}, {0.0, 187.0, 0.0}, [{id, 1}]},
{chair, { 104.0, 16.0, 194.0}, {0.0, 200.0, 0.0}, [{id, 2}]},
{chair, { 116.0, 16.0, 187.0}, {0.0, 220.0, 0.0}, [{id, 3}]},
{chair, { 127.0, 16.0, 177.0}, {0.0, 235.0, 0.0}, [{id, 4}]},
{chair, { 134.0, 16.0, 164.0}, {0.0, 250.0, 0.0}, [{id, 5}]},
{chair, { 136.0, 16.0, 150.0}, {0.0, 260.0, 0.0}, [{id, 6}]},
{chair, { -90.0, 16.0, 197.0}, {0.0, 173.0, 0.0}, [{id, 7}]},
{chair, {-104.0, 16.0, 194.0}, {0.0, 160.0, 0.0}, [{id, 8}]},
{chair, {-116.0, 16.0, 187.0}, {0.0, 140.0, 0.0}, [{id, 9}]},
{chair, {-127.0, 16.0, 177.0}, {0.0, 125.0, 0.0}, [{id, 10}]},
{chair, {-134.0, 16.0, 164.0}, {0.0, 110.0, 0.0}, [{id, 11}]},
{chair, {-136.0, 16.0, 150.0}, {0.0, 100.0, 0.0}, [{id, 12}]},
{door, { 0.0, 0.0, 640.0}, {0.0, 0.0, 0.0}, [{model, 61}]},
{door, { 25.0, 30.0, -290.0}, {0.0, 0.0, 0.0}, [{model, 66}]},
{door, {-25.0, 30.0, -290.0}, {0.0, 0.0, 0.0}, [{model, 66}]},
{exit, {0.0, 0.0, 620.0}, {0.0, 0.0, 0.0}, [
{entryid, 0}, {type, map}, {animation, run},
{exit_box, {0.0, 70.0, 20.0, 20.0}}, {exit_movement, {0.0, 0.0, 45.0}},
{camera_box, {0.0, 70.0, 19.0, 90.0}}, {camera_movement, {0.0, 30.0, -100.0}}
]},
{entrance, { 0.0, 0.0, 550.0}, {0.0, 180.0, 0.0}, [{entryid, 0}]},
{entrance, { 25.0, 30.0, -260.0}, {0.0, 0.0, 0.0}, [{entryid, 23}]},
{entrance, {-25.0, 30.0, -260.0}, {0.0, 0.0, 0.0}, [{entryid, 22}]},
{label, { 25.0, 51.0, -290.0}, {0.0, 0.0, 0.0}, [{id, 0}, {box, {20.0, 60.0, 30.0, 180.0}}]},
{label, {-25.0, 51.0, -290.0}, {0.0, 0.0, 0.0}, [{id, 0}, {box, {20.0, 60.0, 30.0, 180.0}}]},
{npc, { 0.0, 11.0, 85.0}, {0.0, 0.0, 0.0}, [{model, 80}, {id, 0}, {talk_radius, 30.0}]},
{npc, { 25.0, 11.0, 60.0}, {0.0, 90.0, 0.0}, [{model, 81}, {id, 1}]},
{npc, {-25.0, 11.0, 60.0}, {0.0, -90.0, 0.0}, [{model, 81}, {id, 2}]},
{npc, { 0.0, 11.0, 35.0}, {0.0, 180.0, 0.0}, [{model, 81}, {id, 3}]},
{npc, {-70.0, 0.0, 305.0}, {0.0, 90.0, 0.0}, [{model, 81}, {id, 4}]},
{pp_cube, {60.0, 0.0, 285.0}, {0.0, 0.0, 0.0}, []},
{sensor, { 25.0, 31.0, -290.0}, {0.0, 0.0, 0.0}, [{id, 23}, {box, {30.0, 40.0, 40.0, 40.0}}]},
{sensor, {-25.0, 31.0, -290.0}, {0.0, 0.0, 0.0}, [{id, 22}, {box, {30.0, 40.0, 40.0, 40.0}}]},
{uni_cube, {-30.0, 0.0, 485.0}, {0.0, 0.0, 0.0}, [{i, 0}, {entryid, 0}]}
]
]}.
{{map, 103}, [ {{map, 103}, [
[ %% Always available. [ %% Always available.
{chair, {-599.0, 186.0, -1049.0}, {0.0, 180.0, 0.0}, [{id, 1}, {stand_dist, 8.0}]}, {chair, {-599.0, 186.0, -1049.0}, {0.0, 180.0, 0.0}, [{id, 1}, {stand_dist, 8.0}]},

BIN
rebar vendored

Binary file not shown.

19
rebar.config Normal file
View File

@ -0,0 +1,19 @@
{sub_dirs, [
"apps/egs",
"apps/egs_net",
"apps/egs_patch",
"apps/egs_store",
"apps/prs"
]}.
{dialyzer_opts, [src, {warnings, [
behaviours,
error_handling,
race_conditions,
unmatched_returns
%% underspecs
]}]}.
{erl_opts, [
%% bin_opt_info,
%% warnings_as_errors,
warn_export_all
]}.

View File

@ -1,70 +0,0 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin.
%% @doc EGS startup code.
%%
%% 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).
-compile(export_all).
-include("include/records.hrl").
%% @spec ensure_started(App) -> ok
%% @doc Make sure the given App is started.
ensure_started(App) ->
case application:start(App) of
ok -> ok;
{error, {already_started, App}} -> ok
end.
%% @spec start() -> ok
%% @doc Start the EGS server.
start() ->
ensure_started(crypto),
ensure_started(public_key),
ensure_started(ssl),
ssl:seed(crypto:rand_bytes(256)),
ensure_started(mnesia),
application:start(egs).
%% @spec stop() -> ok
%% @doc Stop the EGS server.
stop() ->
Res = application:stop(egs),
application:stop(mnesia),
application:stop(ssl),
application:stop(public_key),
application:stop(crypto),
Res.
%% @doc Send a global message.
global(Message) ->
if length(Message) > 511 ->
io:format("global: message too long~n");
true ->
{ok, List} = egs_users:select(all),
lists:foreach(fun(User) -> User#users.pid ! {egs, notice, top, Message} end, List)
end.
%% @doc Warp all players to a new map.
warp(QuestID, ZoneID, MapID, EntryID) ->
{ok, List} = egs_users:select(all),
lists:foreach(fun(User) -> User#users.pid ! {egs, warp, QuestID, ZoneID, MapID, EntryID} end, List).
%% @doc Warp one player to a new map.
warp(GID, QuestID, ZoneID, MapID, EntryID) ->
{ok, User} = egs_users:read(GID),
User#users.pid ! {egs, warp, QuestID, ZoneID, MapID, EntryID}.

View File

@ -1,65 +0,0 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 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).
-export([get_folder/1, key_auth/2, key_auth_init/1, key_auth_timeout/1, login_auth/2]).
-define(TABLE, accounts).
-include("include/records.hrl").
%% @todo Temporary code until we properly save the player data.
get_folder(GID) ->
{atomic, [#accounts{username=Username, password=Password}]} = mnesia:transaction(fun() -> mnesia:read({?TABLE, GID}) end),
<< Username/binary, "-", Password/binary >>.
key_auth(GID, AuthKey) ->
{atomic, [#accounts{auth_state=AuthState}]} = mnesia:transaction(fun() -> mnesia:read({?TABLE, GID}) end),
case AuthState of
{wait_for_authentication, AuthKey, TRef} ->
timer:cancel(TRef),
mnesia:transaction(fun() ->
Account = mnesia:read({?TABLE, GID}),
mnesia:write(Account#accounts{auth_state=undefined})
end),
ok;
_Any ->
{error, badarg}
end.
key_auth_init(GID) ->
AuthKey = crypto:rand_bytes(4),
TRef = timer:apply_after(10000, ?MODULE, key_auth_timeout, [GID]),
mnesia:transaction(fun() ->
[Account] = mnesia:read({?TABLE, GID}),
mnesia:write(Account#accounts{auth_state={wait_for_authentication, AuthKey, TRef}})
end),
{ok, AuthKey}.
key_auth_timeout(GID) ->
mnesia:transaction(fun() ->
Account = mnesia:read({?TABLE, GID}),
mnesia:write(Account#accounts{auth_state=undefined})
end).
%% @todo Properly handle login authentication when accounts are saved.
login_auth(Username, Password) ->
GID = 10000000 + mnesia:dirty_update_counter(counters, gid, 1),
mnesia:transaction(fun() -> mnesia:write(#accounts{gid=GID, username=Username, password=Password}) end),
{ok, GID}.

View File

@ -1,75 +0,0 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 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]).
-include("include/records.hrl").
%% @spec start(_Type, _StartArgs) -> ServerRet
%% @doc application start callback for egs.
start(_Type, _StartArgs) ->
case is_fresh_startup() of
true ->
db_init();
{exists, Tables} ->
ok = mnesia:wait_for_tables(Tables, 20000)
end,
egs_sup:start_link().
%% @spec stop(_State) -> ServerRet
%% @doc application stop callback for egs.
stop(_State) ->
ok.
%% @spec is_fresh_startup() -> true | false
%% @doc Returns true if mnesia has not been initialized with the egs schema.
%% Thanks to Dale Harvey for this function posted to the erlang questions mailing list.
is_fresh_startup() ->
Node = node(),
case mnesia:system_info(tables) of
[schema] -> true;
Tables ->
case mnesia:table_info(schema, cookie) of
{_, Node} -> {exists, Tables};
_ -> true
end
end.
%% @spec db_init() -> ok
%% @doc Initialize the database.
db_init() ->
Nodes = [node()],
case mnesia:system_info(is_running) of
yes ->
error_logger:info_report("stopping mnesia"),
mnesia:stop();
_ -> pass
end,
mnesia:create_schema(Nodes),
error_logger:info_report("mnesia schema created"),
error_logger:info_report("starting mnesia"),
mnesia:start(),
mnesia:create_table(accounts, [{attributes, record_info(fields, accounts)}]),
mnesia:create_table(counters, [{attributes, record_info(fields, counters)}]),
mnesia:create_table(psu_object, [{attributes, record_info(fields, psu_object)}]),
mnesia:create_table(users, [{attributes, record_info(fields, users)}]),
error_logger:info_report("mnesia tables created"),
ok.

View File

@ -1,107 +0,0 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 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([keepalive/1, info/2, cast/3, raw/3, event/2]).
-include("include/records.hrl").
%% @doc Send a keepalive.
keepalive(#state{socket=Socket}) ->
psu_proto:send_keepalive(Socket).
%% @doc We don't expect any message here.
info(_Msg, _State) ->
ok.
%% @doc Nothing to broadcast.
cast(_Command, _Data, _State) ->
ok.
%% @doc Dismiss all raw commands with a log notice.
%% @todo Have a log event handler instead.
raw(Command, _Data, State) ->
io:format("~p (~p): dismissed command ~4.16.0b~n", [?MODULE, State#state.gid, Command]).
%% Events.
%% @doc Character screen selection request and delivery.
event(char_select_request, #state{gid=GID}) ->
Folder = egs_accounts:get_folder(GID),
psu_game:send_0d03(data_load(Folder, 0), data_load(Folder, 1), data_load(Folder, 2), data_load(Folder, 3));
%% @doc The options default to 0 for everything except brightness to 4.
%% @todo Don't forget to check for the character's name.
event({char_select_create, Slot, CharBin}, #state{gid=GID}) ->
%% check for valid character appearance
%~ << _Name:512, RaceID:8, GenderID:8, _TypeID:8, AppearanceBin:776/bits, _/bits >> = CharBin,
%~ Race = proplists:get_value(RaceID, [{0, human}, {1, newman}, {2, cast}, {3, beast}]),
%~ Gender = proplists:get_value(GenderID, [{0, male}, {1, female}]),
%~ Appearance = psu_appearance:binary_to_tuple(Race, AppearanceBin),
%~ psu_characters:validate_name(Name),
%~ psu_appearance:validate_char_create(Race, Gender, Appearance),
%% end of check, continue doing it wrong past that point for now
Folder = egs_accounts:get_folder(GID),
Dir = io_lib:format("save/~s", [Folder]),
File = io_lib:format("~s/~b-character", [Dir, Slot]),
_ = file:make_dir(Dir),
file:write_file(File, CharBin),
file:write_file(io_lib:format("~s.options", [File]), << 0:128, 4, 0:56 >>);
%% @doc Load the selected character into the game's default universe.
event({char_select_enter, Slot, _BackToPreviousField}, State=#state{gid=GID}) ->
{ok, User} = egs_users:read(GID),
Folder = egs_accounts:get_folder(GID),
[{status, 1}, {char, CharBin}, {options, OptionsBin}] = data_load(Folder, Slot),
<< Name:512/bits, RaceBin:8, GenderBin:8, ClassBin:8, AppearanceBin:776/bits, _/bits >> = CharBin,
Race = psu_characters:race_binary_to_atom(RaceBin),
Gender = psu_characters:gender_binary_to_atom(GenderBin),
Class = psu_characters:class_binary_to_atom(ClassBin),
Appearance = psu_appearance:binary_to_tuple(Race, AppearanceBin),
Options = psu_characters:options_binary_to_tuple(OptionsBin),
Character = #characters{slot=Slot, name=Name, race=Race, gender=Gender, class=Class, appearance=Appearance, options=Options}, % TODO: temporary set the slot here, won't be needed later
UniID = egs_universes:defaultid(),
egs_universes:enter(UniID),
User2 = User#users{uni=UniID, character=Character, area={1100000, 0, 4}, entryid=0},
egs_users:write(User2),
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, User3} = egs_users:read(GID),
State2 = State#state{slot=Slot},
psu_game:char_load(User3, State2),
{ok, egs_game, State2}.
%% Internal.
%% @doc Load the given character's data.
%% @todo This function is temporary until we get permanent mnesia accounts.
data_load(Folder, Number) ->
Filename = io_lib:format("save/~s/~b-character", [Folder, Number]),
case file:read_file(Filename) of
{ok, Char} ->
{ok, Options} = file:read_file(io_lib:format("~s.options", [Filename])),
[{status, 1}, {char, Char}, {options, Options}];
{error, _Reason} ->
[{status, 0}, {char, << 0:2208 >>}]
end.

View File

@ -1,50 +0,0 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin.
%% @doc General purpose module for monitoring exit signals of linked processes.
%%
%% 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_exit_mon).
-export([start_link/1]). %% External.
-export([start/1, loop/1]). %% Internal.
%% @spec start_link(CleanupFn) -> {ok,Pid::pid()}
%% @doc Start the monitor and return the process' Pid.
start_link(CallbackFn) ->
Pid = spawn(?MODULE, start, [CallbackFn]),
{ok, Pid}.
%% @spec start(CallbackFn) -> ok
%% @doc Start the main loop.
start(CallbackFn) ->
error_logger:info_report(io_lib:format("egs_exit_mon started with callback ~p", [CallbackFn])),
process_flag(trap_exit, true),
?MODULE:loop(CallbackFn).
%% @spec loop(CallbackFn) -> ok
%% @doc Main loop, trap exit messages and call the callback function.
loop(CallbackFn = {Module, Function}) ->
receive
{'EXIT', Pid, _} ->
spawn(Module, Function, [Pid]);
{link, Pid} ->
link(Pid);
_ ->
reload
after 5000 ->
reload
end,
?MODULE:loop(CallbackFn).

View File

@ -1,792 +0,0 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 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([keepalive/1, info/2, cast/3, raw/3, event/2]).
-include("include/records.hrl").
%% @doc Send a keepalive.
keepalive(#state{socket=Socket}) ->
psu_proto:send_keepalive(Socket).
%% @doc Forward the broadcasted command to the client.
info({egs, cast, Command}, #state{gid=GID}) ->
<< A:64/bits, _:32, B:96/bits, _:64, C/bits >> = Command,
psu_game:send(<< A/binary, 16#00011300:32, B/binary, 16#00011300:32, GID:32/little-unsigned-integer, C/binary >>);
%% @doc Forward the chat message to the client.
info({egs, chat, FromGID, ChatTypeID, ChatGID, ChatName, ChatModifiers, ChatMessage}, State) ->
psu_proto:send_0304(FromGID, ChatTypeID, ChatGID, ChatName, ChatModifiers, ChatMessage, State);
info({egs, notice, Type, Message}, State) ->
psu_proto:send_0228(Type, 2, Message, State);
%% @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}, State) ->
psu_proto:send_0111(Player, 6, State),
psu_proto:send_010d(Player, State),
psu_proto:send_0205(Player, 0, State),
psu_proto:send_0203(Player, State),
psu_proto:send_0201(Player, State);
%% @doc Inform the client that a player has unspawn.
info({egs, player_unspawn, Player}, State) ->
psu_proto:send_0204(Player, State);
%% @doc Warp the player to the given location.
info({egs, warp, QuestID, ZoneID, MapID, EntryID}, State) ->
event({area_change, QuestID, ZoneID, MapID, EntryID}, State).
%% 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, State=#state{gid=GID}) ->
<< _:424, Dir:24/little-unsigned-integer, _PrevCoords:96, X:32/little-float, Y:32/little-float, Z:32/little-float,
QuestID:32/little-unsigned-integer, ZoneID:32/little-unsigned-integer, MapID:32/little-unsigned-integer, EntryID:32/little-unsigned-integer, _: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, State);
%% @doc Stand still. Save the position and then dispatch it.
cast(16#0514, Data, State=#state{gid=GID}) ->
<< _:424, Dir:24/little-unsigned-integer, X:32/little-float, Y:32/little-float, Z:32/little-float,
QuestID:32/little-unsigned-integer, ZoneID:32/little-unsigned-integer,
MapID:32/little-unsigned-integer, EntryID:32/little-unsigned-integer, _/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, State);
%% @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 State.
cast(Command, Data, #state{gid=GID})
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,
case egs_users:read(GID) of
{error, _Reason} ->
ignore;
{ok, Self} ->
LID = Self#users.lid,
Packet = << A/binary, 16#00011300:32, GID:32/little-unsigned-integer, B/binary,
GID:32/little-unsigned-integer, LID:32/little-unsigned-integer, C/binary >>,
{ok, SpawnList} = egs_users:select({neighbors, Self}),
lists:foreach(fun(User) -> User#users.pid ! {egs, cast, Packet} end, SpawnList)
end.
%% 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 >>, #state{gid=GID}) ->
<< SpawnID:32/little-unsigned-integer, _:64, Type:32/little-unsigned-integer, _:64 >> = Data,
case Type of
7 -> % spawn cleared @todo 1201 sent back with same values apparently, but not always
log("cleared spawn ~b", [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 -> psu_game:send_1205(EventID, BlockID, 0)
end;
_ ->
ignore
end;
%% @todo Handle this packet.
%% @todo 3rd Unsafe Passage C, EventID 10 BlockID 2 = mission cleared?
raw(16#0404, << _:352, Data/bits >>, _State) ->
<< EventID:8, BlockID:8, _:16, Value:8, _/bits >> = Data,
log("unknown command 0404: eventid ~b blockid ~b value ~b", [EventID, BlockID, Value]),
psu_game:send_1205(EventID, BlockID, Value);
%% @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, #state{gid=GID}) ->
psu_game:send(<< 16#0a090300:32, 0:32, 16#00011300:32, GID:32/little-unsigned-integer, 0:64, 16#00011300:32, GID:32/little-unsigned-integer, 0:64, 16#00003300:32, 0:32 >>);
%% @todo Figure out this command.
raw(16#0c11, << _:352, A:32/little, B:32/little >>, #state{gid=GID}) ->
log("0c11 ~p ~p", [A, B]),
psu_game:send(<< 16#0c120300:32, 0:160, 16#00011300:32, GID:32/little-unsigned-integer, 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 >>, #state{gid=GID}) ->
<< Flag:128/bits, A:16/bits, _:8, B/bits >> = Data,
log("flag handler for ~s", [re:replace(Flag, "\\0+", "", [global, {return, binary}])]),
psu_game:send(<< 16#0d040300:32, 0:160, 16#00011300:32, GID:32/little-unsigned-integer, 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 >>, _State) ->
<< A:32/little-unsigned-integer, 0:16, B:16/little-unsigned-integer, 0:16, C:16/little-unsigned-integer, 0, Whut:8, D:16/little-unsigned-integer, 0:16,
E:16/little-unsigned-integer, 0:16, F:16/little-unsigned-integer, G:16/little-unsigned-integer, H:16/little-unsigned-integer, I:32/little-unsigned-integer >> = Data,
log("init vehicle: ~b ~b ~b ~b ~b ~b ~b ~b ~b ~b", [A, B, C, Whut, D, E, F, G, H, I]),
psu_game:send(<< (psu_game:header(16#1208))/binary, A:32/little-unsigned-integer, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32,
0:16, B:16/little-unsigned-integer, 0:16, C:16/little-unsigned-integer, 0:16, D:16/little-unsigned-integer, 0:112,
E:16/little-unsigned-integer, 0:16, F:16/little-unsigned-integer, H:16/little-unsigned-integer, 1, 0, 100, 0, 10, 0, G:16/little-unsigned-integer, 0:16 >>);
%% @doc Enter vehicle.
%% @todo Separate the reply.
raw(16#0f02, << _:352, Data/bits >>, _State) ->
<< A:32/little-unsigned-integer, B:32/little-unsigned-integer, C:32/little-unsigned-integer >> = Data,
log("enter vehicle: ~b ~b ~b", [A, B, C]),
HP = 100,
psu_game:send(<< (psu_game:header(16#120a))/binary, A:32/little-unsigned-integer, B:32/little-unsigned-integer, C:32/little-unsigned-integer, HP:32/little-unsigned-integer >>);
%% @doc Sent right after entering the vehicle. Can't move without it.
%% @todo Separate the reply.
raw(16#0f07, << _:352, Data/bits >>, _State) ->
<< A:32/little-unsigned-integer, B:32/little-unsigned-integer >> = Data,
log("after enter vehicle: ~b ~b", [A, B]),
psu_game:send(<< (psu_game:header(16#120f))/binary, A:32/little-unsigned-integer, B:32/little-unsigned-integer >>);
%% @todo Not sure yet.
raw(16#1019, _Data, _State) ->
ignore;
%~ psu_game:send(<< (psu_game:header(16#1019))/binary, 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 >>, _State) ->
psu_game:send_110e(Data);
%% @doc Probably asking permission to start the video (used for syncing?).
raw(16#1112, << _:352, Data/bits >>, _State) ->
psu_game:send_1113(Data);
%% @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 >>, _State) ->
<< Value:32/little-unsigned-integer >> = Data,
log("command 1216 with value ~b", [Value]),
psu_game:send_1216(Value);
%% @doc Dismiss all unknown raw commands with a log notice.
%% @todo Have a log event handler instead.
raw(Command, _Data, State) ->
io:format("~p (~p): dismissed command ~4.16.0b~n", [?MODULE, State#state.gid, Command]).
%% Events.
%% @todo When changing lobby to the room, or room to lobby, we must perform an universe change.
%% @todo Probably move area_load inside the event and make other events call this one when needed.
event({area_change, QuestID, ZoneID, MapID, EntryID}, State) ->
event({area_change, QuestID, ZoneID, MapID, EntryID, 16#ffffffff}, State);
event({area_change, QuestID, ZoneID, MapID, EntryID, PartyPos}, State) ->
case PartyPos of
16#ffffffff ->
log("area change (~b,~b,~b,~b,~b)", [QuestID, ZoneID, MapID, EntryID, PartyPos]),
psu_game:area_load(QuestID, ZoneID, MapID, EntryID, State);
_Any -> %% @todo Handle area_change event for NPCs in story missions.
ignore
end;
%% @doc After the character has been (re)loaded, change the area he's in.
%% @todo The area_load function should probably not change the user's values.
%% @todo Remove that ugly code when the above is done.
event(char_load_complete, State=#state{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}, State);
%% @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, _FromTypeID, FromGID, _FromName, Modifiers, ChatMsg}, #state{gid=UserGID}) ->
[BcastTypeID, BcastGID, BcastName] = case FromGID of
0 -> %% This probably shouldn't happen. Just make it crash on purpose.
log("chat FromGID=0"),
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.character)#characters.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.character)#characters.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}]),
log("chat from ~s: ~s", [[re:replace(LogName, "\\0", "", [global, {return, binary}])], [re:replace(LogMessage, "\\0", "", [global, {return, binary}])]]),
%% broadcast
{ok, List} = egs_users:select(all),
lists:foreach(fun(X) -> X#users.pid ! {egs, chat, UserGID, BcastTypeID, BcastGID, BcastName, Modifiers, ChatMsg} end, List);
%% @todo There's at least 9 different sets of locations. Handle all of them correctly.
event(counter_background_locations_request, _State) ->
psu_game:send_170c();
%% @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}, State=#state{gid=GID}) ->
log("counter load ~b", [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),
{ok, ZoneData} = file:read_file("data/lobby/counter.zone.nbl"),
%% load counter
psu_proto:send_0c00(User, State),
psu_proto:send_020e(QuestData, State),
psu_proto:send_0a05(State),
psu_proto:send_010d(User, State),
psu_proto:send_0200(0, mission, State),
psu_proto:send_020f(ZoneData, 0, 255, State),
State2 = State#state{areanb=State#state.areanb + 1},
psu_proto:send_0205(User, 0, State2),
psu_proto:send_100e(CounterID, "Counter", State2),
psu_proto:send_0215(0, State2),
psu_proto:send_0215(0, State2),
psu_proto:send_020c(State2),
psu_game:send_1202(),
psu_proto:send_1204(State2),
psu_game:send_1206(),
psu_game:send_1207(),
psu_game:send_1212(),
psu_proto:send_0201(User, State2),
psu_proto:send_0a06(User, State2),
case User#users.partypid of
undefined -> ignore;
_ -> psu_game:send_022c(0, 16#12)
end,
State3 = State2#state{areanb=State2#state.areanb + 1},
psu_proto:send_0208(State3),
psu_proto:send_0236(State3),
{ok, State3};
%% @todo Handle parties to join.
event(counter_join_party_request, State) ->
psu_proto:send_1701(State);
%% @doc Leave mission counter handler.
event(counter_leave, State=#state{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}, State);
%% @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}, State) ->
log("counter options request ~p", [CounterID]),
psu_proto:send_1711(egs_counters_db:bg(CounterID), State);
%% @todo Handle when the party already exists! And stop doing it wrong.
event(counter_party_info_request, #state{gid=GID}) ->
{ok, User} = egs_users:read(GID),
psu_game:send_1706((User#users.character)#characters.name);
%% @todo Item distribution is always set to random for now.
event(counter_party_options_request, _State) ->
psu_game:send_170a();
%% @doc Request the counter's quest files.
event({counter_quest_files_request, CounterID}, State) ->
log("counter quest files request ~p", [CounterID]),
psu_proto:send_0c06(egs_counters_db:pack(CounterID), State);
%% @doc Counter available mission list request handler.
event({counter_quest_options_request, CounterID}, State) ->
log("counter quest options request ~p", [CounterID]),
psu_proto:send_0c10(egs_counters_db:opts(CounterID), State);
%% @todo A and B are mostly unknown. Like most of everything else from the command 0e00...
event({hit, FromTargetID, ToTargetID, A, B}, State=#state{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, State);
_ ->
PlayerHP = (NewUser#users.character)#characters.currenthp,
case lists:member(death, TargetSE) of
true -> SE = 16#01000200;
false -> SE = 16#01000000
end,
psu_game:send(<< 16#0e070300:32, 0:160, 16#00011300:32, GID:32/little-unsigned-integer, 0:64,
1:32/little-unsigned-integer, 16#01050000:32, Damage:32/little-unsigned-integer,
A/binary, 0:64, PlayerHP:32/little-unsigned-integer, 0:32, SE:32,
0:32, TargetHP:32/little-unsigned-integer, 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 ->
psu_proto:send_0115(NewUser, ToTargetID, State);
true -> ignore
end,
%% save
egs_users:write(NewUser);
event({hits, Hits}, State) ->
events(Hits, State);
event({item_description_request, ItemID}, State) ->
psu_proto:send_0a11(ItemID, egs_items_db:desc(ItemID), State);
%% @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}, #state{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 >>,
psu_game:send(<< 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,
psu_game:send(<< 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,
psu_game:send(<< 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}, #state{gid=GID}) ->
{ItemID, _Variables} = egs_users:item_nth(GID, ItemIndex),
egs_users:item_qty_add(GID, ItemIndex, -1),
<< Category:8, _:24 >> = << ItemID:32 >>,
psu_game:send(<< 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}, #state{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,
psu_game:send(<< 16#01050300:32, 0:64, GID:32/little-unsigned-integer, 0:64, 16#00011300:32, GID:32/little-unsigned-integer,
0:64, TargetGID:32/little-unsigned-integer, TargetLID:32/little-unsigned-integer, ItemIndex, 2, Category, A, B:32/little-unsigned-integer >>);
%% @todo Just ignore the meseta price for now and send the player where he wanna be!
event(lobby_transport_request, State) ->
psu_proto:send_0c08(State);
event(lumilass_options_request, State=#state{gid=GID}) ->
{ok, User} = egs_users:read(GID),
psu_proto:send_1a03(User, State);
%% @todo Probably replenish the player HP when entering a non-mission area rather than when aborting the mission?
event(mission_abort, State=#state{gid=GID}) ->
psu_proto:send_1006(11, State),
{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
Character = User#users.character,
MaxHP = Character#characters.maxhp,
NewCharacter = Character#characters{currenthp=MaxHP},
NewUser = User#users{character=NewCharacter, instancepid=undefined},
egs_users:write(NewUser),
%% map change
if User#users.areatype =:= mission ->
PrevArea = User#users.prev_area,
event({area_change, element(1, PrevArea), element(2, PrevArea), element(3, PrevArea), User#users.prev_entryid}, State);
true -> ignore
end;
%% @todo Forward the mission start to other players of the same party, whatever their location is.
event({mission_start, QuestID}, State) ->
log("mission start ~b", [QuestID]),
psu_proto:send_1020(State),
psu_game:send_1015(QuestID),
psu_game:send_0c02();
%% @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}, State=#state{gid=GID}) ->
{ok, User} = egs_users:read(GID),
%% Create NPC.
log("npc force invite ~p", [NPCid]),
TmpNPCUser = egs_npc_db:create(NPCid, ((User#users.character)#characters.mainlevel)#level.number),
%% 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.
Character = NPCUser#users.character,
SentNPCCharacter = Character#characters{gid=NPCid, npcid=NPCid},
SentNPCUser = NPCUser#users{character=SentNPCCharacter},
psu_proto:send_010d(SentNPCUser, State),
psu_proto:send_0201(SentNPCUser, State),
psu_proto:send_0215(0, State),
psu_game:send_0a04(SentNPCUser#users.gid),
psu_game:send_022c(0, 16#12),
psu_game:send_1004(npc_mission, SentNPCUser, PartyPos),
psu_game:send_100f((SentNPCUser#users.character)#characters.npcid, PartyPos),
psu_game:send_1601(PartyPos);
%% @todo Also at the end send a 101a (NPC:16, PartyPos:16, ffffffff). Not sure about PartyPos.
event({npc_invite, NPCid}, #state{gid=GID}) ->
{ok, User} = egs_users:read(GID),
%% Create NPC.
log("invited npcid ~b", [NPCid]),
TmpNPCUser = egs_npc_db:create(NPCid, ((User#users.character)#characters.mainlevel)#level.number),
%% Create and join party.
case User#users.partypid of
undefined ->
{ok, PartyPid} = psu_party:start_link(GID),
psu_game:send_022c(0, 16#12);
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.
Character = NPCUser#users.character,
SentNPCCharacter = Character#characters{gid=NPCid, npcid=NPCid},
SentNPCUser = NPCUser#users{character=SentNPCCharacter},
psu_game:send_1004(npc_invite, SentNPCUser, PartyPos),
psu_game:send_101a(NPCid, PartyPos);
%% @todo Should be 0115(money) 010a03(confirm sale).
event({npc_shop_buy, ShopItemIndex, QuantityOrColor}, State=#state{gid=GID}) ->
ShopID = egs_users:shop_get(GID),
ItemID = egs_shops_db:nth(ShopID, ShopItemIndex + 1),
log("npc shop ~p buy itemid ~8.16.0b quantity/color+1 ~p", [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),
psu_proto:send_0115(User, State), %% @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,
psu_game:send(<< 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, (psu_game:build_item_variables(ItemID, ItemUUID, Variables))/binary,
UCS2Name/binary, 0:NamePadding, RarityInt:8, Category:8, SellPrice:32/little, (psu_game:build_item_constants(Constants))/binary >>);
%% @todo Currently send the normal items shop for all shops, differentiate.
event({npc_shop_enter, ShopID}, #state{gid=GID}) ->
log("npc shop enter ~p", [ShopID]),
egs_users:shop_enter(GID, ShopID),
psu_game:send_010a(egs_shops_db:read(ShopID));
event({npc_shop_leave, ShopID}, #state{gid=GID}) ->
log("npc shop leave ~p", [ShopID]),
egs_users:shop_leave(GID),
psu_game:send(<< 16#010a0300:32, 0:64, GID:32/little-unsigned-integer, 0:64, 16#00011300:32,
GID:32/little-unsigned-integer, 0:64, GID:32/little-unsigned-integer, 0:32 >>);
%% @todo Should be 0115(money) 010a03(confirm sale).
event({npc_shop_sell, InventoryItemIndex, Quantity}, _State) ->
log("npc shop sell itemindex ~p quantity ~p", [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}, State) ->
log("npc shop request ~p", [ShopID]),
case ShopID of
80 -> psu_proto:send_1a02(17, 17, 3, 9, State); %% lumilass
90 -> psu_proto:send_1a02(5, 1, 4, 5, State); %% parum weapon grinding
91 -> psu_proto:send_1a02(5, 5, 4, 7, State); %% tenora weapon grinding
92 -> psu_proto:send_1a02(5, 8, 4, 0, State); %% yohmei weapon grinding
93 -> psu_proto:send_1a02(5, 18, 4, 0, State); %% kubara weapon grinding
_ -> psu_proto:send_1a02(0, 1, 0, 0, State)
end;
%% @todo Not sure what are those hardcoded values.
event({object_boss_gate_activate, ObjectID}, _State) ->
psu_game:send_1213(ObjectID, 0),
psu_game:send_1215(2, 16#7008),
%% @todo Following sent after the warp?
psu_game:send_1213(37, 0),
%% @todo Why resend this?
psu_game:send_1213(ObjectID, 0);
event({object_boss_gate_enter, ObjectID}, _State) ->
psu_game:send_1213(ObjectID, 1);
%% @todo Do we need to send something back here?
event({object_boss_gate_leave, _ObjectID}, _State) ->
ignore;
event({object_box_destroy, ObjectID}, _State) ->
psu_game:send_1213(ObjectID, 3);
%% @todo Second send_1211 argument should be User#users.lid. Fix when it's correctly handled.
event({object_chair_sit, ObjectTargetID}, _State) ->
%~ {ok, User} = egs_users:read(get(gid)),
psu_game:send_1211(ObjectTargetID, 0, 8, 0);
%% @todo Second psu_game:send_1211 argument should be User#users.lid. Fix when it's correctly handled.
event({object_chair_stand, ObjectTargetID}, _State) ->
%~ {ok, User} = egs_users:read(get(gid)),
psu_game:send_1211(ObjectTargetID, 0, 8, 2);
event({object_crystal_activate, ObjectID}, _State) ->
psu_game:send_1213(ObjectID, 1);
%% @doc Server-side event.
event({object_event_trigger, BlockID, EventID}, _State) ->
psu_game:send_1205(EventID, BlockID, 0);
event({object_goggle_target_activate, ObjectID}, #state{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, EventID} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
psu_game:send_1205(EventID, BlockID, 0),
psu_game:send_1213(ObjectID, 8);
%% @todo Make NPC characters heal too.
event({object_healing_pad_tick, [_PartyPos]}, State=#state{gid=GID}) ->
{ok, User} = egs_users:read(GID),
Character = User#users.character,
if Character#characters.currenthp =:= Character#characters.maxhp -> ignore;
true ->
NewHP = Character#characters.currenthp + Character#characters.maxhp div 10,
NewHP2 = if NewHP > Character#characters.maxhp -> Character#characters.maxhp; true -> NewHP end,
User2 = User#users{character=Character#characters{currenthp=NewHP2}},
egs_users:write(User2),
psu_proto:send_0117(User2, State),
psu_proto:send_0111(User2, 4, State)
end;
event({object_key_console_enable, ObjectID}, #state{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, [EventID|_]} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
psu_game:send_1205(EventID, BlockID, 0),
psu_game:send_1213(ObjectID, 1);
event({object_key_console_init, ObjectID}, #state{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, [_, EventID, _]} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
psu_game:send_1205(EventID, BlockID, 0);
event({object_key_console_open_gate, ObjectID}, #state{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, [_, _, EventID]} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
psu_game:send_1205(EventID, BlockID, 0),
psu_game:send_1213(ObjectID, 1);
%% @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}, #state{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, [EventID|_]} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
psu_game:send_1205(EventID, BlockID, 0),
psu_game:send_1213(ObjectID, 1);
%% @todo Some switch objects apparently work differently, like the light switch in Mines in MAG'.
event({object_switch_off, ObjectID}, #state{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, EventID} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
psu_game:send_1205(EventID, BlockID, 1),
psu_game:send_1213(ObjectID, 0);
event({object_switch_on, ObjectID}, #state{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, EventID} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
psu_game:send_1205(EventID, BlockID, 0),
psu_game:send_1213(ObjectID, 1);
event({object_vehicle_boost_enable, ObjectID}, _State) ->
psu_game:send_1213(ObjectID, 1);
event({object_vehicle_boost_respawn, ObjectID}, _State) ->
psu_game:send_1213(ObjectID, 0);
%% @todo Second send_1211 argument should be User#users.lid. Fix when it's correctly handled.
event({object_warp_take, BlockID, ListNb, ObjectNb}, #state{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),
psu_game:send_0503(User#users.pos),
psu_game:send_1211(16#ffffffff, 0, 14, 0);
%% @todo Don't send_0204 if the player is removed from the party while in the lobby I guess.
event({party_remove_member, PartyPos}, State=#state{gid=GID}) ->
log("party remove member ~b", [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.character)#characters.type of
npc -> egs_users:delete(RemovedGID);
_ -> ignore
end,
psu_proto:send_1006(8, PartyPos, State),
psu_proto:send_0204(RemovedUser, State),
psu_proto:send_0215(0, State);
event({player_options_change, Options}, #state{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, State=#state{gid=GID}) ->
% @todo send_0115(get(gid), 16#ffffffff, LV=1, EXP=idk, Money=1000), % apparently sent everytime you die...
%% use scape:
NewHP = 10,
{ok, User} = egs_users:read(GID),
Char = User#users.character,
User2 = User#users{character=Char#characters{currenthp=NewHP}},
egs_users:write(User2),
psu_proto:send_0117(User2, State),
psu_proto:send_1022(User2, State);
%% red screen with return to lobby choice:
%~ psu_proto:send_0111(User2, 3, 1, State);
%% @todo Refill the player's HP to maximum, remove SEs etc.
event(player_death_return_to_lobby, State=#state{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}, State);
event(player_type_availability_request, State) ->
psu_proto:send_1a07(State);
event(player_type_capabilities_request, _State) ->
psu_game:send_0113();
event(ppcube_request, _State) ->
psu_game:send_1a04();
event(unicube_request, State) ->
psu_proto:send_021e(egs_universes:all(), State);
%% @todo When selecting 'Your room', don't load a default room that's not yours.
event({unicube_select, cancel, _EntryID}, _State) ->
ignore;
event({unicube_select, Selection, EntryID}, State=#state{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,
psu_proto:send_0230(State),
%% 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),
psu_game:char_load(User2, State).
%% Internal.
%% @doc Trigger many events.
events(Events, State) ->
[event(Event, State) || Event <- Events],
ok.
%% @doc Log message to the console.
log(Message) ->
io:format("~p: ~s~n", [get(gid), Message]).
log(Message, Format) ->
FormattedMessage = io_lib:format(Message, Format),
log(FormattedMessage).

View File

@ -1,70 +0,0 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin.
%% @doc Game server 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_server).
-export([start_link/1, on_exit/1, init/1]).
-include("include/records.hrl").
%% @spec start_link(Port) -> {ok,Pid::pid()}
%% @doc Start the game server.
start_link(Port) ->
{ok, MPid} = egs_exit_mon:start_link({?MODULE, on_exit}),
register(egs_game_server_exit_mon, MPid),
LPid = spawn(egs_network, listen, [Port, ?MODULE]),
{ok, LPid}.
%% @spec on_exit(Pid) -> ok
%% @doc Cleanup the data associated with the failing process.
%% @todo Cleanup the instance process if there's nobody in it anymore.
%% @todo Leave party instead of stopping it.
on_exit(Pid) ->
case egs_users:read({pid, Pid}) of
{ok, User} ->
case User#users.partypid of
undefined ->
ignore;
PartyPid ->
{ok, NPCList} = psu_party:get_npc(PartyPid),
[egs_users:delete(NPCGID) || {_Spot, NPCGID} <- NPCList],
psu_party:stop(PartyPid)
end,
egs_users:delete(User#users.gid),
case User#users.uni of
undefined ->
ignore;
UniID ->
egs_universes:leave(UniID),
{ok, List} = egs_users:select({neighbors, User}),
lists:foreach(fun(Other) -> Other#users.pid ! {egs, player_unspawn, User} end, List)
end,
io:format("game (~p): quit~n", [User#users.gid]);
{error, _Reason} ->
ignore
end.
%% @doc Initialize the game state and start receiving messages.
%% @todo Handle keepalive messages globally?
init(Socket) ->
egs_game_server_exit_mon ! {link, self()},
timer:send_interval(5000, {egs, keepalive}),
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).

View File

@ -1,108 +0,0 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin.
%% @doc Login and game servers low-level network 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_network).
-export([listen/2, recv/3]). %% API.
-export([accept/2]). %% Internal.
-define(OPTIONS, [binary, {active, true}, {reuseaddr, true}, {ssl_imp, old}, {certfile, "priv/ssl/servercert.pem"}, {keyfile, "priv/ssl/serverkey.pem"}, {password, "alpha"}]).
%% @doc Listen for connections.
listen(Port, CallbackMod) ->
error_logger:info_report(io_lib:format("listener started for ~p on port ~b", [CallbackMod, Port])),
{ok, LSocket} = ssl:listen(Port, ?OPTIONS),
?MODULE:accept(LSocket, CallbackMod).
%% @doc Accept connections.
accept(LSocket, CallbackMod) ->
case ssl:transport_accept(LSocket, 5000) of
{ok, CSocket} ->
case ssl:ssl_accept(CSocket, 5000) of
ok ->
Pid = spawn(CallbackMod, init, [CSocket]),
ssl:controlling_process(CSocket, Pid);
{error, _Reason} ->
ignore
end;
{error, _Reason} ->
ignore
end,
?MODULE:accept(LSocket, CallbackMod).
%% @doc Main loop for the network stack. Receive and handle messages.
recv(SoFar, CallbackMod, State) ->
receive
{ssl, _Any, Data} ->
{Commands, Rest} = split(<< SoFar/bits, Data/bits >>, []),
case dispatch(Commands, CallbackMod, CallbackMod, State) of
{ok, NextCallbackMod, NewState} ->
?MODULE:recv(Rest, NextCallbackMod, NewState);
closed -> closed
end;
{ssl_closed, _} ->
ssl_closed; %% exit
{ssl_error, _, _} ->
ssl_error; %% exit
{egs, keepalive} ->
CallbackMod:keepalive(State),
?MODULE:recv(SoFar, CallbackMod, State);
Tuple when element(1, Tuple) =:= egs ->
case CallbackMod:info(Tuple, State) of
{ok, NewState} -> ?MODULE:recv(SoFar, CallbackMod, NewState);
_Any -> ?MODULE:recv(SoFar, CallbackMod, State)
end;
_ ->
?MODULE:recv(SoFar, CallbackMod, State)
end.
%% @doc Dispatch the commands received to the right handler.
dispatch([], _CallbackMod, NextMod, State) ->
{ok, NextMod, State};
dispatch([Data|Tail], CallbackMod, NextMod, State) ->
Ret = case psu_proto:parse(Data) of
{command, Command, Channel} ->
case Channel of
1 -> CallbackMod:cast(Command, Data, State);
_ -> CallbackMod:raw(Command, Data, State)
end;
ignore ->
ignore;
Event ->
CallbackMod:event(Event, State)
end,
case Ret of
{ok, NewMod, NewState} ->
dispatch(Tail, CallbackMod, NewMod, NewState);
{ok, NewState} ->
dispatch(Tail, CallbackMod, NextMod, NewState);
closed ->
closed;
_Any ->
dispatch(Tail, CallbackMod, NextMod, State)
end.
%% @doc Split the network data received into commands.
split(Data, Acc) when byte_size(Data) < 4 ->
{lists:reverse(Acc), Data};
split(<< Size:32/little, _/bits >> = Data, Acc) when Size > byte_size(Data) ->
{lists:reverse(Acc), Data};
split(<< Size:32/little, _/bits >> = Data, Acc) ->
BitSize = Size * 8,
<< Split:BitSize/bits, Rest/bits >> = Data,
split(Rest, [Split|Acc]).

View File

@ -1,234 +0,0 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin.
%% @doc Patch server 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_patch_server).
-export([start_link/1]). %% External.
-export([listen/1, accept/1, init/1, recv/3]). %% Internal
-define(OPTIONS, [binary, {active, true}, {packet, 0}, {reuseaddr, true}, {send_timeout, 5000}]).
-record(state, {step=start, files=[]}).
%% @spec start_link(Port) -> {ok,Pid::pid()}
%% @doc Start the PSU patch server for inclusion in a supervisor tree.
start_link(Port) ->
Pid = spawn(?MODULE, listen, [Port]),
{ok, Pid}.
%% @spec listen(Port) -> ok
%% @doc Listen for connections.
listen(Port) ->
error_logger:info_report(io_lib:format("listener started for ~p on port ~b", [?MODULE, Port])),
{ok, LSocket} = gen_tcp:listen(Port, ?OPTIONS),
?MODULE:accept(LSocket).
%% @spec accept(LSocket) -> ok
%% @doc Accept connections.
accept(LSocket) ->
case gen_tcp:accept(LSocket, 5000) of
{ok, CSocket} ->
Pid = spawn(?MODULE, init, [CSocket]),
gen_tcp:controlling_process(CSocket, Pid);
{error, timeout} ->
reload
end,
?MODULE:accept(LSocket).
%% @spec init(CSocket) -> ok
%% @doc Send the hello packet and move on to the main loop.
init(CSocket) ->
send_01(CSocket),
recv(CSocket, << >>, #state{}).
%% @spec recv(CSocket, SoFar, State) -> ok
%% @doc Receive commands from the client and process them.
recv(CSocket, SoFar, State) ->
receive
{tcp, CSocket, Data} ->
{Commands, Rest} = split(<< SoFar/bits, Data/bits >>, []),
case handle(CSocket, Commands, State) of
closed -> closed;
State2 -> ?MODULE:recv(CSocket, Rest, State2)
end;
{tcp_closed, CSocket} ->
tcp_closed;
{tcp_error, CSocket, _Any} ->
tcp_error;
_ ->
?MODULE:recv(CSocket, SoFar, State)
end.
%% @spec split(Bin, Acc) -> {Commands, Rest}
%% @doc Split the given binary into a list of commands.
split(<< >>, Acc) ->
{lists:reverse(Acc), << >>};
split(Rest = << Size:32/little, Data/bits >>, Acc) when Data + 4 < Size ->
{lists:reverse(Acc), Rest};
split(<< Size:32/little, Cmd:16/little, _Junk:16, Rest/bits >>, Acc) ->
BitSize = 8 * Size - 64,
<< Data:BitSize/bits, Rest2/bits >> = Rest,
split(Rest2, [{command, Cmd, Size, Data}|Acc]).
%% @spec handle(CSocket, CommandsList) -> closed | State
%% @doc Handle the given commands.
handle(_CSocket, [], State) ->
State;
%% Start of file info reply.
handle(CSocket, [{command, 16#0c, 8, << >>}|Tail], State=#state{step=waitfileinfo}) ->
handle(CSocket, Tail, State#state{step=recvfileinfo});
%% File info.
handle(CSocket, [{command, 16#0d, 20, << FileNumber:32/little, CRC:32/little, Size:32/little >>}|Tail], State=#state{step=recvfileinfo}) ->
State2 = case egs_patch_files_db:check(FileNumber, CRC, Size) of
ok -> State;
invalid -> State#state{files=[FileNumber|State#state.files]}
end,
handle(CSocket, Tail, State2);
%% End of file info reply. Nothing expected from the client afterward.
handle(CSocket, [{command, 16#0e, 8, << >>}], #state{step=recvfileinfo, files=Files}) ->
case Files of
[] -> ok; %% No files to update.
_List -> update(CSocket, lists:reverse(Files))
end,
send_13(CSocket),
closed;
%% Hello reply.
%% @todo Figure out the remaining unknown values.
handle(CSocket, [{command, 16#14, 52, << 16#e44c0915:32, UnknownA:32/little, UnknownB:32/little, UnknownC:32/little,
UnknownD:32/little, _GameVersion:32/little, UnknownE:32/little, 0:128 >>}|Tail], State=#state{step=start}) ->
io:format("patch #14: ~p ~p ~p ~p ~p~n", [UnknownA, UnknownB, UnknownC, UnknownD, UnknownE]),
ListBin = egs_patch_files_db:list(),
gen_tcp:send(CSocket, ListBin),
handle(CSocket, Tail, State#state{step=waitfileinfo});
%% Unknown command.
handle(_CSocket, [{command, Cmd, _Size, Data}|_Tail], State) ->
io:format("~p: dismissed command ~2.16.0b - ~p - ~p~n", [?MODULE, Cmd, Data, State]),
closed.
%% @spec update(CSocket, Files) -> ok
%% @doc Update the invalid client files.
update(CSocket, Files) ->
Size = update_size(Files),
send_0f(CSocket, Size, length(Files)),
update_files(CSocket, root, Files).
%% @spec update_files(CSocket, Files) -> ok
%% @doc Send all the files the client needs to update.
update_files(_CSocket, _CurrentFolder, []) ->
ok;
update_files(CSocket, CurrentFolder, [FileNumber|Tail]) ->
{file, _CRC, Size, Folder, FilenameBin, FullFilename} = egs_patch_files_db:get_info(FileNumber),
case CurrentFolder of
Folder -> ok;
_Any ->
if CurrentFolder =/= root ->
send_0a(CSocket);
true -> ok
end,
if Folder =/= root ->
send_09(CSocket, Folder);
true -> ok
end
end,
send_10(CSocket, Size, FilenameBin),
update_send_file(CSocket, FullFilename),
send_12(CSocket),
update_files(CSocket, Folder, Tail).
%% @spec update_send_file(CSocket, Filename) -> ok
%% @doc Send a file by fragmenting it into many chunks of fixed size.
update_send_file(CSocket, Filename) ->
{ok, IoDevice} = file:open(Filename, [read, raw, binary]),
update_send_file(CSocket, IoDevice, 0).
update_send_file(CSocket, IoDevice, N) ->
case file:read(IoDevice, 24576) of
{ok, Data} ->
send_11(CSocket, Data, N),
update_send_file(CSocket, IoDevice, N + 1);
eof ->
file:close(IoDevice)
end.
%% @spec update_size(Files) -> Size
%% @doc Return the total size for all the files given.
update_size(Files) ->
update_size(Files, 0).
update_size([], Size) ->
Size;
update_size([FileNumber|Tail], Size) ->
FileSize = egs_patch_files_db:get_size(FileNumber),
update_size(Tail, Size + FileSize).
%% @spec send_01(CSocket) -> ok
%% @doc Hello command sent when a client connects to the server. Encryption is disabled.
send_01(CSocket) ->
Bin = << 16#28:32/little, 16#01:32/little, 16#8b9f2dfa:32, 0:96, 1:32/little, 0:96 >>,
gen_tcp:send(CSocket, Bin).
%% @spec send_09(CSocket, Folder) -> ok
%% @doc Change folder command.
send_09(CSocket, Folder) ->
FolderBin = list_to_binary(Folder),
Padding = 8 * (64 - length(Folder)),
Bin = << 16#48:32/little, 16#09:32/little, FolderBin/binary, 0:Padding >>,
gen_tcp:send(CSocket, Bin).
%% @spec send_0a(CSocket) -> ok
%% @doc Back to root folder command.
send_0a(CSocket) ->
Bin = << 16#8:32/little, 16#0a:32/little >>,
gen_tcp:send(CSocket, Bin).
%% @spec send_0f(CSocket, TotalSize, NbFiles) -> ok
%% @doc General update information command. Prepare the update screen.
send_0f(CSocket, Size, NbFiles) ->
Bin = << 16#10:32/little, 16#0f:32/little, Size:32/little, NbFiles:32/little >>,
gen_tcp:send(CSocket, Bin).
%% @spec send_10(CSocket, Size, FilenameBin) -> ok
%% @doc File update begin command. Prepare sending an individual file.
send_10(CSocket, Size, FilenameBin) ->
Bin = << 16#50:32/little, 16#10:32/little, 0:32, Size:32/little, FilenameBin/binary >>,
gen_tcp:send(CSocket, Bin).
%% @spec send_11(CSocket, Data, N) -> ok
%% @doc Command to send a file fragment.
send_11(CSocket, 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 >>,
gen_tcp:send(CSocket, Bin).
%% @spec send_12(CSocket) -> ok
%% @doc File update end command.
send_12(CSocket) ->
Bin = << 16#8:32/little, 16#12:32/little >>,
gen_tcp:send(CSocket, Bin).
%% @spec send_13(CSocket) -> ok
%% @doc Update complete command. Usually followed by the server closing the connection.
send_13(CSocket) ->
Bin = << 16#8:32/little, 16#13:32/little >>,
gen_tcp:send(CSocket, Bin).

View File

@ -1,47 +0,0 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin.
%% @doc 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([init/1]). %% Supervisor callbacks.
-export([start_link/0]). %% Other functions.
%% @spec start_link() -> ServerRet
%% @doc API for starting the supervisor.
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%% @spec init([]) -> SupervisorTree
%% @doc supervisor callback.
init([]) ->
Procs = [
{egs_conf, {egs_conf, start_link, []}, permanent, 5000, worker, dynamic},
{egs_servers_sup, {egs_servers_sup, start_link, []}, permanent, 5000, supervisor, [egs_servers_sup]},
{egs_quests_sup, {egs_quests_sup, start_link, []}, permanent, 5000, supervisor, [egs_quests_sup]},
{egs_zones_sup, {egs_zones_sup, start_link, []}, permanent, 5000, supervisor, [egs_zones_sup]},
{egs_seasons, {egs_seasons, start_link, []}, permanent, 5000, worker, dynamic},
{egs_counters_db, {egs_counters_db, start_link, []}, permanent, 5000, worker, dynamic},
{egs_items_db, {egs_items_db, start_link, []}, permanent, 5000, worker, dynamic},
{egs_npc_db, {egs_npc_db, start_link, []}, permanent, 5000, worker, dynamic},
{egs_patch_files_db, {egs_patch_files_db, start_link, []}, permanent, 5000, worker, dynamic},
{egs_quests_db, {egs_quests_db, start_link, []}, permanent, 5000, worker, dynamic},
{egs_shops_db, {egs_shops_db, start_link, []}, permanent, 5000, worker, dynamic},
{egs_universes, {egs_universes, start_link, []}, permanent, 5000, worker, dynamic}
],
{ok, {{one_for_one, 10, 10}, Procs}}.

View File

@ -1,183 +0,0 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 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).
-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, broadcast_spawn/2, broadcast_unspawn/2, set_zone/3]).
-define(TABLE, users).
-include("include/records.hrl").
-include_lib("stdlib/include/qlc.hrl").
%% @spec do(Q) -> Record
%% @doc Perform a mnesia transaction using a QLC query.
do(Q) ->
F = fun() -> qlc:e(Q) end,
{atomic, Val} = mnesia:transaction(F),
Val.
%% --
%% @spec read({pid, Pid}) -> {ok, User} | {error, badarg}
%% @spec read(ID) -> {ok, User} | {error, badarg}
read({pid, Pid}) ->
List = do(qlc:q([X || X <- mnesia:table(?TABLE), X#?TABLE.pid =:= Pid])),
case List of
[] -> {error, badarg};
[User] -> {ok, User}
end;
read(ID) ->
case mnesia:transaction(fun() -> mnesia:read({?TABLE, ID}) end) of
{atomic, []} -> {error, badarg};
{atomic, [Val]} -> {ok, Val}
end.
%% @spec select(all) -> {ok, List}
%% @spec select({neighbors, User}) -> {ok, List}
%% @todo state = undefined | {wait_for_authentication, Key} | authenticated | online
select(all) ->
List = do(qlc:q([X || X <- mnesia:table(?TABLE),
X#?TABLE.pid /= undefined
])),
{ok, List};
select({neighbors, User}) ->
List = do(qlc:q([X || X <- mnesia:table(?TABLE),
X#?TABLE.gid /= User#?TABLE.gid,
X#?TABLE.pid /= undefined,
X#?TABLE.instancepid =:= User#?TABLE.instancepid,
X#?TABLE.area =:= User#?TABLE.area
])),
{ok, List};
select(UsersGID) ->
L = [read(GID) || GID <- UsersGID],
[User || {ok, User} <- L].
%% @spec write(User) -> ok
write(User) ->
mnesia:transaction(fun() -> mnesia:write(User) end).
%% @spec delete(ID) -> ok
delete(ID) ->
mnesia:transaction(fun() -> mnesia:delete({?TABLE, ID}) end).
item_nth(GID, ItemIndex) ->
{atomic, [User]} = mnesia:transaction(fun() -> mnesia:read({?TABLE, GID}) end),
lists:nth(ItemIndex + 1, (User#users.character)#characters.inventory).
item_add(GID, ItemID, Variables) ->
{atomic, [User]} = mnesia:transaction(fun() -> mnesia:read({?TABLE, GID}) end),
Character = User#users.character,
Inventory = Character#characters.inventory,
Inventory2 = 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, 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, 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, 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, Inventory, {ItemID, #psu_trap_item_variables{quantity=Quantity3}})
end;
_ ->
New = true,
if length(Inventory) < 60 ->
Inventory ++ [{ItemID, Variables}]
end
end,
Character2 = Character#characters{inventory=Inventory2},
mnesia:transaction(fun() -> mnesia:write(User#users{character=Character2}) end),
if New =:= false -> 16#ffffffff;
true -> length(Inventory2)
end.
%% @todo Consumable items.
item_qty_add(GID, ItemIndex, QuantityDiff) ->
{atomic, [User]} = mnesia:transaction(fun() -> mnesia:read({?TABLE, GID}) end),
Character = User#users.character,
Inventory = Character#characters.inventory,
{ItemID, Variables} = lists:nth(ItemIndex + 1, 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 ->
Inventory2 = string:substr(Inventory, 1, ItemIndex) ++ string:substr(Inventory, ItemIndex + 2);
Quantity2 > 0, Quantity2 =< MaxQuantity ->
Variables2 = Variables#psu_trap_item_variables{quantity=Quantity2},
Inventory2 = string:substr(Inventory, 1, ItemIndex) ++ [{ItemID, Variables2}] ++ string:substr(Inventory, ItemIndex + 2)
end
end,
Character2 = Character#characters{inventory=Inventory2},
mnesia:transaction(fun() -> mnesia:write(User#users{character=Character2}) end).
shop_enter(GID, ShopID) ->
mnesia:transaction(fun() ->
[User] = mnesia:wread({?TABLE, GID}),
mnesia:write(User#users{shopid=ShopID})
end).
shop_leave(GID) ->
mnesia:transaction(fun() ->
[User] = mnesia:wread({?TABLE, GID}),
mnesia:write(User#users{shopid=undefined})
end).
shop_get(GID) ->
{atomic, [User]} = mnesia:transaction(fun() -> mnesia:read({?TABLE, GID}) end),
User#users.shopid.
money_add(GID, MoneyDiff) ->
mnesia:transaction(fun() ->
[User] = mnesia:wread({?TABLE, GID}),
Character = User#users.character,
Money = Character#characters.money + MoneyDiff,
if Money >= 0 ->
Character2 = Character#characters{money=Money},
mnesia:write(User#users{character=Character2})
end
end).
broadcast_spawn(GID, PlayersGID) ->
{ok, OrigUser} = read(GID),
lists:foreach(fun(DestGID) ->
{ok, DestUser} = read(DestGID),
DestUser#users.pid ! {egs, player_spawn, OrigUser}
end, PlayersGID).
broadcast_unspawn(GID, PlayersGID) ->
{ok, OrigUser} = read(GID),
lists:foreach(fun(DestGID) ->
{ok, DestUser} = read(DestGID),
DestUser#users.pid ! {egs, player_unspawn, OrigUser}
end, PlayersGID).
set_zone(GID, ZonePid, LID) ->
{ok, User} = read(GID),
write(User#users{zonepid=ZonePid, lid=LID}).

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