egs/src/egs_game.erl

497 lines
18 KiB
Erlang
Raw Normal View History

% EGS: Erlang Game Server
% Copyright (C) 2010 Loic Hoguin
%
% 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_game).
-export([start/0]). % external
2010-05-22 09:59:19 +08:00
-export([listen/0, accept/1, process/2, char_select/3, lobby_load/5, loop/3, loop/4]). % internal
-include("include/records.hrl").
-include("include/network.hrl").
-include("include/maps.hrl").
%% @doc Start the game server.
start() ->
Pid = spawn_link(?MODULE, listen, []),
Pid.
%% @doc Listen for connections.
listen() ->
process_flag(trap_exit, true),
{ok, LSocket} = ssl:listen(?GAME_PORT, ?GAME_LISTEN_OPTIONS),
?MODULE:accept(LSocket).
%% @doc Accept connections.
accept(LSocket) ->
case ssl:transport_accept(LSocket, 5000) of
{ok, CSocket} ->
ssl:ssl_accept(CSocket),
try
log(0, "hello (new connection)"),
egs_proto:send_hello(CSocket),
Pid = spawn_link(?MODULE, process, [CSocket, 0]),
ssl:controlling_process(CSocket, Pid)
catch
_:_ ->
reload
end;
_ ->
reload
end,
?MODULE:accept(LSocket).
%% @doc Process the new connections.
%% Send an hello packet, authenticate the user and send him to character select.
process(CSocket, Version) ->
case egs_proto:packet_recv(CSocket, 5000) of
{ok, Packet} ->
<< _:32, Command:16/unsigned-integer, _/bits >> = Packet,
process_handle(Command, CSocket, Version, Packet);
{error, timeout} ->
reload,
?MODULE:process(CSocket, Version);
{error, closed} ->
log(0, "recv error, closing")
end.
%% @doc Game server auth request handler.
process_handle(16#020d, CSocket, Version, Packet) ->
[{gid, GID}, {auth, Auth}] = egs_proto:parse_game_auth(Packet),
case egs_db:users_select(GID) of
error ->
log(GID, "can't find user, closing"),
ssl:close(CSocket);
User ->
case User#users.auth of
Auth ->
log(GID, "good auth, proceed"),
LID = egs_db:next(lobby),
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_proto:send_flags(CSocket, GID),
?MODULE:char_select(CSocket, GID, Version);
_ ->
log(GID, "bad auth, closing"),
egs_db:users_delete(GID),
ssl:close(CSocket)
end
end;
%% @doc Platform information handler. Obtain the game version.
process_handle(16#080e, CSocket, _, Packet) ->
[{version, RealVersion}] = egs_proto:parse_platform_info(Packet),
?MODULE:process(CSocket, RealVersion);
%% @doc Unknown command handler. Do nothing.
process_handle(Command, CSocket, Version, _) ->
log(0, io_lib:format("(process) dismissed packet ~4.16.0b", [Command])),
?MODULE:process(CSocket, Version).
%% @doc Character selection screen loop.
%% The default entry point currently is first floor, near the uni cube.
char_select(CSocket, GID, Version) ->
case egs_proto:packet_recv(CSocket, 5000) of
{ok, Packet} ->
<< _:32, Command:16/unsigned-integer, _/bits >> = Packet,
char_select_handle(Command, CSocket, GID, Version, Packet);
{error, timeout} ->
egs_proto:send_keepalive(CSocket, GID),
reload,
?MODULE:char_select(CSocket, GID, Version);
{error, closed} ->
log(GID, "recv error, closing"),
egs_db:users_delete(GID)
end.
%% @doc Character selection handler.
char_select_handle(16#020b, CSocket, GID, Version, Packet) ->
log(GID, "character selection"),
[{number, Number}] = egs_proto:parse_character_select(Packet),
char_select_load(CSocket, GID, Version, Number);
%% @doc Character creation handler.
char_select_handle(16#0d02, CSocket, GID, Version, Packet) ->
log(GID, "character creation"),
User = egs_db:users_select(GID),
[{number, Number}, {char, Char}] = egs_proto:parse_character_create(Packet),
_ = 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:192 >>),
char_select_load(CSocket, GID, Version, Number);
%% @doc Character selection screen request.
char_select_handle(16#0d06, CSocket, GID, Version, _) ->
log(GID, "send character selection screen"),
User = egs_db:users_select(GID),
egs_proto:send_character_list(CSocket, GID,
char_load(User#users.folder, 0),
char_load(User#users.folder, 1),
char_load(User#users.folder, 2),
char_load(User#users.folder, 3)),
?MODULE:char_select(CSocket, GID, Version);
%% @doc Unknown command handler. Do nothing.
char_select_handle(Command, CSocket, GID, Version, _) ->
log(GID, io_lib:format("(char_select) dismissed packet ~4.16.0b", [Command])),
?MODULE:char_select(CSocket, GID, Version).
%% @doc Load the given character's data.
char_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, _} ->
[{status, 0}, {char, << 0:2208 >>}]
end.
%% @doc Load the selected character and start the main game's loop.
char_select_load(CSocket, GID, Version, Number) ->
User = egs_db:users_select(GID),
[{status, _}, {char, << Name:512/bits, _/bits >>}|_] = char_load(User#users.folder, Number),
NewRow = User#users{charnumber=Number, charname=Name},
egs_db:users_insert(NewRow),
2010-05-22 09:59:19 +08:00
?MODULE:lobby_load(CSocket, GID, 0, 1, 1),
ssl:setopts(CSocket, [{active, true}]),
?MODULE:loop(CSocket, GID, Version).
%% @doc Load the given map as a standard lobby.
2010-05-22 09:59:19 +08:00
lobby_load(CSocket, GID, MapType, MapNumber, MapEntry) ->
OldUser = egs_db:users_select(GID),
2010-05-22 09:59:19 +08:00
User = OldUser#users{maptype=MapType, mapnumber=MapNumber, mapentry=MapEntry},
egs_db:users_insert(User),
[{status, 1}, {char, Char}, {options, Options}] = char_load(User#users.folder, User#users.charnumber),
2010-05-22 09:59:19 +08:00
[{name, _}, {quest, Quest}, {zone, Zone}] = proplists:get_value(MapNumber, ?MAPS, [{name, "Unknown"}, {quest, "p/quest.gc1.nbl"}, {zone, "p/zone.gc1.nbl"}]),
try
% broadcast spawn to other people
lists:foreach(fun(Other) -> Other#users.pid ! {psu_player_spawn, User} end, egs_db:users_select_others(GID)),
% load lobby and character
egs_proto:send_character_selected(CSocket, GID, Char, Options),
% 0246 0a0a 1006
send_packet_1005(CSocket, GID, Char),
% 1006 0210
egs_proto:send_universe_info(CSocket, GID),
egs_proto:send_player_card(CSocket, GID, Char),
% 1501 1512 0303
egs_proto:send_npc_info(CSocket, GID),
% 0c00
egs_proto:send_quest(CSocket, Quest),
% 0a05 0111 010d
send_packet_200(CSocket, GID),
egs_proto:send_zone(CSocket, Zone),
2010-05-22 09:59:19 +08:00
egs_proto:send_map(CSocket, MapType, MapNumber, MapEntry),
% 100e 020c
egs_proto:send_load_quest(CSocket, GID),
2010-05-22 09:59:19 +08:00
send_packet_201(CSocket, GID, User, Char),
% 0a06
Users = egs_db:users_select_others(GID),
send_packet_233(CSocket, GID, Users),
egs_proto:send_loading_end(CSocket, GID),
egs_proto:send_camera_center(CSocket, GID)
catch
_ ->
ssl:close(CSocket),
log(GID, "send error, closing")
end.
%% @doc Alias for the game main's loop when the buffer is empty.
loop(CSocket, GID, Version) ->
loop(CSocket, GID, Version, << >>).
%% @doc Game's main loop.
loop(CSocket, GID, Version, SoFar) ->
receive
{psu_broadcast, Packet} ->
<< A:192/bits, _:64, B/bits >> = Packet,
Broadcast = << A/binary, 16#00011300:32, GID:32/little-unsigned-integer, B/binary >>,
egs_proto:packet_send(CSocket, Broadcast),
?MODULE:loop(CSocket, GID, Version, SoFar);
{psu_chat, ChatGID, ChatName, ChatModifiers, ChatMessage} ->
egs_proto:send_chat(CSocket, Version, ChatGID, ChatName, ChatModifiers, ChatMessage),
?MODULE:loop(CSocket, GID, Version, SoFar);
{psu_keepalive} ->
egs_proto:send_keepalive(CSocket, GID),
?MODULE:loop(CSocket, GID, Version, SoFar);
{psu_player_spawn, PlayerGID} ->
send_spawn(CSocket, GID, PlayerGID),
?MODULE:loop(CSocket, GID, Version, SoFar);
{ssl, _, Data} ->
{Packets, Rest} = egs_proto:packet_split(<< SoFar/bits, Data/bits >>),
[dispatch(CSocket, GID, Version, P) || P <- Packets],
?MODULE:loop(CSocket, GID, Version, Rest);
{ssl_closed, _} ->
close(CSocket, GID);
{ssl_error, _, _} ->
close(CSocket, GID);
_ ->
?MODULE:loop(CSocket, GID, Version, SoFar)
after 1000 ->
reload,
?MODULE:loop(CSocket, GID, Version, SoFar)
end.
%% @doc Close the connection for the given user.
close(CSocket, GID) ->
log(GID, "quit"),
egs_db:users_delete(GID),
ssl:close(CSocket).
%% @doc Dispatch the command to the right handler.
dispatch(CSocket, GID, Version, Packet) ->
<< _:32, Command:16/unsigned-integer, Channel:8/little-unsigned-integer, _/bits >> = Packet,
case Channel of
1 ->
broadcast(Command, CSocket, GID, Version, Packet);
_ ->
handle(Command, CSocket, GID, Version, Packet)
end.
%% @doc Position change broadcast handler. Save the position and then dispatch it.
broadcast(16#0503, _, GID, _, Packet) ->
2010-05-22 09:59:19 +08:00
<< _:448, Coords:96/bits, _:128, MapType:32/little-unsigned-integer, MapNumber:32/little-unsigned-integer,
MapEntry:32/little-unsigned-integer, _/bits >> = Packet,
User = egs_db:users_select(GID),
2010-05-22 09:59:19 +08:00
NewUser = User#users{coords=Coords, maptype=MapType, mapnumber=MapNumber, mapentry=MapEntry},
egs_db:users_insert(NewUser),
broadcast(default, ignore, GID, ignore, Packet);
%% @doc Default broadcast handler. Dispatch the packet to everyone (for now).
%% We clean up the packet and use the real GID and LID of the user, disregarding what was sent and possibly tampered with.
%% @todo Don't query the user data everytime! Keep an User instead of a GID probably.
broadcast(_, _, GID, _, Packet) ->
<< _:32, A:64/bits, _:64, B:192/bits, _:64, C/bits >> = Packet,
case egs_db:users_select(GID) of
error ->
ignore;
Self ->
LID = Self#users.lid,
SafePacket = << 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, SafePacket} end, egs_db:users_select_others(GID))
end.
%% @doc Movement (non-broadcast) handler. Do nothing.
handle(16#0102, _, _, _, _) ->
ignore;
%% @doc Keepalive handler. Do nothing.
handle(16#021c, _, _, _, _) ->
ignore;
%% @doc Uni cube handler.
handle(16#021d, CSocket, GID, _, _) ->
log(GID, "uni cube"),
egs_proto:send_universe_cube(CSocket);
%% @doc Uni selection handler.
%% When selecting 'Your room', load first floor for now.
%% When selecting 'Reload', load first floor.
%% @todo Load 'Your room' correctly.
handle(16#021f, CSocket, GID, _, Packet) ->
case egs_proto:parse_uni_select(Packet) of
[{uni, 0}] ->
log(GID, "uni selection cancelled");
[{uni, 16#ffffffff}] ->
log(GID, "uni selection (my room)"),
% 0230 0220
% myroom_load(CSocket, GID, Version, 16#a701, 16#0100);
2010-05-22 10:00:07 +08:00
lobby_load(CSocket, GID, 11, 103, 1);
_ ->
log(GID, "uni selection (reload)"),
% 0230 0220
2010-05-22 09:59:19 +08:00
lobby_load(CSocket, GID, 0, 1, 1)
end;
%% @doc Shortcut changes handler. Do nothing.
%% @todo Save it.
handle(16#0302, _, GID, _, _) ->
log(GID, "dismissed shortcut changes");
%% @doc Chat broadcast handler. Dispatch the message to everyone (for now).
%% We must take extra precautions to handle different versions of the game correctly.
%% @todo Only broadcast to people in the same map.
handle(16#0304, _, GID, Version, Packet) ->
log(GID, "broadcast chat"),
[{gid, _}, {name, ChatName}, {modifiers, ChatModifiers}, {message, ChatMessage}] = egs_proto:parse_chat(Version, Packet),
case ChatName of
missing ->
case egs_db:users_select(GID) of
error ->
ActualName = ChatName;
User ->
ActualName = User#users.charname
end;
_ ->
ActualName = ChatName
end,
lists:foreach(fun(User) -> User#users.pid ! {psu_chat, GID, ActualName, ChatModifiers, ChatMessage} end, egs_db:users_select_all());
%% @doc Lobby change handler.
handle(16#0807, CSocket, GID, _, Packet) ->
2010-05-22 09:59:19 +08:00
[{maptype, MapType}, {mapnumber, MapNumber}, {mapentry, MapEntry}] = egs_proto:parse_lobby_change(Packet),
log(GID, io_lib:format("lobby change (~4.16.0b,~4.16.0b,~4.16.0b)", [MapType, MapNumber, MapEntry])),
lobby_load(CSocket, GID, MapType, MapNumber, MapEntry);
%% @doc Unknown flags-related command handler.
%% Just reply with a success value.
%% @todo Find what it really does and handle it correctly.
handle(16#0d04, CSocket, GID, _, Orig) ->
log(GID, "fake flags handler"),
<< _:352, A:144/bits, _:8, B/bits >> = Orig,
Packet = << 16#0d040300:32, 0:160, 16#00011300:32, GID:32/little-unsigned-integer, 0:64, A/binary, 1, B/binary >>,
file:write_file("fakingit.bin", Packet),
egs_proto:packet_send(CSocket, Packet);
%% @doc Options changes handler.
handle(16#0d07, _, GID, _, Packet) ->
log(GID, "options changes"),
[{options, Options}] = egs_proto:parse_options_change(Packet),
User = egs_db:users_select(GID),
file:write_file(io_lib:format("save/~s/~b-character.options", [User#users.folder, User#users.charnumber]), Options);
2010-05-20 06:09:03 +08:00
%% @doc Lobby event handler. Handle chairs!
%% Apparently used for elevator, sit on chairs, and more?
2010-05-20 13:57:01 +08:00
%% @todo Handle more than sit on chair.
2010-05-20 06:09:03 +08:00
handle(16#0f0a, CSocket, GID, _, Orig) ->
<< _:448, A:32/little-unsigned-integer, _:64, B:32/little-unsigned-integer, _/bits >> = Orig,
Packet = << 16#1211:16, 0:176, 16#00011300:32, GID:32/little-unsigned-integer, 0:64, A:32/little-unsigned-integer, B:32/little-unsigned-integer, 8:32/little-unsigned-integer, 0:32 >>,
egs_proto:packet_send(CSocket, Packet),
log(GID, "lobby event (can only chair so far)");
%% @doc Unknown command handler. Do nothing.
handle(Command, _, GID, _, _) ->
log(GID, io_lib:format("(game) dismissed packet ~4.16.0b", [Command])).
%% @todo Figure out what the packet is.
send_packet_200(CSocket, GID) ->
{ok, File} = file:read_file("p/packet0200.bin"),
<< _:288, After/bits >> = File,
Packet = << 16#0200:16, 0:208, GID:32/little-unsigned-integer, After/binary >>,
egs_proto:packet_send(CSocket, Packet).
%% @todo Figure out what the other things are.
2010-05-22 09:59:19 +08:00
send_packet_201(CSocket, GID, User, Char) ->
MapType = User#users.maptype,
MapNumber = User#users.mapnumber,
MapEntry = User#users.mapentry,
CharGID = User#users.gid,
CharLID = User#users.lid,
{ok, File} = file:read_file("p/packet0201.bin"),
2010-05-22 09:59:19 +08:00
<< _:96, A:32/bits, _:96, B:32/bits, _:256, D:64/bits, _:2624, After/bits >> = File,
Packet = << 16#0201:16, 0:48, A/binary, CharGID:32/little-unsigned-integer, 0:64, B/binary, GID:32/little-unsigned-integer,
2010-05-22 09:59:19 +08:00
0:64, CharLID:32/little-unsigned-integer, CharGID:32/little-unsigned-integer, 0:96, D/binary, MapType:32/little-unsigned-integer,
MapNumber:32/little-unsigned-integer, MapEntry:32/little-unsigned-integer, 0:320, Char/binary, After/binary >>,
egs_proto:packet_send(CSocket, Packet).
%% @todo Figure out what the other things are.
send_packet_233(CSocket, GID, Users) ->
NbUsers = length(Users),
case NbUsers of
0 ->
ignore;
_ ->
Header = << 16#02330300:32, 0:32, 16#00001200:32, GID:32/little-unsigned-integer, 0:64, 16#00011300:32,
GID:32/little-unsigned-integer, 0:64, NbUsers:32/little-unsigned-integer >>,
Contents = build_packet_233_contents(Users),
Packet = << Header/binary, Contents/binary >>,
egs_proto:packet_send(CSocket, Packet)
end.
build_packet_233_contents([]) ->
<< >>;
build_packet_233_contents(Users) ->
[User|Rest] = Users,
{ok, File} = file:read_file("p/player.bin"),
2010-05-22 09:59:19 +08:00
<< A:32/bits, _:32, B:64/bits, _:32, C:64/bits, _:96, D:32/bits, _:96, E:96/bits, _:2304, F/bits >> = File,
{ok, CharFile} = file:read_file(io_lib:format("save/~s/~b-character", [User#users.folder, User#users.charnumber])),
CharGID = User#users.gid,
LID = User#users.lid,
case User#users.coords of % TODO: temporary? undefined handling
undefined ->
Coords = << 0:96 >>,
2010-05-22 09:59:19 +08:00
MapType = 0,
MapNumber = 1,
MapEntry = 0;
_ ->
Coords = User#users.coords,
2010-05-22 09:59:19 +08:00
MapType = User#users.maptype,
MapNumber = User#users.mapnumber,
MapEntry = User#users.mapentry
end,
Chunk = << A/binary, CharGID:32/little-unsigned-integer, B/binary, LID:16/little-unsigned-integer, 16#0100:16, C/binary,
2010-05-22 09:59:19 +08:00
MapType:32/little-unsigned-integer, MapNumber:32/little-unsigned-integer, MapEntry:32/little-unsigned-integer, D:32/bits, Coords:96/bits, E/binary,
MapType:32/little-unsigned-integer, MapNumber:32/little-unsigned-integer, MapEntry:32/little-unsigned-integer, CharFile/binary, F/binary >>,
Next = build_packet_233_contents(Rest),
<< Chunk/binary, Next/binary >>.
%% @todo Figure out what the packet is.
send_packet_1005(CSocket, GID, Char) ->
{ok, File} = file:read_file("p/packet1005.bin"),
<< _:352, Before:160/bits, _:608, After/bits >> = File,
<< Name:512/bits, _/bits >> = Char,
Packet = << 16#1005:16, 0:208, GID:32/little-unsigned-integer, 0:64, Before/binary, GID:32/little-unsigned-integer, 0:64, Name/binary, After/binary >>,
egs_proto:packet_send(CSocket, Packet).
%% @todo Figure out what the other things are and do it right.
%% @todo Temporarily send 233 until the correct process is figured out.
%% Should be something along the lines of 203 201 204.
send_spawn(CSocket, GID, _) ->
send_packet_233(CSocket, GID, egs_db:users_select_others(GID)).
%% @doc Log message to the console.
log(GID, Message) ->
io:format("game (~.10b): ~s~n", [GID, Message]).