diff --git a/src/client/Client.cpp b/src/client/Client.cpp index 2182b1e66..1e2d8531d 100644 --- a/src/client/Client.cpp +++ b/src/client/Client.cpp @@ -511,34 +511,6 @@ void Client::SaveAuthorInfo(Json::Value *saveInto) } } -bool AddCustomGol(String ruleString, String nameString, unsigned int highColor, unsigned int lowColor) -{ - auto &prefs = GlobalPrefs::Ref(); - auto customGOLTypes = prefs.Get("CustomGOL.Types", std::vector{}); - std::vector newCustomGOLTypes; - bool nameTaken = false; - for (auto gol : customGOLTypes) - { - auto parts = gol.FromUtf8().PartitionBy(' '); - if (parts.size()) - { - if (parts[0] == nameString) - { - nameTaken = true; - } - } - newCustomGOLTypes.push_back(gol); - } - if (nameTaken) - return false; - - StringBuilder sb; - sb << nameString << " " << ruleString << " " << highColor << " " << lowColor; - newCustomGOLTypes.push_back(sb.Build().ToUtf8()); - prefs.Set("CustomGOL.Types", newCustomGOLTypes); - return true; -} - String Client::DoMigration(ByteString fromDir, ByteString toDir) { if (fromDir.at(fromDir.length() - 1) != '/') diff --git a/src/client/Client.h b/src/client/Client.h index 365dfa51e..c3beb6f7b 100644 --- a/src/client/Client.h +++ b/src/client/Client.h @@ -105,5 +105,3 @@ public: String DoMigration(ByteString fromDir, ByteString toDir); }; - -bool AddCustomGol(String ruleString, String nameString, unsigned int highColor, unsigned int lowColor); diff --git a/src/graphics/Pixel.h b/src/graphics/Pixel.h index 43b90b4b6..5e6c2545f 100644 --- a/src/graphics/Pixel.h +++ b/src/graphics/Pixel.h @@ -24,6 +24,8 @@ struct alignas(alignof(uint32_t) > alignof(T) ? alignof(uint32_t) : alignof(T)) { T Blue, Green, Red; + constexpr RGB() = default; + constexpr RGB(T r, T g, T b): Blue(b), Green(g), @@ -121,6 +123,8 @@ struct alignas(alignof(uint32_t) > alignof(T) ? alignof(uint32_t) : alignof(T)) { T Blue, Green, Red, Alpha; + constexpr RGBA() = default; + constexpr RGBA(T r, T g, T b, T a): Blue(b), Green(g), diff --git a/src/gui/game/GameController.cpp b/src/gui/game/GameController.cpp index ac0a0834c..1037ee21f 100644 --- a/src/gui/game/GameController.cpp +++ b/src/gui/game/GameController.cpp @@ -1719,9 +1719,9 @@ bool GameController::GetMouseClickRequired() return gameModel->GetMouseClickRequired(); } -void GameController::RemoveCustomGOLType(const ByteString &identifier) +void GameController::RemoveCustomGol(const ByteString &identifier) { - gameModel->RemoveCustomGOLType(identifier); + gameModel->RemoveCustomGol(identifier); } void GameController::BeforeSimDraw() diff --git a/src/gui/game/GameController.h b/src/gui/game/GameController.h index fb605d9bb..b3f0ded37 100644 --- a/src/gui/game/GameController.h +++ b/src/gui/game/GameController.h @@ -201,7 +201,7 @@ public: void RunUpdater(UpdateInfo info); bool GetMouseClickRequired(); - void RemoveCustomGOLType(const ByteString &identifier); + void RemoveCustomGol(const ByteString &identifier); void BeforeSimDraw(); void AfterSimDraw(); diff --git a/src/gui/game/GameModel.cpp b/src/gui/game/GameModel.cpp index 70d6399e2..a2f6f1323 100644 --- a/src/gui/game/GameModel.cpp +++ b/src/gui/game/GameModel.cpp @@ -137,6 +137,7 @@ GameModel::GameModel(): currentUser = Client::Ref().GetAuthUser(); } + LoadCustomGol(); BuildMenus(); perfectCircle = prefs.Get("PerfectCircleBrush", true); @@ -312,60 +313,10 @@ void GameModel::BuildMenus() Tool * tempTool = AddTool(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); } + for (auto &gd : sd.GetCustomGol()) { - 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; - } - 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 = AddTool(PT_LIFE|PMAPID(gd.rule), gd.nameString, "Custom GOL type: " + gd.ruleString, RGB::Unpack(gd.colour1), "DEFAULT_PT_LIFECUST_"+gd.nameString.ToAscii(), nullptr); - menuList[SC_LIFE]->AddTool(tempTool); - } - sd.SetCustomGOL(newCustomGol); + Tool * tempTool = AddTool(PT_LIFE|PMAPID(gd.rule), gd.nameString, "Custom GOL type: " + SerialiseGOLRule(gd.rule), gd.colour1, "DEFAULT_PT_LIFECUST_"+gd.nameString.ToAscii(), nullptr); + menuList[SC_LIFE]->AddTool(tempTool); } //Build other menus from wall data @@ -1664,28 +1615,129 @@ void GameModel::SetPerfectCircle(bool perfectCircle) } } -bool GameModel::RemoveCustomGOLType(const ByteString &identifier) +bool GameModel::AddCustomGol(String ruleString, String nameString, RGB color1, RGB color2) +{ + if (auto gd = CheckCustomGol(ruleString, nameString, color1, color2)) + { + auto &sd = SimulationData::Ref(); + auto newCustomGol = sd.GetCustomGol(); + newCustomGol.push_back(*gd); + sd.SetCustomGOL(newCustomGol); + SaveCustomGol(); + BuildMenus(); + return true; + } + return false; +} + +bool GameModel::RemoveCustomGol(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) + std::vector newCustomGol; + auto &sd = SimulationData::Ref(); + for (auto gol : sd.GetCustomGol()) { - auto parts = gol.PartitionBy(' '); - if (parts.size() && "DEFAULT_PT_LIFECUST_" + parts[0] == identifier) + if ("DEFAULT_PT_LIFECUST_" + gol.nameString == identifier.FromUtf8()) + { removedAny = true; + } else - newCustomGOLTypes.push_back(gol); + { + newCustomGol.push_back(gol); + } } if (removedAny) { - prefs.Set("CustomGOL.Types", newCustomGOLTypes); + sd.SetCustomGOL(newCustomGol); + BuildMenus(); + SaveCustomGol(); } - BuildMenus(); return removedAny; } +void GameModel::LoadCustomGol() +{ + auto &prefs = GlobalPrefs::Ref(); + auto customGOLTypes = prefs.Get("CustomGOL.Types", std::vector{}); + bool removedAny = false; + std::vector newCustomGol; + for (auto gol : customGOLTypes) + { + auto parts = gol.FromUtf8().PartitionBy(' '); + if (parts.size() != 4) + { + removedAny = true; + continue; + } + auto nameString = parts[0]; + auto ruleString = parts[1]; + auto &colour1String = parts[2]; + auto &colour2String = parts[3]; + RGB color1; + RGB color2; + try + { + color1 = RGB::Unpack(colour1String.ToNumber()); + color2 = RGB::Unpack(colour2String.ToNumber()); + } + catch (std::exception &) + { + removedAny = true; + continue; + } + if (auto gd = CheckCustomGol(ruleString, nameString, color1, color2)) + { + newCustomGol.push_back(*gd); + } + else + { + removedAny = true; + } + } + auto &sd = SimulationData::Ref(); + sd.SetCustomGOL(newCustomGol); + if (removedAny) + { + SaveCustomGol(); + } +} + +void GameModel::SaveCustomGol() +{ + auto &prefs = GlobalPrefs::Ref(); + std::vector newCustomGOLTypes; + auto &sd = SimulationData::Ref(); + for (auto &gd : sd.GetCustomGol()) + { + StringBuilder sb; + sb << gd.nameString << " " << SerialiseGOLRule(gd.rule) << " " << gd.colour1.Pack() << " " << gd.colour2.Pack(); + newCustomGOLTypes.push_back(sb.Build().ToUtf8()); + } + prefs.Set("CustomGOL.Types", newCustomGOLTypes); +} + +std::optional GameModel::CheckCustomGol(String ruleString, String nameString, RGB color1, RGB color2) +{ + if (!ValidateGOLName(nameString)) + { + return std::nullopt; + } + auto rule = ParseGOLString(ruleString); + if (rule == -1) + { + return std::nullopt; + } + auto &sd = SimulationData::Ref(); + for (auto &gd : sd.GetCustomGol()) + { + if (gd.nameString == nameString) + { + return std::nullopt; + } + } + return CustomGOLData{ rule, color1, color2, nameString }; +} + void GameModel::UpdateUpTo(int upTo) { if (upTo < sim->debug_nextToUpdate) diff --git a/src/gui/game/GameModel.h b/src/gui/game/GameModel.h index 2bd82ff31..6f9cfe8c7 100644 --- a/src/gui/game/GameModel.h +++ b/src/gui/game/GameModel.h @@ -3,6 +3,7 @@ #include "client/User.h" #include "gui/interface/Point.h" #include "graphics/RendererSettings.h" +#include "simulation/CustomGOLData.h" #include #include #include @@ -295,7 +296,11 @@ public: void AddNotification(Notification * notification); void RemoveNotification(Notification * notification); - bool RemoveCustomGOLType(const ByteString &identifier); + bool AddCustomGol(String ruleString, String nameString, RGB color1, RGB color2); + bool RemoveCustomGol(const ByteString &identifier); + void LoadCustomGol(); + void SaveCustomGol(); + std::optional CheckCustomGol(String ruleString, String nameString, RGB color1, RGB color2); ByteString SelectNextIdentifier; int SelectNextTool; diff --git a/src/gui/game/GameView.cpp b/src/gui/game/GameView.cpp index 06f923d21..eda9c67a2 100644 --- a/src/gui/game/GameView.cpp +++ b/src/gui/game/GameView.cpp @@ -602,7 +602,7 @@ void GameView::NotifyToolListChanged(GameModel * sender) else if (identifier.BeginsWith("DEFAULT_PT_LIFECUST_")) { new ConfirmPrompt("Remove custom GOL type", "Are you sure you want to remove " + identifier.Substr(20).FromUtf8() + "?", { [this, identifier]() { - c->RemoveCustomGOLType(identifier); + c->RemoveCustomGol(identifier); } }); } } diff --git a/src/gui/game/tool/GOLTool.cpp b/src/gui/game/tool/GOLTool.cpp index 1bfd5ff26..467a5dc81 100644 --- a/src/gui/game/tool/GOLTool.cpp +++ b/src/gui/game/tool/GOLTool.cpp @@ -159,9 +159,7 @@ void GOLWindow::validate() prefs.Set("CustomGOL.Rule", ruleString); } - auto color1 = (((highColour.Red << 8) | highColour.Green) << 8) | highColour.Blue; - auto color2 = (((lowColour.Red << 8) | lowColour.Green) << 8) | lowColour.Blue; - if (!AddCustomGol(ruleString, nameString, color1, color2)) + if (!gameModel.AddCustomGol(ruleString, nameString, highColour.NoAlpha(), lowColour.NoAlpha())) { new ErrorMessage("Could not add GOL type", "Name already taken"); return; diff --git a/src/lua/LuaSimulation.cpp b/src/lua/LuaSimulation.cpp index 6dd6e55c2..d2371d94a 100644 --- a/src/lua/LuaSimulation.cpp +++ b/src/lua/LuaSimulation.cpp @@ -1489,13 +1489,13 @@ static int listCustomGol(lua_State *L) lua_newtable(L); tpt_lua_pushString(L, cgol.nameString); lua_setfield(L, -2, "name"); - tpt_lua_pushString(L, cgol.ruleString); + tpt_lua_pushString(L, SerialiseGOLRule(cgol.rule)); lua_setfield(L, -2, "rulestr"); lua_pushnumber(L, cgol.rule); lua_setfield(L, -2, "rule"); - lua_pushnumber(L, cgol.colour1); + lua_pushnumber(L, cgol.colour1.Pack()); lua_setfield(L, -2, "color1"); - lua_pushnumber(L, cgol.colour2); + lua_pushnumber(L, cgol.colour2.Pack()); lua_setfield(L, -2, "color2"); lua_rawseti(L, -2, ++i); } @@ -1529,10 +1529,9 @@ static int addCustomGol(lua_State *L) if (sd.GetCustomGOLByRule(rule)) return luaL_error(L, "This Custom GoL rule already exists"); - if (!AddCustomGol(ruleString, nameString, color1, color2)) - return luaL_error(L, "Duplicate name, cannot add"); auto *lsi = GetLSI(); - lsi->gameModel->BuildMenus(); + if (!lsi->gameModel->AddCustomGol(ruleString, nameString, RGB::Unpack(color1), RGB::Unpack(color2))) + return luaL_error(L, "Duplicate name, cannot add"); return 0; } @@ -1540,9 +1539,7 @@ static int removeCustomGol(lua_State *L) { auto *lsi = GetLSI(); ByteString nameString = tpt_lua_checkByteString(L, 1); - bool removedAny = lsi->gameModel->RemoveCustomGOLType("DEFAULT_PT_LIFECUST_" + nameString); - if (removedAny) - lsi->gameModel->BuildMenus(); + bool removedAny = lsi->gameModel->RemoveCustomGol("DEFAULT_PT_LIFECUST_" + nameString); lua_pushboolean(L, removedAny); return 1; } diff --git a/src/simulation/CustomGOLData.h b/src/simulation/CustomGOLData.h new file mode 100644 index 000000000..50f951bcb --- /dev/null +++ b/src/simulation/CustomGOLData.h @@ -0,0 +1,15 @@ +#pragma once +#include "graphics/Pixel.h" +#include "common/String.h" + +struct CustomGOLData +{ + int rule; + RGB colour1, colour2; + String nameString; + + inline bool operator <(const CustomGOLData &other) const + { + return rule < other.rule; + } +}; diff --git a/src/simulation/SimulationData.h b/src/simulation/SimulationData.h index 0490b081e..3e9f0129a 100644 --- a/src/simulation/SimulationData.h +++ b/src/simulation/SimulationData.h @@ -9,6 +9,7 @@ #include "Particle.h" #include "WallType.h" #include "graphics/gcache_item.h" +#include "CustomGOLData.h" #include #include #include @@ -140,17 +141,6 @@ constexpr int NGT_BRAN = 23; constexpr auto REPLACE_MODE = UINT32_C(0x00000001); constexpr auto SPECIFIC_DELETE = UINT32_C(0x00000002); -struct CustomGOLData -{ - int rule, colour1, colour2; - String nameString, ruleString; - - inline bool operator <(const CustomGOLData &other) const - { - return rule < other.rule; - } -}; - class SimulationData : public ExplicitSingleton { public: diff --git a/src/simulation/elements/LIFE.cpp b/src/simulation/elements/LIFE.cpp index a0fb1058f..67563f470 100644 --- a/src/simulation/elements/LIFE.cpp +++ b/src/simulation/elements/LIFE.cpp @@ -111,8 +111,8 @@ static void create(ELEMENT_CREATE_FUNC_ARGS) auto *cgol = sd.GetCustomGOLByRule(v); if (cgol) { - sim->parts[i].dcolour = cgol->colour1; - sim->parts[i].tmp = cgol->colour2; + sim->parts[i].dcolour = cgol->colour1.Pack(); + sim->parts[i].tmp = cgol->colour2.Pack(); } } sim->parts[i].tmp2 = ((v >> 17) & 0xF) + 1;