-module(travianmap_map). -export([parse_map/1, parse_line/1]). -export_type([village_record/0]). -define(SEPARATOR_CHAR, ","). -define(ESCAPE_CHAR, "'"). -define(END_CHAR, ");"). -define(SEPARATOR_BIN, <>). -define(ESCAPE_BIN, <>). -define(END_BIN, <>). -define(LINE_INTRO, "INSERT INTO `x_world` VALUES ("). -type village_record() :: #{ grid_position := integer(), x_position := integer(), y_position := integer(), tribe := non_neg_integer(), village_id := non_neg_integer(), village_name := binary(), player_id := non_neg_integer(), player_name := binary(), alliance_id := non_neg_integer(), alliance_name := binary(), population := non_neg_integer(), region := binary() | nil, is_capital := boolean() | nil, is_city := boolean() | nil, has_harbor => boolean() | nil, victory_points := non_neg_integer() | nil }. -spec parse_map(Binary_Map :: binary()) -> [{ok, village_record()} | {error, any()}]. parse_map(Binary_Map) -> Parts = binary:split(Binary_Map, <<"\n">>, [global, trim_all]), lists:map(fun(X) -> parse_line(X) end, Parts). -spec parse_line(Line :: binary()) -> {ok, village_record()} | {error, any()}. parse_line(Line) -> <> = Line, case parse_line(NewLine, []) of {error, nomatch} -> {error, {nomatch, Line}}; {ok, Values} -> case length(Values) of 16 -> {ok, #{Key => Value || {Key, Value} <- lists:zip(schema_16_harbor_added(), Values)}}; 15 -> {ok, #{Key => Value || {Key, Value} <- lists:zip(schema_15_original(), Values)}}; X -> {error, {no_schema_available, X}} end end. parse_line(Line, Values) -> case next_cut(Line) of nomatch -> %% {error, nomatch}; {error, nomatch}; {_, ?END_BIN} -> <> = Line, {ok, lists:reverse([parse_int_or_bool(Value) | Values])}; {Pos, ?SEPARATOR_BIN} -> <> = Line, NewValues = [parse_int_or_bool(Value) | Values], parse_line(NewLine, NewValues); {StartPos, ?ESCAPE_BIN} -> EndPos = next_scape(Line), <> = Line, NewValues = [Value | Values], parse_line(NewLine, NewValues) end. next_cut(Line) -> case binary:match(Line, [?SEPARATOR_BIN, ?ESCAPE_BIN, ?END_BIN]) of {Pos, Len} -> {Pos, binary:part(Line, Pos, Len)}; nomatch -> nomatch end. next_scape(<>) -> case binary:match(Line, ?ESCAPE_BIN) of nomatch -> nomatch; {Pos, _} -> Pos + 1 end. parse_int_or_bool(<<"TRUE">>) -> true; parse_int_or_bool(<<"FALSE">>) -> false; parse_int_or_bool(<<"NULL">>) -> nil; parse_int_or_bool(X) -> list_to_integer(binary_to_list(X)). schema_16_harbor_added() -> [ grid_position, x_position, y_position, tribe, village_id, village_name, player_id, player_name, alliance_id, alliance_name, population, region, is_capital, is_city, has_harbor, victory_points ]. schema_15_original() -> [ grid_position, x_position, y_position, tribe, village_id, village_name, player_id, player_name, alliance_id, alliance_name, population, region, is_capital, is_city, victory_points ].