#include "GameModel.h" #include "BitmapBrush.h" #include "EllipseBrush.h" #include "Favorite.h" #include "Format.h" #include "GameController.h" #include "GameModelException.h" #include "GameView.h" #include "Menu.h" #include "Notification.h" #include "RectangleBrush.h" #include "TriangleBrush.h" #include "QuickOptions.h" #include "lua/CommandInterface.h" #include "prefs/GlobalPrefs.h" #include "client/Client.h" #include "client/GameSave.h" #include "client/SaveFile.h" #include "client/SaveInfo.h" #include "client/http/ExecVoteRequest.h" #include "common/platform/Platform.h" #include "graphics/Renderer.h" #include "simulation/Air.h" #include "simulation/GOLString.h" #include "simulation/gravity/Gravity.h" #include "simulation/Simulation.h" #include "simulation/Snapshot.h" #include "simulation/SnapshotDelta.h" #include "simulation/ElementClasses.h" #include "simulation/ElementGraphics.h" #include "simulation/ToolClasses.h" #include "gui/game/DecorationTool.h" #include "gui/interface/Engine.h" #include "gui/dialogues/ErrorMessage.h" #include #include #include HistoryEntry::~HistoryEntry() { // * Needed because Snapshot and SnapshotDelta are incomplete types in GameModel.h, // so the default dtor for ~HistoryEntry cannot be generated. } GameModel::GameModel(): activeMenu(-1), currentBrush(0), currentUser(0, ""), toolStrength(1.0f), historyPosition(0), activeColourPreset(0), colourSelector(false), colour(255, 0, 0, 255), edgeMode(0), ambientAirTemp(R_TEMP + 273.15f), decoSpace(0) { sim = new Simulation(); ren = new Renderer(sim); activeTools = regularToolset; std::fill(decoToolset, decoToolset+4, (Tool*)NULL); std::fill(regularToolset, regularToolset+4, (Tool*)NULL); //Default render prefs ren->SetRenderMode({ RENDER_FIRE, RENDER_EFFE, RENDER_BASC, }); ren->SetDisplayMode({}); ren->SetColourMode(0); //Load config into renderer auto &prefs = GlobalPrefs::Ref(); ren->SetColourMode(prefs.Get("Renderer.ColourMode", 0U)); auto displayModes = prefs.Get("Renderer.DisplayModes", std::vector{}); if (displayModes.size()) { ren->SetDisplayMode(displayModes); } auto renderModes = prefs.Get("Renderer.RenderModes", std::vector{}); if (renderModes.size()) { ren->SetRenderMode(renderModes); } ren->gravityFieldEnabled = prefs.Get("Renderer.GravityField", false); ren->decorations_enable = prefs.Get("Renderer.Decorations", true); //Load config into simulation edgeMode = prefs.Get("Simulation.EdgeMode", 0); // TODO: EdgeMode enum sim->SetEdgeMode(edgeMode); ambientAirTemp = float(R_TEMP) + 273.15f; { auto temp = prefs.Get("Simulation.AmbientAirTemp", ambientAirTemp); if (MIN_TEMP <= temp && MAX_TEMP >= temp) { ambientAirTemp = temp; } } sim->air->ambientAirTemp = ambientAirTemp; decoSpace = prefs.Get("Simulation.DecoSpace", 0); // TODO: DecoSpace enum sim->SetDecoSpace(decoSpace); int ngrav_enable = prefs.Get("Simulation.NewtonianGravity", 0); // TODO: NewtonianGravity enum if (ngrav_enable) sim->grav->start_grav_async(); sim->aheat_enable = prefs.Get("Simulation.AmbientHeat", 0); // TODO: AmbientHeat enum sim->pretty_powder = prefs.Get("Simulation.PrettyPowder", 0); // TODO: PrettyPowder enum Favorite::Ref().LoadFavoritesFromPrefs(); //Load last user if(Client::Ref().GetAuthUser().UserID) { currentUser = Client::Ref().GetAuthUser(); } BuildMenus(); perfectCircle = prefs.Get("PerfectCircleBrush", true); BuildBrushList(); //Set default decoration colour unsigned char colourR = std::max(std::min(prefs.Get("Decoration.Red", 200), 255), 0); unsigned char colourG = std::max(std::min(prefs.Get("Decoration.Green", 100), 255), 0); unsigned char colourB = std::max(std::min(prefs.Get("Decoration.Blue", 50), 255), 0); unsigned char colourA = std::max(std::min(prefs.Get("Decoration.Alpha", 255), 255), 0); SetColourSelectorColour(ui::Colour(colourR, colourG, colourB, colourA)); colourPresets.push_back(ui::Colour(255, 255, 255)); colourPresets.push_back(ui::Colour(0, 255, 255)); colourPresets.push_back(ui::Colour(255, 0, 255)); colourPresets.push_back(ui::Colour(255, 255, 0)); colourPresets.push_back(ui::Colour(255, 0, 0)); colourPresets.push_back(ui::Colour(0, 255, 0)); colourPresets.push_back(ui::Colour(0, 0, 255)); colourPresets.push_back(ui::Colour(0, 0, 0)); undoHistoryLimit = prefs.Get("Simulation.UndoHistoryLimit", 5U); // cap due to memory usage (this is about 3.4GB of RAM) if (undoHistoryLimit > 200) SetUndoHistoryLimit(200); mouseClickRequired = prefs.Get("MouseClickRequired", false); includePressure = prefs.Get("Simulation.IncludePressure", true); temperatureScale = prefs.Get("Renderer.TemperatureScale", 1); // TODO: TemperatureScale enum ClearSimulation(); } GameModel::~GameModel() { auto &prefs = GlobalPrefs::Ref(); { //Save to config: Prefs::DeferWrite dw(prefs); prefs.Set("Renderer.ColourMode", ren->GetColourMode()); prefs.Set("Renderer.DisplayModes", ren->GetDisplayMode()); prefs.Set("Renderer.RenderModes", ren->GetRenderMode()); prefs.Set("Renderer.GravityField", (bool)ren->gravityFieldEnabled); prefs.Set("Renderer.Decorations", (bool)ren->decorations_enable); prefs.Set("Renderer.DebugMode", ren->debugLines); //These two should always be equivalent, even though they are different things prefs.Set("Simulation.NewtonianGravity", sim->grav->IsEnabled()); prefs.Set("Simulation.AmbientHeat", sim->aheat_enable); prefs.Set("Simulation.PrettyPowder", sim->pretty_powder); prefs.Set("Decoration.Red", (int)colour.Red); prefs.Set("Decoration.Green", (int)colour.Green); prefs.Set("Decoration.Blue", (int)colour.Blue); prefs.Set("Decoration.Alpha", (int)colour.Alpha); } for (size_t i = 0; i < menuList.size(); i++) { if (i == SC_FAVORITES) menuList[i]->ClearTools(); delete menuList[i]; } for (std::vector::iterator iter = extraElementTools.begin(), end = extraElementTools.end(); iter != end; ++iter) { delete *iter; } delete sim; delete ren; //if(activeTools) // delete[] activeTools; } void GameModel::UpdateQuickOptions() { for(std::vector::iterator iter = quickOptions.begin(), end = quickOptions.end(); iter != end; ++iter) { QuickOption * option = *iter; option->Update(); } } void GameModel::BuildQuickOptionMenu(GameController * controller) { for(std::vector::iterator iter = quickOptions.begin(), end = quickOptions.end(); iter != end; ++iter) { delete *iter; } quickOptions.clear(); quickOptions.push_back(new SandEffectOption(this)); quickOptions.push_back(new DrawGravOption(this)); quickOptions.push_back(new DecorationsOption(this)); quickOptions.push_back(new NGravityOption(this)); quickOptions.push_back(new AHeatOption(this)); quickOptions.push_back(new ConsoleShowOption(this, controller)); notifyQuickOptionsChanged(); UpdateQuickOptions(); } void GameModel::BuildMenus() { int lastMenu = -1; if(activeMenu != -1) lastMenu = activeMenu; ByteString activeToolIdentifiers[4]; if(regularToolset[0]) activeToolIdentifiers[0] = regularToolset[0]->Identifier; if(regularToolset[1]) activeToolIdentifiers[1] = regularToolset[1]->Identifier; if(regularToolset[2]) activeToolIdentifiers[2] = regularToolset[2]->Identifier; if(regularToolset[3]) activeToolIdentifiers[3] = regularToolset[3]->Identifier; //Empty current menus for (size_t i = 0; i < menuList.size(); i++) { if (i == SC_FAVORITES) menuList[i]->ClearTools(); delete menuList[i]; } menuList.clear(); toolList.clear(); for(std::vector::iterator iter = extraElementTools.begin(), end = extraElementTools.end(); iter != end; ++iter) { delete *iter; } extraElementTools.clear(); elementTools.clear(); //Create menus for (int i = 0; i < SC_TOTAL; i++) { menuList.push_back(new Menu(sim->msections[i].icon, sim->msections[i].name, sim->msections[i].doshow)); } //Build menus from Simulation elements for(int i = 0; i < PT_NUM; i++) { if(sim->elements[i].Enabled) { Tool * tempTool; if(i == PT_LIGH) { tempTool = new Element_LIGH_Tool(i, sim->elements[i].Name, sim->elements[i].Description, sim->elements[i].Colour, sim->elements[i].Identifier, sim->elements[i].IconGenerator); } else if(i == PT_TESC) { tempTool = new Element_TESC_Tool(i, sim->elements[i].Name, sim->elements[i].Description, sim->elements[i].Colour, sim->elements[i].Identifier, sim->elements[i].IconGenerator); } else if(i == PT_STKM || i == PT_FIGH || i == PT_STKM2) { tempTool = new PlopTool(i, sim->elements[i].Name, sim->elements[i].Description, sim->elements[i].Colour, sim->elements[i].Identifier, sim->elements[i].IconGenerator); } else { tempTool = new ElementTool(i, sim->elements[i].Name, sim->elements[i].Description, sim->elements[i].Colour, sim->elements[i].Identifier, sim->elements[i].IconGenerator); } if (sim->elements[i].MenuSection >= 0 && sim->elements[i].MenuSection < SC_TOTAL && sim->elements[i].MenuVisible) { menuList[sim->elements[i].MenuSection]->AddTool(tempTool); } else { extraElementTools.push_back(tempTool); } elementTools.push_back(tempTool); } } //Build menu for GOL types for(int i = 0; i < NGOL; i++) { Tool * tempTool = new ElementTool(PT_LIFE|PMAPID(i), builtinGol[i].name, builtinGol[i].description, builtinGol[i].colour, "DEFAULT_PT_LIFE_"+builtinGol[i].name.ToAscii()); menuList[SC_LIFE]->AddTool(tempTool); } { auto &prefs = GlobalPrefs::Ref(); auto customGOLTypes = prefs.Get("CustomGOL.Types", std::vector{}); std::vector validatedCustomLifeTypes; std::vector newCustomGol; bool removedAny = false; for (auto gol : customGOLTypes) { auto parts = gol.FromUtf8().PartitionBy(' '); if (parts.size() != 4) { removedAny = true; continue; } Simulation::CustomGOLData gd; gd.nameString = parts[0]; gd.ruleString = parts[1]; auto &colour1String = parts[2]; auto &colour2String = parts[3]; if (!ValidateGOLName(gd.nameString)) { removedAny = true; continue; } gd.rule = ParseGOLString(gd.ruleString); if (gd.rule == -1) { removedAny = true; continue; } try { gd.colour1 = colour1String.ToNumber(); gd.colour2 = colour2String.ToNumber(); } catch (std::exception &) { removedAny = true; continue; } newCustomGol.push_back(gd); validatedCustomLifeTypes.push_back(gol); } if (removedAny) { // All custom rules that fail validation will be removed prefs.Set("CustomGOL.Types", validatedCustomLifeTypes); } for (auto &gd : newCustomGol) { Tool * tempTool = new ElementTool(PT_LIFE|PMAPID(gd.rule), gd.nameString, "Custom GOL type: " + gd.ruleString, RGB::Unpack(gd.colour1), "DEFAULT_PT_LIFECUST_"+gd.nameString.ToAscii(), NULL); menuList[SC_LIFE]->AddTool(tempTool); } sim->SetCustomGOL(newCustomGol); } //Build other menus from wall data for(int i = 0; i < UI_WALLCOUNT; i++) { Tool * tempTool = new WallTool(i, sim->wtypes[i].descs, sim->wtypes[i].colour, sim->wtypes[i].identifier, sim->wtypes[i].textureGen); menuList[SC_WALL]->AddTool(tempTool); //sim->wtypes[i] } //Build menu for tools for (size_t i = 0; i < sim->tools.size(); i++) { Tool *tempTool = new Tool( i, sim->tools[i].Name, sim->tools[i].Description, sim->tools[i].Colour, sim->tools[i].Identifier ); menuList[SC_TOOL]->AddTool(tempTool); } //Add special sign and prop tools menuList[SC_TOOL]->AddTool(new WindTool()); menuList[SC_TOOL]->AddTool(new PropertyTool(*this)); menuList[SC_TOOL]->AddTool(new SignTool(*this)); menuList[SC_TOOL]->AddTool(new SampleTool(*this)); menuList[SC_LIFE]->AddTool(new GOLTool(*this)); //Add decoration tools to menu menuList[SC_DECO]->AddTool(new DecorationTool(*ren, DECO_ADD, "ADD", "Colour blending: Add.", 0x000000_rgb, "DEFAULT_DECOR_ADD")); menuList[SC_DECO]->AddTool(new DecorationTool(*ren, DECO_SUBTRACT, "SUB", "Colour blending: Subtract.", 0x000000_rgb, "DEFAULT_DECOR_SUB")); menuList[SC_DECO]->AddTool(new DecorationTool(*ren, DECO_MULTIPLY, "MUL", "Colour blending: Multiply.", 0x000000_rgb, "DEFAULT_DECOR_MUL")); menuList[SC_DECO]->AddTool(new DecorationTool(*ren, DECO_DIVIDE, "DIV", "Colour blending: Divide." , 0x000000_rgb, "DEFAULT_DECOR_DIV")); menuList[SC_DECO]->AddTool(new DecorationTool(*ren, DECO_SMUDGE, "SMDG", "Smudge tool, blends surrounding deco together.", 0x000000_rgb, "DEFAULT_DECOR_SMDG")); menuList[SC_DECO]->AddTool(new DecorationTool(*ren, DECO_CLEAR, "CLR", "Erase any set decoration.", 0x000000_rgb, "DEFAULT_DECOR_CLR")); menuList[SC_DECO]->AddTool(new DecorationTool(*ren, DECO_DRAW, "SET", "Draw decoration (No blending).", 0x000000_rgb, "DEFAULT_DECOR_SET")); SetColourSelectorColour(colour); // update tool colors decoToolset[0] = GetToolFromIdentifier("DEFAULT_DECOR_SET"); decoToolset[1] = GetToolFromIdentifier("DEFAULT_DECOR_CLR"); decoToolset[2] = GetToolFromIdentifier("DEFAULT_UI_SAMPLE"); decoToolset[3] = GetToolFromIdentifier("DEFAULT_PT_NONE"); regularToolset[0] = GetToolFromIdentifier(activeToolIdentifiers[0]); regularToolset[1] = GetToolFromIdentifier(activeToolIdentifiers[1]); regularToolset[2] = GetToolFromIdentifier(activeToolIdentifiers[2]); regularToolset[3] = GetToolFromIdentifier(activeToolIdentifiers[3]); //Set default tools if (!regularToolset[0]) regularToolset[0] = GetToolFromIdentifier("DEFAULT_PT_DUST"); if (!regularToolset[1]) regularToolset[1] = GetToolFromIdentifier("DEFAULT_PT_NONE"); if (!regularToolset[2]) regularToolset[2] = GetToolFromIdentifier("DEFAULT_UI_SAMPLE"); if (!regularToolset[3]) regularToolset[3] = GetToolFromIdentifier("DEFAULT_PT_NONE"); lastTool = activeTools[0]; //Set default menu activeMenu = SC_POWDERS; if(lastMenu != -1) activeMenu = lastMenu; if(activeMenu != -1) toolList = menuList[activeMenu]->GetToolList(); else toolList = std::vector(); notifyMenuListChanged(); notifyToolListChanged(); notifyActiveToolsChanged(); notifyLastToolChanged(); //Build menu for favorites BuildFavoritesMenu(); } void GameModel::BuildFavoritesMenu() { menuList[SC_FAVORITES]->ClearTools(); std::vector favList = Favorite::Ref().GetFavoritesList(); for (size_t i = 0; i < favList.size(); i++) { Tool *tool = GetToolFromIdentifier(favList[i]); if (tool) menuList[SC_FAVORITES]->AddTool(tool); } if (activeMenu == SC_FAVORITES) toolList = menuList[SC_FAVORITES]->GetToolList(); notifyMenuListChanged(); notifyToolListChanged(); notifyActiveToolsChanged(); notifyLastToolChanged(); } void GameModel::BuildBrushList() { ui::Point radius{ 4, 4 }; if (brushList.size()) radius = brushList[currentBrush]->GetRadius(); brushList.clear(); brushList.push_back(std::make_unique(perfectCircle)); brushList.push_back(std::make_unique()); brushList.push_back(std::make_unique()); //Load more from brushes folder for (ByteString brushFile : Platform::DirectorySearch(BRUSH_DIR, "", { ".ptb" })) { std::vector brushData; if (!Platform::ReadFile(brushData, ByteString::Build(BRUSH_DIR, PATH_SEP_CHAR, brushFile))) { std::cout << "Brushes: Skipping " << brushFile << ". Could not open" << std::endl; continue; } auto dimension = size_t(std::sqrt(brushData.size())); if (dimension * dimension != brushData.size()) { std::cout << "Brushes: Skipping " << brushFile << ". Invalid bitmap size" << std::endl; continue; } brushList.push_back(std::make_unique(ui::Point(dimension, dimension), reinterpret_cast(brushData.data()))); } brushList[currentBrush]->SetRadius(radius); notifyBrushChanged(); } Tool *GameModel::GetToolFromIdentifier(ByteString const &identifier) { for (auto *menu : menuList) { for (auto *tool : menu->GetToolList()) { if (identifier == tool->Identifier) { return tool; } } } for (auto *extra : extraElementTools) { if (identifier == extra->Identifier) { return extra; } } return nullptr; } void GameModel::SetEdgeMode(int edgeMode) { this->edgeMode = edgeMode; sim->SetEdgeMode(edgeMode); } int GameModel::GetEdgeMode() { return this->edgeMode; } void GameModel::SetTemperatureScale(int temperatureScale) { this->temperatureScale = temperatureScale; } void GameModel::SetAmbientAirTemperature(float ambientAirTemp) { this->ambientAirTemp = ambientAirTemp; sim->air->ambientAirTemp = ambientAirTemp; } float GameModel::GetAmbientAirTemperature() { return this->ambientAirTemp; } void GameModel::SetDecoSpace(int decoSpace) { sim->SetDecoSpace(decoSpace); this->decoSpace = sim->deco_space; } int GameModel::GetDecoSpace() { return this->decoSpace; } // * SnapshotDelta d is the difference between the two Snapshots A and B (i.e. d = B - A) // if auto d = SnapshotDelta::FromSnapshots(A, B). In this case, a Snapshot that is // identical to B can be constructed from d and A via d.Forward(A) (i.e. B = A + d) // and a Snapshot that is identical to A can be constructed from d and B via // d.Restore(B) (i.e. A = B - d). SnapshotDeltas often consume less memory than Snapshots, // although pathological cases of pairs of Snapshots exist, the SnapshotDelta constructed // from which actually consumes more than the two snapshots combined. // * GameModel::history is an N-item deque of HistoryEntry structs, each of which owns either // a SnapshotDelta, except for history[N-1], which always owns a Snapshot. A logical Snapshot // accompanies each item in GameModel::history. This logical Snapshot may or may not be // materialised (present in memory). If an item owns an actual Snapshot, the aforementioned // logical Snapshot is this materialised Snapshot. If, however, an item owns a SnapshotDelta d, // the accompanying logical Snapshot A is the Snapshot obtained via A = d.Restore(B), where B // is the logical Snapshot accompanying the next (at an index that is one higher than the // index of this item) item in history. Slightly more visually: // // i | history[i] | the logical Snapshot | relationships | // | | accompanying history[i] | | // -------|-----------------|-------------------------|---------------| // | | | | // N - 1 | Snapshot A | Snapshot A | A | // | | | / | // N - 2 | SnapshotDelta b | Snapshot B | B+b=A b-B | // | | | / | // N - 3 | SnapshotDelta c | Snapshot C | C+c=B c-C | // | | | / | // N - 4 | SnapshotDelta d | Snapshot D | D+d=C d-D | // | | | / | // ... | ... | ... | ... ... | // // * GameModel::historyPosition is an integer in the closed range 0 to N, which is decremented // by GameModel::HistoryRestore and incremented by GameModel::HistoryForward, by 1 at a time. // GameModel::historyCurrent "follows" historyPosition such that it always holds a Snapshot // that is identical to the logical Snapshot of history[historyPosition], except when // historyPosition = N, in which case it's empty. This following behaviour is achieved either // by "stepping" historyCurrent by Forwarding and Restoring it via the SnapshotDelta in // history[historyPosition], cloning the Snapshot in history[historyPosition] into it if // historyPosition = N-1, or clearing if it historyPosition = N. // * GameModel::historyCurrent is lost when a new Snapshot item is pushed into GameModel::history. // This item appears wherever historyPosition currently points, and every other item above it // is deleted. If historyPosition is below N, this gets rid of the Snapshot in history[N-1]. // Thus, N is set to historyPosition, after which the new Snapshot is pushed and historyPosition // is incremented to the new N. // * Pushing a new Snapshot into the history is a bit involved: // * If there are no history entries yet, the new Snapshot is simply placed into GameModel::history. // From now on, we consider cases in which GameModel::history is originally not empty. // // === after pushing Snapshot A' into the history // // i | history[i] | the logical Snapshot | relationships | // | | accompanying history[i] | | // -------|-----------------|-------------------------|---------------| // | | | | // 0 | Snapshot A | Snapshot A | A | // // * If there were discarded history entries (i.e. the user decided to continue from some state // which they arrived to via at least one Ctrl+Z), history[N-2] is a SnapshotDelta that when // Forwarded with the logical Snapshot of history[N-2] yields the logical Snapshot of history[N-1] // from before the new item was pushed. This is not what we want, so we replace it with a // SnapshotDelta that is the difference between the logical Snapshot of history[N-2] and the // Snapshot freshly placed in history[N-1]. // // === after pushing Snapshot A' into the history // // i | history[i] | the logical Snapshot | relationships | // | | accompanying history[i] | | // -------|-----------------|-------------------------|---------------| // | | | | // N - 1 | Snapshot A' | Snapshot A' | A' | b needs to be replaced with b', // | | | / | B+b'=A'; otherwise we'd run // N - 2 | SnapshotDelta b | Snapshot B | B+b=A b-B | into problems when trying to // | | | / | reconstruct B from A' and b // N - 3 | SnapshotDelta c | Snapshot C | C+c=B c-C | in HistoryRestore. // | | | / | // N - 4 | SnapshotDelta d | Snapshot D | D+d=C d-D | // | | | / | // ... | ... | ... | ... ... | // // === after replacing b with b' // // i | history[i] | the logical Snapshot | relationships | // | | accompanying history[i] | | // -------|-----------------|-------------------------|---------------| // | | | | // N - 1 | Snapshot A' | Snapshot A' | A' | // | | | / | // N - 2 | SnapshotDelta b'| Snapshot B | B+b'=A' b'-B | // | | | / | // N - 3 | SnapshotDelta c | Snapshot C | C+c=B c-C | // | | | / | // N - 4 | SnapshotDelta d | Snapshot D | D+d=C d-D | // | | | / | // ... | ... | ... | ... ... | // // * If there weren't any discarded history entries, history[N-2] is now also a Snapshot. Since // the freshly pushed Snapshot in history[N-1] should be the only Snapshot in history, this is // replaced with the SnapshotDelta that is the difference between history[N-2] and the Snapshot // freshly placed in history[N-1]. // // === after pushing Snapshot A' into the history // // i | history[i] | the logical Snapshot | relationships | // | | accompanying history[i] | | // -------|-----------------|-------------------------|---------------| // | | | | // N - 1 | Snapshot A' | Snapshot A' | A' | A needs to be converted to a, // | | | | otherwise Snapshots would litter // N - 1 | Snapshot A | Snapshot A | A | GameModel::history, which we // | | | / | want to avoid because they // N - 2 | SnapshotDelta b | Snapshot B | B+b=A b-B | waste a ton of memory // | | | / | // N - 3 | SnapshotDelta c | Snapshot C | C+c=B c-C | // | | | / | // N - 4 | SnapshotDelta d | Snapshot D | D+d=C d-D | // | | | / | // ... | ... | ... | ... ... | // // === after replacing A with a // // i | history[i] | the logical Snapshot | relationships | // | | accompanying history[i] | | // -------|-----------------|-------------------------|---------------| // | | | | // N - 1 | Snapshot A' | Snapshot A' | A' | // | | | / | // N - 1 | SnapshotDelta a | Snapshot A | A+a=A' a-A | // | | | / | // N - 2 | SnapshotDelta b | Snapshot B | B+b=A b-B | // | | | / | // N - 3 | SnapshotDelta c | Snapshot C | C+c=B c-C | // | | | / | // N - 4 | SnapshotDelta d | Snapshot D | D+d=C d-D | // | | | / | // ... | ... | ... | ... ... | // // * After all this, the front of the deque is truncated such that there are on more than // undoHistoryLimit entries left. const Snapshot *GameModel::HistoryCurrent() const { return historyCurrent.get(); } bool GameModel::HistoryCanRestore() const { return historyPosition > 0U; } void GameModel::HistoryRestore() { if (!HistoryCanRestore()) { return; } historyPosition -= 1U; if (history[historyPosition].snap) { historyCurrent = std::make_unique(*history[historyPosition].snap); } else { historyCurrent = history[historyPosition].delta->Restore(*historyCurrent); } } bool GameModel::HistoryCanForward() const { return historyPosition < history.size(); } void GameModel::HistoryForward() { if (!HistoryCanForward()) { return; } historyPosition += 1U; if (historyPosition == history.size()) { historyCurrent = nullptr; } else if (history[historyPosition].snap) { historyCurrent = std::make_unique(*history[historyPosition].snap); } else { historyCurrent = history[historyPosition - 1U].delta->Forward(*historyCurrent); } } void GameModel::HistoryPush(std::unique_ptr last) { Snapshot *rebaseOnto = nullptr; if (historyPosition) { rebaseOnto = history.back().snap.get(); if (historyPosition < history.size()) { historyCurrent = history[historyPosition - 1U].delta->Restore(*historyCurrent); rebaseOnto = historyCurrent.get(); } } while (historyPosition < history.size()) { history.pop_back(); } if (rebaseOnto) { auto &prev = history.back(); prev.delta = SnapshotDelta::FromSnapshots(*rebaseOnto, *last); prev.snap.reset(); } history.emplace_back(); history.back().snap = std::move(last); historyPosition += 1U; historyCurrent.reset(); while (undoHistoryLimit < history.size()) { history.pop_front(); historyPosition -= 1U; } } unsigned int GameModel::GetUndoHistoryLimit() { return undoHistoryLimit; } void GameModel::SetUndoHistoryLimit(unsigned int undoHistoryLimit_) { undoHistoryLimit = undoHistoryLimit_; GlobalPrefs::Ref().Set("Simulation.UndoHistoryLimit", undoHistoryLimit); } void GameModel::SetVote(int direction) { queuedVote = direction; } void GameModel::Tick() { if (execVoteRequest && execVoteRequest->CheckDone()) { try { execVoteRequest->Finish(); currentSave->vote = execVoteRequest->Direction(); notifySaveChanged(); } catch (const http::RequestError &ex) { new ErrorMessage("Error while voting", ByteString(ex.what()).FromUtf8()); } execVoteRequest.reset(); } if (!execVoteRequest && queuedVote) { if (currentSave) { execVoteRequest = std::make_unique(currentSave->GetID(), *queuedVote); execVoteRequest->Start(); } queuedVote.reset(); } } Brush &GameModel::GetBrush() { return *brushList[currentBrush]; } Brush *GameModel::GetBrushByID(int i) { if (i >= 0 && i < (int)brushList.size()) return brushList[i].get(); else return nullptr; } int GameModel::GetBrushID() { return currentBrush; } void GameModel::SetBrushID(int i) { auto prevRadius = brushList[currentBrush]->GetRadius(); currentBrush = i%brushList.size(); brushList[currentBrush]->SetRadius(prevRadius); notifyBrushChanged(); } void GameModel::AddObserver(GameView * observer){ observers.push_back(observer); observer->NotifySimulationChanged(this); observer->NotifyRendererChanged(this); observer->NotifyPausedChanged(this); observer->NotifySaveChanged(this); observer->NotifyBrushChanged(this); observer->NotifyMenuListChanged(this); observer->NotifyToolListChanged(this); observer->NotifyUserChanged(this); observer->NotifyZoomChanged(this); observer->NotifyColourSelectorVisibilityChanged(this); observer->NotifyColourSelectorColourChanged(this); observer->NotifyColourPresetsChanged(this); observer->NotifyColourActivePresetChanged(this); observer->NotifyQuickOptionsChanged(this); observer->NotifyLastToolChanged(this); UpdateQuickOptions(); } void GameModel::SetToolStrength(float value) { toolStrength = value; } float GameModel::GetToolStrength() { return toolStrength; } void GameModel::SetActiveMenu(int menuID) { activeMenu = menuID; toolList = menuList[menuID]->GetToolList(); notifyToolListChanged(); if(menuID == SC_DECO) { if(activeTools != decoToolset) { activeTools = decoToolset; notifyActiveToolsChanged(); } } else { if(activeTools != regularToolset) { activeTools = regularToolset; notifyActiveToolsChanged(); } } } std::vector GameModel::GetUnlistedTools() { return extraElementTools; } std::vector GameModel::GetToolList() { return toolList; } int GameModel::GetActiveMenu() { return activeMenu; } //Get an element tool from an element ID Tool * GameModel::GetElementTool(int elementID) { for(std::vector::iterator iter = elementTools.begin(), end = elementTools.end(); iter != end; ++iter) { if((*iter)->ToolID == elementID) return *iter; } return NULL; } Tool * GameModel::GetActiveTool(int selection) { return activeTools[selection]; } void GameModel::SetActiveTool(int selection, Tool * tool) { activeTools[selection] = tool; notifyActiveToolsChanged(); } std::vector GameModel::GetQuickOptions() { return quickOptions; } std::vector GameModel::GetMenuList() { return menuList; } SaveInfo *GameModel::GetSave() // non-owning { return currentSave.get(); } std::unique_ptr GameModel::TakeSave() { // we don't notify listeners because we'll get a new save soon anyway return std::move(currentSave); } void GameModel::SaveToSimParameters(const GameSave &saveData) { SetPaused(saveData.paused | GetPaused()); sim->gravityMode = saveData.gravityMode; sim->customGravityX = saveData.customGravityX; sim->customGravityY = saveData.customGravityY; sim->air->airMode = saveData.airMode; sim->air->ambientAirTemp = saveData.ambientAirTemp; sim->edgeMode = saveData.edgeMode; sim->legacy_enable = saveData.legacyEnable; sim->water_equal_test = saveData.waterEEnabled; sim->aheat_enable = saveData.aheatEnable; if (saveData.gravityEnable && !sim->grav->IsEnabled()) { sim->grav->start_grav_async(); } else if (!saveData.gravityEnable && sim->grav->IsEnabled()) { sim->grav->stop_grav_async(); } sim->frameCount = saveData.frameCount; if (saveData.hasRngState) { sim->rng.state(saveData.rngState); } else { sim->rng = RNG(); } sim->ensureDeterminism = saveData.ensureDeterminism; } void GameModel::SetSave(std::unique_ptr newSave, bool invertIncludePressure) { currentSave = std::move(newSave); currentFile.reset(); if (currentSave && currentSave->GetGameSave()) { auto *saveData = currentSave->GetGameSave(); SaveToSimParameters(*saveData); sim->clear_sim(); ren->ClearAccumulation(); sim->Load(saveData, !invertIncludePressure, { 0, 0 }); // This save was created before logging existed // Add in the correct info if (saveData->authors.size() == 0) { auto gameSave = currentSave->TakeGameSave(); gameSave->authors["type"] = "save"; gameSave->authors["id"] = currentSave->id; gameSave->authors["username"] = currentSave->userName; gameSave->authors["title"] = currentSave->name.ToUtf8(); gameSave->authors["description"] = currentSave->Description.ToUtf8(); gameSave->authors["published"] = (int)currentSave->Published; gameSave->authors["date"] = (Json::Value::UInt64)currentSave->updatedDate; currentSave->SetGameSave(std::move(gameSave)); } // This save was probably just created, and we didn't know the ID when creating it // Update with the proper ID else if (saveData->authors.get("id", -1) == 0 || saveData->authors.get("id", -1) == -1) { auto gameSave = currentSave->TakeGameSave(); gameSave->authors["id"] = currentSave->id; currentSave->SetGameSave(std::move(gameSave)); } Client::Ref().OverwriteAuthorInfo(saveData->authors); } notifySaveChanged(); UpdateQuickOptions(); } const SaveFile *GameModel::GetSaveFile() const { return currentFile.get(); } std::unique_ptr GameModel::TakeSaveFile() { // we don't notify listeners because we'll get a new save soon anyway return std::move(currentFile); } void GameModel::SetSaveFile(std::unique_ptr newSave, bool invertIncludePressure) { currentFile = std::move(newSave); currentSave.reset(); if (currentFile && currentFile->GetGameSave()) { auto *saveData = currentFile->GetGameSave(); SaveToSimParameters(*saveData); sim->clear_sim(); ren->ClearAccumulation(); sim->Load(saveData, !invertIncludePressure, { 0, 0 }); Client::Ref().OverwriteAuthorInfo(saveData->authors); } notifySaveChanged(); UpdateQuickOptions(); } Simulation * GameModel::GetSimulation() { return sim; } Renderer * GameModel::GetRenderer() { return ren; } User GameModel::GetUser() { return currentUser; } Tool * GameModel::GetLastTool() { return lastTool; } void GameModel::SetLastTool(Tool * newTool) { if(lastTool != newTool) { lastTool = newTool; notifyLastToolChanged(); } } void GameModel::SetZoomEnabled(bool enabled) { ren->zoomEnabled = enabled; notifyZoomChanged(); } bool GameModel::GetZoomEnabled() { return ren->zoomEnabled; } void GameModel::SetZoomPosition(ui::Point position) { ren->zoomScopePosition = position; notifyZoomChanged(); } ui::Point GameModel::GetZoomPosition() { return ren->zoomScopePosition; } bool GameModel::MouseInZoom(ui::Point position) { if (!GetZoomEnabled()) return false; int zoomFactor = GetZoomFactor(); ui::Point zoomWindowPosition = GetZoomWindowPosition(); ui::Point zoomWindowSize = ui::Point(GetZoomSize()*zoomFactor, GetZoomSize()*zoomFactor); if (position.X >= zoomWindowPosition.X && position.Y >= zoomWindowPosition.Y && position.X < zoomWindowPosition.X+zoomWindowSize.X && position.Y < zoomWindowPosition.Y+zoomWindowSize.Y) return true; return false; } ui::Point GameModel::AdjustZoomCoords(ui::Point position) { if (!GetZoomEnabled()) return position; int zoomFactor = GetZoomFactor(); ui::Point zoomWindowPosition = GetZoomWindowPosition(); ui::Point zoomWindowSize = ui::Point(GetZoomSize()*zoomFactor, GetZoomSize()*zoomFactor); if (position.X >= zoomWindowPosition.X && position.Y >= zoomWindowPosition.Y && position.X < zoomWindowPosition.X+zoomWindowSize.X && position.Y < zoomWindowPosition.Y+zoomWindowSize.Y) return ((position-zoomWindowPosition)/GetZoomFactor())+GetZoomPosition(); return position; } void GameModel::SetZoomWindowPosition(ui::Point position) { ren->zoomWindowPosition = position; notifyZoomChanged(); } ui::Point GameModel::GetZoomWindowPosition() { return ren->zoomWindowPosition; } void GameModel::SetZoomSize(int size) { ren->zoomScopeSize = size; notifyZoomChanged(); } int GameModel::GetZoomSize() { return ren->zoomScopeSize; } void GameModel::SetZoomFactor(int factor) { ren->ZFACTOR = factor; notifyZoomChanged(); } int GameModel::GetZoomFactor() { return ren->ZFACTOR; } void GameModel::SetActiveColourPreset(size_t preset) { if (activeColourPreset-1 != preset) activeColourPreset = preset+1; else { activeTools[0] = GetToolFromIdentifier("DEFAULT_DECOR_SET"); notifyActiveToolsChanged(); } notifyColourActivePresetChanged(); } size_t GameModel::GetActiveColourPreset() { return activeColourPreset-1; } void GameModel::SetPresetColour(ui::Colour colour) { if (activeColourPreset > 0 && activeColourPreset <= colourPresets.size()) { colourPresets[activeColourPreset-1] = colour; notifyColourPresetsChanged(); } } std::vector GameModel::GetColourPresets() { return colourPresets; } void GameModel::SetColourSelectorVisibility(bool visibility) { if(colourSelector != visibility) { colourSelector = visibility; notifyColourSelectorVisibilityChanged(); } } bool GameModel::GetColourSelectorVisibility() { return colourSelector; } void GameModel::SetColourSelectorColour(ui::Colour colour_) { colour = colour_; std::vector tools = GetMenuList()[SC_DECO]->GetToolList(); for (auto tool : tools) static_cast(tool)->Colour = colour; notifyColourSelectorColourChanged(); } ui::Colour GameModel::GetColourSelectorColour() { return colour; } void GameModel::SetUser(User user) { currentUser = user; //Client::Ref().SetAuthUser(user); notifyUserChanged(); } void GameModel::SetPaused(bool pauseState) { if (!pauseState && sim->debug_nextToUpdate > 0) { String logmessage = String::Build("Updated particles from #", sim->debug_nextToUpdate, " to end due to unpause"); UpdateUpTo(NPART); Log(logmessage, false); } sim->sys_pause = pauseState?1:0; notifyPausedChanged(); } bool GameModel::GetPaused() { return sim->sys_pause?true:false; } void GameModel::SetDecoration(bool decorationState) { if (ren->decorations_enable != (decorationState?1:0)) { ren->decorations_enable = decorationState?1:0; notifyDecorationChanged(); UpdateQuickOptions(); if (decorationState) SetInfoTip("Decorations Layer: On"); else SetInfoTip("Decorations Layer: Off"); } } bool GameModel::GetDecoration() { return ren->decorations_enable?true:false; } void GameModel::SetAHeatEnable(bool aHeat) { sim->aheat_enable = aHeat; UpdateQuickOptions(); if (aHeat) SetInfoTip("Ambient Heat: On"); else SetInfoTip("Ambient Heat: Off"); } bool GameModel::GetAHeatEnable() { return sim->aheat_enable; } void GameModel::ResetAHeat() { sim->air->ClearAirH(); } void GameModel::SetNewtonianGravity(bool newtonainGravity) { if (newtonainGravity) { sim->grav->start_grav_async(); SetInfoTip("Newtonian Gravity: On"); } else { sim->grav->stop_grav_async(); SetInfoTip("Newtonian Gravity: Off"); } UpdateQuickOptions(); } bool GameModel::GetNewtonianGrvity() { return sim->grav->IsEnabled(); } void GameModel::ShowGravityGrid(bool showGrid) { ren->gravityFieldEnabled = showGrid; if (showGrid) SetInfoTip("Gravity Grid: On"); else SetInfoTip("Gravity Grid: Off"); } bool GameModel::GetGravityGrid() { return ren->gravityFieldEnabled; } void GameModel::FrameStep(int frames) { sim->framerender += frames; } void GameModel::ClearSimulation() { //Load defaults sim->gravityMode = 0; sim->customGravityX = 0.0f; sim->customGravityY = 0.0f; sim->air->airMode = 0; sim->legacy_enable = false; sim->water_equal_test = false; sim->SetEdgeMode(edgeMode); sim->air->ambientAirTemp = ambientAirTemp; sim->clear_sim(); ren->ClearAccumulation(); Client::Ref().ClearAuthorInfo(); notifySaveChanged(); UpdateQuickOptions(); } void GameModel::SetPlaceSave(std::unique_ptr save) { transformedPlaceSave.reset(); placeSave = std::move(save); notifyPlaceSaveChanged(); } void GameModel::TransformPlaceSave(Mat2 transform, Vec2 nudge) { if (placeSave) { transformedPlaceSave = std::make_unique(*placeSave); transformedPlaceSave->Transform(transform, nudge); } notifyTransformedPlaceSaveChanged(); } void GameModel::SetClipboard(std::unique_ptr save) { clipboard = std::move(save); } const GameSave *GameModel::GetClipboard() const { return clipboard.get(); } const GameSave *GameModel::GetTransformedPlaceSave() const { return transformedPlaceSave.get(); } void GameModel::Log(String message, bool printToFile) { consoleLog.push_front(message); if(consoleLog.size()>100) consoleLog.pop_back(); notifyLogChanged(message); if (printToFile) std::cout << message.ToUtf8() << std::endl; } std::deque GameModel::GetLog() { return consoleLog; } std::vector GameModel::GetNotifications() { return notifications; } void GameModel::AddNotification(Notification * notification) { notifications.push_back(notification); notifyNotificationsChanged(); } void GameModel::RemoveNotification(Notification * notification) { for(std::vector::iterator iter = notifications.begin(); iter != notifications.end(); ++iter) { if(*iter == notification) { delete *iter; notifications.erase(iter); break; } } notifyNotificationsChanged(); } void GameModel::SetToolTip(String text) { toolTip = text; notifyToolTipChanged(); } void GameModel::SetInfoTip(String text) { infoTip = text; notifyInfoTipChanged(); } String GameModel::GetToolTip() { return toolTip; } String GameModel::GetInfoTip() { return infoTip; } void GameModel::notifyNotificationsChanged() { for (std::vector::iterator iter = observers.begin(); iter != observers.end(); ++iter) { (*iter)->NotifyNotificationsChanged(this); } } void GameModel::notifyColourPresetsChanged() { for (std::vector::iterator iter = observers.begin(); iter != observers.end(); ++iter) { (*iter)->NotifyColourPresetsChanged(this); } } void GameModel::notifyColourActivePresetChanged() { for (std::vector::iterator iter = observers.begin(); iter != observers.end(); ++iter) { (*iter)->NotifyColourActivePresetChanged(this); } } void GameModel::notifyColourSelectorColourChanged() { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifyColourSelectorColourChanged(this); } } void GameModel::notifyColourSelectorVisibilityChanged() { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifyColourSelectorVisibilityChanged(this); } } void GameModel::notifyRendererChanged() { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifyRendererChanged(this); } } void GameModel::notifySaveChanged() { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifySaveChanged(this); } } void GameModel::notifySimulationChanged() { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifySimulationChanged(this); } } void GameModel::notifyPausedChanged() { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifyPausedChanged(this); } } void GameModel::notifyDecorationChanged() { for (size_t i = 0; i < observers.size(); i++) { //observers[i]->NotifyPausedChanged(this); } } void GameModel::notifyBrushChanged() { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifyBrushChanged(this); } } void GameModel::notifyMenuListChanged() { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifyMenuListChanged(this); } } void GameModel::notifyToolListChanged() { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifyToolListChanged(this); } } void GameModel::notifyActiveToolsChanged() { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifyActiveToolsChanged(this); } } void GameModel::notifyUserChanged() { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifyUserChanged(this); } } void GameModel::notifyZoomChanged() { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifyZoomChanged(this); } } void GameModel::notifyPlaceSaveChanged() { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifyPlaceSaveChanged(this); } } void GameModel::notifyTransformedPlaceSaveChanged() { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifyTransformedPlaceSaveChanged(this); } } void GameModel::notifyLogChanged(String entry) { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifyLogChanged(this, entry); } } void GameModel::notifyInfoTipChanged() { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifyInfoTipChanged(this); } } void GameModel::notifyToolTipChanged() { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifyToolTipChanged(this); } } void GameModel::notifyQuickOptionsChanged() { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifyQuickOptionsChanged(this); } } void GameModel::notifyLastToolChanged() { for (size_t i = 0; i < observers.size(); i++) { observers[i]->NotifyLastToolChanged(this); } } bool GameModel::GetMouseClickRequired() { return mouseClickRequired; } void GameModel::SetMouseClickRequired(bool mouseClickRequired) { this->mouseClickRequired = mouseClickRequired; } bool GameModel::GetIncludePressure() { return includePressure; } void GameModel::SetIncludePressure(bool includePressure) { this->includePressure = includePressure; } void GameModel::SetPerfectCircle(bool perfectCircle) { if (perfectCircle != this->perfectCircle) { this->perfectCircle = perfectCircle; BuildBrushList(); } } bool GameModel::RemoveCustomGOLType(const ByteString &identifier) { bool removedAny = false; auto &prefs = GlobalPrefs::Ref(); auto customGOLTypes = prefs.Get("CustomGOL.Types", std::vector{}); std::vector newCustomGOLTypes; for (auto gol : customGOLTypes) { auto parts = gol.PartitionBy(' '); if (parts.size() && "DEFAULT_PT_LIFECUST_" + parts[0] == identifier) removedAny = true; else newCustomGOLTypes.push_back(gol); } if (removedAny) { prefs.Set("CustomGOL.Types", newCustomGOLTypes); } BuildMenus(); return removedAny; } void GameModel::UpdateUpTo(int upTo) { if (upTo < sim->debug_nextToUpdate) { upTo = NPART; } if (sim->debug_nextToUpdate == 0) { BeforeSim(); } sim->UpdateParticles(sim->debug_nextToUpdate, upTo); if (upTo < NPART) { sim->debug_nextToUpdate = upTo; } else { AfterSim(); sim->debug_nextToUpdate = 0; } } void GameModel::BeforeSim() { if (!sim->sys_pause || sim->framerender) { commandInterface->HandleEvent(BeforeSimEvent{}); } sim->BeforeSim(); } void GameModel::AfterSim() { sim->AfterSim(); commandInterface->HandleEvent(AfterSimEvent{}); }