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 0000000..90c2f83
Binary files /dev/null and b/priv/quests/1100000/unit_title_00.bin.en_US.txt differ
diff --git a/priv/quests/1100000/unit_title_01.bin.en_US.txt b/priv/quests/1100000/unit_title_01.bin.en_US.txt
new file mode 100644
index 0000000..9494d00
Binary files /dev/null and b/priv/quests/1100000/unit_title_01.bin.en_US.txt differ
diff --git a/priv/quests/1100000/unit_title_02.bin.en_US.txt b/priv/quests/1100000/unit_title_02.bin.en_US.txt
new file mode 100644
index 0000000..a5a3891
Binary files /dev/null and b/priv/quests/1100000/unit_title_02.bin.en_US.txt differ
diff --git a/priv/quests/1100000/unit_title_03.bin.en_US.txt b/priv/quests/1100000/unit_title_03.bin.en_US.txt
new file mode 100644
index 0000000..100ed45
Binary files /dev/null and b/priv/quests/1100000/unit_title_03.bin.en_US.txt differ
diff --git a/priv/quests/1100000/unit_title_04.bin.en_US.txt b/priv/quests/1100000/unit_title_04.bin.en_US.txt
new file mode 100644
index 0000000..b0d4587
Binary files /dev/null and b/priv/quests/1100000/unit_title_04.bin.en_US.txt differ
diff --git a/priv/quests/1100000/unit_title_07.bin.en_US.txt b/priv/quests/1100000/unit_title_07.bin.en_US.txt
new file mode 100644
index 0000000..3a2cb2f
Binary files /dev/null and b/priv/quests/1100000/unit_title_07.bin.en_US.txt differ
diff --git a/priv/quests/1100000/unit_title_11.bin.en_US.txt b/priv/quests/1100000/unit_title_11.bin.en_US.txt
new file mode 100644
index 0000000..48f3464
Binary files /dev/null and b/priv/quests/1100000/unit_title_11.bin.en_US.txt differ
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 0000000..36ec83d
Binary files /dev/null and b/priv/quests/1100000/unit_title_12.bin.en_US.txt differ
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 0000000..ffe5db9
Binary files /dev/null and b/priv/quests/1100000/unit_title_13.bin.en_US.txt differ
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}) ->