summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJon Santmyer <jon@jonsantmyer.com>2022-06-22 17:41:59 -0400
committerJon Santmyer <jon@jonsantmyer.com>2022-06-22 17:41:59 -0400
commit5e3a2492c7bb73daa4e27398daaf490d09980ff3 (patch)
tree75178d823d596b6a898002c3f1d45b9ceede0e1e /src
downloadsystemviewer-5e3a2492c7bb73daa4e27398daaf490d09980ff3.tar.gz
systemviewer-5e3a2492c7bb73daa4e27398daaf490d09980ff3.tar.bz2
systemviewer-5e3a2492c7bb73daa4e27398daaf490d09980ff3.zip
Base system viewer with data loaded from csv files
Diffstat (limited to 'src')
-rw-r--r--src/camera.cpp197
-rw-r--r--src/game.cpp115
-rw-r--r--src/input.cpp37
-rw-r--r--src/keybind.cpp64
-rw-r--r--src/main.cpp26
-rw-r--r--src/system.cpp517
-rw-r--r--src/timeman.cpp49
-rw-r--r--src/units.cpp61
-rw-r--r--src/window.cpp72
9 files changed, 1138 insertions, 0 deletions
diff --git a/src/camera.cpp b/src/camera.cpp
new file mode 100644
index 0000000..59e99a0
--- /dev/null
+++ b/src/camera.cpp
@@ -0,0 +1,197 @@
+#include "camera.hpp"
+#include "shape.hpp"
+#include <cmath>
+#include <algorithm>
+#include <variant>
+#include <typeinfo>
+#include <any>
+
+void
+Camera::updateFrustum(){
+ if(m_dirty) {
+ *m_viewport << straw::clear(' ');
+ m_frustum = shapes::rectangle<long>(0, 0, (long)m_viewport->getwidth(), (long)m_viewport->getheight());
+ m_frustum.position -= (m_frustum.bounds / 2);
+ m_frustum.position += m_position + m_origin;
+ m_dirty = false;
+ }
+}
+
+void
+RenderBatchEntry::translate(Camera *camera)
+{
+ std::visit([camera](auto &shapeval) {
+ shapeval.translate(-camera->m_frustum.position);
+ shapeval.translate((camera->m_frustum.bounds * camera->m_scale) / 2);
+ shapeval.position = vex::vec2<long>(
+ (long)std::floor((double)shapeval.position[0] / (double)camera->m_scale),
+ (long)std::floor((double)shapeval.position[1] / (double)camera->m_scale));
+ shapeval.scale(camera->m_scale);
+ }, shape);
+}
+
+void
+Camera::draw()
+{
+ if(!m_dirty) return;
+ updateFrustum();
+ shapes::rectangle<long> ssfrustum{vex::vec2<long>(0, 0), vex::vec2<long>(m_viewport->getwidth(), m_viewport->getheight())};
+
+ //Remove all batches not intersecting the screenspace frustum
+ m_shapeBatch.erase(
+ std::remove_if(
+ m_shapeBatch.begin(),
+ m_shapeBatch.end(),
+ [ssfrustum, this](RenderBatchEntry &entry){
+ entry.translate(this);
+ return std::visit([ssfrustum](auto &&shape) {
+ return shapes::intersects(shape, ssfrustum) == 0;
+ }, entry.shape);
+ }),
+ m_shapeBatch.end());
+ for(RenderBatchEntry &entry : m_shapeBatch) entry.plot(this);
+ m_shapeBatch.clear();
+}
+
+void
+RenderBatchEntry::plotPoint(shapes::point<long> point, Camera *camera) {
+ long ploty = camera->m_viewport->getheight() - point.position[1];
+ if(point.position[0] < 0 || point.position[0] >= camera->m_viewport->getwidth() ||
+ ploty < 0 || ploty >= camera->m_viewport->getheight()) return;
+ *camera->m_viewport << straw::setcolor(fg, bg) << straw::plot(point.position[0], ploty, c);
+}
+
+void
+RenderBatchEntry::plotLine(shapes::line<long> line, Camera *camera) {
+ if(line.end == line.position) {
+ plotPoint(shapes::point<long>(line.position), camera);
+ }
+ vex::vec2<long> vpdim(camera->m_viewport->getwidth(),
+ camera->m_viewport->getheight());
+ vex::vec2<long> d(line.end - line.position);
+ long adx = std::abs(d[0]);
+ long ady = std::abs(d[1]);
+
+ long offX= d[0] > 0 ? 1 : -1;
+ long offY = d[1] > 0 ? 1 : -1;
+
+ *camera->m_viewport << straw::setcolor(fg, bg);
+ if(adx < ady) {
+ long err = ady / 2;
+ long x = line.position[0];
+ long y = line.position[1];
+ for(long i = 0; i < ady; i++) {
+ if(x > 0 && x < vpdim[0] && y > 0 && y < vpdim[1]) {
+ long ploty = vpdim[1] - y;
+ *camera->m_viewport << straw::plot(x, ploty, c);
+ }
+ if(err >= ady) {
+ x += offX;
+ y += offY;
+ err += adx - ady;
+ }else {
+ y += offY;
+ err += adx;
+ }
+ }
+
+ }else{
+ long err = adx / 2;
+ long x = line.position[0];
+ long y = line.position[1];
+ for(long i = 0; i < adx; i++) {
+ if(x > 0 && x < vpdim[0] && y > 0 && y < vpdim[1]) {
+ long ploty = vpdim[1] - y;
+ *camera->m_viewport << straw::plot(x, ploty, c);
+ }
+ if(err >= adx) {
+ x += offX;
+ y += offY;
+ err += ady - adx;
+ }else {
+ x += offX;
+ err += ady;
+ }
+ }
+ }
+}
+
+void
+RenderBatchEntry::plotCircle(shapes::circle<long> circle, Camera *camera) {
+ if(circle.radius == 1) {
+ plotPoint(shapes::point<long>(circle.position), camera);
+ return;
+ }
+ long sy = std::max<long>(1, circle.position[1] - circle.radius);
+ long ey = std::min<long>(camera->m_viewport->getheight(), circle.position[1] + circle.radius);
+ *camera->m_viewport << straw::setcolor(fg, bg);
+ for(long y = sy; y <= ey; y++) {
+ long ploty = camera->m_viewport->getheight() - y;
+ long r2 = circle.radius * circle.radius;
+ long dy = circle.position[1] - y;
+ long dx = (long)std::sqrt(r2 - (dy * dy));
+ long sx = std::max<long>(0, circle.position[0] - dx + 1);
+ long ex = std::min<long>(camera->m_viewport->getwidth(), circle.position[0] + dx);
+ for(unsigned x = (unsigned)sx; x < ex; x++) {
+ *camera->m_viewport << straw::plot(x, ploty, c);
+ }
+ }
+}
+
+void
+RenderBatchEntry::plotRectangle(shapes::rectangle<long> rectangle, Camera *camera)
+{
+ if(rectangle.bounds[1] == 1) {
+ plotPoint(shapes::point<long>(rectangle.position), camera);
+ return;
+ }
+ long sy = std::max<long>(1, rectangle.position[1]);
+ long ey = std::min<long>(camera->m_viewport->getheight(), rectangle.position[1] + rectangle.bounds[1]);
+ *camera->m_viewport << straw::setcolor(fg, bg);
+ for(long y = sy; y < ey; y++) {
+ long ploty = camera->m_viewport->getheight() - y;
+ long sx = std::max<long>(0, rectangle.position[1]);
+ long ex = std::min<long>(camera->m_viewport->getwidth(), rectangle.position[1] + rectangle.bounds[1]);
+ for(unsigned x = (unsigned)sx; x < ex; x++) {
+ *camera->m_viewport << straw::plot(x, ploty, c);
+ }
+ }
+}
+
+void
+RenderBatchEntry::plotEllipse(shapes::ellipse<long> shapeval, Camera *camera) {
+ if(shapeval.a == 1) {
+ plotPoint(shapes::point<long>(shapeval.position), camera);
+ return;
+ }
+ long sy = std::max<long>(1, shapeval.position[1] - shapeval.b);
+ long ey = std::min<long>(camera->m_viewport->getheight(), shapeval.position[1] + shapeval.b);
+ *camera->m_viewport << straw::setcolor(fg, bg);
+ for(long y = sy; y <= ey; y++) {
+ long ploty = camera->m_viewport->getheight() - y;
+ long dy = shapeval.position[1] - y;
+ long dy2 = dy * dy;
+ long a2 = shapeval.a * shapeval.a;
+ long b2 = shapeval.b * shapeval.b;
+ long dx = (long)(((2.0 * (double)shapeval.a) /
+ (double)shapeval.b) * std::sqrt(b2 - dy2) / 2.0);
+ long sx = std::max<long>(0, shapeval.position[0] - dx + 1);
+ long ex = std::min<long>(camera->m_viewport->getwidth(), shapeval.position[0] + dx);
+
+ for(unsigned x = (unsigned)sx; x < ex; x++) {
+ *camera->m_viewport << straw::plot(x, ploty, c);
+ }
+ }
+}
+void
+RenderBatchEntry::plot(Camera *camera)
+{
+ std::visit([this, camera](auto &shapeval) -> void{
+ using T = std::decay_t<decltype(shapeval)>;
+ if constexpr(std::is_same_v<T, shapes::point<long>>) plotPoint(shapeval, camera);
+ if constexpr(std::is_same_v<T, shapes::line<long>>) plotLine(shapeval, camera);
+ if constexpr(std::is_same_v<T, shapes::ellipse<long>>) plotEllipse(shapeval, camera);
+ if constexpr(std::is_same_v<T, shapes::circle<long>>) plotCircle(shapeval, camera);
+ if constexpr(std::is_same_v<T, shapes::rectangle<long>>) plotRectangle(shapeval, camera);
+ }, shape);
+}
diff --git a/src/game.cpp b/src/game.cpp
new file mode 100644
index 0000000..b1d0b00
--- /dev/null
+++ b/src/game.cpp
@@ -0,0 +1,115 @@
+#include "game.hpp"
+#include "keybind.hpp"
+#include "input.hpp"
+#include <thread>
+#include <chrono>
+
+constexpr static double REQUESTED_FPS = 60.0;
+constexpr static double REQ_FPS_MSPT = 1000.0 / REQUESTED_FPS;
+
+std::unordered_map<std::string, WindowContext> Game::m_contexts;
+std::string Game::m_currentContext;
+
+std::unique_ptr<Camera> Game::m_camera;
+std::unique_ptr<System> Game::m_system;
+SystemView Game::m_systemView(nullptr);
+
+input::Context Game::m_inputContext;
+
+double Game::m_delta;
+Game::State Game::m_state = State::RUNNING;
+Game::WindowContexts Game::contexts;
+
+void
+Game::setup(unsigned w, unsigned h)
+{
+ KeyMan::loadKeybindsFrom("keybinds.csv");
+
+ m_inputContext.echo(false);
+ m_inputContext.canon(false);
+ m_inputContext.cbreak(false);
+
+ unsigned int viewh = h - 1;
+ unsigned int infow = 24;
+ unsigned int infoh = 12;
+ unsigned int timeh = 10;
+
+ TimeMan::init();
+ m_contexts.emplace(WINCTX_GAME, WindowContext());
+ WindowContext *gameContext = &m_contexts[WINCTX_GAME];
+
+ gameContext->registerWindow(WINDOW_SYSTEMVIEW_ID, "System View", infow, 0, w - infow, viewh);
+ gameContext->registerWindow(WINDOW_BODYINFO_ID, "Body Info", 0, 0, infow, infoh);
+ gameContext->registerWindow(WINDOW_EVENTS_ID, "Events", 0, infoh, infow, viewh - infoh - timeh);
+ gameContext->registerWindow(WINDOW_TIMEMAN_ID, "Time", 0, viewh - timeh, infow, timeh);
+
+ gameContext->registerWindow(WINDOW_SYSTEMVIEW_SEARCH_ID, "Search", infow, 0, (w - infow) / 4, viewh, true);
+ m_currentContext = WINCTX_GAME;
+
+ m_camera = std::make_unique<Camera>((*gameContext)[WINDOW_SYSTEMVIEW_ID].screen());
+ m_system = std::make_unique<System>();
+ m_systemView.view(m_system.get());
+
+ KeyMan::registerBind('\x1B', BIND_G_ESCAPE, CTX_GLOBAL, "Escape from focused searchbox / window");
+ KeyMan::registerBind('\n', BIND_G_SELECT, CTX_GLOBAL, "Select something");
+ KeyMan::registerBind('x', BIND_G_NEXTWIN, CTX_GLOBAL, "Change the focused window to the next in the stack");
+ KeyMan::registerBind('X', BIND_G_PREVWIN, CTX_GLOBAL, "Change the focused window to the previous in the stack");
+ KeyMan::registerBind('y', BIND_G_QUIT, CTX_GLOBAL, "Terminate the game");
+
+ KeyMan::registerBind('w', BIND_SYSTEMVIEW_PANUP, CTX_SYSTEMVIEW, "Moves the camera upwards on the system viewer");
+ KeyMan::registerBind('a', BIND_SYSTEMVIEW_PANLEFT, CTX_SYSTEMVIEW, "Moves the camera to the left on the system viewer");
+ KeyMan::registerBind('s', BIND_SYSTEMVIEW_PANDOWN, CTX_SYSTEMVIEW, "Moves the camera downwards on the system viewer");
+ KeyMan::registerBind('d', BIND_SYSTEMVIEW_PANRIGHT, CTX_SYSTEMVIEW, "Moves the camera to the right on the system viewer");
+ KeyMan::registerBind('-', BIND_SYSTEMVIEW_INCSCALE, CTX_SYSTEMVIEW, "Decreases zoom from center of screen");
+ KeyMan::registerBind('+', BIND_SYSTEMVIEW_DECSCALE, CTX_SYSTEMVIEW, "Increases zoom into center of screen");
+ KeyMan::registerBind('/', BIND_SYSTEMVIEW_SEARCH, CTX_SYSTEMVIEW, "Search through bodies in the system");
+
+ KeyMan::registerBind(input::CTRL_KEY_ARROWUP, BIND_SYSTEMVIEW_SEARCH_PREV, CTX_SYSTEMVIEW, "Move the cursor up in the search view");
+ KeyMan::registerBind(input::CTRL_KEY_ARROWDOWN, BIND_SYSTEMVIEW_SEARCH_NEXT, CTX_SYSTEMVIEW, "Move the cursor down in the search view");
+ KeyMan::registerBind(input::CTRL_KEY_HOME, BIND_SYSTEMVIEW_SEARCH_TOP, CTX_SYSTEMVIEW, "Move to first entry in search view");
+ KeyMan::registerBind(input::CTRL_KEY_END, BIND_SYSTEMVIEW_SEARCH_BOTTOM, CTX_SYSTEMVIEW, "Move to last entry in search view");
+ KeyMan::registerBind(input::CTRL_KEY_ARROWRIGHT, BIND_SYSTEMVIEW_SEARCH_COLLAPSE, CTX_SYSTEMVIEW, "Toggle collapsed entry in search view");
+}
+
+void
+Game::cleanup()
+{
+ KeyMan::writeKeybindsTo("keybinds.csv");
+}
+
+void
+Game::turn()
+{
+ auto start = std::chrono::steady_clock::now();
+ auto end = start + std::chrono::milliseconds(16);
+ int c = input::getcode();
+ WindowContext &context = m_contexts.at(m_currentContext);
+
+ /*Global keybinds*/
+ if(!inputMode()) {
+ if(c == KeyMan::binds[BIND_G_QUIT].code) m_state = State::STOPPED;
+ context.update(c);
+ }
+
+ if(m_currentContext == WINCTX_GAME) { /*Game Context*/
+ m_systemView.keypress(m_camera.get(), c);
+
+ m_system->update();
+ m_systemView.update(m_camera.get());
+ TimeMan::update(c);
+ if(TimeMan::changed()) {
+ m_camera->markDirty();
+ }
+
+ m_systemView.draw(m_camera.get());
+ TimeMan::draw();
+ m_camera->draw();
+ m_systemView.drawOver(m_camera.get());
+ context.draw();
+ }
+
+ std::this_thread::sleep_until(end);
+ end = std::chrono::steady_clock::now();
+ auto diff = end - start;
+ m_delta = (double)(std::chrono::duration_cast<std::chrono::milliseconds>(diff).count()) / 1000.0;
+}
diff --git a/src/input.cpp b/src/input.cpp
new file mode 100644
index 0000000..5cbedb2
--- /dev/null
+++ b/src/input.cpp
@@ -0,0 +1,37 @@
+#include "input.hpp"
+namespace input {
+
+int getcode() {
+ int r = 0;
+ char c;
+ if(read(STDIN_FILENO, &c, 1) != 1) return -1;
+ if(c == '\x1b') {
+ char seq[3];
+ if(read(STDIN_FILENO, &seq[0], 1) != 1) return '\x1b';
+ if(read(STDIN_FILENO, &seq[1], 1) != 1) return '\x1b';
+ if(seq[0] == '[') {
+ if(seq[1] >= 0 && seq[1] <= '9') {
+ if(read(STDIN_FILENO, &seq[2], 1) != 1) return '\x1b';
+ if(seq[2] == '~') {
+ switch(seq[1]) {
+ case '1':
+ case '7': return CTRL_KEY_HOME;
+ case '4':
+ case '8': return CTRL_KEY_END;
+ default: return CTRL_RANGE_START + seq[1] + 20;
+ }
+ }
+ }else{
+ switch(seq[1]) {
+ default: return CTRL_RANGE_START + seq[1];
+ }
+ }
+ }else if(seq[0] == 'O') {
+ return CTRL_RANGE_START + seq[1];
+ }
+ return '\x1b';
+ }else{
+ return c;
+ }
+}
+}
diff --git a/src/keybind.cpp b/src/keybind.cpp
new file mode 100644
index 0000000..8283c4c
--- /dev/null
+++ b/src/keybind.cpp
@@ -0,0 +1,64 @@
+#include "keybind.hpp"
+#include "csv.hpp"
+#include "input.hpp"
+
+#include <algorithm>
+
+std::unordered_map<std::string, KeyMan::Bind> KeyMan::m_keybinds;
+std::unordered_map<std::string, std::string> KeyMan::m_keybindContexts;
+
+KeyMan::Binds KeyMan::binds;
+
+static std::unordered_map<int, std::string> CODENAMES =
+{
+ { input::CTRL_KEY_ARROWUP, "Up arrow" },
+ { input::CTRL_KEY_ARROWDOWN, "Down arrow" },
+ { input::CTRL_KEY_ARROWRIGHT, "Right arrow" },
+ { input::CTRL_KEY_ARROWLEFT, "Left arrow" }
+};
+
+void
+KeyMan::registerBind(int def,
+ const std::string &name,
+ const std::string &context,
+ const std::string &desc)
+{
+ auto find = m_keybinds.find(name);
+ if(find != m_keybinds.end()) {
+ find->second.ctx = context;
+ find->second.desc = desc;
+ }else{
+ Bind bind = { def, name, context, desc};
+ m_keybinds[name] = bind;
+ }
+ m_keybindContexts[name] = context;
+}
+
+void
+KeyMan::loadKeybindsFrom(const std::string &csvPath)
+{
+ csv::CSVFile<',', int, std::string> keybindData(csvPath);
+ for(auto &bind : keybindData.get()) {
+ int code = std::get<0>(bind);
+ std::string name = std::get<1>(bind);
+
+ m_keybinds[name] = { .code = code, .name = name, .ctx = "", .desc = ""};
+ }
+}
+
+void
+KeyMan::writeKeybindsTo(const std::string &csvPath)
+{
+ csv::CSVFile<',', int, std::string> keybindData(csvPath, true);
+ for(Bind &bind : KeyMan::binds()) {
+ keybindData.put({bind.code, bind.name});
+ }
+ keybindData.write();
+}
+
+std::string
+KeyMan::translateCode(int code)
+{
+ if(code < 256) return std::string(1, (char)code);
+ return CODENAMES[code];
+}
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..9ff09a6
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,26 @@
+#include <iostream>
+#include "game.hpp"
+
+#include <sys/ioctl.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+int
+main(int argc, char **argv)
+{
+ (void)argc;
+ (void)argv;
+
+ struct winsize w;
+ ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
+ fcntl(STDIN_FILENO, F_SETFL, fcntl(0, F_GETFL) | O_NONBLOCK);
+
+ Game::setup(w.ws_col, w.ws_row);
+
+ while(Game::running()) {
+ Game::turn();
+ }
+
+ Game::cleanup();
+ return 0;
+}
diff --git a/src/system.cpp b/src/system.cpp
new file mode 100644
index 0000000..974d0dc
--- /dev/null
+++ b/src/system.cpp
@@ -0,0 +1,517 @@
+#include "system.hpp"
+#include "timeman.hpp"
+#include "ecs.hpp"
+#include "vex.hpp"
+#include "units.hpp"
+#include "csv.hpp"
+#include "keybind.hpp"
+#include "game.hpp"
+#include <numbers>
+#include <string>
+
+static double G = 6.6743 * std::pow(10, -11);
+
+ecs::Entity &
+System::addOrbital(const std::string &name,
+ const std::string &orbitingName,
+ unsigned long a,
+ double e,
+ unit::Mass m,
+ unsigned r,
+ double M,
+ double w)
+{
+ M *= (std::numbers::pi / 180.0);
+ w *= (std::numbers::pi / 180.0);
+
+ SystemTreeNode *treeNode = getNode(orbitingName);
+ ecs::Entity &newOrbital = m_entityMan.newEntity()
+ .addComponent(ecs::PositionComponent{vex::vec2<long>{0, 0}})
+ .addComponent(ecs::MassComponent{m})
+ .addComponent(ecs::OrbitalComponent{.origin = treeNode->entityId, .a = (long)a, .e = e, .w = w, .M = M, .T = 0, .v = 0})
+ .addComponent(ecs::RenderCircleComponent{r})
+ .addComponent(ecs::NameComponent{name});
+ treeNode->children.push_back({newOrbital.id, {}});
+ return newOrbital;
+}
+
+System::System()
+{
+ auto sol = m_entityMan.newEntity()
+ .addComponent(ecs::PositionComponent{vex::vec2<long>{0, 0}})
+ .addComponent(ecs::MassComponent{unit::solMass})
+ .addComponent(ecs::RenderCircleComponent{695700})
+ .addComponent(ecs::NameComponent{"Sol"});
+ m_systemTree.entityId = sol.id;
+
+ csv::CSVFile<',', std::string, std::string, double, double, double, double, double, double> planetData("data/sol_planets.csv");
+ for(auto &planet : planetData.get()) {
+ std::string name = std::get<0>(planet);
+ std::string orbiting = std::get<1>(planet);
+ double sma = std::get<2>(planet) * unit::AU;
+ double e = std::get<3>(planet);
+ unit::Mass m = unit::earthMass * std::get<4>(planet);
+ double r = std::get<5>(planet) * unit::earthRad;
+ double M = std::get<6>(planet);
+ double w = std::get<7>(planet);
+ addOrbital(name, orbiting, sma, e, m, r, M, w);
+ }
+
+ csv::CSVFile<',', std::string, std::string, double, double, double, double, double, double> satelliteData("data/sol_satellites.csv");
+ for(auto &satellite : satelliteData.get()) {
+ std::string name = std::get<0>(satellite);
+ std::string orbiting = std::get<1>(satellite);
+ double sma = std::get<2>(satellite) * unit::AU;
+ double e = std::get<3>(satellite);
+ unit::Mass m = unit::earthMass * std::get<4>(satellite);
+ double r = std::get<5>(satellite) * unit::earthRad;
+ double M = std::get<6>(satellite);
+ double w = std::get<7>(satellite);
+ addOrbital(name, orbiting, sma, e, m, r, M, w);
+ }
+
+ csv::CSVFile<',', std::string, double, double, double, double, double, std::string> asteroidData("data/sol_asteroids.csv");
+ for(auto &asteroid : asteroidData.get()) {
+ std::string name = std::get<0>(asteroid);
+ if(name == "Missing") name = std::get<6>(asteroid);
+ addOrbital(name,
+ "Sol",
+ (unsigned long)(std::get<1>(asteroid) * unit::AU),
+ std::get<2>(asteroid),
+ unit::Mass(0),
+ (unsigned)std::get<3>(asteroid),
+ std::get<4>(asteroid),
+ std::get<5>(asteroid));
+ }
+}
+
+constexpr static double tau = std::numbers::pi * 2;
+
+void
+System::tickOrbitals(unit::Time time)
+{
+ for(ecs::Entity &e : m_entityMan.getWith<ecs::OrbitalComponent>()) {
+ auto &oc = e.get<ecs::OrbitalComponent>();
+ auto &pc = e.get<ecs::PositionComponent>();
+ auto &mc = e.get<ecs::MassComponent>();
+
+ ecs::Entity &o = m_entityMan[oc.origin];
+ auto &opc = o.get<ecs::PositionComponent>();
+ auto &om = o.get<ecs::MassComponent>();
+
+ double e2 = oc.e * oc.e;
+ double td = (double)time();
+ if(oc.T == 0) {
+ double u = G * (om.mass() + mc.mass());
+ double am = (double)oc.a * 1000.0;
+ oc.T = tau * std::sqrt((am * am * am) / u);
+ }
+ double n = tau / oc.T;
+ double M = oc.M + (n * td);
+ double E = M;
+
+ int its = 0;
+ while(true) {
+ double dE = (E - oc.e * std::sin(E) - M) / (1 - oc.e * std::cos(E));
+ E -= dE;
+ its++;
+ if(std::abs(dE) < 1e-6) break;
+ }
+
+ double x = std::cos(E) - oc.e;
+ double y = std::sin(E) * std::sqrt(1 - e2);
+
+ double v = std::atan2(y, x) + oc.w;
+ oc.v = v;
+
+ double r = std::sqrt(x*x + y*y) * (double)oc.a;
+
+ vex::vec2<double> polar(r, v);
+ pc.position = vex::cartesian<long>(polar) + opc.position;
+ }
+
+}
+
+void
+System::update()
+{
+ tickOrbitals(TimeMan::time());
+}
+
+ecs::Entity &
+System::getBody(std::size_t id) {
+ return m_entityMan[id];
+}
+
+System::SystemTreeNode *
+System::traverseSystemTree(SystemTreeNode &node, const std::string &name)
+{
+ SystemTreeNode *found = nullptr;
+ ecs::Entity &entity = getBody(node.entityId);
+ auto &namecomp = entity.get<ecs::NameComponent>();
+ if(namecomp.name == name) found = &node;
+
+ for(SystemTreeNode &child : node.children) {
+ if(found != nullptr) break;
+ found = traverseSystemTree(child, name);
+ }
+ return found;
+}
+
+System::SystemTreeNode *
+System::getNode(const std::string &name)
+{
+ SystemTreeNode *treeRes = traverseSystemTree(m_systemTree, name);
+ if(treeRes == nullptr)
+ treeRes = &m_systemTree;
+ return treeRes;
+}
+
+void
+SystemView::view(System *system)
+{
+ m_system = system;
+ m_focus = &system->m_systemTree;
+}
+
+void
+SystemView::keypress(Camera *camera, int key)
+{
+ WindowContext &context = Game::contexts();
+ if(context.getFocusedString() == WINDOW_SYSTEMVIEW_SEARCH_ID) {
+ m_focusSearch->keypress(key);
+ if(!Game::paused()) {
+ Search *fp = m_focusSearch.release();
+ delete fp;
+ }
+ }
+ if(context.getFocusedString() != WINDOW_SYSTEMVIEW_ID) return;
+ if(key == KeyMan::binds[BIND_SYSTEMVIEW_PANUP].code) camera->move(0, camera->getscale());
+ if(key == KeyMan::binds[BIND_SYSTEMVIEW_PANDOWN].code) camera->move(0, -(long)camera->getscale());
+ if(key == KeyMan::binds[BIND_SYSTEMVIEW_PANRIGHT].code) camera->move(camera->getscale(), 0);
+ if(key == KeyMan::binds[BIND_SYSTEMVIEW_PANLEFT].code) camera->move(-(long)camera->getscale(), 0);
+ if(key == KeyMan::binds[BIND_SYSTEMVIEW_INCSCALE].code) camera->setscale(camera->getscale() * 2);
+ if(key == KeyMan::binds[BIND_SYSTEMVIEW_DECSCALE].code) camera->setscale(camera->getscale() / 2);
+ if(key == KeyMan::binds[BIND_SYSTEMVIEW_SEARCH].code) {
+ Game::setState(Game::State::PAUSED_INPUT);
+ context.focus(WINDOW_SYSTEMVIEW_SEARCH_ID);
+ context.setWindowHidden(WINDOW_SYSTEMVIEW_SEARCH_ID, false);
+ m_focusSearch = std::make_unique<Search>(this);
+ }
+}
+
+void
+SystemView::update(Camera *camera)
+{
+ if(Game::paused()) return;
+ auto &efoc = m_system->m_entityMan[m_focus->entityId];
+ auto &efocp = efoc.get<ecs::PositionComponent>();
+ if(efocp.position != camera->getorigin()) {
+ camera->setorigin(efocp.position);
+ }
+}
+
+void
+SystemView::drawOver(Camera *camera) {
+ auto &efoc = m_system->m_entityMan[m_focus->entityId];
+ auto &efocp = efoc.get<ecs::PositionComponent>();
+ auto &efocm = efoc.get<ecs::MassComponent>();
+
+ WindowContext &context = Game::contexts();
+ Window &infoWindow = context[WINDOW_BODYINFO_ID];
+ Window &viewWindow = context[WINDOW_SYSTEMVIEW_ID];
+
+ infoWindow << straw::clear(' ');
+ infoWindow << straw::move(0, 0) << "Focus: " << efoc.get<ecs::NameComponent>().name << '\n';
+ if(efoc.contains<ecs::OrbitalComponent>()) {
+ auto &efoco = efoc.get<ecs::OrbitalComponent>();
+ ecs::Entity &efoc_origin = m_system->m_entityMan[efoco.origin];
+
+ infoWindow << "Orbiting: " << efoc_origin.get<ecs::NameComponent>().name << '\n';
+ infoWindow << "Distance: " << std::abs((double)(efocp.position - efoc_origin.get<ecs::PositionComponent>().position).magnitude()) << "km\n";
+ infoWindow << "Period: " << efoco.T / unit::DAY_SECONDS << " days\n";
+ infoWindow << "Angle: " << efoco.v * (180.0 / std::numbers::pi) << '\n';
+ infoWindow << "Eccentricity: " << efoco.e << '\n';
+ infoWindow << "Mass: " << efocm.mass() << '\n';
+ }
+ vex::vec2<unsigned> viewdims(
+ viewWindow.screen()->getwidth(),
+ viewWindow.screen()->getheight());
+
+ viewWindow << straw::move(0, 0) <<
+ straw::setcolor(straw::WHITE, straw::BLACK) <<
+ "Press '" <<
+ KeyMan::translateCode(KeyMan::binds[BIND_SYSTEMVIEW_SEARCH].code) <<
+ "' to change focus";
+
+ double scale = camera->getscale() * (viewdims[0] / 2.0);
+ if(scale > 1e7) {
+ viewWindow << straw::move(0, viewdims[1] - 1) << scale / unit::AU << " AU";
+ }else{
+ viewWindow << straw::move(0, viewdims[1] - 1) << scale << " km";
+ }
+
+}
+
+void
+SystemView::draw(Camera *camera)
+{
+ if(m_focusSearch != nullptr) m_focusSearch->draw();
+ if(!camera->dirty()) return;
+ auto &efoc = m_system->m_entityMan[m_focus->entityId];
+ auto &efocp = efoc.get<ecs::PositionComponent>();
+ auto &efocm = efoc.get<ecs::MassComponent>();
+
+ if(efoc.contains<ecs::OrbitalComponent>()) {
+ auto &oc = efoc.get<ecs::OrbitalComponent>();
+ auto &om = efoc.get<ecs::MassComponent>();
+ ecs::Entity &origin = m_system->m_entityMan[oc.origin];
+ auto pc = origin.get<ecs::PositionComponent>();
+
+ std::vector<vex::vec2<long>> points;
+ float i = 0.0;
+ while(i < tau){
+ double e2 = oc.e * oc.e;
+ double M = oc.M + i;
+ double E = M;
+
+ int its = 0;
+ while(its < 512) {
+ double dE = (E - oc.e * std::sin(E) - M) / (1 - oc.e * std::cos(E));
+ E -= dE;
+ its++;
+ if(std::abs(dE) < 1e-6) break;
+ }
+
+ double x = (std::cos(E) - oc.e);
+ double y = (std::sin(E) * std::sqrt(1 - e2));
+
+ double v = std::atan2(y, x) + oc.w;
+ double r = std::sqrt(x*x + y*y) * (double)oc.a;
+
+
+ vex::vec2<double> polar{r, v};
+ vex::vec2<long> cart = vex::cartesian<long>(polar) + pc.position;
+
+ points.push_back(cart);
+ i += 0.01;
+ }
+ for(unsigned i = 0; i < points.size(); i++) {
+ if(i == 0) {
+ camera->batchShape(shapes::line<long>(points[points.size() - 1], points[i]), straw::color(0, 0, 255), '#');
+ }else{
+ camera->batchShape(shapes::line<long>(points[i-1], points[i]), straw::color(0, 0, 255), '#');
+ }
+ }
+ }
+
+ for(ecs::Entity &e : m_system->m_entityMan.getWith<ecs::RenderCircleComponent>()) {
+ auto &pc = e.get<ecs::PositionComponent>();
+ auto &cc = e.get<ecs::RenderCircleComponent>();
+ unsigned id = e.id;
+
+ long cr = cc.radius;
+ if(cr < camera->getscale()) cr = camera->getscale();
+ shapes::ellipse<long> circle(pc.position, cr, cr);
+
+ straw::color color = id == m_focus->entityId ? straw::color{255, 255, 0} : straw::WHITE;
+ if(cc.radius < cr) {
+ camera->batchShape(circle, color, '*');
+ }else{
+ camera->batchShape(circle, color, '#');
+ }
+ }
+}
+
+ecs::Entity &
+SystemView::getBody(int id) const
+{
+ return m_system->m_entityMan[id];
+}
+
+int
+SystemView::getBodyIdByName(const std::string &name)
+{
+ for(ecs::Entity &entity : m_system->m_entityMan.all()) {
+ int id = entity.id;
+ if(entity.contains<ecs::NameComponent>()) {
+ auto &namecomp = entity.get<ecs::NameComponent>();
+ if(namecomp.name == name) return id;
+ }
+ }
+ return -1;
+}
+
+void
+SystemView::Search::addNodeToTree(SystemTreeDisplayNode &root, System::SystemTreeNode *node)
+{
+ m_displayTreeFlat.emplace_back(&root);
+ for(auto &child : node->children)
+ {
+ ecs::NameComponent &namecomp = m_systemView->m_system->m_entityMan[child.entityId].get<ecs::NameComponent>();
+ root.children.push_back(
+ {&child,
+ {},
+ &root,
+ (unsigned)m_displayTreeFlat.size(),
+ m_query.empty(),
+ m_query.empty() ? false : namecomp.name.find(m_query) == std::string::npos});
+ addNodeToTree(root.children.back(), &child);
+ }
+}
+
+void
+SystemView::Search::rebuild()
+{
+ System::SystemTreeNode *systemRoot = &m_systemView->m_system->m_systemTree;
+ m_displayTree = {systemRoot, {}, nullptr, 0, false, false};
+ m_displayTreeFlat.clear();
+ m_selectionIndex = 0;
+
+ addNodeToTree(m_displayTree, systemRoot);
+ for(unsigned i = 0; auto *node : m_displayTreeFlat) {
+ if(m_systemView->m_focus == node->node) {
+ m_selectionIndex = i;
+ break;
+ }
+ i++;
+ }
+ SystemTreeDisplayNode *recurse = m_displayTreeFlat[m_selectionIndex];
+ while(recurse->parent != nullptr) {
+ recurse->parent->collapsed = false;
+ recurse->parent->hidden = false;
+ recurse = recurse->parent;
+ }
+}
+
+void
+SystemView::Search::drawNode(
+ SystemTreeDisplayNode &root,
+ Window &searchWindow,
+ unsigned indent)
+{
+ unsigned windowH = searchWindow.screen()->getheight();
+ unsigned cursorY = searchWindow.screen()->getcursory();
+ if(cursorY == windowH && root.index > m_selectionIndex) return;
+
+ ecs::NameComponent &namecomp = m_systemView->m_system->m_entityMan[root.node->entityId].get<ecs::NameComponent>();
+
+ if(root.index == m_selectionIndex) {
+ searchWindow << straw::setcolor(straw::BLACK, straw::WHITE);
+ }else searchWindow << straw::setcolor(straw::WHITE, straw::BLACK);
+
+
+ if(!root.hidden) {
+ if(m_query.empty()) searchWindow << std::string((indent * 4), ' ');
+ if(root.children.size() > 0)
+ searchWindow << '[' << (root.collapsed ? '+' : '-') << "] ";
+ searchWindow << namecomp.name << straw::setcolor(straw::WHITE, straw::BLACK) << '\n';
+ }
+
+ if(!root.collapsed) {
+ for(auto &child : root.children) drawNode(child, searchWindow, indent + 1);
+ }
+}
+
+SystemView::Search::Search(SystemView *systemView) :
+ m_systemView(systemView), m_selectionIndex(0), m_dirty(true)
+{
+ rebuild();
+}
+
+void
+SystemView::Search::finish()
+{
+ WindowContext &context = Game::contexts();
+ Game::setState(Game::State::RUNNING);
+ context.setWindowHidden(WINDOW_SYSTEMVIEW_SEARCH_ID, true);
+ context.focus(WINDOW_SYSTEMVIEW_ID);
+}
+
+void
+SystemView::Search::keypress(int key)
+{
+ if(key == KeyMan::binds[BIND_G_ESCAPE].code) {
+ finish();
+ }else
+ if(key == KeyMan::binds[BIND_SYSTEMVIEW_SEARCH_NEXT].code) {
+ if(m_selectionIndex == m_displayTreeFlat.size() - 1) return;
+ for(m_selectionIndex++; m_selectionIndex < m_displayTreeFlat.size() - 1; ++m_selectionIndex) {
+ if(!m_displayTreeFlat[m_selectionIndex]->hidden) break;
+ }
+ if(m_displayTreeFlat[m_selectionIndex]->hidden) m_selectionIndex = 0;
+ SystemTreeDisplayNode *node = m_displayTreeFlat[m_selectionIndex];
+ while(node->parent != nullptr) {
+ if(!node->parent->collapsed) break;
+ m_selectionIndex += node->parent->children.size();
+ node = node->parent;
+ }
+
+ m_dirty = true;
+ }else
+ if(key == KeyMan::binds[BIND_SYSTEMVIEW_SEARCH_PREV].code) {
+ if(m_selectionIndex == 0) return;
+ for(m_selectionIndex--; m_selectionIndex > 0; --m_selectionIndex) {
+ if(!m_displayTreeFlat[m_selectionIndex]->hidden) break;
+ }
+ SystemTreeDisplayNode *node = m_displayTreeFlat[m_selectionIndex];
+ while(node->parent != nullptr) {
+ if(!node->parent->collapsed) break;
+ m_selectionIndex -= node->parent->children.size();
+ node = node->parent;
+ }
+
+ m_dirty = true;
+ }else
+ if(key == KeyMan::binds[BIND_SYSTEMVIEW_SEARCH_TOP].code) {
+ m_selectionIndex = 0;
+ for(; m_selectionIndex < m_displayTreeFlat.size(); ++m_selectionIndex) {
+ if(!m_displayTreeFlat[m_selectionIndex]->hidden) break;
+ }
+ m_dirty = true;
+ }else
+ if(key == KeyMan::binds[BIND_SYSTEMVIEW_SEARCH_BOTTOM].code) {
+ m_selectionIndex = m_displayTreeFlat.size() - 1;
+ for(; m_selectionIndex > 0; --m_selectionIndex) {
+ if(!m_displayTreeFlat[m_selectionIndex]->hidden) break;
+ }
+ m_dirty = true;
+ }else
+ if(key == KeyMan::binds[BIND_SYSTEMVIEW_SEARCH_COLLAPSE].code) {
+ m_displayTreeFlat[m_selectionIndex]->collapsed = !m_displayTreeFlat[m_selectionIndex]->collapsed;
+ m_dirty = true;
+ }else
+ if(key == KeyMan::binds[BIND_G_SELECT].code) {
+ m_systemView->m_focus = m_displayTreeFlat[m_selectionIndex]->node;
+ finish();
+ }else
+ {
+ if(key < 0) return;
+ switch(key) {
+ case 127:
+ case '\b': {
+ if(!m_query.empty()) m_query.pop_back();
+ } break;
+ default: m_query.push_back((char)key); break;
+ }
+ rebuild();
+ m_dirty = true;
+ }
+}
+
+void
+SystemView::Search::draw()
+{
+ if(!m_dirty) return;
+
+ WindowContext &windowContext = Game::contexts();
+ Window &searchWindow = windowContext[WINDOW_SYSTEMVIEW_SEARCH_ID];
+
+ searchWindow << straw::clear(' ') << straw::move(0, 0);
+ searchWindow << "Query: " << m_query << '\n';
+
+ drawNode(m_displayTree, searchWindow, 0);
+
+ searchWindow << straw::flush();
+ m_dirty = false;
+}
diff --git a/src/timeman.cpp b/src/timeman.cpp
new file mode 100644
index 0000000..f043894
--- /dev/null
+++ b/src/timeman.cpp
@@ -0,0 +1,49 @@
+#include "timeman.hpp"
+#include "keybind.hpp"
+#include "window.hpp"
+#include "game.hpp"
+
+unit::Time TimeMan::m_time(0);
+unit::Time TimeMan::m_step(unit::DAY_SECONDS);
+bool TimeMan::m_auto;
+bool TimeMan::m_changed;
+
+void
+TimeMan::init()
+{
+ KeyMan::registerBind('.', BIND_TIMEMAN_STEP, CTX_TIMEMAN, "Move time ahead by a step");
+ KeyMan::registerBind('+', BIND_TIMEMAN_INCSTEP, CTX_TIMEMAN, "Increase the timestep");
+ KeyMan::registerBind('-', BIND_TIMEMAN_DECSTEP, CTX_TIMEMAN, "Decrease the timestep");
+ KeyMan::registerBind('a', BIND_TIMEMAN_TOGGLEAUTO, CTX_TIMEMAN, "Toggle if time will move automatically");
+ m_changed = true;
+}
+
+void
+TimeMan::update(int c)
+{
+ WindowContext &context = Game::contexts();
+ m_changed = false;
+ if(!Game::paused()) {
+ if(m_auto) {
+ m_time += (m_step);
+ m_changed = true;
+ }
+ }
+ if(context.getFocusedString() != WINDOW_TIMEMAN_ID) return;
+ if(c == KeyMan::binds[BIND_TIMEMAN_INCSTEP].code) m_step = unit::Time(std::max<long>(1, m_step() * 2));
+ if(c == KeyMan::binds[BIND_TIMEMAN_DECSTEP].code) m_step = unit::Time(std::max<long>(1, m_step() / 2));
+ if(c == KeyMan::binds[BIND_TIMEMAN_TOGGLEAUTO].code) m_auto = !m_auto;
+ if(c == KeyMan::binds[BIND_TIMEMAN_STEP].code && !Game::paused()) { m_time += m_step; m_changed = true; }
+}
+
+void
+TimeMan::draw()
+{
+ WindowContext &context = Game::contexts();
+ Window &timeWindow = context[WINDOW_TIMEMAN_ID];
+ timeWindow << straw::clear(' ');
+
+ timeWindow << straw::move(0, 0) << straw::resetcolor() << m_time.format("%S %D, %C \n%H:%m\n\n");
+ timeWindow << m_step.format("Step:\n%Y Years, %M Months\n%D Days, %H Hours\n%m Minutes, %s Seconds\n\n");
+ if(m_auto) timeWindow << "Auto";
+}
diff --git a/src/units.cpp b/src/units.cpp
new file mode 100644
index 0000000..7cc7d61
--- /dev/null
+++ b/src/units.cpp
@@ -0,0 +1,61 @@
+#include "units.hpp"
+#include <sstream>
+#include <cstring>
+#include <iostream>
+
+namespace unit {
+
+std::string
+Time::format(const char *fmt)
+{
+ std::stringstream ss;
+ for(; *fmt; fmt++) {
+ if(*fmt != '%') {
+ ss << *fmt;
+ continue;
+ }
+ fmt++;
+ switch(*fmt) {
+ case '%':
+ ss << '%';
+ break;
+ case 'Y':
+ ss << real_years();
+ break;
+ case 'C':
+ ss << years();
+ break;
+ case 'S': {
+ Time year = current_year();
+ ss << month_str[year.months()];
+ break; }
+ case 'M': {
+ Time year = current_year();
+ ss << year.months();
+ break; }
+ case 'W': {
+ Time month = current_month();
+ ss << month.weeks();
+ break; }
+ case 'D': {
+ Time month = current_month();
+ ss << month.days();
+ break; }
+ case 'H': {
+ Time day = current_day();
+ ss << day.hours();
+ break; }
+ case 'm': {
+ Time hour = current_hour();
+ ss << hour.minutes();
+ break; }
+ case 's': {
+ Time minute = current_minute();
+ ss << minute.seconds();
+ break; }
+ }
+ }
+ return ss.str();
+}
+
+}
diff --git a/src/window.cpp b/src/window.cpp
new file mode 100644
index 0000000..4e487f3
--- /dev/null
+++ b/src/window.cpp
@@ -0,0 +1,72 @@
+#include "window.hpp"
+#include "keybind.hpp"
+
+void
+Window::draw(bool focus)
+{
+ if(m_hidden) return;
+ if(focus) {
+ m_border << straw::setcolor(straw::BLACK, straw::WHITE) << straw::clear(' ') << straw::move(0, 0) << m_title << straw::resetcolor();
+ }else{
+ m_border << straw::setcolor(straw::WHITE, straw::BLACK) << straw::clear(' ') << straw::move(0, 0) << m_title;
+ }
+ m_border.flush();
+ m_screen.flush();
+}
+
+void
+WindowContext::registerWindow(const std::string &id,
+ const std::string &title,
+ unsigned x, unsigned y,
+ unsigned w, unsigned h,
+ bool hidden)
+{
+ m_windows.emplace(id, Window(title, x, y, w, h, hidden));
+ m_windowOrder.push_back(id);
+}
+
+void
+WindowContext::update(int code)
+{
+ if(code == KeyMan::binds[BIND_G_NEXTWIN].code) {
+ m_focus = (m_focus + 1) % m_windows.size();
+ while(m_windows.at(m_windowOrder[m_focus]).hidden()) {
+ m_focus = (m_focus + 1) % m_windows.size();
+ }
+ }
+ if(code == KeyMan::binds[BIND_G_PREVWIN].code) {
+ m_focus = (m_focus == 0 ? m_windows.size() - 1 : m_focus - 1);
+ while(m_windows.at(m_windowOrder[m_focus]).hidden()) {
+ m_focus = (m_focus == 0 ? m_windows.size() - 1 : m_focus - 1);
+ }
+ }
+}
+
+void
+WindowContext::draw()
+{
+ for(unsigned i = 0; i < m_windows.size(); i++) {
+ m_windows.at(m_windowOrder[i]).draw(i == m_focus);
+ }
+}
+
+void
+WindowContext::focus(const std::string &id)
+{
+ for(unsigned i = 0; i < m_windowOrder.size(); i++) {
+ if(m_windowOrder[i] == id) {
+ m_focus = i;
+ return;
+ }
+ }
+}
+
+void
+WindowContext::setWindowHidden(const std::string &id, bool mode)
+{
+ m_windows.at(id).setHidden(mode);
+ for(unsigned i = 0; i < m_windows.size(); i++) {
+ if(m_windowOrder[i] == id && mode) continue;
+ m_windows.at(m_windowOrder[i]) << straw::redraw();
+ }
+}