egs/src/egs_proto.erl
2010-06-04 02:03:43 +02:00

363 lines
13 KiB
Erlang

% 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_proto).
-compile(export_all).
%% @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 Receive exactly one packet command. Handle errors properly. Return the full packet for the command.
packet_recv(CSocket, Timeout) ->
case packet_safe_recv(CSocket, 4, Timeout) of
{error, A} ->
{error, A};
{ok, << Size:32/little-unsigned-integer >>} ->
case packet_safe_recv(CSocket, Size - 4, Timeout) of
{error, B} ->
{error, B};
{ok, Tail} ->
{ok, << Size:32/little-unsigned-integer, Tail/binary >>}
end
end.
%% @doc Safely receive a packet. Close the connection if an error happens.
packet_safe_recv(CSocket, Size, Timeout) ->
try ssl:recv(CSocket, Size, Timeout) of
{ok, Packet} ->
{ok, Packet};
{error, timeout} ->
{error, timeout};
{error, _} ->
ssl:close(CSocket),
{error, closed}
catch
_ ->
ssl:close(CSocket),
{error, closed}
end.
%% @doc Send a packet. The packet argument must not contain the size field.
packet_send(CSocket, Packet) ->
{ok, Size, Padding} = packet_prepare(Packet),
packet_send(CSocket, << Size:32/little-unsigned-integer, Packet/binary, Padding/binary >>, Size).
%% @doc Send a normal command.
packet_send(CSocket, Packet, Size) when Size =< 16#4000 ->
ssl:send(CSocket, Packet);
%% @doc Send a fragmented command when size is too big.
%% @todo Wait for fragments reception confirmation?
packet_send(CSocket, Packet, Size) ->
packet_fragment_send(CSocket, Packet, Size, 0).
%% @doc Send the last chunk of a fragmented command.
packet_fragment_send(CSocket, Packet, Size, Current) when Size - Current =< 16#4000 ->
FragmentSize = 16#10 + byte_size(Packet),
Fragment = << FragmentSize:32/little-unsigned-integer, 16#0b030000:32/unsigned-integer,
Size:32/little-unsigned-integer, Current:32/little-unsigned-integer, Packet/binary >>,
ssl:send(CSocket, Fragment);
%% @doc Send another chunk of a fragmented command.
packet_fragment_send(CSocket, Packet, Size, Current) ->
<< Chunk:131072/bits, Rest/bits >> = Packet,
Fragment = << 16#10400000:32/unsigned-integer, 16#0b030000:32/unsigned-integer,
Size:32/little-unsigned-integer, Current:32/little-unsigned-integer, Chunk/binary >>,
ssl:send(CSocket, Fragment),
packet_fragment_send(CSocket, Rest, Size, Current + 16#4000).
%% @doc Split a packet received into commands. This is only needed when receiving packets in active mode.
packet_split(Packet) ->
packet_split(Packet, []).
packet_split(Packet, Result) ->
<< Size:32/little-unsigned-integer, _/bits >> = Packet,
case Size > byte_size(Packet) of
true ->
{Result, Packet};
false ->
BitSize = Size * 8,
<< Split:BitSize/bits, Rest/bits >> = Packet,
case Rest of
<< >> ->
{Result ++ [Split], << >>};
_ ->
packet_split(Rest, Result ++ [Split])
end
end.
%% @doc Parse a character creation command. Return the character number and data.
parse_character_create(Packet) ->
<< _:352, Number:32/little-unsigned-integer, Char/bits >> = Packet,
[{number, Number}, {char, Char}].
%% @doc Parse a character selection command. Return the selected character's number.
parse_character_select(Packet) ->
<< _:352, Number:32/little-unsigned-integer, _/bits >> = Packet,
[{number, Number}].
%% @doc Parse a chat command. AOTI v2.000 version of the command.
parse_chat(0, Packet) ->
<< _:384, FromGID:32/unsigned-integer, Modifiers:128/bits, Message/bits >> = Packet,
[{gid, FromGID}, {name, missing}, {modifiers, Modifiers}, {message, Message}];
%% @doc Parse a chat command. AOTI since an unknown version of the game.
parse_chat(_, Packet) ->
<< _:384, FromGID:32/unsigned-integer, Modifiers:128/bits, FromName:512/bits, Message/bits >> = Packet,
[{gid, FromGID}, {name, FromName}, {modifiers, Modifiers}, {message, Message}].
%% @doc Parse the game server auth command. Used when first connecting to a game server.
parse_game_auth(Packet) ->
<< _:352, GID:32/little-unsigned-integer, Auth:32/bits, _/bits >> = Packet,
[{gid, GID}, {auth, Auth}].
%% @doc Parse a lobby change command.
parse_lobby_change(Packet) ->
<< _:352, Quest:32/little-unsigned-integer, MapType:16/little-unsigned-integer,
MapNumber:16/little-unsigned-integer, MapEntry:16/little-unsigned-integer, _/bits >> = Packet,
[{quest, Quest}, {maptype, MapType}, {mapnumber, MapNumber}, {mapentry, MapEntry}].
%% @doc Parse the options change command. Retrieve the options for saving.
parse_options_change(Packet) ->
<< _:352, Options/bits >> = Packet,
[{options, Options}].
%% @doc Parse the platform information command. Retrieve the game version for later use.
parse_platform_info(Packet) ->
<< _:416, Version:32/little-unsigned-integer, _/bits >> = Packet,
[{version, Version}].
%% @doc Parse the uni selection command.
parse_uni_select(Packet) ->
<< _:352, Uni:32/little-unsigned-integer, _/bits >> = Packet,
[{uni, Uni}].
%% @doc Center the camera on the player, if possible.
%% @todo Probably.
send_camera_center(CSocket, GID) ->
Packet = << 16#0236:16, 0:208, GID:32/little-unsigned-integer, 0:64 >>,
packet_send(CSocket, Packet).
%% @doc Send the character list for selection.
send_character_list(CSocket, GID, Data0, Data1, Data2, Data3) ->
[{status, Status0}, {char, Char0}|_] = Data0,
[{status, Status1}, {char, Char1}|_] = Data1,
[{status, Status2}, {char, Char2}|_] = Data2,
[{status, Status3}, {char, Char3}|_] = Data3,
Packet = << 16#0d03:16/unsigned-integer, 0:80, GID:32/little-unsigned-integer, 0:96, GID:32/little-unsigned-integer, 0:104,
Status0:8/unsigned-integer, 0:48, Char0/binary, 0:520,
Status1:8/unsigned-integer, 0:48, Char1/binary, 0:520,
Status2:8/unsigned-integer, 0:48, Char2/binary, 0:520,
Status3:8/unsigned-integer, 0:48, Char3/binary, 0:512 >>,
egs_proto:packet_send(CSocket, Packet).
%% @doc Send the data for the selected character.
send_character_selected(CSocket, GID, Char, Options) ->
Packet = << 16#0d01:16, 0:208, GID:32/little-unsigned-integer, 0:64, Char/binary, 0:8128, Options/binary >>,
packet_send(CSocket, Packet).
%% @doc Send a chat command. AOTI v2.000 version of the command.
send_chat(CSocket, 0, FromGID, _, Modifiers, Message) ->
Packet = << 16#0304:16/unsigned-integer, 0:320, 16#1200:16/unsigned-integer, FromGID:32/little-unsigned-integer, Modifiers:128/bits, Message/bits >>,
packet_send(CSocket, Packet);
%% @doc Send a chat command. AOTI since an unknown version of the game.
send_chat(CSocket, _, FromGID, FromName, Modifiers, Message) ->
Packet = << 16#0304:16, 0:320, 16#1200:16/unsigned-integer, FromGID:32/little-unsigned-integer, Modifiers:128/bits, FromName:512/bits, Message/bits >>,
packet_send(CSocket, Packet).
%% @doc Send the character flags list.
%% Sent without fragmentation on official for unknown reasons. Do the same here.
send_flags(CSocket, GID) ->
{ok, Flags} = file:read_file("p/flags.bin"),
Packet = << 16#0d050300:32, 0:32, 16#00011300:32, GID:32/little-unsigned-integer, 0:64, 16#00011300:32, GID:32/little-unsigned-integer, 0:64, Flags/binary >>,
Size = 4 + byte_size(Packet),
ssl:send(CSocket, << Size:32/little-unsigned-integer, Packet/binary >>).
%% @doc Shortcut for send_global/4.
send_global(CSocket, Type, Message) ->
send_global(CSocket, Type, Message, 2).
%% @doc Send a global message.
%% There are four types of global messages: 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 Player X joined the party.
%% * timeout: A dialog in the center of the screen that disappears after Duration seconds.
send_global(CSocket, Type, Message, Duration) ->
TypeID = case Type of
dialog -> 0;
top -> 1;
scroll -> 2;
timeout -> 3;
_ -> 1
end,
UCS2Message = << << X:8, 0:8 >> || X <- Message >>,
try
Packet = << 16#0228:16, 0:304, TypeID:32/little-unsigned-integer, Duration:32/little-unsigned-integer, UCS2Message/binary, 0, 0 >>,
packet_send(CSocket, Packet)
catch
_:_ ->
ignore
end.
%% @doc Init quest.
send_init_quest(CSocket, GID, QuestID) ->
Packet = << 16#0c000300:32, 0:160, 16#00011300:32, GID:32/little-unsigned-integer, 0:64, QuestID:32/little-unsigned-integer,
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 >>,
packet_send(CSocket, Packet).
%% @doc Keepalive.
send_keepalive(CSocket, GID) ->
Packet = << 16#2b1b:16, 0:208, GID:32/little-unsigned-integer, 0:64 >>,
packet_send(CSocket, Packet).
%% @doc Make the client load the quest previously sent.
send_load_quest(CSocket, GID) ->
Packet = << 16#1212:16, 0:208, GID:32/little-unsigned-integer, 0:64 >>,
packet_send(CSocket, Packet).
%% @doc Indicate to the client that loading should finish.
send_loading_end(CSocket, GID) ->
Packet = << 16#0208:16, 0:208, GID:32/little-unsigned-integer, 0:96 >>,
packet_send(CSocket, Packet).
%% @doc Send the player's current location.
%% @todo Figure out what the last value is. No counter without it.
send_location(CSocket, GID, Quest, MapType, MapNumber, Location) ->
UCS2Location = << << X:8, 0:8 >> || X <- Location >>,
Packet = << 16#100e0300:32, 0:160, 16#00011300:32, GID:32/little-unsigned-integer, 0:64,
1:32/little-unsigned-integer, MapNumber:16/little-unsigned-integer, MapType:16/little-unsigned-integer,
Quest:32/little-unsigned-integer, UCS2Location/binary >>,
PaddingSize = (128 - byte_size(Packet) - 4) * 8,
packet_send(CSocket, << Packet/binary, 0:PaddingSize, 1:32/little-unsigned-integer >>).
%% @doc Send the map ID to be loaded by the client.
send_map(CSocket, Quest, MapType, MapNumber, MapEntry) ->
Packet = << 16#0205:16, 0:304, Quest:32/little-unsigned-integer, MapType:32/little-unsigned-integer,
MapNumber:32/little-unsigned-integer, MapEntry:32/little-unsigned-integer, 0:64 >>,
packet_send(CSocket, Packet).
%% @doc Send the player's NPC and PM information.
send_npc_info(CSocket, GID) ->
{ok, File} = file:read_file("p/npc.bin"),
Packet = << 16#16020300:16, 0:192, GID:32/little-unsigned-integer, 0:96, File/binary >>,
packet_send(CSocket, Packet).
%% @doc Send the player's partner card.
send_player_card(CSocket, GID, Char) ->
<< CharInfo:576/bits, _/bits >> = Char,
Packet = << 16#1500:16, 0:208, GID:32/little-unsigned-integer, 0:64, CharInfo/binary, 0:3072, 16#01040103:32, 0:64 >>,
packet_send(CSocket, Packet).
%% @doc Send the quest file to be loaded.
send_quest(CSocket, Filename) ->
{ok, File} = file:read_file(Filename),
Size = byte_size(File),
Packet = << 16#020e:16, 0:304, Size:32/little-unsigned-integer, 0:32, File/binary >>,
packet_send(CSocket, Packet).
%% @doc Send the trial start notification.
send_trial_start(CSocket, GID) ->
Packet = << 16#0c090300:32, 0:160, 16#00011300:32, GID:32/little-unsigned-integer, 0:128 >>,
packet_send(CSocket, Packet).
%% @doc Send the list of available universes.
send_universe_cube(CSocket) ->
{ok, File} = file:read_file("p/unicube.bin"),
Packet = << 16#021e:16, 0:304, File/binary >>,
packet_send(CSocket, Packet).
%% @doc Send the current universe name and number.
%% @todo Currently only have universe number 2, named EGS Test.
send_universe_info(CSocket, GID) ->
Packet = << 16#0222:16, 0:80, GID:32/little-unsigned-integer, 0:96, GID:32/little-unsigned-integer, 0:64,
2:32/little-unsigned-integer, 0:32, 16#45, 0, 16#47, 0, 16#53, 0, 16#20, 0, 16#54, 0, 16#65, 0, 16#73, 0, 16#74, 0:24 >>,
packet_send(CSocket, Packet).
%% @doc Send the zone file to be loaded.
send_zone(CSocket, Filename) ->
{ok, File} = file:read_file(Filename),
Size = byte_size(File),
Packet = << 16#020f:16, 0:336, Size:32/little-unsigned-integer, File/binary >>,
packet_send(CSocket, Packet).
%% @doc Send the zone initialization notification.
send_zone_init(CSocket, GID, ZoneType) ->
case ZoneType of
mission ->
Var = << 16#06000500:32, 16#01000000:32, 0:64, 16#00040000:32, 16#00010000:32, 16#00140000:32 >>;
_ ->
Var = << 16#00040000:32, 0:160, 16#00140000:32 >>
end,
Packet = << 16#02000300:32, 0:160, 16#00011300:32, GID:32/little-unsigned-integer, 0:96, 16#01000000:32,
16#ffffffff:32, Var/binary, 16#ffffffff:32, 16#ffffffff:32 >>,
egs_proto:packet_send(CSocket, Packet).