diff --git a/include/maps.hrl b/include/maps.hrl index 8ba555b..38c3247 100644 --- a/include/maps.hrl +++ b/include/maps.hrl @@ -651,22 +651,6 @@ %% Background values include: 01 parum, 02 moatoob, 03 neudaiz, 04 guardians hq, 05 parum guardians, 06 moatoob guardians, 07 neudaiz guardians, 08 pitch black, ff destroyed colony -define(COUNTERS, [ - % Linear Line: Phantom Ruins, Unsafe Passage, Unsafe Passage (AOTI, missing) - - { 0, [{quests, "data/counters/colony.ll.pack"}, {bg, 255}, {options, << 16#01a92800:32, 3, 3, 0, - 0, 3, 0, 3, 0, % Phantom Ruins C-S - 3, 3, 3, 3, 3, % Unsafe Passage C-S2 variant 1 - 3, 3, 3, 3, 3, % Unsafe Passage C-S2 variant 2 - 3, 3, 3, 3, 3, % Unsafe Passage C-S2 variant 3 - 0:136 >>}]}, - - %~ { 0, [{quests, "data/counters/colony.ll.pack"}, {bg, 255}, {options, << 16#01a92800:32, 3, 3, 0, - %~ 3, 3, 3, 3, 0, % Phantom Ruins C-S - %~ 3, 3, 3, 3, 3, % Unsafe Passage C-S2 variant 1 - %~ 3, 3, 3, 3, 3, % Unsafe Passage C-S2 variant 2 - %~ 3, 3, 3, 3, 3, % Unsafe Passage C-S2 variant 3 - %~ 0:136 >>}]}, - % Space docks: Phantom Ruins, Dark Satellite, Familiar Trees (missing), Boss quest category (missing), Unit category (missing), Enemy category (missing) { 1, [{quests, "data/counters/colony.docks.pack"}, {bg, 255}, {options, << 16#01805400:32, 0, 3, 0, 0, 0, 0, diff --git a/lib/nbl.erl b/lib/nbl.erl index 0f76b36..b204222 100644 --- a/lib/nbl.erl +++ b/lib/nbl.erl @@ -23,17 +23,15 @@ %% @doc Pack an nbl file according to the given Options. %% Example usage: nbl:pack([{files, [{file, "table.rel", [16#184, 16#188, 16#1a0]}, {file, "text.bin", []}]}]). pack(Options) -> - OutFilename = proplists:get_value(out, Options, "unnamed.nbl"), Files = proplists:get_value(files, Options), {Header, Data, DataSize, PtrArray, PtrArraySize} = pack_files(Files), NbFiles = length(Files), HeaderSize = 16#30 + 16#60 * NbFiles, CompressedDataSize = 0, EncryptSeed = 0, - NBL = << $N, $M, $L, $L, 2:16/little, 16#1300:16, HeaderSize:32/little, NbFiles:32/little, + << $N, $M, $L, $L, 2:16/little, 16#1300:16, HeaderSize:32/little, NbFiles:32/little, DataSize:32/little, CompressedDataSize:32/little, PtrArraySize:32/little, EncryptSeed:32/little, - 0:128, Header/binary, Data/binary, PtrArray/binary >>, - file:write_file(OutFilename, NBL). + 0:128, Header/binary, Data/binary, PtrArray/binary >>. %% @doc Pack a list of files and return the header, data and pointer array parts. pack_files(Files) -> diff --git a/priv/counters/0/counter.conf b/priv/counters/0/counter.conf new file mode 100644 index 0000000..816685c --- /dev/null +++ b/priv/counters/0/counter.conf @@ -0,0 +1,53 @@ +%% 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 . + +%% Linear Line counter. + +{bg, 255}. +{cursor, {640, 425}}. +{t_name, 0}. +{t_desc, 1}. + +{groups, [ + %% Phantom Ruins. + [{t_name, 2}, {t_desc, 6}, {quests, [ + {1060301, "phantom-ruins.b.quest.nbl"}, + {1060303, "phantom-ruins.s.quest.nbl"} + ]}], + %% Unsafe Passage 1. + [{t_name, 3}, {t_desc, 6}, {quests, [ + {1000000, "unsafe-passage.1.c.quest.nbl"}, + {1000001, "unsafe-passage.1.b.quest.nbl"}, + {1000002, "unsafe-passage.1.a.quest.nbl"}, + {1000003, "unsafe-passage.1.s.quest.nbl"}, + {1000004, "unsafe-passage.1.s2.quest.nbl"} + ]}], + %% Unsafe Passage 2. + [{t_name, 4}, {t_desc, 6}, {quests, [ + {1000010, "unsafe-passage.2.c.quest.nbl"}, + {1000011, "unsafe-passage.2.b.quest.nbl"}, + {1000012, "unsafe-passage.2.a.quest.nbl"}, + {1000013, "unsafe-passage.2.s.quest.nbl"}, + {1000014, "unsafe-passage.2.s2.quest.nbl"} + ]}], + %% Unsafe Passage 3. + [{t_name, 5}, {t_desc, 6}, {quests, [ + {1000020, "unsafe-passage.3.c.quest.nbl"}, + {1000021, "unsafe-passage.3.b.quest.nbl"}, + {1000022, "unsafe-passage.3.a.quest.nbl"}, + {1000023, "unsafe-passage.3.s.quest.nbl"}, + {1000024, "unsafe-passage.3.s2.quest.nbl"} + ]}] +]}. diff --git a/priv/counters/0/text.bin.en_US.txt b/priv/counters/0/text.bin.en_US.txt new file mode 100644 index 0000000..b62b3e2 Binary files /dev/null and b/priv/counters/0/text.bin.en_US.txt differ diff --git a/priv/counters/README b/priv/counters/README new file mode 100644 index 0000000..6f3ec16 --- /dev/null +++ b/priv/counters/README @@ -0,0 +1,3 @@ +List of counters: + + 0 Linear Line diff --git a/src/egs.app.src b/src/egs.app.src index b33f524..5d7c256 100644 --- a/src/egs.app.src +++ b/src/egs.app.src @@ -12,6 +12,7 @@ egs_npc_db, egs_shops_db, egs_accounts, + egs_counters, egs_game_server, egs_login_server, egs_exit_mon, diff --git a/src/egs_counters.erl b/src/egs_counters.erl new file mode 100644 index 0000000..a2dc9ee --- /dev/null +++ b/src/egs_counters.erl @@ -0,0 +1,208 @@ +%% @author Loïc Hoguin +%% @copyright 2010 Loïc Hoguin. +%% @doc EGS counters database and cache manager. +%% +%% 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_counters). +-behavior(gen_server). +-export([start_link/0, stop/0, bg/1, opts/1, pack/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). + +%% 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 bg(CounterID) -> integer() +bg(CounterID) -> + gen_server:call(?SERVER, {bg, CounterID}). + +%% @spec opts(CounterID) -> binary() +opts(CounterID) -> + gen_server:call(?SERVER, {opts, CounterID}). + +%% @spec pack(CounterID) -> binary() +pack(CounterID) -> + gen_server:call(?SERVER, {pack, CounterID}). + +%% @spec reload() -> ok +reload() -> + gen_server:cast(?SERVER, reload). + +%% gen_server. + +init([]) -> + {ok, []}. + +%% @doc Possible keys: bg, opts, pack. +handle_call({Key, CounterID}, _From, State) -> + {Counter, State2} = get_counter(CounterID, State), + {reply, proplists:get_value(Key, Counter), State2}; + +handle_call(stop, _From, State) -> + {stop, normal, stopped, State}; + +handle_call(_Request, _From, State) -> + {reply, ignored, State}. + +handle_cast(reload, _State) -> + {noreply, []}; + +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 Build a counter's pack file, options and return them along with the background value. +build_counter(ConfFilename, CounterNbl) -> + {ok, Settings} = file:consult(ConfFilename), + Groups = proplists:get_value(groups, Settings), + {FilesBin, PosList, SizeList} = build_counter_groups(Groups, [0], [byte_size(CounterNbl)]), + NbFiles = length(PosList), + PosBin = iolist_to_binary([<< P:32/little >> || P <- PosList]), + SizeBin = iolist_to_binary([<< S:32/little >> || S <- SizeList]), + Padding = 8 * (512 - byte_size(PosBin)), + Pack = << PosBin/binary, 0:Padding, SizeBin/binary, 0:Padding, NbFiles:32/little, CounterNbl/binary, FilesBin/binary >>, + OptsList = lists:seq(1, length(Groups) + length(PosList)), + Opts = iolist_to_binary([<< 3:8 >> || _N <- OptsList]), + [{bg, proplists:get_value(bg, Settings)}, {opts, Opts}, {pack, Pack}]. + +build_counter_groups(Groups, PosList, SizeList) -> + build_counter_groups(Groups, PosList, SizeList, []). +build_counter_groups([], PosList, SizeList, Acc) -> + GroupsBin = iolist_to_binary(lists:reverse(Acc)), + {GroupsBin, lists:reverse(PosList), lists:reverse(SizeList)}; +build_counter_groups([Group|Tail], PosList, SizeList, Acc) -> + Quests = proplists:get_value(quests, Group), + {QuestsBin, PosList2, SizeList2} = build_counter_quests(Quests, PosList, SizeList), + build_counter_groups(Tail, PosList2, SizeList2, [QuestsBin|Acc]). + +build_counter_quests(Quests, PosList, SizeList) -> + build_counter_quests(Quests, PosList, SizeList, []). +build_counter_quests([], PosList, SizeList, Acc) -> + QuestsBin = iolist_to_binary(lists:reverse(Acc)), + {QuestsBin, PosList, SizeList}; +build_counter_quests([{_QuestID, Filename}|Tail], PosList, SizeList, Acc) -> + {ok, File} = file:read_file("data/missions/" ++ Filename), + Pos = lists:sum(SizeList), + Size = byte_size(File), + build_counter_quests(Tail, [Pos|PosList], [Size|SizeList], [File|Acc]). + +%% @doc Return a counter information either from the cache or from the configuration file, +%% in which case it gets added to the cache for subsequent attempts. +get_counter(CounterID, Cache) -> + case proplists:get_value(CounterID, Cache) of + undefined -> + Dir = io_lib:format("priv/counters/~b/", [CounterID]), + ConfFilename = Dir ++ "counter.conf", + {TableRelData, TableRelPtrs} = load_table_rel(ConfFilename), + TextBinData = load_text_bin(Dir ++ "text.bin.en_US.txt"), + CounterNbl = nbl:pack([{files, [ + {data, "table.rel", TableRelData, TableRelPtrs}, + {data, "text.bin", TextBinData, []} + ]}]), + Counter = build_counter(ConfFilename, CounterNbl), + Cache2 = [{CounterID, Counter}|Cache], + {Counter, Cache2}; + Counter -> + {Counter, Cache} + end. + +%% @doc Load a counter configuration file and return a table.rel binary along with its pointers array. +%% @todo City isn't handled properly yet. +load_table_rel(ConfFilename) -> + {ok, Settings} = file:consult(ConfFilename), + TName = proplists:get_value(t_name, Settings), + TDesc = proplists:get_value(t_desc, Settings), + {CursorX, CursorY} = proplists:get_value(cursor, Settings), + {NbQuests, QuestsBin, NbGroups, GroupsBin} = load_table_rel_groups_to_bin(proplists:get_value(groups, Settings)), + QuestsPos = 16, + GroupsPos = 16 + byte_size(QuestsBin), + CityBin = case proplists:get_value(city, Settings) of + undefined -> << >>; + {QuestID, ZoneID, MapID, EntryID} -> + << QuestID:32/little, ZoneID:16/little, MapID:16/little, EntryID:16/little >> + end, + CityPos = 0, + UnixTime = calendar:datetime_to_gregorian_seconds(calendar:now_to_universal_time(now())) + - calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}), + MainBin = << 16#00000100:32, UnixTime:32/little, GroupsPos:32/little, QuestsPos:32/little, + NbGroups:16/little, NbQuests:16/little, TName:16/little, TDesc:16/little, CursorX:16/little, CursorY:16/little, + 0:32, 0:16, 16#ffff:16, CityPos:32 >>, + MainPos = GroupsPos + byte_size(GroupsBin), + Data = << MainPos:32/little, 0:32, QuestsBin/binary, GroupsBin/binary, MainBin/binary >>, + Size = byte_size(Data), + Data2 = << $N, $X, $R, 0, Size:32/little, Data/binary >>, + {Data2, [MainPos + 8, MainPos + 12]}. + +%% @doc Convert groups of quests to their binary equivalent for load_table_rel. +load_table_rel_groups_to_bin(Groups) -> + load_table_rel_groups_to_bin(Groups, 0, [], []). +load_table_rel_groups_to_bin([], N, QAcc, GAcc) -> + NbGroups = length(GAcc), + Quests = iolist_to_binary(lists:reverse(QAcc)), + Groups = iolist_to_binary(lists:reverse(GAcc)), + {N, Quests, NbGroups, Groups}; +load_table_rel_groups_to_bin([Settings|Tail], N, QAcc, GAcc) -> + TName = proplists:get_value(t_name, Settings), + TDesc = proplists:get_value(t_desc, Settings), + Quests = proplists:get_value(quests, Settings), + QuestsBin = [<< Q:32/little >> || {Q, _Filename} <- Quests], + L = length(Quests), + GroupBin = << N:16/little, L:16/little, TName:16/little, TDesc:16/little, 0:16, 16#ff03:16, 0:160 >>, + load_table_rel_groups_to_bin(Tail, N + L, [QuestsBin|QAcc], [GroupBin|GAcc]). + +%% @doc Load a text.bin file from its UCS-2 txt file equivalent. +load_text_bin(TextFilename) -> + {ok, << 16#fffe:16, File/binary >>} = file:read_file(TextFilename), + Strings = re:split(File, "\n."), + TextBin = load_text_bin_strings(Strings), + Size = 12 + byte_size(TextBin), + << Size:32/little, 8:32/little, 12:32/little, TextBin/binary >>. + +load_text_bin_strings(Strings) -> + load_text_bin_strings(Strings, 0, [], []). +load_text_bin_strings([], _Pos, PosList, Acc) -> + L = length(PosList) * 4, + PosList2 = [P + L + 12 || P <- lists:reverse(PosList)], + PosBin = iolist_to_binary([<< P:32/little >> || P <- PosList2]), + StringsBin = iolist_to_binary(lists:reverse(Acc)), + << PosBin/binary, StringsBin/binary >>; +%% empty line at the end of a text.bin.txt file. +load_text_bin_strings([<< >>], Pos, PosList, Acc) -> + load_text_bin_strings([], Pos, PosList, Acc); +load_text_bin_strings([String|Tail], Pos, PosList, Acc) -> + String2 = re:replace(String, "~.n.", "\n\0", [global, {return, binary}]), + String3 = << String2/binary, 0, 0 >>, + load_text_bin_strings(Tail, Pos + byte_size(String3), [Pos|PosList], [String3|Acc]). diff --git a/src/egs_game.erl b/src/egs_game.erl index f36c559..2bccdd2 100644 --- a/src/egs_game.erl +++ b/src/egs_game.erl @@ -303,10 +303,14 @@ event(counter_leave, State=#state{gid=GID}) -> %% @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) -> log("counter options request ~p", [CounterID]), - [{quests, _}, {bg, Background}|_Tail] = proplists:get_value(CounterID, ?COUNTERS), - psu_game:send_1711(Background); + Bg = case proplists:get_value(CounterID, ?COUNTERS) of + undefined -> egs_counters:bg(CounterID); + [{quests, _}, {bg, Background}|_Tail] -> Background + end, + psu_game:send_1711(Bg); %% @todo Handle when the party already exists! And stop doing it wrong. event(counter_party_info_request, #state{gid=GID}) -> @@ -318,16 +322,20 @@ event(counter_party_options_request, _State) -> psu_game:send_170a(); %% @doc Request the counter's quest files. -event({counter_quest_files_request, CounterID}, _State) -> +event({counter_quest_files_request, CounterID}, State) -> log("counter quest files request ~p", [CounterID]), - [{quests, Filename}|_Tail] = proplists:get_value(CounterID, ?COUNTERS), - psu_game:send_0c06(Filename); + case proplists:get_value(CounterID, ?COUNTERS) of + undefined -> psu_proto:send_0c06(egs_counters:pack(CounterID), State); + [{quests, Filename}|_Tail] -> psu_game:send_0c06(Filename) + end; %% @doc Counter available mission list request handler. -event({counter_quest_options_request, CounterID}, _State) -> +event({counter_quest_options_request, CounterID}, State) -> log("counter quest options request ~p", [CounterID]), - [{quests, _}, {bg, _}, {options, Options}] = proplists:get_value(CounterID, ?COUNTERS), - psu_game:send_0c10(Options); + case proplists:get_value(CounterID, ?COUNTERS) of + undefined -> psu_proto:send_0c10(egs_counters:opts(CounterID), State); + [{quests, _}, {bg, _}, {options, Options}] -> psu_game:send_0c10(Options) + end; %% @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}) -> diff --git a/src/egs_sup.erl b/src/egs_sup.erl index f58b83a..c093af3 100644 --- a/src/egs_sup.erl +++ b/src/egs_sup.erl @@ -59,6 +59,7 @@ init([]) -> {egs_npc_db, {egs_npc_db, start_link, []}, permanent, 5000, worker, dynamic}, {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_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/psu/psu_proto.erl b/src/psu/psu_proto.erl index 1f1b21b..3cd3e08 100644 --- a/src/psu/psu_proto.erl +++ b/src/psu/psu_proto.erl @@ -1372,6 +1372,16 @@ 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, 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}) -> + packet_send(Socket, << 16#0c060300:32, 0:288, 1:32/little-unsigned-integer, Pack/binary >>). + +%% @doc Send the counter's mission options (0 = invisible, 2 = disabled, 3 = available). +%% @todo LID. +send_0c10(Options, #state{socket=Socket, gid=DestGID}) -> + Size = byte_size(Options), + packet_send(Socket, << 16#0c100300:32, 0:160, 16#00011300:32, DestGID:32/little, 0:64, 1, 0, Size:16/little, Options/binary >>). + %% @doc Send the character 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}) ->