Rename the client state record #state into #client for clarity.

This commit is contained in:
Loïc Hoguin 2011-02-26 17:00:41 +01:00
parent d69fe073a8
commit 86bb5c81b3
9 changed files with 311 additions and 311 deletions

View File

@ -33,11 +33,11 @@
%% Records.
%% @doc Per-process state used by the various EGS modules.
-record(state, {
%% @doc Client state. One per connected client.
-record(client, {
socket :: sslsocket(),
gid :: integer(),
slot :: 0..3,
slot :: 0..3, %% @todo Probably should remove this one from the state.
lid = 16#ffff :: 0..16#ffff,
areanb = 0 :: non_neg_integer()
}).

View File

@ -23,32 +23,32 @@
-include("include/records.hrl").
%% @doc Send a keepalive.
keepalive(#state{socket=Socket}) ->
keepalive(#client{socket=Socket}) ->
psu_proto:send_keepalive(Socket).
%% @doc We don't expect any message here.
info(_Msg, _State) ->
info(_Msg, _Client) ->
ok.
%% @doc Nothing to broadcast.
cast(_Command, _Data, _State) ->
cast(_Command, _Data, _Client) ->
ok.
%% @doc Dismiss all raw commands with a log notice.
%% @todo Have a log event handler instead.
raw(Command, _Data, State) ->
io:format("~p (~p): dismissed command ~4.16.0b~n", [?MODULE, State#state.gid, Command]).
raw(Command, _Data, Client) ->
io:format("~p (~p): dismissed command ~4.16.0b~n", [?MODULE, Client#client.gid, Command]).
%% Events.
%% @doc Character screen selection request and delivery.
event(char_select_request, #state{gid=GID}) ->
event(char_select_request, #client{gid=GID}) ->
Folder = egs_accounts:get_folder(GID),
psu_game:send_0d03(data_load(Folder, 0), data_load(Folder, 1), data_load(Folder, 2), data_load(Folder, 3));
%% @doc The options default to 0 for everything except brightness to 4.
%% @todo Don't forget to check for the character's name.
event({char_select_create, Slot, CharBin}, #state{gid=GID}) ->
event({char_select_create, Slot, CharBin}, #client{gid=GID}) ->
%% check for valid character appearance
%~ << _Name:512, RaceID:8, GenderID:8, _TypeID:8, AppearanceBin:776/bits, _/bits >> = CharBin,
%~ Race = proplists:get_value(RaceID, [{0, human}, {1, newman}, {2, cast}, {3, beast}]),
@ -65,7 +65,7 @@ event({char_select_create, Slot, CharBin}, #state{gid=GID}) ->
file:write_file(io_lib:format("~s.options", [File]), << 0:128, 4, 0:56 >>);
%% @doc Load the selected character into the game's default universe.
event({char_select_enter, Slot, _BackToPreviousField}, State=#state{gid=GID}) ->
event({char_select_enter, Slot, _BackToPreviousField}, Client=#client{gid=GID}) ->
Folder = egs_accounts:get_folder(GID),
[{status, 1}, {char, CharBin}, {options, OptionsBin}] = data_load(Folder, Slot),
<< Name:512/bits, RaceBin:8, GenderBin:8, ClassBin:8, AppearanceBin:776/bits, _/bits >> = CharBin,
@ -88,9 +88,9 @@ event({char_select_enter, Slot, _BackToPreviousField}, State=#state{gid=GID}) ->
egs_users:item_add(GID, 16#01010a00, #psu_striking_weapon_item_variables{current_pp=99, max_pp=100, element=#psu_element{type=2, percent=50}}),
egs_users:item_add(GID, 16#01010b00, #psu_striking_weapon_item_variables{current_pp=99, max_pp=100, element=#psu_element{type=3, percent=50}}),
{ok, User2} = egs_users:read(GID),
State2 = State#state{slot=Slot},
psu_game:char_load(User2, State2),
{ok, egs_game, State2}.
Client2 = Client#client{slot=Slot},
psu_game:char_load(User2, Client2),
{ok, egs_game, Client2}.
%% Internal.

View File

@ -23,66 +23,66 @@
-include("include/records.hrl").
%% @doc Send a keepalive.
keepalive(#state{socket=Socket}) ->
keepalive(#client{socket=Socket}) ->
psu_proto:send_keepalive(Socket).
%% @doc Forward the broadcasted command to the client.
info({egs, cast, Command}, #state{gid=GID}) ->
info({egs, cast, Command}, #client{gid=GID}) ->
<< A:64/bits, _:32, B:96/bits, _:64, C/bits >> = Command,
psu_game:send(<< A/binary, 16#00011300:32, B/binary, 16#00011300:32, GID:32/little, C/binary >>);
%% @doc Forward the chat message to the client.
info({egs, chat, FromGID, ChatTypeID, ChatGID, ChatName, ChatModifiers, ChatMessage}, State) ->
psu_proto:send_0304(FromGID, ChatTypeID, ChatGID, ChatName, ChatModifiers, ChatMessage, State);
info({egs, chat, FromGID, ChatTypeID, ChatGID, ChatName, ChatModifiers, ChatMessage}, Client) ->
psu_proto:send_0304(FromGID, ChatTypeID, ChatGID, ChatName, ChatModifiers, ChatMessage, Client);
info({egs, notice, Type, Message}, State) ->
psu_proto:send_0228(Type, 2, Message, State);
info({egs, notice, Type, Message}, Client) ->
psu_proto:send_0228(Type, 2, Message, Client);
%% @doc Inform the client that a player has spawn.
%% @todo Not sure what IsSeasonal or the AreaNb in 0205 should be for other spawns.
info({egs, player_spawn, Player}, State) ->
psu_proto:send_0111(Player, 6, State),
psu_proto:send_010d(Player, State),
psu_proto:send_0205(Player, 0, State),
psu_proto:send_0203(Player, State),
psu_proto:send_0201(Player, State);
info({egs, player_spawn, Player}, Client) ->
psu_proto:send_0111(Player, 6, Client),
psu_proto:send_010d(Player, Client),
psu_proto:send_0205(Player, 0, Client),
psu_proto:send_0203(Player, Client),
psu_proto:send_0201(Player, Client);
%% @doc Inform the client that a player has unspawn.
info({egs, player_unspawn, Player}, State) ->
psu_proto:send_0204(Player, State);
info({egs, player_unspawn, Player}, Client) ->
psu_proto:send_0204(Player, Client);
%% @doc Warp the player to the given location.
info({egs, warp, QuestID, ZoneID, MapID, EntryID}, State) ->
event({area_change, QuestID, ZoneID, MapID, EntryID}, State).
info({egs, warp, QuestID, ZoneID, MapID, EntryID}, Client) ->
event({area_change, QuestID, ZoneID, MapID, EntryID}, Client).
%% Broadcasts.
%% @todo Handle broadcasting better than that. Review the commands at the same time.
%% @doc Position change. Save the position and then dispatch it.
cast(16#0503, Data, State=#state{gid=GID}) ->
cast(16#0503, Data, Client=#client{gid=GID}) ->
<< _:424, Dir:24/little, _PrevCoords:96, X:32/little-float, Y:32/little-float, Z:32/little-float,
QuestID:32/little, ZoneID:32/little, MapID:32/little, EntryID:32/little, _:32 >> = Data,
FloatDir = Dir / 46603.375,
{ok, User} = egs_users:read(GID),
NewUser = User#users{pos={X, Y, Z, FloatDir}, area={QuestID, ZoneID, MapID}, entryid=EntryID},
egs_users:write(NewUser),
cast(valid, Data, State);
cast(valid, Data, Client);
%% @doc Stand still. Save the position and then dispatch it.
cast(16#0514, Data, State=#state{gid=GID}) ->
cast(16#0514, Data, Client=#client{gid=GID}) ->
<< _:424, Dir:24/little, X:32/little-float, Y:32/little-float, Z:32/little-float,
QuestID:32/little, ZoneID:32/little, MapID:32/little, EntryID:32/little, _/bits >> = Data,
FloatDir = Dir / 46603.375,
{ok, User} = egs_users:read(GID),
NewUser = User#users{pos={X, Y, Z, FloatDir}, area={QuestID, ZoneID, MapID}, entryid=EntryID},
egs_users:write(NewUser),
cast(valid, Data, State);
cast(valid, Data, Client);
%% @doc Default broadcast handler. Dispatch the command to everyone.
%% We clean up the command and use the real GID and LID of the user, disregarding what was sent and possibly tampered with.
%% Only a handful of commands are allowed to broadcast. An user tampering with it would get disconnected instantly.
%% @todo Don't query the user data everytime! Keep the needed information in the State.
cast(Command, Data, #state{gid=GID, lid=LID})
%% @todo Don't query the user data everytime! Keep the needed information in the Client.
cast(Command, Data, #client{gid=GID, lid=LID})
when Command =:= 16#0101;
Command =:= 16#0102;
Command =:= 16#0104;
@ -100,7 +100,7 @@ cast(Command, Data, #state{gid=GID, lid=LID})
%% @todo Handle this packet properly.
%% @todo Spawn cleared response event shouldn't be handled following this packet but when we see the spawn actually dead HP-wise.
%% @todo Type shouldn't be :32 but it seems when the later 16 have something it's not a spawn event.
raw(16#0402, << _:352, Data/bits >>, #state{gid=GID}) ->
raw(16#0402, << _:352, Data/bits >>, #client{gid=GID}) ->
<< SpawnID:32/little, _:64, Type:32/little, _:64 >> = Data,
case Type of
7 -> % spawn cleared @todo 1201 sent back with same values apparently, but not always
@ -116,25 +116,25 @@ raw(16#0402, << _:352, Data/bits >>, #state{gid=GID}) ->
%% @todo Handle this packet.
%% @todo 3rd Unsafe Passage C, EventID 10 BlockID 2 = mission cleared?
raw(16#0404, << _:352, Data/bits >>, _State) ->
raw(16#0404, << _:352, Data/bits >>, _Client) ->
<< EventID:8, BlockID:8, _:16, Value:8, _/bits >> = Data,
log("unknown command 0404: eventid ~b blockid ~b value ~b", [EventID, BlockID, Value]),
psu_game:send_1205(EventID, BlockID, Value);
%% @todo Used in the tutorial. Not sure what it does. Give an item (the PA) maybe?
%% @todo Probably should ignore that until more is known.
raw(16#0a09, _Data, #state{gid=GID}) ->
raw(16#0a09, _Data, #client{gid=GID}) ->
psu_game:send(<< 16#0a090300:32, 0:32, 16#00011300:32, GID:32/little, 0:64, 16#00011300:32, GID:32/little, 0:64, 16#00003300:32, 0:32 >>);
%% @todo Figure out this command.
raw(16#0c11, << _:352, A:32/little, B:32/little >>, #state{gid=GID}) ->
raw(16#0c11, << _:352, A:32/little, B:32/little >>, #client{gid=GID}) ->
log("0c11 ~p ~p", [A, B]),
psu_game:send(<< 16#0c120300:32, 0:160, 16#00011300:32, GID:32/little, 0:64, A:32/little, 1:32/little >>);
%% @doc Set flag handler. Associate a new flag with the character.
%% Just reply with a success value for now.
%% @todo God save the flags.
raw(16#0d04, << _:352, Data/bits >>, #state{gid=GID}) ->
raw(16#0d04, << _:352, Data/bits >>, #client{gid=GID}) ->
<< Flag:128/bits, A:16/bits, _:8, B/bits >> = Data,
log("flag handler for ~s", [re:replace(Flag, "\\0+", "", [global, {return, binary}])]),
psu_game:send(<< 16#0d040300:32, 0:160, 16#00011300:32, GID:32/little, 0:64, Flag/binary, A/binary, 1, B/binary >>);
@ -142,7 +142,7 @@ raw(16#0d04, << _:352, Data/bits >>, #state{gid=GID}) ->
%% @doc Initialize a vehicle object.
%% @todo Find what are the many values, including the odd Whut value (and whether it's used in the reply).
%% @todo Separate the reply.
raw(16#0f00, << _:352, Data/bits >>, _State) ->
raw(16#0f00, << _:352, Data/bits >>, _Client) ->
<< A:32/little, 0:16, B:16/little, 0:16, C:16/little, 0, Whut:8, D:16/little, 0:16,
E:16/little, 0:16, F:16/little, G:16/little, H:16/little, I:32/little >> = Data,
log("init vehicle: ~b ~b ~b ~b ~b ~b ~b ~b ~b ~b", [A, B, C, Whut, D, E, F, G, H, I]),
@ -152,7 +152,7 @@ raw(16#0f00, << _:352, Data/bits >>, _State) ->
%% @doc Enter vehicle.
%% @todo Separate the reply.
raw(16#0f02, << _:352, Data/bits >>, _State) ->
raw(16#0f02, << _:352, Data/bits >>, _Client) ->
<< A:32/little, B:32/little, C:32/little >> = Data,
log("enter vehicle: ~b ~b ~b", [A, B, C]),
HP = 100,
@ -160,46 +160,46 @@ raw(16#0f02, << _:352, Data/bits >>, _State) ->
%% @doc Sent right after entering the vehicle. Can't move without it.
%% @todo Separate the reply.
raw(16#0f07, << _:352, Data/bits >>, _State) ->
raw(16#0f07, << _:352, Data/bits >>, _Client) ->
<< A:32/little, B:32/little >> = Data,
log("after enter vehicle: ~b ~b", [A, B]),
psu_game:send(<< (psu_game:header(16#120f))/binary, A:32/little, B:32/little >>);
%% @todo Not sure yet.
raw(16#1019, _Data, _State) ->
raw(16#1019, _Data, _Client) ->
ignore;
%~ psu_game:send(<< (psu_game:header(16#1019))/binary, 0:192, 16#00200000:32, 0:32 >>);
%% @todo Not sure about that one though. Probably related to 1112 still.
raw(16#1106, << _:352, Data/bits >>, _State) ->
raw(16#1106, << _:352, Data/bits >>, _Client) ->
psu_game:send_110e(Data);
%% @doc Probably asking permission to start the video (used for syncing?).
raw(16#1112, << _:352, Data/bits >>, _State) ->
raw(16#1112, << _:352, Data/bits >>, _Client) ->
psu_game:send_1113(Data);
%% @todo Not sure yet. Value is probably a TargetID. Used in Airboard Rally. Replying with the same value starts the race.
raw(16#1216, << _:352, Data/bits >>, _State) ->
raw(16#1216, << _:352, Data/bits >>, _Client) ->
<< Value:32/little >> = Data,
log("command 1216 with value ~b", [Value]),
psu_game:send_1216(Value);
%% @doc Dismiss all unknown raw commands with a log notice.
%% @todo Have a log event handler instead.
raw(Command, _Data, State) ->
io:format("~p (~p): dismissed command ~4.16.0b~n", [?MODULE, State#state.gid, Command]).
raw(Command, _Data, Client) ->
io:format("~p (~p): dismissed command ~4.16.0b~n", [?MODULE, Client#client.gid, Command]).
%% Events.
%% @todo When changing lobby to the room, or room to lobby, we must perform an universe change.
%% @todo Probably move area_load inside the event and make other events call this one when needed.
event({area_change, QuestID, ZoneID, MapID, EntryID}, State) ->
event({area_change, QuestID, ZoneID, MapID, EntryID, 16#ffffffff}, State);
event({area_change, QuestID, ZoneID, MapID, EntryID, PartyPos}, State) ->
event({area_change, QuestID, ZoneID, MapID, EntryID}, Client) ->
event({area_change, QuestID, ZoneID, MapID, EntryID, 16#ffffffff}, Client);
event({area_change, QuestID, ZoneID, MapID, EntryID, PartyPos}, Client) ->
case PartyPos of
16#ffffffff ->
log("area change (~b,~b,~b,~b,~b)", [QuestID, ZoneID, MapID, EntryID, PartyPos]),
psu_game:area_load(QuestID, ZoneID, MapID, EntryID, State);
psu_game:area_load(QuestID, ZoneID, MapID, EntryID, Client);
_Any -> %% @todo Handle area_change event for NPCs in story missions.
ignore
end;
@ -207,10 +207,10 @@ event({area_change, QuestID, ZoneID, MapID, EntryID, PartyPos}, State) ->
%% @doc After the character has been (re)loaded, change the area he's in.
%% @todo The area_load function should probably not change the user's values.
%% @todo Remove that ugly code when the above is done.
event(char_load_complete, State=#state{gid=GID}) ->
event(char_load_complete, Client=#client{gid=GID}) ->
{ok, User=#users{area={QuestID, ZoneID, MapID}, entryid=EntryID}} = egs_users:read(GID),
egs_users:write(User#users{area={0, 0, 0}, entryid=0}),
event({area_change, QuestID, ZoneID, MapID, EntryID}, State);
event({area_change, QuestID, ZoneID, MapID, EntryID}, Client);
%% @doc Chat broadcast handler. Dispatch the message to everyone (for now).
%% Disregard the name sent by the server. Use the name saved in memory instead, to prevent client-side editing.
@ -218,7 +218,7 @@ event(char_load_complete, State=#state{gid=GID}) ->
%% @todo In the case of NPC characters, when FromTypeID is 00001d00, check that the NPC is in the party and broadcast only to the party (probably).
%% @todo When the game doesn't find an NPC (probably) and forces it to talk like in the tutorial mission it seems FromTypeID, FromGID and Name are all 0.
%% @todo Make sure modifiers have correct values.
event({chat, _FromTypeID, FromGID, _FromName, Modifiers, ChatMsg}, #state{gid=UserGID}) ->
event({chat, _FromTypeID, FromGID, _FromName, Modifiers, ChatMsg}, #client{gid=UserGID}) ->
[BcastTypeID, BcastGID, BcastName] = case FromGID of
0 -> %% This probably shouldn't happen. Just make it crash on purpose.
log("chat FromGID=0"),
@ -238,13 +238,13 @@ event({chat, _FromTypeID, FromGID, _FromName, Modifiers, ChatMsg}, #state{gid=Us
egs_users:broadcast_all({egs, chat, UserGID, BcastTypeID, BcastGID, BcastName, Modifiers, ChatMsg});
%% @todo There's at least 9 different sets of locations. Handle all of them correctly.
event(counter_background_locations_request, _State) ->
event(counter_background_locations_request, _Client) ->
psu_game:send_170c();
%% @todo Make sure non-mission counters follow the same loading process.
%% @todo Probably validate the From* values, to not send the player back inside a mission.
%% @todo Handle the LID change when entering counters.
event({counter_enter, CounterID, FromZoneID, FromMapID, FromEntryID}, State=#state{gid=GID}) ->
event({counter_enter, CounterID, FromZoneID, FromMapID, FromEntryID}, Client=#client{gid=GID}) ->
log("counter load ~b", [CounterID]),
{ok, OldUser} = egs_users:read(GID),
FromArea = {element(1, OldUser#users.area), FromZoneID, FromMapID},
@ -255,79 +255,79 @@ event({counter_enter, CounterID, FromZoneID, FromMapID, FromEntryID}, State=#sta
QuestData = egs_quests_db:quest_nbl(0),
ZoneData = << 0:16000 >>, %% Doing like official just in case.
%% load counter
psu_proto:send_0c00(User, State),
psu_proto:send_020e(QuestData, State),
psu_proto:send_0a05(State),
psu_proto:send_010d(User, State),
psu_proto:send_0200(0, mission, State),
psu_proto:send_020f(ZoneData, 0, 255, State),
State2 = State#state{areanb=State#state.areanb + 1},
psu_proto:send_0205(User, 0, State2),
psu_proto:send_100e(CounterID, "Counter", State2),
psu_proto:send_0215(0, State2),
psu_proto:send_0215(0, State2),
psu_proto:send_020c(State2),
psu_proto:send_0c00(User, Client),
psu_proto:send_020e(QuestData, Client),
psu_proto:send_0a05(Client),
psu_proto:send_010d(User, Client),
psu_proto:send_0200(0, mission, Client),
psu_proto:send_020f(ZoneData, 0, 255, Client),
Client2 = Client#client{areanb=Client#client.areanb + 1},
psu_proto:send_0205(User, 0, Client2),
psu_proto:send_100e(CounterID, "Counter", Client2),
psu_proto:send_0215(0, Client2),
psu_proto:send_0215(0, Client2),
psu_proto:send_020c(Client2),
psu_game:send_1202(),
psu_proto:send_1204(State2),
psu_proto:send_1204(Client2),
psu_game:send_1206(),
psu_game:send_1207(),
psu_game:send_1212(),
psu_proto:send_0201(User, State2),
psu_proto:send_0a06(User, State2),
psu_proto:send_0201(User, Client2),
psu_proto:send_0a06(User, Client2),
case User#users.partypid of
undefined -> ignore;
_ -> psu_game:send_022c(0, 16#12)
end,
State3 = State2#state{areanb=State2#state.areanb + 1},
psu_proto:send_0208(State3),
psu_proto:send_0236(State3),
{ok, State3};
Client3 = Client2#client{areanb=Client2#client.areanb + 1},
psu_proto:send_0208(Client3),
psu_proto:send_0236(Client3),
{ok, Client3};
%% @todo Handle parties to join.
event(counter_join_party_request, State) ->
psu_proto:send_1701(State);
event(counter_join_party_request, Client) ->
psu_proto:send_1701(Client);
%% @doc Leave mission counter handler.
event(counter_leave, State=#state{gid=GID}) ->
event(counter_leave, Client=#client{gid=GID}) ->
{ok, User} = egs_users:read(GID),
PrevArea = User#users.prev_area,
event({area_change, element(1, PrevArea), element(2, PrevArea), element(3, PrevArea), User#users.prev_entryid}, State);
event({area_change, element(1, PrevArea), element(2, PrevArea), element(3, PrevArea), User#users.prev_entryid}, Client);
%% @doc Send the code for the background image to use. But there's more that should be sent though.
%% @todo Apparently background values 1 2 3 are never used on official servers. Find out why.
%% @todo Rename to counter_bg_request.
event({counter_options_request, CounterID}, State) ->
event({counter_options_request, CounterID}, Client) ->
log("counter options request ~p", [CounterID]),
psu_proto:send_1711(egs_counters_db:bg(CounterID), State);
psu_proto:send_1711(egs_counters_db:bg(CounterID), Client);
%% @todo Handle when the party already exists! And stop doing it wrong.
event(counter_party_info_request, #state{gid=GID}) ->
event(counter_party_info_request, #client{gid=GID}) ->
{ok, User} = egs_users:read(GID),
psu_game:send_1706((User#users.character)#characters.name);
%% @todo Item distribution is always set to random for now.
event(counter_party_options_request, _State) ->
event(counter_party_options_request, _Client) ->
psu_game:send_170a();
%% @doc Request the counter's quest files.
event({counter_quest_files_request, CounterID}, State) ->
event({counter_quest_files_request, CounterID}, Client) ->
log("counter quest files request ~p", [CounterID]),
psu_proto:send_0c06(egs_counters_db:pack(CounterID), State);
psu_proto:send_0c06(egs_counters_db:pack(CounterID), Client);
%% @doc Counter available mission list request handler.
event({counter_quest_options_request, CounterID}, State) ->
event({counter_quest_options_request, CounterID}, Client) ->
log("counter quest options request ~p", [CounterID]),
psu_proto:send_0c10(egs_counters_db:opts(CounterID), State);
psu_proto:send_0c10(egs_counters_db:opts(CounterID), Client);
%% @todo A and B are mostly unknown. Like most of everything else from the command 0e00...
event({hit, FromTargetID, ToTargetID, A, B}, State=#state{gid=GID}) ->
event({hit, FromTargetID, ToTargetID, A, B}, Client=#client{gid=GID}) ->
{ok, User} = egs_users:read(GID),
%% hit!
#hit_response{type=Type, user=NewUser, exp=HasEXP, damage=Damage, targethp=TargetHP, targetse=TargetSE, events=Events} = psu_instance:hit(User, FromTargetID, ToTargetID),
case Type of
box ->
%% @todo also has a hit sent, we should send it too
events(Events, State);
events(Events, Client);
_ ->
PlayerHP = (NewUser#users.character)#characters.currenthp,
case lists:member(death, TargetSE) of
@ -343,17 +343,17 @@ event({hit, FromTargetID, ToTargetID, A, B}, State=#state{gid=GID}) ->
end,
%% exp
if HasEXP =:= true ->
psu_proto:send_0115(NewUser, ToTargetID, State);
psu_proto:send_0115(NewUser, ToTargetID, Client);
true -> ignore
end,
%% save
egs_users:write(NewUser);
event({hits, Hits}, State) ->
events(Hits, State);
event({hits, Hits}, Client) ->
events(Hits, Client);
event({item_description_request, ItemID}, State) ->
psu_proto:send_0a11(ItemID, egs_items_db:desc(ItemID), State);
event({item_description_request, ItemID}, Client) ->
psu_proto:send_0a11(ItemID, egs_items_db:desc(ItemID), Client);
%% @todo A and B are unknown.
%% Melee uses a format similar to: AAAA--BBCCCC----DDDDDDDDEE----FF with
@ -364,7 +364,7 @@ event({item_description_request, ItemID}, State) ->
%% @todo Currently use a separate file for the data sent for the weapons.
%% @todo TargetGID and TargetLID must be validated, they're either the player's or his NPC characters.
%% @todo Handle NPC characters properly.
event({item_equip, ItemIndex, TargetGID, TargetLID, A, B}, #state{gid=GID}) ->
event({item_equip, ItemIndex, TargetGID, TargetLID, A, B}, #client{gid=GID}) ->
case egs_users:item_nth(GID, ItemIndex) of
{ItemID, Variables} when element(1, Variables) =:= psu_special_item_variables ->
<< Category:8, _:24 >> = << ItemID:32 >>,
@ -399,7 +399,7 @@ event({item_equip, ItemIndex, TargetGID, TargetLID, A, B}, #state{gid=GID}) ->
ignore
end;
event({item_set_trap, ItemIndex, TargetGID, TargetLID, A, B}, #state{gid=GID}) ->
event({item_set_trap, ItemIndex, TargetGID, TargetLID, A, B}, #client{gid=GID}) ->
{ItemID, _Variables} = egs_users:item_nth(GID, ItemIndex),
egs_users:item_qty_add(GID, ItemIndex, -1),
<< Category:8, _:24 >> = << ItemID:32 >>,
@ -408,7 +408,7 @@ event({item_set_trap, ItemIndex, TargetGID, TargetLID, A, B}, #state{gid=GID}) -
%% @todo A and B are unknown.
%% @see item_equip
event({item_unequip, ItemIndex, TargetGID, TargetLID, A, B}, #state{gid=GID}) ->
event({item_unequip, ItemIndex, TargetGID, TargetLID, A, B}, #client{gid=GID}) ->
Category = case ItemIndex of
% units would be 8, traps would be 12
19 -> 2; % armor
@ -419,16 +419,16 @@ event({item_unequip, ItemIndex, TargetGID, TargetLID, A, B}, #state{gid=GID}) ->
0:64, TargetGID:32/little, TargetLID:32/little, ItemIndex, 2, Category, A, B:32/little >>);
%% @todo Just ignore the meseta price for now and send the player where he wanna be!
event(lobby_transport_request, State) ->
psu_proto:send_0c08(State);
event(lobby_transport_request, Client) ->
psu_proto:send_0c08(Client);
event(lumilass_options_request, State=#state{gid=GID}) ->
event(lumilass_options_request, Client=#client{gid=GID}) ->
{ok, User} = egs_users:read(GID),
psu_proto:send_1a03(User, State);
psu_proto:send_1a03(User, Client);
%% @todo Probably replenish the player HP when entering a non-mission area rather than when aborting the mission?
event(mission_abort, State=#state{gid=GID}) ->
psu_proto:send_1006(11, State),
event(mission_abort, Client=#client{gid=GID}) ->
psu_proto:send_1006(11, Client),
{ok, User} = egs_users:read(GID),
%% delete the mission
if User#users.instancepid =:= undefined -> ignore;
@ -443,20 +443,20 @@ event(mission_abort, State=#state{gid=GID}) ->
%% map change
if User#users.areatype =:= mission ->
PrevArea = User#users.prev_area,
event({area_change, element(1, PrevArea), element(2, PrevArea), element(3, PrevArea), User#users.prev_entryid}, State);
event({area_change, element(1, PrevArea), element(2, PrevArea), element(3, PrevArea), User#users.prev_entryid}, Client);
true -> ignore
end;
%% @todo Forward the mission start to other players of the same party, whatever their location is.
event({mission_start, QuestID}, State) ->
event({mission_start, QuestID}, Client) ->
log("mission start ~b", [QuestID]),
psu_proto:send_1020(State),
psu_proto:send_1020(Client),
psu_game:send_1015(QuestID),
psu_game:send_0c02();
%% @doc Force the invite of an NPC character while inside a mission. Mostly used by story missions.
%% Note that the NPC is often removed and reinvited between block/cutscenes.
event({npc_force_invite, NPCid}, State=#state{gid=GID}) ->
event({npc_force_invite, NPCid}, Client=#client{gid=GID}) ->
{ok, User} = egs_users:read(GID),
%% Create NPC.
log("npc force invite ~p", [NPCid]),
@ -477,9 +477,9 @@ event({npc_force_invite, NPCid}, State=#state{gid=GID}) ->
Character = NPCUser#users.character,
SentNPCCharacter = Character#characters{gid=NPCid, npcid=NPCid},
SentNPCUser = NPCUser#users{character=SentNPCCharacter},
psu_proto:send_010d(SentNPCUser, State),
psu_proto:send_0201(SentNPCUser, State),
psu_proto:send_0215(0, State),
psu_proto:send_010d(SentNPCUser, Client),
psu_proto:send_0201(SentNPCUser, Client),
psu_proto:send_0215(0, Client),
psu_game:send_0a04(SentNPCUser#users.gid),
psu_game:send_022c(0, 16#12),
psu_game:send_1004(npc_mission, SentNPCUser, PartyPos),
@ -487,7 +487,7 @@ event({npc_force_invite, NPCid}, State=#state{gid=GID}) ->
psu_game:send_1601(PartyPos);
%% @todo Also at the end send a 101a (NPC:16, PartyPos:16, ffffffff). Not sure about PartyPos.
event({npc_invite, NPCid}, #state{gid=GID}) ->
event({npc_invite, NPCid}, #client{gid=GID}) ->
{ok, User} = egs_users:read(GID),
%% Create NPC.
log("invited npcid ~b", [NPCid]),
@ -512,7 +512,7 @@ event({npc_invite, NPCid}, #state{gid=GID}) ->
psu_game:send_101a(NPCid, PartyPos);
%% @todo Should be 0115(money) 010a03(confirm sale).
event({npc_shop_buy, ShopItemIndex, QuantityOrColor}, State=#state{gid=GID}) ->
event({npc_shop_buy, ShopItemIndex, QuantityOrColor}, Client=#client{gid=GID}) ->
ShopID = egs_users:shop_get(GID),
ItemID = egs_shops_db:nth(ShopID, ShopItemIndex + 1),
log("npc shop ~p buy itemid ~8.16.0b quantity/color+1 ~p", [ShopID, ItemID, QuantityOrColor]),
@ -537,7 +537,7 @@ event({npc_shop_buy, ShopItemIndex, QuantityOrColor}, State=#state{gid=GID}) ->
egs_users:money_add(GID, -1 * BuyPrice * Quantity),
ItemUUID = egs_users:item_add(GID, ItemID, Variables),
{ok, User} = egs_users:read(GID),
psu_proto:send_0115(User, State), %% @todo This one is apparently broadcast to everyone in the same zone.
psu_proto:send_0115(User, Client), %% @todo This one is apparently broadcast to everyone in the same zone.
%% @todo Following command isn't done 100% properly.
UCS2Name = << << X:8, 0:8 >> || X <- Name >>,
NamePadding = 8 * (46 - byte_size(UCS2Name)),
@ -548,37 +548,37 @@ event({npc_shop_buy, ShopItemIndex, QuantityOrColor}, State=#state{gid=GID}) ->
UCS2Name/binary, 0:NamePadding, RarityInt:8, Category:8, SellPrice:32/little, (psu_game:build_item_constants(Constants))/binary >>);
%% @todo Currently send the normal items shop for all shops, differentiate.
event({npc_shop_enter, ShopID}, #state{gid=GID}) ->
event({npc_shop_enter, ShopID}, #client{gid=GID}) ->
log("npc shop enter ~p", [ShopID]),
egs_users:shop_enter(GID, ShopID),
psu_game:send_010a(egs_shops_db:read(ShopID));
event({npc_shop_leave, ShopID}, #state{gid=GID}) ->
event({npc_shop_leave, ShopID}, #client{gid=GID}) ->
log("npc shop leave ~p", [ShopID]),
egs_users:shop_leave(GID),
psu_game:send(<< 16#010a0300:32, 0:64, GID:32/little, 0:64, 16#00011300:32,
GID:32/little, 0:64, GID:32/little, 0:32 >>);
%% @todo Should be 0115(money) 010a03(confirm sale).
event({npc_shop_sell, InventoryItemIndex, Quantity}, _State) ->
event({npc_shop_sell, InventoryItemIndex, Quantity}, _Client) ->
log("npc shop sell itemindex ~p quantity ~p", [InventoryItemIndex, Quantity]);
%% @todo First 1a02 value should be non-0.
%% @todo Could the 2nd 1a02 parameter simply be the shop type or something?
%% @todo Although the values replied should be right, they seem mostly ignored by the client.
event({npc_shop_request, ShopID}, State) ->
event({npc_shop_request, ShopID}, Client) ->
log("npc shop request ~p", [ShopID]),
case ShopID of
80 -> psu_proto:send_1a02(17, 17, 3, 9, State); %% lumilass
90 -> psu_proto:send_1a02(5, 1, 4, 5, State); %% parum weapon grinding
91 -> psu_proto:send_1a02(5, 5, 4, 7, State); %% tenora weapon grinding
92 -> psu_proto:send_1a02(5, 8, 4, 0, State); %% yohmei weapon grinding
93 -> psu_proto:send_1a02(5, 18, 4, 0, State); %% kubara weapon grinding
_ -> psu_proto:send_1a02(0, 1, 0, 0, State)
80 -> psu_proto:send_1a02(17, 17, 3, 9, Client); %% lumilass
90 -> psu_proto:send_1a02(5, 1, 4, 5, Client); %% parum weapon grinding
91 -> psu_proto:send_1a02(5, 5, 4, 7, Client); %% tenora weapon grinding
92 -> psu_proto:send_1a02(5, 8, 4, 0, Client); %% yohmei weapon grinding
93 -> psu_proto:send_1a02(5, 18, 4, 0, Client); %% kubara weapon grinding
_ -> psu_proto:send_1a02(0, 1, 0, 0, Client)
end;
%% @todo Not sure what are those hardcoded values.
event({object_boss_gate_activate, ObjectID}, _State) ->
event({object_boss_gate_activate, ObjectID}, _Client) ->
psu_game:send_1213(ObjectID, 0),
psu_game:send_1215(2, 16#7008),
%% @todo Following sent after the warp?
@ -586,41 +586,41 @@ event({object_boss_gate_activate, ObjectID}, _State) ->
%% @todo Why resend this?
psu_game:send_1213(ObjectID, 0);
event({object_boss_gate_enter, ObjectID}, _State) ->
event({object_boss_gate_enter, ObjectID}, _Client) ->
psu_game:send_1213(ObjectID, 1);
%% @todo Do we need to send something back here?
event({object_boss_gate_leave, _ObjectID}, _State) ->
event({object_boss_gate_leave, _ObjectID}, _Client) ->
ignore;
event({object_box_destroy, ObjectID}, _State) ->
event({object_box_destroy, ObjectID}, _Client) ->
psu_game:send_1213(ObjectID, 3);
%% @todo Second send_1211 argument should be User#users.lid. Fix when it's correctly handled.
event({object_chair_sit, ObjectTargetID}, _State) ->
event({object_chair_sit, ObjectTargetID}, _Client) ->
%~ {ok, User} = egs_users:read(get(gid)),
psu_game:send_1211(ObjectTargetID, 0, 8, 0);
%% @todo Second psu_game:send_1211 argument should be User#users.lid. Fix when it's correctly handled.
event({object_chair_stand, ObjectTargetID}, _State) ->
event({object_chair_stand, ObjectTargetID}, _Client) ->
%~ {ok, User} = egs_users:read(get(gid)),
psu_game:send_1211(ObjectTargetID, 0, 8, 2);
event({object_crystal_activate, ObjectID}, _State) ->
event({object_crystal_activate, ObjectID}, _Client) ->
psu_game:send_1213(ObjectID, 1);
%% @doc Server-side event.
event({object_event_trigger, BlockID, EventID}, _State) ->
event({object_event_trigger, BlockID, EventID}, _Client) ->
psu_game:send_1205(EventID, BlockID, 0);
event({object_goggle_target_activate, ObjectID}, #state{gid=GID}) ->
event({object_goggle_target_activate, ObjectID}, #client{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, EventID} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
psu_game:send_1205(EventID, BlockID, 0),
psu_game:send_1213(ObjectID, 8);
%% @todo Make NPC characters heal too.
event({object_healing_pad_tick, [_PartyPos]}, State=#state{gid=GID}) ->
event({object_healing_pad_tick, [_PartyPos]}, Client=#client{gid=GID}) ->
{ok, User} = egs_users:read(GID),
Character = User#users.character,
if Character#characters.currenthp =:= Character#characters.maxhp -> ignore;
@ -629,55 +629,55 @@ event({object_healing_pad_tick, [_PartyPos]}, State=#state{gid=GID}) ->
NewHP2 = if NewHP > Character#characters.maxhp -> Character#characters.maxhp; true -> NewHP end,
User2 = User#users{character=Character#characters{currenthp=NewHP2}},
egs_users:write(User2),
psu_proto:send_0117(User2, State),
psu_proto:send_0111(User2, 4, State)
psu_proto:send_0117(User2, Client),
psu_proto:send_0111(User2, 4, Client)
end;
event({object_key_console_enable, ObjectID}, #state{gid=GID}) ->
event({object_key_console_enable, ObjectID}, #client{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, [EventID|_]} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
psu_game:send_1205(EventID, BlockID, 0),
psu_game:send_1213(ObjectID, 1);
event({object_key_console_init, ObjectID}, #state{gid=GID}) ->
event({object_key_console_init, ObjectID}, #client{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, [_, EventID, _]} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
psu_game:send_1205(EventID, BlockID, 0);
event({object_key_console_open_gate, ObjectID}, #state{gid=GID}) ->
event({object_key_console_open_gate, ObjectID}, #client{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, [_, _, EventID]} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
psu_game:send_1205(EventID, BlockID, 0),
psu_game:send_1213(ObjectID, 1);
%% @todo Now that it's separate from object_key_console_enable, handle it better than that, don't need a list of events.
event({object_key_enable, ObjectID}, #state{gid=GID}) ->
event({object_key_enable, ObjectID}, #client{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, [EventID|_]} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
psu_game:send_1205(EventID, BlockID, 0),
psu_game:send_1213(ObjectID, 1);
%% @todo Some switch objects apparently work differently, like the light switch in Mines in MAG'.
event({object_switch_off, ObjectID}, #state{gid=GID}) ->
event({object_switch_off, ObjectID}, #client{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, EventID} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
psu_game:send_1205(EventID, BlockID, 1),
psu_game:send_1213(ObjectID, 0);
event({object_switch_on, ObjectID}, #state{gid=GID}) ->
event({object_switch_on, ObjectID}, #client{gid=GID}) ->
{ok, User} = egs_users:read(GID),
{BlockID, EventID} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
psu_game:send_1205(EventID, BlockID, 0),
psu_game:send_1213(ObjectID, 1);
event({object_vehicle_boost_enable, ObjectID}, _State) ->
event({object_vehicle_boost_enable, ObjectID}, _Client) ->
psu_game:send_1213(ObjectID, 1);
event({object_vehicle_boost_respawn, ObjectID}, _State) ->
event({object_vehicle_boost_respawn, ObjectID}, _Client) ->
psu_game:send_1213(ObjectID, 0);
%% @todo Second send_1211 argument should be User#users.lid. Fix when it's correctly handled.
event({object_warp_take, BlockID, ListNb, ObjectNb}, #state{gid=GID}) ->
event({object_warp_take, BlockID, ListNb, ObjectNb}, #client{gid=GID}) ->
{ok, User} = egs_users:read(GID),
Pos = psu_instance:warp_event(User#users.instancepid, element(2, User#users.area), BlockID, ListNb, ObjectNb),
NewUser = User#users{pos=Pos},
@ -686,7 +686,7 @@ event({object_warp_take, BlockID, ListNb, ObjectNb}, #state{gid=GID}) ->
psu_game:send_1211(16#ffffffff, 0, 14, 0);
%% @todo Don't send_0204 if the player is removed from the party while in the lobby I guess.
event({party_remove_member, PartyPos}, State=#state{gid=GID}) ->
event({party_remove_member, PartyPos}, Client=#client{gid=GID}) ->
log("party remove member ~b", [PartyPos]),
{ok, DestUser} = egs_users:read(GID),
{ok, RemovedGID} = psu_party:get_member(DestUser#users.partypid, PartyPos),
@ -696,17 +696,17 @@ event({party_remove_member, PartyPos}, State=#state{gid=GID}) ->
npc -> egs_users:delete(RemovedGID);
_ -> ignore
end,
psu_proto:send_1006(8, PartyPos, State),
psu_proto:send_0204(RemovedUser, State),
psu_proto:send_0215(0, State);
psu_proto:send_1006(8, PartyPos, Client),
psu_proto:send_0204(RemovedUser, Client),
psu_proto:send_0215(0, Client);
event({player_options_change, Options}, #state{gid=GID, slot=Slot}) ->
event({player_options_change, Options}, #client{gid=GID, slot=Slot}) ->
Folder = egs_accounts:get_folder(GID),
file:write_file(io_lib:format("save/~s/~b-character.options", [Folder, Slot]), Options);
%% @todo If the player has a scape, use it! Otherwise red screen.
%% @todo Right now we force revive with a dummy HP value.
event(player_death, State=#state{gid=GID}) ->
event(player_death, Client=#client{gid=GID}) ->
% @todo send_0115(get(gid), 16#ffffffff, LV=1, EXP=idk, Money=1000), % apparently sent everytime you die...
%% use scape:
NewHP = 10,
@ -714,33 +714,33 @@ event(player_death, State=#state{gid=GID}) ->
Char = User#users.character,
User2 = User#users{character=Char#characters{currenthp=NewHP}},
egs_users:write(User2),
psu_proto:send_0117(User2, State),
psu_proto:send_1022(User2, State);
psu_proto:send_0117(User2, Client),
psu_proto:send_1022(User2, Client);
%% red screen with return to lobby choice:
%~ psu_proto:send_0111(User2, 3, 1, State);
%~ psu_proto:send_0111(User2, 3, 1, Client);
%% @todo Refill the player's HP to maximum, remove SEs etc.
event(player_death_return_to_lobby, State=#state{gid=GID}) ->
event(player_death_return_to_lobby, Client=#client{gid=GID}) ->
{ok, User} = egs_users:read(GID),
PrevArea = User#users.prev_area,
event({area_change, element(1, PrevArea), element(2, PrevArea), element(3, PrevArea), User#users.prev_entryid}, State);
event({area_change, element(1, PrevArea), element(2, PrevArea), element(3, PrevArea), User#users.prev_entryid}, Client);
event(player_type_availability_request, State) ->
psu_proto:send_1a07(State);
event(player_type_availability_request, Client) ->
psu_proto:send_1a07(Client);
event(player_type_capabilities_request, _State) ->
event(player_type_capabilities_request, _Client) ->
psu_game:send_0113();
event(ppcube_request, _State) ->
event(ppcube_request, _Client) ->
psu_game:send_1a04();
event(unicube_request, State) ->
psu_proto:send_021e(egs_universes:all(), State);
event(unicube_request, Client) ->
psu_proto:send_021e(egs_universes:all(), Client);
%% @todo When selecting 'Your room', don't load a default room that's not yours.
event({unicube_select, cancel, _EntryID}, _State) ->
event({unicube_select, cancel, _EntryID}, _Client) ->
ignore;
event({unicube_select, Selection, EntryID}, State=#state{gid=GID}) ->
event({unicube_select, Selection, EntryID}, Client=#client{gid=GID}) ->
{ok, User} = egs_users:read(GID),
case Selection of
16#ffffffff ->
@ -750,7 +750,7 @@ event({unicube_select, Selection, EntryID}, State=#state{gid=GID}) ->
UniID = Selection,
User2 = User#users{uni=UniID, entryid=EntryID}
end,
psu_proto:send_0230(State),
psu_proto:send_0230(Client),
%% 0220
case User#users.partypid of
undefined -> ignore;
@ -764,13 +764,13 @@ event({unicube_select, Selection, EntryID}, State=#state{gid=GID}) ->
egs_users:write(User2),
egs_universes:leave(User#users.uni),
egs_universes:enter(UniID),
psu_game:char_load(User2, State).
psu_game:char_load(User2, Client).
%% Internal.
%% @doc Trigger many events.
events(Events, State) ->
[event(Event, State) || Event <- Events],
events(Events, Client) ->
[event(Event, Client) || Event <- Events],
ok.
%% @doc Log message to the console.

View File

@ -53,10 +53,10 @@ on_exit(Pid) ->
egs_users:delete(User#users.gid),
io:format("game (~p): quit~n", [User#users.gid]).
%% @doc Initialize the game state and start receiving messages.
%% @doc Initialize the game client and start receiving messages.
%% @todo Handle keepalive messages globally?
init(Socket) ->
timer:send_interval(5000, {egs, keepalive}),
State = #state{socket=Socket, gid=egs_accounts:tmp_gid()},
psu_proto:send_0202(State),
egs_network:recv(<< >>, egs_login, State).
Client = #client{socket=Socket, gid=egs_accounts:tmp_gid()},
psu_proto:send_0202(Client),
egs_network:recv(<< >>, egs_login, Client).

View File

@ -23,66 +23,66 @@
-include("include/records.hrl").
%% @doc Don't keep alive here, authentication should go fast.
keepalive(_State) ->
keepalive(_Client) ->
ok.
%% @doc We don't expect any message here.
info(_Msg, _State) ->
info(_Msg, _Client) ->
ok.
%% @doc Nothing to broadcast.
cast(_Command, _Data, _State) ->
cast(_Command, _Data, _Client) ->
ok.
%% Raw commands.
%% @doc Dismiss all raw commands with a log notice.
%% @todo Have a log event handler instead.
raw(Command, _Data, _State) ->
raw(Command, _Data, _Client) ->
io:format("~p: dismissed command ~4.16.0b~n", [?MODULE, Command]).
%% Events.
%% @doc Reject version < 2.0009.2.
%% @todo Reject wrong platforms too.
event({system_client_version_info, _Entrance, _Language, _Platform, Version}, State=#state{socket=Socket}) ->
event({system_client_version_info, _Entrance, _Language, _Platform, Version}, Client=#client{socket=Socket}) ->
if Version >= 2009002 -> ignore; true ->
psu_proto:send_0231("http://psumods.co.uk/forums/comments.php?DiscussionID=40#Item_1", State),
psu_proto:send_0231("http://psumods.co.uk/forums/comments.php?DiscussionID=40#Item_1", Client),
{ok, ErrorMsg} = file:read_file("priv/login/error_version.txt"),
psu_proto:send_0223(ErrorMsg, State),
psu_proto:send_0223(ErrorMsg, Client),
ssl:close(Socket),
closed
end;
%% @doc Game server info request handler.
event(system_game_server_request, State=#state{socket=Socket}) ->
event(system_game_server_request, Client=#client{socket=Socket}) ->
{ServerIP, ServerPort} = egs_conf:read(game_server),
psu_proto:send_0216(ServerIP, ServerPort, State),
psu_proto:send_0216(ServerIP, ServerPort, Client),
ssl:close(Socket),
closed;
%% @doc Authenticate the user by pattern matching its saved state against the key received.
%% If the user is authenticated, send him the character flags list.
%% @todo Remove the put calls when all the send_xxxx are moved out of psu_game and into psu_proto.
event({system_key_auth_request, AuthGID, AuthKey}, State=#state{socket=Socket}) ->
event({system_key_auth_request, AuthGID, AuthKey}, Client=#client{socket=Socket}) ->
egs_accounts:key_auth(AuthGID, AuthKey),
put(socket, Socket),
put(gid, AuthGID),
State2 = State#state{gid=AuthGID},
psu_proto:send_0d05(State2),
{ok, egs_char_select, State2};
Client2 = Client#client{gid=AuthGID},
psu_proto:send_0d05(Client2),
{ok, egs_char_select, Client2};
%% @doc Authentication request handler. Currently always succeed.
%% @todo Handle real GIDs whenever there's real authentication. GID is the second SessionID in the reply.
%% @todo Apparently it's possible to ask a question in the reply here. Used for free course on JP.
event({system_login_auth_request, Username, Password}, State) ->
event({system_login_auth_request, Username, Password}, Client) ->
{ok, GID} = egs_accounts:login_auth(Username, Password),
{ok, AuthKey} = egs_accounts:key_auth_init(GID),
io:format("auth success for ~s ~s~n", [Username, Password]),
psu_proto:send_0223(GID, AuthKey, State);
psu_proto:send_0223(GID, AuthKey, Client);
%% @doc MOTD request handler. Page number starts at 0.
%% @todo Currently ignore the language and send the same MOTD file to everyone.
event({system_motd_request, Page, _Language}, State) ->
event({system_motd_request, Page, _Language}, Client) ->
{ok, MOTD} = file:read_file("priv/login/motd.txt"),
psu_proto:send_0225(MOTD, Page, State).
psu_proto:send_0225(MOTD, Page, Client).

View File

@ -28,8 +28,8 @@ start_link(Port) ->
Pid = spawn(egs_network, listen, [Port, ?MODULE]),
{ok, Pid}.
%% @doc Initialize the game state and start receiving messages.
%% @doc Initialize the game client and start receiving messages.
init(Socket) ->
State = #state{socket=Socket, gid=egs_accounts:tmp_gid()},
psu_proto:send_0202(State),
egs_network:recv(<< >>, egs_login, State).
Client = #client{socket=Socket, gid=egs_accounts:tmp_gid()},
psu_proto:send_0202(Client),
egs_network:recv(<< >>, egs_login, Client).

View File

@ -46,13 +46,13 @@ accept(LSocket, CallbackMod) ->
?MODULE:accept(LSocket, CallbackMod).
%% @doc Main loop for the network stack. Receive and handle messages.
recv(SoFar, CallbackMod, State) ->
recv(SoFar, CallbackMod, Client) ->
receive
{ssl, _Any, Data} ->
{Commands, Rest} = split(<< SoFar/bits, Data/bits >>, []),
case dispatch(Commands, CallbackMod, CallbackMod, State) of
{ok, NextCallbackMod, NewState} ->
?MODULE:recv(Rest, NextCallbackMod, NewState);
case dispatch(Commands, CallbackMod, CallbackMod, Client) of
{ok, NextCallbackMod, NewClient} ->
?MODULE:recv(Rest, NextCallbackMod, NewClient);
closed -> closed
end;
{ssl_closed, _} ->
@ -60,41 +60,41 @@ recv(SoFar, CallbackMod, State) ->
{ssl_error, _, _} ->
ssl_error; %% exit
{egs, keepalive} ->
CallbackMod:keepalive(State),
?MODULE:recv(SoFar, CallbackMod, State);
CallbackMod:keepalive(Client),
?MODULE:recv(SoFar, CallbackMod, Client);
Tuple when element(1, Tuple) =:= egs ->
case CallbackMod:info(Tuple, State) of
{ok, NewState} -> ?MODULE:recv(SoFar, CallbackMod, NewState);
_Any -> ?MODULE:recv(SoFar, CallbackMod, State)
case CallbackMod:info(Tuple, Client) of
{ok, NewClient} -> ?MODULE:recv(SoFar, CallbackMod, NewClient);
_Any -> ?MODULE:recv(SoFar, CallbackMod, Client)
end;
_ ->
?MODULE:recv(SoFar, CallbackMod, State)
?MODULE:recv(SoFar, CallbackMod, Client)
end.
%% @doc Dispatch the commands received to the right handler.
dispatch([], _CallbackMod, NextMod, State) ->
{ok, NextMod, State};
dispatch([Data|Tail], CallbackMod, NextMod, State) ->
dispatch([], _CallbackMod, NextMod, Client) ->
{ok, NextMod, Client};
dispatch([Data|Tail], CallbackMod, NextMod, Client) ->
Ret = case psu_proto:parse(Data) of
{command, Command, Channel} ->
case Channel of
1 -> CallbackMod:cast(Command, Data, State);
_ -> CallbackMod:raw(Command, Data, State)
1 -> CallbackMod:cast(Command, Data, Client);
_ -> CallbackMod:raw(Command, Data, Client)
end;
ignore ->
ignore;
Event ->
CallbackMod:event(Event, State)
CallbackMod:event(Event, Client)
end,
case Ret of
{ok, NewMod, NewState} ->
dispatch(Tail, CallbackMod, NewMod, NewState);
{ok, NewState} ->
dispatch(Tail, CallbackMod, NextMod, NewState);
{ok, NewMod, NewClient} ->
dispatch(Tail, CallbackMod, NewMod, NewClient);
{ok, NewClient} ->
dispatch(Tail, CallbackMod, NextMod, NewClient);
closed ->
closed;
_Any ->
dispatch(Tail, CallbackMod, NextMod, State)
dispatch(Tail, CallbackMod, NextMod, Client)
end.
%% @doc Split the network data received into commands.

View File

@ -24,25 +24,25 @@
%% @doc Load and send the character information to the client.
%% @todo Move this whole function directly to psu_proto, probably.
char_load(User, State) ->
psu_proto:send_0d01(User#users.character, State),
char_load(User, Client) ->
psu_proto:send_0d01(User#users.character, Client),
%% 0246
send_0a0a((User#users.character)#characters.inventory),
psu_proto:send_1006(5, 0, State), %% @todo The 0 here is PartyPos, save it in User.
psu_proto:send_1005(User#users.character, State),
psu_proto:send_1006(12, State),
psu_proto:send_0210(State),
psu_proto:send_0222(User#users.uni, State),
psu_proto:send_1500(User#users.character, State),
psu_proto:send_1006(5, 0, Client), %% @todo The 0 here is PartyPos, save it in User.
psu_proto:send_1005(User#users.character, Client),
psu_proto:send_1006(12, Client),
psu_proto:send_0210(Client),
psu_proto:send_0222(User#users.uni, Client),
psu_proto:send_1500(User#users.character, Client),
send_1501(),
send_1512(),
%% 0303
send_1602(),
psu_proto:send_021b(State).
psu_proto:send_021b(Client).
%% @doc Load the given map as a standard lobby.
area_load(QuestID, ZoneID, MapID, EntryID, State) ->
{ok, OldUser} = egs_users:read(State#state.gid),
area_load(QuestID, ZoneID, MapID, EntryID, Client) ->
{ok, OldUser} = egs_users:read(Client#client.gid),
{OldQuestID, OldZoneID, _OldMapID} = OldUser#users.area,
QuestChange = OldQuestID /= QuestID,
ZoneChange = if OldQuestID =:= QuestID, OldZoneID =:= ZoneID -> false; true -> true end,
@ -53,78 +53,78 @@ area_load(QuestID, ZoneID, MapID, EntryID, State) ->
egs_users:write(User), %% @todo Booh ugly! But temporary.
%% Load the quest.
User2 = if QuestChange ->
psu_proto:send_0c00(User, State),
psu_proto:send_020e(egs_quests_db:quest_nbl(QuestID), State),
psu_proto:send_0c00(User, Client),
psu_proto:send_020e(egs_quests_db:quest_nbl(QuestID), Client),
User#users{questpid=egs_universes:lobby_pid(User#users.uni, QuestID)};
true -> User
end,
%% Load the zone.
State1 = if ZoneChange ->
Client1 = if ZoneChange ->
ZonePid = egs_quests:zone_pid(User2#users.questpid, ZoneID),
egs_zones:leave(User2#users.zonepid, User2#users.gid),
NewLID = egs_zones:enter(ZonePid, User2#users.gid),
NewState = State#state{lid=NewLID},
NewClient = Client#client{lid=NewLID},
{ok, User3} = egs_users:read(User2#users.gid),
psu_proto:send_0a05(NewState),
psu_proto:send_0111(User3, 6, NewState),
psu_proto:send_010d(User3, NewState),
psu_proto:send_0200(ZoneID, AreaType, NewState),
psu_proto:send_020f(egs_quests_db:zone_nbl(QuestID, ZoneID), egs_zones:setid(ZonePid), SeasonID, NewState),
NewState;
psu_proto:send_0a05(NewClient),
psu_proto:send_0111(User3, 6, NewClient),
psu_proto:send_010d(User3, NewClient),
psu_proto:send_0200(ZoneID, AreaType, NewClient),
psu_proto:send_020f(egs_quests_db:zone_nbl(QuestID, ZoneID), egs_zones:setid(ZonePid), SeasonID, NewClient),
NewClient;
true ->
User3 = User2,
State
Client
end,
%% Save the user.
egs_users:write(User3),
%% Load the player location.
State2 = State1#state{areanb=State#state.areanb + 1},
psu_proto:send_0205(User3, IsSeasonal, State2),
psu_proto:send_100e(User3#users.area, User3#users.entryid, AreaShortName, State2),
Client2 = Client1#client{areanb=Client#client.areanb + 1},
psu_proto:send_0205(User3, IsSeasonal, Client2),
psu_proto:send_100e(User3#users.area, User3#users.entryid, AreaShortName, Client2),
%% Load the zone objects.
if ZoneChange ->
send_1212(); %% @todo Only sent if there is a set file.
true -> ignore
end,
%% Load the player.
psu_proto:send_0201(User3, State2),
psu_proto:send_0201(User3, Client2),
if ZoneChange ->
psu_proto:send_0a06(User3, State2),
psu_proto:send_0a06(User3, Client2),
%% Load the other players in the zone.
OtherPlayersGID = egs_zones:get_all_players(User3#users.zonepid, User3#users.gid),
if OtherPlayersGID =:= [] -> ignore;
true ->
OtherPlayers = egs_users:select(OtherPlayersGID),
psu_proto:send_0233(OtherPlayers, State)
psu_proto:send_0233(OtherPlayers, Client)
end;
true -> ignore
end,
%% End of loading.
State3 = State2#state{areanb=State2#state.areanb + 1},
psu_proto:send_0208(State3),
psu_proto:send_0236(State3),
Client3 = Client2#client{areanb=Client2#client.areanb + 1},
psu_proto:send_0208(Client3),
psu_proto:send_0236(Client3),
%% @todo Load APC characters.
{ok, State3}.
{ok, Client3}.
%% @todo Don't change the NPC info unless you are the leader!
npc_load(_Leader, [], _State) ->
npc_load(_Leader, [], _Client) ->
ok;
npc_load(Leader, [{PartyPos, NPCGID}|NPCList], State) ->
npc_load(Leader, [{PartyPos, NPCGID}|NPCList], Client) ->
{ok, OldNPCUser} = egs_users:read(NPCGID),
#users{instancepid=InstancePid, area=Area, entryid=EntryID, pos=Pos} = Leader,
NPCUser = OldNPCUser#users{lid=PartyPos, instancepid=InstancePid, areatype=mission, area=Area, entryid=EntryID, pos=Pos},
%% @todo This one on mission end/abort?
%~ OldNPCUser#users{lid=PartyPos, instancepid=undefined, areatype=AreaType, area={0, 0, 0}, entryid=0, pos={0.0, 0.0, 0.0, 0}}
egs_users:write(NPCUser),
psu_proto:send_010d(NPCUser, State),
psu_proto:send_0201(NPCUser, State),
psu_proto:send_0215(0, State),
psu_proto:send_010d(NPCUser, Client),
psu_proto:send_0201(NPCUser, Client),
psu_proto:send_0215(0, Client),
send_0a04(NPCUser#users.gid),
send_1004(npc_mission, NPCUser, PartyPos),
send_100f((NPCUser#users.character)#characters.npcid, PartyPos),
send_1601(PartyPos),
send_1016(PartyPos),
npc_load(Leader, NPCList, State).
npc_load(Leader, NPCList, Client).
%% @doc Build the packet header.
header(Command) ->

View File

@ -1196,7 +1196,7 @@ parse_hits(Hits, Acc) ->
%% @doc Send character appearance and other information.
%% @todo Probably don't pattern match the data like this...
send_010d(CharUser, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_010d(CharUser, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
CharGID = CharUser#users.gid,
CharLID = CharUser#users.lid,
<< _:640, CharBin/bits >> = psu_characters:character_user_to_binary(CharUser),
@ -1205,16 +1205,16 @@ send_010d(CharUser, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
0:192, CharGID:32/little, CharLID:32/little, 16#ffffffff:32, CharBin/binary >>).
%% @doc Trigger a character-related event.
send_0111(CharUser, EventID, State) ->
send_0111(CharUser, EventID, 0, State).
send_0111(#users{gid=CharGID, lid=CharLID}, EventID, Param, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0111(CharUser, EventID, Client) ->
send_0111(CharUser, EventID, 0, Client).
send_0111(#users{gid=CharGID, lid=CharLID}, EventID, Param, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
packet_send(Socket, << 16#01110300:32, DestLID:16/little, 0:48, CharGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64,
CharGID:32/little, CharLID:32/little, EventID:32/little, Param:32/little >>).
%% @doc Update the character level, blastbar, luck and money information.
send_0115(CharUser, State) ->
send_0115(CharUser, 16#ffffffff, State).
send_0115(#users{gid=CharGID, lid=CharLID, character=Character}, EnemyTargetID, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0115(CharUser, Client) ->
send_0115(CharUser, 16#ffffffff, Client).
send_0115(#users{gid=CharGID, lid=CharLID, character=Character}, EnemyTargetID, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
packet_send(Socket, << 16#01150300:32, DestLID:16/little, 0:48, CharGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64,
CharGID:32/little, CharLID:32/little, EnemyTargetID:32/little, (build_char_level(Character))/binary >>).
@ -1234,14 +1234,14 @@ build_char_level(#characters{type=Type, mainlevel=#level{number=Level, exp=EXP},
%% @doc Revive player with optional SEs.
%% @todo SEs.
send_0117(#users{gid=CharGID, lid=CharLID, character=#characters{currenthp=HP}}, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0117(#users{gid=CharGID, lid=CharLID, character=#characters{currenthp=HP}}, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
SE = << 0:64 >>,
packet_send(Socket, << 16#01170300:32, DestLID:16/little, 0:48, CharGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64,
CharGID:32/little, CharLID:32/little, SE/binary, HP:32/little, 0:32 >>).
%% @doc Send the zone initialization command.
%% @todo Handle NbPlayers properly. There's more than 1 player!
send_0200(ZoneID, ZoneType, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0200(ZoneID, ZoneType, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
Var = case ZoneType of
mission -> << 16#06000500:32, 16#01000000:32, 0:64, 16#00040000:32, 16#00010000:32, 16#00140000:32 >>;
myroom -> << 16#06000000:32, 16#02000000:32, 0:64, 16#40000000:32, 16#00010000:32, 16#00010000:32 >>;
@ -1251,7 +1251,7 @@ send_0200(ZoneID, ZoneType, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
DestLID:16/little, ZoneID:16/little, 1:32/little, 16#ffffffff:32, Var/binary, 16#ffffffff:32, 16#ffffffff:32 >>).
%% @doc Send character location, appearance and other information.
send_0201(CharUser, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0201(CharUser, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
[CharTypeID, GameVersion] = case (CharUser#users.character)#characters.type of
npc -> [16#00001d00, 255];
_ -> [16#00001200, 0]
@ -1265,17 +1265,17 @@ send_0201(CharUser, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
%% @doc Hello command. Sent when a client connects to the game or login server.
%% @todo Can contain an error message if 0:1024 is setup similar to this: 0:32, 3:32/little, 0:48, Len:16/little, Error/binary, 0:Padding.
send_0202(#state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0202(#client{socket=Socket, gid=DestGID, lid=DestLID}) ->
packet_send(Socket, << 16#020203bf:32, DestLID:16/little, 0:272, DestGID:32/little, 0:1024 >>).
%% @doc Spawn a player with the given GID and LID.
send_0203(#users{gid=CharGID, lid=CharLID}, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0203(#users{gid=CharGID, lid=CharLID}, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
packet_send(Socket, << 16#02030300:32, DestLID:16/little, 0:144, 16#00011300:32,
DestGID:32/little, 0:64, CharGID:32/little, CharLID:32/little >>).
%% @doc Unspawn the given character.
%% @todo The last 4 bytes are probably the number of players remaining in the zone.
send_0204(User, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0204(User, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
CharTypeID = case (User#users.character)#characters.type of
npc -> 16#00001d00;
_ -> 16#00001200
@ -1285,51 +1285,51 @@ send_0204(User, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
16#00011300:32, DestGID:32/little, 0:64, CharGID:32/little, CharLID:32/little, 100:32/little >>).
%% @doc Make the client load a new map.
send_0205(CharUser, IsSeasonal, #state{socket=Socket, gid=DestGID, lid=DestLID, areanb=AreaNb}) ->
send_0205(CharUser, IsSeasonal, #client{socket=Socket, gid=DestGID, lid=DestLID, areanb=AreaNb}) ->
#users{lid=CharLID, area={_QuestID, ZoneID, MapID}, entryid=EntryID} = CharUser,
packet_send(Socket, << 16#02050300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
16#ffffffff:32, ZoneID:32/little, MapID:32/little, EntryID:32/little, AreaNb:32/little, CharLID:16/little, 0:8, IsSeasonal:8 >>).
%% @doc Indicate to the client that loading should finish.
send_0208(#state{socket=Socket, gid=DestGID, lid=DestLID, areanb=AreaNb}) ->
send_0208(#client{socket=Socket, gid=DestGID, lid=DestLID, areanb=AreaNb}) ->
packet_send(Socket, << 16#02080300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64, AreaNb:32/little >>).
%% @todo No idea what this one does. For unknown reasons it uses channel 2.
%% @todo Handle the DestLID properly?
send_020c(#state{socket=Socket}) ->
send_020c(#client{socket=Socket}) ->
packet_send(Socket, << 16#020c0200:32, 16#ffff0000:32, 0:256 >>).
%% @doc Send the quest file to be loaded by the client.
%% @todo Handle the DestLID properly?
send_020e(QuestData, #state{socket=Socket}) ->
send_020e(QuestData, #client{socket=Socket}) ->
Size = byte_size(QuestData),
packet_send(Socket, << 16#020e0300:32, 16#ffff:16, 0:272, Size:32/little, 0:32, QuestData/binary, 0:32 >>).
%% @doc Send the zone file to be loaded.
send_020f(ZoneData, SetID, SeasonID, #state{socket=Socket}) ->
send_020f(ZoneData, SetID, SeasonID, #client{socket=Socket}) ->
Size = byte_size(ZoneData),
packet_send(Socket, << 16#020f0300:32, 16#ffff:16, 0:272, SetID, SeasonID, 0:16, Size:32/little, ZoneData/binary >>).
%% @doc Send the current UNIX time.
send_0210(#state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0210(#client{socket=Socket, gid=DestGID, lid=DestLID}) ->
UnixTime = calendar:datetime_to_gregorian_seconds(calendar:now_to_universal_time(now()))
- calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}),
packet_send(Socket, << 16#02100300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:96, UnixTime:32/little >>).
%% @todo No idea what this is doing.
send_0215(UnknownValue, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0215(UnknownValue, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
packet_send(Socket, << 16#02150300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64, UnknownValue:32/little >>).
%% @doc Send the game server's IP and port that the client requested.
send_0216(IP, Port, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0216(IP, Port, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
packet_send(Socket, << 16#02160300:32, DestLID:16/little, 0:144, 16#00000f00:32, DestGID:32/little, 0:64, IP/binary, Port:16/little, 0:16 >>).
%% @doc End of character loading.
send_021b(#state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_021b(#client{socket=Socket, gid=DestGID, lid=DestLID}) ->
packet_send(Socket, << 16#021b0300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64 >>).
%% @doc Send the list of available universes.
send_021e(Universes, #state{socket=Socket}) ->
send_021e(Universes, #client{socket=Socket}) ->
NbUnis = length(Universes),
UnisBin = build_021e_uni(Universes, []),
packet_send(Socket, << 16#021e0300:32, 0:288, NbUnis:32/little, UnisBin/binary >>).
@ -1348,7 +1348,7 @@ build_021e_uni([{UniID, {universe, Name, NbPlayers, _MaxPlayers}}|Tail], Acc) ->
build_021e_uni(Tail, [Bin|Acc]).
%% @doc Send the current universe info along with the current level cap.
send_0222(UniID, #state{socket=Socket, gid=DestGID}) ->
send_0222(UniID, #client{socket=Socket, gid=DestGID}) ->
{_Type, Name, NbPlayers, MaxPlayers} = egs_universes:read(UniID),
Padding = 8 * (44 - byte_size(Name)),
LevelCap = egs_conf:read(level_cap),
@ -1356,14 +1356,14 @@ send_0222(UniID, #state{socket=Socket, gid=DestGID}) ->
UniID:32/little, NbPlayers:16/little, MaxPlayers:16/little, Name/binary, 0:Padding, LevelCap:32/little >>).
%% @doc Send the auth key, or, in case of failure, a related error message.
send_0223(AuthGID, AuthKey, #state{socket=Socket, gid=DestGID}) ->
send_0223(AuthGID, AuthKey, #client{socket=Socket, gid=DestGID}) ->
packet_send(Socket, << 16#02230300:32, 0:160, 16#00000f00:32, DestGID:32/little, 0:64, AuthGID:32/little, AuthKey:32/bits >>).
send_0223(ErrorMsg, #state{socket=Socket, gid=DestGID}) ->
send_0223(ErrorMsg, #client{socket=Socket, gid=DestGID}) ->
Length = byte_size(ErrorMsg) div 2 + 2,
packet_send(Socket, << 16#02230300:32, 0:160, 16#00000f00:32, DestGID:32/little, 0:128, 3:32/little, 0:48, Length:16/little, ErrorMsg/binary, 0:16 >>).
%% @doc Send a MOTD page.
send_0225(MOTD, CurrentPage, #state{socket=Socket, lid=DestLID}) ->
send_0225(MOTD, CurrentPage, #client{socket=Socket, lid=DestLID}) ->
Tokens = re:split(MOTD, "\n."),
Msg = << << Line/binary, "\n", 0 >> || Line <- lists:sublist(Tokens, 1 + CurrentPage * 15, 15) >>,
NbPages = 1 + length(Tokens) div 15,
@ -1376,18 +1376,18 @@ send_0225(MOTD, CurrentPage, #state{socket=Socket, lid=DestLID}) ->
%% * top: Horizontal scroll on top of the screen, traditionally used for server-wide messages.
%% * scroll: Vertical scroll on the right of the screen, traditionally used for rare missions obtention messages.
%% * timeout: A dialog in the center of the screen that disappears after Duration seconds.
send_0228(Type, Duration, Message, #state{socket=Socket, gid=DestGID}) ->
send_0228(Type, Duration, Message, #client{socket=Socket, gid=DestGID}) ->
TypeInt = case Type of dialog -> 0; top -> 1; scroll -> 2; timeout -> 3 end,
UCS2Message = << << X:8, 0:8 >> || X <- Message >>,
packet_send(Socket, << 16#02280300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
TypeInt:32/little, Duration:32/little, UCS2Message/binary, 0:16 >>).
%% @todo Not sure. Sent when going to or from room. Possibly when changing universes too?
send_0230(#state{socket=Socket, gid=DestGID}) ->
send_0230(#client{socket=Socket, gid=DestGID}) ->
packet_send(Socket, << 16#02300300:32, 16#ffff:16, 0:16, 16#00011300:32, DestGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64 >>).
%% @doc Forward the player to a website. The website will open when the player closes the game. Used for login issues mostly.
send_0231(URL, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0231(URL, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
URLBin = list_to_binary(URL),
Length = byte_size(URLBin) + 1,
Padding = 8 * (512 - Length - 1),
@ -1395,7 +1395,7 @@ send_0231(URL, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
16#00000f00:32, DestGID:32/little, 0:64, Length:32/little, URLBin/binary, 0:Padding >>).
%% @doc Send the list of players already spawned in the zone when entering it.
send_0233(Users, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0233(Users, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
NbUsers = length(Users),
Bin = build_0233_users(Users, []),
packet_send(Socket, << 16#02330300:32, DestLID:16/little, 0:16, 16#00001200:32, DestGID:32/little, 0:64,
@ -1408,22 +1408,22 @@ build_0233_users([User|Tail], Acc) ->
build_0233_users(Tail, [<< Bin/binary, 0:32 >>|Acc]).
%% @doc Start the zone handling: load the zone file and the objects sent separately.
send_0236(#state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0236(#client{socket=Socket, gid=DestGID, lid=DestLID}) ->
packet_send(Socket, << 16#02360300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64 >>).
%% @doc Chat message.
send_0304(FromGID, ChatTypeID, ChatGID, ChatName, ChatModifiers, ChatMessage, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0304(FromGID, ChatTypeID, ChatGID, ChatName, ChatModifiers, ChatMessage, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
{chat_modifiers, ChatType, ChatCutIn, ChatCutInAngle, ChatMsgLength, ChatChannel, ChatCharacterType} = ChatModifiers,
packet_send(Socket, << 16#03040300:32, DestLID:16/little, 0:16, 16#00011300:32, FromGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64,
ChatTypeID:32, ChatGID:32/little, 0:64, ChatType:8, ChatCutIn:8, ChatCutInAngle:8, ChatMsgLength:8,
ChatChannel:8, ChatCharacterType:8, 0:16, ChatName/binary, ChatMessage/binary >>).
%% @todo Inventory related. Doesn't seem to do anything.
send_0a05(#state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0a05(#client{socket=Socket, gid=DestGID, lid=DestLID}) ->
packet_send(Socket, << 16#0a050300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64 >>).
%% @doc Send the list of ItemUUID for the items in the inventory.
send_0a06(CharUser, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0a06(CharUser, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
Len = length((CharUser#users.character)#characters.inventory),
UUIDs = lists:seq(1, Len),
Bin = iolist_to_binary([ << N:32/little >> || N <- UUIDs]),
@ -1432,14 +1432,14 @@ send_0a06(CharUser, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
packet_send(Socket, << 16#0a060300:32, DestLID:16/little, 0:48, DestGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64, Bin/binary, Bin2/binary >>).
%% @doc Send an item's description.
send_0a11(ItemID, ItemDesc, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0a11(ItemID, ItemDesc, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
Length = 1 + byte_size(ItemDesc) div 2,
packet_send(Socket, << 16#0a110300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
ItemID:32, Length:32/little, ItemDesc/binary, 0:16 >>).
%% @doc Quest init.
%% @todo When first entering a zone it seems LID should be set to ffff apparently.
send_0c00(CharUser, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0c00(CharUser, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
#users{area={QuestID, _ZoneID, _MapID}} = CharUser,
packet_send(Socket, << 16#0c000300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64, QuestID:32/little,
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32,
@ -1448,21 +1448,21 @@ send_0c00(CharUser, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>).
%% @doc Send the huge pack of quest files available in the counter.
send_0c06(Pack, #state{socket=Socket}) ->
send_0c06(Pack, #client{socket=Socket}) ->
packet_send(Socket, << 16#0c060300:32, 0:288, 1:32/little, Pack/binary >>).
%% @doc Reply that the player is allowed to use the lobby transport. Always allow.
send_0c08(#state{socket=Socket, gid=DestGID}) ->
send_0c08(#client{socket=Socket, gid=DestGID}) ->
packet_send(Socket, << 16#0c080300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:96 >>).
%% @doc Send the counter's mission options (0 = invisible, 2 = disabled, 3 = available).
send_0c10(Options, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_0c10(Options, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
Size = byte_size(Options),
packet_send(Socket, << 16#0c100300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64, 1, 0, Size:16/little, Options/binary >>).
%% @doc Send the general data and flags for the selected character.
%% @todo Handle bitflags and value flags properly.
send_0d01(Character, #state{socket=Socket, gid=DestGID}) ->
send_0d01(Character, #client{socket=Socket, gid=DestGID}) ->
CharBin = psu_characters:character_tuple_to_binary(Character),
OptionsBin = psu_characters:options_tuple_to_binary(Character#characters.options),
packet_send(Socket, << 16#0d010300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, CharBin/binary,
@ -1472,7 +1472,7 @@ send_0d01(Character, #state{socket=Socket, gid=DestGID}) ->
%% @doc Send the flags list. This is the whole list of available values, not the character's.
%% Sent without fragmentation on official for unknown reasons. Do the same here.
send_0d05(#state{socket=Socket, gid=DestGID}) ->
send_0d05(#client{socket=Socket, gid=DestGID}) ->
{ok, Flags} = file:read_file("p/flags.bin"),
Packet = << 16#0d050300:32, 0:32, 16#00011300:32, DestGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64, Flags/binary >>,
Size = 4 + byte_size(Packet),
@ -1480,7 +1480,7 @@ send_0d05(#state{socket=Socket, gid=DestGID}) ->
%% @doc Send the client's own player's party information, on the bottom left of the screen.
%% @todo Location and the 20 bytes following sometimes have values, not sure why; when joining a party maybe?
send_1005(Character, #state{socket=Socket, gid=DestGID}) ->
send_1005(Character, #client{socket=Socket, gid=DestGID}) ->
#characters{name=Name, mainlevel=#level{number=Level}, currenthp=CurrentHP, maxhp=MaxHP} = Character,
Location = << 0:512 >>,
packet_send(Socket, << 16#10050300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
@ -1496,9 +1496,9 @@ send_1005(Character, #state{socket=Socket, gid=DestGID}) ->
16#ffff0000:32, 16#ffff0000:32, 16#ffff0000:32, 0:3680 >>).
%% @doc Party-related events.
send_1006(EventID, State) ->
send_1006(EventID, 0, State).
send_1006(EventID, PartyPos, #state{socket=Socket, gid=DestGID}) ->
send_1006(EventID, Client) ->
send_1006(EventID, 0, Client).
send_1006(EventID, PartyPos, #client{socket=Socket, gid=DestGID}) ->
packet_send(Socket, << 16#10060300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, EventID:8, PartyPos:8, 0:16 >>).
%% @doc Send the player's current location.
@ -1506,14 +1506,14 @@ send_1006(EventID, PartyPos, #state{socket=Socket, gid=DestGID}) ->
%% @todo Receive the AreaName as UCS2 directly to allow for color codes and the like.
%% @todo Handle TargetLID probably (right after the padding).
%% @todo Do counters even have a name?
send_100e(CounterID, AreaName, #state{socket=Socket, gid=DestGID}) ->
send_100e(CounterID, AreaName, #client{socket=Socket, gid=DestGID}) ->
PartyPos = 0,
UCS2Name = << << X:8, 0:8 >> || X <- AreaName >>,
Padding = 8 * (64 - byte_size(UCS2Name)),
CounterType = if CounterID =:= 16#ffffffff -> 2; true -> 1 end,
packet_send(Socket, << 16#100e0300:32, 16#ffffffbf:32, 0:128, 16#00011300:32, DestGID:32, 0:64,
1, PartyPos, 0:48, 16#ffffff7f:32, UCS2Name/binary, 0:Padding, 0:32, CounterID:32/little, CounterType:32/little >>).
send_100e({QuestID, ZoneID, MapID}, EntryID, AreaName, #state{socket=Socket, gid=DestGID}) ->
send_100e({QuestID, ZoneID, MapID}, EntryID, AreaName, #client{socket=Socket, gid=DestGID}) ->
PartyPos = 0,
UCS2Name = << << X:8, 0:8 >> || X <- AreaName >>,
Padding = 8 * (64 - byte_size(UCS2Name)),
@ -1522,22 +1522,22 @@ send_100e({QuestID, ZoneID, MapID}, EntryID, AreaName, #state{socket=Socket, gid
UCS2Name/binary, 0:Padding, 0:32, 16#ffffffff:32, 0:32 >>).
%% @doc Mission start related.
send_1020(#state{socket=Socket, gid=DestGID}) ->
send_1020(#client{socket=Socket, gid=DestGID}) ->
packet_send(Socket, << 16#10200300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64 >>).
%% @doc Update HP in the party members information on the left.
%% @todo Handle PartyPos. Probably only pass HP later.
send_1022(#users{character=#characters{currenthp=HP}}, #state{socket=Socket, gid=DestGID}) ->
send_1022(#users{character=#characters{currenthp=HP}}, #client{socket=Socket, gid=DestGID}) ->
PartyPos = 0,
packet_send(Socket, << 16#10220300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, HP:32/little, PartyPos:32/little >>).
%% @todo Always the same value, no idea what it's for.
send_1204(#state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_1204(#client{socket=Socket, gid=DestGID, lid=DestLID}) ->
packet_send(Socket, << 16#12040300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:96, 16#20000000:32, 0:256 >>).
%% @doc Send the player's partner card.
%% @todo Handle the LID and comment properly.
send_1500(Character, #state{socket=Socket, gid=DestGID}) ->
send_1500(Character, #client{socket=Socket, gid=DestGID}) ->
#characters{slot=Slot, name=Name, race=Race, gender=Gender, class=Class, appearance=Appearance} = Character,
case Appearance of
#flesh_appearance{voicetype=VoiceType, voicepitch=VoicePitch} -> ok;
@ -1554,20 +1554,20 @@ send_1500(Character, #state{socket=Socket, gid=DestGID}) ->
%% @doc Send the list of parties to join.
%% @todo Handle lists of parties.
%% @todo Probably has to handle a LID here, although it should always be 0.
send_1701(#state{socket=Socket, gid=DestGID}) ->
send_1701(#client{socket=Socket, gid=DestGID}) ->
packet_send(Socket, << 16#17010300:32, 0:160, 16#00011300:32, DestGID:32/little, 0:96 >>).
%% @doc Send the background to use for the counter.
send_1711(Bg, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_1711(Bg, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
packet_send(Socket, << 16#17110300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64, Bg:8, 0:24 >>).
%% @doc NPC shop request reply.
send_1a02(A, B, C, D, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_1a02(A, B, C, D, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
packet_send(Socket, << 16#1a020300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:96,
A:16/little, B:16/little, C:16/little, D:16/little >>).
%% @doc Lumilass available hairstyles/headtypes handler.
send_1a03(CharUser, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_1a03(CharUser, #client{socket=Socket, gid=DestGID, lid=DestLID}) ->
{ok, Conf} = file:consult("priv/lumilass.conf"),
Character = CharUser#users.character,
NbHeadtypes = proplists:get_value({headtypes, Character#characters.gender, Character#characters.race}, Conf, 0),
@ -1578,7 +1578,7 @@ send_1a03(CharUser, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
NbHairstyles:32/little, NbHeadtypes:32/little, 0:416, HairstylesBin/binary, 0:32 >>).
%% @doc Available types handler. Enable all 16 types.
send_1a07(#state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_1a07(#client{socket=Socket, gid=DestGID, lid=DestLID}) ->
packet_send(Socket, << 16#1a070300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:160,
16#01010101:32, 16#01010101:32, 16#01010101:32, 16#01010101:32 >>).