Compare commits
114 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ceea04c3b4 | ||
![]() |
9adab0ea87 | ||
![]() |
6579c26f98 | ||
![]() |
368bb0f7b4 | ||
![]() |
807510f669 | ||
![]() |
c69513073d | ||
![]() |
35b86b58ba | ||
![]() |
ac0c3c551b | ||
![]() |
289ce855ad | ||
![]() |
bc47fcb049 | ||
![]() |
408e7e99e6 | ||
![]() |
c42b1a85f8 | ||
![]() |
e2274666b8 | ||
![]() |
6fd1119777 | ||
![]() |
eab53bc3a7 | ||
![]() |
2325c7cf63 | ||
![]() |
8889ca8332 | ||
![]() |
23e781f498 | ||
![]() |
60b8009382 | ||
![]() |
325c1a4c10 | ||
![]() |
8f3db6480a | ||
![]() |
8363741b3a | ||
![]() |
935b490461 | ||
![]() |
948873ddec | ||
![]() |
72726bdf6c | ||
![]() |
0a399238f4 | ||
![]() |
c957d9a8b9 | ||
![]() |
d9cde30b0b | ||
![]() |
c7cb5ab589 | ||
![]() |
dc26bb82f7 | ||
![]() |
d8906226a5 | ||
![]() |
730f47d837 | ||
![]() |
d2f7c9e83f | ||
![]() |
6466f05728 | ||
![]() |
3821a1e7bf | ||
![]() |
a1bf3e43f3 | ||
![]() |
3290aba95d | ||
![]() |
72989f5332 | ||
![]() |
6cbb987ab0 | ||
![]() |
9c8ad80a07 | ||
![]() |
b5d6b3934b | ||
![]() |
cdc2c56d5c | ||
![]() |
5b255b211b | ||
![]() |
edb061662f | ||
![]() |
9268f3f7ae | ||
![]() |
dd4a228b01 | ||
![]() |
34b4a21ce7 | ||
![]() |
2a7383b9a1 | ||
![]() |
3f1d1d2fb0 | ||
![]() |
18429b8d76 | ||
![]() |
82aca844f2 | ||
![]() |
1b36ff2589 | ||
![]() |
4ff28e0939 | ||
![]() |
f21f23a92f | ||
![]() |
979e99f6a5 | ||
![]() |
58cfb8a61e | ||
![]() |
9aca48a697 | ||
![]() |
722bcf7c9e | ||
![]() |
dc77b8e804 | ||
![]() |
eaafdc213f | ||
![]() |
f8524ca9c4 | ||
![]() |
8c95aab709 | ||
![]() |
87c256edba | ||
![]() |
9c7f8f6eaa | ||
![]() |
5c234257dc | ||
![]() |
a571b9a56b | ||
![]() |
dfa5634adb | ||
![]() |
53a4b3dbbe | ||
![]() |
4faabbda8f | ||
![]() |
023214793c | ||
![]() |
250a22dea8 | ||
![]() |
f40ba44364 | ||
![]() |
d0e15316b8 | ||
![]() |
4848b3c218 | ||
![]() |
7415da1c89 | ||
![]() |
e8c1c98824 | ||
![]() |
fde0f8b3fe | ||
![]() |
82e82503fa | ||
![]() |
fde008c7a8 | ||
![]() |
564ab8749f | ||
![]() |
6dc9fb52dc | ||
![]() |
0b02718faa | ||
![]() |
333e898bb4 | ||
![]() |
64d8bf7c25 | ||
![]() |
a44fc4274f | ||
![]() |
5f0bd73303 | ||
![]() |
19350ba1ff | ||
![]() |
998263b417 | ||
![]() |
b6c1bf277d | ||
![]() |
86bb5c81b3 | ||
![]() |
d69fe073a8 | ||
![]() |
6c8b831fd2 | ||
![]() |
953da28a3e | ||
![]() |
344c534812 | ||
![]() |
3280e79743 | ||
![]() |
6e922a7ec9 | ||
![]() |
823ee73e7d | ||
![]() |
40d2eed01b | ||
![]() |
ac8d6858cd | ||
![]() |
e409241a50 | ||
![]() |
25c9548ec3 | ||
![]() |
0b8c4dbd85 | ||
![]() |
8eae404797 | ||
![]() |
840db6b7b3 | ||
![]() |
d5b5afa0a7 | ||
![]() |
344b88eec4 | ||
![]() |
69a07dfad2 | ||
![]() |
57e4e91187 | ||
![]() |
722e0a53f4 | ||
![]() |
0f64bea72d | ||
![]() |
b380fe9d23 | ||
![]() |
ecee1226aa | ||
![]() |
378e9a9927 | ||
![]() |
2de4359c32 |
7
.gitignore
vendored
7
.gitignore
vendored
@ -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
|
||||
|
23
Makefile
23
Makefile
@ -1,5 +1,5 @@
|
||||
# EGS: Erlang Game Server
|
||||
# Copyright (C) 2010 Loic Hoguin
|
||||
# Copyright (C) 2010-2011 Loic Hoguin
|
||||
#
|
||||
# This file is part of EGS.
|
||||
#
|
||||
@ -16,14 +16,23 @@
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
all: server
|
||||
REBAR = rebar
|
||||
|
||||
server:
|
||||
@./rebar compile
|
||||
all: app
|
||||
|
||||
app: deps
|
||||
@$(REBAR) compile
|
||||
|
||||
deps:
|
||||
@$(REBAR) get-deps
|
||||
|
||||
clean:
|
||||
@./rebar clean
|
||||
@$(REBAR) clean
|
||||
rm -f erl_crash.dump
|
||||
|
||||
fclean: clean
|
||||
rm -rf Mnesia.egs*
|
||||
tests:
|
||||
@$(REBAR) eunit
|
||||
@$(REBAR) ct
|
||||
|
||||
dialyze:
|
||||
@$(REBAR) dialyze
|
||||
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010-2011 Loïc Hoguin.
|
||||
%% @doc Project-wide Erlang records.
|
||||
%%
|
||||
%% This file is part of EGS.
|
||||
@ -17,64 +17,64 @@
|
||||
%% You should have received a copy of the GNU Affero General Public License
|
||||
%% 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.
|
||||
|
||||
%% @doc Per-process state used by the various EGS modules.
|
||||
-record(state, {
|
||||
socket :: sslsocket(),
|
||||
gid :: integer(),
|
||||
slot :: 0..3,
|
||||
lid = 16#ffff :: 0..16#ffff,
|
||||
%% @doc Client state. One per connected client.
|
||||
-record(egs_net, {
|
||||
socket :: ssl:sslsocket(),
|
||||
transport :: module(),
|
||||
handler :: module(),
|
||||
buffer = <<>> :: binary(),
|
||||
keepalive = false :: boolean(),
|
||||
gid = 0 :: egs:gid(),
|
||||
lid = 16#ffff :: egs:lid(),
|
||||
slot = 0 :: 0..3,
|
||||
areanb = 0 :: non_neg_integer()
|
||||
}).
|
||||
|
||||
%% @doc 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.
|
||||
%% @todo Probably can use a "param" or "extra" field to store the game-specific information (for things that don't need to be queried).
|
||||
-record(users, {
|
||||
%% General information.
|
||||
gid :: integer(),
|
||||
lid = 16#ffff :: 0..16#ffff,
|
||||
gid :: egs:gid(),
|
||||
lid = 16#ffff :: egs:lid(),
|
||||
pid :: pid(),
|
||||
time :: integer(),
|
||||
character :: tuple(), %% @todo Details.
|
||||
%% Character information.
|
||||
%% @todo Specs it.
|
||||
type = white,
|
||||
slot,
|
||||
npcid = 16#ffff,
|
||||
name,
|
||||
race,
|
||||
gender,
|
||||
class,
|
||||
level = 1,
|
||||
exp = 0,
|
||||
currenthp = 100,
|
||||
maxhp = 100,
|
||||
stats = {stats, 1000, 2000, 3000, 4000, 5000, 6000, 7000},
|
||||
money = 1000000,
|
||||
blastbar = 0,
|
||||
luck = 3,
|
||||
appearance,
|
||||
onlinestatus = 0,
|
||||
options = {options, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0},
|
||||
inventory = [],
|
||||
%% Location/state related information.
|
||||
uni :: integer(),
|
||||
questpid :: pid(),
|
||||
zonepid :: pid(),
|
||||
partypid :: pid(),
|
||||
areatype :: counter | mission | lobby | myroom,
|
||||
area :: area(),
|
||||
entryid :: entryid(),
|
||||
pos = {0.0, 0.0, 0.0, 0.0} :: position(),
|
||||
area :: egs:area(),
|
||||
entryid :: egs:entryid(),
|
||||
pos = {0.0, 0.0, 0.0, 0.0} :: egs:position(),
|
||||
shopid :: integer(),
|
||||
prev_area = {0, 0, 0} :: area(),
|
||||
prev_entryid = 0 :: entryid(),
|
||||
prev_area = {0, 0, 0} :: egs:area(),
|
||||
prev_entryid = 0 :: egs:entryid(),
|
||||
%% To be moved or deleted later on.
|
||||
instancepid :: pid()
|
||||
instancepid :: pid(),
|
||||
char
|
||||
}).
|
||||
|
||||
%% Past this point needs to be reviewed.
|
||||
@ -111,13 +111,6 @@
|
||||
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.
|
||||
|
||||
-record(stats, {atp, ata, tp, dfp, evp, mst, sta}).
|
||||
@ -128,38 +121,6 @@
|
||||
cutindisplay, mainmenucursorposition, camera3y, camera3x, camera1y, camera1x, controller, weaponswap,
|
||||
lockon, brightness, functionkeysetting, buttondetaildisplay}).
|
||||
|
||||
%% @doc Characters data structure.
|
||||
%% @todo Make a disk table for storing characters permanently. Also keep the current character in #users.
|
||||
|
||||
-record(characters, {
|
||||
gid,
|
||||
type=white,
|
||||
slot,
|
||||
npcid=16#ffff,
|
||||
name,
|
||||
race,
|
||||
gender,
|
||||
class,
|
||||
mainlevel={level, 1, 0},
|
||||
classlevels,
|
||||
currenthp=100,
|
||||
maxhp=100,
|
||||
stats={stats, 1000, 2000, 3000, 4000, 5000, 6000, 7000},
|
||||
se=[],
|
||||
money=1000000,
|
||||
blastbar=0,
|
||||
luck=3,
|
||||
playtime=0,
|
||||
appearance,
|
||||
onlinestatus=0,
|
||||
options={options, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0},
|
||||
inventory=[]
|
||||
}). % also: shortcuts partnercards blacklist npcs flags...
|
||||
|
||||
%% @doc Table containing all mission objects.
|
||||
|
||||
-record(psu_object, {id, instancepid, type, args}).
|
||||
|
||||
%% @doc Hit response data.
|
||||
|
||||
-record(hit_response, {type, user, exp, damage, targethp, targetse, events}).
|
6
apps/egs/rebar.config
Normal file
6
apps/egs/rebar.config
Normal 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]}.
|
@ -1,7 +1,7 @@
|
||||
%%-*- mode: erlang -*-
|
||||
{application, egs, [
|
||||
{description, "EGS online action-RPG game server"},
|
||||
{vsn, "0.9"},
|
||||
{vsn, "0.14"},
|
||||
{modules, []},
|
||||
{registered, []},
|
||||
{applications, [
|
||||
@ -9,7 +9,7 @@
|
||||
stdlib,
|
||||
crypto,
|
||||
ssl,
|
||||
mnesia
|
||||
cowboy
|
||||
]},
|
||||
{mod, {egs_app, []}},
|
||||
{env, []}
|
85
apps/egs/src/egs.erl
Normal file
85
apps/egs/src/egs.erl
Normal 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.
|
143
apps/egs/src/egs_accounts.erl
Normal file
143
apps/egs/src/egs_accounts.erl
Normal 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
58
apps/egs/src/egs_app.erl
Normal 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
64
apps/egs/src/egs_char.erl
Normal 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
|
||||
}}.
|
86
apps/egs/src/egs_char_select.erl
Normal file
86
apps/egs/src/egs_char_select.erl
Normal 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.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010-2011 Loïc Hoguin.
|
||||
%% @doc EGS configuration gen_server.
|
||||
%%
|
||||
%% This file is part of EGS.
|
||||
@ -19,41 +19,35 @@
|
||||
|
||||
-module(egs_conf).
|
||||
-behavior(gen_server).
|
||||
-export([start_link/0, stop/0, read/1, reload/0]). %% API.
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||
|
||||
%% Use the module name for the server's name.
|
||||
-export([start_link/0, stop/0, read/1, reload/0]). %% API.
|
||||
-export([init/1, handle_call/3, handle_cast/2,
|
||||
handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
%% API.
|
||||
|
||||
%% @spec start_link() -> {ok,Pid::pid()}
|
||||
-spec start_link() -> {ok, pid()}.
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||
|
||||
%% @spec stop() -> stopped
|
||||
-spec stop() -> stopped.
|
||||
stop() ->
|
||||
gen_server:call(?SERVER, stop).
|
||||
|
||||
%% @spec read(Key) -> Value | undefined
|
||||
-spec read(atom()) -> undefined | any().
|
||||
read(Key) ->
|
||||
gen_server:call(?SERVER, {read, Key}).
|
||||
|
||||
%% @spec reload() -> ok
|
||||
-spec reload() -> ok.
|
||||
reload() ->
|
||||
gen_server:cast(?SERVER, reload).
|
||||
|
||||
%% gen_server.
|
||||
|
||||
init([]) ->
|
||||
case file:consult("priv/egs.conf") of
|
||||
{ok, Terms} ->
|
||||
error_logger:info_report("egs_conf started"),
|
||||
{ok, Terms};
|
||||
Error ->
|
||||
error_logger:error_report(["An error occurred when trying to load the configuration file:", Error]),
|
||||
Error
|
||||
end.
|
||||
{ok, _Terms} = file:consult("priv/egs.conf").
|
||||
|
||||
handle_call({read, Key}, _From, State) ->
|
||||
{reply, proplists:get_value(Key, State), State};
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010-2011 Loïc Hoguin.
|
||||
%% @doc EGS counters database and cache manager.
|
||||
%%
|
||||
%% This file is part of EGS.
|
||||
@ -19,6 +19,7 @@
|
||||
|
||||
-module(egs_counters_db).
|
||||
-behavior(gen_server).
|
||||
|
||||
-export([start_link/0, stop/0, bg/1, opts/1, pack/1, reload/0]). %% API.
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010-2011 Loïc Hoguin.
|
||||
%% @doc EGS file creation functions.
|
||||
%%
|
||||
%% This file is part of EGS.
|
||||
@ -506,7 +506,7 @@ nbl_pack_files([], {AccH, AccD, AccP, _FilePos, _PtrIndex}) ->
|
||||
{BinD3, CompressedDataSize} = if BinDSize < 16#800 ->
|
||||
{BinD, 0};
|
||||
true ->
|
||||
BinD2 = egs_prs:compress(BinD),
|
||||
BinD2 = prs:compress(BinD),
|
||||
BinD2Size = byte_size(BinD2),
|
||||
{BinD2, BinD2Size}
|
||||
end,
|
857
apps/egs/src/egs_game.erl
Normal file
857
apps/egs/src/egs_game.erl
Normal 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).
|
62
apps/egs/src/egs_game_protocol.erl
Normal file
62
apps/egs/src/egs_game_protocol.erl
Normal 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.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010-2011 Loïc Hoguin.
|
||||
%% @doc EGS items database.
|
||||
%%
|
||||
%% This file is part of EGS.
|
||||
@ -19,6 +19,7 @@
|
||||
|
||||
-module(egs_items_db).
|
||||
-behavior(gen_server).
|
||||
|
||||
-export([start_link/0, stop/0, desc/1, read/1, reload/0]). %% API.
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||
|
||||
@ -26,7 +27,7 @@
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-include("include/records.hrl").
|
||||
-include("priv/items.hrl").
|
||||
-include("../../priv/items.hrl").
|
||||
|
||||
%% API.
|
||||
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010-2011 Loïc Hoguin.
|
||||
%% @doc Log in and authentication callback module.
|
||||
%%
|
||||
%% This file is part of EGS.
|
||||
@ -18,72 +18,64 @@
|
||||
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
-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").
|
||||
|
||||
%% @doc Don't keep alive here, authentication should go fast.
|
||||
keepalive(_State) ->
|
||||
ok.
|
||||
|
||||
%% @doc We don't expect any message here.
|
||||
info(_Msg, _State) ->
|
||||
info(_Msg, _Client) ->
|
||||
ok.
|
||||
|
||||
%% @doc Nothing to broadcast.
|
||||
cast(_Command, _Data, _State) ->
|
||||
cast(_Command, _Data, _Client) ->
|
||||
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.
|
||||
|
||||
%% @doc Reject version < 2.0009.2.
|
||||
%% @todo Reject wrong platforms too.
|
||||
event({system_client_version_info, _Entrance, _Language, _Platform, Version}, State=#state{socket=Socket}) ->
|
||||
if Version >= 2009002 -> ignore; true ->
|
||||
psu_proto:send_0231("http://psumods.co.uk/forums/comments.php?DiscussionID=40#Item_1", State),
|
||||
{ok, ErrorMsg} = file:read_file("priv/login/error_version.txt"),
|
||||
psu_proto:send_0223(ErrorMsg, State),
|
||||
ssl:close(Socket),
|
||||
closed
|
||||
end;
|
||||
%% @todo Put the URL in a configuration file.
|
||||
event({client_version, _Entrance, _Language, _Platform, Version}, Client)
|
||||
when Version < 2009002 ->
|
||||
egs_net:system_open_url(<<"http://psumods.co.uk/forums/comments.php?DiscussionID=40#Item_1">>, Client),
|
||||
{ok, Error} = file:read_file("priv/login/error_version.txt"),
|
||||
egs_net:system_auth_error(Error, Client),
|
||||
egs_net:terminate(Client),
|
||||
closed;
|
||||
event({client_version, _Entrance, _Language, _Platform, _Version}, _Client) ->
|
||||
ok;
|
||||
|
||||
%% @doc Game server info request handler.
|
||||
event(system_game_server_request, State=#state{socket=Socket}) ->
|
||||
event(system_game_server_request, Client) ->
|
||||
{ServerIP, ServerPort} = egs_conf:read(game_server),
|
||||
psu_proto:send_0216(ServerIP, ServerPort, State),
|
||||
ssl:close(Socket),
|
||||
egs_net:system_game_server_response(ServerIP, ServerPort, Client),
|
||||
egs_net:terminate(Client),
|
||||
closed;
|
||||
|
||||
%% @doc Authenticate the user by pattern matching its saved state against the key received.
|
||||
%% If the user is authenticated, send him the character flags list.
|
||||
%% @todo Remove the put calls when all the send_xxxx are moved out of psu_game and into psu_proto.
|
||||
event({system_key_auth_request, AuthGID, AuthKey}, State=#state{socket=Socket}) ->
|
||||
event({system_key_auth, AuthGID, AuthKey}, Client) ->
|
||||
egs_accounts:key_auth(AuthGID, AuthKey),
|
||||
egs_users:write(#users{gid=AuthGID, pid=self()}),
|
||||
put(socket, Socket),
|
||||
put(gid, AuthGID),
|
||||
State2 = State#state{gid=AuthGID},
|
||||
psu_proto:send_0d05(State2),
|
||||
{ok, egs_char_select, State2};
|
||||
Client2 = egs_net:set_gid(AuthGID, Client),
|
||||
ValueFlags = egs_conf:read(value_flags),
|
||||
BoolFlags = egs_conf:read(bool_flags),
|
||||
TempFlags = egs_conf:read(temp_flags),
|
||||
egs_net:account_flags(ValueFlags, BoolFlags, TempFlags, Client2),
|
||||
Client3 = egs_net:set_handler(egs_char_select, Client2),
|
||||
Client4 = egs_net:set_keepalive(Client3),
|
||||
{ok, Client4};
|
||||
|
||||
%% @doc Authentication request handler. Currently always succeed.
|
||||
%% @todo Handle real GIDs whenever there's real authentication. GID is the second SessionID in the reply.
|
||||
%% @todo Apparently it's possible to ask a question in the reply here. Used for free course on JP.
|
||||
event({system_login_auth_request, Username, Password}, State) ->
|
||||
{ok, GID} = egs_accounts:login_auth(Username, Password),
|
||||
{ok, AuthKey} = egs_accounts:key_auth_init(GID),
|
||||
event({system_login_auth, Username, Password}, Client) ->
|
||||
{ok, AuthGID} = egs_accounts:login_auth(Username, Password),
|
||||
{ok, AuthKey} = egs_accounts:key_auth_init(AuthGID),
|
||||
io:format("auth success for ~s ~s~n", [Username, Password]),
|
||||
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.
|
||||
%% @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"),
|
||||
psu_proto:send_0225(MOTD, Page, State).
|
||||
egs_net:system_motd_response(MOTD, Page, Client).
|
@ -1,6 +1,6 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010 Loïc Hoguin.
|
||||
%% @doc Login server module.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010-2011 Loïc Hoguin.
|
||||
%% @doc Cowboy protocol module for the login server.
|
||||
%%
|
||||
%% This file is part of EGS.
|
||||
%%
|
||||
@ -17,25 +17,19 @@
|
||||
%% You should have received a copy of the GNU Affero General Public License
|
||||
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
-module(egs_login_server).
|
||||
-export([start_link/1, on_exit/1, init/1]).
|
||||
-module(egs_login_protocol).
|
||||
-export([start_link/4, init/2]).
|
||||
|
||||
-include("include/records.hrl").
|
||||
|
||||
%% @spec start_link(Port) -> {ok,Pid::pid()}
|
||||
%% @doc Start the login server.
|
||||
start_link(Port) ->
|
||||
Pid = spawn(egs_network, listen, [Port, ?MODULE]),
|
||||
-spec start_link(pid(), ssl:sslsocket(), module(), []) -> {ok, pid()}.
|
||||
start_link(_ListenerPid, Socket, Transport, []) ->
|
||||
Pid = spawn_link(?MODULE, init, [Socket, Transport]),
|
||||
{ok, Pid}.
|
||||
|
||||
%% @spec on_exit(Pid) -> ok
|
||||
%% @doc Nothing to do for the login server.
|
||||
on_exit(_Pid) ->
|
||||
ok.
|
||||
|
||||
%% @doc Initialize the game state and start receiving messages.
|
||||
init(Socket) ->
|
||||
TmpGID = 16#ff000000 + mnesia:dirty_update_counter(counters, tmpgid, 1),
|
||||
State = #state{socket=Socket, gid=TmpGID},
|
||||
psu_proto:send_0202(State),
|
||||
egs_network:recv(<< >>, egs_login, State).
|
||||
-spec init(ssl:sslsocket(), module()) -> ok | closed.
|
||||
init(Socket, Transport) ->
|
||||
Client = egs_net:init(Socket, Transport, egs_login,
|
||||
egs_accounts:tmp_gid()),
|
||||
egs_net:system_hello(Client),
|
||||
egs_net:loop(Client).
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010-2011 Loïc Hoguin.
|
||||
%% @doc EGS NPC database.
|
||||
%%
|
||||
%% This file is part of EGS.
|
||||
@ -19,6 +19,7 @@
|
||||
|
||||
-module(egs_npc_db).
|
||||
-behavior(gen_server).
|
||||
|
||||
-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.
|
||||
|
||||
@ -26,7 +27,7 @@
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-include("include/records.hrl").
|
||||
-include("priv/npc.hrl").
|
||||
-include("../../priv/npc.hrl").
|
||||
|
||||
%% API.
|
||||
|
||||
@ -61,14 +62,15 @@ handle_call(all, _From, State) ->
|
||||
|
||||
%% @todo Handle stats, experience, based on level.
|
||||
handle_call({create, NPCid, BaseLevel}, _From, State) ->
|
||||
NPCGID = 16#ff000000 + mnesia:dirty_update_counter(counters, tmpgid, 1),
|
||||
NPCGID = egs_accounts:tmp_gid(),
|
||||
#npc{name=Name, race=Race, gender=Gender, class=Class, level_diff=LevelDiff, appearance=Appearance} = proplists:get_value(NPCid, ?NPC),
|
||||
TmpUCS2Name = << << X:8, 0:8 >> || X <- Name >>,
|
||||
Padding = 8 * (64 - byte_size(TmpUCS2Name)),
|
||||
UCS2Name = << TmpUCS2Name/binary, 0:Padding >>,
|
||||
Character = #characters{gid=NPCGID, slot=0, type=npc, npcid=NPCid, name=UCS2Name, race=Race, gender=Gender, class=Class, appearance=Appearance,
|
||||
mainlevel={level, calc_level(BaseLevel, LevelDiff), 0}, blastbar=0, luck=2, money=0, playtime=0, stats={stats, 0, 0, 0, 0, 0, 0, 0}, se=[], currenthp=100, maxhp=100},
|
||||
User = #users{gid=NPCGID, character=Character, areatype=lobby, area={0, 0, 0}, entryid=0},
|
||||
User = #users{gid=NPCGID, slot=0, type=npc, npcid=NPCid, name=UCS2Name, race=Race, gender=Gender, class=Class, appearance=Appearance,
|
||||
level=calc_level(BaseLevel, LevelDiff), blastbar=0, luck=2, money=0,
|
||||
stats={stats, 0, 0, 0, 0, 0, 0, 0}, currenthp=100, maxhp=100,
|
||||
areatype=lobby, area={0, 0, 0}, entryid=0},
|
||||
{reply, User, State};
|
||||
|
||||
handle_call(stop, _From, State) ->
|
726
apps/egs/src/egs_proto.erl
Normal file
726
apps/egs/src/egs_proto.erl
Normal 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).
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @doc Quest handler.
|
||||
%%
|
||||
%% This file is part of EGS.
|
||||
@ -43,7 +43,7 @@ zone_pid(Pid, ZoneID) ->
|
||||
init([UniID, QuestID]) ->
|
||||
Zones = egs_quests_db:quest_zones(QuestID),
|
||||
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}
|
||||
end, Zones),
|
||||
{ok, #state{zones=ZonesPids}}.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010-2011 Loïc Hoguin.
|
||||
%% @doc EGS quests database and cache manager.
|
||||
%%
|
||||
%% This file is part of EGS.
|
||||
@ -19,14 +19,15 @@
|
||||
|
||||
-module(egs_quests_db).
|
||||
-behavior(gen_server).
|
||||
|
||||
-export([start_link/0, stop/0, quest_nbl/1, zone_nbl/2, area_type/2, quest_zones/1, set/3, reload/0]). %% API.
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||
|
||||
-record(state, {quests=[], quests_bin=[], zones_bin=[], sets=[]}).
|
||||
|
||||
%% Use the module name for the server's name.
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-record(state, {quests=[], quests_bin=[], zones_bin=[], sets=[]}).
|
||||
|
||||
%% API.
|
||||
|
||||
%% @spec start_link() -> {ok,Pid::pid()}
|
||||
@ -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),
|
||||
ScriptBin = egs_files:load_script_bin(ZoneDir ++ "script.es"),
|
||||
ScriptBinSize = byte_size(ScriptBin),
|
||||
ScriptBin2 = egs_prs:compress(ScriptBin),
|
||||
ScriptBin2 = prs:compress(ScriptBin),
|
||||
ScriptBinSize2 = byte_size(ScriptBin2),
|
||||
ScriptBin3 = << ScriptBinSize:32/little, ScriptBinSize2:32/little, 0:32, 1:32/little, 0:96, ScriptBin2/binary >>,
|
||||
TextBin = egs_files:load_text_bin(ZoneDir ++ "text.bin.en_US.txt"),
|
@ -1,6 +1,6 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @doc Supervisor for the patch, login and game listener processes.
|
||||
%% @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.
|
||||
%%
|
||||
@ -17,27 +17,30 @@
|
||||
%% 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_servers_sup).
|
||||
-module(egs_quests_sup).
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([start_link/0]). %% API.
|
||||
-export([start_link/0, start_quest/2]). %% API.
|
||||
-export([init/1]). %% supervisor.
|
||||
|
||||
-define(SUPERVISOR, ?MODULE).
|
||||
|
||||
%% API.
|
||||
|
||||
-spec start_link() -> {ok, Pid::pid()}.
|
||||
-spec start_link() -> {ok, pid()}.
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []).
|
||||
|
||||
-spec start_quest(egs:uniid(), egs:questid()) -> {ok, pid()}.
|
||||
start_quest(UniID, QuestID) ->
|
||||
supervisor:start_child(?SUPERVISOR, [UniID, QuestID]).
|
||||
|
||||
%% supervisor.
|
||||
|
||||
-spec init([]) -> {ok, {{simple_one_for_one, 0, 1}, [{egs_quests,
|
||||
{egs_quests, start_link, []}, temporary, brutal_kill,
|
||||
worker, [egs_quests]}]}}.
|
||||
init([]) ->
|
||||
PatchPorts = egs_conf:read(patch_ports),
|
||||
LoginPorts = egs_conf:read(login_ports),
|
||||
{_ServerIP, GamePort} = egs_conf:read(game_server),
|
||||
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}}.
|
||||
{ok, {{simple_one_for_one, 0, 1}, [{egs_quests,
|
||||
{egs_quests, start_link, []}, temporary, brutal_kill,
|
||||
worker, [egs_quests]}]}}.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010-2011 Loïc Hoguin.
|
||||
%% @doc EGS script compiler.
|
||||
%%
|
||||
%% This file is part of EGS.
|
1076
apps/egs/src/egs_script_lexer.erl
Normal file
1076
apps/egs/src/egs_script_lexer.erl
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010-2011 Loïc Hoguin.
|
||||
%% @doc EGS script lexer.
|
||||
%%
|
||||
%% This file is part of EGS.
|
1331
apps/egs/src/egs_script_parser.erl
Normal file
1331
apps/egs/src/egs_script_parser.erl
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010-2011 Loïc Hoguin.
|
||||
%% @doc EGS script parser.
|
||||
%%
|
||||
%% This file is part of EGS.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010-2011 Loïc Hoguin.
|
||||
%% @doc EGS seasons management.
|
||||
%% @todo When we know how to do it we should change the lobby automatically to the next season.
|
||||
%%
|
||||
@ -20,6 +20,7 @@
|
||||
|
||||
-module(egs_seasons).
|
||||
-behavior(gen_server).
|
||||
|
||||
-export([start_link/0, stop/0, read/1]). %% API.
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010-2011 Loïc Hoguin.
|
||||
%% @doc EGS shops database.
|
||||
%%
|
||||
%% This file is part of EGS.
|
||||
@ -19,6 +19,7 @@
|
||||
|
||||
-module(egs_shops_db).
|
||||
-behavior(gen_server).
|
||||
|
||||
-export([start_link/0, stop/0, nth/2, read/1, reload/0]). %% API.
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||
|
54
apps/egs/src/egs_sup.erl
Normal file
54
apps/egs/src/egs_sup.erl
Normal 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]}.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010-2011 Loïc Hoguin.
|
||||
%% @doc EGS universes handler.
|
||||
%%
|
||||
%% This file is part of EGS.
|
||||
@ -19,14 +19,15 @@
|
||||
|
||||
-module(egs_universes).
|
||||
-behavior(gen_server).
|
||||
|
||||
-export([start_link/0, stop/0, all/0, defaultid/0, enter/1, leave/1, myroomid/0, read/1, lobby_pid/2, reload/0]). %% API.
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||
|
||||
-record(state, {unis=[], lobbies=[]}).
|
||||
|
||||
%% Use the module name for the server's name.
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-record(state, {unis=[], lobbies=[]}).
|
||||
|
||||
%% Default universe IDs.
|
||||
-define(MYROOM_ID, 21).
|
||||
-define(DEFAULT_ID, 26).
|
||||
@ -151,6 +152,6 @@ create_unis([Name|Tail], UniID, Acc) ->
|
||||
%% @doc Start lobbies for the given universe.
|
||||
init_lobbies(UniID) ->
|
||||
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}
|
||||
end, ?LOBBIES).
|
239
apps/egs/src/egs_users.erl
Normal file
239
apps/egs/src/egs_users.erl
Normal 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}.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @doc Zone handler.
|
||||
%%
|
||||
%% This file is part of EGS.
|
||||
@ -20,7 +20,7 @@
|
||||
-module(egs_zones).
|
||||
-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.
|
||||
|
||||
-record(state, {
|
||||
@ -48,12 +48,17 @@ setid(Pid) ->
|
||||
enter(Pid, GID) ->
|
||||
gen_server:call(Pid, {enter, GID}).
|
||||
|
||||
leave(undefined, _GID) ->
|
||||
ok;
|
||||
leave(Pid, GID) ->
|
||||
gen_server:cast(Pid, {leave, GID}).
|
||||
gen_server:call(Pid, {leave, GID}).
|
||||
|
||||
get_all_players(Pid, ExcludeGID) ->
|
||||
gen_server:call(Pid, {get_all_players, ExcludeGID}).
|
||||
|
||||
broadcast(Pid, FromGID, Packet) ->
|
||||
gen_server:cast(Pid, {broadcast, FromGID, Packet}).
|
||||
|
||||
%% gen_server.
|
||||
|
||||
init([UniID, QuestID, ZoneID, ZoneData]) ->
|
||||
@ -70,10 +75,16 @@ handle_call(setid, _From, State) ->
|
||||
handle_call({enter, GID}, _From, State) ->
|
||||
[LID|FreeLIDs] = State#state.freelids,
|
||||
egs_users:set_zone(GID, self(), LID),
|
||||
Players = State#state.players,
|
||||
PlayersGID = players_gid(Players),
|
||||
egs_users:broadcast_spawn(GID, PlayersGID),
|
||||
{reply, LID, State#state{players=[{GID, LID}|Players], freelids=FreeLIDs}};
|
||||
{ok, Spawn} = egs_users:read(GID),
|
||||
egs_users:broadcast({egs, player_spawn, Spawn}, players_gid(State#state.players)),
|
||||
{reply, LID, State#state{players=[{GID, LID}|State#state.players], freelids=FreeLIDs}};
|
||||
|
||||
handle_call({leave, GID}, _From, State) ->
|
||||
{_, LID} = lists:keyfind(GID, 1, State#state.players),
|
||||
Players = lists:delete({GID, LID}, State#state.players),
|
||||
{ok, Spawn} = egs_users:read(GID),
|
||||
egs_users:broadcast({egs, player_unspawn, Spawn}, players_gid(Players)),
|
||||
{reply, ok, State#state{players=Players, freelids=[LID|State#state.freelids]}};
|
||||
|
||||
handle_call({get_all_players, ExcludeGID}, _From, State) ->
|
||||
{reply, lists:delete(ExcludeGID, players_gid(State#state.players)), State};
|
||||
@ -84,13 +95,10 @@ handle_call(stop, _From, State) ->
|
||||
handle_call(_Request, _From, State) ->
|
||||
{reply, ignored, State}.
|
||||
|
||||
handle_cast({leave, GID}, State) ->
|
||||
{_, LID} = lists:keyfind(GID, 1, State#state.players),
|
||||
Players = lists:delete({GID, LID}, State#state.players),
|
||||
PlayersGID = players_gid(Players),
|
||||
FreeLIDs = State#state.freelids,
|
||||
egs_users:broadcast_unspawn(GID, PlayersGID),
|
||||
{noreply, State#state{players=Players, freelids=[LID|FreeLIDs]}};
|
||||
handle_cast({broadcast, FromGID, Packet}, State) ->
|
||||
PlayersGID = lists:delete(FromGID, players_gid(State#state.players)),
|
||||
egs_users:broadcast({egs, cast, Packet}, PlayersGID),
|
||||
{noreply, State};
|
||||
|
||||
handle_cast(_Msg, State) ->
|
||||
{noreply, State}.
|
47
apps/egs/src/egs_zones_sup.erl
Normal file
47
apps/egs/src/egs_zones_sup.erl
Normal 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]}]}}.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @doc Chair object.
|
||||
%%
|
||||
%% This file is part of EGS.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @doc Door object.
|
||||
%%
|
||||
%% This file is part of EGS.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @doc Entrance object.
|
||||
%%
|
||||
%% This file is part of EGS.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @doc Exit object.
|
||||
%%
|
||||
%% This file is part of EGS.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @doc Invisible block object.
|
||||
%%
|
||||
%% This file is part of EGS.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @doc Label object.
|
||||
%%
|
||||
%% This file is part of EGS.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @doc NPC object.
|
||||
%%
|
||||
%% This file is part of EGS.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @doc PP cube object.
|
||||
%%
|
||||
%% This file is part of EGS.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @doc Sensor object.
|
||||
%%
|
||||
%% This file is part of EGS.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @doc Static model object.
|
||||
%%
|
||||
%% This file is part of EGS.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @doc Type counter object.
|
||||
%%
|
||||
%% This file is part of EGS.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @doc Uni cube object.
|
||||
%%
|
||||
%% This file is part of EGS.
|
96
apps/egs/src/psu/psu_characters.erl
Normal file
96
apps/egs/src/psu/psu_characters.erl
Normal 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 >>.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010-2011 Loïc Hoguin.
|
||||
%% @doc Party gen_server.
|
||||
%%
|
||||
%% This file is part of EGS.
|
4
apps/egs_net/rebar.config
Normal file
4
apps/egs_net/rebar.config
Normal file
@ -0,0 +1,4 @@
|
||||
{deps, [
|
||||
{erlson, ".*", {git, "https://github.com/alavrik/erlson.git", "HEAD"}}
|
||||
]}.
|
||||
{plugins, [erlson_rebar_plugin]}.
|
12
apps/egs_net/src/egs_net.app.src
Normal file
12
apps/egs_net/src/egs_net.app.src
Normal 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
1576
apps/egs_net/src/egs_net.erl
Normal file
File diff suppressed because it is too large
Load Diff
BIN
apps/egs_patch/priv/files/DATA/1ffd0db3e0b54048caff394e9c09eda8
Normal file
BIN
apps/egs_patch/priv/files/DATA/1ffd0db3e0b54048caff394e9c09eda8
Normal file
Binary file not shown.
BIN
apps/egs_patch/priv/files/DATA/bb04cc8e1727288bd2a336d60040eff1
Normal file
BIN
apps/egs_patch/priv/files/DATA/bb04cc8e1727288bd2a336d60040eff1
Normal file
Binary file not shown.
@ -22,4 +22,6 @@
|
||||
|
||||
%% @doc Files in the DATA folder.
|
||||
{{folder, "DATA"}, [
|
||||
"1ffd0db3e0b54048caff394e9c09eda8", %% crc.bin
|
||||
"bb04cc8e1727288bd2a336d60040eff1" %% SmutFilter_J.bin
|
||||
]}.
|
3
apps/egs_patch/rebar.config
Normal file
3
apps/egs_patch/rebar.config
Normal file
@ -0,0 +1,3 @@
|
||||
{deps, [
|
||||
{cowboy, ".*", {git, "git://github.com/extend/cowboy.git", "HEAD"}}
|
||||
]}.
|
14
apps/egs_patch/src/egs_patch.app.src
Normal file
14
apps/egs_patch/src/egs_patch.app.src
Normal 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, []}
|
||||
]}.
|
47
apps/egs_patch/src/egs_patch_app.erl
Normal file
47
apps/egs_patch/src/egs_patch_app.erl
Normal 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).
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010-2011 Loïc Hoguin.
|
||||
%% @doc EGS patch files database and cache manager.
|
||||
%%
|
||||
%% This file is part of EGS.
|
||||
@ -19,17 +19,18 @@
|
||||
|
||||
-module(egs_patch_files_db).
|
||||
-behavior(gen_server).
|
||||
|
||||
-export([start_link/0, stop/0, list/0, check/3, get_size/1, get_info/1, reload/0]). %% API.
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||
|
||||
%% Use the module name for the server's name.
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-include_lib("kernel/include/file.hrl").
|
||||
|
||||
-record(state, {list_bin=[], files=[]}).
|
||||
-record(file, {crc, size, folder, filename_bin, full_filename}).
|
||||
|
||||
%% Use the module name for the server's name.
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
%% API.
|
||||
|
||||
%% @spec start_link() -> {ok,Pid::pid()}
|
||||
@ -106,21 +107,23 @@ code_change(_OldVsn, State, _Extra) ->
|
||||
%% Internal.
|
||||
|
||||
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),
|
||||
{ListBin, Files} = build_list_bin(Folders, Terms),
|
||||
{ListBin, Files} = build_list_bin(Folders, Terms, [PrivDir, "/files/"]),
|
||||
#state{list_bin=ListBin, files=Files}.
|
||||
|
||||
%% The file number must start at 0.
|
||||
build_list_bin(Folders, Terms) ->
|
||||
build_list_bin(Folders, Terms, 0, [], []).
|
||||
build_list_bin([], _Terms, _N, Acc, FilesAcc) ->
|
||||
build_list_bin(Folders, Terms, PatchDir) ->
|
||||
build_list_bin(Folders, Terms, PatchDir, 0, [], []).
|
||||
build_list_bin([], _Terms, _PatchDir, _N, Acc, FilesAcc) ->
|
||||
Bin = list_to_binary(lists:reverse(Acc)),
|
||||
Bin2 = << 16#08:32/little, 16#06:32/little, Bin/binary, 16#08:32/little, 16#08:32/little >>,
|
||||
{Bin2, lists:flatten(FilesAcc)};
|
||||
build_list_bin([Folder|Tail], Terms, N, Acc, FilesAcc) ->
|
||||
build_list_bin([Folder|Tail], Terms, PatchDir, N, Acc, FilesAcc) ->
|
||||
Filenames = proplists:get_value({folder, Folder}, Terms),
|
||||
{BinFiles, Files, N2} = build_files_bin(Folder, Filenames, N),
|
||||
{BinFiles, Files, N2} = build_files_bin(Folder, Filenames, PatchDir, N),
|
||||
BinFiles2 = case Folder of
|
||||
root -> BinFiles;
|
||||
_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,
|
||||
BinFiles/binary, 16#08:32/little, 16#0a:32/little >>
|
||||
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, N, [], []).
|
||||
build_files_bin(_Folder, [], N, Acc, FilesAcc) ->
|
||||
build_files_bin(Folder, Filenames, PatchDir, N) ->
|
||||
build_files_bin(Folder, Filenames, PatchDir, N, [], []).
|
||||
build_files_bin(_Folder, [], _PatchDir, N, Acc, FilesAcc) ->
|
||||
Bin = list_to_binary(lists:reverse(Acc)),
|
||||
{Bin, FilesAcc, N};
|
||||
build_files_bin(Folder, [Filename|Tail], N, Acc, FilesAcc) ->
|
||||
build_files_bin(Folder, [Filename|Tail], PatchDir, N, Acc, FilesAcc) ->
|
||||
FullFilename = case Folder of
|
||||
root -> ["priv/patch/"|Filename];
|
||||
_Any -> ["priv/patch/",Folder,"/"|Filename]
|
||||
root -> [PatchDir|Filename];
|
||||
_Any -> [PatchDir,Folder,"/"|Filename]
|
||||
end,
|
||||
Size = file_get_size(FullFilename),
|
||||
CRC = file_get_crc(FullFilename),
|
||||
@ -147,7 +150,7 @@ build_files_bin(Folder, [Filename|Tail], N, Acc, FilesAcc) ->
|
||||
Padding = 8 * (64 - length(Filename)),
|
||||
FilenameBin2 = << FilenameBin/binary, 0:Padding >>,
|
||||
Bin = << 16#4c:32/little, 16#07:32/little, N:32/little, FilenameBin2/binary >>,
|
||||
build_files_bin(Folder, Tail, 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) ->
|
||||
{ok, FileInfo} = file:read_file_info(Filename),
|
218
apps/egs_patch/src/egs_patch_protocol.erl
Normal file
218
apps/egs_patch/src/egs_patch_protocol.erl
Normal 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).
|
@ -1,6 +1,4 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2011 Loïc Hoguin.
|
||||
%% @doc Supervisor for the egs_zones gen_server.
|
||||
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
|
||||
%%
|
||||
%% This file is part of EGS.
|
||||
%%
|
||||
@ -17,22 +15,18 @@
|
||||
%% 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).
|
||||
-module(egs_patch_sup).
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([start_link/0]). %% API.
|
||||
-export([init/1]). %% supervisor.
|
||||
-export([init/1]). %% Supervisor.
|
||||
|
||||
-define(SUPERVISOR, ?MODULE).
|
||||
|
||||
%% API.
|
||||
|
||||
-spec start_link() -> {ok, Pid::pid()}.
|
||||
-spec start_link() -> {ok, pid()}.
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []).
|
||||
|
||||
%% supervisor.
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
-spec init([]) -> {ok, {{one_for_one, 10, 10}, [supervisor:child_spec(), ...]}}.
|
||||
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}}.
|
0
apps/egs_store/priv/.gitignore
vendored
Normal file
0
apps/egs_store/priv/.gitignore
vendored
Normal file
13
apps/egs_store/src/egs_store.app.src
Normal file
13
apps/egs_store/src/egs_store.app.src
Normal 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, []}
|
||||
]}.
|
92
apps/egs_store/src/egs_store.erl
Normal file
92
apps/egs_store/src/egs_store.erl
Normal 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}.
|
31
apps/egs_store/src/egs_store_app.erl
Normal file
31
apps/egs_store/src/egs_store_app.erl
Normal 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.
|
@ -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.
|
||||
%%
|
||||
%% 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
|
||||
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
-module(egs_quests_sup).
|
||||
-module(egs_store_sup).
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([start_link/0]). %% API.
|
||||
-export([init/1]). %% supervisor.
|
||||
-export([init/1]). %% Supervisor.
|
||||
|
||||
-define(SUPERVISOR, ?MODULE).
|
||||
|
||||
%% API.
|
||||
|
||||
-spec start_link() -> {ok, Pid::pid()}.
|
||||
-spec start_link() -> {ok, pid()}.
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []).
|
||||
|
||||
%% supervisor.
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
-spec init([]) -> {ok, {{one_for_one, 10, 10}, [supervisor:child_spec(), ...]}}.
|
||||
init([]) ->
|
||||
Procs = [],
|
||||
{ok, {{one_for_one, 10, 10}, Procs}}.
|
||||
{ok, {{one_for_one, 10, 10}, [
|
||||
{egs_store, {egs_store, start_link, []},
|
||||
permanent, 5000, worker, [egs_store]}
|
||||
]}}.
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
@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.
|
||||
|
||||
This file is part of EGS.
|
||||
@ -71,4 +71,4 @@ static ErlNifFunc nif_funcs[] = {
|
||||
{"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
11
apps/prs/src/prs.app.src
Normal file
@ -0,0 +1,11 @@
|
||||
%%-*- mode: erlang -*-
|
||||
{application, prs, [
|
||||
{description, "PRS compression library."},
|
||||
{vsn, "0.1.0"},
|
||||
{modules, []},
|
||||
{registered, []},
|
||||
{applications, [
|
||||
kernel,
|
||||
stdlib
|
||||
]}
|
||||
]}.
|
@ -1,5 +1,5 @@
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010 Loïc Hoguin.
|
||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||
%% @copyright 2010-2011 Loïc Hoguin.
|
||||
%% @doc EGS file creation functions.
|
||||
%%
|
||||
%% This file is part of EGS.
|
||||
@ -17,12 +17,13 @@
|
||||
%% 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_prs).
|
||||
-module(prs).
|
||||
-export([init/0, compress/1]).
|
||||
-on_load(init/0).
|
||||
|
||||
init() ->
|
||||
erlang:load_nif("priv/egs_drv", 0).
|
||||
PrivDir = code:priv_dir(prs),
|
||||
erlang:load_nif(PrivDir ++ "/prs_drv", 0).
|
||||
|
||||
compress(_SrcBin) ->
|
||||
exit(nif_library_not_loaded).
|
||||
erlang:nif_error(not_loaded).
|
1
c_src/.gitignore
vendored
1
c_src/.gitignore
vendored
@ -1 +0,0 @@
|
||||
*.o
|
1
data/lobby/.gitignore
vendored
1
data/lobby/.gitignore
vendored
@ -1 +0,0 @@
|
||||
*
|
1
data/missions/.gitignore
vendored
1
data/missions/.gitignore
vendored
@ -1 +0,0 @@
|
||||
*
|
1
data/rooms/.gitignore
vendored
1
data/rooms/.gitignore
vendored
@ -1 +0,0 @@
|
||||
*
|
1
data/tutorial/.gitignore
vendored
1
data/tutorial/.gitignore
vendored
@ -1 +0,0 @@
|
||||
*
|
2
ebin/.gitignore
vendored
2
ebin/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
*.beam
|
||||
egs.app
|
BIN
p/flags.bin
BIN
p/flags.bin
Binary file not shown.
BIN
p/packet1309.bin
BIN
p/packet1309.bin
Binary file not shown.
BIN
p/packet1332.bin
BIN
p/packet1332.bin
Binary file not shown.
2
priv/.gitignore
vendored
2
priv/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
egs_drv.so
|
||||
patch.conf
|
@ -27,9 +27,26 @@
|
||||
%% @doc Game server IP address and port.
|
||||
%% They can be modified freely without problem.
|
||||
%% 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.
|
||||
|
||||
%% @doc Maximum level players can reach.
|
||||
{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"
|
||||
]}.
|
||||
|
1
priv/patch/.gitignore
vendored
1
priv/patch/.gitignore
vendored
@ -1 +0,0 @@
|
||||
*
|
@ -27,15 +27,15 @@
|
||||
%% @todo Default enemy_level to 1 if unspecified (lobbies).
|
||||
%% @todo Default sets to [100] if unspecified (lobbies).
|
||||
{zones, [
|
||||
{ 0, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [1, 2, 3, 4, 103]}]},
|
||||
{ 1, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9001]}]},
|
||||
{ 2, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9000]}]},
|
||||
{ 3, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9102]}]},
|
||||
{ 4, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9010]}]},
|
||||
{ 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]}]},
|
||||
{12, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [100, 101, 102]}]},
|
||||
{13, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [100, 101, 102]}]}
|
||||
{ 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]}]},
|
||||
%% { 2, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9000]}]},
|
||||
%% { 3, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9102]}]},
|
||||
%% { 4, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9010]}]},
|
||||
%% { 7, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9200, 9202]}]},
|
||||
%% {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]}]},
|
||||
%% {13, [{areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [100, 101, 102]}]}
|
||||
]}.
|
||||
|
||||
{temp_flags, []}.
|
||||
@ -101,13 +101,12 @@
|
||||
%% @todo Exit 6.
|
||||
%% 4rd floor.
|
||||
{{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, 3}, {1104000, 0, 900, 0}},
|
||||
{{1100000, 0, 4, 4}, {1104000, 0, 900, 0}},
|
||||
%% 5th floor.
|
||||
%% @todo Add 5th floor to zone 0.
|
||||
{{1100000, 11, 5, 0}, {1100000, 0, 4, 1}},
|
||||
{{1100000, 0, 5, 0}, {1100000, 0, 4, 1}},
|
||||
%% 2nd floor shops.
|
||||
{{1100000, 11, 101, 0}, {1100000, 0, 2, 3}},
|
||||
{{1100000, 12, 101, 1}, {1100000, 0, 2, 4}},
|
||||
|
Binary file not shown.
Binary file not shown.
@ -83,6 +83,10 @@ event entr_unit0004 ->
|
||||
push 0, npc.talk_on, %% Linear Line 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.
|
||||
event entr_unit0103 ->
|
||||
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
|
||||
mes.select_win_b,
|
||||
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;
|
||||
2 -> push 0, push 3, 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
|
||||
mes.select_win_b,
|
||||
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;
|
||||
2 -> push 0, push 3, 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
|
||||
mes.select_win_b,
|
||||
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;
|
||||
3 -> push 0, push 2, 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
|
||||
mes.select_win_b,
|
||||
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;
|
||||
3 -> push 0, push 2, 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,
|
||||
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.
|
||||
|
||||
num_var nTransportNPC.
|
||||
|
@ -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}, [
|
||||
[ %% Always available.
|
||||
{chair, {-599.0, 186.0, -1049.0}, {0.0, 180.0, 0.0}, [{id, 1}, {stand_dist, 8.0}]},
|
||||
|
19
rebar.config
Normal file
19
rebar.config
Normal 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
|
||||
]}.
|
70
src/egs.erl
70
src/egs.erl
@ -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}.
|
@ -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}.
|
@ -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.
|
@ -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.
|
@ -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).
|
792
src/egs_game.erl
792
src/egs_game.erl
@ -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).
|
@ -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).
|
@ -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]).
|
@ -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).
|
@ -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}}.
|
@ -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
Loading…
Reference in New Issue
Block a user