Move the db user code into its own gen_server, egs_user_model.

This commit is contained in:
Loïc Hoguin 2010-07-19 02:11:31 +02:00
parent ac4a19dab9
commit bc66480d8e
12 changed files with 308 additions and 204 deletions

View File

@ -7,6 +7,7 @@
egs_app,
egs_sup,
egs_exit_mon,
egs_user_model,
reloader,
egs_cron,
egs_db,

View File

@ -24,28 +24,19 @@
-record(pos, {x, y, z, dir}).
%% @doc Table containing the users currently logged in.
%% @doc Character area location data structure.
-record(users, {
gid,
pid,
socket,
auth,
time,
folder,
character,
lid,
instanceid,
areatype,
questid,
zoneid,
mapid,
entryid,
savedquestid,
savedzoneid,
savedmapid,
savedentryid,
pos
-record(psu_area, {questid, zoneid, mapid}).
%% @doc Table containing the users currently logged in.
%% @todo Probably can use a "param" or "extra" field to store the game-specific information (for things that don't need to be queried).
-record(egs_user_model, {
id, pid, socket, state, time, character, instanceid, areatype, area, entryid, pos,
%% psu specific fields
lid, prev_area, prev_entryid,
%% temporary fields
folder
}).
%% @doc Character main or class level data structure.

View File

@ -51,15 +51,17 @@ stop() ->
%% @doc Send a global message.
%% @todo Move that in a psu module.
global(Type, Message) ->
lists:foreach(fun(User) -> egs_proto:send_global(User#users.socket, Type, Message) end, egs_db:users_select_all()).
{ok, List} = egs_user_model:select(all),
lists:foreach(fun(User) -> egs_proto:send_global(User#egs_user_model.socket, Type, Message) end, List).
%% @doc Warp all players to a new map.
%% @todo Move that in a psu module.
warp(QuestID, ZoneID, MapID, EntryID) ->
lists:foreach(fun(User) -> User#users.pid ! {psu_warp, QuestID, ZoneID, MapID, EntryID} end, egs_db:users_select_all()).
{ok, List} = egs_user_model:select(all),
lists:foreach(fun(User) -> User#egs_user_model.pid ! {psu_warp, QuestID, ZoneID, MapID, EntryID} end, List).
%% @doc Warp one player to a new map.
%% @todo Move that in a psu module.
warp(GID, QuestID, ZoneID, MapID, EntryID) ->
User = egs_db:users_select(GID),
User#users.pid ! {psu_warp, QuestID, ZoneID, MapID, EntryID}.
{ok, User} = egs_user_model:read(GID),
User#egs_user_model.pid ! {psu_warp, QuestID, ZoneID, MapID, EntryID}.

View File

@ -71,5 +71,5 @@ db_init() ->
mnesia:start(),
mnesia:create_table(ids, [{attributes, record_info(fields, ids)}]),
mnesia:create_table(objects, [{attributes, record_info(fields, objects)}]),
mnesia:create_table(users, [{attributes, record_info(fields, users)}]),
mnesia:create_table(egs_user_model, [{attributes, record_info(fields, egs_user_model)}]),
error_logger:info_report("mnesia tables created").

View File

@ -35,7 +35,7 @@ cleanup() ->
_ ->
?MODULE:cleanup()
after 300000 ->
egs_db:users_cleanup(),
egs_user_model:cleanup(),
reload,
?MODULE:cleanup()
end.

View File

@ -32,7 +32,7 @@ do(Q) ->
Val.
%% @doc Retrieve the next unique ID.
%% @todo Used only for the LID so far...
next(Type) ->
mnesia:dirty_update_counter(ids, Type, 1).
@ -72,61 +72,3 @@ objects_delete(InstanceID) ->
List = do(qlc:q([X || X <- mnesia:table(objects), X#objects.instanceid =:= InstanceID])),
[mnesia:transaction(fun() -> mnesia:delete({objects, ID}) end) || #objects{id=ID} <- List],
ok.
%% @doc Count the number of users online.
users_count() ->
mnesia:table_info(users, size).
%% @doc Select exactly one user by its GID. Return an #users record.
users_select(GID) ->
case mnesia:transaction(fun() -> mnesia:read({users, GID}) end) of
{atomic, []} ->
error;
{atomic, [Val]} ->
Val
end.
%% @doc Select exactly one user by its Pid. Return an #users record.
users_select_by_pid(Pid) ->
[User] = do(qlc:q([X || X <- mnesia:table(users), X#users.pid =:= Pid])),
User.
%% @doc Select all users. Return a list of #users records.
users_select_all() ->
do(qlc:q([X || X <- mnesia:table(users), (X#users.character)#characters.slot /= undefined])).
%% @doc Select all other users in the same area. Return a list of #users records.
users_select_others_in_area(Self) ->
do(qlc:q([X || X <- mnesia:table(users),
X#users.gid /= Self#users.gid,
(X#users.character)#characters.slot /= undefined,
X#users.instanceid =:= Self#users.instanceid,
X#users.questid =:= Self#users.questid,
X#users.zoneid =:= Self#users.zoneid,
X#users.mapid =:= Self#users.mapid
])).
%% @doc Insert or update an user.
users_insert(User) ->
mnesia:transaction(fun() -> mnesia:write(User) end).
%% @doc Delete an user.
users_delete(GID) ->
mnesia:transaction(fun() -> mnesia:delete({users, GID}) end).
%% @doc Cleanup the disconnected users who failed after the login stage but before the game stage.
users_cleanup() ->
Timeout = calendar:datetime_to_gregorian_seconds(calendar:universal_time()) - 300,
Users = do(qlc:q([X#users.gid || X <- mnesia:table(users),
X#users.auth /= success, X#users.time < Timeout])),
mnesia:transaction(fun() ->
lists:foreach(fun(GID) -> users_delete(GID) end, Users)
end).

View File

@ -50,6 +50,7 @@ upgrade() ->
init([]) ->
%% Start egs_cron, egs_game, egs_login, egs_patch. To be replaced by configurable modules.
Processes = [{egs_cron, {egs_cron, start, []}, permanent, 5000, worker, dynamic},
{egs_user_model, {egs_user_model, start_link, []}, permanent, 5000, worker, dynamic},
{psu_game, {psu_game, start_link, [?GAME_PORT]}, permanent, 5000, worker, dynamic},
{psu_login_jp1, {psu_login, start_link, [?LOGIN_PORT_JP_ONE, 10000001]}, permanent, 5000, worker, dynamic},
{psu_login_jp2, {psu_login, start_link, [?LOGIN_PORT_JP_TWO, 20000001]}, permanent, 5000, worker, dynamic},

150
src/egs_user_model.erl Normal file
View File

@ -0,0 +1,150 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin.
%% @doc User domain model.
%%
%% This file is part of EGS.
%%
%% EGS is free software: you can redistribute it and/or modify
%% it under the terms of the GNU 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 General Public License for more details.
%%
%% You should have received a copy of the GNU General Public License
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
-module(egs_user_model).
-behavior(gen_server).
-export([start_link/0, stop/0, count/0, read/1, select/1, write/1, delete/1, cleanup/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 and for the table name.
-define(SERVER, ?MODULE).
-define(TABLE, ?MODULE).
-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.
%% API
%% @spec start_link() -> {ok,Pid::pid()}
%% @todo Start the cleanup timer.
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%% @spec stop() -> stopped
%% @todo Stop the cleanup timer.
stop() ->
gen_server:call(?SERVER, stop).
%% @spec count() -> {ok, Count}
count() ->
gen_server:call(?SERVER, count).
%% @spec read({pid, Pid}) -> {ok, User} | {error, badarg}
%% @spec read(ID) -> {ok, User} | {error, badarg}
read(ID) ->
gen_server:call(?SERVER, {read, ID}).
%% @spec select(all) -> {ok, List}
%% @spec select({neighbors, User}) -> {ok, List}
select(Type) ->
gen_server:call(?SERVER, {select, Type}).
%% @spec write(User) -> ok
write(User) ->
gen_server:cast(?SERVER, {write, User}).
%% @spec delete(ID) -> ok
delete(ID) ->
gen_server:cast(?SERVER, {delete, ID}).
%% @spec cleanup() -> ok
cleanup() ->
gen_server:cast(?SERVER, cleanup).
%% gen_server
init([]) ->
error_logger:info_report("egs_user_model started"),
{ok, undefined}.
handle_call(count, _From, State) ->
Count = mnesia:table_info(?TABLE, size),
{reply, {ok, Count}, State};
handle_call({read, {pid, Pid}}, _From, State) ->
List = do(qlc:q([X || X <- mnesia:table(?TABLE), X#?TABLE.pid =:= Pid])),
case List of
[] -> {reply, {error, badarg}, State};
[User] -> {reply, {ok, User}, State}
end;
handle_call({read, ID}, _From, State) ->
case mnesia:transaction(fun() -> mnesia:read({?TABLE, ID}) end) of
{atomic, []} -> {reply, {error, badarg}, State};
{atomic, [Val]} -> {reply, {ok, Val}, State}
end;
%% @todo state = undefined | {wait_for_authentication, Key} | authenticated | online
handle_call({select, all}, _From, State) ->
List = do(qlc:q([X || X <- mnesia:table(?TABLE), X#?TABLE.state =:= online])),
{reply, {ok, List}, State};
handle_call({select, {neighbors, User}}, _From, State) ->
List = do(qlc:q([X || X <- mnesia:table(?TABLE),
X#?TABLE.id /= User#?TABLE.id,
X#?TABLE.state =:= online,
X#?TABLE.instanceid =:= User#?TABLE.instanceid,
X#?TABLE.area =:= User#?TABLE.area
])),
{reply, {ok, List}, State};
handle_call(stop, _From, State) ->
{stop, normal, stopped, State};
handle_call(_Request, _From, State) ->
{reply, ignored, State}.
handle_cast({write, User}, State) ->
mnesia:transaction(fun() -> mnesia:write(User) end),
{noreply, State};
handle_cast({delete, ID}, State) ->
mnesia:transaction(fun() -> mnesia:delete({?TABLE, ID}) end),
{noreply, State};
%% @todo Cleanup more than the auth failures?
handle_cast(cleanup, State) ->
Timeout = calendar:datetime_to_gregorian_seconds(calendar:universal_time()) - 300,
List = do(qlc:q([X#?TABLE.id || X <- mnesia:table(?TABLE),
X#?TABLE.state /= online,
X#?TABLE.time < Timeout
])),
mnesia:transaction(fun() ->
lists:foreach(fun(ID) -> delete(ID) end, List)
end),
{noreply, State};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.

View File

@ -36,10 +36,11 @@ start_link(Port) ->
%% @spec cleanup(Pid) -> ok
%% @doc Cleanup the data associated with the failing process.
cleanup(Pid) ->
User = egs_db:users_select_by_pid(Pid),
egs_db:users_delete(User#users.gid),
lists:foreach(fun(Other) -> Other#users.pid ! {psu_player_unspawn, User} end, egs_db:users_select_others_in_area(User)),
io:format("game (~p): quit~n", [User#users.gid]).
{ok, User} = egs_user_model:read({pid, Pid}),
egs_user_model:delete(User#egs_user_model.id),
{ok, List} = egs_user_model:select({neighbors, User}),
lists:foreach(fun(Other) -> Other#egs_user_model.pid ! {psu_player_unspawn, User} end, List),
io:format("game (~p): quit~n", [User#egs_user_model.id]).
%% @doc Listen for connections.
@ -88,23 +89,23 @@ process() ->
process_handle(16#020d, << GID:32/little-unsigned-integer, Auth:32/bits, _/bits >>) ->
CSocket = get(socket),
case egs_db:users_select(GID) of
error ->
case egs_user_model:read(GID) of
{error, badarg} ->
log("can't find user, closing"),
ssl:close(CSocket);
User ->
case User#users.auth of
Auth ->
{ok, User} ->
case User#egs_user_model.state of
{wait_for_authentication, Auth} ->
put(gid, GID),
log("auth success"),
LID = 1 + egs_db:next(lobby) rem 1023,
Time = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
egs_db:users_insert(#users{gid=GID, pid=self(), socket=CSocket, auth=success, time=Time, folder=User#users.folder, lid=LID}),
egs_user_model:write(#egs_user_model{id=GID, pid=self(), socket=CSocket, state=authenticated, time=Time, folder=User#egs_user_model.folder, lid=LID}),
send_0d05(),
?MODULE:char_select();
_ ->
log("quit, auth failed"),
egs_db:users_delete(GID),
egs_user_model:delete(GID),
ssl:close(CSocket)
end
end;
@ -134,7 +135,7 @@ char_select() ->
?MODULE:char_select();
{error, closed} ->
log("quit"),
egs_db:users_delete(get(gid))
egs_user_model:delete(get(gid))
end.
%% @doc Character selection handler.
@ -154,17 +155,17 @@ char_select_handle(16#0d02, << Number:32/little-unsigned-integer, Char/bits >>)
Appearance = psu_appearance:binary_to_tuple(Race, AppearanceBin),
psu_appearance:validate_char_create(Race, Gender, Appearance),
% end of check, continue doing it wrong past that point for now
User = egs_db:users_select(get(gid)),
_ = file:make_dir(io_lib:format("save/~s", [User#users.folder])),
file:write_file(io_lib:format("save/~s/~b-character", [User#users.folder, Number]), Char),
file:write_file(io_lib:format("save/~s/~b-character.options", [User#users.folder, Number]), << 0:128, 4, 0:56 >>), % default 0 to everything except brightness 4
{ok, User} = egs_user_model:read(get(gid)),
_ = file:make_dir(io_lib:format("save/~s", [User#egs_user_model.folder])),
file:write_file(io_lib:format("save/~s/~b-character", [User#egs_user_model.folder, Number]), Char),
file:write_file(io_lib:format("save/~s/~b-character.options", [User#egs_user_model.folder, Number]), << 0:128, 4, 0:56 >>), % default 0 to everything except brightness 4
char_select_load(Number);
%% @doc Character selection screen request.
char_select_handle(16#0d06, _) ->
User = egs_db:users_select(get(gid)),
send_0d03(data_load(User#users.folder, 0), data_load(User#users.folder, 1), data_load(User#users.folder, 2), data_load(User#users.folder, 3)),
{ok, User} = egs_user_model:read(get(gid)),
send_0d03(data_load(User#egs_user_model.folder, 0), data_load(User#egs_user_model.folder, 1), data_load(User#egs_user_model.folder, 2), data_load(User#egs_user_model.folder, 3)),
?MODULE:char_select();
%% @doc Silently ignore packet 0818. Gives CPU/GPU information.
@ -182,8 +183,8 @@ char_select_handle(Command, _) ->
%% The default entry point currently is 4th floor, Linear Line counter.
char_select_load(Number) ->
OldUser = egs_db:users_select(get(gid)),
[{status, 1}, {char, CharBin}, {options, OptionsBin}] = data_load(OldUser#users.folder, Number),
{ok, OldUser} = egs_user_model:read(get(gid)),
[{status, 1}, {char, CharBin}, {options, OptionsBin}] = data_load(OldUser#egs_user_model.folder, Number),
<< Name:512/bits, RaceBin:8, GenderBin:8, ClassBin:8, AppearanceBin:776/bits, _/bits >> = CharBin,
psu_characters:validate_name(Name), % TODO: don't validate name when loading character, do it at creation
Race = psu_characters:race_binary_to_atom(RaceBin),
@ -192,8 +193,8 @@ char_select_load(Number) ->
Appearance = psu_appearance:binary_to_tuple(Race, AppearanceBin),
Options = psu_characters:options_binary_to_tuple(OptionsBin),
Character = #characters{slot=Number, name=Name, race=Race, gender=Gender, class=Class, appearance=Appearance, options=Options}, % TODO: temporary set the slot here, won't be needed later
User = OldUser#users{character=Character, pos=#pos{x=0.0, y=0.0, z=0.0, dir=0.0}},
egs_db:users_insert(User),
User = OldUser#egs_user_model{state=online, character=Character, area=#psu_area{questid=undefined, zoneid=undefined, mapid=undefined}, pos=#pos{x=0.0, y=0.0, z=0.0, dir=0.0}},
egs_user_model:write(User),
char_load(User),
send_021b(),
area_load(1100000, 0, 4, 5),
@ -219,7 +220,7 @@ char_load(User) ->
% 0246
send_0a0a(),
send_1006(5),
send_1005((User#users.character)#characters.name),
send_1005((User#egs_user_model.character)#characters.name),
send_1006(12),
send_0210(),
send_0222(),
@ -232,15 +233,17 @@ char_load(User) ->
%% @doc Load the given map as a mission counter.
counter_load(QuestID, ZoneID, MapID, EntryID) ->
OldUser = egs_db:users_select(get(gid)),
User = OldUser#users{areatype=counter, questid=QuestID, zoneid=ZoneID, mapid=MapID, entryid=EntryID,
savedquestid=OldUser#users.questid, savedzoneid=OldUser#users.zoneid, savedmapid=ZoneID, savedentryid=MapID},
egs_db:users_insert(User),
{ok, OldUser} = egs_user_model:read(get(gid)),
OldArea = OldUser#egs_user_model.area,
User = OldUser#egs_user_model{areatype=counter, area={psu_area, QuestID, ZoneID, MapID}, entryid=EntryID, prev_entryid=MapID,
prev_area={psu_area, OldArea#psu_area.questid, OldArea#psu_area.zoneid, ZoneID}},
egs_user_model:write(User),
AreaName = "Mission counter",
QuestFile = "data/lobby/counter.quest.nbl",
ZoneFile = "data/lobby/counter.zone.nbl",
% broadcast unspawn to other people
lists:foreach(fun(Other) -> Other#users.pid ! {psu_player_unspawn, User} end, egs_db:users_select_others_in_area(OldUser)),
{ok, UnspawnList} = egs_user_model:select({neighbors, OldUser}),
lists:foreach(fun(Other) -> Other#egs_user_model.pid ! {psu_player_unspawn, User} end, UnspawnList),
% load counter
send_0c00(16#7fffffff),
send_020e(QuestFile),
@ -306,14 +309,14 @@ area_get_season(QuestID) ->
%% @doc Load the given map as a standard lobby.
area_load(QuestID, ZoneID, MapID, EntryID) ->
OldUser = egs_db:users_select(get(gid)),
{ok, OldUser} = egs_user_model:read(get(gid)),
[{type, AreaType}, {file, QuestFile}|StartInfo] = proplists:get_value(QuestID, ?QUESTS, [{type, undefined}, {file, undefined}]),
[IsStart, InstanceID, RealZoneID, RealMapID, RealEntryID] = case AreaType of
mission ->
if ZoneID =:= 65535 ->
[{start, [TmpZoneID, TmpMapID, TmpEntryID]}] = StartInfo,
[true, OldUser#users.gid, TmpZoneID, TmpMapID, TmpEntryID];
true -> [false, OldUser#users.gid, ZoneID, MapID, EntryID]
[true, OldUser#egs_user_model.id, TmpZoneID, TmpMapID, TmpEntryID];
true -> [false, OldUser#egs_user_model.id, ZoneID, MapID, EntryID]
end;
myroom ->
if ZoneID =:= 0 ->
@ -329,8 +332,8 @@ area_load(QuestID, ZoneID, MapID, EntryID) ->
true ->
[{name, AreaName}] = proplists:get_value([QuestID, RealMapID], ?MAPS, [{name, "dammy"}])
end,
User = OldUser#users{instanceid=InstanceID, areatype=AreaType, questid=QuestID, zoneid=RealZoneID, mapid=RealMapID, entryid=RealEntryID},
egs_db:users_insert(User),
User = OldUser#egs_user_model{instanceid=InstanceID, areatype=AreaType, area={psu_area, QuestID, RealZoneID, RealMapID}, entryid=RealEntryID},
egs_user_model:write(User),
%% @todo Don't recalculate SetID when leaving the mission and back (this changes the spawns).
SetID = if IsStart =:= true -> crypto:rand_uniform(0, 4); true -> 0 end,
if IsStart =:= true -> % initialize the mission
@ -340,28 +343,32 @@ area_load(QuestID, ZoneID, MapID, EntryID) ->
area_load(AreaType, IsStart, SetID, OldUser, User, QuestFile, ZoneFile, AreaName).
area_load(AreaType, IsStart, SetID, OldUser, User, QuestFile, ZoneFile, AreaName) ->
QuestChange = if OldUser#users.questid /= User#users.questid, QuestFile /= undefined -> true; true -> false end,
#psu_area{questid=OldQuestID, zoneid=OldZoneID} = OldUser#egs_user_model.area,
#psu_area{questid=QuestID, zoneid=ZoneID} = User#egs_user_model.area,
QuestChange = if OldQuestID /= QuestID, QuestFile /= undefined -> true; true -> false end,
if ZoneFile =:= undefined ->
ZoneChange = false;
true ->
ZoneChange = if OldUser#users.questid =:= User#users.questid, OldUser#users.zoneid =:= User#users.zoneid -> false; true -> true end
ZoneChange = if OldQuestID =:= QuestID, OldZoneID =:= ZoneID -> false; true -> true end
end,
[{status, IsSeasonal}, {season, SeasonID}] = area_get_season(User#users.questid),
[{status, IsSeasonal}, {season, SeasonID}] = area_get_season(QuestID),
% broadcast spawn and unspawn to other people
lists:foreach(fun(Other) -> Other#users.pid ! {psu_player_unspawn, User} end, egs_db:users_select_others_in_area(OldUser)),
{ok, UnspawnList} = egs_user_model:select({neighbors, OldUser}),
{ok, SpawnList} = egs_user_model:select({neighbors, User}),
lists:foreach(fun(Other) -> Other#egs_user_model.pid ! {psu_player_unspawn, User} end, UnspawnList),
if AreaType =:= lobby ->
lists:foreach(fun(Other) -> Other#users.pid ! {psu_player_spawn, User} end, egs_db:users_select_others_in_area(User));
lists:foreach(fun(Other) -> Other#egs_user_model.pid ! {psu_player_spawn, User} end, SpawnList);
true -> ignore
end,
% load area
if QuestChange =:= true ->
% reload the character if entering or leaving the room quest
if OldUser#users.questid =:= 1120000; User#users.questid =:= 1120000 ->
if OldQuestID =:= 1120000; QuestID =:= 1120000 ->
char_load(User);
true -> ignore
end,
% load new quest
send_0c00(User#users.questid),
send_0c00(QuestID),
send_020e(QuestFile);
true -> ignore
end,
@ -381,8 +388,8 @@ area_load(AreaType, IsStart, SetID, OldUser, User, QuestFile, ZoneFile, AreaName
send_020f(ZoneFile, SetID, SeasonID);
true -> ignore
end,
send_0205(User#users.zoneid, User#users.mapid, User#users.entryid, IsSeasonal),
send_100e(User#users.questid, User#users.zoneid, User#users.mapid, AreaName, 16#ffffffff),
send_0205(ZoneID, (User#egs_user_model.area)#psu_area.mapid, User#egs_user_model.entryid, IsSeasonal),
send_100e(QuestID, ZoneID, (User#egs_user_model.area)#psu_area.mapid, AreaName, 16#ffffffff),
if AreaType =:= mission ->
send_0215(0),
if IsStart =:= true ->
@ -419,7 +426,7 @@ area_load(AreaType, IsStart, SetID, OldUser, User, QuestFile, ZoneFile, AreaName
send_0a06();
true -> ignore
end,
send_0233(egs_db:users_select_others_in_area(User)),
send_0233(SpawnList),
send_0208(),
send_0236().
@ -445,10 +452,12 @@ loop(SoFar) ->
?MODULE:loop(SoFar);
{psu_player_spawn, _Spawn} ->
% Should be something along the lines of 203 201 204 or something.
send_0233(egs_db:users_select_others_in_area(egs_db:users_select(get(gid)))),
{ok, User} = egs_user_model:read(get(gid)),
{ok, SpawnList} = egs_user_model:select({neighbors, User}),
send_0233(SpawnList),
?MODULE:loop(SoFar);
{psu_player_unspawn, Spawn} ->
send_0204(Spawn#users.gid, Spawn#users.lid, 5),
send_0204(Spawn#egs_user_model.id, Spawn#egs_user_model.lid, 5),
?MODULE:loop(SoFar);
{psu_warp, QuestID, ZoneID, MapID, EntryID} ->
area_load(QuestID, ZoneID, MapID, EntryID),
@ -484,9 +493,9 @@ broadcast(16#0503, Orig) ->
<< _: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 >> = Orig,
FloatDir = Dir / 46603.375,
User = egs_db:users_select(get(gid)),
NewUser = User#users{pos=#pos{x=X, y=Y, z=Z, dir=FloatDir}, questid=QuestID, zoneid=ZoneID, mapid=MapID, entryid=EntryID},
egs_db:users_insert(NewUser),
{ok, User} = egs_user_model:read(get(gid)),
NewUser = User#egs_user_model{pos=#pos{x=X, y=Y, z=Z, dir=FloatDir}, area=#psu_area{questid=QuestID, zoneid=ZoneID, mapid=MapID}, entryid=EntryID},
egs_user_model:write(NewUser),
broadcast(default, Orig);
%% @doc Stand still broadcast handler. Save the position and then dispatch it.
@ -496,9 +505,9 @@ broadcast(16#0514, Orig) ->
QuestID:32/little-unsigned-integer, ZoneID:32/little-unsigned-integer,
MapID:32/little-unsigned-integer, EntryID:32/little-unsigned-integer, _/bits >> = Orig,
FloatDir = Dir / 46603.375,
User = egs_db:users_select(get(gid)),
NewUser = User#users{pos=#pos{x=X, y=Y, z=Z, dir=FloatDir}, questid=QuestID, zoneid=ZoneID, mapid=MapID, entryid=EntryID},
egs_db:users_insert(NewUser),
{ok, User} = egs_user_model:read(get(gid)),
NewUser = User#egs_user_model{pos=#pos{x=X, y=Y, z=Z, dir=FloatDir}, area=#psu_area{questid=QuestID, zoneid=ZoneID, mapid=MapID}, entryid=EntryID},
egs_user_model:write(NewUser),
broadcast(default, Orig);
%% @doc Default broadcast handler. Dispatch the command to everyone.
@ -516,14 +525,15 @@ broadcast(Command, Orig)
Command =:= default ->
<< _:32, A:64/bits, _:64, B:192/bits, _:64, C/bits >> = Orig,
GID = get(gid),
case egs_db:users_select(GID) of
error ->
case egs_user_model:read(GID) of
{error, _Reason} ->
ignore;
Self ->
LID = Self#users.lid,
{ok, Self} ->
LID = Self#egs_user_model.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 >>,
lists:foreach(fun(User) -> User#users.pid ! {psu_broadcast, Packet} end, egs_db:users_select_others_in_area(Self))
{ok, SpawnList} = egs_user_model:select({neighbors, Self}),
lists:foreach(fun(User) -> User#egs_user_model.pid ! {psu_broadcast, Packet} end, SpawnList)
end.
%% @doc Movement (non-broadcast) handler. Do nothing.
@ -647,10 +657,11 @@ handle(16#021f, << Uni:32/little-unsigned-integer, _/bits >>) ->
send_0230(),
% 0220
% force reloading the character and data files (hack)
User = egs_db:users_select(get(gid)),
NewRow = User#users{questid=1120000, zoneid=undefined},
egs_db:users_insert(NewRow),
area_load(User#users.questid, User#users.zoneid, User#users.mapid, User#users.entryid)
{ok, User} = egs_user_model:read(get(gid)),
Area = User#egs_user_model.area,
NewRow = User#egs_user_model{area=Area#psu_area{questid=1120000, zoneid=undefined}},
egs_user_model:write(NewRow),
area_load(Area#psu_area.questid, Area#psu_area.zoneid, Area#psu_area.mapid, User#egs_user_model.entryid)
end;
%% @doc Shortcut changes handler. Do nothing.
@ -665,18 +676,19 @@ handle(16#0302, _) ->
%% @todo Only broadcast to people in the same map.
handle(16#0304, Data) ->
User = egs_db:users_select(get(gid)),
{ok, User} = egs_user_model:read(get(gid)),
case get(version) of
0 -> % AOTI v2.000
<< _:64, Modifiers:128/bits, Message/bits >> = Data;
_ -> % Above
<< _:64, Modifiers:128/bits, _:512, Message/bits >> = Data
end,
[LogName|_] = re:split((User#users.character)#characters.name, "\\0\\0", [{return, binary}]),
[LogName|_] = re:split((User#egs_user_model.character)#characters.name, "\\0\\0", [{return, binary}]),
[TmpMessage|_] = re:split(Message, "\\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}])]]),
lists:foreach(fun(X) -> X#users.pid ! {psu_chat, get(gid), (User#users.character)#characters.name, Modifiers, Message} end, egs_db:users_select_all());
{ok, List} = egs_user_model:select(all),
lists:foreach(fun(X) -> X#egs_user_model.pid ! {psu_chat, get(gid), (User#egs_user_model.character)#characters.name, Modifiers, Message} end, List);
%% @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.
@ -686,8 +698,8 @@ handle(16#0402, Data) ->
case Type of
7 -> % spawn cleared @todo 1201 sent back with same values apparently, but not always
log("cleared spawn ~b", [SpawnID]),
User = egs_db:users_select(get(gid)),
[EventID, BlockID] = psu_missions:spawn_cleared(User#users.instanceid, SpawnID),
{ok, User} = egs_user_model:read(get(gid)),
[EventID, BlockID] = psu_missions:spawn_cleared(User#egs_user_model.instanceid, SpawnID),
if EventID =:= false -> ignore;
true -> send_1205(EventID, BlockID, 0)
end;
@ -724,8 +736,10 @@ handle(16#0811, Data) ->
%% @doc Leave mission counter handler. Lobby values depend on which counter was entered.
handle(16#0812, _) ->
User = egs_db:users_select(get(gid)),
area_load(User#users.savedquestid, User#users.savedzoneid, User#users.zoneid, User#users.mapid);
{ok, User} = egs_user_model:read(get(gid)),
Area = User#egs_user_model.area,
PrevArea = User#egs_user_model.prev_area,
area_load(PrevArea#psu_area.questid, PrevArea#psu_area.zoneid, Area#psu_area.zoneid, Area#psu_area.mapid);
%% @doc Item description request.
%% @todo Send something other than just "dammy".
@ -747,8 +761,8 @@ handle(16#0c01, << QuestID:32/little-unsigned-integer >>) ->
%% @todo Handle correctly.
handle(16#0c05, _) ->
User = egs_db:users_select(get(gid)),
[{quests, Filename}, {bg, _}, {options, _}] = proplists:get_value(User#users.entryid, ?COUNTERS),
{ok, User} = egs_user_model:read(get(gid)),
[{quests, Filename}, {bg, _}, {options, _}] = proplists:get_value(User#egs_user_model.entryid, ?COUNTERS),
send_0c06(Filename);
%% @doc Lobby transport handler? Just ignore the meseta price for now and send the player where he wanna be!
@ -762,18 +776,19 @@ handle(16#0c07, _) ->
handle(16#0c0e, _) ->
send_1006(11),
User = egs_db:users_select(get(gid)),
{ok, User} = egs_user_model:read(get(gid)),
%% delete the mission
psu_missions:stop(User#users.instanceid),
psu_missions:stop(User#egs_user_model.instanceid),
%% full hp
Character = User#users.character,
Character = User#egs_user_model.character,
MaxHP = Character#characters.maxhp,
NewCharacter = Character#characters{currenthp=MaxHP},
NewUser = User#users{character=NewCharacter},
egs_db:users_insert(NewUser),
NewUser = User#egs_user_model{character=NewCharacter},
egs_user_model:write(NewUser),
%% map change (temporary)
if User#users.areatype =:= mission ->
area_load(User#users.savedquestid, User#users.savedzoneid, User#users.savedmapid, User#users.savedentryid);
if User#egs_user_model.areatype =:= mission ->
Area = User#egs_user_model.prev_area,
area_load(Area#psu_area.questid, Area#psu_area.zoneid, Area#psu_area.mapid, User#egs_user_model.prev_entryid);
true -> ignore
end;
@ -781,8 +796,8 @@ handle(16#0c0e, _) ->
%% @todo Temporarily allow rare mission and LL all difficulties to all players.
handle(16#0c0f, _) ->
User = egs_db:users_select(get(gid)),
[{quests, _}, {bg, _}, {options, Options}] = proplists:get_value(User#users.entryid, ?COUNTERS),
{ok, User} = egs_user_model:read(get(gid)),
[{quests, _}, {bg, _}, {options, Options}] = proplists:get_value(User#egs_user_model.entryid, ?COUNTERS),
send_0c10(Options);
%% @doc Set flag handler. Associate a new flag with the character.
@ -803,8 +818,8 @@ handle(16#0d07, Data) ->
Options = psu_characters:options_binary_to_tuple(Data),
psu_characters:validate_options(Options),
% End of validation
User = egs_db:users_select(get(gid)),
file:write_file(io_lib:format("save/~s/~b-character.options", [User#users.folder, (User#users.character)#characters.slot]), Data);
{ok, User} = egs_user_model:read(get(gid)),
file:write_file(io_lib:format("save/~s/~b-character.options", [User#egs_user_model.folder, (User#egs_user_model.character)#characters.slot]), Data);
%% @doc Hit handler.
%% @todo Finish the work on it.
@ -824,11 +839,11 @@ handle(16#0f0a, Data) ->
log("object event handler: action ~b object ~b a ~b b ~b c ~b", [Action, ObjectID, A, B, C]),
case Action of
0 -> % warp
User = egs_db:users_select(get(gid)),
{X, Y, Z, Dir} = psu_missions:warp_event(User#users.instanceid, BlockID, ListNb, ObjectNb),
NewUser = User#users{pos=#pos{x=X, y=Y, z=Z, dir=Dir}},
egs_db:users_insert(NewUser),
send_0503(User#users.pos),
{ok, User} = egs_user_model:read(get(gid)),
{X, Y, Z, Dir} = psu_missions:warp_event(User#egs_user_model.instanceid, BlockID, ListNb, ObjectNb),
NewUser = User#egs_user_model{pos=#pos{x=X, y=Y, z=Z, dir=Dir}},
egs_user_model:write(NewUser),
send_0503(User#egs_user_model.pos),
send_1211(A, C, B, 0);
3 -> % crystal activation
send_1213(ObjectID, 1);
@ -847,8 +862,8 @@ handle(16#0f0a, Data) ->
% 0117, 0111, 0117?
ignore;
12 -> % pick/use key
User = egs_db:users_select(get(gid)),
[[EventID|_], BlockID] = psu_missions:key_event(User#users.instanceid, ObjectID),
{ok, User} = egs_user_model:read(get(gid)),
[[EventID|_], BlockID] = psu_missions:key_event(User#egs_user_model.instanceid, ObjectID),
send_1205(EventID, BlockID, 0),
send_1213(ObjectID, 1);
13 -> % floor_button on (also sent when clearing a few of the rooms in black nest)
@ -862,12 +877,12 @@ handle(16#0f0a, Data) ->
20 -> % enter counter/elevator/room/spaceport/pick key/use key
ignore;
23 -> % initialize key slots (called when picking a key or checking the gate directly with no key)
User = egs_db:users_select(get(gid)),
[[_, EventID, _], BlockID] = psu_missions:key_event(User#users.instanceid, ObjectID),
{ok, User} = egs_user_model:read(get(gid)),
[[_, EventID, _], BlockID] = psu_missions:key_event(User#egs_user_model.instanceid, ObjectID),
send_1205(EventID, BlockID, 0); % in block 1, 202 = key [1] x1, 203 = key [-] x1
24 -> % open gate (only when client has key)
User = egs_db:users_select(get(gid)),
[[_, _, EventID], BlockID] = psu_missions:key_event(User#users.instanceid, ObjectID),
{ok, User} = egs_user_model:read(get(gid)),
[[_, _, EventID], BlockID] = psu_missions:key_event(User#egs_user_model.instanceid, ObjectID),
send_1205(EventID, BlockID, 0),
send_1213(ObjectID, 1);
25 -> % sit on chair
@ -894,8 +909,8 @@ handle(16#1112, Data) ->
%% @todo Handle when the party already exists! And stop doing it wrong.
handle(16#1705, _) ->
User = egs_db:users_select(get(gid)),
send_1706((User#users.character)#characters.name);
{ok, User} = egs_user_model:read(get(gid)),
send_1706((User#egs_user_model.character)#characters.name);
%% @doc Mission selected handler. Send the currently selected mission.
%% @todo Probably need to dispatch that info to other party members in the same counter.
@ -918,8 +933,8 @@ handle(16#170b, _) ->
%% @todo Handle correctly.
handle(16#1710, _) ->
User = egs_db:users_select(get(gid)),
[{quests, _}, {bg, Background}, {options, _}] = proplists:get_value(User#users.entryid, ?COUNTERS),
{ok, User} = egs_user_model:read(get(gid)),
[{quests, _}, {bg, Background}, {options, _}] = proplists:get_value(User#egs_user_model.entryid, ?COUNTERS),
send_1711(Background);
%% @doc Dialog request handler. Do what we can.
@ -986,7 +1001,7 @@ handle_hits(Data) ->
<< _:96, SourceID:32/little-unsigned-integer, TargetID:32/little-unsigned-integer, _/bits >> = A,
% retrieve
GID = get(gid),
User = egs_db:users_select(GID),
{ok, User} = egs_user_model:read(GID),
% hit!
#hit_response{type=Type, user=NewUser, exp=HasEXP, damage=Damage, targethp=TargetHP, targetse=TargetSE, events=Events} = psu_missions:object_hit(User, SourceID, TargetID),
case Type of
@ -994,7 +1009,7 @@ handle_hits(Data) ->
% TODO: also has a hit sent, we should send it too
handle_events(Events);
_ ->
PlayerHP = (NewUser#users.character)#characters.currenthp,
PlayerHP = (NewUser#egs_user_model.character)#characters.currenthp,
case lists:member(death, TargetSE) of
true -> SE = 16#01000200;
false -> SE = 16#01000000
@ -1007,13 +1022,13 @@ handle_hits(Data) ->
end,
% exp
if HasEXP =:= true ->
Character = NewUser#users.character,
Character = NewUser#egs_user_model.character,
Level = Character#characters.mainlevel,
send_0115(GID, TargetID, Level#level.number, Level#level.exp, Character#characters.money);
true -> ignore
end,
% save
egs_db:users_insert(NewUser),
egs_user_model:write(NewUser),
% next
handle_hits(Rest).
@ -1046,8 +1061,8 @@ send(Packet) ->
send_010d(User) ->
GID = get(gid),
CharGID = User#users.gid,
<< _:640, CharBin/bits >> = psu_characters:character_user_to_binary(User#users{lid=0}),
CharGID = User#egs_user_model.id,
<< _:640, CharBin/bits >> = psu_characters:character_user_to_binary(User#egs_user_model{lid=0}),
send(<< 16#010d0300:32, 0:160, 16#00011300:32, GID:32/little-unsigned-integer, 0:64,
1:32/little-unsigned-integer, 0:32, 16#00000300:32, 16#ffff0000:32, 0:32, CharGID:32/little-unsigned-integer,
0:192, CharGID:32/little-unsigned-integer, 0:32, 16#ffffffff:32, CharBin/binary >>).
@ -1100,8 +1115,8 @@ send_0200(ZoneType) ->
send_0201(User) ->
GID = get(gid),
CharGID = User#users.gid,
CharBin = psu_characters:character_user_to_binary(User#users{lid=0}),
CharGID = User#egs_user_model.id,
CharBin = psu_characters:character_user_to_binary(User#egs_user_model{lid=0}),
IsGM = 0,
OnlineStatus = 0,
GameVersion = 0,
@ -1175,7 +1190,8 @@ send_021b() ->
send_021e() ->
{ok, << File:1184/bits, _/bits >>} = file:read_file("p/unicube.bin"),
[StrCount] = io_lib:format("~b", [egs_db:users_count()]),
{ok, Count} = egs_user_model:count(),
[StrCount] = io_lib:format("~b", [Count]),
UCS2Count = << << X:8, 0:8 >> || X <- StrCount >>,
PaddingSize = (12 - byte_size(UCS2Count)) * 8,
send(<< 16#021e0300:32, 0:288, File/binary, UCS2Count/binary, 0:PaddingSize >>).
@ -1217,8 +1233,8 @@ build_0233_contents([]) ->
<< >>;
build_0233_contents(Users) ->
[User|Rest] = Users,
LID = 16#010000 + User#users.lid, % @todo The LID must be 16 bits and 0233 seems to (almost always) require that 01 right there...
CharBin = psu_characters:character_user_to_binary(User#users{lid=LID}),
LID = 16#010000 + User#egs_user_model.lid, % @todo The LID must be 16 bits and 0233 seems to (almost always) require that 01 right there...
CharBin = psu_characters:character_user_to_binary(User#egs_user_model{lid=LID}),
IsGM = 0,
GameVersion = 0,
Chunk = << CharBin/binary, IsGM:8, 0:8, GameVersion:8, 0:8 >>,
@ -1243,7 +1259,8 @@ send_0304(FromGID, FromName, Modifiers, Message) ->
%% @todo The value before IntDir seems to be the player's current animation. 01 stand up, 08 ?, 17 normal sit
send_0503(#pos{x=PrevX, y=PrevY, z=PrevZ, dir=_}) ->
#users{gid=GID, pos=#pos{x=X, y=Y, z=Z, dir=Dir}, questid=QuestID, zoneid=ZoneID, mapid=MapID, entryid=EntryID} = egs_db:users_select(get(gid)),
{ok, User} = egs_user_model:read(get(gid)),
#egs_user_model{id=GID, pos=#pos{x=X, y=Y, z=Z, dir=Dir}, area=#psu_area{questid=QuestID, zoneid=ZoneID, mapid=MapID}, entryid=EntryID} = User,
IntDir = trunc(Dir * 182.0416),
send(<< 16#05030300: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,
16#1000:16, IntDir:16/little-unsigned-integer, PrevX:32/little-float, PrevY:32/little-float, PrevZ:32/little-float, X:32/little-float, Y:32/little-float, Z:32/little-float,
@ -1320,8 +1337,8 @@ send_0c10(Options) ->
%% @todo The values after the Char variable are the flags. Probably use bits to define what flag is and isn't set. Handle correctly.
send_0d01(User) ->
CharBin = psu_characters:character_tuple_to_binary(User#users.character),
OptionsBin = psu_characters:options_tuple_to_binary((User#users.character)#characters.options),
CharBin = psu_characters:character_tuple_to_binary(User#egs_user_model.character),
OptionsBin = psu_characters:options_tuple_to_binary((User#egs_user_model.character)#characters.options),
send(<< (header(16#0d01))/binary, CharBin/binary,
16#ffbbef1c:32, 16#f8ff0700:32, 16#fc810916:32, 16#7802134c:32,
16#b0c0040f:32, 16#7cf0e583:32, 16#b7bce0c6:32, 16#7ff8f963:32,
@ -1465,7 +1482,7 @@ send_1215(A, B) ->
%% @todo Find out the remaining values.
send_1500(User) ->
#characters{slot=Slot, name=Name, race=Race, gender=Gender, class=Class} = User#users.character,
#characters{slot=Slot, name=Name, race=Race, gender=Gender, class=Class} = User#egs_user_model.character,
RaceBin = psu_characters:race_atom_to_binary(Race),
GenderBin = psu_characters:gender_atom_to_binary(Gender),
ClassBin = psu_characters:class_atom_to_binary(Class),

View File

@ -76,7 +76,7 @@ loop(CSocket, SessionID) ->
?MODULE:loop(CSocket, SessionID);
{error, closed} ->
log(SessionID, "quit"),
egs_db:users_delete(SessionID)
egs_user_model:delete(SessionID)
end.
%% @spec handle(Command, CSocket, SessionID, Orig) -> ok | closed
@ -106,7 +106,7 @@ handle(16#0219, CSocket, SessionID, Orig) ->
Auth = crypto:rand_bytes(4),
Folder = << Username/binary, "-", Password/binary >>,
Time = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
egs_db:users_insert(#users{gid=SessionID, pid=self(), socket=CSocket, auth=Auth, time=Time, folder=Folder}),
egs_user_model:write(#egs_user_model{id=SessionID, pid=self(), socket=CSocket, state={wait_for_authentication, Auth}, time=Time, folder=Folder}),
Packet = << 16#02230300:32, 0:192, SessionID:32/little-unsigned-integer, 0:64, SessionID:32/little-unsigned-integer, Auth:32/bits >>,
egs_proto:packet_send(CSocket, Packet);

View File

@ -50,7 +50,7 @@ character_tuple_to_binary(Tuple) ->
%% @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, character=Character, pos=#pos{x=X, y=Y, z=Z, dir=Dir}, questid=QuestID, zoneid=ZoneID, mapid=MapID, entryid=EntryID} = User,
#egs_user_model{id=CharGID, lid=CharLID, character=Character, pos=#pos{x=X, y=Y, z=Z, dir=Dir}, area={psu_area, QuestID, ZoneID, MapID}, entryid=EntryID} = User,
#characters{mainlevel=Level, stats=Stats, se=SE, currenthp=CurrentHP, maxhp=MaxHP} = Character,
#level{number=LV} = Level,
CharBin = psu_characters:character_tuple_to_binary(Character),

View File

@ -110,7 +110,7 @@ warp_event(InstanceID, BlockID, ListNb, ObjectNb) ->
object_hit(User, _SourceID, TargetID) ->
try
Object = egs_db:objects_select_by_targetid(User#users.instanceid, TargetID),
Object = egs_db:objects_select_by_targetid(User#egs_user_model.instanceid, TargetID),
if Object#objects.type =:= box ->
box_hit(User, Object);
true ->
@ -135,18 +135,18 @@ box_hit(User, Box) ->
enemy_hit(User) ->
Damage = 1,
IncEXP = 1,
Character = User#users.character,
Character = User#egs_user_model.character,
Level = Character#characters.mainlevel,
NewEXP = Level#level.exp + IncEXP,
NewLevel = Level#level{exp=NewEXP},
NewCharacter = Character#characters{mainlevel=NewLevel},
NewUser = User#users{character=NewCharacter},
NewUser = User#egs_user_model{character=NewCharacter},
% todo delete the enemy from the db when it dies
#hit_response{type=enemy, user=NewUser, exp=true, damage=Damage, targethp=0, targetse=[death]}.
player_hit(User) ->
Damage = 10,
Character = User#users.character,
Character = User#egs_user_model.character,
TmpHP = Character#characters.currenthp - Damage,
if TmpHP =< 0 ->
NewHP = 0,
@ -156,7 +156,7 @@ player_hit(User) ->
SE = [flinch]
end,
NewCharacter = Character#characters{currenthp=NewHP},
NewUser = User#users{character=NewCharacter},
NewUser = User#egs_user_model{character=NewCharacter},
#hit_response{type=player, user=NewUser, damage=Damage, targethp=NewHP, targetse=SE}.
spawn_cleared(InstanceID, SpawnID) ->