Introduce egs_universes for universe handling. Review and move send_021e and send_0222 to psu_proto.

This commit is contained in:
Loïc Hoguin 2010-10-21 17:00:30 +02:00
parent a6563c7378
commit c91880be1f
12 changed files with 202 additions and 80 deletions

View File

@ -77,7 +77,7 @@
%% General information.
id, lid, pid, time, character,
%% Location/state related information.
instancepid, partypid, areatype, area, entryid, pos=#pos{x=0.0, y=0.0, z=0.0, dir=0.0}, shopid,
uni, instancepid, partypid, areatype, area, entryid, pos=#pos{x=0.0, y=0.0, z=0.0, dir=0.0}, shopid,
prev_area=#psu_area{questid=0, zoneid=0, mapid=0}, prev_entryid=0, %% universeid
%% To be moved or deleted later on.
setid=0 %% @todo Current area's set number. Move that to psu_instance probably.

View File

@ -28,3 +28,8 @@
%% They can be modified freely without problem.
%% Note that the port should be available and above 1024.
{game_server, {<< 127, 0, 0, 1 >>, 12061}}.
%% Caps and limitations.
%% @doc Maximum level players can reach.
{level_cap, 200}.

Binary file not shown.

Binary file not shown.

View File

@ -64,7 +64,7 @@ event({char_select_create, Slot, CharBin}, #state{gid=GID}) ->
file:write_file(File, CharBin),
file:write_file(io_lib:format("~s.options", [File]), << 0:128, 4, 0:56 >>);
%% @doc Load the selected character into the game's universe.
%% @doc Load the selected character into the game's default universe.
event({char_select_enter, Slot, _BackToPreviousField}, State=#state{gid=GID}) ->
{ok, User} = egs_user_model:read(GID),
Folder = egs_accounts:get_folder(GID),
@ -76,7 +76,9 @@ event({char_select_enter, Slot, _BackToPreviousField}, State=#state{gid=GID}) ->
Appearance = psu_appearance:binary_to_tuple(Race, AppearanceBin),
Options = psu_characters:options_binary_to_tuple(OptionsBin),
Character = #characters{slot=Slot, name=Name, race=Race, gender=Gender, class=Class, appearance=Appearance, options=Options}, % TODO: temporary set the slot here, won't be needed later
User2 = User#egs_user_model{character=Character, area=#psu_area{questid=1100000, zoneid=7, mapid=9202}, entryid=0},
UniID = egs_universes:defaultid(),
egs_universes:enter(UniID),
User2 = User#egs_user_model{uni=UniID, character=Character, area=#psu_area{questid=1100000, zoneid=7, mapid=9202}, entryid=0},
egs_user_model:write(User2),
egs_user_model:item_add(GID, 16#11010000, #psu_special_item_variables{}),
egs_user_model:item_add(GID, 16#11020000, #psu_special_item_variables{}),
@ -87,7 +89,6 @@ event({char_select_enter, Slot, _BackToPreviousField}, State=#state{gid=GID}) ->
egs_user_model:item_add(GID, 16#01010b00, #psu_striking_weapon_item_variables{current_pp=99, max_pp=100, element=#psu_element{type=3, percent=50}}),
{ok, User3} = egs_user_model:read(GID),
State2 = State#state{slot=Slot},
mnesia:dirty_update_counter(counters, population, 1),
psu_game:char_load(User3, State2),
{ok, egs_game, State2}.

View File

@ -199,7 +199,7 @@ raw(Command, _Data, State) ->
%% Events.
%% @todo When changing lobby to the room, 0230 must also be sent. Same when going from room to lobby.
%% @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);
@ -751,44 +751,37 @@ event(player_type_capabilities_request, _State) ->
event(ppcube_request, _State) ->
psu_game:send_1a04();
%% @doc Uni cube handler.
event(unicube_request, _State) ->
psu_game:send_021e();
event(unicube_request, State) ->
psu_proto:send_021e(egs_universes:all(), State);
%% @doc Uni selection handler. Selecting anything reloads the character which will then be sent to its associated area.
%% @todo When selecting 'Your room', load a default room.
%% @todo When selecting 'Reload', reload the character in the current lobby.
%% @todo Delete NPC characters and stop the party on entering myroom too.
%% @todo When selecting 'Your room', don't load a default room that's not yours.
event({unicube_select, cancel, _EntryID}, _State) ->
ignore;
event({unicube_select, Selection, EntryID}, State=#state{gid=GID}) ->
{ok, User} = egs_user_model:read(GID),
case Selection of
cancel -> ignore;
16#ffffffff ->
log("uni selection (my room)"),
psu_proto:send_0230(State),
% 0220
{ok, User} = egs_user_model:read(GID),
User2 = User#egs_user_model{area=#psu_area{questid=1120000, zoneid=0, mapid=100}, entryid=0},
egs_user_model:write(User2),
psu_game:char_load(User2, State);
_UniID ->
log("uni selection (reload)"),
psu_proto:send_0230(State),
% 0220
{ok, User} = egs_user_model:read(GID),
case User#egs_user_model.partypid of
undefined ->
ignore;
PartyPid ->
%% @todo Replace stop by leave when leaving stops the party correctly when nobody's there anymore.
%~ psu_party:leave(User#egs_user_model.partypid, User#egs_user_model.id)
{ok, NPCList} = psu_party:get_npc(PartyPid),
[egs_user_model:delete(NPCGID) || {_Spot, NPCGID} <- NPCList],
psu_party:stop(PartyPid)
end,
User2 = User#egs_user_model{entryid=EntryID},
egs_user_model:write(User2),
psu_game:char_load(User2, State)
end.
UniID = egs_universes:myroomid(),
User2 = User#egs_user_model{uni=UniID, area=#psu_area{questid=1120000, zoneid=0, mapid=100}, entryid=0};
_ ->
UniID = Selection,
User2 = User#egs_user_model{uni=UniID, entryid=EntryID}
end,
psu_proto:send_0230(State),
%% 0220
case User#egs_user_model.partypid of
undefined -> ignore;
PartyPid ->
%% @todo Replace stop by leave when leaving stops the party correctly when nobody's there anymore.
%~ psu_party:leave(User#egs_user_model.partypid, User#egs_user_model.id)
{ok, NPCList} = psu_party:get_npc(PartyPid),
[egs_user_model:delete(NPCGID) || {_Spot, NPCGID} <- NPCList],
psu_party:stop(PartyPid)
end,
egs_user_model:write(User2),
egs_universes:leave(User#egs_user_model.uni),
egs_universes:enter(UniID),
psu_game:char_load(User2, State).
%% Internal.

View File

@ -37,7 +37,6 @@ start_link(Port) ->
on_exit(Pid) ->
case egs_user_model:read({pid, Pid}) of
{ok, User} ->
mnesia:dirty_update_counter(counters, population, -1),
case User#egs_user_model.partypid of
undefined ->
ignore;
@ -47,6 +46,7 @@ on_exit(Pid) ->
psu_party:stop(PartyPid)
end,
egs_user_model:delete(User#egs_user_model.id),
egs_universes:leave(User#egs_user_model.uni),
{ok, List} = egs_user_model:select({neighbors, User}),
lists:foreach(fun(Other) -> Other#egs_user_model.pid ! {egs, player_unspawn, User} end, List),
io:format("game (~p): quit~n", [User#egs_user_model.id]);

View File

@ -60,6 +60,7 @@ init([]) ->
{egs_shops_db, {egs_shops_db, start_link, []}, permanent, 5000, worker, dynamic},
{egs_accounts, {egs_accounts, start_link, []}, permanent, 5000, worker, dynamic},
{egs_counters, {egs_counters, start_link, []}, permanent, 5000, worker, dynamic},
{egs_universes, {egs_universes, start_link, []}, permanent, 5000, worker, dynamic},
{egs_user_model, {egs_user_model, start_link, []}, permanent, 5000, worker, dynamic},
{egs_game_server, {egs_game_server, start_link, [GamePort]}, permanent, 5000, worker, dynamic}
],

133
src/egs_universes.erl Normal file
View File

@ -0,0 +1,133 @@
%% @author Loïc Hoguin <essen@dev-extend.eu>
%% @copyright 2010 Loïc Hoguin.
%% @doc EGS universes handler.
%%
%% 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_universes).
-behavior(gen_server).
-export([start_link/0, stop/0, all/0, defaultid/0, enter/1, leave/1, myroomid/0, read/1, reload/0]). %% API.
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
%% Use the module name for the server's name.
-define(SERVER, ?MODULE).
%% Default universe IDs.
-define(MYROOM_ID, 21).
-define(DEFAULT_ID, 26).
%% API.
%% @spec start_link() -> {ok,Pid::pid()}
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%% @spec stop() -> stopped
stop() ->
gen_server:call(?SERVER, stop).
%% @spec all() -> term()
all() ->
gen_server:call(?SERVER, all).
%% @spec defaultid() -> 26
%% @doc Return the default universe, Uni 01, with ID 26.
defaultid() ->
?DEFAULT_ID.
%% @spec enter(UniID) -> term()
enter(UniID) ->
gen_server:cast(?SERVER, {enter, UniID}).
%% @spec leave(UniID) -> term()
leave(UniID) ->
gen_server:cast(?SERVER, {leave, UniID}).
%% @spec myroomid() -> 21
%% @doc Return the ID for the myroom universe.
myroomid() ->
?MYROOM_ID.
%% @spec read(UniID) -> term()
read(UniID) ->
gen_server:call(?SERVER, {read, UniID}).
%% @spec reload() -> ok
reload() ->
gen_server:cast(?SERVER, reload).
%% gen_server.
init([]) ->
{ok, [create_myroom()|create_unis()]}.
handle_call(all, _From, State) ->
{reply, State, State};
handle_call({read, UniID}, _From, State) ->
{reply, proplists:get_value(UniID, State), State};
handle_call(stop, _From, State) ->
{stop, normal, stopped, State};
handle_call(_Request, _From, State) ->
{reply, ignored, State}.
handle_cast({enter, UniID}, State) ->
{Type, Name, NbPlayers, MaxPlayers} = proplists:get_value(UniID, State),
State2 = proplists:delete(UniID, State),
State3 = [{UniID, {Type, Name, NbPlayers + 1, MaxPlayers}}|State2],
State4 = lists:keysort(1, State3),
{noreply, State4};
handle_cast({leave, UniID}, State) ->
{Type, Name, NbPlayers, MaxPlayers} = proplists:get_value(UniID, State),
State2 = proplists:delete(UniID, State),
State3 = [{UniID, {Type, Name, NbPlayers - 1, MaxPlayers}}|State2],
State4 = lists:keysort(1, State3),
{noreply, State4};
handle_cast(reload, _State) ->
{noreply, [create_myroom()|create_unis()]};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% Internal.
%% @doc Max players defaults to 5000 for now.
create_myroom() ->
{ok, << 16#fffe:16, MyRoomName/binary >>} = file:read_file("priv/universes/myroom.en_US.txt"),
{?MYROOM_ID, {myroom, MyRoomName, 0, 5000}}.
%% @doc Max players defaults to 1000 for now.
create_unis() ->
{ok, << 16#fffe:16, Universes/binary >>} = file:read_file("priv/universes/universes.en_US.txt"),
Universes2 = re:split(Universes, "\n."),
create_unis(Universes2, ?DEFAULT_ID, []).
create_unis([], _UniID, Acc) ->
lists:reverse(Acc);
create_unis([Name|Tail], UniID, Acc) ->
create_unis(Tail, UniID + 2, [{UniID, {universe, Name, 0, 1000}}|Acc]).

View File

@ -19,7 +19,7 @@
-module(egs_user_model).
-behavior(gen_server).
-export([start_link/0, stop/0, count/0, read/1, select/1, write/1, delete/1, item_nth/2, item_add/3, item_qty_add/3, shop_enter/2, shop_leave/1, shop_get/1, money_add/2]). %% API.
-export([start_link/0, stop/0, read/1, select/1, write/1, delete/1, item_nth/2, item_add/3, item_qty_add/3, shop_enter/2, shop_leave/1, shop_get/1, money_add/2]). %% API.
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
%% Use the module name for the server's name and for the table name.
@ -46,10 +46,6 @@ start_link() ->
stop() ->
gen_server:call(?SERVER, stop).
%% @spec count() -> {ok, Count}
count() ->
gen_server:call(?SERVER, count).
%% @spec read({pid, Pid}) -> {ok, User} | {error, badarg}
%% @spec read(ID) -> {ok, User} | {error, badarg}
read(ID) ->
@ -95,10 +91,6 @@ init([]) ->
error_logger:info_report("egs_user_model started"),
{ok, undefined}.
handle_call(count, _From, State) ->
Count = mnesia:dirty_update_counter(counters, population, 0),
{reply, {ok, Count}, State};
handle_call({read, {pid, Pid}}, _From, State) ->
List = do(qlc:q([X || X <- mnesia:table(?TABLE), X#?TABLE.pid =:= Pid])),
case List of

View File

@ -35,7 +35,7 @@ char_load(User, State) ->
send_1005((User#egs_user_model.character)#characters.name),
psu_proto:send_1006(12, State),
psu_proto:send_0210(State),
send_0222(),
psu_proto:send_0222(User#egs_user_model.uni, State),
send_1500(User),
send_1501(),
send_1512(),
@ -255,36 +255,6 @@ send_0204(DestUser, TargetUser, Action) ->
16#00011300:32, DestGID:32/little-unsigned-integer, 0:64, TargetGID:32/little-unsigned-integer,
TargetLID:32/little-unsigned-integer, Action:32/little-unsigned-integer >>).
%% @doc Send the list of available universes.
send_021e() ->
{ok, Count} = egs_user_model:count(),
[StrCount] = io_lib:format("~b", [Count]),
Unis = [{16#ffffffff, center, "Your Room", ""}, {1, justify, "Reload", " "}, {2, justify, "EGS Test", StrCount}],
NbUnis = length(Unis),
Bin = send_021e_build(Unis, []),
send(<< 16#021e0300:32, 0:288, NbUnis:32/little-unsigned-integer, Bin/binary >>).
send_021e_build([], Acc) ->
iolist_to_binary(lists:reverse(Acc));
send_021e_build([{ID, Align, Name, Pop}|Tail], Acc) ->
UCS2Name = << << X:8, 0:8 >> || X <- Name >>,
UCS2Pop = << << X:8, 0:8 >> || X <- Pop >>,
NamePadding = 8 * (32 - byte_size(UCS2Name)),
PopPadding = 8 * (12 - byte_size(UCS2Pop)),
IntAlign = case Align of justify -> 643; center -> 0 end,
send_021e_build(Tail, [<< ID:32/little-unsigned-integer, 0:16, IntAlign:16/little-unsigned-integer, UCS2Name/binary, 0:NamePadding, UCS2Pop/binary, 0:PopPadding >>|Acc]).
%% @doc Send the current universe name and number.
%% @todo Currently only have universe number 2, named EGS Test.
%% @todo We must have a parameter indicating whether this is a room or a normal universe.
send_0222() ->
UCS2Name = << << X:8, 0:8 >> || X <- "EGS Test" >>,
Padding = 8 * (44 - byte_size(UCS2Name)),
UniID = 2,
GID = get(gid),
send(<< 16#02220300:32, 16#ffff:16, 0:16, 16#00001200:32, GID:32/little, 0:64, 16#00011300:32, GID:32/little, 0:64,
UniID:32/little, 16#01009e02:32, UCS2Name/binary, 0:Padding, 16#aa000000:32 >>).
%% @todo No idea!
send_022c(A, B) ->
send(<< (header(16#022c))/binary, A:16/little-unsigned-integer, B:16/little-unsigned-integer >>).

View File

@ -1301,6 +1301,33 @@ send_0216(IP, Port, #state{socket=Socket, gid=DestGID, lid=DestLID}) ->
send_021b(#state{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}) ->
NbUnis = length(Universes),
UnisBin = build_021e_uni(Universes, []),
packet_send(Socket, << 16#021e0300:32, 0:288, NbUnis:32/little, UnisBin/binary >>).
build_021e_uni([], Acc) ->
iolist_to_binary(lists:reverse(Acc));
build_021e_uni([{_UniID, {myroom, Name, NbPlayers, _MaxPlayers}}|Tail], Acc) ->
Padding = 8 * (44 - byte_size(Name)),
Bin = << 16#ffffffff:32, NbPlayers:16/little, 0:16, Name/binary, 0:Padding >>,
build_021e_uni(Tail, [Bin|Acc]);
build_021e_uni([{UniID, {universe, Name, NbPlayers, _MaxPlayers}}|Tail], Acc) ->
Padding = 8 * (32 - byte_size(Name)),
PopString = lists:flatten(io_lib:format("~5b", [NbPlayers])),
PopString2 = << << X:8, 0:8 >> || X <- PopString >>,
Bin = << UniID:32/little, NbPlayers:16/little, 643:16/little, Name/binary, 0:Padding, PopString2/binary, 0:16 >>,
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}) ->
{_Type, Name, NbPlayers, MaxPlayers} = egs_universes:read(UniID),
Padding = 8 * (44 - byte_size(Name)),
LevelCap = egs_conf:read(level_cap),
packet_send(Socket, << 16#02220300:32, 16#ffff:16, 0:16, 16#00001200:32, DestGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64,
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}) ->
packet_send(Socket, << 16#02230300:32, 0:160, 16#00000f00:32, DestGID:32/little, 0:64, AuthGID:32/little, AuthKey:32/bits >>).