summaryrefslogtreecommitdiff
path: root/src/ast
diff options
context:
space:
mode:
Diffstat (limited to 'src/ast')
-rw-r--r--src/ast/fwd.hpp45
-rw-r--r--src/ast/item.cpp155
-rw-r--r--src/ast/item.hpp82
-rw-r--r--src/ast/item_test.cpp150
-rw-r--r--src/ast/npc.cpp620
-rw-r--r--src/ast/npc.hpp142
-rw-r--r--src/ast/npc_test.cpp556
-rw-r--r--src/ast/quest.cpp125
-rw-r--r--src/ast/quest.hpp65
-rw-r--r--src/ast/script.cpp75
-rw-r--r--src/ast/script.hpp112
11 files changed, 2127 insertions, 0 deletions
diff --git a/src/ast/fwd.hpp b/src/ast/fwd.hpp
new file mode 100644
index 0000000..24bf545
--- /dev/null
+++ b/src/ast/fwd.hpp
@@ -0,0 +1,45 @@
+#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"
+
+#include "../compat/fwd.hpp" // rank 2
+#include "../io/fwd.hpp" // rank 4
+#include "../net/fwd.hpp" // rank 5
+#include "../sexpr/fwd.hpp" // rank 5
+#include "../mmo/fwd.hpp" // rank 6
+#include "../high/fwd.hpp" // rank 9
+// ast/fwd.hpp is rank 10
+
+namespace tmwa
+{
+namespace ast
+{
+namespace npc
+{
+class Warp;
+} // namespace npc
+namespace script
+{
+class ScriptBody;
+} // namespace script
+} // namespace ast
+// meh, add more when I feel like it
+} // namespace tmwa
diff --git a/src/ast/item.cpp b/src/ast/item.cpp
new file mode 100644
index 0000000..bd4f295
--- /dev/null
+++ b/src/ast/item.cpp
@@ -0,0 +1,155 @@
+#include "item.hpp"
+// ast/item.cpp - Structure of itemdb
+//
+// 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/extract.hpp"
+#include "../io/line.hpp"
+
+#include "../mmo/extract_enums.hpp"
+
+#include "../poison.hpp"
+
+
+namespace tmwa
+{
+namespace ast
+{
+namespace item
+{
+ using io::respan;
+
+ static
+ void skip_comma_space(io::LineCharReader& lr)
+ {
+ io::LineChar c;
+ if (lr.get(c) && c.ch() == ',')
+ {
+ lr.adv();
+ while (lr.get(c) && c.ch() == ' ')
+ {
+ lr.adv();
+ }
+ }
+ }
+ static
+ Option<Spanned<RString>> lex_nonscript(io::LineCharReader& lr, bool first)
+ {
+ io::LineChar c;
+ if (first)
+ {
+ while (lr.get(c) && c.ch() == '\n')
+ {
+ lr.adv();
+ }
+ }
+ if (!lr.get(c))
+ {
+ return None;
+ }
+ io::LineSpan span;
+ MString accum;
+ accum += c.ch();
+ span.begin = c;
+ span.end = c;
+ lr.adv();
+ if (c.ch() != '/')
+ first = false;
+
+ if (first && lr.get(c) && c.ch() == '/')
+ {
+ accum += c.ch();
+ span.end = c;
+ lr.adv();
+ while (lr.get(c) && c.ch() != '\n')
+ {
+ accum += c.ch();
+ span.end = c;
+ lr.adv();
+ }
+ return Some(respan(span, RString(accum)));
+ }
+
+ while (lr.get(c) && c.ch() != ',' && c.ch() != '\n')
+ {
+ accum += c.ch();
+ span.end = c;
+ lr.adv();
+ }
+ skip_comma_space(lr);
+ return Some(respan(span, RString(accum)));
+ }
+
+ static
+ Result<ast::script::ScriptBody> lex_script(io::LineCharReader& lr)
+ {
+ ast::script::ScriptOptions opt;
+ opt.implicit_start = true;
+ opt.implicit_end = true;
+ opt.one_line = true;
+ opt.no_event = true;
+ auto rv = ast::script::parse_script_body(lr, opt);
+ if (rv.get_success().is_some())
+ {
+ skip_comma_space(lr);
+ }
+ return rv;
+ }
+
+#define SPAN_EXTRACT(bitexpr, var) ({ auto bit = bitexpr; if (!extract(bit.data, &var.data)) return Err(bit.span.error_str("failed to extract "_s #var)); var.span = bit.span; })
+
+#define EOL_ERROR(lr) ({ io::LineChar c; lr.get(c) ? Err(c.error_str("unexpected EOL"_s)) : Err("unexpected EOF before unexpected EOL"_s); })
+ Option<Result<ItemOrComment>> parse_item(io::LineCharReader& lr)
+ {
+ Spanned<RString> first = TRY_UNWRAP(lex_nonscript(lr, true), return None);
+ if (first.data.startswith("//"_s))
+ {
+ Comment comment;
+ comment.comment = first.data;
+ ItemOrComment rv = std::move(comment);
+ rv.span = first.span;
+ return Some(Ok(std::move(rv)));
+ }
+ Item item;
+ SPAN_EXTRACT(first, item.id);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.name);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.jname);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.type);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.buy_price);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.sell_price);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.weight);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.atk);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.def);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.range);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.magic_bonus);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.slot_unused);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.gender);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.loc);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.wlv);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.elv);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.view);
+ item.use_script = TRY(lex_script(lr));
+ item.equip_script = TRY(lex_script(lr));
+ ItemOrComment rv = std::move(item);
+ rv.span.begin = item.id.span.begin;
+ rv.span.end = item.equip_script.span.end;
+ return Some(Ok(std::move(rv)));
+ }
+} // namespace item
+} // namespace ast
+} // namespace tmwa
diff --git a/src/ast/item.hpp b/src/ast/item.hpp
new file mode 100644
index 0000000..a8fe908
--- /dev/null
+++ b/src/ast/item.hpp
@@ -0,0 +1,82 @@
+#pragma once
+// ast/item.hpp - Structure of tmwa itemdb
+//
+// 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/span.hpp"
+
+#include "../sexpr/variant.hpp"
+
+#include "../mmo/clif.t.hpp"
+#include "../mmo/ids.hpp"
+#include "../mmo/strs.hpp"
+
+#include "script.hpp"
+
+
+namespace tmwa
+{
+namespace ast
+{
+namespace item
+{
+ using io::Spanned;
+
+ struct Comment
+ {
+ RString comment;
+ };
+ struct Item
+ {
+ Spanned<ItemNameId> id;
+ Spanned<ItemName> name;
+ Spanned<ItemName> jname;
+ Spanned<ItemType> type;
+ Spanned<int> buy_price;
+ Spanned<int> sell_price;
+ Spanned<int> weight;
+ Spanned<int> atk;
+ Spanned<int> def;
+ Spanned<int> range;
+ Spanned<int> magic_bonus;
+ Spanned<RString> slot_unused;
+ Spanned<SEX> gender;
+ Spanned<EPOS> loc;
+ Spanned<int> wlv;
+ Spanned<int> elv;
+ Spanned<ItemLook> view;
+ ast::script::ScriptBody use_script;
+ ast::script::ScriptBody equip_script;
+ };
+
+ using ItemOrCommentBase = Variant<Comment, Item>;
+ struct ItemOrComment : ItemOrCommentBase
+ {
+ ItemOrComment(Comment o) : ItemOrCommentBase(std::move(o)) {}
+ ItemOrComment(Item o) : ItemOrCommentBase(std::move(o)) {}
+ io::LineSpan span;
+ };
+
+ Option<Result<ItemOrComment>> parse_item(io::LineCharReader& lr);
+} // namespace item
+} // namespace ast
+} // namespace tmwa
diff --git a/src/ast/item_test.cpp b/src/ast/item_test.cpp
new file mode 100644
index 0000000..a77662e
--- /dev/null
+++ b/src/ast/item_test.cpp
@@ -0,0 +1,150 @@
+#include "item.hpp"
+// ast/item_test.cpp - Testsuite for itemdb 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 "../io/line.hpp"
+
+#include "../tests/fdhack.hpp"
+
+#include "../poison.hpp"
+
+
+namespace tmwa
+{
+namespace ast
+{
+namespace item
+{
+#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(itemast, eof)
+ {
+ QuietFd q;
+ LString inputs[] =
+ {
+ ""_s,
+ "\n"_s,
+ "\n\n"_s,
+ };
+ for (auto input : inputs)
+ {
+ io::LineCharReader lr(io::from_string, "<string>"_s, input);
+ auto res = parse_item(lr);
+ EXPECT_TRUE(res.is_none());
+ }
+ }
+ TEST(itemast, comment)
+ {
+ QuietFd q;
+ LString inputs[] =
+ {
+ //23456789
+ "// hello"_s,
+ "// hello\n "_s,
+ "// hello\nabc"_s,
+ };
+ for (auto input : inputs)
+ {
+ io::LineCharReader lr(io::from_string, "<string>"_s, input);
+ auto res = TRY_UNWRAP(parse_item(lr), FAIL());
+ 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 = top.get_if<Comment>();
+ EXPECT_TRUE(p);
+ if (p)
+ {
+ EXPECT_EQ(p->comment, "// hello"_s);
+ }
+ }
+ }
+ TEST(itemast, item)
+ {
+ QuietFd q;
+ LString inputs[] =
+ {
+ // 1 2 3 4 5
+ //2345678901234567890123456789012345678901234567890123456789
+ "1,abc , def,3,4,5,6,7,8,9,10,xx,2,16,12,13,11, {end;}, {}"_s,
+ "1,abc , def,3,4,5,6,7,8,9,10,xx,2,16,12,13,11, {end;}, {}\n"_s,
+ "1,abc , def,3,4,5,6,7,8,9,10,xx,2,16,12,13,11, {end;}, {}\nabc"_s,
+ };
+ for (auto input : inputs)
+ {
+ io::LineCharReader lr(io::from_string, "<string>"_s, input);
+ auto res = TRY_UNWRAP(parse_item(lr), FAIL());
+ EXPECT_TRUE(res.get_success().is_some());
+ auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL());
+ EXPECT_SPAN(top.span, 1,1, 1,58);
+ auto p = top.get_if<Item>();
+ EXPECT_TRUE(p);
+ if (p)
+ {
+ EXPECT_SPAN(p->id.span, 1,1, 1,1);
+ EXPECT_EQ(p->id.data, wrap<ItemNameId>(1));
+ EXPECT_SPAN(p->name.span, 1,3, 1,6);
+ EXPECT_EQ(p->name.data, stringish<ItemName>("abc "_s));
+ EXPECT_SPAN(p->jname.span, 1,10, 1,12);
+ EXPECT_EQ(p->jname.data, stringish<ItemName>("def"_s));
+ EXPECT_SPAN(p->type.span, 1,14, 1,14);
+ EXPECT_EQ(p->type.data, ItemType::JUNK);
+ EXPECT_SPAN(p->buy_price.span, 1,16, 1,16);
+ EXPECT_EQ(p->buy_price.data, 4);
+ EXPECT_SPAN(p->sell_price.span, 1,18, 1,18);
+ EXPECT_EQ(p->sell_price.data, 5);
+ EXPECT_SPAN(p->weight.span, 1,20, 1,20);
+ EXPECT_EQ(p->weight.data, 6);
+ EXPECT_SPAN(p->atk.span, 1,22, 1,22);
+ EXPECT_EQ(p->atk.data, 7);
+ EXPECT_SPAN(p->def.span, 1,24, 1,24);
+ EXPECT_EQ(p->def.data, 8);
+ EXPECT_SPAN(p->range.span, 1,26, 1,26);
+ EXPECT_EQ(p->range.data, 9);
+ EXPECT_SPAN(p->magic_bonus.span, 1,28, 1,29);
+ EXPECT_EQ(p->magic_bonus.data, 10);
+ EXPECT_SPAN(p->slot_unused.span, 1,31, 1,32);
+ EXPECT_EQ(p->slot_unused.data, "xx"_s);
+ EXPECT_SPAN(p->gender.span, 1,34, 1,34);
+ EXPECT_EQ(p->gender.data, SEX::NEUTRAL);
+ EXPECT_SPAN(p->loc.span, 1,36, 1,37);
+ EXPECT_EQ(p->loc.data, EPOS::MISC1);
+ EXPECT_SPAN(p->wlv.span, 1,39, 1,40);
+ EXPECT_EQ(p->wlv.data, 12);
+ EXPECT_SPAN(p->elv.span, 1,42, 1,43);
+ EXPECT_EQ(p->elv.data, 13);
+ EXPECT_SPAN(p->view.span, 1,45, 1,46);
+ EXPECT_EQ(p->view.data, ItemLook::BOW);
+ EXPECT_SPAN(p->use_script.span, 1,49, 1,54);
+ EXPECT_EQ(p->use_script.braced_body, "{end;}"_s);
+ EXPECT_SPAN(p->equip_script.span, 1,57, 1,58);
+ EXPECT_EQ(p->equip_script.braced_body, "{}"_s);
+ }
+ }
+ }
+} // namespace item
+} // namespace ast
+} // namespace tmwa
diff --git a/src/ast/npc.cpp b/src/ast/npc.cpp
new file mode 100644
index 0000000..8d4a43e
--- /dev/null
+++ b/src/ast/npc.cpp
@@ -0,0 +1,620 @@
+#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 "../io/extract.hpp"
+#include "../io/line.hpp"
+
+#include "../mmo/extract_enums.hpp"
+
+#include "../high/extract_mmo.hpp"
+
+#include "../poison.hpp"
+
+
+namespace tmwa
+{
+namespace ast
+{
+namespace npc
+{
+ using io::respan;
+
+#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("in |component 1| 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("in |component 3| expect 1 ,component,s"_s));
+ }
+ if (bits[3].data.size() != 5)
+ {
+ return Err(bits[3].span.error_str("in |component 4| expect 5 ,component,s"_s));
+ }
+
+ Warp warp;
+ 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);
+ if (bits[3].data[0].data == "-1"_s)
+ bits[3].data[0].data = "4294967295"_s;
+ TRY_EXTRACT(bits[3].data[0], warp.xs);
+ warp.xs.data += 2;
+ if (bits[3].data[1].data == "-1"_s)
+ bits[3].data[1].data = "4294967295"_s;
+ TRY_EXTRACT(bits[3].data[1], warp.ys);
+ warp.ys.data += 2;
+ 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("in |component 1| 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("in |component 3| expect 1 ,component,s"_s));
+ }
+ if (bits[3].data.size() < 2)
+ {
+ return Err(bits[3].span.error_str("in |component 4| expect at least 2 ,component,s"_s));
+ }
+
+ Shop shop;
+ 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, value;
+ if (!extract(data.data, record<':'>(&name, &value)))
+ 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.data.name.span.error_str("item name problem (too long?)"_s));
+ }
+ item.data.value_multiply = false;
+ if (value.startswith('-'))
+ {
+ item.data.value.span.begin.warning("Shop value multiplier should use '*' instead of '-' now"_s);
+ item.data.value_multiply = true;
+ item.data.value.span.begin.column += 1;
+ value = value.xslice_t(1);
+ }
+ else if (value.startswith('*'))
+ {
+ item.data.value_multiply = true;
+ item.data.value.span.begin.column += 1;
+ value = value.xslice_t(1);
+ }
+ if (!extract(value, &item.data.value.data))
+ {
+ return Err(item.data.value.span.error_str("invalid item value"_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("in |component 1| 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("in |component 3| 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("in |component 4| expect 2, 4, or 5 ,component,s"_s));
+ }
+
+ Monster mob;
+ 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("in |component 1| 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("in |component 3| expect 1 ,component,s"_s));
+ }
+
+ MapFlag mapflag;
+ 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)
+ {
+ mapflag.vec_extra.span = bits[3].span;
+ for (auto& bit : bits[3].data)
+ {
+ mapflag.vec_extra.data.emplace_back();
+ TRY_EXTRACT(bit, mapflag.vec_extra.data.back());
+ }
+ }
+ else
+ {
+ mapflag.vec_extra.data = {};
+ mapflag.vec_extra.span = bits[2].span;
+ mapflag.vec_extra.span.end.column++;
+ mapflag.vec_extra.span.begin.column = mapflag.vec_extra.span.end.column;
+ }
+ return Ok(std::move(mapflag));
+ }
+ static
+ Result<ScriptFunction> parse_script_function_head(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits)
+ {
+ // 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("in |component 3| expect 1 ,component,s"_s));
+ }
+
+ ScriptFunction script_function;
+ script_function.key1_span = bits[0].data[0].span;
+ TRY_EXTRACT(bits[2].data[0], script_function.name);
+ // also expect '{' and parse real script (in caller)
+ return Ok(std::move(script_function));
+ }
+ static
+ Result<ScriptNone> parse_script_none_head(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits)
+ {
+ // ScriptNone: -|script|script name|32767{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("in |component 3| expect 1 ,component,s"_s));
+ }
+ assert(bits[3].data[0].data == "32767"_s);
+ if (bits[3].data.size() != 1)
+ {
+ return Err(bits[3].span.error_str("in |component 4| should be just 32767"_s));
+ }
+
+ ScriptNone script_none;
+ script_none.key1_span = bits[0].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 (in caller)
+ return Ok(std::move(script_none));
+ }
+ static
+ Result<ScriptMap> parse_script_map_head(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits)
+ {
+ // 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("in |component 1| 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("in |component 3| expect 1 ,component,s"_s));
+ }
+ if (bits[3].data.size() != 1 && bits[3].data.size() != 3)
+ {
+ return Err(bits[3].span.error_str("in |component 4| expect 1 or 3 ,component,s"_s));
+ }
+
+ ScriptMap script_map;
+ 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);
+ TRY_EXTRACT(bits[2].data[0], script_map.name);
+ TRY_EXTRACT(bits[3].data[0], script_map.npc_class);
+ if (bits[3].data.size() >= 3)
+ {
+ TRY_EXTRACT(bits[3].data[1], script_map.xs);
+ script_map.xs.data = script_map.xs.data * 2 + 1;
+ TRY_EXTRACT(bits[3].data[2], script_map.ys);
+ script_map.ys.data = script_map.ys.data * 2 + 1;
+ }
+ else
+ {
+ script_map.xs.data = 0;
+ script_map.xs.span = script_map.npc_class.span;
+ script_map.xs.span.end.column++;
+ script_map.xs.span.begin = script_map.xs.span.end;
+ script_map.ys.data = 0;
+ script_map.ys.span = script_map.xs.span;
+ }
+ // also expect '{' and parse real script (in caller)
+ return Ok(std::move(script_map));
+ }
+ static
+ Result<Script> parse_script_any(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits, io::LineCharReader& lr)
+ {
+ // 3 cases:
+ // ScriptFunction: function|script|Fun Name{code}
+ // ScriptNone: -|script|script name|32767{code}
+ // ScriptMap: m,x,y,d|script|script name|class,xs,ys{code}
+ if (bits[0].data[0].data == "function"_s)
+ {
+ Script rv = TRY(parse_script_function_head(span, bits));
+ rv.key_span = bits[1].data[0].span;
+
+ ast::script::ScriptOptions opt;
+ opt.implicit_start = true;
+ opt.default_label = "OnCall"_s;
+ opt.no_event = true;
+ rv.body = TRY(ast::script::parse_script_body(lr, opt));
+ return Ok(std::move(rv));
+ }
+ else if (bits[0].data[0].data == "-"_s)
+ {
+ Script rv = TRY(parse_script_none_head(span, bits));
+ rv.key_span = bits[1].data[0].span;
+
+ ast::script::ScriptOptions opt;
+ opt.implicit_start = true;
+ opt.no_start = true;
+ rv.body = TRY(ast::script::parse_script_body(lr, opt));
+ return Ok(std::move(rv));
+ }
+ else
+ {
+ ScriptMap script_map = TRY(parse_script_map_head(span, bits));
+ bool no_touch = !script_map.xs.data && !script_map.ys.data;
+ Script rv = std::move(script_map);
+ rv.key_span = bits[1].data[0].span;
+
+ ast::script::ScriptOptions opt;
+ opt.implicit_start = true;
+ opt.default_label = "OnClick"_s;
+ opt.no_touch = no_touch;
+ rv.body = TRY(ast::script::parse_script_body(lr, opt));
+ 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)
+ {
+ // you know, I just realized a lot of the if (.get()) checks are not
+ // actually going to fail, since LineCharReader guarantees the \n
+ // occurs before EOF
+ 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 ... NOT.
+ // Reasonably, we should only ever eat a single char here.
+ // Unfortunately, we aren't dealing with reasonable people here.
+ if (c.ch() == '|' || c.ch() == ',')
+ {
+ lr.adv();
+ while (true)
+ {
+ io::LineChar c2;
+ if (!lr.get(c2) || c2.ch() == '\n' || c2.ch() == '{')
+ {
+ c.warning("Separator at EOL"_s);
+ return None;
+ }
+ if (c2.ch() == ',' || c2.ch() == '|')
+ {
+ lr.adv();
+ c = c2;
+ c.warning("Adjacent separators"_s);
+ continue;
+ }
+ break;
+ }
+ return Some(respan({c, c}, RString(VString<1>(c.ch()))));
+ }
+ 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() == '|')
+ {
+ return Some(respan(span, RString(accum)));
+ }
+
+ 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')
+ {
+ accum += c.ch();
+ span.end = c;
+ lr.adv();
+ }
+ return Some(respan(span, RString(accum)));
+ }
+ // 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();
+ }
+ return Some(respan(span, RString(accum)));
+ }
+
+ Option<Result<TopLevel>> parse_top(io::LineCharReader& in)
+ {
+ Spanned<std::vector<Spanned<std::vector<Spanned<RString>>>>> bits;
+
+ // special logic for the first 'bit'
+ {
+ 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 None;
+ });
+ if (mayc.data.startswith("//"_s))
+ {
+ Comment com;
+ com.comment = std::move(mayc.data);
+ TopLevel rv = std::move(com);
+ rv.span = std::move(mayc.span);
+ return Some(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)
+ {
+ TopLevel rv = TRY(parse_warp(bits.span, bits.data));
+ rv.span = bits.span;
+ return Some(Ok(std::move(rv)));
+ }
+ else if (w2.data == "shop"_s)
+ {
+ TopLevel rv = TRY(parse_shop(bits.span, bits.data));
+ rv.span = bits.span;
+ return Some(Ok(std::move(rv)));
+ }
+ else if (w2.data == "monster"_s)
+ {
+ TopLevel rv = TRY(parse_monster(bits.span, bits.data));
+ rv.span = bits.span;
+ return Some(Ok(std::move(rv)));
+ }
+ else if (w2.data == "mapflag"_s)
+ {
+ TopLevel rv = TRY(parse_mapflag(bits.span, bits.data));
+ rv.span = bits.span;
+ return Some(Ok(std::move(rv)));
+ }
+ else if (w2.data == "script"_s)
+ {
+ TopLevel rv = TRY_MOVE(parse_script_any(bits.span, bits.data, in));
+ rv.span = bits.span;
+ return Some(Ok(std::move(rv)));
+ }
+ else
+ {
+ return Err(w2.span.error_str("Unknown type"_s));
+ }
+ }
+} // namespace npc
+} // namespace ast
+} // namespace tmwa
diff --git a/src/ast/npc.hpp b/src/ast/npc.hpp
new file mode 100644
index 0000000..ca6479e
--- /dev/null
+++ b/src/ast/npc.hpp
@@ -0,0 +1,142 @@
+#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 "../io/span.hpp"
+
+#include "../net/timer.t.hpp"
+
+#include "../sexpr/variant.hpp"
+
+#include "../mmo/clif.t.hpp"
+#include "../mmo/ids.hpp"
+#include "../mmo/strs.hpp"
+
+#include "script.hpp"
+
+
+namespace tmwa
+{
+namespace ast
+{
+namespace npc
+{
+ using io::Spanned;
+
+ struct Comment
+ {
+ RString comment;
+ };
+ struct Warp
+ {
+ 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;
+ bool value_multiply;
+ Spanned<int> value;
+ };
+ struct Shop
+ {
+ 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
+ {
+ 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
+ {
+ Spanned<MapName> m;
+ io::LineSpan key_span;
+ Spanned<RString> name;
+ Spanned<std::vector<Spanned<RString>>> vec_extra;
+ };
+ struct ScriptFunction
+ {
+ io::LineSpan key1_span;
+ Spanned<RString> name;
+ };
+ struct ScriptNone
+ {
+ io::LineSpan key1_span;
+ Spanned<NpcName> name;
+ io::LineSpan key4_span;
+ };
+ struct ScriptMap
+ {
+ Spanned<MapName> m;
+ Spanned<unsigned> x, y;
+ Spanned<DIR> d;
+ Spanned<NpcName> name;
+ Spanned<Species> npc_class;
+ Spanned<unsigned> xs, ys;
+ };
+ using ScriptBase = Variant<ScriptFunction, ScriptNone, ScriptMap>;
+ struct Script : ScriptBase
+ {
+ Script() = default;
+ Script(ScriptFunction s) : ScriptBase(std::move(s)) {}
+ Script(ScriptNone s) : ScriptBase(std::move(s)) {}
+ Script(ScriptMap s) : ScriptBase(std::move(s)) {}
+
+ io::LineSpan key_span;
+ ast::script::ScriptBody body;
+ };
+ using TopLevelBase = Variant<Comment, Warp, Shop, Monster, MapFlag, Script>;
+ struct TopLevel : TopLevelBase
+ {
+ TopLevel() = default;
+ TopLevel(Comment t) : TopLevelBase(std::move(t)) {}
+ TopLevel(Warp t) : TopLevelBase(std::move(t)) {}
+ TopLevel(Shop t) : TopLevelBase(std::move(t)) {}
+ TopLevel(Monster t) : TopLevelBase(std::move(t)) {}
+ TopLevel(MapFlag t) : TopLevelBase(std::move(t)) {}
+ TopLevel(Script t) : TopLevelBase(std::move(t)) {}
+ io::LineSpan span;
+ };
+
+ Option<Result<TopLevel>> parse_top(io::LineCharReader& in);
+} // namespace npc
+} // namespace ast
+} // namespace tmwa
diff --git a/src/ast/npc_test.cpp b/src/ast/npc_test.cpp
new file mode 100644
index 0000000..dadeba7
--- /dev/null
+++ b/src/ast/npc_test.cpp
@@ -0,0 +1,556 @@
+#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 "../io/line.hpp"
+
+#include "../tests/fdhack.hpp"
+
+#include "../poison.hpp"
+
+
+namespace tmwa
+{
+namespace ast
+{
+namespace npc
+{
+#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(npcast, eof)
+ {
+ QuietFd q;
+ LString inputs[] =
+ {
+ ""_s,
+ "\n"_s,
+ "\n\n"_s,
+ };
+ for (auto input : inputs)
+ {
+ io::LineCharReader lr(io::from_string, "<string>"_s, input);
+ auto res = parse_top(lr);
+ EXPECT_TRUE(res.is_none());
+ }
+ }
+ TEST(npcast, comment)
+ {
+ QuietFd q;
+ LString inputs[] =
+ {
+ //23456789
+ "// hello"_s,
+ "// hello\n "_s,
+ "// hello\nabc"_s,
+ };
+ for (auto input : inputs)
+ {
+ io::LineCharReader lr(io::from_string, "<string>"_s, input);
+ auto res = TRY_UNWRAP(parse_top(lr), FAIL());
+ 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 = top.get_if<Comment>();
+ EXPECT_TRUE(p);
+ if (p)
+ {
+ EXPECT_EQ(p->comment, "// hello"_s);
+ }
+ }
+ }
+ TEST(npcast, warp)
+ {
+ QuietFd q;
+ LString inputs[] =
+ {
+ // 1 2 3 4
+ //234567890123456789012345678901234567890123456789
+ "map.gat,1,2|warp|To Other Map|3,4,other.gat,7,8"_s,
+ "map.gat,1,2|warp|To Other Map|3,4,other.gat,7,8\n"_s,
+ "map.gat,1,2|warp|To Other Map|3,4,other.gat,7,8{"_s,
+ // no optional fields in warp
+ };
+ for (auto input : inputs)
+ {
+ io::LineCharReader lr(io::from_string, "<string>"_s, input);
+ auto res = TRY_UNWRAP(parse_top(lr), FAIL());
+ 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 = top.get_if<Warp>();
+ 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, 5);
+ EXPECT_SPAN(p->ys.span, 1,33, 1,33);
+ EXPECT_EQ(p->ys.data, 6);
+ 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, 7);
+ EXPECT_SPAN(p->to_y.span, 1,47, 1,47);
+ EXPECT_EQ(p->to_y.data, 8);
+ }
+ }
+ }
+ TEST(npcast, 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(io::from_string, "<string>"_s, input);
+ auto res = TRY_UNWRAP(parse_top(lr), FAIL());
+ EXPECT_TRUE(res.get_success().is_some());
+ auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL());
+ EXPECT_SPAN(top.span, 1,1, 1,55);
+ auto p = top.get_if<Shop>();
+ 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,55);
+ 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_EQ(p->items.data[0].data.value_multiply, false);
+ 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_EQ(p->items.data[1].data.value_multiply, false);
+ 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,55);
+ 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_EQ(p->items.data[2].data.value_multiply, true);
+ EXPECT_SPAN(p->items.data[2].data.value.span, 1,55, 1,55);
+ EXPECT_EQ(p->items.data[2].data.value.data, 8);
+ }
+ }
+ }
+ TEST(npcast, 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(io::from_string, "<string>"_s, input);
+ auto res = TRY_UNWRAP(parse_top(lr), FAIL());
+ 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 = top.get_if<Monster>();
+ 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(npcast, 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,
+ "nap.gat|mapflag|flagname|aa,b,c"_s,
+ "nap.gat|mapflag|flagname|aa,b,c\n"_s,
+ "nap.gat|mapflag|flagname|aa,b,c{"_s,
+ };
+ for (auto input : inputs)
+ {
+ bool first = input.startswith('m');
+ bool second = input.startswith('M');
+ bool third = input.startswith('n');
+ EXPECT_EQ(first + second + third, 1);
+ io::LineCharReader lr(io::from_string, "<string>"_s, input);
+ auto res = TRY_UNWRAP(parse_top(lr), FAIL());
+ 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?24:31);
+ auto p = top.get_if<MapFlag>();
+ 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));
+ }
+ if (second)
+ {
+ EXPECT_EQ(p->m.data, stringish<MapName>("Map"_s));
+ }
+ if (third)
+ {
+ EXPECT_EQ(p->m.data, stringish<MapName>("nap"_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 (first)
+ {
+ EXPECT_SPAN(p->vec_extra.span, 1,25, 1,25);
+ EXPECT_EQ(p->vec_extra.data.size(), 0);
+ }
+ if (second)
+ {
+ EXPECT_SPAN(p->vec_extra.span, 1,26, 1,31);
+ EXPECT_EQ(p->vec_extra.data.size(), 1);
+ EXPECT_SPAN(p->vec_extra.data[0].span, 1,26, 1,31);
+ EXPECT_EQ(p->vec_extra.data[0].data, "optval"_s);
+ }
+ if (third)
+ {
+ EXPECT_SPAN(p->vec_extra.span, 1,26, 1,31);
+ EXPECT_EQ(p->vec_extra.data.size(), 3);
+ EXPECT_SPAN(p->vec_extra.data[0].span, 1,26, 1,27);
+ EXPECT_EQ(p->vec_extra.data[0].data, "aa"_s);
+ EXPECT_SPAN(p->vec_extra.data[1].span, 1,29, 1,29);
+ EXPECT_EQ(p->vec_extra.data[1].data, "b"_s);
+ EXPECT_SPAN(p->vec_extra.data[2].span, 1,31, 1,31);
+ EXPECT_EQ(p->vec_extra.data[2].data, "c"_s);
+ }
+ }
+ }
+ }
+
+ TEST(npcast, 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(io::from_string, "<string>"_s, input);
+ auto res = TRY_UNWRAP(parse_top(lr), FAIL());
+ 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 script = top.get_if<Script>();
+ EXPECT_TRUE(script);
+ auto p = script->get_if<ScriptFunction>();
+ EXPECT_TRUE(p);
+ if (p)
+ {
+ EXPECT_SPAN(p->key1_span, 1,1, 1,8);
+ EXPECT_SPAN(script->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(script->body.span, 1,25, 1,30);
+ }
+ else if (input.endswith('\n'))
+ {
+ EXPECT_SPAN(script->body.span, 2,1, 2,6);
+ }
+ else if (input.endswith(' '))
+ {
+ EXPECT_SPAN(script->body.span, 3,2, 3,7);
+ }
+ else
+ {
+ FAIL();
+ }
+ EXPECT_EQ(script->body.braced_body, "{end;}"_s);
+ }
+ }
+ }
+ TEST(npcast, scriptnone)
+ {
+ QuietFd q;
+ LString inputs[] =
+ {
+ // 1 2 3
+ //23456789012345678901234567890123456789
+ "-|script|#config|32767{end;}"_s,
+ // 123456
+ "-|script|#config|32767\n{end;}\n"_s,
+ // 1234567
+ "-|script|#config|32767\n \n {end;} "_s,
+ };
+ for (auto input : inputs)
+ {
+ io::LineCharReader lr(io::from_string, "<string>"_s, input);
+ auto res = TRY_UNWRAP(parse_top(lr), FAIL());
+ EXPECT_TRUE(res.get_success().is_some());
+ auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL());
+ EXPECT_SPAN(top.span, 1,1, 1,22);
+ auto script = top.get_if<Script>();
+ EXPECT_TRUE(script);
+ auto p = script->get_if<ScriptNone>();
+ EXPECT_TRUE(p);
+ if (p)
+ {
+ EXPECT_SPAN(p->key1_span, 1,1, 1,1);
+ EXPECT_SPAN(script->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,22);
+ if (input.endswith('}'))
+ {
+ EXPECT_SPAN(script->body.span, 1,23, 1,28);
+ }
+ else if (input.endswith('\n'))
+ {
+ EXPECT_SPAN(script->body.span, 2,1, 2,6);
+ }
+ else if (input.endswith(' '))
+ {
+ EXPECT_SPAN(script->body.span, 3,2, 3,7);
+ }
+ else
+ {
+ FAIL();
+ }
+ EXPECT_EQ(script->body.braced_body, "{end;}"_s);
+ }
+ }
+ }
+ TEST(npcast, 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,
+ "Map.gat,1,2,3|script|Asdf|40506{end;}"_s,
+ "Map.gat,1,2,3|script|Asdf|40506\n{end;}\n"_s,
+ "Map.gat,1,2,3|script|Asdf|40506\n \n {end;} "_s,
+ };
+ for (auto input : inputs)
+ {
+ bool second = input.startswith('M');
+ io::LineCharReader lr(io::from_string, "<string>"_s, input);
+ auto res = TRY_UNWRAP(parse_top(lr), FAIL());
+ 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 script = top.get_if<Script>();
+ EXPECT_TRUE(script);
+ auto p = script->get_if<ScriptMap>();
+ 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->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(script->key_span, 1,15, 1,20);
+ EXPECT_SPAN(p->name.span, 1,22, 1,25);
+ EXPECT_EQ(p->name.data, stringish<NpcName>("Asdf"_s));
+ if (!second)
+ {
+ 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, 11);
+ EXPECT_SPAN(p->ys.span, 1,31, 1,31);
+ EXPECT_EQ(p->ys.data, 13);
+ }
+ else
+ {
+ EXPECT_SPAN(p->npc_class.span, 1,27, 1,31);
+ EXPECT_EQ(p->npc_class.data, wrap<Species>(40506));
+ EXPECT_SPAN(p->xs.span, 1,32, 1,32);
+ EXPECT_EQ(p->xs.data, 0);
+ EXPECT_SPAN(p->ys.span, 1,32, 1,32);
+ EXPECT_EQ(p->ys.data, 0);
+ }
+ if (input.endswith('}'))
+ {
+ EXPECT_SPAN(script->body.span, 1,32, 1,37);
+ }
+ else if (input.endswith('\n'))
+ {
+ EXPECT_SPAN(script->body.span, 2,1, 2,6);
+ }
+ else if (input.endswith(' '))
+ {
+ EXPECT_SPAN(script->body.span, 3,2, 3,7);
+ }
+ else
+ {
+ FAIL();
+ }
+ EXPECT_EQ(script->body.braced_body, "{end;}"_s);
+ }
+ }
+ }
+} // namespace npc
+} // namespace ast
+} // namespace tmwa
diff --git a/src/ast/quest.cpp b/src/ast/quest.cpp
new file mode 100644
index 0000000..bd339c2
--- /dev/null
+++ b/src/ast/quest.cpp
@@ -0,0 +1,125 @@
+#include "quest.hpp"
+// ast/quest.cpp - Structure of tmwa questdb
+//
+// Copyright © 2015 Ed Pasek <pasekei@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/extract.hpp"
+#include "../io/line.hpp"
+
+#include "../mmo/extract_enums.hpp"
+
+#include "../poison.hpp"
+
+
+namespace tmwa
+{
+namespace ast
+{
+namespace quest
+{
+ using io::respan;
+
+ static
+ void skip_comma_space(io::LineCharReader& lr)
+ {
+ io::LineChar c;
+ if (lr.get(c) && c.ch() == ',')
+ {
+ lr.adv();
+ while (lr.get(c) && c.ch() == ' ')
+ {
+ lr.adv();
+ }
+ }
+ }
+ static
+ Option<Spanned<RString>> lex_nonscript(io::LineCharReader& lr, bool first)
+ {
+ io::LineChar c;
+ if (first)
+ {
+ while (lr.get(c) && c.ch() == '\n')
+ {
+ lr.adv();
+ }
+ }
+ if (!lr.get(c))
+ {
+ return None;
+ }
+ io::LineSpan span;
+ MString accum;
+ accum += c.ch();
+ span.begin = c;
+ span.end = c;
+ lr.adv();
+ if (c.ch() != '/')
+ first = false;
+
+ if (first && lr.get(c) && c.ch() == '/')
+ {
+ accum += c.ch();
+ span.end = c;
+ lr.adv();
+ while (lr.get(c) && c.ch() != '\n')
+ {
+ accum += c.ch();
+ span.end = c;
+ lr.adv();
+ }
+ return Some(respan(span, RString(accum)));
+ }
+
+ while (lr.get(c) && c.ch() != ',' && c.ch() != '\n')
+ {
+ accum += c.ch();
+ span.end = c;
+ lr.adv();
+ }
+ skip_comma_space(lr);
+ return Some(respan(span, RString(accum)));
+ }
+
+#define SPAN_EXTRACT(bitexpr, var) ({ auto bit = bitexpr; if (!extract(bit.data, &var.data)) return Err(bit.span.error_str("failed to extract "_s #var)); var.span = bit.span; })
+
+#define EOL_ERROR(lr) ({ io::LineChar c; lr.get(c) ? Err(c.error_str("unexpected EOL"_s)) : Err("unexpected EOF before unexpected EOL"_s); })
+ Option<Result<QuestOrComment>> parse_quest(io::LineCharReader& lr)
+ {
+ Spanned<RString> first = TRY_UNWRAP(lex_nonscript(lr, true), return None);
+ if (first.data.startswith("//"_s))
+ {
+ Comment comment;
+ comment.comment = first.data;
+ QuestOrComment rv = std::move(comment);
+ rv.span = first.span;
+ return Some(Ok(std::move(rv)));
+ }
+ Quest quest;
+ SPAN_EXTRACT(first, quest.questid);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), quest.quest_var);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), quest.quest_vr);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), quest.quest_shift);
+ SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), quest.quest_mask);
+ QuestOrComment rv = std::move(quest);
+ rv.span.begin = quest.questid.span.begin;
+ rv.span.end = quest.quest_mask.span.end;
+ return Some(Ok(std::move(rv)));
+ }
+} // namespace quest
+} // namespace ast
+} // namespace tmwa
diff --git a/src/ast/quest.hpp b/src/ast/quest.hpp
new file mode 100644
index 0000000..5112524
--- /dev/null
+++ b/src/ast/quest.hpp
@@ -0,0 +1,65 @@
+#pragma once
+// ast/quest.hpp - Structure of tmwa questdb
+//
+// Copyright © 2015 Ed Pasek <pasekei@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/span.hpp"
+
+#include "../sexpr/variant.hpp"
+
+#include "../mmo/clif.t.hpp"
+#include "../mmo/ids.hpp"
+#include "../mmo/strs.hpp"
+
+namespace tmwa
+{
+namespace ast
+{
+namespace quest
+{
+ using io::Spanned;
+
+ struct Comment
+ {
+ RString comment;
+ };
+ struct Quest
+ {
+ Spanned<QuestId> questid;
+ Spanned<VarName> quest_var;
+ Spanned<VarName> quest_vr;
+ Spanned<int> quest_shift;
+ Spanned<int> quest_mask;
+ };
+
+ using QuestOrCommentBase = Variant<Comment, Quest>;
+ struct QuestOrComment : QuestOrCommentBase
+ {
+ QuestOrComment(Comment o) : QuestOrCommentBase(std::move(o)) {}
+ QuestOrComment(Quest o) : QuestOrCommentBase(std::move(o)) {}
+ io::LineSpan span;
+ };
+
+ Option<Result<QuestOrComment>> parse_quest(io::LineCharReader& lr);
+} // namespace quest
+} // namespace ast
+} // namespace tmwa
diff --git a/src/ast/script.cpp b/src/ast/script.cpp
new file mode 100644
index 0000000..ec958e1
--- /dev/null
+++ b/src/ast/script.cpp
@@ -0,0 +1,75 @@
+#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 "../io/line.hpp"
+
+#include "../poison.hpp"
+
+
+namespace tmwa
+{
+namespace ast
+{
+namespace script
+{
+ Result<ScriptBody> parse_script_body(io::LineCharReader& lr, ScriptOptions opt)
+ {
+ 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() == ' ' || (!opt.one_line && 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));
+ if (opt.one_line && c.ch() == '\n')
+ return Err(c.error_str("unexpected EOL 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 script
+} // namespace ast
+} // namespace tmwa
diff --git a/src/ast/script.hpp b/src/ast/script.hpp
new file mode 100644
index 0000000..74b11e1
--- /dev/null
+++ b/src/ast/script.hpp
@@ -0,0 +1,112 @@
+#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/span.hpp"
+
+
+namespace tmwa
+{
+namespace ast
+{
+namespace script
+{
+ using io::Spanned;
+
+ struct ScriptBody
+ {
+ RString braced_body;
+ io::LineSpan span;
+ };
+
+ struct ScriptOptions
+ {
+ // don't require a label at the beginning
+ bool implicit_start = false;
+ // label to generate at the beginning if not already present
+ RString default_label;
+ // beginning must be only 'end;'
+ bool no_start;
+ // don't requite an 'end;' at the end
+ bool implicit_end = false;
+ // forbid newlines anywhere between { and }
+ bool one_line = false;
+ // forbid the OnTouch event
+ bool no_touch = false;
+ // forbid all events
+ bool no_event = false;
+ };
+
+ Result<ScriptBody> parse_script_body(io::LineCharReader& lr, ScriptOptions opt);
+
+ /*
+ (-- 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 script
+} // namespace ast
+} // namespace tmwa