From 586287db789e17cd6e5af53b83768171a055c3f9 Mon Sep 17 00:00:00 2001
From: Ethan Uppal <113849268+ethanuppal@users.noreply.github.com>
Date: Sat, 18 May 2024 19:46:04 -0400
Subject: [PATCH] Reapply "Merge branch 'file-parsing-2' into main"
This reverts commit 818c3519654755b9e77054986aeb1e0f73717dff.
---
.gitmodules | 3 ++
.vscode/settings.json | 82 +++++++++++++++++++++++++++++++++---
Makefile | 15 +++++--
README.md | 14 +++---
data/example_chat.htm | 8 ++++
efsw | 1 +
main.cpp | 74 ++++++++++++++++++++++++--------
src/bwmodel.h | 1 +
src/chat/parseline.cpp | 96 ++++++++++++++++++++++++++++++++++++++++++
src/chat/parseline.h | 14 ++++++
src/game/game.cpp | 18 ++++----
src/util/logger.h | 3 +-
tests/parseline.cpp | 37 ++++++++++++++++
13 files changed, 325 insertions(+), 41 deletions(-)
create mode 100644 .gitmodules
create mode 100644 data/example_chat.htm
create mode 160000 efsw
create mode 100644 src/chat/parseline.cpp
create mode 100644 src/chat/parseline.h
create mode 100644 tests/parseline.cpp
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