egs/apps/egs_net/src/egs_net.erl
2012-05-16 13:31:37 +02:00

1577 lines
48 KiB
Erlang

%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010-2012 Loïc Hoguin.
%% @doc Login and game servers 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_net).
%% Client state manipulation API.
-export([init/4]).
-export([terminate/1]).
-export([get_gid/1]).
-export([set_gid/2]).
-export([set_handler/2]).
-export([set_keepalive/1]).
%% Receive loop.
-export([loop/1]).
%% Response API.
-export([account_character/2]).
-export([account_characters_response/2]).
-export([account_flags/4]).
-export([comm_own_card/2]).
-export([system_auth_error/2]).
-export([system_game_server_response/3]).
-export([system_hello/1]).
-export([system_key_auth_info/3]).
-export([system_motd_response/3]).
-export([system_open_url/2]).
%% @todo Temporary, remove.
-export([character_appearance_to_binary/2]).
-include_lib("erlson/include/erlson.hrl").
%% Network state.
-record(egs_net, {
socket :: ssl:sslsocket(),
transport :: module(),
handler :: module(),
buffer = <<>> :: binary(),
keepalive = false :: boolean(),
gid = 0 :: egs:gid(),
targetid = 16#ffff :: egs:targetid(),
slot = 0 :: 0..3, %% @todo Remove.
areanb = 0 :: non_neg_integer()
}).
%% Little-endian macros.
-define(l16, :16/little).
-define(l32, :32/little).
-define(l32f, :32/little-float).
%% Client state manipulation API.
init(Socket, Transport, Handler, GID) ->
#egs_net{socket=Socket, transport=Transport,
handler=Handler, gid=GID}.
terminate(#egs_net{socket=Socket, transport=Transport}) ->
Transport:close(Socket).
get_gid(#egs_net{gid=GID}) ->
GID.
set_gid(GID, State) ->
State#egs_net{gid=GID}.
set_handler(Handler, State) ->
State#egs_net{handler=Handler}.
set_keepalive(State) ->
State#egs_net{keepalive=true}.
%% Receive loop.
loop(State=#egs_net{socket=Socket, transport=Transport}) ->
Transport:setopts(Socket, [{active, once}]),
{OK, Closed, Error} = Transport:messages(),
receive
{OK, _, Data} -> handle(State, Data);
{Closed, _} -> closed;
{Error, _, _} -> closed;
{egs, keepalive} -> keepalive(State);
Info when element(1, Info) =:= egs -> info(State, Info)
end.
handle(State=#egs_net{buffer=Buffer}, Data) ->
{Commands, Rest} = split(<< Buffer/binary, Data/binary >>),
dispatch(State#egs_net{buffer=Rest}, Commands).
dispatch(State, []) ->
?MODULE:loop(State);
dispatch(State, [Data|Tail]) ->
case parse(Data) of
ignore ->
dispatch(State, Tail);
{Type, Event} ->
case call(State, Event, Type) of
closed -> closed;
ok -> dispatch(State, Tail);
{ok, State2} -> dispatch(State2, Tail)
end
end.
%% If keepalive is enabled we just send an empty packet since the
%% real keepalive packet is managed by Gameguard which is better disabled.
keepalive(State=#egs_net{keepalive=false}) ->
?MODULE:loop(State);
keepalive(State=#egs_net{keepalive=true}) ->
send_packet(<< 8?l32, 0:32 >>, State),
?MODULE:loop(State).
info(State, Info) ->
case call(State, Info, info) of
closed -> closed;
ok -> ?MODULE:loop(State);
{ok, State2} -> ?MODULE:loop(State2)
end.
%% @todo The try..catch should be only enabled for debug.
call(State=#egs_net{handler=Handler, gid=GID}, Data, Name) ->
try
%% @todo Make this io:format optional.
io:format("~b -> ~p ~p~n", [GID, Name, Data]),
Handler:Name(Data, State)
catch Class:Reason ->
error_logger:error_msg(
"** Handler error in ~p for ~p:~n"
" ~p~n"
" for the reason ~p:~p~n"
"** Stacktrace: ~p~n~n",
[Handler, Name, Data, Class, Reason, erlang:get_stacktrace()])
end.
%% Packet parsing code.
split(Data) ->
split(Data, []).
split(Data, Acc) when byte_size(Data) < 4 ->
{lists:reverse(Acc), Data};
split(Data = << Size:32/little, _/bits >>, Acc) when Size > byte_size(Data) ->
{lists:reverse(Acc), Data};
split(Data = << Size:32/little, _/bits >>, Acc) ->
<< Packet:Size/binary, Rest/bits >> = Data,
split(Rest, [Packet|Acc]).
%% Completely ignore the fragmented packet replies.
parse(<< 8:32/little, 16#0b05:16, _:16 >>) ->
ignore;
%% Catch parse errors and prints a dump of the packet with useful info.
parse(<< Size:32/little, Category:8, Sub:8, Channel:8, _:8, Data/bits >>) ->
try begin
Event = parse(Size, Category * 256 + Sub, Channel, Data),
case {Event, Channel} of
{ignore, _} -> ignore; %% @todo Always return something.
{Event, 1} -> {cast, Event};
{Event, _} -> {event, Event}
end
end catch Class:Reason ->
error_logger:error_msg(lists:flatten([
"** Parse error in with reason ~p:~p~n"
" for command #~2.16.0b~2.16.0b of size ~b on channel ~b~n~n",
binary_to_dump(Data), "~n~n"]),
[Class, Reason, Category, Sub, Size, Channel]),
ignore
end.
%% @todo Documentation.
%% @todo Probably shouldn't be ignored.
parse(92, 16#0102, 2, Data) ->
<< DestTargetID?l16, _:16,
CharActType:32, CharGID?l32, 0:32, 0:32,
DestActType:32, DestGID?l32, 0:32, 0:32,
CharGID?l32, CharTargetID?l16, _:16,
AnimType:8, AnimState:8, Dir?l16, 0:32,
X?l32f, Y?l32f, Z?l32f, QuestID?l32,
ZoneID?l16, _:16, MapID?l16, _:16, EntryID?l16, _:16, 0:32
>> = Data,
ignore;
%% @todo Documentation.
%% @todo A B
parse(Size, 16#0105, 2, Data) ->
<< DestTargetID?l16, _:16,
0:32, CharGID?l32, 0:32, 0:32,
DestActType:32, DestGID?l32, 0:32, 0:32,
CharGID?l32, CharTargetID?l16, _:16,
ItemIndex:8, EventID:8, PAID:8, A:8,
B?l32, Rest/binary
>> = Data,
Event = item_eventid_to_atom(EventID),
case Event of
item_drop ->
Size = 76,
<< Quantity?l32, X?l32f, Y?l32f, Z?l32f >> = Rest,
{Event, ItemIndex, CharGID, CharTargetID, A, B,
Quantity, X, Y, Z};
Event ->
Size = 60,
<<>> = Rest,
{Event, ItemIndex, CharGID, CharTargetID, A, B}
end;
%% @todo Documentation.
%% @todo A _B
parse(60, 16#010a, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
DestGID?l32, DestTargetID?l16, _:16,
EventID?l16, QuantityOrColor:8, A:8, Param:32/bits
>> = Data,
Event = npc_shop_eventid_to_atom(EventID),
case Event of
Event when Event =:= npc_shop_enter; Event =:= npc_shop_leave ->
<< ShopID?l16, 0:16 >> = Param,
QuantityOrColor = 0,
A = 0,
{Event, ShopID};
npc_shop_buy ->
<< ItemIndex?l16, 0:16 >> = Param,
QuantityOrColor = A,
{Event, ItemIndex, QuantityOrColor};
npc_shop_sell ->
<< ItemIndex:8, _B:8, 0:16 >> = Param,
A = 0,
{Event, ItemIndex, QuantityOrColor}
end;
%% @todo Documentation.
%% @todo Probably shouldn't be ignored.
%% @todo We should send the spawn to everyone in this command,
%% rather than in area_change.
parse(92, 16#010b, 2, Data) ->
<< DestTargetID?l16, _:16, 0:32, CharGID?l32, 0:64, 0:128,
CharGID?l32, CharTargetID?l16, _:16, 0:16, Dir?l16,
X?l32f, Y?l32f, Z?l32f, 0:64,
QuestID?l32, ZoneID?l16, _:16, MapID?l16, _:16, EntryID?l16, _:16
>> = Data,
DestTargetID = CharTargetID,
ignore; %% @todo character_enter_area
%% @todo Documentation.
parse(60, 16#0110, 2, Data) ->
<< DestTargetID?l16, _:16, 0:32, CharGID?l32, 0:64, 0:128,
CharGID?l32, CharTargetID?l16, _:16, EventID?l32, Param?l32
>> = Data,
Event = character_eventid_to_atom(EventID),
case Event of
character_type_change ->
{character_type_change, Param};
character_status_change ->
{character_status_change, Param};
Event when Event =/= unknown ->
Param = 0,
Event
end;
parse(52, 16#020b, 2, Data) ->
<< 16#ffff:16, _:16, 0:128, 0:128,
Slot:32/little, 0:8, BackToField:8, 0:16
>> = Data,
BackToFieldAtom = case BackToField of 0 -> false; 1 -> true end,
{system_character_select, Slot, BackToFieldAtom};
parse(60, 16#020d, 2, Data) ->
<< 16#ffff:16, _:16, 0:128, 0:128,
AuthGID:32/little, AuthKey:32/bits, 0:64
>> = Data,
{system_key_auth, AuthGID, AuthKey};
parse(44, 16#0217, 2, Data) ->
<< 16#ffff:16, _:16, 0:128, 0:128
>> = Data,
system_game_server_request;
%% @todo _A _B
parse(100, 16#0219, 2, Data) ->
<< 16#ffff:16, _:16, 0:128, 0:128,
Username:192/bits,
Password:192/bits,
_A?l32, _B?l32
>> = Data,
Username2 = iolist_to_binary(
re:split(Username, "\\0", [{return, binary}])),
Password2 = iolist_to_binary(
re:split(Password, "\\0", [{return, binary}])),
{system_login_auth, Username2, Password2};
parse(44, 16#021c, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128
>> = Data,
system_character_load_complete;
parse(48, 16#021d, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
EntryID?l32
>> = Data,
unicube_request;
parse(52, 16#021f, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
UniID?l32, EntryID?l32
>> = Data,
case UniID of
0 -> ignore;
UniID -> {unicube_select, UniID, EntryID}
end;
%% Same as 023f, except for the odd channel, and that only JP clients use it.
parse(48, 16#0226, 3, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
Page:8, Language:8, 0:16
>> = Data,
{system_motd_request, Page, language_to_atom(Language)};
%% Whether the MOTD is accepted.
parse(48, 16#0227, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
AcceptMOTD?l32
>> = Data,
ignore;
%% Same as 0226, except for the odd channel, and that only US clients use it.
parse(48, 16#023f, 2, Data) ->
<< 16#ffff:16, _:16, 0:128, 0:128,
Page:8, Language:8, 0:16
>> = Data,
{system_motd_request, Page, language_to_atom(Language)};
%% @todo Check Size properly.
%% @todo _A
parse(Size, 16#0304, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
CharAccType?l32, CharGID?l32, 0:64,
ChatType:8, ChatCutIn:8, ChatCutInAngle:8, ChatLength:8,
ChatChannel:8, ChatCharType:8, 0:8, _A:8,
CharName:512/bits, ChatMsg/binary
>> = Data,
ChatTypeA = chat_type_to_atom(ChatType),
ChatCutInA = chat_cutin_to_atom(ChatCutIn),
ChatChannelA = chat_channel_to_atom(ChatChannel),
ChatCharTypeA = chat_character_type_to_atom(ChatCharType),
Modifiers = {chat_modifiers, ChatTypeA,
ChatCutInA, ChatCutInAngle, ChatChannelA, ChatCharTypeA},
{chat, CharAccType, CharGID, CharName, Modifiers, ChatLength, ChatMsg};
%% @todo AreaNb should be the same that was sent with 0205 apparently.
parse(48, 16#0806, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
AreaNb?l32
>> = Data,
ignore;
%% @todo Check if NbAreaChanges is related to AreaNb or something.
parse(60, 16#0807, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
QuestID?l32, ZoneID?l16, MapID?l16, EntryID?l16,
NbAreaChanges?l16, PartyPos?l32
>> = Data,
{area_change, QuestID, ZoneID, MapID, EntryID, PartyPos};
%% @todo AreaNb should be the same that was sent with 0208 apparently.
parse(48, 16#0808, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128, AreaNb?l32
>> = Data,
ignore;
parse(648, 16#080c, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
16#ffffffff:32, APCid?l16, _A?l16,
16#ffffffff:32, 16#ffffffff:32, 0:16, _B?l16,
0:4736
>> = Data,
{apc_force_invite, APCid};
%% @todo Probably indicates a successful area change.
parse(44, 16#080d, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128
>> = Data,
ignore;
parse(68, 16#080e, 2, Data) ->
<< 16#ffff:16, _:16, 0:128, 0:128,
0:8, Language:8, 1:8, Entrance:8, Platform:8, 0:24,
Revision:8, Minor:4, _A:12, Major:4, _B:4,
0:96
>> = Data,
LanguageA = language_to_atom(Language),
PlatformA = platform_to_atom(Platform),
Version = Major * 1000000 + Minor * 1000 + Revision,
{client_version, LanguageA, Entrance, PlatformA, Version};
%% @todo No idea what this packet is about.
parse(48, 16#080f, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
PartyPos?l32
>> = Data,
ignore;
parse(60, 16#0811, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
CounterType:8, 41:8, FromZoneID?l16, FromMapID?l16, FromEntryID?l16,
CounterID?l32, 16#ffffffff:32
>> = Data,
{counter_enter, CounterID, FromZoneID, FromMapID, FromEntryID};
parse(44, 16#0812, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128
>> = Data,
counter_leave;
parse(52, 16#0813, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
16#ffffffff:32, APCid?l32
>> = Data,
{npc_invite, APCid};
%% @todo Probably indicates a successful mission block change.
parse(44, 16#0814, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128
>> = Data,
ignore;
%% @todo Probably indicates a successful area change.
parse(44, 16#0815, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128
>> = Data,
ignore;
parse(160, 16#0818, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
GPU:512/bits,
CPU:384/bits,
_A?l32
>> = Data,
GPU2 = iolist_to_binary(re:split(GPU, "\\0", [{return, binary}])),
CPU2 = iolist_to_binary(re:split(CPU, "\\0", [{return, binary}])),
{client_hardware, GPU2, CPU2};
parse(48, 16#0a10, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
ItemID?l32
>> = Data,
{item_description_request, ItemID};
parse(48, 16#0c01, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
QuestID?l32
>> = Data,
{mission_start, QuestID};
parse(48, 16#0c05, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
CounterID?l32
>> = Data,
{counter_quest_files_request, CounterID};
%% On official, Price = Rate * 200.
parse(52, 16#0c07, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
QuestID?l32, Rate?l32
>> = Data,
lobby_transport_request;
%% Probably indicates a successful mission block change.
parse(44, 16#0c0d, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128
>> = Data,
ignore;
parse(44, 16#0c0e, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128
>> = Data,
mission_abort;
parse(48, 16#0c0f, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
CounterID?l32
>> = Data,
{counter_quest_options_request, CounterID};
%% We completely skip the class given and set one depending on the
%% race we received.
%%
%% We completely skip the class levels part as we can't trust what the
%% client tells us, and they should be set to 1 everywhere anyway.
parse(324, 16#0d02, 2, Data) ->
<< 0:32, 0:128, 0:128,
Slot?l32,
NameBin:512/bits,
Race:8, Gender:8, _:8,
Infos/bits
>> = Data,
Name = iolist_to_binary(
re:split(NameBin, "\\0\\0", [{return, binary}])),
RaceAtom = race_to_atom(Race),
GenderAtom = gender_to_atom(Gender),
ClassAtom = case RaceAtom of
human -> acro;
newman -> force;
cast -> ranger;
beast -> hunter
end,
Appearance = case RaceAtom of
cast ->
<< VoiceType:8, VoicePitch:8, _:24,
Torso:32, Legs:32, Arms:32,
Ears:32, Face:32, HeadType:32,
MainColor:8, _:32,
Eyebrows:8, Eyelashes:8, EyesGroup:8, Eyes:8,
_:24, EyesColorY?l32, EyesColorX?l32,
_:96, BodyColor?l32, SubColor?l32,
HairstyleColorY?l32, HairstyleColorX?l32,
Proportion?l32, ProportionBoxX?l32, ProportionBoxY?l32,
FaceBoxX?l32, FaceBoxY?l32,
_/binary
>> = Infos,
#{
arms=Arms,
body_color=BodyColor,
ears=Ears,
eyebrows=Eyebrows,
eyelashes=Eyelashes,
eyes_group=EyesGroup,
eyes=Eyes,
eyes_color_x=EyesColorX,
eyes_color_y=EyesColorY,
face=Face,
face_box_x=FaceBoxX,
face_box_y=FaceBoxY,
hairstyle_color_x=HairstyleColorX,
hairstyle_color_y=HairstyleColorY,
head_type=HeadType,
legs=Legs,
main_color=MainColor,
proportion=Proportion,
proportion_box_x=ProportionBoxX,
proportion_box_y=ProportionBoxY,
shield_color=0,
sub_color=SubColor,
torso=Torso,
voice_pitch=VoicePitch,
voice_type=VoiceType
};
RaceAtom ->
<< VoiceType:8, VoicePitch:8, _:24,
Jacket:32, Pants:32, Shoes:32,
Ears:32, Face:32, Hairstyle:32,
JacketColor:8, PantsColor:8, ShoesColor:8,
_:16, Eyebrows:8, Eyelashes:8, EyesGroup:8, Eyes:8,
BodySuit:8, _:16, EyesColorY?l32, EyesColorX?l32,
_:96, SkinColor?l32, _:32,
HairstyleColorY?l32, HairstyleColorX?l32,
Proportion?l32, ProportionBoxX?l32, ProportionBoxY?l32,
FaceBoxX?l32, FaceBoxY?l32,
_/binary
>> = Infos,
#{
blast_badge=0,
body_suit=BodySuit,
ears=Ears,
eyebrows=Eyebrows,
eyelashes=Eyelashes,
eyes_group=EyesGroup,
eyes=Eyes,
eyes_color_x=EyesColorX,
eyes_color_y=EyesColorY,
face=Face,
face_box_x=FaceBoxX,
face_box_y=FaceBoxY,
hairstyle=Hairstyle,
hairstyle_color_x=HairstyleColorX,
hairstyle_color_y=HairstyleColorY,
jacket=Jacket,
jacket_color=JacketColor,
lips_color_x=0,
lips_color_y=32767,
lips_intensity=32767,
pants=Pants,
pants_color=PantsColor,
proportion=Proportion,
proportion_box_x=ProportionBoxX,
proportion_box_y=ProportionBoxY,
shield_color=0,
shoes=Shoes,
shoes_color=ShoesColor,
skin_color=SkinColor,
voice_pitch=VoicePitch,
voice_type=VoiceType
}
end,
validate_new_character(RaceAtom, GenderAtom, Appearance),
{account_create_character, Slot, Name,
RaceAtom, GenderAtom, ClassAtom, Appearance};
parse(44, 16#0d06, 2, Data) ->
<< 0:32, 0:128, 0:128
>> = Data,
account_characters_request;
parse(68, 16#0d07, 2, Data) ->
<< 0:32, 0:128, 0:128,
TextDisplaySpeed:8, Sound:8, MusicVolume:8, SoundEffectVolume:8,
Vibration:8, RadarMapDisplay:8, CutInDisplay:8, MainMenuCursorPos:8,
0:8, Camera3rdY:8, Camera3rdX:8, Camera1stY:8,
Camera1stX:8, Controller:8, WeaponSwap:8, LockOn:8,
Brightness:8, FunctionKeySetting:8, 0:8, ButtonDetailDisplay:8,
0:32
>> = Data,
%% Make sure the options are valid.
true = TextDisplaySpeed =< 1,
true = Sound =< 1,
true = MusicVolume =< 9,
true = SoundEffectVolume =< 9,
true = Vibration =< 1,
true = RadarMapDisplay =< 1,
true = CutInDisplay =< 1,
true = MainMenuCursorPos =< 1,
true = Camera3rdY =< 1,
true = Camera3rdX =< 1,
true = Camera1stY =< 1,
true = Camera1stX =< 1,
true = Controller =< 1,
true = WeaponSwap =< 1,
true = LockOn =< 1,
true = Brightness =< 4,
true = FunctionKeySetting =< 1,
true = ButtonDetailDisplay =< 2,
%% Options are considered safe past this point.
Options = {options,
TextDisplaySpeed, Sound, MusicVolume, SoundEffectVolume,
Vibration, RadarMapDisplay, CutInDisplay, MainMenuCursorPos,
Camera3rdY, Camera3rdX, Camera1stY, Camera1stX,
Controller, WeaponSwap, LockOn, Brightness,
FunctionKeySetting, ButtonDetailDisplay
},
{account_set_options, Options};
%% @todo Maybe the first 32+128+128 bits contain useful info?
%% @todo HitNb is an auto incremented hit number.
parse(Size, 16#0e00, 2, Data) ->
<< _:288,
NbHits?l32, PartyPos?l32, HitNb?l32,
HitsBin/binary
>> = Data,
Size = 56 + NbHits * 80,
Hits = [{hit, FromTargetID, ToTargetID}
|| << X1?l32f, Y1?l32f, Z1?l32f, FromTargetID?l32, ToTargetID?l32,
_:64,
_:128, %% probably anim+dir followed by x,y,z
_:128, %% probably the same
_:128, _:32,
Rest/binary >>
<= HitsBin],
{hits, Hits};
%% ObjectBaseTargetID is ObjectTargetID - 1024 or 16#ffff.
%% All the ffffffff after PartyPos are other PartyPos values too.
%% @todo Those aren't PartyPos but TargetID of the party.
parse(112, 16#0f0a, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
BlockID?l16, GroupNb?l16, ObjectNb?l16, MapID?l16,
ObjectID?l16, A?l16, ObjectTargetID?l32,
ObjectType?l16, 0:16, ObjectBaseTargetID?l16, B?l16,
PartyPos?l32, C?l32, D?l32, 16#ffffffff:32,
16#ffffffff:32, 16#ffffffff:32, 0:32, 0:32,
0:32, ObjectType?l16, EventID:8, NbTargets:8,
E?l32
>> = Data,
case {ObjectType, EventID} of
{ 5, 13} ->
C = 16#ffffffff,
D = 16#ffffffff,
NbTargets = 1,
E = 0,
{object_switch_on, ObjectID};
{ 5, 14} ->
C = 16#ffffffff,
D = 16#ffffffff,
NbTargets = 1,
E = 0,
{object_switch_off, ObjectID};
{ 9, 20} ->
ignore; %% @todo object_sensor_trigger
{14, 0} ->
ObjectID = 16#ffff,
ObjectTargetID = 16#ffffffff,
ObjectBaseTargetID = 16#ffff,
C = 16#ffffffff,
D = 16#ffffffff,
NbTargets = 1,
{object_warp_enter, BlockID, GroupNb, ObjectNb};
{22, 12} ->
A = 134,
ObjectTargetID = 16#ffffffff,
ObjectBaseTargetID = 16#ffff,
B = 116,
C = 16#ffffffff,
D = 16#ffffffff,
NbTargets = 1,
E = 0,
{object_key_console_enable, ObjectID};
{22, 23} ->
A = 134,
ObjectTargetID = 16#ffffffff,
ObjectBaseTargetID = 16#ffff,
C = 16#ffffffff,
D = 16#ffffffff,
NbTargets = 1,
E = 0,
{object_key_console_init, ObjectID};
{22, 24} ->
A = 134,
ObjectTargetID = 16#ffffffff,
ObjectBaseTargetID = 16#ffff,
B = 116,
C = 16#ffffffff,
D = 16#ffffffff,
NbTargets = 1,
E = 0,
{object_key_console_open_gate, ObjectID};
{31, 12} ->
A = 134,
ObjectTargetID = 16#ffffffff,
ObjectBaseTargetID = 16#ffff,
C = 16#ffffffff,
D = 16#ffffffff,
NbTargets = 1,
E = 0,
{object_key_enable, ObjectID};
{48, 4} ->
A = 134,
B = 116,
C = 16#ffffffff,
D = 16#ffffffff,
NbTargets = 1,
E = 0,
{object_boss_gate_enter, ObjectID};
{48, 5} ->
A = 134,
B = 116,
C = 16#ffffffff,
D = 16#ffffffff,
NbTargets = 1,
E = 0,
{object_boss_gate_leave, ObjectID};
{48, 6} ->
A = 134,
B = 116,
C = 16#ffffffff,
D = 16#ffffffff,
NbTargets = 1,
E = 0,
{object_boss_gate_activate, ObjectID};
{48, 7} ->
A = 134,
B = 116,
C = 16#ffffffff,
D = 16#ffffffff,
NbTargets = 1,
E = 0,
unknown; %% @todo object_boss_gate_??
{49, 3} ->
A = 134,
ObjectTargetID = 16#ffffffff,
ObjectBaseTargetID = 16#ffff,
B = 116,
C = 16#ffffffff,
D = 16#ffffffff,
NbTargets = 1,
E = 0,
{object_crystal_activate, ObjectID};
{50, 9} ->
%% @todo Handle more than one PartyPos.
A = 134,
ObjectTargetID = 16#ffffffff,
ObjectBaseTargetID = 16#ffff,
B = 116,
C = 16#ffffffff,
D = 16#ffffffff,
NbTargets = 1,
E = 0,
{object_healing_pad_tick, [PartyPos]};
{51, 1} ->
B = 116,
C = ObjectTargetID,
D = 16#ffffffff,
NbTargets = 1,
E = 0,
{object_goggle_target_activate, ObjectID};
{56, 25} ->
C = 16#ffffffff,
D = 16#ffffffff,
NbTargets = 1,
E = 0,
%% @todo Do we only have the ObjectTargetID here?
{object_chair_sit, ObjectTargetID};
{56, 26} ->
C = 16#ffffffff,
D = 16#ffffffff,
NbTargets = 1,
E = 0,
%% @todo Do we only have the ObjectTargetID here?
{object_chair_stand, ObjectTargetID};
{57, 12} ->
A = 134,
ObjectTargetID = 16#ffffffff,
ObjectBaseTargetID = 16#ffff,
B = 116,
C = 16#ffffffff,
D = 16#ffffffff,
NbTargets = 1,
{object_vehicle_boost_enable, ObjectID};
{57, 28} ->
A = 134,
ObjectTargetID = 16#ffffffff,
ObjectBaseTargetID = 16#ffff,
B = 116,
C = 16#ffffffff,
D = 16#ffffffff,
NbTargets = 1,
E = 0,
{object_vehicle_boost_respawn, ObjectID};
{71, 27} ->
A = 134,
C = ObjectTargetID,
D = 16#ffffffff,
NbTargets = 1,
E = 0,
unknown %% @todo object_trap3_??
end;
parse(112, 16#1007, 2, Data) ->
<< 0:32, 0:128, 0:128,
PartyPos?l32, CharName:512/bits
>> = Data,
{party_remove_member, PartyPos};
parse(52, 16#1701, 2, Data) ->
<< 0:32, 0:128, 0:128,
0:32, 16#ffffffff
>> = Data,
counter_party_list_request;
parse(44, 16#1705, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128
>> = Data,
counter_party_info_request;
%% @todo Probably needs to be broadcasted to other players in the party.
parse(48, 16#1707, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
QuestID?l32
>> = Data,
ignore; %% @todo {counter_quest_select, QuestID}
parse(44, 16#1709, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128
>> = Data,
counter_party_options_request;
parse(44, 16#170b, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128
>> = Data,
counter_background_locations_request;
parse(48, 16#1710, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
CounterID?l32
>> = Data,
{counter_options_request, CounterID};
parse(64, 16#1a01, 2, Data) ->
<< DestTargetID?l16, _:16, 0:128, 0:128,
DestTargetID?l16, _:16, ShopID?l32, EventID?l32,
A?l32, B?l32
>> = Data,
Event = dialog_eventid_to_atom(EventID),
case Event of
npc_shop_request ->
A = 0,
{Event, ShopID};
lumilass_options_request ->
ShopID = 0,
A = 0,
Event;
ppcube_request ->
ShopID = 0,
A = 0,
Event;
ppcube_charge_all ->
ShopID = 0,
unknown; %% @todo
ppcube_charge_one ->
ShopID = 0,
unknown; %% @todo
put_on_outfit ->
ShopID = 0,
A = 0,
unknown; %% @todo
remove_outfit ->
ShopID = 0,
unknown; %% @todo
player_type_availability_request ->
ShopID = 0,
A = 0,
B = 0,
Event
end;
%% @todo Proper cast parsing.
parse(_, Command, 1, Data) ->
{Command, << 0:32, Command:16, 1:8, 0:8, Data/binary >>}.
%% Data validation.
validate_new_character(cast, male, Appearance) ->
validate_new_metal_character(Appearance),
VoiceType = Appearance.voice_type,
true = (VoiceType >= 27 andalso VoiceType =< 38)
orelse (VoiceType >= 89 andalso VoiceType =< 96),
true = Appearance.eyelashes =< 2,
4 = Appearance.eyes_group,
true = lists:member(Appearance.torso,
[16#00F70100, 16#00F90100, 16#00FC0100]),
true = lists:member(Appearance.legs,
[16#00F70101, 16#00F90101, 16#00FC0101]),
true = lists:member(Appearance.arms,
[16#00F70102, 16#00F90102, 16#00FC0102]),
case lists:member(Appearance.face, [16#00040004, 16#0A040004,
16#14040004, 16#1E040004, 16#28040004, 16#000E0004]) of
true ->
true = lists:member(Appearance.ears, [16#001E0003,
16#001F0003, 16#00200003, 16#00210003, 16#00220003]),
true = Appearance.eyes_color_x =< 327679,
validate_new_male_hairstyle(Appearance.head_type);
false ->
true = lists:member(Appearance.face, [16#00F40104,
16#00F50104, 16#00F60104, 16#00F70104, 16#00F80104,
16#00F90104, 16#00FA0104, 16#00FD0104, 16#00020204,
16#00030204, 16#00040204, 16#00060204, 16#00070204]),
16#FFFFFFFF = Appearance.ears,
true = Appearance.eyes_color_x =< 458751,
true = lists:member(Appearance.head_type, [16#00F40105,
16#00F50105, 16#00F60105, 16#00F70105, 16#00F80105,
16#00F90105, 16#00FA0105, 16#00FB0105, 16#00FD0105,
16#00020205, 16#00030205, 16#00040205, 16#00060205,
16#00070205])
end;
validate_new_character(cast, female, Appearance) ->
validate_new_metal_character(Appearance),
VoiceType = Appearance.voice_type,
true = (VoiceType >= 39 andalso VoiceType =< 50)
orelse (VoiceType >= 97 andalso VoiceType =< 101),
true = Appearance.eyelashes =< 12,
5 = Appearance.eyes_group,
true = lists:member(Appearance.torso,
[16#00F51100, 16#00F91100, 16#00FA1100]),
true = lists:member(Appearance.legs,
[16#00F51101, 16#00F91101, 16#00FA1101]),
true = lists:member(Appearance.arms,
[16#00F51102, 16#00F91102, 16#00F61102]),
case lists:member(Appearance.face, [16#00041004, 16#0A041004,
16#14041004, 16#1E041004, 16#3C041004]) of
true ->
true = lists:member(Appearance.ears, [16#001E1003,
16#001F1003, 16#00201003, 16#00211003, 16#00221003]),
true = Appearance.eyes_color_x =< 327679,
validate_new_female_hairstyle(Appearance.head_type);
false ->
true = lists:member(Appearance.face, [16#00F41104,
16#00F51104, 16#00F61104, 16#00F71104, 16#00F81104,
16#00F91104, 16#00FA1104, 16#00FD1104, 16#00031204,
16#00041204, 16#00051204, 16#00061204, 16#00081204]),
16#FFFFFFFF = Appearance.ears,
true = Appearance.eyes_color_x =< 458751,
true = lists:member(Appearance.head_type, [16#00F41105,
16#00F51105, 16#00F61105, 16#00F71105, 16#00F81105,
16#00F91105, 16#00FA1105, 16#00FB1105, 16#00FD1105,
16#00031205, 16#00041205, 16#00051205, 16#00061205,
16#00081205])
end;
validate_new_character(human, male, Appearance) ->
validate_new_fleshy_character(Appearance),
validate_new_fleshy_male_character(Appearance),
true = lists:member(Appearance.ears, [16#00000003, 16#00010003]),
true = lists:member(Appearance.face, [16#00010004, 16#01010004,
16#14010004, 16#15010004, 16#16010004, 16#17010004, 16#18010004,
16#19010004, 16#1A010004, 16#1E010004, 16#1F010004, 16#20010004,
16#21010004, 16#22010004, 16#23010004, 16#24010004, 16#28010004,
16#29010004, 16#2A010004, 16#2B010004, 16#2C010004, 16#2D010004,
16#2E010004, 16#000B0004]),
0 = Appearance.eyes_group,
true = Appearance.eyes =< 5;
validate_new_character(newman, male, Appearance) ->
validate_new_fleshy_character(Appearance),
validate_new_fleshy_male_character(Appearance),
true = lists:member(Appearance.ears,
[16#00030003, 16#00650003, 16#00660003]),
true = lists:member(Appearance.face, [16#00020004, 16#01020004,
16#14020004, 16#15020004, 16#16020004, 16#17020004, 16#18020004,
16#19020004, 16#1A020004, 16#1E020004, 16#1F020004, 16#20020004,
16#21020004, 16#22020004, 16#23020004, 16#24020004, 16#28020004,
16#29020004, 16#2A020004, 16#2B020004, 16#2C020004, 16#2D020004,
16#2E020004, 16#000C0004]),
2 = Appearance.eyes_group,
true = Appearance.eyes =< 5;
validate_new_character(beast, male, Appearance) ->
validate_new_fleshy_character(Appearance),
validate_new_fleshy_male_character(Appearance),
true = lists:member(Appearance.ears,
[16#00020003, 16#00CD0003, 16#00CE0003]),
true = lists:member(Appearance.face, [16#00030004, 16#0A030004,
16#14030004, 16#15030004, 16#16030004, 16#17030004, 16#18030004,
16#19030004, 16#1A030004, 16#1E030004, 16#1F030004, 16#20030004,
16#21030004, 16#22030004, 16#23030004, 16#24030004, 16#28030004,
16#29030004, 16#2A030004, 16#2B030004, 16#2C030004, 16#2D030004,
16#2E030004, 16#000D0004]),
6 = Appearance.eyes_group,
true = Appearance.eyes =< 6;
validate_new_character(human, female, Appearance) ->
validate_new_fleshy_character(Appearance),
validate_new_fleshy_female_character(Appearance),
true = lists:member(Appearance.ears, [16#00001003, 16#00011003]),
true = lists:member(Appearance.face, [16#00011004, 16#0A011004,
16#14011004, 16#1E011004, 16#3C011004]),
1 = Appearance.eyes_group,
true = Appearance.eyes =< 5;
validate_new_character(newman, female, Appearance) ->
validate_new_fleshy_character(Appearance),
validate_new_fleshy_female_character(Appearance),
true = lists:member(Appearance.ears,
[16#00031003, 16#00651003, 16#00661003]),
true = lists:member(Appearance.face, [16#00021004, 16#0A021004,
16#14021004, 16#1E021004, 16#3C021004]),
3 = Appearance.eyes_group,
true = Appearance.eyes =< 5;
validate_new_character(beast, female, Appearance) ->
validate_new_fleshy_character(Appearance),
validate_new_fleshy_female_character(Appearance),
true = lists:member(Appearance.ears,
[16#00021003, 16#00CD1003, 16#00CE1003, 16#00CF1003]),
true = lists:member(Appearance.face, [16#00031004, 16#0A031004,
16#14031004, 16#1E031004, 16#3C031004]),
7 = Appearance.eyes_group,
true = Appearance.eyes =< 6.
validate_new_metal_character(Appearance) ->
true = Appearance.body_color =< 131071,
true = Appearance.eyes =< 2,
true = Appearance.eyes_color_y =< 65535,
true = Appearance.main_color =< 7,
true = Appearance.sub_color =< 393215,
validate_new_common_character(Appearance).
validate_new_fleshy_character(Appearance) ->
true = Appearance.body_suit =< 4,
true = Appearance.eyes_color_x =< 327679,
true = Appearance.jacket_color =< 4,
true = Appearance.pants_color =< 4,
true = Appearance.shoes_color =< 4,
true = Appearance.skin_color =< 131071,
validate_new_common_character(Appearance).
validate_new_common_character(Appearance) ->
true = Appearance.eyebrows =< 18,
true = Appearance.eyes_color_y =< 65535,
true = Appearance.face_box_x =< 131071,
true = Appearance.face_box_y =< 131071,
true = Appearance.hairstyle_color_x =< 327679,
true = Appearance.hairstyle_color_y =< 65535,
true = Appearance.proportion =< 131071,
true = Appearance.proportion_box_x =< 131071,
true = Appearance.proportion_box_y =< 131071.
validate_new_fleshy_male_character(Appearance) ->
VoiceType = Appearance.voice_type,
true = (VoiceType >= 1 andalso VoiceType =< 14)
orelse (VoiceType >= 76 andalso VoiceType =< 83),
true = lists:member(Appearance.jacket,
[16#00060000, 16#00020000, 16#00030000]),
true = lists:member(Appearance.pants,
[16#00060001, 16#000B0001, 16#00030001]),
true = lists:member(Appearance.shoes,
[16#00060002, 16#00020002, 16#00040002]),
validate_new_male_hairstyle(Appearance.hairstyle),
true = Appearance.eyelashes =< 2.
validate_new_fleshy_female_character(Appearance) ->
VoiceType = Appearance.voice_type,
true = (VoiceType >= 15 andalso VoiceType =< 26)
orelse (VoiceType >= 84 andalso VoiceType =< 88),
true = lists:member(Appearance.jacket,
[16#00011000, 16#00021000, 16#00031000]),
true = lists:member(Appearance.pants,
[16#00011001, 16#00021001, 16#00031001]),
true = lists:member(Appearance.shoes,
[16#00091002, 16#00071002, 16#00031002]),
validate_new_female_hairstyle(Appearance.hairstyle),
true = Appearance.eyelashes =< 12.
validate_new_male_hairstyle(Hairstyle) ->
true = lists:member(Hairstyle, [16#00000005, 16#000A0005, 16#00140005,
16#001E0005, 16#00280005, 16#00320005, 16#003C0005, 16#00460005,
16#00500005, 16#005A0005, 16#00640005, 16#006E0005, 16#00780005,
16#00820005, 16#008C0005, 16#00960005, 16#00A00005, 16#00AA0005]).
validate_new_female_hairstyle(Hairstyle) ->
true = lists:member(Hairstyle, [16#00001005, 16#000A1005, 16#00141005,
16#001E1005, 16#00281005, 16#00321005, 16#003C1005, 16#00461005,
16#00501005, 16#005A1005, 16#00641005, 16#006E1005, 16#00781005,
16#00821005, 16#008C1005, 16#00961005, 16#00A01005]).
%% Response API.
%% @todo We need to be able to optionally output commands sent. How?
%% @doc Send the general data and flags for the selected character.
%% @todo Handle bitflags and value flags properly.
%% @todo Check that DestTargetID is ffff here as it should be.
account_character(Char, State=#egs_net{gid=DestGID, targetid=DestTargetID}) ->
CharBin = character_to_binary(Char),
OptionsBin = character_options_to_binary(Char.options),
send(16#0d01, <<
DestTargetID?l16, 0:144,
16#00011300:32, DestGID?l32, 0:64,
CharBin/binary,
0:8128, %% bit flags followed directly by value flags
OptionsBin/binary
>>, State).
character_options_to_binary(Opts) ->
Brightness = Opts.brightness,
ButtonHelp = Opts.buttonhelp,
Cam1stX = Opts.cam1stx,
Cam1stY = Opts.cam1sty,
Cam3rdX = Opts.cam3rdx,
Cam3rdY = Opts.cam3rdy,
Controller = Opts.controller,
CursorPos = Opts.cursorpos,
CutIn = Opts.cutin,
FnKeys = Opts.fnkeys,
LockOn = Opts.lockon,
MusicVolume = Opts.musicvolume,
RadarMap = Opts.radarmap,
SfxVolume = Opts.sfxvolume,
Sound = Opts.sound,
TextSpeed = Opts.textspeed,
Vibration = Opts.vibration,
WeaponSwap = Opts.weaponswap,
<< TextSpeed, Sound, MusicVolume, SfxVolume,
Vibration, RadarMap, CutIn, CursorPos,
0, Cam3rdY, Cam3rdX, Cam1stY,
Cam1stX, Controller, WeaponSwap, LockOn,
Brightness, FnKeys, 0, ButtonHelp >>.
%% @doc Send the character list for the selection screen.
%% @todo Some values aren't handled yet, like the previous location.
account_characters_response(Characters, State=#egs_net{gid=DestGID}) ->
[Char1, Char2, Char3, Char4] = Characters,
Char1Bin = account_character_to_binary(Char1),
Char2Bin = account_character_to_binary(Char2),
Char3Bin = account_character_to_binary(Char3),
Char4Bin = account_character_to_binary(Char4),
send(16#0d03, <<
0:32,
16#00011300:32, DestGID?l32, 0:64,
16#00011300:32, DestGID?l32, 0:64,
0:32,
Char1Bin/binary, Char2Bin/binary,
Char3Bin/binary, Char4Bin/binary
>>, State).
%% @todo We shouldn't care about the version *here*, but rather in egs_store.
account_character_to_binary(notfound) ->
<< 0:2784 >>;
account_character_to_binary({1, Char}) ->
CharBin = character_to_binary(Char),
<< 0:8, 1:8, 0:48, CharBin/binary, 0:512 >>.
character_to_binary(notfound) ->
<< 0:2208 >>;
character_to_binary(Char) ->
Name = Char.name,
NameBin = << Name/binary, 0:(512 - bit_size(Name)) >>,
Race = atom_to_race(Char.race),
Gender = atom_to_gender(Char.gender),
Class = atom_to_class(Char.class),
AppearanceBin = character_appearance_to_binary(Char.race, Char.appearance),
LevelsBin = character_levels_to_binary(Char),
<< NameBin/binary, Race:8, Gender:8, Class:8,
AppearanceBin/binary, LevelsBin/binary >>.
character_levels_to_binary(Char) ->
Level = Char.level,
BlastBar = Char.blast_bar,
Luck = Char.luck,
EXP = Char.exp,
Money = Char.money,
Playtime = Char.playtime,
ClassesBin = character_classes_to_binary(Char.type,
Char.hunter_level, Char.ranger_level,
Char.force_level, Char.acro_level),
<< Level?l32, BlastBar?l16,
Luck:8, 0:40, EXP?l32, 0:32, Money?l32,
Playtime?l32, ClassesBin/binary >>.
%% @todo Figure out what the extra values for NPC are.
character_classes_to_binary(player, HunterLv, RangerLv, ForceLv, AcroLv) ->
<< 0:160,
1?l32, 1?l32, 1?l32, 1?l32,
1?l32, 1?l32, 1?l32, 1?l32,
1?l32, 1?l32, 1?l32, 1?l32,
HunterLv?l32, RangerLv?l32,
ForceLv?l32, AcroLv?l32 >>;
character_classes_to_binary(npc, HunterLv, RangerLv, ForceLv, AcroLv) ->
<< 1?l32, 1?l32, 1?l32, 1?l32,
1?l32, 1?l32, 1?l32, 1?l32,
1?l32, 1?l32, 1?l32, 1?l32,
HunterLv?l32, RangerLv?l32,
ForceLv?l32, AcroLv?l32,
16#4e4f4630:32, 16#08000000:32, 0:32, 0:32, 16#4e454e44:32 >>.
character_appearance_to_binary(cast, Appearance) ->
Arms = Appearance.arms,
BodyColor = Appearance.body_color,
Ears = Appearance.ears,
Eyebrows = Appearance.eyebrows,
Eyelashes = Appearance.eyelashes,
EyesColorX = Appearance.eyes_color_x,
EyesColorY = Appearance.eyes_color_y,
EyesGroup = Appearance.eyes_group,
Eyes = Appearance.eyes,
Face = Appearance.face,
FaceBoxX = Appearance.face_box_x,
FaceBoxY = Appearance.face_box_y,
HairstyleColorX = Appearance.hairstyle_color_x,
HairstyleColorY = Appearance.hairstyle_color_y,
HeadType = Appearance.head_type,
Legs = Appearance.legs,
MainColor = Appearance.main_color,
Proportion = Appearance.proportion,
ProportionBoxX = Appearance.proportion_box_x,
ProportionBoxY = Appearance.proportion_box_y,
ShieldColor = Appearance.shield_color,
SubColor = Appearance.sub_color,
Torso = Appearance.torso,
VoicePitch = Appearance.voice_pitch,
VoiceType = Appearance.voice_type,
<< VoiceType:8, VoicePitch:8, 0:24,
Torso:32, Legs:32, Arms:32, Ears:32, Face:32, HeadType:32,
MainColor:8, 0:16, ShieldColor:8,
0:8, Eyebrows:8, Eyelashes:8, EyesGroup:8, Eyes:8,
0:24, EyesColorY?l32, EyesColorX?l32,
16#ff7f0000:32, 16#ff7f0000:32, 0:32,
BodyColor?l32, SubColor?l32,
HairstyleColorY?l32, HairstyleColorX?l32,
Proportion?l32, ProportionBoxX?l32, ProportionBoxY?l32,
FaceBoxX?l32, FaceBoxY?l32 >>;
character_appearance_to_binary(_, Appearance) ->
Badge = Appearance.blast_badge,
BodySuit = Appearance.body_suit,
Ears = Appearance.ears,
Eyebrows = Appearance.eyebrows,
Eyelashes = Appearance.eyelashes,
EyesColorX = Appearance.eyes_color_x,
EyesColorY = Appearance.eyes_color_y,
EyesGroup = Appearance.eyes_group,
Eyes = Appearance.eyes,
Face = Appearance.face,
FaceBoxX = Appearance.face_box_x,
FaceBoxY = Appearance.face_box_y,
Hairstyle = Appearance.hairstyle,
HairstyleColorX = Appearance.hairstyle_color_x,
HairstyleColorY = Appearance.hairstyle_color_y,
Jacket = Appearance.jacket,
JacketColor = Appearance.jacket_color,
LipsColorX = Appearance.lips_color_x,
LipsColorY = Appearance.lips_color_y,
LipsIntensity = Appearance.lips_intensity,
Pants = Appearance.pants,
PantsColor = Appearance.pants_color,
Proportion = Appearance.proportion,
ProportionBoxX = Appearance.proportion_box_x,
ProportionBoxY = Appearance.proportion_box_y,
ShieldColor = Appearance.shield_color,
Shoes = Appearance.shoes,
ShoesColor = Appearance.shoes_color,
SkinColor = Appearance.skin_color,
VoicePitch = Appearance.voice_pitch,
VoiceType = Appearance.voice_type,
<< VoiceType:8, VoicePitch:8, 0:24,
Jacket:32, Pants:32, Shoes:32, Ears:32, Face:32, Hairstyle:32,
JacketColor:8, PantsColor:8, ShoesColor:8, ShieldColor:8,
Badge:8, Eyebrows:8, Eyelashes:8, EyesGroup:8, Eyes:8,
BodySuit:8, 0:16, EyesColorY?l32, EyesColorX?l32,
LipsIntensity?l32, LipsColorY?l32, LipsColorX?l32, SkinColor?l32,
16#ffff0200:32, HairstyleColorY?l32, HairstyleColorX?l32,
Proportion?l32, ProportionBoxX?l32, ProportionBoxY?l32,
FaceBoxX?l32, FaceBoxY?l32 >>.
%% @doc Send the defined account flags for the server.
account_flags(ValueFlags, BoolFlags, TempFlags, State=#egs_net{gid=DestGID}) ->
NbValue = length(ValueFlags),
NbBool = length(BoolFlags),
NbTemp = length(TempFlags),
F = fun(Flag) ->
FlagBin = list_to_binary(Flag),
Padding = 8 * (16 - byte_size(FlagBin)),
<< FlagBin/binary, 0:Padding >>
end,
ValueFlagsBin = iolist_to_binary(lists:map(F, ValueFlags)),
BoolFlagsBin = iolist_to_binary(lists:map(F, BoolFlags)),
TempFlagsBin = iolist_to_binary(lists:map(F, TempFlags)),
send(16#0d05, <<
0:32,
16#00011300:32, DestGID?l32, 0:64,
16#00011300:32, DestGID?l32, 0:64,
ValueFlagsBin/binary, BoolFlagsBin/binary, TempFlagsBin/binary,
NbValue?l32, NbBool?l32, NbTemp?l32
>>, State).
%% @doc Send the player's own partner card.
comm_own_card(Char, State=#egs_net{gid=DestGID, targetid=DestTargetID}) ->
Slot = Char.slot,
Name = Char.name,
NameBin = << Name/binary, 0:(512 - bit_size(Name)) >>,
Race = atom_to_race(Char.race),
Gender = atom_to_gender(Char.gender),
Class = atom_to_class(Char.class),
VoiceType = Char.appearance.voice_type,
VoicePitch = Char.appearance.voice_pitch,
Comment = Char.card_comment,
CommentBin = << Comment/binary, 0:(2816 - bit_size(Comment)) >>,
send(16#1500, <<
DestTargetID?l32, 0:144,
16#00011300:32, DestGID?l32, 0:64,
NameBin/binary,
Race:8, Gender:8, Class:8, VoiceType:8, VoicePitch:8, 0:24,
DestGID?l32, 0:224,
CommentBin/binary,
1:8, 4:8, 1:8, Slot:8,
0:64 >>, State).
%% @doc Display an error to the client.
system_auth_error(Error, State=#egs_net{gid=DestGID}) ->
Length = byte_size(Error) div 2 + 2,
send(16#0223, <<
0:160,
16#00000f00:32, DestGID?l32, 0:64,
0:64,
3?l32, 0:48, Length?l16,
Error/binary, 0:16
>>, State).
%% @doc Send the game server's IP and port that the client will connect to.
%% @todo Take IP as a list, not a binary.
system_game_server_response(ServerIP, ServerPort,
State=#egs_net{gid=DestGID, targetid=DestTargetID}) ->
send(16#0216, <<
DestTargetID?l16, 0:16,
0:128,
16#00000f00:32, DestGID?l32, 0:64,
ServerIP/binary, ServerPort?l16, 0:16
>>, State).
%% @doc Say hello to a newly connected client.
system_hello(State=#egs_net{gid=DestGID, targetid=DestTargetID}) ->
send(16#0202, << DestTargetID?l16, 0:272, DestGID?l32, 0:1024 >>, State).
%% @doc Send the authentication information for key-based authentication.
system_key_auth_info(AuthGID, AuthKey, State=#egs_net{gid=DestGID}) ->
send(16#0223, <<
0:160,
16#00000f00:32, DestGID?l32, 0:64,
AuthGID?l32, AuthKey/binary
>>, State).
%% @doc Send the given MOTD page to the client for display.
%%
%% The full MOTD is expected as this function will only take the
%% page it needs, automatically.
system_motd_response(MOTD, Page, State=#egs_net{targetid=DestTargetID}) ->
Lines = re:split(MOTD, "\n\\0"),
NbPages = 1 + length(Lines) div 15,
true = Page >= 0,
true = Page < NbPages,
Text = << << Line/binary, "\n", 0 >>
|| Line <- lists:sublist(Lines, 1 + Page * 15, 15) >>,
Length = byte_size(Text) div 2 + 2,
send(16#0225, <<
DestTargetID?l16, 0:272,
NbPages:8, Page:8, Length?l16,
Text/binary, 0:16
>>, State).
%% @doc Make the client open the given URL in a browser, after the game closes.
%% @todo Take URL as a list, not a binary.
system_open_url(URL, State=#egs_net{gid=DestGID, targetid=DestTargetID}) ->
Length = byte_size(URL) + 1,
Padding = 8 * (512 - Length - 1),
send(16#0231, <<
DestTargetID?l16, 0:16,
16#00000f00:32, DestGID?l32, 0:64,
16#00000f00:32, DestGID?l32, 0:64,
Length?l32, URL/binary, 0:Padding
>>, State).
%% Response primitives.
send(Command, Data, State) ->
send(Command, 3, Data, State).
%% @todo We may also optionally output packets sent here.
send(Command, Channel, Data, State) ->
Size = 8 + byte_size(Data),
{Size2, Padding} = case Size rem 4 of
0 -> {Size, <<>>};
2 -> {Size + 2, << 0:16 >>}
end,
send_packet(<< Size2?l32, Command:16, Channel:8, 0:8,
Data/binary, Padding/binary >>, State).
send_packet(Packet, #egs_net{socket=Socket, transport=Transport})
when byte_size(Packet) =< 16#4000 ->
Transport:send(Socket, Packet);
send_packet(Packet, State) ->
send_fragments(Packet, byte_size(Packet), 0, State).
send_fragments(Packet, Size, Current, #egs_net{
socket=Socket, transport=Transport})
when Size - Current =< 16#4000 ->
FragmentSize = 16#10 + byte_size(Packet),
Transport:send(Socket, << FragmentSize?l32, 16#0b030000:32,
Size?l32, Current?l32, Packet/binary >>);
send_fragments(Packet, Size, Current, State=#egs_net{
socket=Socket, transport=Transport}) ->
<< Fragment:16#4000/binary, Rest/binary >> = Packet,
Transport:send(Socket, << 16#10400000:32, 16#0b030000:32,
Size?l32, Current?l32, Fragment/binary >>),
send_fragments(Rest, Size, Current + 16#4000, State).
%% Data conversion.
atom_to_class(hunter) -> 12;
atom_to_class(ranger) -> 13;
atom_to_class(force ) -> 14;
atom_to_class(acro ) -> 15.
atom_to_gender(male ) -> 0;
atom_to_gender(female) -> 1.
atom_to_race(human ) -> 0;
atom_to_race(newman) -> 1;
atom_to_race(cast ) -> 2;
atom_to_race(beast ) -> 3.
character_eventid_to_atom( 1) -> unknown; %% @todo
character_eventid_to_atom( 2) -> character_type_capabilities_request;
character_eventid_to_atom( 3) -> character_type_change;
character_eventid_to_atom( 4) -> unknown; %% @todo
character_eventid_to_atom( 6) -> unknown; %% @todo
character_eventid_to_atom( 7) -> character_death;
character_eventid_to_atom( 8) -> character_death_return_to_lobby;
character_eventid_to_atom( 9) -> unknown; %% @todo
character_eventid_to_atom(10) -> character_status_change.
chat_type_to_atom(0) -> speak;
chat_type_to_atom(1) -> shout;
chat_type_to_atom(2) -> whisper.
chat_cutin_to_atom( 0) -> none;
chat_cutin_to_atom( 1) -> laugh;
chat_cutin_to_atom( 2) -> smile;
chat_cutin_to_atom( 3) -> wry_smile;
chat_cutin_to_atom( 4) -> surprised;
chat_cutin_to_atom( 5) -> confused;
chat_cutin_to_atom( 6) -> disappointed;
chat_cutin_to_atom( 7) -> deep_in_thought;
chat_cutin_to_atom( 8) -> sneer;
chat_cutin_to_atom( 9) -> dissatisfied;
chat_cutin_to_atom(10) -> angry.
chat_channel_to_atom(0) -> public;
chat_channel_to_atom(1) -> private.
chat_character_type_to_atom(0) -> player;
chat_character_type_to_atom(2) -> apc. %% @todo Check that this is right.
class_to_atom(12) -> hunter;
class_to_atom(13) -> ranger;
class_to_atom(14) -> force;
class_to_atom(15) -> acro.
dialog_eventid_to_atom(0) -> npc_shop_request;
dialog_eventid_to_atom(2) -> lumilass_options_request;
dialog_eventid_to_atom(3) -> ppcube_request;
dialog_eventid_to_atom(4) -> ppcube_charge_all;
dialog_eventid_to_atom(5) -> ppcube_charge_one;
dialog_eventid_to_atom(6) -> put_on_outfit;
dialog_eventid_to_atom(7) -> remove_outfit;
dialog_eventid_to_atom(9) -> player_type_availability_request.
gender_to_atom(0) -> male;
gender_to_atom(1) -> female.
item_eventid_to_atom( 1) -> item_equip;
item_eventid_to_atom( 2) -> item_unequip;
item_eventid_to_atom( 3) -> item_link_pa;
item_eventid_to_atom( 4) -> item_unlink_pa;
item_eventid_to_atom( 5) -> item_drop;
item_eventid_to_atom( 7) -> item_learn_pa;
item_eventid_to_atom( 8) -> item_use;
item_eventid_to_atom( 9) -> item_set_trap;
item_eventid_to_atom(18) -> item_unlearn_pa.
language_to_atom(0) -> japanese;
language_to_atom(1) -> american_english;
language_to_atom(2) -> british_english;
language_to_atom(3) -> french;
language_to_atom(4) -> german;
language_to_atom(5) -> spanish;
language_to_atom(6) -> italian;
language_to_atom(7) -> korean;
language_to_atom(8) -> simplified_chinese;
language_to_atom(9) -> traditional_chinese.
npc_shop_eventid_to_atom(1) -> npc_shop_enter;
npc_shop_eventid_to_atom(2) -> npc_shop_buy;
npc_shop_eventid_to_atom(3) -> npc_shop_sell;
npc_shop_eventid_to_atom(4) -> unknown; %% @todo npc_shop_gift_wrap
npc_shop_eventid_to_atom(5) -> npc_shop_leave;
npc_shop_eventid_to_atom(6) -> unknown. %% @todo
platform_to_atom(0) -> ps2;
platform_to_atom(1) -> pc.
race_to_atom(0) -> human;
race_to_atom(1) -> newman;
race_to_atom(2) -> cast;
race_to_atom(3) -> beast.
%% Debug.
binary_to_dump(Data) ->
binary_to_dump(Data, 4, []).
binary_to_dump(<<>>, _, Acc) ->
lists:reverse(Acc);
binary_to_dump(Data, 0, Acc) ->
binary_to_dump(Data, 4, ["~n"|Acc]);
binary_to_dump(<< A, B, C, D, Rest/binary >>, N, Acc) ->
Str = io_lib:format("~2.16.0b ~2.16.0b ~2.16.0b ~2.16.0b ", [A, B, C, D]),
binary_to_dump(Rest, N - 1, [Str|Acc]).