summaryrefslogtreecommitdiff
path: root/src/ast
diff options
context:
space:
mode:
Diffstat (limited to 'src/ast')
-rw-r--r--src/ast/fwd.hpp27
-rw-r--r--src/ast/npc.cpp594
-rw-r--r--src/ast/npc.hpp135
-rw-r--r--src/ast/npc_test.cpp569
-rw-r--r--src/ast/script.cpp71
-rw-r--r--src/ast/script.hpp94
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