diff options
Diffstat (limited to 'src/ast')
-rw-r--r-- | src/ast/fwd.hpp | 27 | ||||
-rw-r--r-- | src/ast/npc.cpp | 594 | ||||
-rw-r--r-- | src/ast/npc.hpp | 135 | ||||
-rw-r--r-- | src/ast/npc_test.cpp | 569 | ||||
-rw-r--r-- | src/ast/script.cpp | 71 | ||||
-rw-r--r-- | src/ast/script.hpp | 94 |
6 files changed, 1490 insertions, 0 deletions
diff --git a/src/ast/fwd.hpp b/src/ast/fwd.hpp new file mode 100644 index 0000000..77328c9 --- /dev/null +++ b/src/ast/fwd.hpp @@ -0,0 +1,27 @@ +#pragma once +// ast/fwd.hpp - list of type names for new independent tmwa ast +// +// Copyright © 2014 Ben Longbons <b.r.longbons@gmail.com> +// +// This file is part of The Mana World (Athena server) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +#include "../sanity.hpp" + + +namespace tmwa +{ +// meh, add more when I feel like it +} // namespace tmwa diff --git a/src/ast/npc.cpp b/src/ast/npc.cpp new file mode 100644 index 0000000..362943c --- /dev/null +++ b/src/ast/npc.cpp @@ -0,0 +1,594 @@ +#include "npc.hpp" +// ast/npc.cpp - Structure of non-player characters (including mobs). +// +// Copyright © 2014 Ben Longbons <b.r.longbons@gmail.com> +// +// This file is part of The Mana World (Athena server) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +#include "../io/cxxstdio.hpp" + +#include "../mmo/extract.hpp" + +#include "../map/clif.hpp" + +#include "../poison.hpp" + + +namespace tmwa +{ +namespace npc +{ +namespace parse +{ + // separate file because virtual + TopLevel::~TopLevel() {} + +#define TRY_EXTRACT(bit, var) ({ if (!extract(bit.data, &var.data)) return Err(bit.span.error_str("failed to extract"_s #var)); var.span = bit.span; }) + + static + Result<Warp> parse_warp(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits) + { + if (bits.size() != 4) + { + return Err(span.error_str("expect 4 |component|s"_s)); + } + if (bits[0].data.size() != 3) + { + return Err(bits[0].span.error_str("expect 3 ,component,s"_s)); + } + assert(bits[1].data.size() == 1); + assert(bits[1].data[0].data == "warp"_s); + if (bits[2].data.size() != 1) + { + return Err(bits[2].span.error_str("expect 1 ,component,s"_s)); + } + if (bits[3].data.size() != 5) + { + return Err(bits[3].span.error_str("expect 5 ,component,s"_s)); + } + + Warp warp; + warp.span = span; + TRY_EXTRACT(bits[0].data[0], warp.m); + TRY_EXTRACT(bits[0].data[1], warp.x); + TRY_EXTRACT(bits[0].data[2], warp.y); + warp.key_span = bits[1].data[0].span; + TRY_EXTRACT(bits[2].data[0], warp.name); + TRY_EXTRACT(bits[3].data[0], warp.xs); + TRY_EXTRACT(bits[3].data[1], warp.ys); + TRY_EXTRACT(bits[3].data[2], warp.to_m); + TRY_EXTRACT(bits[3].data[3], warp.to_x); + TRY_EXTRACT(bits[3].data[4], warp.to_y); + return Ok(std::move(warp)); + } + static + Result<Shop> parse_shop(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits) + { + if (bits.size() != 4) + { + return Err(span.error_str("expect 4 |component|s"_s)); + } + if (bits[0].data.size() != 4) + { + return Err(bits[0].span.error_str("expect 4 ,component,s"_s)); + } + assert(bits[1].data.size() == 1); + assert(bits[1].data[0].data == "shop"_s); + if (bits[2].data.size() != 1) + { + return Err(bits[2].span.error_str("expect 1 ,component,s"_s)); + } + if (bits[3].data.size() < 2) + { + return Err(bits[3].span.error_str("expect at least 2 ,component,s"_s)); + } + + Shop shop; + shop.span = span; + TRY_EXTRACT(bits[0].data[0], shop.m); + TRY_EXTRACT(bits[0].data[1], shop.x); + TRY_EXTRACT(bits[0].data[2], shop.y); + TRY_EXTRACT(bits[0].data[3], shop.d); + shop.key_span = bits[1].data[0].span; + TRY_EXTRACT(bits[2].data[0], shop.name); + TRY_EXTRACT(bits[3].data[0], shop.npc_class); + shop.items.span = bits[3].span; + shop.items.span.begin = bits[3].data[1].span.begin; + for (size_t i = 1; i < bits[3].data.size(); ++i) + { + shop.items.data.emplace_back(); + auto& item = shop.items.data.back(); + auto& data = bits[3].data[i]; + assert(data.span.begin.line == data.span.end.line); + item.span = data.span; + + XString name; + if (!extract(data.data, record<':'>(&name, &item.data.value.data))) + return Err(data.span.error_str("Failed to split item:value"_s)); + item.data.name.span = item.span; + item.data.name.span.end.column = item.data.name.span.begin.column + name.size() - 1; + item.data.value.span = item.span; + item.data.value.span.begin.column = item.data.name.span.begin.column + name.size() + 1; + if (name.endswith(' ')) + { + item.data.name.span.warning("Shop item has useless space before the colon"_s); + name = name.rstrip(); + } + if (name.is_digit10()) + { + item.data.name.span.warning("Shop item is an id; should be a name"_s); + } + if (!extract(name, &item.data.name.data)) + return Err("item name problem (too long?)"_s); + } + return Ok(std::move(shop)); + } + static + Result<Monster> parse_monster(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits) + { + if (bits.size() != 4) + { + return Err(span.error_str("expect 4 |component|s"_s)); + } + if (bits[0].data.size() != 3 && bits[0].data.size() != 5) + { + return Err(bits[0].span.error_str("expect 3 or 5 ,component,s"_s)); + } + assert(bits[1].data.size() == 1); + assert(bits[1].data[0].data == "monster"_s); + if (bits[2].data.size() != 1) + { + return Err(bits[2].span.error_str("expect 1 ,component,s"_s)); + } + if (bits[3].data.size() != 2 && bits[3].data.size() != 4 && bits[3].data.size() != 5) + { + return Err(bits[3].span.error_str("expect 2, 4, or 5 ,component,s"_s)); + } + + Monster mob; + mob.span = span; + TRY_EXTRACT(bits[0].data[0], mob.m); + TRY_EXTRACT(bits[0].data[1], mob.x); + TRY_EXTRACT(bits[0].data[2], mob.y); + if (bits[0].data.size() >= 5) + { + TRY_EXTRACT(bits[0].data[3], mob.xs); + TRY_EXTRACT(bits[0].data[4], mob.ys); + } + else + { + mob.xs.data = 0; + mob.xs.span = bits[0].data[2].span; + mob.xs.span.end.column++; + mob.xs.span.begin.column = mob.xs.span.end.column; + mob.ys.data = 0; + mob.ys.span = mob.xs.span; + } + mob.key_span = bits[1].data[0].span; + TRY_EXTRACT(bits[2].data[0], mob.name); + TRY_EXTRACT(bits[3].data[0], mob.mob_class); + TRY_EXTRACT(bits[3].data[1], mob.num); + if (bits[3].data.size() >= 4) + { + static_assert(std::is_same<decltype(mob.delay1.data)::period, std::chrono::milliseconds::period>::value, "delay1 is milliseconds"); + static_assert(std::is_same<decltype(mob.delay2.data)::period, std::chrono::milliseconds::period>::value, "delay2 is milliseconds"); + if (bits[3].data[2].data.is_digit10()) + bits[3].data[2].span.warning("delay1 lacks units; defaulting to ms"_s); + if (bits[3].data[3].data.is_digit10()) + bits[3].data[3].span.warning("delay2 lacks units; defaulting to ms"_s); + TRY_EXTRACT(bits[3].data[2], mob.delay1); + TRY_EXTRACT(bits[3].data[3], mob.delay2); + if (bits[3].data.size() >= 5) + { + TRY_EXTRACT(bits[3].data[4], mob.event); + } + else + { + mob.event.data = NpcEvent(); + mob.event.span = bits[3].data[3].span; + mob.event.span.end.column++; + mob.event.span.begin.column = mob.event.span.end.column; + } + } + else + { + mob.delay1.data = std::chrono::milliseconds::zero(); + mob.delay1.span = bits[3].data[1].span; + mob.delay1.span.end.column++; + mob.delay1.span.begin.column = mob.delay1.span.end.column; + mob.delay2.data = std::chrono::milliseconds::zero(); + mob.delay2.span = mob.delay1.span; + mob.event.data = NpcEvent(); + mob.event.span = mob.delay1.span; + } + return Ok(std::move(mob)); + } + static + Result<MapFlag> parse_mapflag(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits) + { + if (bits.size() != 3 && bits.size() != 4) + { + return Err(span.error_str("expect 3 or 4 |component|s"_s)); + } + if (bits[0].data.size() != 1) + { + return Err(bits[0].span.error_str("expect 1 ,component,s"_s)); + } + assert(bits[1].data.size() == 1); + assert(bits[1].data[0].data == "mapflag"_s); + if (bits[2].data.size() != 1) + { + return Err(bits[2].span.error_str("expect 1 ,component,s"_s)); + } + if (bits.size() >= 4) + { + if (bits[3].data.size() != 1) + { + return Err(bits[3].span.error_str("expect 1 ,component,s"_s)); + } + } + + MapFlag mapflag; + mapflag.span = span; + TRY_EXTRACT(bits[0].data[0], mapflag.m); + mapflag.key_span = bits[1].data[0].span; + TRY_EXTRACT(bits[2].data[0], mapflag.name); + if (bits.size() >= 4) + { + TRY_EXTRACT(bits[3].data[0], mapflag.opt_extra); + } + else + { + mapflag.opt_extra.data = ""_s; + mapflag.opt_extra.span = bits[2].span; + mapflag.opt_extra.span.end.column++; + mapflag.opt_extra.span.begin.column = mapflag.opt_extra.span.end.column; + } + return Ok(std::move(mapflag)); + } + static + Result<ScriptFunction> parse_script_function(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits, io::LineCharReader& lr) + { + // ScriptFunction: function|script|Fun Name{code} + if (bits.size() != 3) + { + return Err(span.error_str("expect 3 |component|s"_s)); + } + assert(bits[0].data.size() == 1); + assert(bits[0].data[0].data == "function"_s); + assert(bits[1].data.size() == 1); + assert(bits[1].data[0].data == "script"_s); + if (bits[2].data.size() != 1) + { + return Err(bits[2].span.error_str("expect 1 ,component,s"_s)); + } + + ScriptFunction script_function; + script_function.span = span; + script_function.key1_span = bits[0].data[0].span; + script_function.key_span = bits[1].data[0].span; + TRY_EXTRACT(bits[2].data[0], script_function.name); + // also expect '{' and parse real script + script_function.body = TRY(script::parse::parse_script_body(lr)); + return Ok(std::move(script_function)); + } + static + Result<ScriptNone> parse_script_none(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits, io::LineCharReader& lr) + { + // ScriptNone: -|script|script name|-1{code} + if (bits.size() != 4) + { + return Err(span.error_str("expect 4 |component|s"_s)); + } + assert(bits[0].data.size() == 1); + assert(bits[0].data[0].data == "-"_s); + assert(bits[1].data.size() == 1); + assert(bits[1].data[0].data == "script"_s); + if (bits[2].data.size() != 1) + { + return Err(bits[2].span.error_str("expect 1 ,component,s"_s)); + } + assert(bits[3].data[0].data == "-1"_s); + if (bits[3].data.size() != 1) + { + return Err(bits[2].span.error_str("last |component| should be just -1"_s)); + } + + ScriptNone script_none; + script_none.span = span; + script_none.key1_span = bits[0].data[0].span; + script_none.key_span = bits[1].data[0].span; + TRY_EXTRACT(bits[2].data[0], script_none.name); + script_none.key4_span = bits[3].data[0].span; + // also expect '{' and parse real script + script_none.body = TRY(script::parse::parse_script_body(lr)); + return Ok(std::move(script_none)); + } + static + Result<ScriptMapNone> parse_script_map_none(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits, io::LineCharReader& lr) + { + // ScriptMapNone: m,x,y,d|script|script name|-1{code} + if (bits.size() != 4) + { + return Err(span.error_str("expect 4 |component|s"_s)); + } + if (bits[0].data.size() != 4) + { + return Err(bits[0].span.error_str("expect 3 ,component,s"_s)); + } + assert(bits[1].data.size() == 1); + assert(bits[1].data[0].data == "script"_s); + if (bits[2].data.size() != 1) + { + return Err(bits[2].span.error_str("expect 1 ,component,s"_s)); + } + if (bits[3].data.size() != 1 || bits[3].data[0].data != "-1"_s) + { + return Err(bits[2].span.error_str("last |component| should be just -1"_s)); + } + + ScriptMapNone script_map_none; + script_map_none.span = span; + TRY_EXTRACT(bits[0].data[0], script_map_none.m); + TRY_EXTRACT(bits[0].data[1], script_map_none.x); + TRY_EXTRACT(bits[0].data[2], script_map_none.y); + TRY_EXTRACT(bits[0].data[3], script_map_none.d); + script_map_none.key_span = bits[1].data[0].span; + TRY_EXTRACT(bits[2].data[0], script_map_none.name); + script_map_none.key4_span = bits[3].data[0].span; + // also expect '{' and parse real script + script_map_none.body = TRY(script::parse::parse_script_body(lr)); + return Ok(std::move(script_map_none)); + } + static + Result<ScriptMap> parse_script_map(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits, io::LineCharReader& lr) + { + // ScriptMap: m,x,y,d|script|script name|class,xs,ys{code} + if (bits.size() != 4) + { + return Err(span.error_str("expect 4 |component|s"_s)); + } + if (bits[0].data.size() != 4) + { + return Err(bits[0].span.error_str("expect 3 ,component,s"_s)); + } + assert(bits[1].data.size() == 1); + assert(bits[1].data[0].data == "script"_s); + if (bits[2].data.size() != 1) + { + return Err(bits[2].span.error_str("expect 1 ,component,s"_s)); + } + if (bits[3].data.size() != 3) + { + return Err(bits[3].span.error_str("expect 3 ,component,s"_s)); + } + + ScriptMap script_map; + script_map.span = span; + TRY_EXTRACT(bits[0].data[0], script_map.m); + TRY_EXTRACT(bits[0].data[1], script_map.x); + TRY_EXTRACT(bits[0].data[2], script_map.y); + TRY_EXTRACT(bits[0].data[3], script_map.d); + script_map.key_span = bits[1].data[0].span; + TRY_EXTRACT(bits[2].data[0], script_map.name); + TRY_EXTRACT(bits[3].data[0], script_map.npc_class); + TRY_EXTRACT(bits[3].data[1], script_map.xs); + TRY_EXTRACT(bits[3].data[2], script_map.ys); + // also expect '{' and parse real script + script_map.body = TRY(script::parse::parse_script_body(lr)); + return Ok(std::move(script_map)); + } + static + Result<std::unique_ptr<Script>> parse_script_any(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits, io::LineCharReader& lr) + { + std::unique_ptr<Script> rv; + // 4 cases: + // ScriptFunction: function|script|Fun Name{code} + // ScriptNone: -|script|script name|-1{code} + // ScriptMapNone: m,x,y,d|script|script name|-1{code} + // ScriptMap: m,x,y,d|script|script name|class,xs,ys{code} + if (bits[0].data[0].data == "function"_s) + { + rv = make_unique<ScriptFunction>(TRY(parse_script_function(span, bits, lr))); + } + else if (bits[0].data[0].data == "-"_s) + { + rv = make_unique<ScriptNone>(TRY(parse_script_none(span, bits, lr))); + } + else if (bits.size() >= 4 && bits[3].data[0].data == "-1"_s) + { + rv = make_unique<ScriptMapNone>(TRY(parse_script_map_none(span, bits, lr))); + } + else + { + rv = make_unique<ScriptMap>(TRY(parse_script_map(span, bits, lr))); + } + return Ok(std::move(rv)); + } + + /// Try to extract a top-level token + /// Return None at EOL, or Some(span) + /// This will alternate betweeen returning words and separators + static + Option<Spanned<RString>> lex(io::LineCharReader& lr, bool first) + { + io::LineChar c; + // at start of line, skip whitespace + if (first) + { + while (lr.get(c) && (/*c.ch() == ' ' ||*/ c.ch() == '\n')) + { + lr.adv(); + } + } + // at end of file, end of line, or start of script, signal end + if (!lr.get(c) || c.ch() == '\n' || c.ch() == '{') + { + return None; + } + // separators are simple + if (c.ch() == '|' || c.ch() == ',') + { + Spanned<RString> bit; + bit.span.begin = c; + bit.span.end = c; + bit.data = RString(VString<1>(c.ch())); + lr.adv(); + return Some(bit); + } + io::LineSpan span; + MString accum; + accum += c.ch(); + span.begin = c; + span.end = c; + lr.adv(); + if (c.ch() != '/') + first = false; + + // if one-char token followed by an end-of-line or separator, stop + if (!lr.get(c) || c.ch() == '\n' || c.ch() == '{' || c.ch() == ',' || c.ch() == '|') + { + Spanned<RString> bit; + bit.span = span; + bit.data = RString(accum); + return Some(bit); + } + + accum += c.ch(); + span.end = c; + lr.adv(); + + // if first token on line, can get comment + if (first && c.ch() == '/') + { + while (lr.get(c) && c.ch() != '\n' && c.ch() != '{') + { + accum += c.ch(); + span.end = c; + lr.adv(); + } + Spanned<RString> bit; + bit.span = span; + bit.data = RString(accum); + return Some(bit); + } + // otherwise, collect until an end of line or separator + while (lr.get(c) && c.ch() != '\n' && c.ch() != '{' && c.ch() != ',' && c.ch() != '|') + { + accum += c.ch(); + span.end = c; + lr.adv(); + } + Spanned<RString> bit; + bit.span = span; + bit.data = RString(accum); + return Some(std::move(bit)); + } + + Result<std::unique_ptr<TopLevel>> parse_top(io::LineCharReader& in) + { + std::unique_ptr<TopLevel> rv; + Spanned<std::vector<Spanned<std::vector<Spanned<RString>>>>> bits; + + { + Spanned<RString> mayc = TRY_UNWRAP(lex(in, true), + { + io::LineChar c; + if (in.get(c) && c.ch() == '{') + { + return Err(c.error_str("Unexpected script open"_s)); + } + return Ok(std::move(rv)); + }); + if (mayc.data.startswith("//"_s)) + { + Comment com; + com.comment = std::move(mayc.data); + com.span = std::move(mayc.span); + rv = make_unique<Comment>(std::move(com)); + return Ok(std::move(rv)); + } + + if (mayc.data == "|"_s || mayc.data == ","_s) + return Err(mayc.span.error_str("Unexpected separator"_s)); + bits.span = mayc.span; + bits.data.emplace_back(); + bits.data.back().span = mayc.span; + bits.data.back().data.push_back(mayc); + } + + while (true) + { + Spanned<RString> sep = TRY_UNWRAP(lex(in, false), + break); + if (sep.data == "|"_s) + { + bits.data.emplace_back(); + } + else if (sep.data != ","_s) + { + return Err(sep.span.error_str("Expected separator"_s)); + } + + Spanned<RString> word = TRY_UNWRAP(lex(in, false), + return Err(sep.span.error_str("Expected word after separator"_s))); + if (bits.data.back().data.empty()) + bits.data.back().span = word.span; + else + bits.data.back().span.end = word.span.end; + bits.span.end = word.span.end; + bits.data.back().data.push_back(std::move(word)); + } + + if (bits.data.size() < 2) + return Err(bits.span.error_str("Expected a line with |s in it"_s)); + for (auto& bit : bits.data) + { + if (bit.data.empty()) + return Err(bit.span.error_str("Empty components are not cool"_s)); + } + if (bits.data[1].data.size() != 1) + return Err(bits.data[1].span.error_str("Expected a single word in type position"_s)); + Spanned<RString>& w2 = bits.data[1].data[0]; + if (w2.data == "warp"_s) + { + rv = make_unique<Warp>(TRY(parse_warp(bits.span, bits.data))); + } + else if (w2.data == "shop"_s) + { + rv = make_unique<Shop>(TRY(parse_shop(bits.span, bits.data))); + } + else if (w2.data == "monster"_s) + { + rv = make_unique<Monster>(TRY(parse_monster(bits.span, bits.data))); + } + else if (w2.data == "mapflag"_s) + { + rv = make_unique<MapFlag>(TRY(parse_mapflag(bits.span, bits.data))); + } + else if (w2.data == "script"_s) + { + rv = TRY_MOVE(parse_script_any(bits.span, bits.data, in)); + } + else + { + return Err(w2.span.error_str("Unknown type"_s)); + } + return Ok(std::move(rv)); + } +} // namespace parse +} // namespace npc +} // namespace tmwa diff --git a/src/ast/npc.hpp b/src/ast/npc.hpp new file mode 100644 index 0000000..e39a704 --- /dev/null +++ b/src/ast/npc.hpp @@ -0,0 +1,135 @@ +#pragma once +// ast/npc.hpp - Structure of non-player characters (including mobs). +// +// Copyright © 2014 Ben Longbons <b.r.longbons@gmail.com> +// +// This file is part of The Mana World (Athena server) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +#include "fwd.hpp" + +#include "../compat/result.hpp" + +#include "../mmo/ids.hpp" +#include "../mmo/strs.hpp" + +#include "../map/map.hpp" + +#include "script.hpp" + + +namespace tmwa +{ +namespace npc +{ +namespace parse +{ + using io::Spanned; + + struct TopLevel + { + io::LineSpan span; + + virtual ~TopLevel(); + }; + struct Comment : TopLevel + { + RString comment; + }; + struct Warp : TopLevel + { + Spanned<MapName> m; + Spanned<unsigned> x, y; + io::LineSpan key_span; + Spanned<NpcName> name; + Spanned<unsigned> xs, ys; + Spanned<MapName> to_m; + Spanned<unsigned> to_x, to_y; + }; + struct ShopItem + { + Spanned<ItemName> name; + Spanned<int> value; + }; + struct Shop : TopLevel + { + Spanned<MapName> m; + Spanned<unsigned> x, y; + Spanned<DIR> d; + io::LineSpan key_span; + Spanned<NpcName> name; + Spanned<Species> npc_class; + Spanned<std::vector<Spanned<ShopItem>>> items; + }; + struct Monster : TopLevel + { + Spanned<MapName> m; + Spanned<unsigned> x, y; + Spanned<unsigned> xs, ys; + io::LineSpan key_span; + Spanned<MobName> name; + Spanned<Species> mob_class; + Spanned<unsigned> num; + Spanned<interval_t> delay1, delay2; + Spanned<NpcEvent> event; + }; + struct MapFlag : TopLevel + { + Spanned<MapName> m; + io::LineSpan key_span; + // TODO should this extract all the way? + Spanned<RString> name; + Spanned<RString> opt_extra; + }; + struct Script : TopLevel + { + io::LineSpan key_span; + // see src/script/parser.hpp + script::parse::ScriptBody body; + }; + struct ScriptFunction : Script + { + io::LineSpan key1_span; + Spanned<RString> name; + }; + struct ScriptNone : Script + { + io::LineSpan key1_span; + Spanned<NpcName> name; + io::LineSpan key4_span; + }; + struct ScriptMapNone : Script + { + Spanned<MapName> m; + Spanned<unsigned> x, y; + Spanned<DIR> d; + Spanned<NpcName> name; + io::LineSpan key4_span; + }; + struct ScriptMap : Script + { + Spanned<MapName> m; + Spanned<unsigned> x, y; + Spanned<DIR> d; + Spanned<NpcName> name; + Spanned<Species> npc_class; + Spanned<unsigned> xs, ys; + }; + // other Script subclasses elsewhere? (for item and magic scripts) + + Result<std::unique_ptr<TopLevel>> parse_top(io::LineCharReader& in); +} // namespace parse +} // namespace npc +} // namespace tmwa diff --git a/src/ast/npc_test.cpp b/src/ast/npc_test.cpp new file mode 100644 index 0000000..2697351 --- /dev/null +++ b/src/ast/npc_test.cpp @@ -0,0 +1,569 @@ +#include "npc.hpp" +// ast/npc_test.cpp - Testsuite for npc parser. +// +// Copyright © 2014 Ben Longbons <b.r.longbons@gmail.com> +// +// This file is part of The Mana World (Athena server) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +#include <gtest/gtest.h> + +#include "../tests/fdhack.hpp" + +//#include "../poison.hpp" + + +namespace tmwa +{ +namespace npc +{ +namespace parse +{ + static + io::FD string_pipe(ZString sz) + { + io::FD rfd, wfd; + if (-1 == io::FD::pipe(rfd, wfd)) + return io::FD(); + if (sz.size() != wfd.write(sz.c_str(), sz.size())) + { + rfd.close(); + wfd.close(); + return io::FD(); + } + wfd.close(); + return rfd; + } + +#define EXPECT_SPAN(span, bl,bc, el,ec) \ + ({ \ + EXPECT_EQ((span).begin.line, bl); \ + EXPECT_EQ((span).begin.column, bc); \ + EXPECT_EQ((span).end.line, el); \ + EXPECT_EQ((span).end.column, ec); \ + }) + + TEST(ast, eof) + { + QuietFd q; + LString inputs[] = + { + ""_s, + "\n"_s, + "\n\n"_s, + }; + for (auto input : inputs) + { + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_EQ(res.get_success(), Some(std::unique_ptr<TopLevel>(nullptr))); + } + } + TEST(ast, comment) + { + QuietFd q; + LString inputs[] = + { + //23456789 + "// hello"_s, + "// hello\n "_s, + "// hello\nabc"_s, + }; + for (auto input : inputs) + { + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_TRUE(res.get_success().is_some()); + auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL()); + EXPECT_SPAN(top->span, 1,1, 1,8); + auto p = dynamic_cast<Comment *>(top.get()); + EXPECT_TRUE(p); + if (p) + { + EXPECT_EQ(p->comment, "// hello"_s); + } + } + } + TEST(ast, warp) + { + QuietFd q; + LString inputs[] = + { + // 1 2 3 4 + //234567890123456789012345678901234567890123456789 + "map.gat,1,2|warp|To Other Map|3,4,other.gat,5,6"_s, + "map.gat,1,2|warp|To Other Map|3,4,other.gat,5,6\n"_s, + "map.gat,1,2|warp|To Other Map|3,4,other.gat,5,6{"_s, + // no optional fields in warp + }; + for (auto input : inputs) + { + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_TRUE(res.get_success().is_some()); + auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL()); + EXPECT_SPAN(top->span, 1,1, 1,47); + auto p = dynamic_cast<Warp *>(top.get()); + EXPECT_TRUE(p); + if (p) + { + EXPECT_SPAN(p->m.span, 1,1, 1,7); + EXPECT_EQ(p->m.data, stringish<MapName>("map"_s)); + EXPECT_SPAN(p->x.span, 1,9, 1,9); + EXPECT_EQ(p->x.data, 1); + EXPECT_SPAN(p->y.span, 1,11, 1,11); + EXPECT_EQ(p->y.data, 2); + EXPECT_SPAN(p->key_span, 1,13, 1,16); + EXPECT_SPAN(p->name.span, 1,18, 1,29); + EXPECT_EQ(p->name.data, stringish<NpcName>("To Other Map"_s)); + EXPECT_SPAN(p->xs.span, 1,31, 1,31); + EXPECT_EQ(p->xs.data, 3); + EXPECT_SPAN(p->ys.span, 1,33, 1,33); + EXPECT_EQ(p->ys.data, 4); + EXPECT_SPAN(p->to_m.span, 1,35, 1,43); + EXPECT_EQ(p->to_m.data, stringish<MapName>("other"_s)); + EXPECT_SPAN(p->to_x.span, 1,45, 1,45); + EXPECT_EQ(p->to_x.data, 5); + EXPECT_SPAN(p->to_y.span, 1,47, 1,47); + EXPECT_EQ(p->to_y.data, 6); + } + } + } + TEST(ast, shop) + { + QuietFd q; + LString inputs[] = + { + // 1 2 3 4 5 + //2345678901234567890123456789012345678901234567890123456789 + "map.gat,1,2,3|shop|Flower Shop|4,5:6,Named:7,Spaced :8"_s, + "map.gat,1,2,3|shop|Flower Shop|4,5:6,Named:7,Spaced :8\n"_s, + "map.gat,1,2,3|shop|Flower Shop|4,5:6,Named:7,Spaced :8{"_s, + // no optional fields in shop + }; + for (auto input : inputs) + { + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_TRUE(res.get_success().is_some()); + auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL()); + EXPECT_SPAN(top->span, 1,1, 1,54); + auto p = dynamic_cast<Shop *>(top.get()); + EXPECT_TRUE(p); + if (p) + { + EXPECT_SPAN(p->m.span, 1,1, 1,7); + EXPECT_EQ(p->m.data, stringish<MapName>("map"_s)); + EXPECT_SPAN(p->x.span, 1,9, 1,9); + EXPECT_EQ(p->x.data, 1); + EXPECT_SPAN(p->y.span, 1,11, 1,11); + EXPECT_EQ(p->y.data, 2); + EXPECT_SPAN(p->d.span, 1,13, 1,13); + EXPECT_EQ(p->d.data, DIR::NW); + EXPECT_SPAN(p->key_span, 1,15, 1,18); + EXPECT_SPAN(p->name.span, 1,20, 1,30); + EXPECT_EQ(p->name.data, stringish<NpcName>("Flower Shop"_s)); + EXPECT_SPAN(p->npc_class.span, 1,32, 1,32); + EXPECT_EQ(p->npc_class.data, wrap<Species>(4)); + EXPECT_SPAN(p->items.span, 1,34, 1,54); + EXPECT_EQ(p->items.data.size(), 3); + EXPECT_SPAN(p->items.data[0].span, 1,34, 1,36); + EXPECT_SPAN(p->items.data[0].data.name.span, 1,34, 1,34); + EXPECT_EQ(p->items.data[0].data.name.data, stringish<ItemName>("5"_s)); + EXPECT_SPAN(p->items.data[0].data.value.span, 1,36, 1,36); + EXPECT_EQ(p->items.data[0].data.value.data, 6); + EXPECT_SPAN(p->items.data[1].span, 1,38, 1,44); + EXPECT_SPAN(p->items.data[1].data.name.span, 1,38, 1,42); + EXPECT_EQ(p->items.data[1].data.name.data, stringish<ItemName>("Named"_s)); + EXPECT_SPAN(p->items.data[1].data.value.span, 1,44, 1,44); + EXPECT_EQ(p->items.data[1].data.value.data, 7); + EXPECT_SPAN(p->items.data[2].span, 1,46, 1,54); + EXPECT_SPAN(p->items.data[2].data.name.span, 1,46, 1,52); + EXPECT_EQ(p->items.data[2].data.name.data, stringish<ItemName>("Spaced"_s)); + EXPECT_SPAN(p->items.data[2].data.value.span, 1,54, 1,54); + EXPECT_EQ(p->items.data[2].data.value.data, 8); + } + } + } + TEST(ast, monster) + { + QuietFd q; + LString inputs[] = + { + // 1 2 3 4 5 6 + //23456789012345678901234567890123456789012345678901234567890123456789 + "map.gat,1,2,3,4|monster|Feeping Creature|5,6,7000,8000,Npc::Event"_s, + "map.gat,1,2,3,4|monster|Feeping Creature|5,6,7000,8000,Npc::Event\n"_s, + "map.gat,1,2,3,4|monster|Feeping Creature|5,6,7000,8000,Npc::Event{"_s, + "Map.gat,1,2,3,4|monster|Feeping Creature|5,6,7000,8000"_s, + "Map.gat,1,2,3,4|monster|Feeping Creature|5,6,7000,8000\n"_s, + "Map.gat,1,2,3,4|monster|Feeping Creature|5,6,7000,8000{"_s, + "nap.gat,1,20304|monster|Feeping Creature|506,700008000"_s, + "nap.gat,1,20304|monster|Feeping Creature|506,700008000\n"_s, + "nap.gat,1,20304|monster|Feeping Creature|506,700008000{"_s, + }; + for (auto input : inputs) + { + bool first = input.startswith('m'); + bool second = input.startswith('M'); + bool third = input.startswith('n'); + assert(first + second + third == 1); + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_TRUE(res.get_success().is_some()); + auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL()); + EXPECT_SPAN(top->span, 1,1, 1,first?65:54); + auto p = dynamic_cast<Monster *>(top.get()); + EXPECT_TRUE(p); + if (p) + { + EXPECT_SPAN(p->m.span, 1,1, 1,7); + if (first) + { + EXPECT_EQ(p->m.data, stringish<MapName>("map"_s)); + } + else if (second) + { + EXPECT_EQ(p->m.data, stringish<MapName>("Map"_s)); + } + else + { + EXPECT_EQ(p->m.data, stringish<MapName>("nap"_s)); + } + EXPECT_SPAN(p->x.span, 1,9, 1,9); + EXPECT_EQ(p->x.data, 1); + if (!third) + { + EXPECT_SPAN(p->y.span, 1,11, 1,11); + EXPECT_EQ(p->y.data, 2); + EXPECT_SPAN(p->xs.span, 1,13, 1,13); + EXPECT_EQ(p->xs.data, 3); + EXPECT_SPAN(p->ys.span, 1,15, 1,15); + EXPECT_EQ(p->ys.data, 4); + } + else + { + EXPECT_SPAN(p->y.span, 1,11, 1,15); + EXPECT_EQ(p->y.data, 20304); + EXPECT_SPAN(p->xs.span, 1,16, 1,16); + EXPECT_EQ(p->xs.data, 0); + EXPECT_SPAN(p->ys.span, 1,16, 1,16); + EXPECT_EQ(p->ys.data, 0); + } + EXPECT_SPAN(p->key_span, 1,17, 1,23); + EXPECT_SPAN(p->name.span, 1,25, 1,40); + EXPECT_EQ(p->name.data, stringish<MobName>("Feeping Creature"_s)); + if (!third) + { + EXPECT_SPAN(p->mob_class.span, 1,42, 1,42); + EXPECT_EQ(p->mob_class.data, wrap<Species>(5)); + EXPECT_SPAN(p->num.span, 1,44, 1,44); + EXPECT_EQ(p->num.data, 6); + EXPECT_SPAN(p->delay1.span, 1,46, 1,49); + EXPECT_EQ(p->delay1.data, 7_s); + EXPECT_SPAN(p->delay2.span, 1,51, 1,54); + EXPECT_EQ(p->delay2.data, 8_s); + } + else + { + EXPECT_SPAN(p->mob_class.span, 1,42, 1,44); + EXPECT_EQ(p->mob_class.data, wrap<Species>(506)); + EXPECT_SPAN(p->num.span, 1,46, 1,54); + EXPECT_EQ(p->num.data, 700008000); + EXPECT_SPAN(p->delay1.span, 1,55, 1,55); + EXPECT_EQ(p->delay1.data, 0_s); + EXPECT_SPAN(p->delay2.span, 1,55, 1,55); + EXPECT_EQ(p->delay2.data, 0_s); + } + if (first) + { + EXPECT_SPAN(p->event.span, 1,56, 1,65); + EXPECT_EQ(p->event.data.npc, stringish<NpcName>("Npc"_s)); + EXPECT_EQ(p->event.data.label, stringish<ScriptLabel>("Event"_s)); + } + else + { + EXPECT_SPAN(p->event.span, 1,55, 1,55); + EXPECT_EQ(p->event.data.npc, NpcName()); + EXPECT_EQ(p->event.data.label, ScriptLabel()); + } + } + } + } + TEST(ast, mapflag) + { + QuietFd q; + LString inputs[] = + { + // 1 2 3 + //23456789012345678901234567890123456789 + "map.gat|mapflag|flagname"_s, + "map.gat|mapflag|flagname\n"_s, + "map.gat|mapflag|flagname{"_s, + "Map.gat|mapflag|flagname|optval"_s, + "Map.gat|mapflag|flagname|optval\n"_s, + "Map.gat|mapflag|flagname|optval{"_s, + }; + for (auto input : inputs) + { + bool second = input.startswith('M'); + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_TRUE(res.get_success().is_some()); + auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL()); + EXPECT_SPAN(top->span, 1,1, 1,!second?24:31); + auto p = dynamic_cast<MapFlag *>(top.get()); + EXPECT_TRUE(p); + if (p) + { + EXPECT_SPAN(p->m.span, 1,1, 1,7); + if (!second) + { + EXPECT_EQ(p->m.data, stringish<MapName>("map"_s)); + } + else + { + EXPECT_EQ(p->m.data, stringish<MapName>("Map"_s)); + } + EXPECT_SPAN(p->key_span, 1,9, 1,15); + EXPECT_SPAN(p->name.span, 1,17, 1,24); + EXPECT_EQ(p->name.data, "flagname"_s); + if (!second) + { + EXPECT_SPAN(p->opt_extra.span, 1,25, 1,25); + EXPECT_EQ(p->opt_extra.data, ""_s); + } + else + { + EXPECT_SPAN(p->opt_extra.span, 1,26, 1,31); + EXPECT_EQ(p->opt_extra.data, "optval"_s); + } + } + } + } + + TEST(ast, scriptfun) + { + QuietFd q; + LString inputs[] = + { + // 1 2 3 + //23456789012345678901234567890123456789 + "function|script|Fun Name{end;}"_s, + // 123456 + "function|script|Fun Name\n{end;}\n"_s, + // 1234567 + "function|script|Fun Name\n \n {end;} "_s, + }; + for (auto input : inputs) + { + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_TRUE(res.get_success().is_some()); + auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL()); + EXPECT_SPAN(top->span, 1,1, 1,24); + auto p = dynamic_cast<ScriptFunction *>(top.get()); + EXPECT_TRUE(p); + if (p) + { + EXPECT_SPAN(p->key1_span, 1,1, 1,8); + EXPECT_SPAN(p->key_span, 1,10, 1,15); + EXPECT_SPAN(p->name.span, 1,17, 1,24); + EXPECT_EQ(p->name.data, "Fun Name"_s); + if (input.endswith('}')) + { + EXPECT_SPAN(p->body.span, 1,25, 1,30); + } + else if (input.endswith('\n')) + { + EXPECT_SPAN(p->body.span, 2,1, 2,6); + } + else if (input.endswith(' ')) + { + EXPECT_SPAN(p->body.span, 3,2, 3,7); + } + else + { + FAIL(); + } + EXPECT_EQ(p->body.braced_body, "{end;}"_s); + } + } + } + TEST(ast, scriptnone) + { + QuietFd q; + LString inputs[] = + { + // 1 2 3 + //23456789012345678901234567890123456789 + "-|script|#config|-1{end;}"_s, + // 123456 + "-|script|#config|-1\n{end;}\n"_s, + // 1234567 + "-|script|#config|-1\n \n {end;} "_s, + }; + for (auto input : inputs) + { + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_TRUE(res.get_success().is_some()); + auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL()); + EXPECT_SPAN(top->span, 1,1, 1,19); + auto p = dynamic_cast<ScriptNone *>(top.get()); + EXPECT_TRUE(p); + if (p) + { + EXPECT_SPAN(p->key1_span, 1,1, 1,1); + EXPECT_SPAN(p->key_span, 1,3, 1,8); + EXPECT_SPAN(p->name.span, 1,10, 1,16); + EXPECT_EQ(p->name.data, stringish<NpcName>("#config"_s)); + EXPECT_SPAN(p->key4_span, 1,18, 1,19); + if (input.endswith('}')) + { + EXPECT_SPAN(p->body.span, 1,20, 1,25); + } + else if (input.endswith('\n')) + { + EXPECT_SPAN(p->body.span, 2,1, 2,6); + } + else if (input.endswith(' ')) + { + EXPECT_SPAN(p->body.span, 3,2, 3,7); + } + else + { + FAIL(); + } + EXPECT_EQ(p->body.braced_body, "{end;}"_s); + } + } + } + TEST(ast, scriptmapnone) + { + QuietFd q; + LString inputs[] = + { + // 1 2 3 + //23456789012345678901234567890123456789 + "map.gat,1,2,3|script|Init|-1{end;}"_s, + "map.gat,1,2,3|script|Init|-1\n{end;}\n"_s, + "map.gat,1,2,3|script|Init|-1\n \n {end;} "_s, + }; + for (auto input : inputs) + { + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_TRUE(res.get_success().is_some()); + auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL()); + EXPECT_SPAN(top->span, 1,1, 1,28); + auto p = dynamic_cast<ScriptMapNone *>(top.get()); + EXPECT_TRUE(p); + if (p) + { + EXPECT_SPAN(p->m.span, 1,1, 1,7); + EXPECT_EQ(p->m.data, stringish<MapName>("map"_s)); + EXPECT_SPAN(p->x.span, 1,9, 1,9); + EXPECT_EQ(p->x.data, 1); + EXPECT_SPAN(p->y.span, 1,11, 1,11); + EXPECT_EQ(p->y.data, 2); + EXPECT_SPAN(p->d.span, 1,13, 1,13); + EXPECT_EQ(p->d.data, DIR::NW); + EXPECT_SPAN(p->key_span, 1,15, 1,20); + EXPECT_SPAN(p->name.span, 1,22, 1,25); + EXPECT_EQ(p->name.data, stringish<NpcName>("Init"_s)); + EXPECT_SPAN(p->key4_span, 1,27, 1,28); + if (input.endswith('}')) + { + EXPECT_SPAN(p->body.span, 1,29, 1,34); + } + else if (input.endswith('\n')) + { + EXPECT_SPAN(p->body.span, 2,1, 2,6); + } + else if (input.endswith(' ')) + { + EXPECT_SPAN(p->body.span, 3,2, 3,7); + } + else + { + FAIL(); + } + EXPECT_EQ(p->body.braced_body, "{end;}"_s); + } + } + } + TEST(ast, scriptmap) + { + QuietFd q; + LString inputs[] = + { + // 1 2 3 + //23456789012345678901234567890123456789 + "map.gat,1,2,3|script|Asdf|4,5,6{end;}"_s, + "map.gat,1,2,3|script|Asdf|4,5,6\n{end;}\n"_s, + "map.gat,1,2,3|script|Asdf|4,5,6\n \n {end;} "_s, + }; + for (auto input : inputs) + { + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_TRUE(res.get_success().is_some()); + auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL()); + EXPECT_SPAN(top->span, 1,1, 1,31); + auto p = dynamic_cast<ScriptMap *>(top.get()); + EXPECT_TRUE(p); + if (p) + { + EXPECT_SPAN(p->m.span, 1,1, 1,7); + EXPECT_EQ(p->m.data, stringish<MapName>("map"_s)); + EXPECT_SPAN(p->x.span, 1,9, 1,9); + EXPECT_EQ(p->x.data, 1); + EXPECT_SPAN(p->y.span, 1,11, 1,11); + EXPECT_EQ(p->y.data, 2); + EXPECT_SPAN(p->d.span, 1,13, 1,13); + EXPECT_EQ(p->d.data, DIR::NW); + EXPECT_SPAN(p->key_span, 1,15, 1,20); + EXPECT_SPAN(p->name.span, 1,22, 1,25); + EXPECT_EQ(p->name.data, stringish<NpcName>("Asdf"_s)); + EXPECT_SPAN(p->npc_class.span, 1,27, 1,27); + EXPECT_EQ(p->npc_class.data, wrap<Species>(4)); + EXPECT_SPAN(p->xs.span, 1,29, 1,29); + EXPECT_EQ(p->xs.data, 5); + EXPECT_SPAN(p->ys.span, 1,31, 1,31); + EXPECT_EQ(p->ys.data, 6); + if (input.endswith('}')) + { + EXPECT_SPAN(p->body.span, 1,32, 1,37); + } + else if (input.endswith('\n')) + { + EXPECT_SPAN(p->body.span, 2,1, 2,6); + } + else if (input.endswith(' ')) + { + EXPECT_SPAN(p->body.span, 3,2, 3,7); + } + else + { + FAIL(); + } + EXPECT_EQ(p->body.braced_body, "{end;}"_s); + } + } + } +} // namespace parse +} // namespace npc +} // namespace tmwa diff --git a/src/ast/script.cpp b/src/ast/script.cpp new file mode 100644 index 0000000..cc67224 --- /dev/null +++ b/src/ast/script.cpp @@ -0,0 +1,71 @@ +#include "script.hpp" +// ast/script.cpp - Structure of tmwa-script +// +// Copyright © 2014 Ben Longbons <b.r.longbons@gmail.com> +// +// This file is part of The Mana World (Athena server) +// +// This program 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. +// +// This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +#include "../poison.hpp" + + +namespace tmwa +{ +namespace script +{ +namespace parse +{ + Result<ScriptBody> parse_script_body(io::LineCharReader& lr) + { + io::LineSpan span; + io::LineChar c; + while (true) + { + if (!lr.get(c)) + { + return Err("error: unexpected EOF before '{' in parse_script_body"_s); + } + if (c.ch() == ' ' || c.ch() == '\n') + { + lr.adv(); + continue; + } + break; + } + if (c.ch() != '{') + { + return Err(c.error_str("expected opening '{'"_s)); + } + + MString accum; + accum += c.ch(); + span.begin = c; + lr.adv(); + while (true) + { + if (!lr.get(c)) + return Err(c.error_str("unexpected EOF before '}' in parse_script_body"_s)); + accum += c.ch(); + span.end = c; + lr.adv(); + if (c.ch() == '}') + { + return Ok(ScriptBody{RString(accum), std::move(span)}); + } + } + } +} // namespace parse +} // namespace script +} // namespace tmwa diff --git a/src/ast/script.hpp b/src/ast/script.hpp new file mode 100644 index 0000000..59e53f0 --- /dev/null +++ b/src/ast/script.hpp @@ -0,0 +1,94 @@ +#pragma once +// ast/script.hpp - Structure of tmwa-script +// +// Copyright © 2014 Ben Longbons <b.r.longbons@gmail.com> +// +// This file is part of The Mana World (Athena server) +// +// This program 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. +// +// This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +#include "fwd.hpp" + +#include "../compat/result.hpp" + +#include "../io/line.hpp" + + +namespace tmwa +{ +namespace script +{ +namespace parse +{ + using io::Spanned; + + struct ScriptBody + { + RString braced_body; + io::LineSpan span; + }; + + Result<ScriptBody> parse_script_body(io::LineCharReader& lr); + + /* + (-- First bare-body-chunk only allowed for npcs, items, magic, functions. + It is not allowed for events. Basically it's an implicit label at times. + Last normal-lines is only permitted on item and magic scripts. --) + { script-body }: "{" bare-body-chunk? body-chunk* normal-lines? "}" + body-chunk: (comment* labelname ":")+ bare-body-chunk + bare-body-chunk: normal-lines terminator-line + normal-lines: normal-line* + any-line: normal-line + any-line: terminator-line + normal-line: "if" "(" expr ")" any-line + normal-line: normal-command ((expr ",")* expr)? ";" + terminator-line: "menu" (expr, labelname)* expr, labelname ";" + terminator-line: "goto" labelname ";" + terminator-line: terminator ((expr ",")* expr)? ";" + terminator: "return" + terminator: "close" + terminator: "end" + terminator: "mapexit" + terminator: "shop" + + expr: subexpr_-1 + subexpr_N: ("+" | "-" | "!" | "~") subexpr_7 + subexpr_N: simple-expr (op_M subexpr_M | "(" ((expr ",")+ expr)? ")")* if N < M; function call only if N < 8 and preceding simple-expr (op sub)* is a known function + op_0: "||" + op_1: "&&" + op_2: "==" + op_2: "!=" + op_2: ">=" + op_2: ">" + op_2: "<=" + op_2: "<" + op_3: "^" + op_4: "|" + op_5: "&" + op_5: ">>" + op_5: "<<" + op_6: "+" + op_6: "-" + op_7: "*" + op_7: "/" + op_7: "%" + simple-expr: "(" expr ")" + simple-expr: integer + simple-expr: string + simple-expr: variable ("[" expr "]")? + simple-expr: function // no longer command/label though + */ +} // namespace parse +} // namespace script +} // namespace tmwa |