diff options
Diffstat (limited to 'src/ast')
-rw-r--r-- | src/ast/fwd.hpp | 45 | ||||
-rw-r--r-- | src/ast/item.cpp | 155 | ||||
-rw-r--r-- | src/ast/item.hpp | 82 | ||||
-rw-r--r-- | src/ast/item_test.cpp | 150 | ||||
-rw-r--r-- | src/ast/npc.cpp | 620 | ||||
-rw-r--r-- | src/ast/npc.hpp | 142 | ||||
-rw-r--r-- | src/ast/npc_test.cpp | 556 | ||||
-rw-r--r-- | src/ast/quest.cpp | 125 | ||||
-rw-r--r-- | src/ast/quest.hpp | 65 | ||||
-rw-r--r-- | src/ast/script.cpp | 75 | ||||
-rw-r--r-- | src/ast/script.hpp | 112 |
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 |