diff --git a/src/client/GameSave.cpp b/src/client/GameSave.cpp index 3bc055752..a47bba349 100644 --- a/src/client/GameSave.cpp +++ b/src/client/GameSave.cpp @@ -1298,6 +1298,16 @@ void GameSave::readOPS(char * data, int dataLength) particles[newIndex].tmp = 0; } break; + case PT_LIFE: + if (savedVersion < 95 || minorVersion < 1) + { + if (particles[newIndex].ctype >= 0 && particles[newIndex].ctype < NGOL) + { + particles[newIndex].tmp2 = particles[newIndex].tmp; + particles[newIndex].dcolour = builtinGol[particles[newIndex].ctype].colour; + particles[newIndex].tmp = builtinGol[particles[newIndex].ctype].colour2; + } + } } //note: PSv was used in version 77.0 and every version before, add something in PSv too if the element is that old newIndex++; @@ -1359,10 +1369,6 @@ void GameSave::readPSv(char * saveDataChar, int dataLength) char tempSignText[255]; sign tempSign("", 0, 0, sign::Left); - //Gol data used to read older saves - std::vector goltype = LoadGOLTypes(); - std::vector > grule = LoadGOLRules(); - std::vector elements = GetElements(); //New file header uses PSv, replacing fuC. This is to detect if the client uses a new save format for temperatures @@ -1658,9 +1664,9 @@ void GameSave::readPSv(char * saveDataChar, int dataLength) ttv |= (data[p++]); particles[i-1].tmp = ttv; if (ver<53 && !particles[i-1].tmp) - for (q = 1; q<=NGOL; q++) { - if (particles[i-1].type==goltype[q-1] && grule[q][9]==2) - particles[i-1].tmp = grule[q][9]-1; + for (q = 0; q < NGOL; q++) { + if (particles[i-1].type==builtinGol[q].oldtype && (builtinGol[q].ruleset >> 17)==0) + particles[i-1].tmp = (builtinGol[q].ruleset >> 17)+1; } if (ver>=51 && ver<53 && particles[i-1].type==PT_PBCN) { @@ -1844,7 +1850,7 @@ void GameSave::readPSv(char * saveDataChar, int dataLength) //Replace old GOL particles[i-1].type = PT_LIFE; for (gnum = 0; gnum= 0 && particles[i-1].ctype < NGOL) + { + particles[i-1].dcolour = builtinGol[particles[i-1].ctype].colour; + particles[i-1].tmp = builtinGol[particles[i-1].ctype].colour2; + } + } if(ty==PT_LCRY){ if(ver<67) { @@ -2269,7 +2285,7 @@ char * GameSave::serialiseOPS(unsigned int & dataLength) } //Dcolour (optional), 4 bytes - if(particles[i].dcolour && (particles[i].dcolour & 0xFF000000)) + if(particles[i].dcolour && (particles[i].dcolour & 0xFF000000 || particles[i].type == PT_LIFE)) { fieldDesc |= 1 << 6; partsData[partsDataLen++] = (particles[i].dcolour&0xFF000000)>>24; @@ -2411,6 +2427,10 @@ char * GameSave::serialiseOPS(unsigned int & dataLength) RESTRICTVERSION(95, 0); } } + if (particles[i].type == PT_LIFE) + { + RESTRICTVERSION(95, 1); + } //Get the pmap entry for the next particle in the same position i = partsPosLink[i]; diff --git a/src/gui/game/GOLTool.cpp b/src/gui/game/GOLTool.cpp new file mode 100644 index 000000000..adddce985 --- /dev/null +++ b/src/gui/game/GOLTool.cpp @@ -0,0 +1,214 @@ +#include "Tool.h" + +#include "simulation/GOLString.h" + +#include "client/Client.h" +#include "common/tpt-rand.h" + +#include "gui/Style.h" +#include "gui/game/GameModel.h" +#include "gui/interface/Window.h" +#include "gui/interface/Button.h" +#include "gui/interface/Label.h" +#include "gui/interface/Textbox.h" +#include "gui/dialogues/ErrorMessage.h" +#include "gui/colourpicker/ColourPickerActivity.h" + +#include "graphics/Graphics.h" + +class GOLWindow: public ui::Window +{ + void UpdateGradient(); + +public: + ui::Colour highColour, lowColour; + ui::Button *highColourButton, *lowColourButton; + ui::Textbox *nameField, *ruleField; + GOLTool * tool; + Simulation *sim; + int toolSelection; + GOLWindow(GOLTool *tool_, Simulation *sim, int toolSelection, int rule, int colour1, int colour2); + void Validate(); + void OnDraw() override; + void OnTryExit(ExitMethod method) override; + virtual ~GOLWindow() {} +}; + +GOLWindow::GOLWindow(GOLTool * tool_, Simulation *sim_, int toolSelection, int rule, int colour1, int colour2): +ui::Window(ui::Point(-1, -1), ui::Point(200, 108)), +tool(tool_), +sim(sim_), +toolSelection(toolSelection) +{ + ui::Label * messageLabel = new ui::Label(ui::Point(4, 5), ui::Point(Size.X-8, 14), "Edit custom GOL type"); + messageLabel->SetTextColour(style::Colour::InformationTitle); + messageLabel->Appearance.HorizontalAlign = ui::Appearance::AlignLeft; + messageLabel->Appearance.VerticalAlign = ui::Appearance::AlignTop; + AddComponent(messageLabel); + + auto *okayButton = new ui::Button(ui::Point(0, Size.Y-17), ui::Point(Size.X, 17), "OK"); + okayButton->Appearance.HorizontalAlign = ui::Appearance::AlignLeft; + okayButton->Appearance.VerticalAlign = ui::Appearance::AlignMiddle; + okayButton->Appearance.BorderInactive = ui::Colour(200, 200, 200); + okayButton->SetActionCallback({ [this] { + CloseActiveWindow(); + if (nameField->GetText().length() && ruleField->GetText().length()) + { + Validate(); + } + SelfDestruct(); + } }); + AddComponent(okayButton); + SetOkayButton(okayButton); + + nameField = new ui::Textbox(ui::Point(8, 25), ui::Point(Size.X-16, 16), "", "[name]"); + nameField->Appearance.HorizontalAlign = ui::Appearance::AlignLeft; + nameField->Appearance.VerticalAlign = ui::Appearance::AlignMiddle; + AddComponent(nameField); + FocusComponent(nameField); + + ruleField = new ui::Textbox(ui::Point(8, 46), ui::Point(Size.X-16, 16), "", "[rule]"); + ruleField->Appearance.HorizontalAlign = ui::Appearance::AlignLeft; + ruleField->Appearance.VerticalAlign = ui::Appearance::AlignMiddle; + AddComponent(ruleField); + FocusComponent(ruleField); + + highColourButton = new ui::Button(ui::Point(8, 67), ui::Point(16, 16), ""); + highColourButton->SetActionCallback({ [this] { + new ColourPickerActivity(highColour, [this](ui::Colour colour) { + highColour = colour; + UpdateGradient(); + }); + } }); + AddComponent(highColourButton); + + lowColourButton = new ui::Button(ui::Point(Size.X - 24, 67), ui::Point(16, 16), ""); + lowColourButton->SetActionCallback({ [this] { + new ColourPickerActivity(lowColour, [this](ui::Colour colour) { + lowColour = colour; + UpdateGradient(); + }); + } }); + AddComponent(lowColourButton); + + if (rule) + { + ruleField->SetText(SerialiseGOLRule(rule)); + nameField->SetText(""); + highColour.Red = PIXR(colour1); + highColour.Green = PIXG(colour1); + highColour.Blue = PIXB(colour1); + lowColour.Red = PIXR(colour2); + lowColour.Green = PIXG(colour2); + lowColour.Blue = PIXB(colour2); + } + else + { + ruleField->SetText(Client::Ref().GetPrefString("CustomGOL.Rule", "B3/S23")); + nameField->SetText(Client::Ref().GetPrefString("CustomGOL.Name", "CGOL")); + highColour.Red = RNG::Ref().between(0x80, 0xFF); + highColour.Green = RNG::Ref().between(0x80, 0xFF); + highColour.Blue = RNG::Ref().between(0x80, 0xFF); + lowColour.Red = RNG::Ref().between(0x00, 0x7F); + lowColour.Green = RNG::Ref().between(0x00, 0x7F); + lowColour.Blue = RNG::Ref().between(0x00, 0x7F); + } + highColour.Alpha = 255; + lowColour.Alpha = 255; + UpdateGradient(); + + MakeActiveWindow(); +} + +void GOLWindow::UpdateGradient() +{ + highColourButton->Appearance.BackgroundInactive = highColour; + highColourButton->Appearance.BackgroundHover = highColour; + lowColourButton->Appearance.BackgroundInactive = lowColour; + lowColourButton->Appearance.BackgroundHover = lowColour; +} + +void GOLWindow::Validate() +{ + tool->selectGOLType.clear(); + auto nameString = nameField->GetText(); + auto ruleString = ruleField->GetText(); + if (!ValidateGOLName(nameString)) + { + new ErrorMessage("Could not add GOL type", "Invalid name provided"); + return; + } + nameString = nameString.ToUpper(); + int rule = ParseGOLString(ruleString); + if (rule == -1) + { + new ErrorMessage("Could not add GOL type", "Invalid rule provided"); + return; + } + ruleString = SerialiseGOLRule(rule); // * Make it canonical. + + Client::Ref().SetPrefUnicode("CustomGOL.Name", nameString); + Client::Ref().SetPrefUnicode("CustomGOL.Rule", ruleString); + + auto customGOLTypes = Client::Ref().GetPrefByteStringArray("CustomGOL.Types"); + Json::Value newCustomGOLTypes(Json::arrayValue); + bool nameTaken = false; + for (auto gol : customGOLTypes) + { + auto parts = gol.FromUtf8().PartitionBy(' '); + if (parts.size()) + { + if (parts[0] == nameString) + { + nameTaken = true; + } + } + newCustomGOLTypes.append(gol); + } + if (nameTaken) + { + new ErrorMessage("Could not add GOL type", "Name already taken"); + return; + } + + StringBuilder sb; + auto colour1 = (((highColour.Red << 8) | highColour.Green) << 8) | highColour.Blue; + auto colour2 = (((lowColour.Red << 8) | lowColour.Green) << 8) | lowColour.Blue; + sb << nameString << " " << ruleString << " " << colour1 << " " << colour2; + newCustomGOLTypes.append(sb.Build().ToUtf8()); + Client::Ref().SetPref("CustomGOL.Types", newCustomGOLTypes); + tool->gameModel->SelectNextIdentifier = "DEFAULT_PT_LIFECUST_" + nameString.ToAscii(); + tool->gameModel->SelectNextTool = toolSelection; +} + +void GOLWindow::OnTryExit(ExitMethod method) +{ + CloseActiveWindow(); + SelfDestruct(); +} + +void GOLWindow::OnDraw() +{ + Graphics * g = GetGraphics(); + + g->clearrect(Position.X-2, Position.Y-2, Size.X+3, Size.Y+3); + g->drawrect(Position.X, Position.Y, Size.X, Size.Y, 200, 200, 200, 255); + + int width = Size.X - 60; + for (int xx = 0; xx < width; ++xx) + { + auto f = xx / (float)width; + for (int yy = 0; yy < 16; ++yy) + { + int rr = highColour.Red * (1.f - f) + lowColour.Red * f; + int gg = highColour.Green * (1.f - f) + lowColour.Green * f; + int bb = highColour.Blue * (1.f - f) + lowColour.Blue * f; + g->blendpixel(Position.X + xx + 30, Position.Y + yy + 67, rr, gg, bb, 255); + } + } +} + +void GOLTool::OpenWindow(Simulation *sim, int toolSelection, int rule, int colour1, int colour2) +{ + new GOLWindow(this, sim, toolSelection, rule, colour1, colour2); +} diff --git a/src/gui/game/GameController.cpp b/src/gui/game/GameController.cpp index 5b7aa7f79..63f7944ad 100644 --- a/src/gui/game/GameController.cpp +++ b/src/gui/game/GameController.cpp @@ -779,6 +779,12 @@ void GameController::Tick() #endif firstTick = false; } + if (gameModel->SelectNextIdentifier.length()) + { + gameModel->BuildMenus(); + gameModel->SetActiveTool(gameModel->SelectNextTool, gameModel->GetToolFromIdentifier(gameModel->SelectNextIdentifier)); + gameModel->SelectNextIdentifier.clear(); + } for(std::vector::iterator iter = debugInfo.begin(), end = debugInfo.end(); iter != end; iter++) { if ((*iter)->debugID & debugFlags) @@ -1114,6 +1120,10 @@ void GameController::SetActiveTool(int toolSelection, Tool * tool) } if(tool->GetIdentifier() == "DEFAULT_UI_PROPERTY") ((PropertyTool *)tool)->OpenWindow(gameModel->GetSimulation()); + if(tool->GetIdentifier() == "DEFAULT_UI_ADDLIFE") + { + ((GOLTool *)tool)->OpenWindow(gameModel->GetSimulation(), toolSelection); + } } void GameController::SetActiveTool(int toolSelection, ByteString identifier) @@ -1672,3 +1682,8 @@ bool GameController::GetMouseClickRequired() { return gameModel->GetMouseClickRequired(); } + +void GameController::RemoveCustomGOLType(const ByteString &identifier) +{ + gameModel->RemoveCustomGOLType(identifier); +} diff --git a/src/gui/game/GameController.h b/src/gui/game/GameController.h index 67496c2c2..385f10e33 100644 --- a/src/gui/game/GameController.h +++ b/src/gui/game/GameController.h @@ -174,6 +174,8 @@ public: void NotifyNewNotification(Client * sender, std::pair notification) override; void RunUpdater(); bool GetMouseClickRequired(); + + void RemoveCustomGOLType(const ByteString &identifier); }; #endif // GAMECONTROLLER_H diff --git a/src/gui/game/GameModel.cpp b/src/gui/game/GameModel.cpp index e3686af24..8da338396 100644 --- a/src/gui/game/GameModel.cpp +++ b/src/gui/game/GameModel.cpp @@ -27,11 +27,13 @@ #include "simulation/Gravity.h" #include "simulation/ElementGraphics.h" #include "simulation/ElementClasses.h" +#include "simulation/GOLString.h" #include "gui/game/DecorationTool.h" #include "gui/interface/Engine.h" #include +#include GameModel::GameModel(): clipboard(NULL), @@ -303,9 +305,55 @@ void GameModel::BuildMenus() //Build menu for GOL types for(int i = 0; i < NGOL; i++) { - Tool * tempTool = new ElementTool(PT_LIFE|PMAPID(i), sim->gmenu[i].name, sim->gmenu[i].description, PIXR(sim->gmenu[i].colour), PIXG(sim->gmenu[i].colour), PIXB(sim->gmenu[i].colour), "DEFAULT_PT_LIFE_"+sim->gmenu[i].name.ToAscii()); + Tool * tempTool = new ElementTool(PT_LIFE|PMAPID(i), builtinGol[i].name, builtinGol[i].description, PIXR(builtinGol[i].colour), PIXG(builtinGol[i].colour), PIXB(builtinGol[i].colour), "DEFAULT_PT_LIFE_"+builtinGol[i].name.ToAscii()); menuList[SC_LIFE]->AddTool(tempTool); } + { + auto customGOLTypes = Client::Ref().GetPrefByteStringArray("CustomGOL.Types"); + Json::Value validatedCustomLifeTypes(Json::arrayValue); + std::vector newCustomGol; + for (auto gol : customGOLTypes) + { + auto parts = gol.FromUtf8().PartitionBy(' '); + if (parts.size() != 4) + { + continue; + } + Simulation::CustomGOLData gd; + gd.nameString = parts[0]; + gd.ruleString = parts[1]; + auto &colour1String = parts[2]; + auto &colour2String = parts[3]; + if (!ValidateGOLName(gd.nameString)) + { + continue; + } + gd.rule = ParseGOLString(gd.ruleString); + if (gd.rule == -1) + { + continue; + } + try + { + gd.colour1 = colour1String.ToNumber(); + gd.colour2 = colour2String.ToNumber(); + } + catch (std::exception &) + { + continue; + } + newCustomGol.push_back(gd); + validatedCustomLifeTypes.append(gol); + } + // All custom rules that fail validation will be removed + Client::Ref().SetPref("CustomGOL.Types", validatedCustomLifeTypes); + for (auto &gd : newCustomGol) + { + Tool * tempTool = new ElementTool(PT_LIFE|PMAPID(gd.rule), gd.nameString, "Custom GOL type: " + gd.ruleString, PIXR(gd.colour1), PIXG(gd.colour1), PIXB(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++) @@ -331,9 +379,10 @@ void GameModel::BuildMenus() } //Add special sign and prop tools menuList[SC_TOOL]->AddTool(new WindTool(0, "WIND", "Creates air movement.", 64, 64, 64, "DEFAULT_UI_WIND")); - menuList[SC_TOOL]->AddTool(new PropertyTool()); + 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.", 0, 0, 0, "DEFAULT_DECOR_ADD")); @@ -1374,3 +1423,27 @@ void GameModel::SetPerfectCircle(bool perfectCircle) BuildBrushList(); } } + +void GameModel::RemoveCustomGOLType(const ByteString &identifier) +{ + auto customGOLTypes = Client::Ref().GetPrefByteStringArray("CustomGOL.Types"); + Json::Value newCustomGOLTypes(Json::arrayValue); + for (auto gol : customGOLTypes) + { + auto parts = gol.PartitionBy(' '); + bool remove = false; + if (parts.size()) + { + if ("DEFAULT_PT_LIFECUST_" + parts[0] == identifier) + { + remove = true; + } + } + if (!remove) + { + newCustomGOLTypes.append(gol); + } + } + Client::Ref().SetPref("CustomGOL.Types", newCustomGOLTypes); + BuildMenus(); +} diff --git a/src/gui/game/GameModel.h b/src/gui/game/GameModel.h index 66c09c798..369aae1d5 100644 --- a/src/gui/game/GameModel.h +++ b/src/gui/game/GameModel.h @@ -219,6 +219,11 @@ public: std::vector GetNotifications(); void AddNotification(Notification * notification); void RemoveNotification(Notification * notification); + + void RemoveCustomGOLType(const ByteString &identifier); + + ByteString SelectNextIdentifier; + int SelectNextTool; }; #endif // GAMEMODEL_H diff --git a/src/gui/game/GameView.cpp b/src/gui/game/GameView.cpp index dc018332a..c10057e87 100644 --- a/src/gui/game/GameView.cpp +++ b/src/gui/game/GameView.cpp @@ -585,8 +585,19 @@ void GameView::NotifyToolListChanged(GameModel * sender) } else if (tempButton->GetSelectionState() == 1) { - Favorite::Ref().RemoveFavorite(tool->GetIdentifier()); - c->RebuildFavoritesMenu(); + auto identifier = tool->GetIdentifier(); + if (Favorite::Ref().IsFavorite(identifier)) + { + Favorite::Ref().RemoveFavorite(identifier); + c->RebuildFavoritesMenu(); + } + else if (identifier.BeginsWith("DEFAULT_PT_LIFECUST_")) + { + if (ConfirmPrompt::Blocking("Remove custom GOL type", "Are you sure you want to remove " + identifier.Substr(20).FromUtf8() + "?")) + { + c->RemoveCustomGOLType(identifier); + } + } } } else @@ -2154,6 +2165,8 @@ void GameView::OnDraw() // Some elements store extra LIFE info in upper bits of ctype, instead of tmp/tmp2 else if (type == PT_CRAY || type == PT_DRAY || type == PT_CONV) sampleInfo << " (" << c->ElementResolve(TYP(ctype), ID(ctype)) << ")"; + else if (type == PT_CLNE || type == PT_BCLN || type == PT_PCLN || type == PT_PBCN) + sampleInfo << " (" << c->ElementResolve(ctype, sample.particle.tmp) << ")"; else if (c->IsValidElement(ctype)) sampleInfo << " (" << c->ElementResolve(ctype, -1) << ")"; else diff --git a/src/gui/game/PropertyTool.cpp b/src/gui/game/PropertyTool.cpp index bf533cd92..1abe9c149 100644 --- a/src/gui/game/PropertyTool.cpp +++ b/src/gui/game/PropertyTool.cpp @@ -1,7 +1,9 @@ #include "Tool.h" #include "client/Client.h" +#include "Menu.h" +#include "gui/game/GameModel.h" #include "gui/Style.h" #include "gui/game/Brush.h" #include "gui/interface/Window.h" @@ -12,7 +14,10 @@ #include "gui/interface/Keys.h" #include "gui/dialogues/ErrorMessage.h" +#include "simulation/GOLString.h" +#include "simulation/BuiltinGOL.h" #include "simulation/Simulation.h" +#include "simulation/SimulationData.h" #include "graphics/Graphics.h" @@ -83,7 +88,7 @@ void PropertyWindow::SetProperty() { if(property->GetOption().second!=-1 && textField->GetText().length() > 0) { - String value = textField->GetText(); + String value = textField->GetText().ToUpper(); try { switch(properties[property->GetOption().second].Type) { @@ -91,7 +96,7 @@ void PropertyWindow::SetProperty() case StructProperty::ParticleType: { int v; - if(value.length() > 2 && value.BeginsWith("0x")) + if(value.length() > 2 && value.BeginsWith("0X")) { //0xC0FFEE v = value.Substr(2).ToNumber(Format::Hex()); @@ -101,18 +106,32 @@ void PropertyWindow::SetProperty() //#C0FFEE v = value.Substr(1).ToNumber(Format::Hex()); } + else if (value.length() > 1 && value.BeginsWith("B") && value.Contains("/")) + { + v = ParseGOLString(value); + if (v == -1) + { + class InvalidGOLString : public std::exception + { + }; + throw InvalidGOLString(); + } + } else { - int type; - if ((type = sim->GetParticleType(value.ToUtf8())) != -1) + v = sim->GetParticleType(value.ToUtf8()); + if (v == -1) { - v = type; - -#ifdef DEBUG - std::cout << "Got type from particle name" << std::endl; -#endif + for (auto *elementTool : tool->gameModel->GetMenuList()[SC_LIFE]->GetToolList()) + { + if (elementTool && elementTool->GetName() == value) + { + v = ID(elementTool->GetToolID()); + break; + } + } } - else + if (v == -1) { v = value.ToNumber(); } @@ -134,7 +153,7 @@ void PropertyWindow::SetProperty() case StructProperty::UInteger: { unsigned int v; - if(value.length() > 2 && value.BeginsWith("0x")) + if(value.length() > 2 && value.BeginsWith("0X")) { //0xC0FFEE v = value.Substr(2).ToNumber(Format::Hex()); diff --git a/src/gui/game/SampleTool.cpp b/src/gui/game/SampleTool.cpp index 3a0cd9079..bacf49d57 100644 --- a/src/gui/game/SampleTool.cpp +++ b/src/gui/game/SampleTool.cpp @@ -38,36 +38,37 @@ void SampleTool::Draw(Simulation * sim, Brush * brush, ui::Point position) } else { - int particleType = 0; - int particleCtype = 0; + Particle *part = nullptr; if (sim->photons[position.Y][position.X]) { - particleType = sim->parts[ID(sim->photons[position.Y][position.X])].type; - particleCtype = sim->parts[ID(sim->pmap[position.Y][position.X])].ctype; + part = &sim->parts[ID(sim->photons[position.Y][position.X])]; } else if (sim->pmap[position.Y][position.X]) { - particleType = sim->parts[ID(sim->pmap[position.Y][position.X])].type; - particleCtype = sim->parts[ID(sim->pmap[position.Y][position.X])].ctype; + part = &sim->parts[ID(sim->pmap[position.Y][position.X])]; } - - if(particleType) + if (part) { - if(particleType == PT_LIFE) + if (part->type == PT_LIFE) { - Menu * lifeMenu = gameModel->GetMenuList()[SC_LIFE]; - std::vector elementTools = lifeMenu->GetToolList(); - - for(std::vector::iterator iter = elementTools.begin(), end = elementTools.end(); iter != end; ++iter) + bool found = false; + for (auto *elementTool : gameModel->GetMenuList()[SC_LIFE]->GetToolList()) { - Tool * elementTool = *iter; - if(elementTool && ID(elementTool->GetToolID()) == particleCtype) + if (elementTool && ID(elementTool->GetToolID()) == part->ctype) + { gameModel->SetActiveTool(0, elementTool); + found = true; + break; + } + } + if (!found) + { + ((GOLTool *)(gameModel->GetToolFromIdentifier("DEFAULT_UI_ADDLIFE")))->OpenWindow(gameModel->GetSimulation(), 0, part->ctype, part->dcolour, part->tmp); } } else { - Tool * elementTool = gameModel->GetElementTool(particleType); + Tool * elementTool = gameModel->GetElementTool(part->type); if(elementTool) gameModel->SetActiveTool(0, elementTool); } diff --git a/src/gui/game/Tool.h b/src/gui/game/Tool.h index a48ef0979..c2381394a 100644 --- a/src/gui/game/Tool.h +++ b/src/gui/game/Tool.h @@ -81,8 +81,10 @@ public: class PropertyTool: public Tool { public: - PropertyTool(): - Tool(0, "PROP", "Property Drawing Tool. Use to alter the properties of elements in the field.", 0xfe, 0xa9, 0x00, "DEFAULT_UI_PROPERTY", NULL) + GameModel * gameModel; + PropertyTool(GameModel *model): + Tool(0, "PROP", "Property Drawing Tool. Use to alter the properties of elements in the field.", 0xfe, 0xa9, 0x00, "DEFAULT_UI_PROPERTY", NULL), + gameModel(model) { } StructProperty::PropertyType propType; @@ -100,6 +102,25 @@ public: void DrawFill(Simulation * sim, Brush * brush, ui::Point position) override; }; +class GOLTool: public Tool +{ +public: + String selectGOLType; + GameModel * gameModel; + GOLTool(GameModel * gameModel): + Tool(0, "CUST", "Add a new custom GOL type. (Use ctrl+shift+rightclick to remove them)", 0xfe, 0xa9, 0x00, "DEFAULT_UI_ADDLIFE", NULL), + gameModel(gameModel) + { + } + void OpenWindow(Simulation *sim, int toolSelection, int rule = 0, int colour1 = 0, int colour2 = 0); + virtual ~GOLTool() {} + void Click(Simulation * sim, Brush * brush, ui::Point position) override { } + void Draw(Simulation *sim, Brush *brush, ui::Point position) override { }; + void DrawLine(Simulation * sim, Brush * brush, ui::Point position1, ui::Point position2, bool dragging = false) override { }; + void DrawRect(Simulation * sim, Brush * brush, ui::Point position1, ui::Point position2) override { }; + void DrawFill(Simulation * sim, Brush * brush, ui::Point position) override { }; +}; + class ElementTool: public Tool { diff --git a/src/simulation/BuiltinGOL.h b/src/simulation/BuiltinGOL.h new file mode 100644 index 000000000..cb3158132 --- /dev/null +++ b/src/simulation/BuiltinGOL.h @@ -0,0 +1,14 @@ +#pragma once + +#include "graphics/Pixel.h" +#include "common/String.h" + +struct BuiltinGOL +{ + String name; + int oldtype; + int ruleset; + pixel colour, colour2; + int goltype; + String description; +}; diff --git a/src/simulation/Element.cpp b/src/simulation/Element.cpp index 177ab21b9..df8bae71f 100644 --- a/src/simulation/Element.cpp +++ b/src/simulation/Element.cpp @@ -250,7 +250,7 @@ bool Element::ctypeDrawVInTmp(CTYPEDRAW_FUNC_ARGS) { return false; } - if (t == PT_LIFE && v >= 0 && v < NGOL) + if (t == PT_LIFE) { sim->parts[i].tmp = v; } @@ -263,7 +263,7 @@ bool Element::ctypeDrawVInCtype(CTYPEDRAW_FUNC_ARGS) { return false; } - if (t == PT_LIFE && v >= 0 && v < NGOL) + if (t == PT_LIFE) { sim->parts[i].ctype |= PMAPID(v); } diff --git a/src/simulation/GOLMenu.h b/src/simulation/GOLMenu.h deleted file mode 100644 index 7fbe71c64..000000000 --- a/src/simulation/GOLMenu.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef The_Powder_Toy_GOLMenu_h -#define The_Powder_Toy_GOLMenu_h - -struct gol_menu -{ - String name; - pixel colour; - int goltype; - String description; -}; - -#endif diff --git a/src/simulation/GOLString.cpp b/src/simulation/GOLString.cpp new file mode 100644 index 000000000..405255de3 --- /dev/null +++ b/src/simulation/GOLString.cpp @@ -0,0 +1,91 @@ +#include "GOLString.h" + +int ParseGOLString(const String &value) +{ + // * Most likely a GOL string. + auto it = value.begin() + 1; + auto begin = 0U; + auto stay = 0U; + auto states = 2U; + + // Scan 'B' section, must be between 1 and 8 + for (; it != value.end() && it[0] >= '1' && it[0] <= '8'; ++it) + { + begin |= 1U << (it[0] - '0'); + } + + // Must have a /S immediately afterwards + if (it < value.end() - 1 && it[0] == '/' && it[1] == 'S') + { + it += 2; + } + else + { + return -1; + } + + // Scan 'S' section, must be between 0 and 8 + for (; it != value.end() && it[0] >= '0' && it[0] <= '8'; ++it) + { + stay |= 1U << (it[0] - '0'); + } + + // Optionally can have a 3rd section, with the number of frames to remain after dying + if (it != value.end()) + { + if (it[0] == '/') + { + it += 1; + } + else + { + return -1; + } + states = String(it, value.end()).ToNumber(true); + if (states < 2 || states > 17) + { + return -1; + } + } + + return stay | (begin << 8) | ((states - 2) << 17); +} + +bool ValidateGOLName(const String &value) +{ + bool nameOk = true; + for (auto ch : value) + { + if (!((ch >= '0' && ch < '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch == '-'))) + { + nameOk = false; + } + } + return nameOk; +} + +String SerialiseGOLRule(int rule) +{ + StringBuilder golName; + golName << "B"; + for (int i = 1; i < 9; ++i) + { + if ((rule >> (i + 8)) & 1) + { + golName << char('0' + i); + } + } + golName << "/S"; + for (int i = 0; i < 9; ++i) + { + if ((rule >> i) & 1) + { + golName << char('0' + i); + } + } + if ((rule >> 17) & 0xF) + { + golName << "/" << ((rule >> 17) & 0xF) + 2; + } + return golName.Build(); +} diff --git a/src/simulation/GOLString.h b/src/simulation/GOLString.h new file mode 100644 index 000000000..78799594b --- /dev/null +++ b/src/simulation/GOLString.h @@ -0,0 +1,8 @@ +#pragma once + +#include "common/String.h" +#include + +bool ValidateGOLName(const String &value); +int ParseGOLString(const String &value); +String SerialiseGOLRule(int rule); diff --git a/src/simulation/Simulation.cpp b/src/simulation/Simulation.cpp index 16734f1e3..60ccfd843 100644 --- a/src/simulation/Simulation.cpp +++ b/src/simulation/Simulation.cpp @@ -21,6 +21,7 @@ #include "ToolClasses.h" #include "Config.h" #include "SimulationData.h" +#include "GOLString.h" #include "graphics/Renderer.h" @@ -2250,7 +2251,7 @@ void Simulation::clear_sim(void) memset(fvy, 0, sizeof(fvy)); memset(photons, 0, sizeof(photons)); memset(wireless, 0, sizeof(wireless)); - memset(gol2, 0, sizeof(gol2)); + memset(gol, 0, sizeof(gol)); memset(portalp, 0, sizeof(portalp)); memset(fighters, 0, sizeof(fighters)); std::fill(elementCount, elementCount+PT_NUM, 0); @@ -4659,120 +4660,6 @@ int Simulation::GetParticleType(ByteString type) return -1; } -void Simulation::SimulateGoL() -{ - CGOL = 0; - //TODO: maybe this should only loop through active particles - for (int ny = CELL; ny < YRES-CELL; ny++) - { - //go through every particle and set neighbor map - for (int nx = CELL; nx < XRES-CELL; nx++) - { - int r = pmap[ny][nx]; - if (!r) - { - gol[ny][nx] = 0; - continue; - } - if (TYP(r) == PT_LIFE) - { - int golnum = parts[ID(r)].ctype + 1; - if (golnum <= 0 || golnum > NGOL) - { - kill_part(ID(r)); - continue; - } - gol[ny][nx] = golnum; - if (parts[ID(r)].tmp == grule[golnum][9]-1) - { - for (int nnx = -1; nnx < 2; nnx++) - { - //it will count itself as its own neighbor, which is needed, but will have 1 extra for delete check - for (int nny = -1; nny < 2; nny++) - { - int adx = ((nx+nnx+XRES-3*CELL)%(XRES-2*CELL))+CELL; - int ady = ((ny+nny+YRES-3*CELL)%(YRES-2*CELL))+CELL; - int rt = pmap[ady][adx]; - if (!rt || TYP(rt) == PT_LIFE) - { - //the total neighbor count is in 0 - gol2[ady][adx][0] ++; - //insert golnum into neighbor table - for (int i = 1; i < 9; i++) - { - if (!gol2[ady][adx][i]) - { - gol2[ady][adx][i] = (golnum<<4)+1; - break; - } - else if((gol2[ady][adx][i]>>4)==golnum) - { - gol2[ady][adx][i]++; - break; - } - } - } - } - } - } - else - { - if (!(bmap[ny/CELL][nx/CELL] == WL_STASIS && emap[ny/CELL][nx/CELL] < 8)) - { - parts[ID(r)].tmp --; - } - } - } - } - } - for (int ny = CELL; ny < YRES-CELL; ny++) - { - //go through every particle again, but check neighbor map, then update particles - for (int nx = CELL; nx < XRES-CELL; nx++) - { - int r = pmap[ny][nx]; - if (r && TYP(r)!=PT_LIFE) - continue; - int neighbors = gol2[ny][nx][0]; - if (neighbors) - { - if (!(bmap[ny/CELL][nx/CELL] == WL_STASIS && emap[ny/CELL][nx/CELL] < 8)) - { - int golnum = gol[ny][nx]; - if (!r) - { - //Find which type we can try and create - int creategol = 0xFF; - for (int i = 1; i < 9; i++) - { - if (!gol2[ny][nx][i]) break; - golnum = (gol2[ny][nx][i]>>4); - if (grule[golnum][neighbors]>= 2 && (gol2[ny][nx][i]&0xF) >= (neighbors%2)+neighbors/2) - { - if (golnum < creategol) - creategol = golnum; - } - } - if (creategol < 0xFF) - create_part(-1, nx, ny, PT_LIFE, creategol-1); - } - else if (grule[golnum][neighbors-1] == 0 || grule[golnum][neighbors-1] == 2)//subtract 1 because it counted itself - { - if (parts[ID(r)].tmp == grule[golnum][9]-1) - parts[ID(r)].tmp--; - } - } - for (int z = 0; z < 9; z++) - gol2[ny][nx][z] = 0;//this improves performance A LOT compared to the memset, i was getting ~23 more fps with this. - } - //we still need to kill things with 0 neighbors (higher state life) - if (r && parts[ID(r)].tmp <= 0) - kill_part(ID(r)); - } - } - //memset(gol2, 0, sizeof(gol2)); -} - void Simulation::RecalcFreeParticles(bool do_life_dec) { int x, y, t; @@ -4870,6 +4757,175 @@ void Simulation::RecalcFreeParticles(bool do_life_dec) elementRecount = false; } +void Simulation::SimulateGoL() +{ + CGOL = 0; + for (int i = 0; i <= parts_lastActiveIndex; ++i) + { + auto &part = parts[i]; + if (part.type != PT_LIFE) + { + continue; + } + auto x = int(part.x + 0.5f); + auto y = int(part.y + 0.5f); + if (x < CELL || y < CELL || x >= XRES - CELL || y >= YRES - CELL) + { + continue; + } + unsigned int golnum = part.ctype; + unsigned int ruleset = golnum; + if (golnum < NGOL) + { + ruleset = builtinGol[golnum].ruleset; + golnum += 1; + } + if (part.tmp2 == int((ruleset >> 17) & 0xF) + 1) + { + for (int yy = -1; yy <= 1; ++yy) + { + for (int xx = -1; xx <= 1; ++xx) + { + if (xx || yy) + { + // * Calculate address of the neighbourList, taking wraparound + // into account. The fact that the GOL space is 2 CELL's worth + // narrower in both dimensions than the simulation area makes + // this a bit awkward. + int ax = ((x + xx + XRES - 3 * CELL) % (XRES - 2 * CELL)) + CELL; + int ay = ((y + yy + YRES - 3 * CELL) % (YRES - 2 * CELL)) + CELL; + unsigned int (&neighbourList)[5] = gol[ay][ax]; + // * Bump overall neighbour counter (bits 30..28) for the entire list. + neighbourList[0] += 1U << 28; + for (int l = 0; l < 5; ++l) + { + auto neighbourRuleset = neighbourList[l] & 0x001FFFFFU; + if (neighbourRuleset == golnum) + { + // * Bump population counter (bits 23..21) of the + // same kind of cell. + neighbourList[l] += 1U << 21; + break; + } + if (neighbourRuleset == 0) + { + // * Add the new kind of cell to the population. Both counters + // have a bias of -1, so they're intentionally initialised + // to 0 instead of 1 here. This is all so they can both + // fit in 3 bits. + neighbourList[l] = ((yy & 3) << 26) | ((xx & 3) << 24) | golnum; + break; + } + // * If after 5 iterations the cell still hasn't contributed + // to a list entry, it's surely a 6th kind of cell, meaning + // there could be at most 3 of it in the neighbourhood, + // as there are already 5 other kinds of cells present in + // the list. This in turn means that it couldn't possibly + // win the population ratio-based contest later on. + } + } + } + } + } + else + { + if (!(bmap[y / CELL][x / CELL] == WL_STASIS && emap[y / CELL][x / CELL] < 8)) + { + part.tmp2 -= 1; + } + } + } + for (int y = CELL; y < YRES - CELL; ++y) + { + for (int x = CELL; x < XRES - CELL; ++x) + { + int r = pmap[y][x]; + if (r && TYP(r) != PT_LIFE) + { + continue; + } + unsigned int (&neighbourList)[5] = gol[y][x]; + auto nl0 = neighbourList[0]; + if (r || nl0) + { + // * Get overall neighbour count (bits 30..28). + unsigned int neighbours = nl0 ? ((nl0 >> 28) & 7) + 1 : 0; + if (!(bmap[y / CELL][x / CELL] == WL_STASIS && emap[y / CELL][x / CELL] < 8)) + { + if (r) + { + auto &part = parts[ID(r)]; + unsigned int ruleset = part.ctype; + if (ruleset < NGOL) + { + ruleset = builtinGol[ruleset].ruleset; + } + if (!((ruleset >> neighbours) & 1) && part.tmp2 == int(ruleset >> 17) + 1) + { + // * Start death sequence. + part.tmp2 -= 1; + } + } + else + { + unsigned int golnumToCreate = 0xFFFFFFFFU; + unsigned int createFromEntry = 0U; + unsigned int majority = neighbours / 2 + neighbours % 2; + for (int l = 0; l < 5; ++l) + { + auto golnum = neighbourList[l] & 0x001FFFFFU; + if (!golnum) + { + break; + } + auto ruleset = golnum; + if (golnum - 1 < NGOL) + { + ruleset = builtinGol[golnum - 1].ruleset; + golnum -= 1; + } + if ((ruleset >> (neighbours + 8)) & 1 && ((neighbourList[l] >> 21) & 7) + 1 >= majority && golnum < golnumToCreate) + { + golnumToCreate = golnum; + createFromEntry = neighbourList[l]; + } + } + if (golnumToCreate != 0xFFFFFFFFU) + { + // * 0x200000: No need to look for colours, they'll be set later anyway. + int i = create_part(-1, x, y, PT_LIFE, golnumToCreate | 0x200000); + int xx = (createFromEntry >> 24) & 3; + int yy = (createFromEntry >> 26) & 3; + if (xx == 3) xx = -1; + if (yy == 3) yy = -1; + int ax = ((x - xx + XRES - 3 * CELL) % (XRES - 2 * CELL)) + CELL; + int ay = ((y - yy + YRES - 3 * CELL) % (YRES - 2 * CELL)) + CELL; + auto &sample = parts[ID(pmap[ay][ax])]; + parts[i].dcolour = sample.dcolour; + parts[i].tmp = sample.tmp; + } + } + } + for (int l = 0; l < 5 && neighbourList[l]; ++l) + { + neighbourList[l] = 0; + } + } + } + } + for (int y = CELL; y < YRES - CELL; ++y) + { + for (int x = CELL; x < XRES - CELL; ++x) + { + int r = pmap[y][x]; + if (r && TYP(r) == PT_LIFE && parts[ID(r)].tmp2 <= 0) + { + kill_part(ID(r)); + } + } + } +} + void Simulation::CheckStacking() { bool excessive_stacking_found = false; @@ -5205,8 +5261,6 @@ Simulation::Simulation(): platent = LoadLatent(); std::copy(GetElements().begin(), GetElements().end(), elements.begin()); tools = GetTools(); - grule = LoadGOLRules(); - gmenu = LoadGOLMenu(); player.comm = 0; player2.comm = 0; @@ -5217,11 +5271,39 @@ Simulation::Simulation(): grav->gravity_mask(); } +const Simulation::CustomGOLData *Simulation::GetCustomGOLByRule(int rule) const +{ + // * Binary search. customGol is already sorted, see SetCustomGOL. + auto it = std::lower_bound(customGol.begin(), customGol.end(), rule, [](const CustomGOLData &item, int rule) { + return item.rule < rule; + }); + if (it != customGol.end() && !(rule < it->rule)) + { + return &*it; + } + return nullptr; +} + +void Simulation::SetCustomGOL(std::vector newCustomGol) +{ + std::sort(newCustomGol.begin(), newCustomGol.end()); + customGol = newCustomGol; +} + String Simulation::ElementResolve(int type, int ctype) { - if (type == PT_LIFE && ctype >= 0 && ctype < NGOL) + if (type == PT_LIFE) { - return gmenu[ctype].name; + if (ctype >= 0 && ctype < NGOL) + { + return builtinGol[ctype].name; + } + auto *cgol = GetCustomGOLByRule(ctype); + if (cgol) + { + return cgol->nameString; + } + return SerialiseGOLRule(ctype); } else if (type >= 0 && type < PT_NUM) { diff --git a/src/simulation/Simulation.h b/src/simulation/Simulation.h index c81f715d6..647bfb7d2 100644 --- a/src/simulation/Simulation.h +++ b/src/simulation/Simulation.h @@ -11,7 +11,7 @@ #include "WallType.h" #include "Sign.h" #include "ElementDefs.h" -#include "GOLMenu.h" +#include "BuiltinGOL.h" #include "MenuSection.h" #include "CoordStack.h" @@ -46,9 +46,6 @@ public: std::vector tools; std::vector platent; std::vector wtypes; - std::vector gmenu; - std::vector goltype; - std::vector > grule; std::vector msections; int currentTick; @@ -83,8 +80,7 @@ public: //Gol sim int CGOL; int GSPEED; - unsigned char gol[YRES][XRES]; - unsigned short gol2[YRES][XRES][9]; + unsigned int gol[YRES][XRES][5]; //Air sim float (*vx)[XRES/CELL]; float (*vy)[XRES/CELL]; @@ -224,6 +220,25 @@ public: String ElementResolve(int type, int ctype); String BasicParticleInfo(Particle const &sample_part); + + struct CustomGOLData + { + int rule, colour1, colour2; + String nameString, ruleString; + + inline bool operator <(const CustomGOLData &other) const + { + return rule < other.rule; + } + }; + +private: + std::vector customGol; + +public: + const CustomGOLData *GetCustomGOLByRule(int rule) const; + void SetCustomGOL(std::vector newCustomGol); + private: CoordStack& getCoordStackSingleton(); }; diff --git a/src/simulation/SimulationData.cpp b/src/simulation/SimulationData.cpp index 8fc7b099e..c646e9070 100644 --- a/src/simulation/SimulationData.cpp +++ b/src/simulation/SimulationData.cpp @@ -4,106 +4,48 @@ #include "ElementDefs.h" #include "ElementClasses.h" -#include "GOLMenu.h" +#include "BuiltinGOL.h" #include "WallType.h" #include "MenuSection.h" #include "graphics/Renderer.h" -std::vector LoadGOLMenu() -{ - return - std::vector{ - {"GOL", PIXPACK(0x0CAC00), 0, String("Game Of Life: Begin 3/Stay 23")}, - {"HLIF", PIXPACK(0xFF0000), 1, String("High Life: B36/S23")}, - {"ASIM", PIXPACK(0x0000FF), 2, String("Assimilation: B345/S4567")}, - {"2x2", PIXPACK(0xFFFF00), 3, String("2x2: B36/S125")}, - {"DANI", PIXPACK(0x00FFFF), 4, String("Day and Night: B3678/S34678")}, - {"AMOE", PIXPACK(0xFF00FF), 5, String("Amoeba: B357/S1358")}, - {"MOVE", PIXPACK(0xFFFFFF), 6, String("'Move' particles. Does not move things.. it is a life type: B368/S245")}, - {"PGOL", PIXPACK(0xE05010), 7, String("Pseudo Life: B357/S238")}, - {"DMOE", PIXPACK(0x500000), 8, String("Diamoeba: B35678/S5678")}, - {"34", PIXPACK(0x500050), 9, String("34: B34/S34")}, - {"LLIF", PIXPACK(0x505050), 10, String("Long Life: B345/S5")}, - {"STAN", PIXPACK(0x5000FF), 11, String("Stains: B3678/S235678")}, - {"SEED", PIXPACK(0xFBEC7D), 12, String("Seeds: B2/S")}, - {"MAZE", PIXPACK(0xA8E4A0), 13, String("Maze: B3/S12345")}, - {"COAG", PIXPACK(0x9ACD32), 14, String("Coagulations: B378/S235678")}, - {"WALL", PIXPACK(0x0047AB), 15, String("Walled cities: B45678/S2345")}, - {"GNAR", PIXPACK(0xE5B73B), 16, String("Gnarl: B1/S1")}, - {"REPL", PIXPACK(0x259588), 17, String("Replicator: B1357/S1357")}, - {"MYST", PIXPACK(0x0C3C00), 18, String("Mystery: B3458/S05678")}, - {"LOTE", PIXPACK(0xFF0000), 19, String("Living on the Edge: B37/S3458/4")}, - {"FRG2", PIXPACK(0x00FF00), 20, String("Like Frogs rule: B3/S124/3")}, - {"STAR", PIXPACK(0x0000FF), 21, String("Like Star Wars rule: B278/S3456/6")}, - {"FROG", PIXPACK(0x00AA00), 22, String("Frogs: B34/S12/3")}, - {"BRAN", PIXPACK(0xCCCC00), 23, String("Brian 6: B246/S6/3")} - }; -} - -std::vector > LoadGOLRules() -{ - return - std::vector >{ - // 0,1,2,3,4,5,6,7,8,STATES live=1 spawn=2 spawn&live=3 States are kind of how long until it dies, normal ones use two states(living,dead) for others the intermediate states live but do nothing - {0,0,0,0,0,0,0,0,0,2},//blank - {0,0,1,3,0,0,0,0,0,2},//GOL - {0,0,1,3,0,0,2,0,0,2},//HLIF - {0,0,0,2,3,3,1,1,0,2},//ASIM - {0,1,1,2,0,1,2,0,0,2},//2x2 - {0,0,0,3,1,0,3,3,3,2},//DANI - {0,1,0,3,0,3,0,2,1,2},//AMOE - {0,0,1,2,1,1,2,0,2,2},//MOVE - {0,0,1,3,0,2,0,2,1,2},//PGOL - {0,0,0,2,0,3,3,3,3,2},//DMOE - {0,0,0,3,3,0,0,0,0,2},//34 - {0,0,0,2,2,3,0,0,0,2},//LLIF - {0,0,1,3,0,1,3,3,3,2},//STAN - {0,0,2,0,0,0,0,0,0,2},//SEED - {0,1,1,3,1,1,0,0,0,2},//MAZE - {0,0,1,3,0,1,1,3,3,2},//COAG - {0,0,1,1,3,3,2,2,2,2},//WALL - {0,3,0,0,0,0,0,0,0,2},//GNAR - {0,3,0,3,0,3,0,3,0,2},//REPL - {1,0,0,2,2,3,1,1,3,2},//MYST - {0,0,0,3,1,1,0,2,1,4},//LOTE - {0,1,1,2,1,0,0,0,0,3},//FRG2 - {0,0,2,1,1,1,1,2,2,6},//STAR - {0,1,1,2,2,0,0,0,0,3},//FROG - {0,0,2,0,2,0,3,0,0,3},//BRAN - }; -} - -std::vector LoadGOLTypes() -{ - return - std::vector{ - GT_GOL, - GT_HLIF, - GT_ASIM, - GT_2x2, - GT_DANI, - GT_AMOE, - GT_MOVE, - GT_PGOL, - GT_DMOE, - GT_34, - GT_LLIF, - GT_STAN, - GT_SEED, - GT_MAZE, - GT_COAG, - GT_WALL, - GT_GNAR, - GT_REPL, - GT_MYST, - GT_LOTE, - GT_FRG2, - GT_STAR, - GT_FROG, - GT_BRAN, - }; -} +const BuiltinGOL builtinGol[NGOL] = { + // * Ruleset: + // * bits x = 8..0: stay if x neighbours present + // * bits x = 16..9: begin if x-8 neighbours present + // * bits 20..17: 4-bit unsigned int encoding the number of states minus 2; 2 states is + // encoded as 0, 3 states as 1, etc. + // * states are kind of long until a cell dies; normal ones use two states (living and dead), + // for others the intermediate states live but do nothing + // * the ruleset constants below look 20-bit, but rulesets actually consist of 21 + // bits of data; bit 20 just happens to not be set for any of the built-in types, + // as none of them have 10 or more states + { "GOL", GT_GOL , 0x0080C, PIXPACK(0x0CAC00), PIXPACK(0x0CAC00), NGT_GOL, String("Game Of Life: Begin 3/Stay 23") }, + { "HLIF", GT_HLIF, 0x0480C, PIXPACK(0xFF0000), PIXPACK(0xFF0000), NGT_HLIF, String("High Life: B36/S23") }, + { "ASIM", GT_ASIM, 0x038F0, PIXPACK(0x0000FF), PIXPACK(0x0000FF), NGT_ASIM, String("Assimilation: B345/S4567") }, + { "2X2", GT_2x2 , 0x04826, PIXPACK(0xFFFF00), PIXPACK(0xFFFF00), NGT_2x2, String("2X2: B36/S125") }, + { "DANI", GT_DANI, 0x1C9D8, PIXPACK(0x00FFFF), PIXPACK(0x00FFFF), NGT_DANI, String("Day and Night: B3678/S34678") }, + { "AMOE", GT_AMOE, 0x0A92A, PIXPACK(0xFF00FF), PIXPACK(0xFF00FF), NGT_AMOE, String("Amoeba: B357/S1358") }, + { "MOVE", GT_MOVE, 0x14834, PIXPACK(0xFFFFFF), PIXPACK(0xFFFFFF), NGT_MOVE, String("'Move' particles. Does not move things.. it is a life type: B368/S245") }, + { "PGOL", GT_PGOL, 0x0A90C, PIXPACK(0xE05010), PIXPACK(0xE05010), NGT_PGOL, String("Pseudo Life: B357/S238") }, + { "DMOE", GT_DMOE, 0x1E9E0, PIXPACK(0x500000), PIXPACK(0x500000), NGT_DMOE, String("Diamoeba: B35678/S5678") }, + { "3-4", GT_34 , 0x01818, PIXPACK(0x500050), PIXPACK(0x500050), NGT_34, String("3-4: B34/S34") }, + { "LLIF", GT_LLIF, 0x03820, PIXPACK(0x505050), PIXPACK(0x505050), NGT_LLIF, String("Long Life: B345/S5") }, + { "STAN", GT_STAN, 0x1C9EC, PIXPACK(0x5000FF), PIXPACK(0x5000FF), NGT_STAN, String("Stains: B3678/S235678") }, + { "SEED", GT_SEED, 0x00400, PIXPACK(0xFBEC7D), PIXPACK(0xFBEC7D), NGT_SEED, String("Seeds: B2/S") }, + { "MAZE", GT_MAZE, 0x0083E, PIXPACK(0xA8E4A0), PIXPACK(0xA8E4A0), NGT_MAZE, String("Maze: B3/S12345") }, + { "COAG", GT_COAG, 0x189EC, PIXPACK(0x9ACD32), PIXPACK(0x9ACD32), NGT_COAG, String("Coagulations: B378/S235678") }, + { "WALL", GT_WALL, 0x1F03C, PIXPACK(0x0047AB), PIXPACK(0x0047AB), NGT_WALL, String("Walled cities: B45678/S2345") }, + { "GNAR", GT_GNAR, 0x00202, PIXPACK(0xE5B73B), PIXPACK(0xE5B73B), NGT_GNAR, String("Gnarl: B1/S1") }, + { "REPL", GT_REPL, 0x0AAAA, PIXPACK(0x259588), PIXPACK(0x259588), NGT_REPL, String("Replicator: B1357/S1357") }, + { "MYST", GT_MYST, 0x139E1, PIXPACK(0x0C3C00), PIXPACK(0x0C3C00), NGT_MYST, String("Mystery: B3458/S05678") }, + { "LOTE", GT_LOTE, 0x48938, PIXPACK(0xFF0000), PIXPACK(0xFFFF00), NGT_LOTE, String("Living on the Edge: B37/S3458/4") }, + { "FRG2", GT_FRG2, 0x20816, PIXPACK(0x006432), PIXPACK(0x00FF5A), NGT_FRG2, String("Like Frogs rule: B3/S124/3") }, + { "STAR", GT_STAR, 0x98478, PIXPACK(0x000040), PIXPACK(0x0000E6), NGT_STAR, String("Like Star Wars rule: B278/S3456/6") }, + { "FROG", GT_FROG, 0x21806, PIXPACK(0x006400), PIXPACK(0x00FF00), NGT_FROG, String("Frogs: B34/S12/3") }, + { "BRAN", GT_BRAN, 0x25440, PIXPACK(0xFFFF00), PIXPACK(0x969600), NGT_BRAN, String("Brian 6: B246/S6/3" )} +}; std::vector LoadWalls() { diff --git a/src/simulation/SimulationData.h b/src/simulation/SimulationData.h index ac0058c27..766969ffb 100644 --- a/src/simulation/SimulationData.h +++ b/src/simulation/SimulationData.h @@ -137,17 +137,13 @@ struct part_type; struct part_transition; struct wall_type; -struct gol_menu; +struct BuiltinGOL; struct menu_section; class SimTool; class Element; -std::vector LoadGOLMenu(); - -std::vector LoadGOLTypes(); - -std::vector > LoadGOLRules(); +extern const BuiltinGOL builtinGol[]; std::vector LoadWalls(); diff --git a/src/simulation/elements/BCLN.cpp b/src/simulation/elements/BCLN.cpp index cb0591851..6f1158775 100644 --- a/src/simulation/elements/BCLN.cpp +++ b/src/simulation/elements/BCLN.cpp @@ -57,7 +57,7 @@ static int update(UPDATE_FUNC_ARGS) parts[i].vx += ADVECTION*sim->vx[y/CELL][x/CELL]; parts[i].vy += ADVECTION*sim->vy[y/CELL][x/CELL]; } - if (parts[i].ctype<=0 || parts[i].ctype>=PT_NUM || !sim->elements[parts[i].ctype].Enabled || (parts[i].ctype==PT_LIFE && (parts[i].tmp<0 || parts[i].tmp>=NGOL))) + if (parts[i].ctype<=0 || parts[i].ctype>=PT_NUM || !sim->elements[parts[i].ctype].Enabled) { int r, rx, ry, rt; for (rx=-1; rx<2; rx++) diff --git a/src/simulation/elements/CLNE.cpp b/src/simulation/elements/CLNE.cpp index a59353ae8..e7a8bd6d7 100644 --- a/src/simulation/elements/CLNE.cpp +++ b/src/simulation/elements/CLNE.cpp @@ -48,7 +48,7 @@ void Element::Element_CLNE() static int update(UPDATE_FUNC_ARGS) { - if (parts[i].ctype<=0 || parts[i].ctype>=PT_NUM || !sim->elements[parts[i].ctype].Enabled || (parts[i].ctype==PT_LIFE && (parts[i].tmp<0 || parts[i].tmp>=NGOL))) + if (parts[i].ctype<=0 || parts[i].ctype>=PT_NUM || !sim->elements[parts[i].ctype].Enabled) { int r, rx, ry, rt; for (rx=-1; rx<2; rx++) diff --git a/src/simulation/elements/CONV.cpp b/src/simulation/elements/CONV.cpp index c3be318ad..681f2e9ed 100644 --- a/src/simulation/elements/CONV.cpp +++ b/src/simulation/elements/CONV.cpp @@ -49,8 +49,8 @@ void Element::Element_CONV() static int update(UPDATE_FUNC_ARGS) { int r, rx, ry; - int ctype = TYP(parts[i].ctype), ctypeExtra = ID(parts[i].ctype); - if (ctype<=0 || ctype>=PT_NUM || !sim->elements[ctype].Enabled || ctype==PT_CONV || (ctype==PT_LIFE && (ctypeExtra<0 || ctypeExtra>=NGOL))) + int ctype = TYP(parts[i].ctype); + if (ctype<=0 || ctype>=PT_NUM || !sim->elements[ctype].Enabled || ctype==PT_CONV) { for (rx=-1; rx<2; rx++) for (ry=-1; ry<2; ry++) diff --git a/src/simulation/elements/LIFE.cpp b/src/simulation/elements/LIFE.cpp index 566c9c982..c8404e2dc 100644 --- a/src/simulation/elements/LIFE.cpp +++ b/src/simulation/elements/LIFE.cpp @@ -1,8 +1,5 @@ #include "simulation/ElementCommon.h" -bool Element_GOL_colourInit = false; -pixel Element_GOL_colour[NGOL]; - static int graphics(GRAPHICS_FUNC_ARGS); static void create(ELEMENT_CREATE_FUNC_ARGS); @@ -49,82 +46,59 @@ void Element::Element_LIFE() Graphics = &graphics; Create = &create; - - if (!Element_GOL_colourInit) - { - Element_GOL_colourInit = true; - - std::vector golMenuT = LoadGOLMenu(); - for(int i = 0; i < NGOL; i++) - { - Element_GOL_colour[i] = golMenuT[i].colour; - } - } } static int graphics(GRAPHICS_FUNC_ARGS) { - pixel pc; - if (cpart->ctype==NGT_LOTE)//colors for life states + auto colour1 = cpart->dcolour; + auto colour2 = cpart->tmp; + if (!colour1) { - if (cpart->tmp==2) - pc = PIXRGB(255, 128, 0); - else if (cpart->tmp==1) - pc = PIXRGB(255, 255, 0); + colour1 = PIXPACK(0xFFFFFF); + } + auto ruleset = cpart->ctype; + if (ruleset >= 0 && ruleset < NGOL) + { + ruleset = builtinGol[ruleset].ruleset; + } + if (!ren->blackDecorations) + { + auto states = ((ruleset >> 17) & 0xF) + 2; + if (states == 2) + { + *colr = PIXR(colour1); + *colg = PIXG(colour1); + *colb = PIXB(colour1); + } else - pc = PIXRGB(255, 0, 0); + { + auto mul = (cpart->tmp2 - 1) / float(states - 2); + *colr = PIXR(colour1) * mul + PIXR(colour2) * (1.f - mul); + *colg = PIXG(colour1) * mul + PIXG(colour2) * (1.f - mul); + *colb = PIXB(colour1) * mul + PIXB(colour2) * (1.f - mul); + } } - else if (cpart->ctype==NGT_FRG2)//colors for life states - { - if (cpart->tmp==2) - pc = PIXRGB(0, 100, 50); - else - pc = PIXRGB(0, 255, 90); - } - else if (cpart->ctype==NGT_STAR)//colors for life states - { - if (cpart->tmp==4) - pc = PIXRGB(0, 0, 128); - else if (cpart->tmp==3) - pc = PIXRGB(0, 0, 150); - else if (cpart->tmp==2) - pc = PIXRGB(0, 0, 190); - else if (cpart->tmp==1) - pc = PIXRGB(0, 0, 230); - else - pc = PIXRGB(0, 0, 70); - } - else if (cpart->ctype==NGT_FROG)//colors for life states - { - if (cpart->tmp==2) - pc = PIXRGB(0, 100, 0); - else - pc = PIXRGB(0, 255, 0); - } - else if (cpart->ctype==NGT_BRAN)//colors for life states - { - if (cpart->tmp==1) - pc = PIXRGB(150, 150, 0); - else - pc = PIXRGB(255, 255, 0); - } - else if (cpart->ctype >= 0 && cpart->ctype < NGOL) - { - pc = Element_GOL_colour[cpart->ctype]; - } - else - pc = ren->sim->elements[cpart->type].Colour; - *colr = PIXR(pc); - *colg = PIXG(pc); - *colb = PIXB(pc); + *pixel_mode |= NO_DECO; return 0; } static void create(ELEMENT_CREATE_FUNC_ARGS) { - if (v >= 0 && v < NGOL) + sim->parts[i].ctype = v & 0x1FFFFF; + if (v < NGOL) { - sim->parts[i].tmp = sim->grule[v+1][9] - 1; - sim->parts[i].ctype = v; + sim->parts[i].dcolour = builtinGol[v].colour; + sim->parts[i].tmp = builtinGol[v].colour2; + v = builtinGol[v].ruleset; } + else if (!(v & 0x200000)) // * 0x200000: No need to look for colours, they'll be set later anyway. + { + auto *cgol = sim->GetCustomGOLByRule(v & 0x1FFFFF); + if (cgol) + { + sim->parts[i].dcolour = cgol->colour1; + sim->parts[i].tmp = cgol->colour2; + } + } + sim->parts[i].tmp2 = ((v >> 17) & 0xF) + 1; } diff --git a/src/simulation/elements/PBCN.cpp b/src/simulation/elements/PBCN.cpp index 31b2e85dc..968792af2 100644 --- a/src/simulation/elements/PBCN.cpp +++ b/src/simulation/elements/PBCN.cpp @@ -66,7 +66,7 @@ static int update(UPDATE_FUNC_ARGS) return 1; } } - if (parts[i].ctype<=0 || parts[i].ctype>=PT_NUM || !sim->elements[parts[i].ctype].Enabled || (parts[i].ctype==PT_LIFE && (parts[i].tmp<0 || parts[i].tmp>=NGOL))) + if (parts[i].ctype<=0 || parts[i].ctype>=PT_NUM || !sim->elements[parts[i].ctype].Enabled) for (rx=-1; rx<2; rx++) for (ry=-1; ry<2; ry++) if (BOUNDS_CHECK) diff --git a/src/simulation/elements/PCLN.cpp b/src/simulation/elements/PCLN.cpp index af8f05b15..e6f30c9c6 100644 --- a/src/simulation/elements/PCLN.cpp +++ b/src/simulation/elements/PCLN.cpp @@ -79,7 +79,7 @@ static int update(UPDATE_FUNC_ARGS) parts[i].life = 10; } } - if (parts[i].ctype<=0 || parts[i].ctype>=PT_NUM || !sim->elements[parts[i].ctype].Enabled || (parts[i].ctype==PT_LIFE && (parts[i].tmp<0 || parts[i].tmp>=NGOL))) + if (parts[i].ctype<=0 || parts[i].ctype>=PT_NUM || !sim->elements[parts[i].ctype].Enabled) for (rx=-1; rx<2; rx++) for (ry=-1; ry<2; ry++) if (BOUNDS_CHECK)