diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..30ddaac --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "efsw"] + path = efsw + url = https://github.com/SpartanJ/efsw diff --git a/.vscode/settings.json b/.vscode/settings.json index 055e60a..edc8cf3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,79 @@ { - "files.associations": { - "map.h": "cpp" - }, - "C_Cpp.formatting": "clangFormat", - "C_Cpp.clang_format_style": "file" + "files.associations": { + "map.h": "cpp", + "optional": "cpp", + "fstream": "cpp", + "cctype": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "array": "cpp", + "atomic": "cpp", + "bit": "cpp", + "*.tcc": "cpp", + "bitset": "cpp", + "charconv": "cpp", + "chrono": "cpp", + "codecvt": "cpp", + "compare": "cpp", + "concepts": "cpp", + "condition_variable": "cpp", + "cstdint": "cpp", + "deque": "cpp", + "map": "cpp", + "set": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "random": "cpp", + "ratio": "cpp", + "regex": "cpp", + "source_location": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "format": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "limits": "cpp", + "mutex": "cpp", + "new": "cpp", + "numbers": "cpp", + "ostream": "cpp", + "semaphore": "cpp", + "span": "cpp", + "sstream": "cpp", + "stacktrace": "cpp", + "stdexcept": "cpp", + "stop_token": "cpp", + "streambuf": "cpp", + "thread": "cpp", + "cinttypes": "cpp", + "typeinfo": "cpp", + "variant": "cpp", + "cassert": "cpp", + "list": "cpp" + }, + "C_Cpp.formatting": "clangFormat", + "C_Cpp.clang_format_style": "file", + "C_Cpp.default.includePath": ["./src"] } diff --git a/Makefile b/Makefile index 4145120..eaed244 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ INCLUDEDIR := ./src TESTSDIR := ./tests CC := $(shell which g++ || which clang) +LDFLAGS := -Lefsw -lefsw CFLAGS := -std=c++17 -pedantic -Wall -Wextra -I $(INCLUDEDIR) CDEBUG := -g CRELEASE := -O2 -DRELEASE_BUILD @@ -13,21 +14,29 @@ TARGET := main SRC := $(shell find $(SRCDIR) -name "*.cpp" -type f) OBJ := $(SRC:.cpp=.o) -CFLAGS += $(CDEBUG) +.PHONY: build +build: library + make $(TARGET) $(TARGET): main.cpp $(OBJ) - $(CC) $(CFLAGS) $^ -o $@ + $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) %.o: %.cpp @echo 'Compiling $@' $(CC) $(CFLAGS) -MMD -MP $< -c -o $@ +.PHONY: library +library: + cmake -DBUILD_SHARED_LIBS=OFF efsw + make -C efsw + .PHONY: clean clean: rm -rf $(OBJ) $(TARGET) $(shell find . -name "*.dSYM") + make -C efsw clean .PHONY: run -run: $(TARGET) $(OBJ) +run: build ./$(TARGET) .PHONY: test diff --git a/README.md b/README.md index 393f745..5ce639d 100644 --- a/README.md +++ b/README.md @@ -9,17 +9,21 @@ The idea is that a computer can process much more than a human brain can. For example, when one player kills another, it is likely that it was in hand-to-hand combat. If a probability distribution of player locations is maintained, we can update them to reflect this new information. -## Usage +## Usage/Building You can use this project through the `Makefile`. You must have C++17. + ```bash -make # builds the ./main script -make run # builds and runs the ./main script -make clean # removes auxillary and executable files -make test # runs the tests +make # builds the ./main script +make run # builds and runs the ./main script +make library # builds efsw (used for file watching) +make clean # removes auxillary and executable files +make test # runs the tests ``` +`cmake` > 3.15 is required to build efsw. + `make run` will run a testing script for the latest model. You can see all models in [`src/models/`](./src/models/). Developing a new model is as simple as implementing the [`GameDelegate`](./src/game/gamedelegate.h) interface. diff --git a/data/example_chat.htm b/data/example_chat.htm new file mode 100644 index 0000000..9f884aa --- /dev/null +++ b/data/example_chat.htm @@ -0,0 +1,8 @@ +[08:25:06] player3 has joined (7/8)!
+[08:26:50] +25 Bed Wars Experience (Time Played)
+[08:28:21] player1 fell into the void.
+[08:27:02] BED DESTRUCTION > Red Bed was dismantled by player4!
+[08:28:05] player2 met their end by player5.
+[08:27:44] player3 was trampled by player6.
+[08:27:02] player4 was killed by player7.
+[08:27:43] player7 was given the cold shoulder by player4. FINAL KILL!
\ No newline at end of file diff --git a/efsw b/efsw new file mode 160000 index 0000000..3b0ffd2 --- /dev/null +++ b/efsw @@ -0,0 +1 @@ +Subproject commit 3b0ffd2908fa8d52a54e228f92c67330dfd015b4 diff --git a/main.cpp b/main.cpp index b8415cd..1269a8d 100644 --- a/main.cpp +++ b/main.cpp @@ -2,8 +2,47 @@ #include #include +#include #include "bwmodel.h" +#include "efsw/include/efsw/efsw.hpp" using namespace bwmodel; +namespace fs = std::filesystem; + +class FileWatcherListener : public efsw::FileWatchListener { + Game& game; + const std::filesystem::path& filename; + std::ifstream stream; + +public: + FileWatcherListener(Game& game, const fs::path& dir, const fs::path& file) + : game(game), filename(file) { + stream.open(dir / file); + if (stream.fail()) throw std::runtime_error("Failed to open file"); + + stream.seekg(0, std::ios::end); + } + + ~FileWatcherListener() { + stream.close(); + } + + void handleFileAction(efsw::WatchID watchid, const std::string& dir, + const std::string& filename, efsw::Action action, + std::string oldFilename) override { + if (action != efsw::Action::Modified || filename != this->filename) + return; + + stream.clear(); + + std::string line; + while (std::getline(stream, line)) { + auto event = parse_line(line); + if (event.has_value()) { + event.value()(game); + } + } + } +}; int main() { try { @@ -16,25 +55,24 @@ int main() { std::shared_ptr model = Model::v1::make(); game.attach(model); - Simulator simulator; - - // we are about to POP OFF - int off = 1; - for (int i = 0; i < PLAYER_COUNT; i++) { - if (i != *me) { - int local_i = i; - simulator.sequence(500 * (i + off), [me, local_i](Game& game) { - game.notify_break_bed(me, - static_cast(local_i)); - game.notify_player_kill(me, - static_cast(local_i)); - }); - } else { - off--; - } - } + std::cout << "Chat log directory: "; + std::string path; + std::getline(std::cin, path); + + std::cout << "Chat log file name: "; + std::string name; + std::getline(std::cin, name); + + efsw::FileWatcher watcher; + FileWatcherListener listener(game, path, name); + + watcher.addWatch(path, &listener, false); + + watcher.watch(); - simulator.simulate(game); + std::cout << "Watching for changes...\n"; + std::cout << "Press enter to exit.\n"; + std::cin.get(); } catch (const MapLoadError& error) { std::cerr << "MapLoadError: " << error.what() << '\n'; return 1; diff --git a/src/bwmodel.h b/src/bwmodel.h index 538a9ee..a16ad5b 100644 --- a/src/bwmodel.h +++ b/src/bwmodel.h @@ -10,3 +10,4 @@ #include "game/sim.h" #include "models/v1.h" #include "util/logger.h" +#include "chat/parseline.h" diff --git a/src/chat/parseline.cpp b/src/chat/parseline.cpp new file mode 100644 index 0000000..8e35298 --- /dev/null +++ b/src/chat/parseline.cpp @@ -0,0 +1,96 @@ +#include "parseline.h" +#include +#include +#include "game/player.h" + +namespace bwmodel { + // TODO: incomplete/untested + std::optional color_from_str(const std::string& str) { + if (str == "000") return PlayerColor::BLACK; + if (str == "55F") return PlayerColor::BLUE; + if (str == "5F5") return PlayerColor::GREEN; + if (str == "5FF") return PlayerColor::AQUA; + if (str == "F55") return PlayerColor::RED; + if (str == "F5F") return PlayerColor::PINK; + if (str == "FF5") return PlayerColor::YELLOW; + if (str == "AAA") return PlayerColor::WHITE; + return {}; + } + + std::vector parse_color_sections(const std::string& line) { + std::regex color_regex("(.*?)<\\/span>", + std::regex_constants::ECMAScript); + std::smatch matches; + + std::vector sections; + + // StackOverflow Answer: https://stackoverflow.com/a/35026140 + std::string::const_iterator search_start(line.cbegin()); + while (std::regex_search(search_start, line.cend(), matches, + color_regex)) { + assert(matches.size() == 3); + sections.push_back({matches[1], matches[2]}); + search_start = matches.suffix().first; + } + + return sections; + } + + bool is_white_space(const std::string& str) { + return std::all_of(str.cbegin(), str.cend(), + [](char c) { return std::isspace(c); }); + } + + bool is_game_start(const std::vector& sections) { + return sections.size() == 2 && sections[0].color == "000" + && is_white_space(sections[0].text) && sections[1].color == "000" + && sections[1].text == "Bed Wars"; + } + + bool is_bed_destruction(const std::vector& sections) { + return sections.size() == 7 && sections[0].color == "000" + && sections[0].text.find("BED DESTRUCTION") != std::string::npos; + } + + bool is_kill(const std::vector& sections) { + return sections.size() == 4 + || (sections.size() == 5 && sections[4].text == "FINAL KILL!"); + } + + std::optional parse_line(const std::string& line) { + auto sections = parse_color_sections(line); + + if (is_game_start(sections)) { + return [](Game& game) { game.notify_start(); }; + } + + if (is_bed_destruction(sections)) { + auto opt_bed_color = color_from_str(sections[2].color); + auto opt_player_color = color_from_str(sections[5].color); + + if (!opt_bed_color.has_value() || !opt_player_color.has_value()) { + return {}; + } + + return [opt_bed_color, opt_player_color](Game& game) { + game.notify_break_bed(opt_player_color.value(), + opt_bed_color.value()); + }; + } + + if (is_kill(sections)) { + auto opt_killed = color_from_str(sections[0].color); + auto opt_killer = color_from_str(sections[2].color); + + if (!opt_killed.has_value() || !opt_killer.has_value()) { + return {}; + } + + return [opt_killed, opt_killer](Game& game) { + game.notify_player_kill(opt_killer.value(), opt_killed.value()); + }; + } + + return {}; + } +} \ No newline at end of file diff --git a/src/chat/parseline.h b/src/chat/parseline.h new file mode 100644 index 0000000..5198ac4 --- /dev/null +++ b/src/chat/parseline.h @@ -0,0 +1,14 @@ +#include +#include +#include "game/game.h" + +namespace bwmodel { + using Event = std::function; + + struct ColorSection { + std::string color; + std::string text; + }; + + std::optional parse_line(const std::string& line); +} \ No newline at end of file diff --git a/src/game/game.cpp b/src/game/game.cpp index 5dc7471..29dd887 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -38,7 +38,7 @@ namespace bwmodel { void Game::attach(std::shared_ptr delegate) { delegates.push_back(delegate); - Log << "attached delegate " << delegate << '\n'; + BW_Log << "attached delegate " << delegate << '\n'; } void Game::notify_start() { @@ -46,15 +46,15 @@ namespace bwmodel { delegate->on_game_start(*this); } start_time = std::chrono::steady_clock::now(); - Log << "(0ms) game started\n"; + BW_Log << "(0ms) game started\n"; } void Game::notify_end() { for (auto delegate: delegates) { delegate->on_game_end(*this); } - Log << "(" << time() << "ms) " - << "game ended\n"; + BW_Log << "(" << time() << "ms) " + << "game ended\n"; } bool Game::should_end() const { @@ -79,9 +79,9 @@ namespace bwmodel { was_final = true; } - Log << "(" << time() << "ms) " << PlayerColorHelper::name(victor) - << " killed " << PlayerColorHelper::name(loser) - << (was_final ? " (final kill!)" : "") << '\n'; + BW_Log << "(" << time() << "ms) " << PlayerColorHelper::name(victor) + << " killed " << PlayerColorHelper::name(loser) + << (was_final ? " (final kill!)" : "") << '\n'; for (auto delegate: delegates) { delegate->on_players_match(*this, victor, loser, was_final); @@ -96,8 +96,8 @@ namespace bwmodel { // update bed status _player_beds[*bed] = false; - Log << "(" << time() << "ms) " << PlayerColorHelper::name(breaker) - << " broke " << PlayerColorHelper::name(bed) << "'s bed\n"; + BW_Log << "(" << time() << "ms) " << PlayerColorHelper::name(breaker) + << " broke " << PlayerColorHelper::name(bed) << "'s bed\n"; for (auto delegate: delegates) { delegate->on_bed_broken(*this, breaker, bed); diff --git a/src/util/logger.h b/src/util/logger.h index 3d4409b..e113de1 100644 --- a/src/util/logger.h +++ b/src/util/logger.h @@ -70,4 +70,5 @@ class Logger { }; // See Logger::main -#define Log Logger::main() << Logger::LocationInfo(__FILE__, __LINE__, __func__) +#define BW_Log \ + Logger::main() << Logger::LocationInfo(__FILE__, __LINE__, __func__) \ No newline at end of file diff --git a/tests/parseline.cpp b/tests/parseline.cpp new file mode 100644 index 0000000..f0feef9 --- /dev/null +++ b/tests/parseline.cpp @@ -0,0 +1,37 @@ +#include "bwmodel.h" +#include +#include +#include +using namespace bwmodel; + +int main() { + std::unique_ptr map = Map::load_from("./data/example.bwmap"); + Game game(std::move(map)); + + // TODO: add game start test + std::ifstream file("./data/example_chat.htm"); + + if (file.fail()) { + std::cout << "Failed to open file\n"; + file.close(); + return 1; + } + + std::string line; + + while (std::getline(file, line)) { + auto event = parse_line(line); + if (event.has_value()) { + event.value()(game); + } + } + + file.close(); + + assert(!game.player_has_bed(PlayerColor::RED)); + assert(game.players_left().size() == 7); + assert(game.players_left().find(PlayerColor::RED) + == game.players_left().end()); + + return 0; +} \ No newline at end of file