From eafeb744af589cb1da8bee1750a233f7d1744141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Sun, 7 Nov 2010 19:45:35 +0100 Subject: [PATCH] Introduce egs_quests_db for quest building and handling. --- include/maps.hrl | 2 +- priv/quests/1100000/quest.conf | 119 +++++++++++ .../1100000/unit_title_00.bin.en_US.txt | Bin 0 -> 298 bytes .../1100000/unit_title_01.bin.en_US.txt | Bin 0 -> 64 bytes .../1100000/unit_title_02.bin.en_US.txt | Bin 0 -> 46 bytes .../1100000/unit_title_03.bin.en_US.txt | Bin 0 -> 24 bytes .../1100000/unit_title_04.bin.en_US.txt | Bin 0 -> 46 bytes .../1100000/unit_title_07.bin.en_US.txt | Bin 0 -> 42 bytes .../1100000/unit_title_11.bin.en_US.txt | Bin 0 -> 262 bytes .../1100000/unit_title_12.bin.en_US.txt | Bin 0 -> 64 bytes .../1100000/unit_title_13.bin.en_US.txt | Bin 0 -> 62 bytes priv/quests/README | 3 + src/egs_files.erl | 202 +++++++++++++++++- src/egs_game.erl | 4 +- src/egs_quests_db.erl | 113 ++++++++++ src/egs_sup.erl | 1 + src/psu/psu_game.erl | 22 +- src/psu/psu_proto.erl | 7 +- 18 files changed, 449 insertions(+), 24 deletions(-) create mode 100644 priv/quests/1100000/quest.conf create mode 100644 priv/quests/1100000/unit_title_00.bin.en_US.txt create mode 100644 priv/quests/1100000/unit_title_01.bin.en_US.txt create mode 100644 priv/quests/1100000/unit_title_02.bin.en_US.txt create mode 100644 priv/quests/1100000/unit_title_03.bin.en_US.txt create mode 100644 priv/quests/1100000/unit_title_04.bin.en_US.txt create mode 100644 priv/quests/1100000/unit_title_07.bin.en_US.txt create mode 100644 priv/quests/1100000/unit_title_11.bin.en_US.txt create mode 100644 priv/quests/1100000/unit_title_12.bin.en_US.txt create mode 100644 priv/quests/1100000/unit_title_13.bin.en_US.txt create mode 100644 priv/quests/README create mode 100644 src/egs_quests_db.erl diff --git a/include/maps.hrl b/include/maps.hrl index b020e46..0dcb83d 100644 --- a/include/maps.hrl +++ b/include/maps.hrl @@ -199,7 +199,7 @@ % Planetary lobbies - {1100000, [{type, lobby}, {file, "data/lobby/colony.quest.nbl"}]}, + {1100000, [{type, lobby}, {file, nofile}]}, {1101000, [{type, lobby}, {file, "data/lobby/parum.quest.nbl"}]}, {1102000, [{type, lobby}, {file, "data/lobby/neudaiz.quest.nbl"}]}, {1103000, [{type, lobby}, {file, "data/lobby/moatoob.quest.nbl"}]}, diff --git a/priv/quests/1100000/quest.conf b/priv/quests/1100000/quest.conf new file mode 100644 index 0000000..c3065b1 --- /dev/null +++ b/priv/quests/1100000/quest.conf @@ -0,0 +1,119 @@ +%% 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 . + +%% GUARDIANS Colony lobbies. + +{questid, 1100000}. + +%% @todo Temporary value until the handling of things are properly figured out. +{quest_type, lobby}. + +%% @todo Default to {1, 6} if missing? +{party_size, {1, 6}}. + +%% @todo Default to [] if missing? +%% cant_join, start_at_once... +{mission_opts, []}. + +%% @todo Those should probably default to those values if unspecified (lobbies). +{cursor, {0, 0}}. +{icon, 65535}. + +%% @todo Default enemy_level to 1 if unspecified (lobbies). +%% @todo Default sets to [100] if unspecified (lobbies). +{zones, [ + [{zoneid, 0}, {areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [1, 2, 3, 4]}], + [{zoneid, 1}, {areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9001]}], + [{zoneid, 2}, {areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9000]}], + [{zoneid, 3}, {areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9102]}], + [{zoneid, 4}, {areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9010]}], + [{zoneid, 7}, {areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [9200, 9202]}], + [{zoneid, 11}, {areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [5, 100, 101, 102, 103, 110]}], + [{zoneid, 12}, {areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [100, 101, 102]}], + [{zoneid, 13}, {areaid, 2}, {enemy_level, 1}, {sets, [100]}, {maps, [100, 101, 102]}] +]}. + +{temp_flags, []}. +{value_flags, [ + "SV_WK_LC_RETURN", "FRAME_SKIP_NUM", "EM06_WORK_POINT", "EM06_WORK_OPEN", + "ON_LIMITED_CP", "ON_DELIVERY_AMP", "EM24_WORK_POINT", "EM24_WORK_OPEN", + "EM28_WORK_KRBN" +]}. +{bool_flags, [ + "SV_FLG_CTRL_CZ", "SVFLG_WENT_CZ1F", "SVFLG_WENT_CZ2F", "SVFLG_WENT_CZ3F", + "SVFLG_WENT_CZ4F", "SVFLG_WENT_CZ5F", "SV_FLG_PASS_PM", "SV_FLG_PASS_HT", + "SV_FLG_PASS_NZ", "SV_FLG_PASS_OT", "SV_FLG_PASS_MB", "SV_FLG_PASS_DO", + "MV_PASS_FL_CN01", "MV_PASS_FL_CN02", "MV_PASS_FL_CN03", "MV_PASS_FL_CN04", + "MV_PASS_FL_CN05", "MV_PASS_FL_CN06", "EX_CL_FLAG_00", "EX_CL_FLAG_01", + "EM13_FLAG_TALK0", "EM03_FLAG_00", "EM03_FLAG_01", "PM09_SVFLG_01", + "ON_DELIVERY_1ST", "ON_DELIVERY_2ND", "ON_DELIVERY_3RD", "SP_BUKI_FLAG_00", + "SP_BUKI_FLAG_01", "SP_BUKI_FLAG_02" +]}. + +{items, [ + %% @todo ItemID defaults to ffffffff if missing? + %% @todo nb_items defaults to 1 if missing? + %% @todo type 5 for lobbies, what else? + [{index, 1}, {itemid, 16#ffffffff}, {nb_items, 1}, {type, 5}, {money, 200}], + [{index, 2}, {itemid, 16#ffffffff}, {nb_items, 1}, {type, 5}, {money, 400}], + [{index, 3}, {itemid, 16#ffffffff}, {nb_items, 1}, {type, 5}, {money, 600}], + [{index, 4}, {itemid, 16#ffffffff}, {nb_items, 1}, {type, 5}, {money, 800}] +]}. + +%% @todo if is questid then defaults to ffffffff. +%% @todo Default to full ffff if missing. +{enter_warp, {1100000, 0, 1, 0}}. +{exit_warp, {1100000, 65535, 65535, 65535}}. +{fail_warp, {1100000, 65535, 65535, 65535}}. + +%% @doc Current -> next. Values are the area and the exit/entrance entryid. +%% @todo if is questid then defaults to ffffffff. +{warps, [ + {{1100000, 0, 3, 0}, {1100000, 0, 2, 2}}, + {{1100000, 0, 1, 1}, {1100000, 0, 2, 0}}, + {{1100000, 0, 4, 2}, {1104000, 0, 900, 0}}, + {{1100000, 0, 3, 1}, {1100000, 0, 4, 0}}, + {{1100000, 0, 2, 0}, {1100000, 0, 1, 1}}, + {{1100000, 0, 1, 2}, {1100000, 0, 2, 1}}, + {{1100000, 0, 4, 0}, {1100000, 0, 3, 1}}, + {{1100000, 11, 103, 0}, {1100000, 0, 3, 5}}, + {{1100000, 0, 3, 2}, {1100000, 13, 102, 0}}, + {{1100000, 11, 102, 2}, {1100000, 0, 3, 4}}, + {{1100000, 0, 2, 5}, {1100000, 13, 101, 2}}, + {{1100000, 13, 101, 2}, {1100000, 0, 2, 5}}, + {{1100000, 11, 101, 0}, {1100000, 0, 2, 3}}, + {{1100000, 13, 100, 0}, {1100000, 0, 2, 6}}, + {{1100000, 0, 2, 6}, {1100000, 13, 100, 0}}, + {{1100000, 11, 100, 2}, {1100000, 0, 2, 8}}, + {{1100000, 0, 2, 3}, {1100000, 11, 101, 0}}, + {{1100000, 12, 102, 1}, {1100000, 0, 3, 3}}, + {{1100000, 0, 2, 4}, {1100000, 12, 101, 1}}, + {{1100000, 11, 110, 0}, {1100000, 11, 5, 1}}, + {{1100000, 11, 5, 0}, {1100000, 0, 4, 1}}, + {{1100000, 0, 3, 3}, {1100000, 12, 102, 1}}, + {{1100000, 0, 3, 4}, {1100000, 11, 102, 2}}, + {{1100000, 12, 101, 1}, {1100000, 0, 2, 4}}, + {{1100000, 0, 4, 3}, {1104000, 0, 900, 0}}, + {{1100000, 0, 3, 5}, {1100000, 11, 103, 0}}, + {{1100000, 0, 2, 7}, {1100000, 12, 100, 1}}, + {{1100000, 0, 2, 1}, {1100000, 0, 1, 2}}, + {{1100000, 0, 4, 4}, {1104000, 0, 900, 0}}, + {{1100000, 13, 102, 0}, {1100000, 0, 3, 2}}, + {{1100000, 12, 100, 1}, {1100000, 0, 2, 7}}, + {{1100000, 0, 2, 8}, {1100000, 11, 100, 2}}, + {{1100000, 0, 2, 2}, {1100000, 0, 3, 0}}, + {{1100000, 0, 1, 0}, {1120000, 0, 1, 1}}, + {{1100000, 0, 4, 1}, {1100000, 11, 5, 0}} +]}. diff --git a/priv/quests/1100000/unit_title_00.bin.en_US.txt b/priv/quests/1100000/unit_title_00.bin.en_US.txt new file mode 100644 index 0000000000000000000000000000000000000000..90c2f83312df78c2c0200852d797f32c737fe089 GIT binary patch literal 298 zcmb7YXF`G(K->t4QGzsZc=fq0QkW@bcD|Xn^SxggHwGdj zfsBUUS76E0DsWJJ7#-WBNAC7JO8bG6`D_(gxciHunUaEi@5OjW;9IfmSjxXGiNRCNS&?Z=A5|GiH*F%w{Rw&N60$W1@FubkwT#-2X2K`iyi59K5K%u~g-3pL%6(ma5mPegOsy BEe`+y literal 0 HcmV?d00001 diff --git a/priv/quests/1100000/unit_title_12.bin.en_US.txt b/priv/quests/1100000/unit_title_12.bin.en_US.txt new file mode 100644 index 0000000000000000000000000000000000000000..36ec83d7d542d26c308f33fc49cf97a2bc686dc4 GIT binary patch literal 64 zcmezW&zHfGL6^auA&9}3A(+96L4m=UA%~%oA(f$ufeS3|0+e?I%7J+548=edz6|~h JE)1?f5de{c3N-)# literal 0 HcmV?d00001 diff --git a/priv/quests/1100000/unit_title_13.bin.en_US.txt b/priv/quests/1100000/unit_title_13.bin.en_US.txt new file mode 100644 index 0000000000000000000000000000000000000000..ffe5db9fca243a2997a237da596e3fc8d3a6270b GIT binary patch literal 62 zcmezWFN`6Pp@<=qA(f$op^`y?A($b9A)ldufs4VNA&9|+K^I5|gL&!<#bEgmhDZh< I23MdM0Ia79@Bjb+ literal 0 HcmV?d00001 diff --git a/priv/quests/README b/priv/quests/README new file mode 100644 index 0000000..8090149 --- /dev/null +++ b/priv/quests/README @@ -0,0 +1,3 @@ +List of quests: + +1100000 GUARDIANS Colony lobbies diff --git a/src/egs_files.erl b/src/egs_files.erl index f615948..1d8cbc9 100644 --- a/src/egs_files.erl +++ b/src/egs_files.erl @@ -18,7 +18,7 @@ %% along with EGS. If not, see . -module(egs_files). --export([load_counter_pack/2, load_table_rel/1, load_text_bin/1, nbl_pack/1]). +-export([load_counter_pack/2, load_quest_xnr/1, load_table_rel/1, load_text_bin/1, load_unit_title_table_rel/2, nbl_pack/1, nbl_padded_size/1]). %% @doc Build a counter's pack file, options and return them along with the background value. load_counter_pack(ConfFilename, CounterNbl) -> @@ -60,6 +60,152 @@ load_counter_pack_quests([{_QuestID, Filename}|Tail], PosList, SizeList, Acc) -> Size = byte_size(File), load_counter_pack_quests(Tail, [Pos|PosList], [Size|SizeList], [File|Acc]). +%% @doc Load a quest configuration file and return a quest.xnr binary along with its pointers array. +load_quest_xnr(ConfFilename) -> + {ok, Settings} = file:consult(ConfFilename), + QuestID = proplists:get_value(questid, Settings), + %% Temp flags. + TmpFlagsList = proplists:get_value(temp_flags, Settings), + TmpFlagsBin = load_quest_xnr_flags(TmpFlagsList), + %% Value flags. + ValFlagsList = proplists:get_value(value_flags, Settings), + ValFlagsBin = load_quest_xnr_flags(ValFlagsList), + %% Bool flags. + BoolFlagsList = proplists:get_value(bool_flags, Settings), + BoolFlagsBin = load_quest_xnr_flags(BoolFlagsList), + %% Items. + ItemsPos = 16 + byte_size(TmpFlagsBin) + byte_size(ValFlagsBin) + byte_size(BoolFlagsBin), + ItemsList = proplists:get_value(items, Settings), + ItemsBin = load_quest_xnr_items(ItemsList), + NbItems = length(ItemsList), + %% Item pointers. + ItemsPtrsPos = ItemsPos + byte_size(ItemsBin), + ItemsPtrs = << ItemsPos:32/little, NbItems:32/little >>, + %% Zones. + ZonesBasePos = ItemsPtrsPos + byte_size(ItemsPtrs), + ZonesList = proplists:get_value(zones, Settings), + {SetsBin, SetsPtrsList, ZonesBin} = load_quest_xnr_zones(ZonesList, ZonesBasePos), + ZonesPos = ZonesBasePos + byte_size(SetsBin), + NbZones = length(ZonesList), + %% Warps. + WarpsPos = ZonesPos + byte_size(ZonesBin), + WarpsList = proplists:get_value(warps, Settings), + WarpsBin = load_quest_xnr_warps(QuestID, WarpsList), + NbWarps = length(WarpsList), + %% Temp flag pointers. + TmpFlagsPtrsPos = WarpsPos + byte_size(WarpsBin), + {NbTmpFlags, TmpFlagsPtrs} = load_quest_xnr_flag_ptrs(TmpFlagsList, 0), + TmpFlagsPtrsPos2 = if NbTmpFlags =/= 0 -> TmpFlagsPtrsPos; true -> 0 end, + %% Value flag pointers. + ValFlagsPtrsPos = TmpFlagsPtrsPos + byte_size(TmpFlagsPtrs), + {NbValFlags, ValFlagsPtrs} = load_quest_xnr_flag_ptrs(ValFlagsList, NbTmpFlags), + ValFlagsPtrsPos2 = if NbValFlags =/= 0 -> ValFlagsPtrsPos; true -> 0 end, + %% Bool flag pointers. + BoolFlagsPtrsPos = ValFlagsPtrsPos + byte_size(ValFlagsPtrs), + {NbBoolFlags, BoolFlagsPtrs} = load_quest_xnr_flag_ptrs(BoolFlagsList, NbTmpFlags + NbValFlags), + BoolFlagsPtrsPos2 = if NbBoolFlags =/= 0 -> BoolFlagsPtrsPos; true -> 0 end, + %% Main pointers. + MainPos = BoolFlagsPtrsPos + byte_size(BoolFlagsPtrs), + NbNPCs = 0, %% @todo + NPCsPos = MainPos + 260, %% @todo if NbNPCs =/= 0 -> todo; true -> MainPos + 260 end, + NPCsBin = << 0:64 >>, + %% Main options. + EnterWarp = load_quest_xnr_warp(QuestID, proplists:get_value(enter_warp, Settings)), + ExitWarp = load_quest_xnr_warp(QuestID, proplists:get_value(exit_warp, Settings)), + FailWarp = load_quest_xnr_warp(QuestID, proplists:get_value(fail_warp, Settings)), + MissionOpts = 0, %% @todo + NbCustomNPCs = 0, %% @todo + Icon = proplists:get_value(icon, Settings), + {PartySizeMin, PartySizeMax} = proplists:get_value(party_size, Settings), + {CursorX, CursorY} = proplists:get_value(cursor, Settings), + 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#16000600:32, UnixTime:32/little, QuestID:32/little, ZonesPos:32/little, NbZones:32/little, + WarpsPos:32/little, NbWarps:32/little, EnterWarp/binary, ExitWarp/binary, MissionOpts:8, 16#040200:24, 0:288, + Icon:16/little, 0:16, PartySizeMin:8, PartySizeMax:8, CursorX:16/little, CursorY:16/little, 0:16, ItemsPtrsPos:32/little, + FailWarp/binary, 0:160, TmpFlagsPtrsPos2:32/little, ValFlagsPtrsPos2:32/little, BoolFlagsPtrsPos2:32/little, NbTmpFlags:8, + NbValFlags:8, NbBoolFlags:8, NbNPCs:8, NPCsPos:32/little, 16#00000100:32, 16#00000100:32, NbCustomNPCs:32/little, 0:704 >>, + %% Wrapping it up. + Data = << MainPos:32/little, 0:32, TmpFlagsBin/binary, ValFlagsBin/binary, BoolFlagsBin/binary, + ItemsBin/binary, ItemsPtrs/binary, SetsBin/binary, ZonesBin/binary, WarpsBin/binary, + TmpFlagsPtrs/binary, ValFlagsPtrs/binary, BoolFlagsPtrs/binary, MainBin/binary, NPCsBin/binary >>, + Size = 8 + byte_size(Data), + Data2 = << $N, $X, $R, 0, Size:32/little, Data/binary >>, + %% Calculate the pointers and return. + L0 = [ItemsPtrsPos] ++ SetsPtrsList, + L1 = L0 ++ lists:seq(ZonesPos + 16, ZonesPos + 16 + 64 * (NbZones - 1), 64), + L2 = L1 ++ lists:seq(TmpFlagsPtrsPos, TmpFlagsPtrsPos + 4 * (NbTmpFlags + NbValFlags + NbBoolFlags - 1), 4), + L3 = L2 ++ [MainPos + 12, MainPos + 20, MainPos + 104], + L4 = if TmpFlagsPtrsPos2 =/= 0 -> L3 ++ [MainPos + 140]; true -> L3 end, + L5 = if ValFlagsPtrsPos2 =/= 0 -> L4 ++ [MainPos + 144]; true -> L4 end, + L6 = if BoolFlagsPtrsPos2 =/= 0 -> L5 ++ [MainPos + 148]; true -> L5 end, + {Data2, L6 ++ [MainPos + 156]}. + +load_quest_xnr_flag_ptrs([], _N) -> + {0, << >>}; +load_quest_xnr_flag_ptrs(FlagsList, N) -> + NbFlags = length(FlagsList), + L1 = lists:seq(16 + N * 16, 16 + N * 16 + (NbFlags - 1) * 16, 16), + L2 = [<< X:32/little >> || X <- L1], + {NbFlags, iolist_to_binary(L2)}. + +load_quest_xnr_flags(FlagsList) -> + load_quest_xnr_flags(FlagsList, []). +load_quest_xnr_flags([], Acc) -> + iolist_to_binary(lists:reverse(Acc)); +load_quest_xnr_flags([Flag|Tail], Acc) -> + L = length(Flag), + Padding = 8 * (16 - L), + FlagBin = list_to_binary(Flag), + Bin = << FlagBin/binary, 0:Padding >>, + load_quest_xnr_flags(Tail, [Bin|Acc]). + +load_quest_xnr_items(ItemsList) -> + load_quest_xnr_items(ItemsList, []). +load_quest_xnr_items([], Acc) -> + iolist_to_binary(lists:reverse(Acc)); +load_quest_xnr_items([Item|Tail], Acc) -> + Index = proplists:get_value(index, Item), + ItemID = proplists:get_value(itemid, Item), + NbItems = proplists:get_value(nb_items, Item), + Type = proplists:get_value(type, Item), + Money = proplists:get_value(money, Item), + Bin = << Index:8, ItemID:32, 0:16, NbItems:8, Type:32/little, Money:32/little >>, + load_quest_xnr_items(Tail, [Bin|Acc]). + +load_quest_xnr_warp(QuestID, {WarpQuestID, WarpZoneID, WarpMapID, WarpEntryID}) -> + WarpQuestID2 = if WarpQuestID =:= QuestID -> 16#ffffffff; true -> WarpQuestID end, + << WarpQuestID2:32/little, WarpZoneID:16/little, WarpMapID:16/little, WarpEntryID:16/little, 0:16 >>. + +load_quest_xnr_warps(QuestID, WarpsList) -> + load_quest_xnr_warps(QuestID, WarpsList, []). +load_quest_xnr_warps(_QuestID, [], Acc) -> + iolist_to_binary(lists:reverse(Acc)); +load_quest_xnr_warps(QuestID, [Warp|Tail], Acc) -> + {CurrentWarp, NextWarp} = Warp, + Bin1 = load_quest_xnr_warp(QuestID, CurrentWarp), + Bin2 = load_quest_xnr_warp(QuestID, NextWarp), + load_quest_xnr_warps(QuestID, Tail, [<< Bin1/binary, Bin2/binary >>|Acc]). + +load_quest_xnr_zones(ZonesList, BasePos) -> + load_quest_xnr_zones(ZonesList, BasePos, [], [], []). +load_quest_xnr_zones([], _BasePos, SetsAcc, SetsPtrsAcc, ZonesAcc) -> + SetsBin = iolist_to_binary(lists:reverse(SetsAcc)), + SetsPtrsList = lists:flatten(lists:reverse(SetsPtrsAcc)), + ZonesBin = iolist_to_binary(lists:reverse(ZonesAcc)), + {SetsBin, SetsPtrsList, ZonesBin}; +load_quest_xnr_zones([Zone|Tail], BasePos, SetsAcc, SetsPtrsAcc, ZonesAcc) -> + ZoneID = proplists:get_value(zoneid, Zone), + AreaID = proplists:get_value(areaid, Zone), + EnemyLevel = proplists:get_value(enemy_level, Zone), + SetList = proplists:get_value(sets, Zone), + NbSets = length(SetList), + SetsBin = iolist_to_binary([<< Set:32/little >> || Set <- SetList]), + SetsBin2 = << SetsBin/binary, BasePos:32/little, NbSets:32/little >>, + SetPos = BasePos + NbSets * 4, + ZoneBin = << ZoneID:16/little, AreaID:16/little, AreaID:32/little, 0:16, EnemyLevel:8, 16#ff:8, 16#04010000:32, SetPos:32/little, 0:352 >>, + load_quest_xnr_zones(Tail, BasePos + byte_size(SetsBin2), [SetsBin2|SetsAcc], [SetPos|SetsPtrsAcc], [ZoneBin|ZonesAcc]). + %% @doc Load a counter configuration file and return a table.rel binary along with its pointers array. load_table_rel(ConfFilename) -> {ok, Settings} = file:consult(ConfFilename), @@ -129,8 +275,38 @@ load_text_bin_strings([String|Tail], Pos, PosList, Acc) -> String3 = << String2/binary, 0, 0 >>, load_text_bin_strings(Tail, Pos + byte_size(String3), [Pos|PosList], [String3|Acc]). +%% @doc Create the unit_title_table.rel file based on a quest configuration file and a starting file position. +%% The file position depends on the previously built quest.xnr. +load_unit_title_table_rel(ConfFilename, FilePos) -> + {ok, Settings} = file:consult(ConfFilename), + {Titles, NbTitles} = load_unit_title_table_rel_zones(proplists:get_value(zones, Settings)), + MainPos = 16 + byte_size(Titles), + Size = 8 + MainPos, + TitlesPtr = FilePos + 16, + Data = << $N, $X, $R, 0, Size:32/little, MainPos:32/little, 0:32, Titles/binary, TitlesPtr:32/little, NbTitles:32/little >>, + {Data, [FilePos + 16 + byte_size(Titles)]}. + +load_unit_title_table_rel_maps(ZoneID, Maps) -> + load_unit_title_table_rel_maps(ZoneID, Maps, 0, []). +load_unit_title_table_rel_maps(_ZoneID, [], N, Acc) -> + {iolist_to_binary(lists:reverse(Acc)), N}; +load_unit_title_table_rel_maps(ZoneID, [MapID|Tail], N, Acc) -> + Bin = << ZoneID:16/little, MapID:16/little, N:32/little >>, + load_unit_title_table_rel_maps(ZoneID, Tail, N + 1, [Bin|Acc]). + +load_unit_title_table_rel_zones(Zones) -> + load_unit_title_table_rel_zones(Zones, 0, []). +load_unit_title_table_rel_zones([], N, Acc) -> + {iolist_to_binary(lists:reverse(Acc)), N}; +load_unit_title_table_rel_zones([Zone|Tail], N, Acc) -> + ZoneID = proplists:get_value(zoneid, Zone), + Maps = proplists:get_value(maps, Zone), + {Bin, N2} = load_unit_title_table_rel_maps(ZoneID, Maps), + load_unit_title_table_rel_zones(Tail, N + N2, [Bin|Acc]). + %% @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", []}]}]). +%% @todo The 0010 value is unknown. If it was too low it would crash the client when it cleans up the nbl. nbl_pack(Options) -> Files = proplists:get_value(files, Options), {Header, Data, DataSize, PtrArray, PtrArraySize} = nbl_pack_files(Files), @@ -138,7 +314,7 @@ nbl_pack(Options) -> HeaderSize = 16#30 + 16#60 * NbFiles, CompressedDataSize = 0, EncryptSeed = 0, - << $N, $M, $L, $L, 2:16/little, 16#1300:16, HeaderSize:32/little, NbFiles:32/little, + << $N, $M, $L, $L, 2:16/little, 16#0010: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 >>. @@ -151,6 +327,7 @@ nbl_pack_files([], {AccH, AccD, AccP, _FilePos, _PtrIndex}) -> PaddingH2 = if PaddingH =< 0 -> 16#800 + PaddingH; true -> PaddingH end, BinD = iolist_to_binary(lists:reverse(AccD)), PaddingD = 8 * (16#800 - (byte_size(BinD) rem 16#800)), + PaddingD2 = if PaddingD =:= 8 * 16#800 -> 0; true -> PaddingD end, BinP = iolist_to_binary(lists:reverse(AccP)), PtrSize = byte_size(BinP), PtrArray = case PtrSize of @@ -160,30 +337,31 @@ nbl_pack_files([], {AccH, AccD, AccP, _FilePos, _PtrIndex}) -> << BinP/binary, 0:PaddingP >> end, {<< BinH/binary, 0:PaddingH2 >>, - << BinD/binary, 0:PaddingD >>, byte_size(BinD), + << BinD/binary, 0:PaddingD2 >>, byte_size(BinD), PtrArray, PtrSize}; nbl_pack_files([{data, Filename, Data, PtrList}|Tail], {AccH, AccD, AccP, FilePos, PtrIndex}) -> ID = case filename:extension(Filename) of ".bin" -> << $S, $T, $D, 0 >>; [$.|String] -> list_to_binary(string:to_upper(String ++ [0])) end, - FilenamePaddingBits = 8 * (32 - length(Filename)), + FilenameBin = iolist_to_binary(Filename), + FilenamePaddingBits = 8 * (32 - byte_size(FilenameBin)), DataSize = byte_size(Data), - DataPadding = 16#20 - (DataSize rem 16#20), - DataPaddingBits = 8 * DataPadding, - DataSizeWithPadding = DataSize + DataPadding, + PaddedSize = nbl_padded_size(DataSize), + DataPaddingBits = 8 * (PaddedSize - DataSize), PtrSize = 4 * length(PtrList), - BinH = << ID/binary, 16#60000000:32, 0:64, (list_to_binary(Filename))/binary, 0:FilenamePaddingBits, + BinD = << Data/binary, 0:DataPaddingBits >>, + BinH = << ID/binary, 16#60000000:32, 0:64, FilenameBin/binary, 0:FilenamePaddingBits, FilePos:32/little, DataSize:32/little, PtrIndex:32/little, PtrSize:32/little >>, NXIF = case filename:extension(Filename) of ".bin" -> << 0:256 >>; _ -> nbl_pack_nxif(DataSize, PtrSize) end, BinH2 = << BinH/binary, NXIF/binary >>, - BinD = << Data/binary, 0:DataPaddingBits >>, BinP = iolist_to_binary([ << Ptr:32/little >> || Ptr <- PtrList]), - nbl_pack_files(Tail, {[BinH2|AccH], [BinD|AccD], [BinP|AccP], FilePos + DataSizeWithPadding, PtrIndex + PtrSize}); + nbl_pack_files(Tail, {[BinH2|AccH], [BinD|AccD], [BinP|AccP], FilePos + PaddedSize, PtrIndex + PtrSize}); nbl_pack_files([{file, Filename, PtrList}|Tail], Acc) -> + io:format("~p~n", [Filename]), {ok, Data} = file:read_file(Filename), nbl_pack_files([{data, Filename, Data, PtrList}|Tail], Acc). @@ -193,3 +371,7 @@ nbl_pack_nxif(DataSize, PtrSize) -> PtrSize2 = PtrSize + 16#20 - (PtrSize rem 16#20), << $N, $X, $I, $F, 16#18000000:32, 16#01000000:32, 16#20000000:32, DataSize:32/little, DataSize2:32/little, PtrSize2:32/little, 16#01000000:32 >>. + +%% @doc Return the padded size of a file to be packed in an nbl archive. +nbl_padded_size(Size) -> + Size + 16#20 - (Size rem 16#20). diff --git a/src/egs_game.erl b/src/egs_game.erl index 1f90fc3..48e8d9a 100644 --- a/src/egs_game.erl +++ b/src/egs_game.erl @@ -261,14 +261,14 @@ event({counter_enter, CounterID, FromZoneID, FromMapID, FromEntryID}, State=#sta FromArea = {psu_area, OldArea#psu_area.questid, FromZoneID, FromMapID}, User = OldUser#egs_user_model{areatype=counter, area={psu_area, 16#7fffffff, 0, 0}, entryid=0, prev_area=FromArea, prev_entryid=FromEntryID}, egs_user_model:write(User), - QuestFile = "data/lobby/counter.quest.nbl", + {ok, QuestData} = file:read_file("data/lobby/counter.quest.nbl"), ZoneFile = "data/lobby/counter.zone.nbl", %% broadcast unspawn to other people {ok, UnspawnList} = egs_user_model:select({neighbors, OldUser}), lists:foreach(fun(Other) -> Other#egs_user_model.pid ! {egs, player_unspawn, User} end, UnspawnList), %% load counter psu_proto:send_0c00(User, State), - psu_proto:send_020e(QuestFile, State), + psu_proto:send_020e(QuestData, State), psu_proto:send_0a05(State), psu_proto:send_010d(User#egs_user_model{lid=0}, State), psu_proto:send_0200(0, mission, State), diff --git a/src/egs_quests_db.erl b/src/egs_quests_db.erl new file mode 100644 index 0000000..258eaff --- /dev/null +++ b/src/egs_quests_db.erl @@ -0,0 +1,113 @@ +%% @author Loïc Hoguin +%% @copyright 2010 Loïc Hoguin. +%% @doc EGS quests 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_quests_db). +-behavior(gen_server). +-export([start_link/0, stop/0, quest/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 quest(QuestID) -> binary() +quest(QuestID) -> + gen_server:call(?SERVER, {quest, QuestID}). + +%% @spec reload() -> ok +reload() -> + gen_server:cast(?SERVER, reload). + +%% gen_server. + +init([]) -> + {ok, []}. + +%% @doc Possible keys: quest. +handle_call({Key, QuestID}, _From, State) -> + {Quest, State2} = get_quest(QuestID, State), + {reply, proplists:get_value(Key, Quest), 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 Return a quest information either from the cache or from the configuration file, +%% in which case it gets added to the cache for subsequent attempts. +get_quest(QuestID, Cache) -> + case proplists:get_value(QuestID, Cache) of + undefined -> + Dir = io_lib:format("priv/quests/~b/", [QuestID]), + ConfFilename = Dir ++ "quest.conf", + {QuestXnrData, QuestXnrPtrs} = egs_files:load_quest_xnr(ConfFilename), + UnitTitleBinFiles = load_unit_title_bin_files(Dir, ConfFilename), + TablePos = egs_files:nbl_padded_size(byte_size(QuestXnrData)), + TextSize = lists:sum([egs_files:nbl_padded_size(byte_size(D)) || {data, _F, D, _P} <- UnitTitleBinFiles]), + TablePos2 = TablePos + TextSize, + {UnitTitleTableRelData, UnitTitleTableRelPtrs} = egs_files:load_unit_title_table_rel(ConfFilename, TablePos2), + QuestNbl = egs_files:nbl_pack([{files, + [{data, "quest.xnr", QuestXnrData, QuestXnrPtrs}] + ++ UnitTitleBinFiles + ++ [{data, "unit_title_table.rel", UnitTitleTableRelData, UnitTitleTableRelPtrs}] + }]), + Quest = [{quest, QuestNbl}], + Cache2 = [{QuestID, Quest}|Cache], + {Quest, Cache2}; + Quest -> + {Quest, Cache} + end. + +load_unit_title_bin_files(Dir, ConfFilename) -> + {ok, Settings} = file:consult(ConfFilename), + Zones = proplists:get_value(zones, Settings), + [load_unit_title_bin(Dir, Zone) || Zone <- Zones]. + +load_unit_title_bin(Dir, Zone) -> + ZoneID = proplists:get_value(zoneid, Zone), + Filename = io_lib:format("unit_title_~2.10.0b.bin", [ZoneID]), + TxtFilename = io_lib:format("~s~s.en_US.txt", [Dir, Filename]), + {data, Filename, egs_files:load_text_bin(TxtFilename), []}. diff --git a/src/egs_sup.erl b/src/egs_sup.erl index 5080811..cadc518 100644 --- a/src/egs_sup.erl +++ b/src/egs_sup.erl @@ -58,6 +58,7 @@ init([]) -> {egs_counters_db, {egs_counters_db, start_link, []}, permanent, 5000, worker, dynamic}, {egs_items_db, {egs_items_db, start_link, []}, permanent, 5000, worker, dynamic}, {egs_npc_db, {egs_npc_db, start_link, []}, permanent, 5000, worker, dynamic}, + {egs_quests_db, {egs_quests_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_universes, {egs_universes, start_link, []}, permanent, 5000, worker, dynamic}, diff --git a/src/psu/psu_game.erl b/src/psu/psu_game.erl index 9d336f1..6e2c089 100644 --- a/src/psu/psu_game.erl +++ b/src/psu/psu_game.erl @@ -46,6 +46,11 @@ char_load(User, State) -> area_load(QuestID, ZoneID, MapID, EntryID, State) -> {ok, OldUser} = egs_user_model:read(get(gid)), [{type, AreaType}, {file, QuestFile}|MissionInfo] = proplists:get_value(QuestID, ?QUESTS, [{type, undefined}, {file, undefined}]), + QuestData = case QuestFile of + nofile -> egs_quests_db:quest(QuestID); + undefined -> undefined; + Filename -> {ok, D} = file:read_file(Filename), D + end, [IsStart, RealZoneID, RealMapID, RealEntryID, NbSetsInQuest] = case AreaType of mission -> if ZoneID =:= 65535 -> @@ -78,12 +83,12 @@ area_load(QuestID, ZoneID, MapID, EntryID, State) -> User = OldUser#egs_user_model{instancepid=InstancePid, areatype=AreaType, area={psu_area, QuestID, RealZoneID, RealMapID}, entryid=RealEntryID}, egs_user_model:write(User), RealSetID = if SetID > NbSetsInZone - 1 -> NbSetsInZone - 1; true -> SetID end, - area_load(AreaType, IsStart, RealSetID, OldUser, User, QuestFile, ZoneFile, AreaName, State). + area_load(AreaType, IsStart, RealSetID, OldUser, User, QuestData, ZoneFile, AreaName, State). -area_load(AreaType, IsStart, SetID, OldUser, User, QuestFile, ZoneFile, AreaName, State) -> +area_load(AreaType, IsStart, SetID, OldUser, User, QuestData, ZoneFile, AreaName, State) -> #psu_area{questid=OldQuestID, zoneid=OldZoneID} = OldUser#egs_user_model.area, #psu_area{questid=QuestID, zoneid=ZoneID, mapid=_MapID} = User#egs_user_model.area, - QuestChange = if OldQuestID /= QuestID, QuestFile /= undefined -> true; true -> false end, + QuestChange = if OldQuestID /= QuestID, QuestData /= undefined -> true; true -> false end, if ZoneFile =:= undefined -> ZoneChange = false; true -> @@ -102,7 +107,7 @@ area_load(AreaType, IsStart, SetID, OldUser, User, QuestFile, ZoneFile, AreaName if QuestChange =:= true -> % load new quest psu_proto:send_0c00(User, State), - psu_proto:send_020e(QuestFile, State); + psu_proto:send_020e(QuestData, State); true -> ignore end, %% @todo The LID changes here. @@ -428,9 +433,12 @@ send_100f(NPCid, PartyPos) -> %% @todo Handle correctly. 0:32 is actually a missing value. Value before that is unknown too. send_1015(QuestID) -> [{type, _}, {file, QuestFile}|_] = proplists:get_value(QuestID, ?QUESTS), - {ok, File} = file:read_file(QuestFile), - Size = byte_size(File), - send(<< (header(16#1015))/binary, QuestID:32/little-unsigned-integer, 16#01010000:32, 0:32, Size:32/little-unsigned-integer, File/binary >>). + QuestData = case QuestFile of + nofile -> egs_quests_db:quest(QuestID); + Filename -> {ok, D} = file:read_file(Filename), D + end, + Size = byte_size(QuestData), + send(<< (header(16#1015))/binary, QuestID:32/little-unsigned-integer, 16#01010000:32, 0:32, Size:32/little-unsigned-integer, QuestData/binary >>). %% @todo No idea. send_1016(PartyPos) -> diff --git a/src/psu/psu_proto.erl b/src/psu/psu_proto.erl index 80b4e77..7bd1606 100644 --- a/src/psu/psu_proto.erl +++ b/src/psu/psu_proto.erl @@ -1307,10 +1307,9 @@ send_020c(#state{socket=Socket}) -> %% @doc Send the quest file to be loaded by the client. %% @todo Handle the DestLID properly? -send_020e(Filename, #state{socket=Socket}) -> - {ok, File} = file:read_file(Filename), - Size = byte_size(File), - packet_send(Socket, << 16#020e0300:32, 16#ffff:16, 0:272, Size:32/little, 0:32, File/binary, 0:32 >>). +send_020e(QuestData, #state{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(Filename, SetID, SeasonID, #state{socket=Socket}) ->