diff --git a/include/records.hrl b/include/records.hrl index 6a33ea4..a6901d4 100644 --- a/include/records.hrl +++ b/include/records.hrl @@ -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. diff --git a/priv/egs.conf b/priv/egs.conf index bbbab68..3d0b642 100644 --- a/priv/egs.conf +++ b/priv/egs.conf @@ -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}. diff --git a/priv/universes/myroom.en_US.txt b/priv/universes/myroom.en_US.txt new file mode 100644 index 0000000..555b053 Binary files /dev/null and b/priv/universes/myroom.en_US.txt differ diff --git a/priv/universes/universes.en_US.txt b/priv/universes/universes.en_US.txt new file mode 100644 index 0000000..680599b Binary files /dev/null and b/priv/universes/universes.en_US.txt differ diff --git a/src/egs_char_select.erl b/src/egs_char_select.erl index 6034fc2..8249203 100644 --- a/src/egs_char_select.erl +++ b/src/egs_char_select.erl @@ -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}. diff --git a/src/egs_game.erl b/src/egs_game.erl index 8d5d506..0308d71 100644 --- a/src/egs_game.erl +++ b/src/egs_game.erl @@ -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. diff --git a/src/egs_game_server.erl b/src/egs_game_server.erl index 580baed..5b8e18f 100644 --- a/src/egs_game_server.erl +++ b/src/egs_game_server.erl @@ -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]); diff --git a/src/egs_sup.erl b/src/egs_sup.erl index c093af3..533e581 100644 --- a/src/egs_sup.erl +++ b/src/egs_sup.erl @@ -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} ], diff --git a/src/egs_universes.erl b/src/egs_universes.erl new file mode 100644 index 0000000..fd98c83 --- /dev/null +++ b/src/egs_universes.erl @@ -0,0 +1,133 @@ +%% @author Loïc Hoguin +%% @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 . + +-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]). diff --git a/src/egs_user_model.erl b/src/egs_user_model.erl index 4185871..9b49a06 100644 --- a/src/egs_user_model.erl +++ b/src/egs_user_model.erl @@ -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 diff --git a/src/psu/psu_game.erl b/src/psu/psu_game.erl index 457026e..a1664fe 100644 --- a/src/psu/psu_game.erl +++ b/src/psu/psu_game.erl @@ -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 >>). diff --git a/src/psu/psu_proto.erl b/src/psu/psu_proto.erl index ff9f5c6..7ca3eca 100644 --- a/src/psu/psu_proto.erl +++ b/src/psu/psu_proto.erl @@ -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 >>).