This repository has been archived on 2025-03-20. You can view files and clone it, but cannot push or open issues or pull requests.
The-Powder-Toy/src/simulation/Simulation.cpp
Tamás Bálint Misius 920f7646e4
Fix more reflection issues
Since b393050e55, surface normals were calculated incorrectly because blocking cells were detected as blocking only if they were within the heading of the scout process (see that commit for terminology). This had been true even before that commit but it had had less visible effects because both processes would traverse their neighbours in the same order, which the initial heading approximation code (direction_to_map, removed in this commit) had worked better with.

This commit fixes this by separating information about blocking entirely from information about current heading, as it should be. This fixes a few prism-type saves such as id:1188302, but is also not entirely backwards-compatible, for all saves that are considered broken as far as normal calculation code is concerned (e.g. the surfaces are not long enough or are wobbly) will now be broken differently from before. This affects for example many coalescing laser-type saves such as id:482187 that rely on very specific arrangements of very few particles of reflective material behaving as perfect 45deg mirrors.
2023-05-17 17:21:41 +02:00

4113 lines
114 KiB
C++

#include "Simulation.h"
#include "Air.h"
#include "ElementClasses.h"
#include "gravity/Gravity.h"
#include "ToolClasses.h"
#include "SimulationData.h"
#include "GOLString.h"
#include "client/GameSave.h"
#include "common/tpt-compat.h"
#include "common/tpt-rand.h"
#include "common/tpt-thread-local.h"
#include "gui/game/Brush.h"
#include <iostream>
#include <set>
extern int Element_PPIP_ppip_changed;
extern int Element_LOLZ_RuleTable[9][9];
extern int Element_LOLZ_lolz[XRES/9][YRES/9];
extern int Element_LOVE_RuleTable[9][9];
extern int Element_LOVE_love[XRES/9][YRES/9];
void Simulation::Load(const GameSave *originalSave, bool includePressure, Vec2<int> blockP)
{
auto save = std::unique_ptr<GameSave>(new GameSave(*originalSave));
auto partP = blockP * CELL;
unsigned int pmapmask = (1<<save->pmapbits)-1;
int partMap[PT_NUM];
for(int i = 0; i < PT_NUM; i++)
{
partMap[i] = i;
}
if(save->palette.size())
{
for(auto &pi : save->palette)
{
if (pi.second > 0 && pi.second < PT_NUM)
{
int myId = 0;
for (int i = 0; i < PT_NUM; i++)
{
if (elements[i].Enabled && elements[i].Identifier == pi.first)
myId = i;
}
// if this is a custom element, set the ID to the ID we found when comparing identifiers in the palette map
// set type to 0 if we couldn't find an element with that identifier present when loading,
// unless this is a default element, in which case keep the current ID, because otherwise when an element is renamed it wouldn't show up anymore in older saves
if (myId != 0 || !pi.first.BeginsWith("DEFAULT_PT_"))
partMap[pi.second] = myId;
}
}
}
RecalcFreeParticles(false);
auto &possiblyCarriesType = Particle::PossiblyCarriesType();
auto &properties = Particle::GetProperties();
bool doFullScan = false;
for (int n = 0; n < NPART && n < save->particlesCount; n++)
{
Particle *tempPart = &save->particles[n];
tempPart->x += (float)partP.X;
tempPart->y += (float)partP.Y;
int x = int(tempPart->x + 0.5f);
int y = int(tempPart->y + 0.5f);
auto &type = tempPart->type;
// Check various scenarios where we are unable to spawn the element, and set type to 0 to block spawning later
if (!InBounds(x, y))
{
type = 0;
continue;
}
if (type < 0 || type >= PT_NUM)
{
type = 0;
continue;
}
type = partMap[type];
for (auto index : possiblyCarriesType)
{
if (elements[type].CarriesTypeIn & (1U << index))
{
auto *prop = reinterpret_cast<int *>(reinterpret_cast<char *>(tempPart) + properties[index].Offset);
auto carriedType = *prop & int(pmapmask);
auto extra = *prop >> save->pmapbits;
if (carriedType >= 0 && carriedType < PT_NUM)
{
carriedType = partMap[carriedType];
}
*prop = PMAP(extra, carriedType);
}
}
// Ensure we can spawn this element
if ((player.spwn == 1 && type==PT_STKM) || (player2.spwn == 1 && type==PT_STKM2))
{
type = 0;
continue;
}
if ((type == PT_SPAWN || type == PT_SPAWN2) && elementCount[type])
{
type = 0;
continue;
}
bool Element_FIGH_CanAlloc(Simulation *sim);
if (type == PT_FIGH && !Element_FIGH_CanAlloc(this))
{
type = 0;
continue;
}
if (!elements[type].Enabled)
{
type = 0;
continue;
}
if (pmap[y][x])
{
// Particle already exists in this location. Set pmap to 0, then kill it and all stacked particles in the loop below
pmap[y][x] = 0;
doFullScan = true;
}
else if (photons[y][x])
{
// Particle already exists in this location. Set photons to 0, then kill it and all stacked particles in the loop below
photons[y][x] = 0;
doFullScan = true;
}
}
if (doFullScan)
{
// Loop through particles to find particles in need of being killed
for (int i = 0; i <= parts_lastActiveIndex; i++) {
if (parts[i].type)
{
int x = int(parts[i].x + 0.5f);
int y = int(parts[i].y + 0.5f);
if (elements[parts[i].type].Properties & TYPE_ENERGY)
{
if (!photons[y][x])
kill_part(i);
}
else
{
if (!pmap[y][x])
kill_part(i);
}
}
}
}
// Map of soap particles loaded into this save, old ID -> new ID
std::map<unsigned int, unsigned int> soapList;
for (int n = 0; n < NPART && n < save->particlesCount; n++)
{
Particle tempPart = save->particles[n];
if (elements[tempPart.type].CreateAllowed)
{
if (!(*(elements[tempPart.type].CreateAllowed))(this, -3, int(tempPart.x + 0.5f), int(tempPart.y + 0.5f), tempPart.type))
{
continue;
}
}
// Allocate particle (this location is guaranteed to be empty due to "full scan" logic above)
if (pfree == -1)
break;
auto i = pfree;
pfree = parts[i].life;
if (i > parts_lastActiveIndex)
parts_lastActiveIndex = i;
parts[i] = tempPart;
elementCount[tempPart.type]++;
void Element_STKM_init_legs(Simulation * sim, playerst *playerp, int i);
switch (parts[i].type)
{
case PT_STKM:
Element_STKM_init_legs(this, &player, i);
player.spwn = 1;
player.elem = PT_DUST;
if ((save->majorVersion < 93 && parts[i].ctype == SPC_AIR) ||
(save->majorVersion < 88 && parts[i].ctype == OLD_SPC_AIR))
{
player.fan = true;
}
if (save->stkm.rocketBoots1)
player.rocketBoots = true;
if (save->stkm.fan1)
player.fan = true;
break;
case PT_STKM2:
Element_STKM_init_legs(this, &player2, i);
player2.spwn = 1;
player2.elem = PT_DUST;
if ((save->majorVersion < 93 && parts[i].ctype == SPC_AIR) ||
(save->majorVersion < 88 && parts[i].ctype == OLD_SPC_AIR))
{
player2.fan = true;
}
if (save->stkm.rocketBoots2)
player2.rocketBoots = true;
if (save->stkm.fan2)
player2.fan = true;
break;
case PT_SPAWN:
player.spawnID = i;
break;
case PT_SPAWN2:
player2.spawnID = i;
break;
case PT_FIGH:
{
unsigned int oldTmp = parts[i].tmp;
int Element_FIGH_Alloc(Simulation *sim);
parts[i].tmp = Element_FIGH_Alloc(this);
if (parts[i].tmp >= 0)
{
bool fan = false;
if ((save->majorVersion < 93 && parts[i].ctype == SPC_AIR)
|| (save->majorVersion < 88 && parts[i].ctype == OLD_SPC_AIR))
{
fan = true;
parts[i].ctype = 0;
}
fighters[parts[i].tmp].elem = PT_DUST;
void Element_FIGH_NewFighter(Simulation *sim, int fighterID, int i, int elem);
Element_FIGH_NewFighter(this, parts[i].tmp, i, parts[i].ctype);
if (fan)
fighters[parts[i].tmp].fan = true;
for (unsigned int fighNum : save->stkm.rocketBootsFigh)
{
if (fighNum == oldTmp)
fighters[parts[i].tmp].rocketBoots = true;
}
for (unsigned int fighNum : save->stkm.fanFigh)
{
if (fighNum == oldTmp)
fighters[parts[i].tmp].fan = true;
}
}
else
{
// Should not be possible because we verify with CanAlloc above this
parts[i].type = 0;
}
break;
}
case PT_SOAP:
soapList.insert(std::pair<unsigned int, unsigned int>(n, i));
break;
}
if (GameSave::PressureInTmp3(parts[i].type) && !includePressure)
{
parts[i].tmp3 = 0;
}
}
parts_lastActiveIndex = NPART-1;
force_stacking_check = true;
Element_PPIP_ppip_changed = 1;
RecalcFreeParticles(false);
// fix SOAP links using soapList, a map of old particle ID -> new particle ID
// loop through every old particle (loaded from save), and convert .tmp / .tmp2
for (std::map<unsigned int, unsigned int>::iterator iter = soapList.begin(), end = soapList.end(); iter != end; ++iter)
{
int i = (*iter).second;
if ((parts[i].ctype & 0x2) == 2)
{
std::map<unsigned int, unsigned int>::iterator n = soapList.find(parts[i].tmp);
if (n != end)
parts[i].tmp = n->second;
// sometimes the proper SOAP isn't found. It should remove the link, but seems to break some saves
// so just ignore it
}
if ((parts[i].ctype & 0x4) == 4)
{
std::map<unsigned int, unsigned int>::iterator n = soapList.find(parts[i].tmp2);
if (n != end)
parts[i].tmp2 = n->second;
// sometimes the proper SOAP isn't found. It should remove the link, but seems to break some saves
// so just ignore it
}
}
for (size_t i = 0; i < save->signs.size() && signs.size() < MAXSIGNS; i++)
{
if (save->signs[i].text[0])
{
sign tempSign = save->signs[i];
tempSign.x += partP.X;
tempSign.y += partP.Y;
signs.push_back(tempSign);
}
}
for (auto bpos : save->blockSize.OriginRect())
{
if(save->blockMap[bpos])
{
bmap[blockP.Y + bpos.Y][blockP.X + bpos.X] = save->blockMap[bpos];
fvx[blockP.Y + bpos.Y][blockP.X + bpos.X] = save->fanVelX[bpos];
fvy[blockP.Y + bpos.Y][blockP.X + bpos.X] = save->fanVelY[bpos];
}
if (includePressure)
{
if (save->hasPressure)
{
pv[blockP.Y + bpos.Y][blockP.X + bpos.X] = save->pressure[bpos];
vx[blockP.Y + bpos.Y][blockP.X + bpos.X] = save->velocityX[bpos];
vy[blockP.Y + bpos.Y][blockP.X + bpos.X] = save->velocityY[bpos];
}
if (save->hasAmbientHeat)
hv[blockP.Y + bpos.Y][blockP.X + bpos.X] = save->ambientHeat[bpos];
if (save->hasBlockAirMaps)
{
air->bmap_blockair[blockP.Y + bpos.Y][blockP.X + bpos.X] = save->blockAir[bpos];
air->bmap_blockairh[blockP.Y + bpos.Y][blockP.X + bpos.X] = save->blockAirh[bpos];
}
}
}
gravWallChanged = true;
if (!save->hasBlockAirMaps)
{
air->ApproximateBlockAirMaps();
}
}
std::unique_ptr<GameSave> Simulation::Save(bool includePressure, Rect<int> blockR)
{
auto blockP = blockR.TopLeft;
auto partR = RectSized(blockR.TopLeft * CELL, blockR.Size() * CELL);
auto newSave = std::make_unique<GameSave>(blockR.Size());
auto &possiblyCarriesType = Particle::PossiblyCarriesType();
auto &properties = Particle::GetProperties();
newSave->frameCount = frameCount;
newSave->rngState = rng.state();
int storedParts = 0;
int elementCount[PT_NUM];
std::fill(elementCount, elementCount+PT_NUM, 0);
// Map of soap particles loaded into this save, old ID -> new ID
// Now stores all particles, not just SOAP (but still only used for soap)
std::map<unsigned int, unsigned int> particleMap;
std::set<int> paletteSet;
for (int i = 0; i < NPART; i++)
{
int x, y;
x = int(parts[i].x + 0.5f);
y = int(parts[i].y + 0.5f);
if (parts[i].type && partR.Contains({ x, y }))
{
Particle tempPart = parts[i];
tempPart.x -= blockP.X * CELL;
tempPart.y -= blockP.Y * CELL;
if (elements[tempPart.type].Enabled)
{
particleMap.insert(std::pair<unsigned int, unsigned int>(i, storedParts));
*newSave << tempPart;
storedParts++;
elementCount[tempPart.type]++;
paletteSet.insert(tempPart.type);
for (auto index : possiblyCarriesType)
{
if (elements[tempPart.type].CarriesTypeIn & (1U << index))
{
auto *prop = reinterpret_cast<const int *>(reinterpret_cast<const char *>(&tempPart) + properties[index].Offset);
paletteSet.insert(TYP(*prop));
}
}
}
}
}
for (int ID : paletteSet)
newSave->palette.push_back(GameSave::PaletteItem(elements[ID].Identifier, ID));
if (storedParts && elementCount[PT_SOAP])
{
// fix SOAP links using particleMap, a map of old particle ID -> new particle ID
// loop through every new particle (saved into the save), and convert .tmp / .tmp2
for (std::map<unsigned int, unsigned int>::iterator iter = particleMap.begin(), end = particleMap.end(); iter != end; ++iter)
{
int i = (*iter).second;
if (newSave->particles[i].type != PT_SOAP)
continue;
if ((newSave->particles[i].ctype & 0x2) == 2)
{
std::map<unsigned int, unsigned int>::iterator n = particleMap.find(newSave->particles[i].tmp);
if (n != end)
newSave->particles[i].tmp = n->second;
else
{
newSave->particles[i].tmp = 0;
newSave->particles[i].ctype ^= 2;
}
}
if ((newSave->particles[i].ctype & 0x4) == 4)
{
std::map<unsigned int, unsigned int>::iterator n = particleMap.find(newSave->particles[i].tmp2);
if (n != end)
newSave->particles[i].tmp2 = n->second;
else
{
newSave->particles[i].tmp2 = 0;
newSave->particles[i].ctype ^= 4;
}
}
}
}
for (size_t i = 0; i < MAXSIGNS && i < signs.size(); i++)
{
if (signs[i].text.length() && partR.Contains({ signs[i].x, signs[i].y }))
{
sign tempSign = signs[i];
tempSign.x -= blockP.X * CELL;
tempSign.y -= blockP.Y * CELL;
*newSave << tempSign;
}
}
for (auto bpos : newSave->blockSize.OriginRect())
{
if(bmap[bpos.Y + blockP.Y][bpos.X + blockP.X])
{
newSave->blockMap[bpos] = bmap[bpos.Y + blockP.Y][bpos.X + blockP.X];
newSave->fanVelX[bpos] = fvx[bpos.Y + blockP.Y][bpos.X + blockP.X];
newSave->fanVelY[bpos] = fvy[bpos.Y + blockP.Y][bpos.X + blockP.X];
}
if (includePressure)
{
newSave->pressure[bpos] = pv[bpos.Y + blockP.Y][bpos.X + blockP.X];
newSave->velocityX[bpos] = vx[bpos.Y + blockP.Y][bpos.X + blockP.X];
newSave->velocityY[bpos] = vy[bpos.Y + blockP.Y][bpos.X + blockP.X];
newSave->ambientHeat[bpos] = hv[bpos.Y + blockP.Y][bpos.X + blockP.X];
newSave->blockAir[bpos] = air->bmap_blockair[bpos.Y + blockP.Y][bpos.X + blockP.X];
newSave->blockAirh[bpos] = air->bmap_blockairh[bpos.Y + blockP.Y][bpos.X + blockP.X];
}
}
if (includePressure || ensureDeterminism)
{
newSave->hasPressure = true;
newSave->hasAmbientHeat = true;
}
newSave->ensureDeterminism = ensureDeterminism;
newSave->stkm.rocketBoots1 = player.rocketBoots;
newSave->stkm.rocketBoots2 = player2.rocketBoots;
newSave->stkm.fan1 = player.fan;
newSave->stkm.fan2 = player2.fan;
for (unsigned char i = 0; i < MAX_FIGHTERS; i++)
{
if (fighters[i].rocketBoots)
newSave->stkm.rocketBootsFigh.push_back(i);
if (fighters[i].fan)
newSave->stkm.fanFigh.push_back(i);
}
SaveSimOptions(*newSave);
newSave->pmapbits = PMAPBITS;
return newSave;
}
void Simulation::SaveSimOptions(GameSave &gameSave)
{
gameSave.gravityMode = gravityMode;
gameSave.customGravityX = customGravityX;
gameSave.customGravityY = customGravityY;
gameSave.airMode = air->airMode;
gameSave.ambientAirTemp = air->ambientAirTemp;
gameSave.edgeMode = edgeMode;
gameSave.legacyEnable = legacy_enable;
gameSave.waterEEnabled = water_equal_test;
gameSave.gravityEnable = grav->IsEnabled();
gameSave.aheatEnable = aheat_enable;
}
bool Simulation::FloodFillPmapCheck(int x, int y, int type)
{
if (type == 0)
return !pmap[y][x] && !photons[y][x];
if (elements[type].Properties&TYPE_ENERGY)
return TYP(photons[y][x]) == type;
else
return TYP(pmap[y][x]) == type;
}
CoordStack& Simulation::getCoordStackSingleton()
{
// Future-proofing in case Simulation is later multithreaded
static THREAD_LOCAL(CoordStack, cs);
return cs;
}
int Simulation::flood_prop(int x, int y, size_t propoffset, PropertyValue propvalue, StructProperty::PropertyType proptype)
{
int i, x1, x2, dy = 1;
int did_something = 0;
int r = pmap[y][x];
if (!r)
r = photons[y][x];
if (!r)
return 0;
int parttype = TYP(r);
char * bitmap = (char*)malloc(XRES*YRES); //Bitmap for checking
if (!bitmap) return -1;
memset(bitmap, 0, XRES*YRES);
try
{
CoordStack& cs = getCoordStackSingleton();
cs.clear();
cs.push(x, y);
do
{
cs.pop(x, y);
x1 = x2 = x;
while (x1>=CELL)
{
if (!FloodFillPmapCheck(x1-1, y, parttype) || bitmap[(y*XRES)+x1-1])
break;
x1--;
}
while (x2<XRES-CELL)
{
if (!FloodFillPmapCheck(x2+1, y, parttype) || bitmap[(y*XRES)+x2+1])
break;
x2++;
}
for (x=x1; x<=x2; x++)
{
i = pmap[y][x];
if (!i)
i = photons[y][x];
if (!i)
continue;
switch (proptype) {
case StructProperty::Float:
*((float*)(((char*)&parts[ID(i)])+propoffset)) = propvalue.Float;
break;
case StructProperty::ParticleType:
case StructProperty::Integer:
*((int*)(((char*)&parts[ID(i)])+propoffset)) = propvalue.Integer;
break;
case StructProperty::UInteger:
*((unsigned int*)(((char*)&parts[ID(i)])+propoffset)) = propvalue.UInteger;
break;
default:
break;
}
bitmap[(y*XRES)+x] = 1;
did_something = 1;
}
if (y>=CELL+dy)
for (x=x1; x<=x2; x++)
if (FloodFillPmapCheck(x, y-dy, parttype) && !bitmap[((y-dy)*XRES)+x])
cs.push(x, y-dy);
if (y<YRES-CELL-dy)
for (x=x1; x<=x2; x++)
if (FloodFillPmapCheck(x, y+dy, parttype) && !bitmap[((y+dy)*XRES)+x])
cs.push(x, y+dy);
} while (cs.getSize()>0);
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
free(bitmap);
return -1;
}
free(bitmap);
return did_something;
}
int Simulation::FloodINST(int x, int y)
{
int x1, x2;
int created_something = 0;
const auto isSparkableInst = [this](int x, int y) -> bool {
return TYP(pmap[y][x])==PT_INST && parts[ID(pmap[y][x])].life==0;
};
const auto isInst = [this](int x, int y) -> bool {
return TYP(pmap[y][x])==PT_INST || (TYP(pmap[y][x])==PT_SPRK && parts[ID(pmap[y][x])].ctype==PT_INST);
};
if (!isSparkableInst(x,y))
return 1;
CoordStack& cs = getCoordStackSingleton();
cs.clear();
cs.push(x, y);
try
{
do
{
cs.pop(x, y);
x1 = x2 = x;
// go left as far as possible
while (x1>=CELL && isSparkableInst(x1-1, y))
{
x1--;
}
// go right as far as possible
while (x2<XRES-CELL && isSparkableInst(x2+1, y))
{
x2++;
}
// fill span
for (x=x1; x<=x2; x++)
{
if (create_part(-1, x, y, PT_SPRK)>=0)
created_something = 1;
}
// add vertically adjacent pixels to stack
// (wire crossing for INST)
if (y>=CELL+1 && x1==x2 &&
isInst(x1-1, y-1) && isInst(x1, y-1) && isInst(x1+1, y-1) &&
!isInst(x1-1, y-2) && isInst(x1, y-2) && !isInst(x1+1, y-2))
{
// travelling vertically up, skipping a horizontal line
if (isSparkableInst(x1, y-2))
{
cs.push(x1, y-2);
}
}
else if (y>=CELL+1)
{
for (x=x1; x<=x2; x++)
{
if (isSparkableInst(x, y-1))
{
if (x==x1 || x==x2 || y>=YRES-CELL-1 || !isInst(x, y+1) || isInst(x+1, y+1) || isInst(x-1, y+1))
{
// if at the end of a horizontal section, or if it's a T junction or not a 1px wire crossing
cs.push(x, y-1);
}
}
}
}
if (y<YRES-CELL-1 && x1==x2 &&
isInst(x1-1, y+1) && isInst(x1, y+1) && isInst(x1+1, y+1) &&
!isInst(x1-1, y+2) && isInst(x1, y+2) && !isInst(x1+1, y+2))
{
// travelling vertically down, skipping a horizontal line
if (isSparkableInst(x1, y+2))
{
cs.push(x1, y+2);
}
}
else if (y<YRES-CELL-1)
{
for (x=x1; x<=x2; x++)
{
if (isSparkableInst(x, y+1))
{
if (x==x1 || x==x2 || y<0 || !isInst(x, y-1) || isInst(x+1, y-1) || isInst(x-1, y-1))
{
// if at the end of a horizontal section, or if it's a T junction or not a 1px wire crossing
cs.push(x, y+1);
}
}
}
}
} while (cs.getSize()>0);
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
return -1;
}
return created_something;
}
bool Simulation::flood_water(int x, int y, int i)
{
int x1, x2, originalX = x, originalY = y;
int r = pmap[y][x];
if (!r)
return false;
// Bitmap for checking where we've already looked
auto bitmapPtr = std::unique_ptr<char[]>(new char[XRES * YRES]);
char *bitmap = bitmapPtr.get();
std::fill(&bitmap[0], &bitmap[0] + XRES * YRES, 0);
try
{
CoordStack& cs = getCoordStackSingleton();
cs.clear();
cs.push(x, y);
do
{
cs.pop(x, y);
x1 = x2 = x;
while (x1 >= CELL)
{
if (elements[TYP(pmap[y][x1 - 1])].Falldown != 2 || bitmap[(y * XRES) + x1 - 1])
break;
x1--;
}
while (x2 < XRES-CELL)
{
if (elements[TYP(pmap[y][x2 + 1])].Falldown != 2 || bitmap[(y * XRES) + x1 - 1])
break;
x2++;
}
for (int x = x1; x <= x2; x++)
{
if ((y - 1) > originalY && !pmap[y - 1][x])
{
// Try to move the water to a random position on this line, because there's probably a free location somewhere
int randPos = rng.between(x, x2);
if (!pmap[y - 1][randPos] && eval_move(parts[i].type, randPos, y - 1, nullptr))
x = randPos;
// Couldn't move to random position, so try the original position on the left
else if (!eval_move(parts[i].type, x, y - 1, nullptr))
continue;
move(i, originalX, originalY, float(x), float(y - 1));
return true;
}
bitmap[(y * XRES) + x] = 1;
}
if (y >= CELL + 1)
for (int x = x1; x <= x2; x++)
if (elements[TYP(pmap[y - 1][x])].Falldown == 2 && !bitmap[((y - 1) * XRES) + x])
cs.push(x, y - 1);
if (y < YRES - CELL - 1)
for (int x = x1; x <= x2; x++)
if (elements[TYP(pmap[y + 1][x])].Falldown == 2 && !bitmap[((y + 1) * XRES) + x])
cs.push(x, y + 1);
} while (cs.getSize() > 0);
}
catch (std::exception &e)
{
std::cerr << e.what() << std::endl;
return false;
}
return false;
}
void Simulation::SetEdgeMode(int newEdgeMode)
{
edgeMode = newEdgeMode;
switch(edgeMode)
{
case 0:
case 2:
for(int i = 0; i<XCELLS; i++)
{
bmap[0][i] = 0;
bmap[YCELLS-1][i] = 0;
}
for(int i = 1; i<(YCELLS-1); i++)
{
bmap[i][0] = 0;
bmap[i][XCELLS-1] = 0;
}
break;
case 1:
int i;
for(i=0; i<XCELLS; i++)
{
bmap[0][i] = WL_WALL;
bmap[YCELLS-1][i] = WL_WALL;
}
for(i=1; i<(YCELLS-1); i++)
{
bmap[i][0] = WL_WALL;
bmap[i][XCELLS-1] = WL_WALL;
}
break;
default:
SetEdgeMode(0);
}
}
// Now simply creates a 0 pixel radius line without all the complicated flags / other checks
// Would make sense to move to Editing.cpp but SPRK needs it.
void Simulation::CreateLine(int x1, int y1, int x2, int y2, int c)
{
bool reverseXY = abs(y2-y1) > abs(x2-x1);
int x, y, dx, dy, sy;
float e, de;
int v = ID(c);
c = TYP(c);
if (reverseXY)
{
y = x1;
x1 = y1;
y1 = y;
y = x2;
x2 = y2;
y2 = y;
}
if (x1 > x2)
{
y = x1;
x1 = x2;
x2 = y;
y = y1;
y1 = y2;
y2 = y;
}
dx = x2 - x1;
dy = abs(y2 - y1);
e = 0.0f;
de = dx ? dy/(float)dx : 0.0f;
y = y1;
sy = (y1<y2) ? 1 : -1;
for (x=x1; x<=x2; x++)
{
if (reverseXY)
create_part(-1, y, x, c, v);
else
create_part(-1, x, y, c, v);
e += de;
if (e >= 0.5f)
{
y += sy;
if ((y1<y2) ? (y<=y2) : (y>=y2))
{
if (reverseXY)
create_part(-1, y, x, c, v);
else
create_part(-1, x, y, c, v);
}
e -= 1.0f;
}
}
}
void Simulation::orbitalparts_get(int block1, int block2, int resblock1[], int resblock2[])
{
resblock1[0] = (block1&0x000000FF);
resblock1[1] = (block1&0x0000FF00)>>8;
resblock1[2] = (block1&0x00FF0000)>>16;
resblock1[3] = (block1&0xFF000000)>>24;
resblock2[0] = (block2&0x000000FF);
resblock2[1] = (block2&0x0000FF00)>>8;
resblock2[2] = (block2&0x00FF0000)>>16;
resblock2[3] = (block2&0xFF000000)>>24;
}
void Simulation::orbitalparts_set(int *block1, int *block2, int resblock1[], int resblock2[])
{
int block1tmp = 0;
int block2tmp = 0;
block1tmp = (resblock1[0]&0xFF);
block1tmp |= (resblock1[1]&0xFF)<<8;
block1tmp |= (resblock1[2]&0xFF)<<16;
block1tmp |= (resblock1[3]&0xFF)<<24;
block2tmp = (resblock2[0]&0xFF);
block2tmp |= (resblock2[1]&0xFF)<<8;
block2tmp |= (resblock2[2]&0xFF)<<16;
block2tmp |= (resblock2[3]&0xFF)<<24;
*block1 = block1tmp;
*block2 = block2tmp;
}
inline int Simulation::is_wire(int x, int y)
{
return bmap[y][x]==WL_DETECT || bmap[y][x]==WL_EWALL || bmap[y][x]==WL_ALLOWLIQUID || bmap[y][x]==WL_WALLELEC || bmap[y][x]==WL_ALLOWALLELEC || bmap[y][x]==WL_EHOLE || bmap[y][x]==WL_STASIS;
}
inline int Simulation::is_wire_off(int x, int y)
{
return (bmap[y][x]==WL_DETECT || bmap[y][x]==WL_EWALL || bmap[y][x]==WL_ALLOWLIQUID || bmap[y][x]==WL_WALLELEC || bmap[y][x]==WL_ALLOWALLELEC || bmap[y][x]==WL_EHOLE || bmap[y][x]==WL_STASIS) && emap[y][x]<8;
}
// implement __builtin_ctz and __builtin_clz on msvc
#ifdef _MSC_VER
unsigned msvc_ctz(unsigned a)
{
unsigned long i;
_BitScanForward(&i, a);
return i;
}
unsigned msvc_clz(unsigned a)
{
unsigned long i;
_BitScanReverse(&i, a);
return 31 - i;
}
#define __builtin_ctz msvc_ctz
#define __builtin_clz msvc_clz
#endif
int Simulation::get_wavelength_bin(int *wm)
{
int i, w0, wM, r;
if (!(*wm & 0x3FFFFFFF))
return -1;
#if defined(__GNUC__) || defined(_MSVC_VER)
w0 = __builtin_ctz(*wm | 0xC0000000);
wM = 31 - __builtin_clz(*wm & 0x3FFFFFFF);
#else
w0 = 30;
wM = 0;
for (i = 0; i < 30; i++)
if (*wm & (1<<i))
{
if (i < w0)
w0 = i;
if (i > wM)
wM = i;
}
#endif
if (wM - w0 < 5)
return wM + w0;
r = rng.gen();
i = (r >> 1) % (wM-w0-4);
i += w0;
if (r & 1)
{
*wm &= 0x1F << i;
return (i + 2) * 2;
}
else
{
*wm &= 0xF << i;
return (i + 2) * 2 - 1;
}
}
void Simulation::set_emap(int x, int y)
{
int x1, x2;
if (!is_wire_off(x, y))
return;
// go left as far as possible
x1 = x2 = x;
while (x1>0)
{
if (!is_wire_off(x1-1, y))
break;
x1--;
}
while (x2<XCELLS-1)
{
if (!is_wire_off(x2+1, y))
break;
x2++;
}
// fill span
for (x=x1; x<=x2; x++)
emap[y][x] = 16;
// fill children
if (y>1 && x1==x2 &&
is_wire(x1-1, y-1) && is_wire(x1, y-1) && is_wire(x1+1, y-1) &&
!is_wire(x1-1, y-2) && is_wire(x1, y-2) && !is_wire(x1+1, y-2))
set_emap(x1, y-2);
else if (y>0)
for (x=x1; x<=x2; x++)
if (is_wire_off(x, y-1))
{
if (x==x1 || x==x2 || y>=YCELLS-1 ||
is_wire(x-1, y-1) || is_wire(x+1, y-1) ||
is_wire(x-1, y+1) || !is_wire(x, y+1) || is_wire(x+1, y+1))
set_emap(x, y-1);
}
if (y<YCELLS-2 && x1==x2 &&
is_wire(x1-1, y+1) && is_wire(x1, y+1) && is_wire(x1+1, y+1) &&
!is_wire(x1-1, y+2) && is_wire(x1, y+2) && !is_wire(x1+1, y+2))
set_emap(x1, y+2);
else if (y<YCELLS-1)
for (x=x1; x<=x2; x++)
if (is_wire_off(x, y+1))
{
if (x==x1 || x==x2 || y<0 ||
is_wire(x-1, y+1) || is_wire(x+1, y+1) ||
is_wire(x-1, y-1) || !is_wire(x, y-1) || is_wire(x+1, y-1))
set_emap(x, y+1);
}
}
int Simulation::parts_avg(int ci, int ni,int t)
{
if (t==PT_INSL)//to keep electronics working
{
int pmr = pmap[((int)(parts[ci].y+0.5f) + (int)(parts[ni].y+0.5f))/2][((int)(parts[ci].x+0.5f) + (int)(parts[ni].x+0.5f))/2];
if (pmr)
return parts[ID(pmr)].type;
else
return PT_NONE;
}
else
{
int pmr2 = pmap[(int)((parts[ci].y + parts[ni].y)/2+0.5f)][(int)((parts[ci].x + parts[ni].x)/2+0.5f)];//seems to be more accurate.
if (pmr2)
{
if (parts[ID(pmr2)].type==t)
return t;
}
else
return PT_NONE;
}
return PT_NONE;
}
void Simulation::clear_sim(void)
{
ensureDeterminism = false;
frameCount = 0;
debug_nextToUpdate = 0;
debug_mostRecentlyUpdated = -1;
emp_decor = 0;
emp_trigger_count = 0;
signs.clear();
memset(bmap, 0, sizeof(bmap));
memset(emap, 0, sizeof(emap));
memset(parts, 0, sizeof(Particle)*NPART);
for (int i = 0; i < NPART-1; i++)
parts[i].life = i+1;
parts[NPART-1].life = -1;
pfree = 0;
parts_lastActiveIndex = 0;
memset(pmap, 0, sizeof(pmap));
memset(fvx, 0, sizeof(fvx));
memset(fvy, 0, sizeof(fvy));
memset(photons, 0, sizeof(photons));
memset(wireless, 0, sizeof(wireless));
memset(gol, 0, sizeof(gol));
memset(portalp, 0, sizeof(portalp));
memset(fighters, 0, sizeof(fighters));
memset(&player, 0, sizeof(player));
memset(&player2, 0, sizeof(player2));
std::fill(elementCount, elementCount+PT_NUM, 0);
elementRecount = true;
fighcount = 0;
player.spwn = 0;
player.spawnID = -1;
player.rocketBoots = false;
player.fan = false;
player2.spwn = 0;
player2.spawnID = -1;
player2.rocketBoots = false;
player2.fan = false;
//memset(pers_bg, 0, WINDOWW*YRES*PIXELSIZE);
//memset(fire_r, 0, sizeof(fire_r));
//memset(fire_g, 0, sizeof(fire_g));
//memset(fire_b, 0, sizeof(fire_b));
//if(gravmask)
//memset(gravmask, 0xFFFFFFFF, NCELL*sizeof(unsigned));
if(grav)
grav->Clear();
if(air)
{
air->Clear();
air->ClearAirH();
}
SetEdgeMode(edgeMode);
}
bool Simulation::IsWallBlocking(int x, int y, int type)
{
if (bmap[y/CELL][x/CELL])
{
int wall = bmap[y/CELL][x/CELL];
if (wall == WL_ALLOWGAS && !(elements[type].Properties&TYPE_GAS))
return true;
else if (wall == WL_ALLOWENERGY && !(elements[type].Properties&TYPE_ENERGY))
return true;
else if (wall == WL_ALLOWLIQUID && !(elements[type].Properties&TYPE_LIQUID))
return true;
else if (wall == WL_ALLOWPOWDER && !(elements[type].Properties&TYPE_PART))
return true;
else if (wall == WL_ALLOWAIR || wall == WL_WALL || wall == WL_WALLELEC)
return true;
else if (wall == WL_EWALL && !emap[y/CELL][x/CELL])
return true;
else if (wall == WL_DETECT && (elements[type].Properties&TYPE_SOLID))
return true;
}
return false;
}
void Simulation::init_can_move()
{
int movingType, destinationType;
// can_move[moving type][type at destination]
// 0 = No move/Bounce
// 1 = Swap
// 2 = Both particles occupy the same space.
// 3 = Varies, go run some extra checks
//particles that don't exist shouldn't move...
for (destinationType = 0; destinationType < PT_NUM; destinationType++)
can_move[0][destinationType] = 0;
//initialize everything else to swapping by default
for (movingType = 1; movingType < PT_NUM; movingType++)
for (destinationType = 0; destinationType < PT_NUM; destinationType++)
can_move[movingType][destinationType] = 1;
//photons go through everything by default
for (destinationType = 1; destinationType < PT_NUM; destinationType++)
can_move[PT_PHOT][destinationType] = 2;
for (movingType = 1; movingType < PT_NUM; movingType++)
{
for (destinationType = 1; destinationType < PT_NUM; destinationType++)
{
//weight check, also prevents particles of same type displacing each other
if (elements[movingType].Weight <= elements[destinationType].Weight || destinationType == PT_GEL)
can_move[movingType][destinationType] = 0;
//other checks for NEUT and energy particles
if (movingType == PT_NEUT && (elements[destinationType].Properties&PROP_NEUTPASS))
can_move[movingType][destinationType] = 2;
if (movingType == PT_NEUT && (elements[destinationType].Properties&PROP_NEUTABSORB))
can_move[movingType][destinationType] = 1;
if (movingType == PT_NEUT && (elements[destinationType].Properties&PROP_NEUTPENETRATE))
can_move[movingType][destinationType] = 1;
if (destinationType == PT_NEUT && (elements[movingType].Properties&PROP_NEUTPENETRATE))
can_move[movingType][destinationType] = 0;
if ((elements[movingType].Properties&TYPE_ENERGY) && (elements[destinationType].Properties&TYPE_ENERGY))
can_move[movingType][destinationType] = 2;
}
}
for (destinationType = 0; destinationType < PT_NUM; destinationType++)
{
//set what stickmen can move through
int stkm_move = 0;
if (elements[destinationType].Properties & (TYPE_LIQUID | TYPE_GAS))
stkm_move = 2;
if (!destinationType || destinationType == PT_PRTO || destinationType == PT_SPAWN || destinationType == PT_SPAWN2)
stkm_move = 2;
can_move[PT_STKM][destinationType] = stkm_move;
can_move[PT_STKM2][destinationType] = stkm_move;
can_move[PT_FIGH][destinationType] = stkm_move;
//spark shouldn't move
can_move[PT_SPRK][destinationType] = 0;
}
for (movingType = 1; movingType < PT_NUM; movingType++)
{
//everything "swaps" with VACU and BHOL to make them eat things
can_move[movingType][PT_BHOL] = 1;
can_move[movingType][PT_NBHL] = 1;
//nothing goes through stickmen
can_move[movingType][PT_STKM] = 0;
can_move[movingType][PT_STKM2] = 0;
can_move[movingType][PT_FIGH] = 0;
//INVS behaviour varies with pressure
can_move[movingType][PT_INVIS] = 3;
//stop CNCT from being displaced by other particles
can_move[movingType][PT_CNCT] = 0;
//VOID and PVOD behaviour varies with powered state and ctype
can_move[movingType][PT_PVOD] = 3;
can_move[movingType][PT_VOID] = 3;
//nothing moves through EMBR (not sure why, but it's killed when it touches anything)
can_move[movingType][PT_EMBR] = 0;
can_move[PT_EMBR][movingType] = 0;
//Energy particles move through VIBR and BVBR, so it can absorb them
if (elements[movingType].Properties & TYPE_ENERGY)
{
can_move[movingType][PT_VIBR] = 1;
can_move[movingType][PT_BVBR] = 1;
}
//SAWD cannot be displaced by other powders
if (elements[movingType].Properties & TYPE_PART)
can_move[movingType][PT_SAWD] = 0;
}
//a list of lots of things PHOT can move through
// TODO: replace with property
for (destinationType = 0; destinationType < PT_NUM; destinationType++)
{
if (destinationType == PT_GLAS || destinationType == PT_PHOT || destinationType == PT_FILT || destinationType == PT_INVIS
|| destinationType == PT_CLNE || destinationType == PT_PCLN || destinationType == PT_BCLN || destinationType == PT_PBCN
|| destinationType == PT_WATR || destinationType == PT_DSTW || destinationType == PT_SLTW || destinationType == PT_GLOW
|| destinationType == PT_ISOZ || destinationType == PT_ISZS || destinationType == PT_QRTZ || destinationType == PT_PQRT
|| destinationType == PT_H2 || destinationType == PT_BGLA || destinationType == PT_C5)
can_move[PT_PHOT][destinationType] = 2;
if (destinationType != PT_DMND && destinationType != PT_INSL && destinationType != PT_VOID && destinationType != PT_PVOD && destinationType != PT_VIBR && destinationType != PT_BVBR && destinationType != PT_PRTI && destinationType != PT_PRTO)
{
can_move[PT_PROT][destinationType] = 2;
can_move[PT_GRVT][destinationType] = 2;
}
}
//other special cases that weren't covered above
can_move[PT_DEST][PT_DMND] = 0;
can_move[PT_DEST][PT_CLNE] = 0;
can_move[PT_DEST][PT_PCLN] = 0;
can_move[PT_DEST][PT_BCLN] = 0;
can_move[PT_DEST][PT_PBCN] = 0;
can_move[PT_DEST][PT_ROCK] = 0;
can_move[PT_NEUT][PT_INVIS] = 2;
can_move[PT_ELEC][PT_LCRY] = 2;
can_move[PT_ELEC][PT_EXOT] = 2;
can_move[PT_ELEC][PT_GLOW] = 2;
can_move[PT_PHOT][PT_LCRY] = 3; //varies according to LCRY life
can_move[PT_PHOT][PT_GPMP] = 3;
can_move[PT_PHOT][PT_BIZR] = 2;
can_move[PT_ELEC][PT_BIZR] = 2;
can_move[PT_PHOT][PT_BIZRG] = 2;
can_move[PT_ELEC][PT_BIZRG] = 2;
can_move[PT_PHOT][PT_BIZRS] = 2;
can_move[PT_ELEC][PT_BIZRS] = 2;
can_move[PT_BIZR][PT_FILT] = 2;
can_move[PT_BIZRG][PT_FILT] = 2;
can_move[PT_ANAR][PT_WHOL] = 1; //WHOL eats ANAR
can_move[PT_ANAR][PT_NWHL] = 1;
can_move[PT_ELEC][PT_DEUT] = 1;
can_move[PT_THDR][PT_THDR] = 2;
can_move[PT_EMBR][PT_EMBR] = 2;
can_move[PT_TRON][PT_SWCH] = 3;
can_move[PT_SOAP][PT_OIL] = 0;
can_move[PT_OIL][PT_SOAP] = 1;
}
/*
RETURN-value explanation
1 = Swap
0 = No move/Bounce
2 = Both particles occupy the same space.
*/
int Simulation::eval_move(int pt, int nx, int ny, unsigned *rr)
{
unsigned r;
int result;
if (nx<0 || ny<0 || nx>=XRES || ny>=YRES)
return 0;
r = pmap[ny][nx];
if (r)
r = (r&~PMAPMASK) | parts[ID(r)].type;
if (rr)
*rr = r;
if (pt>=PT_NUM || TYP(r)>=PT_NUM)
return 0;
result = can_move[pt][TYP(r)];
if (result == 3)
{
switch (TYP(r))
{
case PT_LCRY:
if (pt==PT_PHOT)
result = (parts[ID(r)].life > 5)? 2 : 0;
break;
case PT_GPMP:
if (pt == PT_PHOT)
result = (parts[ID(r)].life < 10) ? 2 : 0;
break;
case PT_INVIS:
{
float pressureResistance = 0.0f;
if (parts[ID(r)].tmp > 0)
pressureResistance = (float)parts[ID(r)].tmp;
else
pressureResistance = 4.0f;
if (pv[ny/CELL][nx/CELL] < -pressureResistance || pv[ny/CELL][nx/CELL] > pressureResistance)
result = 2;
else
result = 0;
break;
}
case PT_PVOD:
if (parts[ID(r)].life == 10)
{
if (!parts[ID(r)].ctype || (parts[ID(r)].ctype==pt)!=(parts[ID(r)].tmp&1))
result = 1;
else
result = 0;
}
else result = 0;
break;
case PT_VOID:
if (!parts[ID(r)].ctype || (parts[ID(r)].ctype==pt)!=(parts[ID(r)].tmp&1))
result = 1;
else
result = 0;
break;
case PT_SWCH:
if (pt == PT_TRON)
{
if (parts[ID(r)].life >= 10)
return 2;
else
return 0;
}
break;
default:
// This should never happen
// If it were to happen, try_move would interpret a 3 as a 1
result = 1;
}
}
if (bmap[ny/CELL][nx/CELL])
{
if (IsWallBlocking(nx, ny, pt))
return 0;
if (bmap[ny/CELL][nx/CELL]==WL_EHOLE && !emap[ny/CELL][nx/CELL] && !(elements[pt].Properties&TYPE_SOLID) && !(elements[TYP(r)].Properties&TYPE_SOLID))
return 2;
}
return result;
}
int Simulation::try_move(int i, int x, int y, int nx, int ny)
{
unsigned r = 0, e;
if (x==nx && y==ny)
return 1;
if (nx<0 || ny<0 || nx>=XRES || ny>=YRES)
return 1;
e = eval_move(parts[i].type, nx, ny, &r);
/* half-silvered mirror */
if (!e && parts[i].type==PT_PHOT && ((TYP(r)==PT_BMTL && rng.chance(1, 2)) || TYP(pmap[y][x])==PT_BMTL))
e = 2;
if (!e) //if no movement
{
int rt = TYP(r);
if (rt == PT_WOOD)
{
float vel = std::sqrt(std::pow(parts[i].vx, 2) + std::pow(parts[i].vy, 2));
if (vel > 5)
part_change_type(ID(r), nx, ny, PT_SAWD);
}
if (!(elements[parts[i].type].Properties & TYPE_ENERGY))
return 0;
if (!legacy_enable && parts[i].type==PT_PHOT && r)//PHOT heat conduction
{
if (rt == PT_COAL || rt == PT_BCOL)
parts[ID(r)].temp = parts[i].temp;
if (rt < PT_NUM && elements[rt].HeatConduct && (rt!=PT_HSWC||parts[ID(r)].life==10) && rt!=PT_FILT)
parts[i].temp = parts[ID(r)].temp = restrict_flt((parts[ID(r)].temp+parts[i].temp)/2, MIN_TEMP, MAX_TEMP);
}
else if ((parts[i].type==PT_NEUT || parts[i].type==PT_ELEC) && (rt==PT_CLNE || rt==PT_PCLN || rt==PT_BCLN || rt==PT_PBCN))
{
if (!parts[ID(r)].ctype)
parts[ID(r)].ctype = parts[i].type;
}
if (rt==PT_PRTI && (elements[parts[i].type].Properties & TYPE_ENERGY))
{
int nnx, count;
for (count=0; count<8; count++)
{
if (isign(x-nx)==isign(portal_rx[count]) && isign(y-ny)==isign(portal_ry[count]))
break;
}
count = count%8;
parts[ID(r)].tmp = (int)((parts[ID(r)].temp-73.15f)/100+1);
if (parts[ID(r)].tmp>=CHANNELS) parts[ID(r)].tmp = CHANNELS-1;
else if (parts[ID(r)].tmp<0) parts[ID(r)].tmp = 0;
for ( nnx=0; nnx<80; nnx++)
if (!portalp[parts[ID(r)].tmp][count][nnx].type)
{
portalp[parts[ID(r)].tmp][count][nnx] = parts[i];
parts[i].type=PT_NONE;
break;
}
}
return 0;
}
int Element_FILT_interactWavelengths(Simulation *sim, Particle* cpart, int origWl);
if (e == 2) //if occupy same space
{
switch (parts[i].type)
{
case PT_PHOT:
{
switch (TYP(r))
{
case PT_GLOW:
if (!parts[ID(r)].life && rng.chance(29, 30))
{
parts[ID(r)].life = 120;
create_gain_photon(i);
}
break;
case PT_FILT:
parts[i].ctype = Element_FILT_interactWavelengths(this, &parts[ID(r)], parts[i].ctype);
break;
case PT_C5:
if (parts[ID(r)].life > 0 && (parts[ID(r)].ctype & parts[i].ctype & 0xFFFFFFC0))
{
float vx = ((parts[ID(r)].tmp << 16) >> 16) / 255.0f;
float vy = (parts[ID(r)].tmp >> 16) / 255.0f;
float vn = parts[i].vx * parts[i].vx + parts[i].vy * parts[i].vy;
// if the resulting velocity would be 0, that would cause division by 0 inside the else
// shoot the photon off at a 90 degree angle instead (probably particle order dependent)
if (parts[i].vx + vx == 0 && parts[i].vy + vy == 0)
{
parts[i].vx = vy;
parts[i].vy = -vx;
}
else
{
parts[i].ctype = (parts[ID(r)].ctype & parts[i].ctype) >> 6;
// add momentum of photons to each other
parts[i].vx += vx;
parts[i].vy += vy;
// normalize velocity to original value
vn /= parts[i].vx * parts[i].vx + parts[i].vy * parts[i].vy;
vn = sqrtf(vn);
parts[i].vx *= vn;
parts[i].vy *= vn;
}
parts[ID(r)].life = 0;
parts[ID(r)].ctype = 0;
}
else if(!parts[ID(r)].ctype && parts[i].ctype & 0xFFFFFFC0)
{
parts[ID(r)].life = 1;
parts[ID(r)].ctype = parts[i].ctype;
parts[ID(r)].tmp = (0xFFFF & (int)(parts[i].vx * 255.0f)) | (0xFFFF0000 & (int)(parts[i].vy * 16711680.0f));
parts[ID(r)].tmp2 = (0xFFFF & (int)((parts[i].x - x) * 255.0f)) | (0xFFFF0000 & (int)((parts[i].y - y) * 16711680.0f));
kill_part(i);
}
break;
case PT_INVIS:
{
float pressureResistance = 0.0f;
pressureResistance = (parts[ID(r)].tmp > 0) ? (float)parts[ID(r)].tmp : 4.0f;
if (pv[ny/CELL][nx/CELL] >= -pressureResistance && pv[ny/CELL][nx/CELL] <= pressureResistance)
{
part_change_type(i,x,y,PT_NEUT);
parts[i].ctype = 0;
}
break;
}
case PT_BIZR:
case PT_BIZRG:
case PT_BIZRS:
part_change_type(i, x, y, PT_ELEC);
parts[i].ctype = 0;
break;
case PT_H2:
if (!(parts[i].tmp&0x1))
{
part_change_type(i, x, y, PT_PROT);
parts[i].ctype = 0;
parts[i].tmp2 = 0x1;
create_part(ID(r), x, y, PT_ELEC);
return 1;
}
break;
case PT_GPMP:
if (parts[ID(r)].life == 0)
{
part_change_type(i, x, y, PT_GRVT);
parts[i].tmp = int(parts[ID(r)].temp - 273.15f);
}
break;
}
break;
}
case PT_NEUT:
if (TYP(r) == PT_GLAS || TYP(r) == PT_BGLA)
if (rng.chance(9, 10))
create_cherenkov_photon(i);
break;
case PT_ELEC:
if (TYP(r) == PT_GLOW)
{
part_change_type(i, x, y, PT_PHOT);
parts[i].ctype = 0x3FFFFFFF;
}
break;
case PT_PROT:
if (TYP(r) == PT_INVIS)
part_change_type(i, x, y, PT_NEUT);
break;
case PT_BIZR:
case PT_BIZRG:
if (TYP(r) == PT_FILT)
parts[i].ctype = Element_FILT_interactWavelengths(this, &parts[ID(r)], parts[i].ctype);
break;
}
return 1;
}
//else e=1 , we are trying to swap the particles, return 0 no swap/move, 1 is still overlap/move, because the swap takes place later
switch (TYP(r))
{
case PT_VOID:
case PT_PVOD:
// this is where void eats particles
// void ctype already checked in eval_move
kill_part(i);
return 0;
case PT_BHOL:
case PT_NBHL:
// this is where blackhole eats particles
if (!legacy_enable)
{
parts[ID(r)].temp = restrict_flt(parts[ID(r)].temp+parts[i].temp/2, MIN_TEMP, MAX_TEMP);//3.0f;
}
kill_part(i);
return 0;
case PT_WHOL:
case PT_NWHL:
// whitehole eats anar
if (parts[i].type == PT_ANAR)
{
if (!legacy_enable)
{
parts[ID(r)].temp = restrict_flt(parts[ID(r)].temp - (MAX_TEMP-parts[i].temp)/2, MIN_TEMP, MAX_TEMP);
}
kill_part(i);
return 0;
}
break;
case PT_DEUT:
if (parts[i].type == PT_ELEC)
{
if(parts[ID(r)].life < 6000)
parts[ID(r)].life += 1;
parts[ID(r)].temp = 0;
kill_part(i);
return 0;
}
break;
case PT_VIBR:
case PT_BVBR:
if ((elements[parts[i].type].Properties & TYPE_ENERGY))
{
parts[ID(r)].tmp += 20;
kill_part(i);
return 0;
}
break;
// SOAP slowly floats up inside OIL
case PT_SOAP:
if (parts[i].type == PT_OIL)
{
if (rng.chance(19, 20) || std::abs(parts[i].x - nx) > 3 || std::abs(parts[i].y - ny) > 3)
return 0;
}
break;
}
switch (parts[i].type)
{
case PT_NEUT:
if (elements[TYP(r)].Properties & PROP_NEUTABSORB)
{
kill_part(i);
return 0;
}
break;
case PT_CNCT:
{
float cnctGravX, cnctGravY; // Calculate offset from gravity
GetGravityField(x, y, elements[PT_CNCT].Gravity, elements[PT_CNCT].Gravity, cnctGravX, cnctGravY);
int offsetX = 0, offsetY = 0;
if (cnctGravX > 0.0f) offsetX++;
else if (cnctGravX < 0.0f) offsetX--;
if (cnctGravY > 0.0f) offsetY++;
else if (cnctGravY < 0.0f) offsetY--;
if ((offsetX != 0) != (offsetY != 0) && // Is this a different position (avoid diagonals, doesn't work well)
((nx - x) * offsetX > 0 || (ny - y) * offsetY > 0) && // Is the destination particle below the moving particle
(TYP(pmap[y+offsetY][x+offsetX]) == PT_CNCT || TYP(pmap[y+offsetY][x+offsetX]) == PT_ROCK)) //check below CNCT for another CNCT or ROCK
return 0;
}
break;
case PT_GBMB:
if (parts[i].life > 0)
return 0;
break;
}
if ((bmap[y/CELL][x/CELL]==WL_EHOLE && !emap[y/CELL][x/CELL]) && !(bmap[ny/CELL][nx/CELL]==WL_EHOLE && !emap[ny/CELL][nx/CELL]))
return 0;
int ri = ID(r); //ri is the particle number at r (pmap[ny][nx])
if (r)//the swap part, if we make it this far, swap
{
if (parts[i].type==PT_NEUT) {
// target material is NEUTPENETRATE, meaning it gets moved around when neutron passes
unsigned s = pmap[y][x];
if (s && !(elements[TYP(s)].Properties&PROP_NEUTPENETRATE))
return 1; // if the element currently underneath neutron isn't NEUTPENETRATE, don't move anything except the neutron
// if nothing is currently underneath neutron, only move target particle
if(bmap[y/CELL][x/CELL] == WL_ALLOWENERGY)
return 1; // do not drag target particle into an energy only wall
if (s)
{
pmap[ny][nx] = (s&~PMAPMASK)|parts[ID(s)].type;
parts[ID(s)].x = float(nx);
parts[ID(s)].y = float(ny);
}
else
pmap[ny][nx] = 0;
parts[ri].x = float(x);
parts[ri].y = float(y);
pmap[y][x] = PMAP(ri, parts[ri].type);
return 1;
}
if (ID(pmap[ny][nx]) == ri)
pmap[ny][nx] = 0;
parts[ri].x += float(x - nx);
parts[ri].y += float(y - ny);
int rx = int(parts[ri].x + 0.5f);
int ry = int(parts[ri].y + 0.5f);
// This check will never fail unless the pmap array has already been corrupted via another bug
// In that case, r's position is inaccurate (not actually at nx/ny) and rx/ry may be out of bounds
if (InBounds(rx, ry))
pmap[ry][rx] = PMAP(ri, parts[ri].type);
}
return 1;
}
// try to move particle, and if successful update pmap and parts[i].x,y
int Simulation::do_move(int i, int x, int y, float nxf, float nyf)
{
int nx = (int)(nxf+0.5f), ny = (int)(nyf+0.5f), result;
if (edgeMode == 2)
{
bool x_ok = (nx >= CELL && nx < XRES-CELL);
bool y_ok = (ny >= CELL && ny < YRES-CELL);
if (!x_ok)
nxf = remainder_p(nxf-CELL+.5f, XRES-CELL*2.0f)+CELL-.5f;
if (!y_ok)
nyf = remainder_p(nyf-CELL+.5f, YRES-CELL*2.0f)+CELL-.5f;
nx = (int)(nxf+0.5f);
ny = (int)(nyf+0.5f);
/*if (!x_ok || !y_ok)
{
//make sure there isn't something blocking it on the other side
//only needed if this if statement is moved after the try_move (like my mod)
//if (!eval_move(t, nx, ny, NULL) || (t == PT_PHOT && pmap[ny][nx]))
// return -1;
}*/
}
if (parts[i].type == PT_NONE)
return 0;
result = try_move(i, x, y, nx, ny);
if (result)
{
if (!move(i, x, y, nxf, nyf))
return -1;
}
return result;
}
bool Simulation::move(int i, int x, int y, float nxf, float nyf)
{
int nx = (int)(nxf+0.5f), ny = (int)(nyf+0.5f);
int t = parts[i].type;
parts[i].x = nxf;
parts[i].y = nyf;
if (ny != y || nx != x)
{
if (ID(pmap[y][x]) == i)
pmap[y][x] = 0;
if (ID(photons[y][x]) == i)
photons[y][x] = 0;
// kill_part if particle is out of bounds
if (nx < CELL || nx >= XRES - CELL || ny < CELL || ny >= YRES - CELL)
{
kill_part(i);
return false;
}
if (elements[t].Properties & TYPE_ENERGY)
photons[ny][nx] = PMAP(i, t);
else if (t)
pmap[ny][nx] = PMAP(i, t);
}
return true;
}
void Simulation::photoelectric_effect(int nx, int ny)//create sparks from PHOT when hitting PSCN and NSCN
{
unsigned r = pmap[ny][nx];
if (TYP(r) == PT_PSCN && !parts[ID(r)].life)
{
if (TYP(pmap[ny][nx-1]) == PT_NSCN || TYP(pmap[ny][nx+1]) == PT_NSCN ||
TYP(pmap[ny-1][nx]) == PT_NSCN || TYP(pmap[ny+1][nx]) == PT_NSCN)
{
parts[ID(r)].ctype = PT_PSCN;
part_change_type(ID(r), nx, ny, PT_SPRK);
parts[ID(r)].life = 4;
}
}
}
int Simulation::is_blocking(int t, int x, int y)
{
if (t & REFRACT) {
if (x<0 || y<0 || x>=XRES || y>=YRES)
return 0;
if (TYP(pmap[y][x]) == PT_GLAS || TYP(pmap[y][x]) == PT_BGLA)
return 1;
return 0;
}
return !eval_move(t, x, y, NULL);
}
int Simulation::is_boundary(int pt, int x, int y)
{
if (!is_blocking(pt,x,y))
return 0;
if (is_blocking(pt,x,y-1) && is_blocking(pt,x,y+1) && is_blocking(pt,x-1,y) && is_blocking(pt,x+1,y))
return 0;
return 1;
}
int Simulation::find_next_boundary(int pt, int *x, int *y, int dm, int *em, bool reverse)
{
static int dx[8] = {1,1,0,-1,-1,-1,0,1};
static int dy[8] = {0,1,1,1,0,-1,-1,-1};
static int de[8] = {0x83,0x07,0x0E,0x1C,0x38,0x70,0xE0,0xC1};
if (*x <= 0 || *x >= XRES-1 || *y <= 0 || *y >= YRES-1)
{
return 0;
}
if (*em != -1)
{
dm &= de[*em];
}
unsigned int mask = 0;
for (int i = 0; i < 8; ++i)
{
if (is_blocking(pt, *x + dx[i], *y + dy[i]))
{
mask |= (1U << i);
}
}
for (int i = 0; i < 8; ++i)
{
int n = (i + (reverse ? 1 : -1)) & 7;
if (((dm & mask & (1U << i))) && !(mask & (1U << n)))
{
*x += dx[i];
*y += dy[i];
*em = i;
return 1;
}
}
return 0;
}
int Simulation::get_normal(int pt, int x, int y, float dx, float dy, float *nx, float *ny)
{
int ldm, rdm, lm, rm;
int lx, ly, lv, rx, ry, rv;
int i, j;
float r, ex, ey;
if (!dx && !dy)
return 0;
if (!is_boundary(pt, x, y))
return 0;
ldm = 0xFF;
rdm = 0xFF;
lx = rx = x;
ly = ry = y;
lv = rv = 1;
lm = rm = -1;
j = 0;
for (i=0; i<SURF_RANGE; i++) {
if (lv)
lv = find_next_boundary(pt, &lx, &ly, ldm, &lm, true);
if (rv)
rv = find_next_boundary(pt, &rx, &ry, rdm, &rm, false);
j += lv + rv;
if (!lv && !rv)
break;
}
if (j < NORMAL_MIN_EST)
return 0;
if ((lx == rx) && (ly == ry))
return 0;
ex = float(rx - lx);
ey = float(ry - ly);
r = 1.0f/hypot(ex, ey);
*nx = ey * r;
*ny = -ex * r;
return 1;
}
int Simulation::get_normal_interp(int pt, float x0, float y0, float dx, float dy, float *nx, float *ny)
{
int x, y, i;
dx /= NORMAL_FRAC;
dy /= NORMAL_FRAC;
for (i=0; i<NORMAL_INTERP; i++) {
x = (int)(x0 + 0.5f);
y = (int)(y0 + 0.5f);
if (x < 0 || y < 0 || x >= XRES || y >= YRES)
{
return 0;
}
if (is_boundary(pt, x, y))
break;
x0 += dx;
y0 += dy;
}
if (i >= NORMAL_INTERP)
return 0;
if (pt == PT_PHOT)
photoelectric_effect(x, y);
return get_normal(pt, x, y, dx, dy, nx, ny);
}
void Simulation::kill_part(int i)//kills particle number i
{
if (i < 0 || i >= NPART)
return;
int x = (int)(parts[i].x + 0.5f);
int y = (int)(parts[i].y + 0.5f);
int t = parts[i].type;
if (t && elements[t].ChangeType)
{
(*(elements[t].ChangeType))(this, i, x, y, t, PT_NONE);
}
if (x >= 0 && y >= 0 && x < XRES && y < YRES)
{
if (ID(pmap[y][x]) == i)
pmap[y][x] = 0;
else if (ID(photons[y][x]) == i)
photons[y][x] = 0;
}
// This shouldn't happen but ... you never know?
if (t == PT_NONE)
return;
elementCount[t]--;
parts[i].type = PT_NONE;
parts[i].life = pfree;
pfree = i;
}
// Changes the type of particle number i, to t. This also changes pmap at the same time
// Returns true if the particle was killed
bool Simulation::part_change_type(int i, int x, int y, int t)
{
if (x<0 || y<0 || x>=XRES || y>=YRES || i>=NPART || t<0 || t>=PT_NUM || !parts[i].type)
return false;
if (!elements[t].Enabled || t == PT_NONE)
{
kill_part(i);
return true;
}
if (elements[t].CreateAllowed)
{
if (!(*(elements[t].CreateAllowed))(this, i, x, y, t))
return false;
}
if (elements[parts[i].type].ChangeType)
(*(elements[parts[i].type].ChangeType))(this, i, x, y, parts[i].type, t);
if (elements[t].ChangeType)
(*(elements[t].ChangeType))(this, i, x, y, parts[i].type, t);
if (parts[i].type > 0 && parts[i].type < PT_NUM && elementCount[parts[i].type])
elementCount[parts[i].type]--;
elementCount[t]++;
parts[i].type = t;
if (elements[t].Properties & TYPE_ENERGY)
{
photons[y][x] = PMAP(i, t);
if (ID(pmap[y][x]) == i)
pmap[y][x] = 0;
}
else
{
pmap[y][x] = PMAP(i, t);
if (ID(photons[y][x]) == i)
photons[y][x] = 0;
}
return false;
}
//the function for creating a particle, use p=-1 for creating a new particle, -2 is from a brush, or a particle number to replace a particle.
//tv = Type (PMAPBITS bits) + Var (32-PMAPBITS bits), var is usually 0
int Simulation::create_part(int p, int x, int y, int t, int v)
{
int i, oldType = PT_NONE;
if (x<0 || y<0 || x>=XRES || y>=YRES || t<=0 || t>=PT_NUM || !elements[t].Enabled)
return -1;
if (t == PT_SPRK && !(p == -2 && elements[TYP(pmap[y][x])].CtypeDraw))
{
int type = TYP(pmap[y][x]);
int index = ID(pmap[y][x]);
if(type == PT_WIRE)
{
parts[index].ctype = PT_DUST;
return index;
}
if (!(type == PT_INST || (elements[type].Properties&PROP_CONDUCTS)) || parts[index].life!=0)
return -1;
if (p == -2 && type == PT_INST)
{
FloodINST(x, y);
return index;
}
parts[index].type = PT_SPRK;
parts[index].life = 4;
parts[index].ctype = type;
pmap[y][x] = (pmap[y][x]&~PMAPMASK) | PT_SPRK;
if (parts[index].temp+10.0f < 673.0f && !legacy_enable && (type==PT_METL || type == PT_BMTL || type == PT_BRMT || type == PT_PSCN || type == PT_NSCN || type == PT_ETRD || type == PT_NBLE || type == PT_IRON))
parts[index].temp = parts[index].temp+10.0f;
return index;
}
if (p == -2)
{
if (pmap[y][x])
{
int drawOn = TYP(pmap[y][x]);
if (elements[drawOn].CtypeDraw)
elements[drawOn].CtypeDraw(this, ID(pmap[y][x]), t, v);
return -1;
}
else if (IsWallBlocking(x, y, t))
return -1;
else if (photons[y][x] && (elements[t].Properties & TYPE_ENERGY))
return -1;
}
if (elements[t].CreateAllowed)
{
if (!(*(elements[t].CreateAllowed))(this, p, x, y, t))
return -1;
}
if (p == -1)//creating from anything but brush
{
// If there is a particle, only allow creation if the new particle can occupy the same space as the existing particle
// If there isn't a particle but there is a wall, check whether the new particle is allowed to be in it
// (not "!=2" for wall check because eval_move returns 1 for moving into empty space)
// If there's no particle and no wall, assume creation is allowed
if (pmap[y][x] ? (eval_move(t, x, y, NULL) != 2) : (bmap[y/CELL][x/CELL] && eval_move(t, x, y, NULL) == 0))
{
return -1;
}
if (pfree == -1)
return -1;
i = pfree;
pfree = parts[i].life;
}
else if (p == -2)//creating from brush
{
if (pfree == -1)
return -1;
i = pfree;
pfree = parts[i].life;
}
else if (p == -3)//skip pmap checks, e.g. for sing explosion
{
if (pfree == -1)
return -1;
i = pfree;
pfree = parts[i].life;
}
else
{
int oldX = (int)(parts[p].x + 0.5f);
int oldY = (int)(parts[p].y + 0.5f);
if (ID(pmap[oldY][oldX]) == p)
pmap[oldY][oldX] = 0;
if (ID(photons[oldY][oldX]) == p)
photons[oldY][oldX] = 0;
oldType = parts[p].type;
if (elements[oldType].ChangeType)
(*(elements[oldType].ChangeType))(this, p, oldX, oldY, oldType, t);
if (oldType)
elementCount[oldType]--;
i = p;
}
if (i>parts_lastActiveIndex) parts_lastActiveIndex = i;
parts[i] = elements[t].DefaultProperties;
parts[i].type = t;
parts[i].x = (float)x;
parts[i].y = (float)y;
//and finally set the pmap/photon maps to the newly created particle
if (elements[t].Properties & TYPE_ENERGY)
photons[y][x] = PMAP(i, t);
else if (t!=PT_STKM && t!=PT_STKM2 && t!=PT_FIGH)
pmap[y][x] = PMAP(i, t);
//Fancy dust effects for powder types
if((elements[t].Properties & TYPE_PART) && pretty_powder)
{
int colr, colg, colb;
int sandcolourToUse = p == -2 ? sandcolour_interface : sandcolour;
RGB<uint8_t> colour = elements[t].Colour;
colr = colour.Red + int(sandcolourToUse * 1.3) + rng.between(-20, 20) + rng.between(-15, 15);
colg = colour.Green + int(sandcolourToUse * 1.3) + rng.between(-20, 20) + rng.between(-15, 15);
colb = colour.Blue + int(sandcolourToUse * 1.3) + rng.between(-20, 20) + rng.between(-15, 15);
colr = std::clamp(colr, 0, 255);
colg = std::clamp(colg, 0, 255);
colb = std::clamp(colb, 0, 255);
parts[i].dcolour = (rng.between(0, 149)<<24) | (colr<<16) | (colg<<8) | colb;
}
// Set non-static properties (such as randomly generated ones)
if (elements[t].Create)
(*(elements[t].Create))(this, i, x, y, t, v);
if (elements[t].ChangeType)
(*(elements[t].ChangeType))(this, i, x, y, oldType, t);
elementCount[t]++;
return i;
}
void Simulation::GetGravityField(int x, int y, float particleGrav, float newtonGrav, float & pGravX, float & pGravY)
{
switch (gravityMode)
{
default:
case 0: //normal, vertical gravity
pGravX = 0;
pGravY = particleGrav;
break;
case 1: //no gravity
pGravX = 0;
pGravY = 0;
break;
case 2: //radial gravity
{
pGravX = 0;
pGravY = 0;
auto dx = float(x - XCNTR);
auto dy = float(y - YCNTR);
if (dx || dy)
{
auto pGravD = 0.01f - hypotf(dx, dy);
pGravX = particleGrav * (dx / pGravD);
pGravY = particleGrav * (dy / pGravD);
}
}
break;
case 3: //custom gravity
pGravX = particleGrav * customGravityX;
pGravY = particleGrav * customGravityY;
break;
}
if (newtonGrav)
{
pGravX += newtonGrav*gravx[(y/CELL)*XCELLS+(x/CELL)];
pGravY += newtonGrav*gravy[(y/CELL)*XCELLS+(x/CELL)];
}
}
void Simulation::create_gain_photon(int pp)//photons from PHOT going through GLOW
{
float xx, yy;
int i, lr, temp_bin, nx, ny;
if (pfree == -1)
return;
i = pfree;
lr = 2*rng.between(0, 1) - 1; // -1 or 1
xx = parts[pp].x - lr*0.3*parts[pp].vy;
yy = parts[pp].y + lr*0.3*parts[pp].vx;
nx = (int)(xx + 0.5f);
ny = (int)(yy + 0.5f);
if (nx<0 || ny<0 || nx>=XRES || ny>=YRES)
return;
if (TYP(pmap[ny][nx]) != PT_GLOW)
return;
pfree = parts[i].life;
if (i>parts_lastActiveIndex) parts_lastActiveIndex = i;
parts[i].type = PT_PHOT;
parts[i].life = 680;
parts[i].x = xx;
parts[i].y = yy;
parts[i].vx = parts[pp].vx;
parts[i].vy = parts[pp].vy;
parts[i].temp = parts[ID(pmap[ny][nx])].temp;
parts[i].tmp = 0;
parts[i].tmp3 = 0;
parts[i].tmp4 = 0;
photons[ny][nx] = PMAP(i, PT_PHOT);
temp_bin = (int)((parts[i].temp-273.0f)*0.25f);
if (temp_bin < 0) temp_bin = 0;
if (temp_bin > 25) temp_bin = 25;
parts[i].ctype = 0x1F << temp_bin;
}
void Simulation::create_cherenkov_photon(int pp)//photons from NEUT going through GLAS
{
int i, lr, nx, ny;
float r;
if (pfree == -1)
return;
i = pfree;
nx = (int)(parts[pp].x + 0.5f);
ny = (int)(parts[pp].y + 0.5f);
if (TYP(pmap[ny][nx]) != PT_GLAS && TYP(pmap[ny][nx]) != PT_BGLA)
return;
if (hypotf(parts[pp].vx, parts[pp].vy) < 1.44f)
return;
pfree = parts[i].life;
if (i>parts_lastActiveIndex) parts_lastActiveIndex = i;
lr = rng.between(0, 1);
parts[i].type = PT_PHOT;
parts[i].ctype = 0x00000F80;
parts[i].life = 680;
parts[i].x = parts[pp].x;
parts[i].y = parts[pp].y;
parts[i].temp = parts[ID(pmap[ny][nx])].temp;
parts[i].tmp = 0;
parts[i].tmp3 = 0;
parts[i].tmp4 = 0;
photons[ny][nx] = PMAP(i, PT_PHOT);
if (lr) {
parts[i].vx = parts[pp].vx - 2.5f*parts[pp].vy;
parts[i].vy = parts[pp].vy + 2.5f*parts[pp].vx;
} else {
parts[i].vx = parts[pp].vx + 2.5f*parts[pp].vy;
parts[i].vy = parts[pp].vy - 2.5f*parts[pp].vx;
}
/* photons have speed of light. no discussion. */
r = 1.269 / hypotf(parts[i].vx, parts[i].vy);
parts[i].vx *= r;
parts[i].vy *= r;
}
void Simulation::delete_part(int x, int y)//calls kill_part with the particle located at x,y
{
unsigned i;
if (x<0 || y<0 || x>=XRES || y>=YRES)
return;
i = photons[y][x] ? photons[y][x] : pmap[y][x];
if (!i)
return;
kill_part(ID(i));
}
void Simulation::UpdateParticles(int start, int end)
{
int i, j, x, y, t, r, surround_space, s, rt, nt;
float mv, dx, dy, nrx, nry, dp, ctemph, ctempl, gravtot;
int fin_x, fin_y, clear_x, clear_y, stagnant;
float fin_xf, fin_yf, clear_xf, clear_yf;
float nn, ct1, ct2, swappage;
float pt = R_TEMP;
float c_heat = 0.0f;
int h_count = 0;
int surround[8];
int surround_hconduct[8];
bool transitionOccurred;
//the main particle loop function, goes over all particles.
for (i = start; i < end && i <= parts_lastActiveIndex; i++)
if (parts[i].type)
{
debug_mostRecentlyUpdated = i;
t = parts[i].type;
x = (int)(parts[i].x+0.5f);
y = (int)(parts[i].y+0.5f);
// Kill a particle off screen
if (x<CELL || y<CELL || x>=XRES-CELL || y>=YRES-CELL)
{
kill_part(i);
continue;
}
// Kill a particle in a wall where it isn't supposed to go
if (bmap[y/CELL][x/CELL] &&
(bmap[y/CELL][x/CELL]==WL_WALL ||
bmap[y/CELL][x/CELL]==WL_WALLELEC ||
bmap[y/CELL][x/CELL]==WL_ALLOWAIR ||
(bmap[y/CELL][x/CELL]==WL_DESTROYALL) ||
(bmap[y/CELL][x/CELL]==WL_ALLOWLIQUID && !(elements[t].Properties&TYPE_LIQUID)) ||
(bmap[y/CELL][x/CELL]==WL_ALLOWPOWDER && !(elements[t].Properties&TYPE_PART)) ||
(bmap[y/CELL][x/CELL]==WL_ALLOWGAS && !(elements[t].Properties&TYPE_GAS)) || //&& elements[t].Falldown!=0 && parts[i].type!=PT_FIRE && parts[i].type!=PT_SMKE && parts[i].type!=PT_CFLM) ||
(bmap[y/CELL][x/CELL]==WL_ALLOWENERGY && !(elements[t].Properties&TYPE_ENERGY)) ||
(bmap[y/CELL][x/CELL]==WL_EWALL && !emap[y/CELL][x/CELL])) && (t!=PT_STKM) && (t!=PT_STKM2) && (t!=PT_FIGH))
{
kill_part(i);
continue;
}
// Make sure that STASIS'd particles don't tick.
if (bmap[y/CELL][x/CELL] == WL_STASIS && emap[y/CELL][x/CELL]<8) {
continue;
}
if (bmap[y/CELL][x/CELL]==WL_DETECT && emap[y/CELL][x/CELL]<8)
set_emap(x/CELL, y/CELL);
//adding to velocity from the particle's velocity
vx[y/CELL][x/CELL] = vx[y/CELL][x/CELL]*elements[t].AirLoss + elements[t].AirDrag*parts[i].vx;
vy[y/CELL][x/CELL] = vy[y/CELL][x/CELL]*elements[t].AirLoss + elements[t].AirDrag*parts[i].vy;
if (elements[t].HotAir)
{
if (t==PT_GAS||t==PT_NBLE)
{
if (pv[y/CELL][x/CELL]<3.5f)
pv[y/CELL][x/CELL] += elements[t].HotAir*(3.5f-pv[y/CELL][x/CELL]);
if (y+CELL<YRES && pv[y/CELL+1][x/CELL]<3.5f)
pv[y/CELL+1][x/CELL] += elements[t].HotAir*(3.5f-pv[y/CELL+1][x/CELL]);
if (x+CELL<XRES)
{
if (pv[y/CELL][x/CELL+1]<3.5f)
pv[y/CELL][x/CELL+1] += elements[t].HotAir*(3.5f-pv[y/CELL][x/CELL+1]);
if (y+CELL<YRES && pv[y/CELL+1][x/CELL+1]<3.5f)
pv[y/CELL+1][x/CELL+1] += elements[t].HotAir*(3.5f-pv[y/CELL+1][x/CELL+1]);
}
}
else//add the hotair variable to the pressure map, like black hole, or white hole.
{
pv[y/CELL][x/CELL] += elements[t].HotAir;
if (y+CELL<YRES)
pv[y/CELL+1][x/CELL] += elements[t].HotAir;
if (x+CELL<XRES)
{
pv[y/CELL][x/CELL+1] += elements[t].HotAir;
if (y+CELL<YRES)
pv[y/CELL+1][x/CELL+1] += elements[t].HotAir;
}
}
}
float pGravX = 0, pGravY = 0;
if (!(elements[t].Properties & TYPE_SOLID) && (elements[t].Gravity || elements[t].NewtonianGravity))
{
GetGravityField(x, y, elements[t].Gravity, elements[t].NewtonianGravity, pGravX, pGravY);
}
//velocity updates for the particle
if (t != PT_SPNG || !(parts[i].flags&FLAG_MOVABLE))
{
parts[i].vx *= elements[t].Loss;
parts[i].vy *= elements[t].Loss;
}
//particle gets velocity from the vx and vy maps
parts[i].vx += elements[t].Advection*vx[y/CELL][x/CELL] + pGravX;
parts[i].vy += elements[t].Advection*vy[y/CELL][x/CELL] + pGravY;
if (elements[t].Diffusion)//the random diffusion that gasses have
{
#ifdef REALISTIC
//The magic number controls diffusion speed
parts[i].vx += 0.05*sqrtf(parts[i].temp)*elements[t].Diffusion*(2.0f*rng.uniform01()-1.0f);
parts[i].vy += 0.05*sqrtf(parts[i].temp)*elements[t].Diffusion*(2.0f*rng.uniform01()-1.0f);
#else
parts[i].vx += elements[t].Diffusion*(2.0f*rng.uniform01()-1.0f);
parts[i].vy += elements[t].Diffusion*(2.0f*rng.uniform01()-1.0f);
#endif
}
transitionOccurred = false;
j = surround_space = nt = 0;//if nt is greater than 1 after this, then there is a particle around the current particle, that is NOT the current particle's type, for water movement.
for (auto nx=-1; nx<2; nx++)
for (auto ny=-1; ny<2; ny++) {
if (nx||ny) {
surround[j] = r = pmap[y+ny][x+nx];
j++;
surround_space += (!TYP(r)); // count empty space
nt += (TYP(r)!=t); // count empty space and particles of different type
}
}
float gel_scale = 1.0f;
if (t==PT_GEL)
gel_scale = parts[i].tmp*2.55f;
if (!legacy_enable)
{
if ((elements[t].Properties&TYPE_LIQUID) && (t!=PT_GEL || gel_scale > (1 + rng.between(0, 254))))
{
float convGravX, convGravY;
GetGravityField(x, y, -2.0f, -2.0f, convGravX, convGravY);
auto offsetX = int(std::round(convGravX + x));
auto offsetY = int(std::round(convGravY + y));
if ((offsetX != x || offsetY != y) && offsetX >= 0 && offsetX < XRES && offsetY >= 0 && offsetY < YRES) {//some heat convection for liquids
r = pmap[offsetY][offsetX];
if (!(!r || parts[i].type != TYP(r))) {
if (parts[i].temp>parts[ID(r)].temp) {
swappage = parts[i].temp;
parts[i].temp = parts[ID(r)].temp;
parts[ID(r)].temp = swappage;
}
}
}
}
//heat transfer code
h_count = 0;
#ifdef REALISTIC
if (t&&(t!=PT_HSWC||parts[i].life==10)&&(elements[t].HeatConduct*gel_scale))
#else
if (t && (t!=PT_HSWC||parts[i].life==10) && rng.chance(int(elements[t].HeatConduct*gel_scale), 250))
#endif
{
if (aheat_enable && !(elements[t].Properties&PROP_NOAMBHEAT))
{
#ifdef REALISTIC
c_heat = parts[i].temp*96.645/elements[t].HeatConduct*gel_scale*fabs(elements[t].Weight) + hv[y/CELL][x/CELL]*100*(pv[y/CELL][x/CELL]-MIN_PRESSURE)/(MAX_PRESSURE-MIN_PRESSURE)*2;
float c_Cm = 96.645/elements[t].HeatConduct*gel_scale*fabs(elements[t].Weight) + 100*(pv[y/CELL][x/CELL]-MIN_PRESSURE)/(MAX_PRESSURE-MIN_PRESSURE)*2;
pt = c_heat/c_Cm;
pt = restrict_flt(pt, -MAX_TEMP+MIN_TEMP, MAX_TEMP-MIN_TEMP);
parts[i].temp = pt;
//Pressure increase from heat (temporary)
pv[y/CELL][x/CELL] += (pt-hv[y/CELL][x/CELL])*0.004;
hv[y/CELL][x/CELL] = pt;
#else
c_heat = (hv[y/CELL][x/CELL]-parts[i].temp)*0.04;
c_heat = restrict_flt(c_heat, -MAX_TEMP+MIN_TEMP, MAX_TEMP-MIN_TEMP);
parts[i].temp += c_heat;
hv[y/CELL][x/CELL] -= c_heat;
#endif
}
c_heat = 0.0f;
#ifdef REALISTIC
float c_Cm = 0.0f;
#endif
for (j=0; j<8; j++)
{
surround_hconduct[j] = i;
r = surround[j];
if (!r)
continue;
rt = TYP(r);
if (rt && elements[rt].HeatConduct && (rt!=PT_HSWC||parts[ID(r)].life==10)
&& (t!=PT_FILT||(rt!=PT_BRAY&&rt!=PT_BIZR&&rt!=PT_BIZRG))
&& (rt!=PT_FILT||(t!=PT_BRAY&&t!=PT_PHOT&&t!=PT_BIZR&&t!=PT_BIZRG))
&& (t!=PT_ELEC||rt!=PT_DEUT)
&& (t!=PT_DEUT||rt!=PT_ELEC)
&& (t!=PT_HSWC || rt!=PT_FILT || parts[i].tmp != 1)
&& (t!=PT_FILT || rt!=PT_HSWC || parts[ID(r)].tmp != 1))
{
surround_hconduct[j] = ID(r);
#ifdef REALISTIC
if (rt==PT_GEL)
gel_scale = parts[ID(r)].tmp*2.55f;
else gel_scale = 1.0f;
c_heat += parts[ID(r)].temp*96.645/elements[rt].HeatConduct*gel_scale*fabs(elements[rt].Weight);
c_Cm += 96.645/elements[rt].HeatConduct*gel_scale*fabs(elements[rt].Weight);
#else
c_heat += parts[ID(r)].temp;
#endif
h_count++;
}
}
#ifdef REALISTIC
if (t==PT_GEL)
gel_scale = parts[i].tmp*2.55f;
else gel_scale = 1.0f;
if (t == PT_PHOT)
pt = (c_heat+parts[i].temp*96.645)/(c_Cm+96.645);
else
pt = (c_heat+parts[i].temp*96.645/elements[t].HeatConduct*gel_scale*fabs(elements[t].Weight))/(c_Cm+96.645/elements[t].HeatConduct*gel_scale*fabs(elements[t].Weight));
c_heat += parts[i].temp*96.645/elements[t].HeatConduct*gel_scale*fabs(elements[t].Weight);
c_Cm += 96.645/elements[t].HeatConduct*gel_scale*fabs(elements[t].Weight);
parts[i].temp = restrict_flt(pt, MIN_TEMP, MAX_TEMP);
#else
pt = (c_heat+parts[i].temp)/(h_count+1);
pt = parts[i].temp = restrict_flt(pt, MIN_TEMP, MAX_TEMP);
for (j=0; j<8; j++)
{
parts[surround_hconduct[j]].temp = pt;
}
#endif
ctemph = ctempl = pt;
// change boiling point with pressure
if (((elements[t].Properties&TYPE_LIQUID) && IsElementOrNone(elements[t].HighTemperatureTransition) && (elements[elements[t].HighTemperatureTransition].Properties&TYPE_GAS))
|| t==PT_LNTG || t==PT_SLTW)
ctemph -= 2.0f*pv[y/CELL][x/CELL];
else if (((elements[t].Properties&TYPE_GAS) && IsElementOrNone(elements[t].LowTemperatureTransition) && (elements[elements[t].LowTemperatureTransition].Properties&TYPE_LIQUID))
|| t==PT_WTRV)
ctempl -= 2.0f*pv[y/CELL][x/CELL];
s = 1;
//A fix for ice with ctype = 0
if ((t==PT_ICEI || t==PT_SNOW) && (!IsElement(parts[i].ctype) || parts[i].ctype==PT_ICEI || parts[i].ctype==PT_SNOW))
parts[i].ctype = PT_WATR;
if (elements[t].HighTemperatureTransition>-1 && ctemph>=elements[t].HighTemperature)
{
// particle type change due to high temperature
#ifdef REALISTIC
float dbt = ctempl - pt;
if (elements[t].HighTemperatureTransition != PT_NUM)
{
if (platent[t] <= (c_heat - (elements[t].HighTemperature - dbt)*c_Cm))
{
pt = (c_heat - platent[t])/c_Cm;
t = elements[t].HighTemperatureTransition;
}
else
{
parts[i].temp = restrict_flt(elements[t].HighTemperature - dbt, MIN_TEMP, MAX_TEMP);
s = 0;
}
}
#else
if (elements[t].HighTemperatureTransition != PT_NUM)
t = elements[t].HighTemperatureTransition;
#endif
else if (t == PT_ICEI || t == PT_SNOW)
{
if (parts[i].ctype > 0 && parts[i].ctype < PT_NUM && parts[i].ctype != t)
{
if (elements[parts[i].ctype].LowTemperatureTransition==PT_ICEI || elements[parts[i].ctype].LowTemperatureTransition==PT_SNOW)
{
if (pt<elements[parts[i].ctype].LowTemperature)
s = 0;
}
else if (pt<273.15f)
s = 0;
if (s)
{
#ifdef REALISTIC
//One ice table value for all it's kinds
if (platent[t] <= (c_heat - (elements[parts[i].ctype].LowTemperature - dbt)*c_Cm))
{
pt = (c_heat - platent[t])/c_Cm;
t = parts[i].ctype;
parts[i].ctype = PT_NONE;
parts[i].life = 0;
}
else
{
parts[i].temp = restrict_flt(elements[parts[i].ctype].LowTemperature - dbt, MIN_TEMP, MAX_TEMP);
s = 0;
}
#else
t = parts[i].ctype;
parts[i].ctype = PT_NONE;
parts[i].life = 0;
#endif
}
}
else
s = 0;
}
else if (t == PT_SLTW)
{
#ifdef REALISTIC
if (platent[t] <= (c_heat - (elements[t].HighTemperature - dbt)*c_Cm))
{
pt = (c_heat - platent[t])/c_Cm;
t = rng.chance(1, 4) ? PT_SALT : PT_WTRV;
}
else
{
parts[i].temp = restrict_flt(elements[t].HighTemperature - dbt, MIN_TEMP, MAX_TEMP);
s = 0;
}
#else
t = rng.chance(1, 4) ? PT_SALT : PT_WTRV;
#endif
}
else if (t == PT_BRMT)
{
if (parts[i].ctype == PT_TUNG)
{
if (ctemph < elements[parts[i].ctype].HighTemperature)
s = 0;
else
{
t = PT_LAVA;
parts[i].type = PT_TUNG;
}
}
else if (ctemph >= elements[t].HighTemperature)
t = PT_LAVA;
else
s = 0;
}
else if (t == PT_CRMC)
{
float pres = std::max((pv[y/CELL][x/CELL]+pv[(y-2)/CELL][x/CELL]+pv[(y+2)/CELL][x/CELL]+pv[y/CELL][(x-2)/CELL]+pv[y/CELL][(x+2)/CELL])*2.0f, 0.0f);
if (ctemph < pres+elements[PT_CRMC].HighTemperature)
s = 0;
else
t = PT_LAVA;
}
else
s = 0;
}
else if (elements[t].LowTemperatureTransition > -1 && ctempl<elements[t].LowTemperature)
{
// particle type change due to low temperature
#ifdef REALISTIC
float dbt = ctempl - pt;
if (elements[t].LowTemperatureTransition != PT_NUM)
{
if (platent[elements[t].LowTemperatureTransition] >= (c_heat - (elements[t].LowTemperature - dbt)*c_Cm))
{
pt = (c_heat + platent[elements[t].LowTemperatureTransition])/c_Cm;
t = elements[t].LowTemperatureTransition;
}
else
{
parts[i].temp = restrict_flt(elements[t].LowTemperature - dbt, MIN_TEMP, MAX_TEMP);
s = 0;
}
}
#else
if (elements[t].LowTemperatureTransition != PT_NUM)
t = elements[t].LowTemperatureTransition;
#endif
else if (t == PT_WTRV)
{
t = (pt < 273.0f) ? PT_RIME : PT_DSTW;
}
else if (t == PT_LAVA)
{
if (parts[i].ctype > 0 && parts[i].ctype < PT_NUM && parts[i].ctype != PT_LAVA && elements[parts[i].ctype].Enabled)
{
if (parts[i].ctype == PT_THRM && pt >= elements[PT_BMTL].HighTemperature)
s = 0;
else if ((parts[i].ctype == PT_VIBR || parts[i].ctype == PT_BVBR) && pt >= 273.15f)
s = 0;
else if (parts[i].ctype == PT_TUNG)
{
// TUNG does its own melting in its update function, so HighTemperatureTransition is not LAVA so it won't be handled by the code for HighTemperatureTransition==PT_LAVA below
// However, the threshold is stored in HighTemperature to allow it to be changed from Lua
if (pt >= elements[parts[i].ctype].HighTemperature)
s = 0;
}
else if (parts[i].ctype == PT_CRMC)
{
float pres = std::max((pv[y/CELL][x/CELL]+pv[(y-2)/CELL][x/CELL]+pv[(y+2)/CELL][x/CELL]+pv[y/CELL][(x-2)/CELL]+pv[y/CELL][(x+2)/CELL])*2.0f, 0.0f);
if (ctemph >= pres+elements[PT_CRMC].HighTemperature)
s = 0;
}
else if (elements[parts[i].ctype].HighTemperatureTransition == PT_LAVA || parts[i].ctype == PT_HEAC)
{
if (pt >= elements[parts[i].ctype].HighTemperature)
s = 0;
}
else if (pt>=973.0f)
s = 0; // freezing point for lava with any other (not listed in ptransitions as turning into lava) ctype
if (s)
{
t = parts[i].ctype;
parts[i].ctype = PT_NONE;
if (t == PT_THRM)
{
parts[i].tmp = 0;
t = PT_BMTL;
}
if (t == PT_PLUT)
{
parts[i].tmp = 0;
t = PT_LAVA;
}
}
}
else if (pt<973.0f)
t = PT_STNE;
else
s = 0;
}
else
s = 0;
}
else
s = 0;
#ifdef REALISTIC
pt = restrict_flt(pt, MIN_TEMP, MAX_TEMP);
for (j=0; j<8; j++)
{
parts[surround_hconduct[j]].temp = pt;
}
#endif
if (s) // particle type change occurred
{
if (t==PT_ICEI || t==PT_LAVA || t==PT_SNOW)
parts[i].ctype = parts[i].type;
if (!(t==PT_ICEI && parts[i].ctype==PT_FRZW))
parts[i].life = 0;
if (t == PT_FIRE)
{
//hackish, if tmp isn't 0 the FIRE might turn into DSTW later
//idealy transitions should use create_part(i) but some elements rely on properties staying constant
//and I don't feel like checking each one right now
parts[i].tmp = 0;
}
if ((elements[t].Properties&TYPE_GAS) && !(elements[parts[i].type].Properties&TYPE_GAS))
pv[y/CELL][x/CELL] += 0.50f;
if (t == PT_NONE)
{
kill_part(i);
goto killed;
}
// part_change_type could refuse to change the type and kill the particle
// for example, changing type to STKM but one already exists
// we need to account for that to not cause simulation corruption issues
if (part_change_type(i,x,y,t))
goto killed;
if (t==PT_FIRE || t==PT_PLSM || t==PT_CFLM)
parts[i].life = rng.between(120, 169);
if (t == PT_LAVA)
{
if (parts[i].ctype == PT_BRMT) parts[i].ctype = PT_BMTL;
else if (parts[i].ctype == PT_SAND) parts[i].ctype = PT_GLAS;
else if (parts[i].ctype == PT_BGLA) parts[i].ctype = PT_GLAS;
else if (parts[i].ctype == PT_PQRT) parts[i].ctype = PT_QRTZ;
else if (parts[i].ctype == PT_LITH && parts[i].tmp2 > 3) parts[i].ctype = PT_GLAS;
parts[i].life = rng.between(240, 359);
}
transitionOccurred = true;
}
pt = parts[i].temp = restrict_flt(parts[i].temp, MIN_TEMP, MAX_TEMP);
if (t == PT_LAVA)
{
parts[i].life = int(restrict_flt((parts[i].temp-700)/7, 0, 400));
if (parts[i].ctype==PT_THRM&&parts[i].tmp>0)
{
parts[i].tmp--;
parts[i].temp = 3500;
}
if (parts[i].ctype==PT_PLUT&&parts[i].tmp>0)
{
parts[i].tmp--;
parts[i].temp = MAX_TEMP;
}
}
}
else
{
if (!(air->bmap_blockairh[y/CELL][x/CELL]&0x8))
air->bmap_blockairh[y/CELL][x/CELL]++;
parts[i].temp = restrict_flt(parts[i].temp, MIN_TEMP, MAX_TEMP);
}
}
if (t==PT_LIFE)
{
parts[i].temp = restrict_flt(parts[i].temp-50.0f, MIN_TEMP, MAX_TEMP);
}
if (t==PT_WIRE)
{
//wire_placed = 1;
}
//spark updates from walls
if ((elements[t].Properties&PROP_CONDUCTS) || t==PT_SPRK)
{
auto nx = x % CELL;
if (nx == 0)
nx = x/CELL - 1;
else if (nx == CELL-1)
nx = x/CELL + 1;
else
nx = x/CELL;
auto ny = y % CELL;
if (ny == 0)
ny = y/CELL - 1;
else if (ny == CELL-1)
ny = y/CELL + 1;
else
ny = y/CELL;
if (nx>=0 && ny>=0 && nx<XCELLS && ny<YCELLS)
{
if (t!=PT_SPRK)
{
if (emap[ny][nx]==12 && !parts[i].life && bmap[ny][nx] != WL_STASIS)
{
part_change_type(i,x,y,PT_SPRK);
parts[i].life = 4;
parts[i].ctype = t;
t = PT_SPRK;
}
}
else if (bmap[ny][nx]==WL_DETECT || bmap[ny][nx]==WL_EWALL || bmap[ny][nx]==WL_ALLOWLIQUID || bmap[ny][nx]==WL_WALLELEC || bmap[ny][nx]==WL_ALLOWALLELEC || bmap[ny][nx]==WL_EHOLE)
set_emap(nx, ny);
}
}
//the basic explosion, from the .explosive variable
if ((elements[t].Explosive&2) && pv[y/CELL][x/CELL]>2.5f)
{
parts[i].life = rng.between(180, 259);
parts[i].temp = restrict_flt(elements[PT_FIRE].DefaultProperties.temp + (elements[t].Flammable/2), MIN_TEMP, MAX_TEMP);
t = PT_FIRE;
part_change_type(i,x,y,t);
pv[y/CELL][x/CELL] += 0.25f * CFDS;
}
s = 1;
gravtot = fabs(gravy[(y/CELL)*XCELLS+(x/CELL)])+fabs(gravx[(y/CELL)*XCELLS+(x/CELL)]);
if (elements[t].HighPressureTransition>-1 && pv[y/CELL][x/CELL]>elements[t].HighPressure) {
// particle type change due to high pressure
if (elements[t].HighPressureTransition!=PT_NUM)
t = elements[t].HighPressureTransition;
else if (t==PT_BMTL) {
if (pv[y/CELL][x/CELL]>2.5f)
t = PT_BRMT;
else if (pv[y/CELL][x/CELL]>1.0f && parts[i].tmp==1)
t = PT_BRMT;
else s = 0;
}
else s = 0;
} else if (elements[t].LowPressureTransition>-1 && pv[y/CELL][x/CELL]<elements[t].LowPressure && gravtot<=(elements[t].LowPressure/4.0f)) {
// particle type change due to low pressure
if (elements[t].LowPressureTransition!=PT_NUM)
t = elements[t].LowPressureTransition;
else s = 0;
} else if (elements[t].HighPressureTransition>-1 && gravtot>(elements[t].HighPressure/4.0f)) {
// particle type change due to high gravity
if (elements[t].HighPressureTransition!=PT_NUM)
t = elements[t].HighPressureTransition;
else if (t==PT_BMTL) {
if (gravtot>0.625f)
t = PT_BRMT;
else if (gravtot>0.25f && parts[i].tmp==1)
t = PT_BRMT;
else s = 0;
}
else s = 0;
} else s = 0;
// particle type change occurred
if (s)
{
if (t == PT_NONE)
{
kill_part(i);
goto killed;
}
parts[i].life = 0;
// part_change_type could refuse to change the type and kill the particle
// for example, changing type to STKM but one already exists
// we need to account for that to not cause simulation corruption issues
if (part_change_type(i,x,y,t))
goto killed;
if (t == PT_FIRE)
parts[i].life = rng.between(120, 169);
transitionOccurred = true;
}
//call the particle update function, if there is one
if (elements[t].Update)
{
if ((*(elements[t].Update))(this, i, x, y, surround_space, nt, parts, pmap))
continue;
x = (int)(parts[i].x+0.5f);
y = (int)(parts[i].y+0.5f);
}
if(legacy_enable)//if heat sim is off
Element::legacyUpdate(this, i,x,y,surround_space,nt, parts, pmap);
killed:
if (parts[i].type == PT_NONE)//if its dead, skip to next particle
continue;
if (transitionOccurred)
continue;
if (!parts[i].vx&&!parts[i].vy)//if its not moving, skip to next particle, movement code it next
continue;
mv = fmaxf(fabsf(parts[i].vx), fabsf(parts[i].vy));
if (mv < ISTP)
{
clear_x = x;
clear_y = y;
clear_xf = parts[i].x;
clear_yf = parts[i].y;
fin_xf = clear_xf + parts[i].vx;
fin_yf = clear_yf + parts[i].vy;
fin_x = (int)(fin_xf+0.5f);
fin_y = (int)(fin_yf+0.5f);
}
else
{
if (mv > SIM_MAXVELOCITY)
{
parts[i].vx *= SIM_MAXVELOCITY/mv;
parts[i].vy *= SIM_MAXVELOCITY/mv;
mv = SIM_MAXVELOCITY;
}
// interpolate to see if there is anything in the way
dx = parts[i].vx*ISTP/mv;
dy = parts[i].vy*ISTP/mv;
fin_xf = parts[i].x;
fin_yf = parts[i].y;
fin_x = (int)(fin_xf+0.5f);
fin_y = (int)(fin_yf+0.5f);
bool closedEholeStart = this->InBounds(fin_x, fin_y) && (bmap[fin_y/CELL][fin_x/CELL] == WL_EHOLE && !emap[fin_y/CELL][fin_x/CELL]);
while (1)
{
mv -= ISTP;
fin_xf += dx;
fin_yf += dy;
fin_x = (int)(fin_xf+0.5f);
fin_y = (int)(fin_yf+0.5f);
if (edgeMode == 2)
{
bool x_ok = (fin_xf >= CELL-.5f && fin_xf < XRES-CELL-.5f);
bool y_ok = (fin_yf >= CELL-.5f && fin_yf < YRES-CELL-.5f);
if (!x_ok)
fin_xf = remainder_p(fin_xf-CELL+.5f, XRES-CELL*2.0f)+CELL-.5f;
if (!y_ok)
fin_yf = remainder_p(fin_yf-CELL+.5f, YRES-CELL*2.0f)+CELL-.5f;
fin_x = (int)(fin_xf+0.5f);
fin_y = (int)(fin_yf+0.5f);
}
if (mv <= 0.0f)
{
// nothing found
fin_xf = parts[i].x + parts[i].vx;
fin_yf = parts[i].y + parts[i].vy;
if (edgeMode == 2)
{
bool x_ok = (fin_xf >= CELL-.5f && fin_xf < XRES-CELL-.5f);
bool y_ok = (fin_yf >= CELL-.5f && fin_yf < YRES-CELL-.5f);
if (!x_ok)
fin_xf = remainder_p(fin_xf-CELL+.5f, XRES-CELL*2.0f)+CELL-.5f;
if (!y_ok)
fin_yf = remainder_p(fin_yf-CELL+.5f, YRES-CELL*2.0f)+CELL-.5f;
}
fin_x = (int)(fin_xf+0.5f);
fin_y = (int)(fin_yf+0.5f);
clear_xf = fin_xf-dx;
clear_yf = fin_yf-dy;
clear_x = (int)(clear_xf+0.5f);
clear_y = (int)(clear_yf+0.5f);
break;
}
//block if particle can't move (0), or some special cases where it returns 1 (can_move = 3 but returns 1 meaning particle will be eaten)
//also photons are still blocked (slowed down) by any particle (even ones it can move through), and absorb wall also blocks particles
int eval = eval_move(t, fin_x, fin_y, NULL);
if (!eval || (can_move[t][TYP(pmap[fin_y][fin_x])] == 3 && eval == 1) || (t == PT_PHOT && pmap[fin_y][fin_x]) || bmap[fin_y/CELL][fin_x/CELL]==WL_DESTROYALL || closedEholeStart!=(bmap[fin_y/CELL][fin_x/CELL] == WL_EHOLE && !emap[fin_y/CELL][fin_x/CELL]))
{
// found an obstacle
clear_xf = fin_xf-dx;
clear_yf = fin_yf-dy;
clear_x = (int)(clear_xf+0.5f);
clear_y = (int)(clear_yf+0.5f);
break;
}
if (bmap[fin_y/CELL][fin_x/CELL]==WL_DETECT && emap[fin_y/CELL][fin_x/CELL]<8)
set_emap(fin_x/CELL, fin_y/CELL);
}
}
stagnant = parts[i].flags & FLAG_STAGNANT;
parts[i].flags &= ~FLAG_STAGNANT;
if (t==PT_STKM || t==PT_STKM2 || t==PT_FIGH)
{
//head movement, let head pass through anything
parts[i].x += parts[i].vx;
parts[i].y += parts[i].vy;
int nx = (int)((float)parts[i].x+0.5f);
int ny = (int)((float)parts[i].y+0.5f);
if (edgeMode == 2)
{
bool x_ok = (nx >= CELL && nx < XRES-CELL);
bool y_ok = (ny >= CELL && ny < YRES-CELL);
int oldnx = nx, oldny = ny;
if (!x_ok)
{
parts[i].x = remainder_p(parts[i].x-CELL+.5f, XRES-CELL*2.0f)+CELL-.5f;
nx = (int)((float)parts[i].x+0.5f);
}
if (!y_ok)
{
parts[i].y = remainder_p(parts[i].y-CELL+.5f, YRES-CELL*2.0f)+CELL-.5f;
ny = (int)((float)parts[i].y+0.5f);
}
if (!x_ok || !y_ok) //when moving from left to right stickmen might be able to fall through solid things, fix with "eval_move(t, nx+diffx, ny+diffy, NULL)" but then they die instead
{
//adjust stickmen legs
playerst* stickman = NULL;
int t = parts[i].type;
if (t == PT_STKM)
stickman = &player;
else if (t == PT_STKM2)
stickman = &player2;
else if (t == PT_FIGH && parts[i].tmp >= 0 && parts[i].tmp < MAX_FIGHTERS)
stickman = &fighters[parts[i].tmp];
if (stickman)
for (int i = 0; i < 16; i+=2)
{
stickman->legs[i] += (nx-oldnx);
stickman->legs[i+1] += (ny-oldny);
stickman->accs[i/2] *= .95f;
}
parts[i].vy *= .95f;
parts[i].vx *= .95f;
}
}
if (ny!=y || nx!=x)
{
if (ID(pmap[y][x]) == i)
pmap[y][x] = 0;
else if (ID(photons[y][x]) == i)
photons[y][x] = 0;
if (nx<CELL || nx>=XRES-CELL || ny<CELL || ny>=YRES-CELL)
{
kill_part(i);
continue;
}
if (elements[t].Properties & TYPE_ENERGY)
photons[ny][nx] = PMAP(i, t);
else if (t)
pmap[ny][nx] = PMAP(i, t);
}
}
else if (elements[t].Properties & TYPE_ENERGY)
{
if (t == PT_PHOT)
{
if (parts[i].flags&FLAG_SKIPMOVE)
{
parts[i].flags &= ~FLAG_SKIPMOVE;
continue;
}
if (eval_move(PT_PHOT, fin_x, fin_y, NULL))
{
int rt = TYP(pmap[fin_y][fin_x]);
int lt = TYP(pmap[y][x]);
int rt_glas = (rt == PT_GLAS) || (rt == PT_BGLA);
int lt_glas = (lt == PT_GLAS) || (lt == PT_BGLA);
if ((rt_glas && !lt_glas) || (lt_glas && !rt_glas))
{
if (!get_normal_interp(REFRACT|t, parts[i].x, parts[i].y, parts[i].vx, parts[i].vy, &nrx, &nry)) {
kill_part(i);
continue;
}
r = get_wavelength_bin(&parts[i].ctype);
if (r == -1 || !(parts[i].ctype&0x3FFFFFFF))
{
kill_part(i);
continue;
}
nn = GLASS_IOR - GLASS_DISP*(r-30)/30.0f;
nn *= nn;
auto enter = rt_glas && !lt_glas;
nrx = enter ? -nrx : nrx;
nry = enter ? -nry : nry;
nn = enter ? 1.0f/nn : nn;
ct1 = parts[i].vx*nrx + parts[i].vy*nry;
ct2 = 1.0f - (nn*nn)*(1.0f-(ct1*ct1));
if (ct2 < 0.0f) {
// total internal reflection
parts[i].vx -= 2.0f*ct1*nrx;
parts[i].vy -= 2.0f*ct1*nry;
fin_xf = parts[i].x;
fin_yf = parts[i].y;
fin_x = x;
fin_y = y;
} else {
// refraction
ct2 = sqrtf(ct2);
ct2 = ct2 - nn*ct1;
parts[i].vx = nn*parts[i].vx + ct2*nrx;
parts[i].vy = nn*parts[i].vy + ct2*nry;
}
}
}
}
if (stagnant)//FLAG_STAGNANT set, was reflected on previous frame
{
// cast coords as int then back to float for compatibility with existing saves
if (!do_move(i, x, y, (float)fin_x, (float)fin_y) && parts[i].type) {
kill_part(i);
continue;
}
}
else if (!do_move(i, x, y, fin_xf, fin_yf))
{
if (parts[i].type == PT_NONE)
continue;
// reflection
parts[i].flags |= FLAG_STAGNANT;
if (t==PT_NEUT && rng.chance(1, 10))
{
kill_part(i);
continue;
}
r = pmap[fin_y][fin_x];
if ((TYP(r)==PT_PIPE || TYP(r) == PT_PPIP) && !TYP(parts[ID(r)].ctype))
{
parts[ID(r)].ctype = parts[i].type;
parts[ID(r)].temp = parts[i].temp;
parts[ID(r)].tmp2 = parts[i].life;
parts[ID(r)].tmp3 = parts[i].tmp;
parts[ID(r)].tmp4 = parts[i].ctype;
kill_part(i);
continue;
}
if (t == PT_PHOT)
{
auto mask = elements[TYP(r)].PhotonReflectWavelengths;
if (TYP(r) == PT_LITH)
{
int wl_bin = parts[ID(r)].ctype / 4;
if (wl_bin < 0) wl_bin = 0;
if (wl_bin > 25) wl_bin = 25;
mask = (0x1F << wl_bin);
}
parts[i].ctype &= mask;
}
if (get_normal_interp(t, parts[i].x, parts[i].y, parts[i].vx, parts[i].vy, &nrx, &nry))
{
if (TYP(r) == PT_CRMC)
{
float r = rng.between(-50, 50) * 0.01f, rx, ry, anrx, anry;
r = r * r * r;
rx = cosf(r); ry = sinf(r);
anrx = rx * nrx + ry * nry;
anry = rx * nry - ry * nrx;
dp = anrx*parts[i].vx + anry*parts[i].vy;
parts[i].vx -= 2.0f*dp*anrx;
parts[i].vy -= 2.0f*dp*anry;
}
else
{
dp = nrx*parts[i].vx + nry*parts[i].vy;
parts[i].vx -= 2.0f*dp*nrx;
parts[i].vy -= 2.0f*dp*nry;
}
// leave the actual movement until next frame so that reflection of fast particles and refraction happen correctly
}
else
{
if (t!=PT_NEUT)
kill_part(i);
continue;
}
if (!(parts[i].ctype&0x3FFFFFFF) && t == PT_PHOT)
{
kill_part(i);
continue;
}
}
}
else if (elements[t].Falldown==0)
{
// gasses and solids (but not powders)
if (!do_move(i, x, y, fin_xf, fin_yf))
{
if (parts[i].type == PT_NONE)
continue;
// can't move there, so bounce off
// TODO
// TODO: Work out what previous TODO was for
if (fin_x>x+ISTP) fin_x=x+ISTP;
if (fin_x<x-ISTP) fin_x=x-ISTP;
if (fin_y>y+ISTP) fin_y=y+ISTP;
if (fin_y<y-ISTP) fin_y=y-ISTP;
if (do_move(i, x, y, 0.25f+(float)(2*x-fin_x), 0.25f+fin_y))
{
parts[i].vx *= elements[t].Collision;
}
else if (do_move(i, x, y, 0.25f+fin_x, 0.25f+(float)(2*y-fin_y)))
{
parts[i].vy *= elements[t].Collision;
}
else
{
parts[i].vx *= elements[t].Collision;
parts[i].vy *= elements[t].Collision;
}
}
}
else
{
// Checking stagnant is cool, but then it doesn't update when you change it later.
if (water_equal_test && elements[t].Falldown == 2 && rng.chance(1, 200))
{
if (flood_water(x, y, i))
goto movedone;
}
// liquids and powders
if (!do_move(i, x, y, fin_xf, fin_yf))
{
if (parts[i].type == PT_NONE)
continue;
if (fin_x!=x && do_move(i, x, y, fin_xf, clear_yf))
{
parts[i].vx *= elements[t].Collision;
parts[i].vy *= elements[t].Collision;
}
else if (fin_y!=y && do_move(i, x, y, clear_xf, fin_yf))
{
parts[i].vx *= elements[t].Collision;
parts[i].vy *= elements[t].Collision;
}
else
{
s = 1;
r = rng.between(0, 1) * 2 - 1;// position search direction (left/right first)
if ((clear_x!=x || clear_y!=y || nt || surround_space) &&
(fabsf(parts[i].vx)>0.01f || fabsf(parts[i].vy)>0.01f))
{
// allow diagonal movement if target position is blocked
// but no point trying this if particle is stuck in a block of identical particles
dx = parts[i].vx - parts[i].vy*r;
dy = parts[i].vy + parts[i].vx*r;
mv = std::max(fabsf(dx), fabsf(dy));
dx /= mv;
dy /= mv;
if (do_move(i, x, y, clear_xf+dx, clear_yf+dy))
{
parts[i].vx *= elements[t].Collision;
parts[i].vy *= elements[t].Collision;
goto movedone;
}
swappage = dx;
dx = dy*r;
dy = -swappage*r;
if (do_move(i, x, y, clear_xf+dx, clear_yf+dy))
{
parts[i].vx *= elements[t].Collision;
parts[i].vy *= elements[t].Collision;
goto movedone;
}
}
if (elements[t].Falldown>1 && !grav->IsEnabled() && gravityMode==0 && parts[i].vy>fabsf(parts[i].vx))
{
s = 0;
// stagnant is true if FLAG_STAGNANT was set for this particle in previous frame
if (!stagnant || nt) //nt is if there is an something else besides the current particle type, around the particle
rt = 30;//slight less water lag, although it changes how it moves a lot
else
rt = 10;
if (t==PT_GEL)
rt = int(parts[i].tmp*0.20f+5.0f);
auto nx = -1, ny = -1;
for (j=clear_x+r; j>=0 && j>=clear_x-rt && j<clear_x+rt && j<XRES; j+=r)
{
if ((TYP(pmap[fin_y][j])!=t || bmap[fin_y/CELL][j/CELL])
&& (s=do_move(i, x, y, (float)j, fin_yf)))
{
nx = (int)(parts[i].x+0.5f);
ny = (int)(parts[i].y+0.5f);
break;
}
if (fin_y!=clear_y && (TYP(pmap[clear_y][j])!=t || bmap[clear_y/CELL][j/CELL])
&& (s=do_move(i, x, y, (float)j, clear_yf)))
{
nx = (int)(parts[i].x+0.5f);
ny = (int)(parts[i].y+0.5f);
break;
}
if (TYP(pmap[clear_y][j])!=t || (bmap[clear_y/CELL][j/CELL] && bmap[clear_y/CELL][j/CELL]!=WL_STREAM))
break;
}
r = (parts[i].vy>0) ? 1 : -1;
if (s==1)
for (j=ny+r; j>=0 && j<YRES && j>=ny-rt && j<ny+rt; j+=r)
{
if ((TYP(pmap[j][nx])!=t || bmap[j/CELL][nx/CELL]) && do_move(i, nx, ny, (float)nx, (float)j))
break;
if (TYP(pmap[j][nx])!=t || (bmap[j/CELL][nx/CELL] && bmap[j/CELL][nx/CELL]!=WL_STREAM))
break;
}
else if (s==-1) {} // particle is out of bounds
else if ((clear_x!=x||clear_y!=y) && do_move(i, x, y, clear_xf, clear_yf)) {}
else parts[i].flags |= FLAG_STAGNANT;
parts[i].vx *= elements[t].Collision;
parts[i].vy *= elements[t].Collision;
}
else if (elements[t].Falldown>1 && fabsf(pGravX*parts[i].vx+pGravY*parts[i].vy)>fabsf(pGravY*parts[i].vx-pGravX*parts[i].vy))
{
float nxf, nyf, prev_pGravX, prev_pGravY, ptGrav = elements[t].Gravity;
s = 0;
// stagnant is true if FLAG_STAGNANT was set for this particle in previous frame
// nt is if there is something else besides the current particle type around the particle
// 30 gives slightly less water lag, although it changes how it moves a lot
rt = (!stagnant || nt) ? 30 : 10;
// clear_xf, clear_yf is the last known position that the particle should almost certainly be able to move to
nxf = clear_xf;
nyf = clear_yf;
auto nx = clear_x;
auto ny = clear_y;
// Look for spaces to move horizontally (perpendicular to gravity direction), keep going until a space is found or the number of positions examined = rt
for (j=0;j<rt;j++)
{
// Calculate overall gravity direction
GetGravityField(nx, ny, ptGrav, 1.0f, pGravX, pGravY);
// Scale gravity vector so that the largest component is 1 pixel
mv = std::max(fabsf(pGravX), fabsf(pGravY));
if (mv<0.0001f) break;
pGravX /= mv;
pGravY /= mv;
// Move 1 pixel perpendicularly to gravity
// r is +1/-1, to try moving left or right at random
if (j)
{
// Not quite the gravity direction
// Gravity direction + last change in gravity direction
// This makes liquid movement a bit less frothy, particularly for balls of liquid in radial gravity. With radial gravity, instead of just moving along a tangent, the attempted movement will follow the curvature a bit better.
nxf += r*(pGravY*2.0f-prev_pGravY);
nyf += -r*(pGravX*2.0f-prev_pGravX);
}
else
{
nxf += r*pGravY;
nyf += -r*pGravX;
}
prev_pGravX = pGravX;
prev_pGravY = pGravY;
// Check whether movement is allowed
nx = (int)(nxf+0.5f);
ny = (int)(nyf+0.5f);
if (nx<0 || ny<0 || nx>=XRES || ny >=YRES)
break;
if (TYP(pmap[ny][nx])!=t || bmap[ny/CELL][nx/CELL])
{
s = do_move(i, x, y, nxf, nyf);
if (s)
{
// Movement was successful
nx = (int)(parts[i].x+0.5f);
ny = (int)(parts[i].y+0.5f);
break;
}
// A particle of a different type, or a wall, was found. Stop trying to move any further horizontally unless the wall should be completely invisible to particles.
if (TYP(pmap[ny][nx])!=t || bmap[ny/CELL][nx/CELL]!=WL_STREAM)
break;
}
}
if (s==1)
{
// The particle managed to move horizontally, now try to move vertically (parallel to gravity direction)
// Keep going until the particle is blocked (by something that isn't the same element) or the number of positions examined = rt
clear_x = nx;
clear_y = ny;
for (j=0;j<rt;j++)
{
// Calculate overall gravity direction
GetGravityField(nx, ny, ptGrav, 1.0f, pGravX, pGravY);
// Scale gravity vector so that the largest component is 1 pixel
mv = std::max(fabsf(pGravX), fabsf(pGravY));
if (mv<0.0001f) break;
pGravX /= mv;
pGravY /= mv;
// Move 1 pixel in the direction of gravity
nxf += pGravX;
nyf += pGravY;
nx = (int)(nxf+0.5f);
ny = (int)(nyf+0.5f);
if (nx<0 || ny<0 || nx>=XRES || ny>=YRES)
break;
// If the space is anything except the same element (a wall, empty space, or occupied by a particle of a different element), try to move into it
if (TYP(pmap[ny][nx])!=t || bmap[ny/CELL][nx/CELL])
{
s = do_move(i, clear_x, clear_y, nxf, nyf);
if (s || TYP(pmap[ny][nx])!=t || bmap[ny/CELL][nx/CELL]!=WL_STREAM)
break; // found the edge of the liquid and movement into it succeeded, so stop moving down
}
}
}
else if (s==-1) {} // particle is out of bounds
else if ((clear_x!=x||clear_y!=y) && do_move(i, x, y, clear_xf, clear_yf)) {} // try moving to the last clear position
else parts[i].flags |= FLAG_STAGNANT;
parts[i].vx *= elements[t].Collision;
parts[i].vy *= elements[t].Collision;
}
else
{
// if interpolation was done, try moving to last clear position
if ((clear_x!=x||clear_y!=y) && do_move(i, x, y, clear_xf, clear_yf)) {}
else parts[i].flags |= FLAG_STAGNANT;
parts[i].vx *= elements[t].Collision;
parts[i].vy *= elements[t].Collision;
}
}
}
}
movedone:
continue;
}
//'f' was pressed (single frame)
if (framerender)
framerender--;
}
void Simulation::RecalcFreeParticles(bool do_life_dec)
{
int x, y, t;
int lastPartUsed = 0;
int lastPartUnused = -1;
memset(pmap, 0, sizeof(pmap));
memset(pmap_count, 0, sizeof(pmap_count));
memset(photons, 0, sizeof(photons));
NUM_PARTS = 0;
//the particle loop that resets the pmap/photon maps every frame, to update them.
for (int i = 0; i <= parts_lastActiveIndex; i++)
{
if (parts[i].type)
{
t = parts[i].type;
x = (int)(parts[i].x+0.5f);
y = (int)(parts[i].y+0.5f);
bool inBounds = false;
if (x>=0 && y>=0 && x<XRES && y<YRES)
{
if (elements[t].Properties & TYPE_ENERGY)
photons[y][x] = PMAP(i, t);
else
{
// Particles are sometimes allowed to go inside INVS and FILT
// To make particles collide correctly when inside these elements, these elements must not overwrite an existing pmap entry from particles inside them
if (!pmap[y][x] || (t!=PT_INVIS && t!= PT_FILT))
pmap[y][x] = PMAP(i, t);
// (there are a few exceptions, including energy particles - currently no limit on stacking those)
if (t!=PT_THDR && t!=PT_EMBR && t!=PT_FIGH && t!=PT_PLSM)
pmap_count[y][x]++;
}
inBounds = true;
}
lastPartUsed = i;
NUM_PARTS ++;
if (elementRecount && t >= 0 && t < PT_NUM && elements[t].Enabled)
elementCount[t]++;
//decrease particle life
if (do_life_dec && (!sys_pause || framerender))
{
if (t<0 || t>=PT_NUM || !elements[t].Enabled)
{
kill_part(i);
continue;
}
unsigned int elem_properties = elements[t].Properties;
if (parts[i].life>0 && (elem_properties&PROP_LIFE_DEC) && !(inBounds && bmap[y/CELL][x/CELL] == WL_STASIS && emap[y/CELL][x/CELL]<8))
{
// automatically decrease life
parts[i].life--;
if (parts[i].life<=0 && (elem_properties&(PROP_LIFE_KILL_DEC|PROP_LIFE_KILL)))
{
// kill on change to no life
kill_part(i);
continue;
}
}
else if (parts[i].life<=0 && (elem_properties&PROP_LIFE_KILL) && !(inBounds && bmap[y/CELL][x/CELL] == WL_STASIS && emap[y/CELL][x/CELL]<8))
{
// kill if no life
kill_part(i);
continue;
}
}
}
else
{
if (lastPartUnused<0) pfree = i;
else parts[lastPartUnused].life = i;
lastPartUnused = i;
}
}
if (lastPartUnused == -1)
{
pfree = (parts_lastActiveIndex>=(NPART-1)) ? -1 : parts_lastActiveIndex+1;
}
else
{
parts[lastPartUnused].life = (parts_lastActiveIndex>=(NPART-1)) ? -1 : parts_lastActiveIndex+1;
}
parts_lastActiveIndex = lastPartUsed;
if (elementRecount)
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;
if (pmap[ay][ax] && TYP(pmap[ay][ax]) != PT_LIFE)
{
continue;
}
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);
if (i >= 0)
{
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;
force_stacking_check = false;
for (int y = 0; y < YRES; y++)
{
for (int x = 0; x < XRES; x++)
{
// Use a threshold, since some particle stacking can be normal (e.g. BIZR + FILT)
// Setting pmap_count[y][x] > NPART means BHOL will form in that spot
if (pmap_count[y][x]>5)
{
if (bmap[y/CELL][x/CELL]==WL_EHOLE)
{
// Allow more stacking in E-hole
if (pmap_count[y][x]>1500)
{
pmap_count[y][x] = pmap_count[y][x] + NPART;
excessive_stacking_found = 1;
}
}
else if (pmap_count[y][x]>1500 || (unsigned int)rng.between(0, 1599) <= (pmap_count[y][x]+100))
{
pmap_count[y][x] = pmap_count[y][x] + NPART;
excessive_stacking_found = true;
}
}
}
}
if (excessive_stacking_found)
{
for (int i = 0; i <= parts_lastActiveIndex; i++)
{
if (parts[i].type)
{
int t = parts[i].type;
int x = (int)(parts[i].x+0.5f);
int y = (int)(parts[i].y+0.5f);
if (x>=0 && y>=0 && x<XRES && y<YRES && !(elements[t].Properties&TYPE_ENERGY))
{
if (pmap_count[y][x]>=NPART)
{
if (pmap_count[y][x]>NPART)
{
create_part(i, x, y, PT_NBHL);
parts[i].temp = MAX_TEMP;
parts[i].tmp = pmap_count[y][x]-NPART;//strength of grav field
if (parts[i].tmp>51200) parts[i].tmp = 51200;
pmap_count[y][x] = NPART;
}
else
{
kill_part(i);
}
}
}
}
}
}
}
//updates pmap, gol, and some other simulation stuff (but not particles)
void Simulation::BeforeSim()
{
if (!sys_pause||framerender)
{
air->update_air();
if(aheat_enable)
air->update_airh();
if(grav->IsEnabled())
{
grav->gravity_update_async();
//Get updated buffer pointers for gravity
gravx = &grav->gravx[0];
gravy = &grav->gravy[0];
gravp = &grav->gravp[0];
gravmap = &grav->gravmap[0];
}
if(emp_decor>0)
emp_decor -= emp_decor/25+2;
if(emp_decor < 0)
emp_decor = 0;
etrd_count_valid = false;
etrd_life0_count = 0;
currentTick++;
elementRecount |= !(currentTick%180);
if (elementRecount)
std::fill(elementCount, elementCount+PT_NUM, 0);
}
sandcolour_interface = (int)(20.0f*sin((float)sandcolour_frame*(TPT_PI_FLT/180.0f)));
sandcolour_frame = (sandcolour_frame+1)%360;
sandcolour = (int)(20.0f*sin((float)(frameCount)*(TPT_PI_FLT/180.0f)));
if (gravWallChanged)
{
grav->gravity_mask();
gravWallChanged = false;
}
if (debug_nextToUpdate == 0)
RecalcFreeParticles(true);
if (!sys_pause || framerender)
{
// decrease wall conduction, make walls block air and ambient heat
int x, y;
for (y = 0; y < YCELLS; y++)
{
for (x = 0; x < XCELLS; x++)
{
if (emap[y][x])
emap[y][x] --;
air->bmap_blockair[y][x] = (bmap[y][x]==WL_WALL || bmap[y][x]==WL_WALLELEC || bmap[y][x]==WL_BLOCKAIR || (bmap[y][x]==WL_EWALL && !emap[y][x]));
air->bmap_blockairh[y][x] = (bmap[y][x]==WL_WALL || bmap[y][x]==WL_WALLELEC || bmap[y][x]==WL_BLOCKAIR || bmap[y][x]==WL_GRAV || (bmap[y][x]==WL_EWALL && !emap[y][x])) ? 0x8:0;
}
}
// check for stacking and create BHOL if found
if (force_stacking_check || rng.chance(1, 10))
{
CheckStacking();
}
// LOVE and LOLZ element handling
if (elementCount[PT_LOVE] > 0 || elementCount[PT_LOLZ] > 0)
{
int nx, nnx, ny, nny, r, rt;
for (ny=0; ny<YRES-4; ny++)
{
for (nx=0; nx<XRES-4; nx++)
{
r=pmap[ny][nx];
if (!r)
{
continue;
}
else if ((ny<9||nx<9||ny>YRES-7||nx>XRES-10)&&(parts[ID(r)].type==PT_LOVE||parts[ID(r)].type==PT_LOLZ))
kill_part(ID(r));
else if (parts[ID(r)].type==PT_LOVE)
{
Element_LOVE_love[nx/9][ny/9] = 1;
}
else if (parts[ID(r)].type==PT_LOLZ)
{
Element_LOLZ_lolz[nx/9][ny/9] = 1;
}
}
}
for (nx=9; nx<=XRES-18; nx++)
{
for (ny=9; ny<=YRES-7; ny++)
{
if (Element_LOVE_love[nx/9][ny/9]==1)
{
for ( nnx=0; nnx<9; nnx++)
for ( nny=0; nny<9; nny++)
{
if (ny+nny>0&&ny+nny<YRES&&nx+nnx>=0&&nx+nnx<XRES)
{
rt=pmap[ny+nny][nx+nnx];
if (!rt&&Element_LOVE_RuleTable[nnx][nny]==1)
create_part(-1,nx+nnx,ny+nny,PT_LOVE);
else if (!rt)
continue;
else if (parts[ID(rt)].type==PT_LOVE&&Element_LOVE_RuleTable[nnx][nny]==0)
kill_part(ID(rt));
}
}
}
Element_LOVE_love[nx/9][ny/9]=0;
if (Element_LOLZ_lolz[nx/9][ny/9]==1)
{
for ( nnx=0; nnx<9; nnx++)
for ( nny=0; nny<9; nny++)
{
if (ny+nny>0&&ny+nny<YRES&&nx+nnx>=0&&nx+nnx<XRES)
{
rt=pmap[ny+nny][nx+nnx];
if (!rt&&Element_LOLZ_RuleTable[nny][nnx]==1)
create_part(-1,nx+nnx,ny+nny,PT_LOLZ);
else if (!rt)
continue;
else if (parts[ID(rt)].type==PT_LOLZ&&Element_LOLZ_RuleTable[nny][nnx]==0)
kill_part(ID(rt));
}
}
}
Element_LOLZ_lolz[nx/9][ny/9]=0;
}
}
}
// make WIRE work
if(elementCount[PT_WIRE] > 0)
{
for (int nx = 0; nx < XRES; nx++)
{
for (int ny = 0; ny < YRES; ny++)
{
int r = pmap[ny][nx];
if (!r)
continue;
if(parts[ID(r)].type == PT_WIRE)
parts[ID(r)].tmp = parts[ID(r)].ctype;
}
}
}
// update PPIP tmp?
if (Element_PPIP_ppip_changed)
{
for (int i = 0; i <= parts_lastActiveIndex; i++)
{
if (parts[i].type==PT_PPIP)
{
parts[i].tmp |= (parts[i].tmp&0xE0000000)>>3;
parts[i].tmp &= ~0xE0000000;
}
}
Element_PPIP_ppip_changed = 0;
}
// Simulate GoL
// GSPEED is frames per generation
if (elementCount[PT_LIFE]>0 && ++CGOL>=GSPEED)
{
SimulateGoL();
}
// wifi channel reseting
if (ISWIRE > 0)
{
for (int q = 0; q < (int)(MAX_TEMP-73.15f)/100+2; q++)
{
wireless[q][0] = wireless[q][1];
wireless[q][1] = 0;
}
ISWIRE--;
}
// spawn STKM and STK2
if (!player.spwn && player.spawnID >= 0)
create_part(-1, (int)parts[player.spawnID].x, (int)parts[player.spawnID].y, PT_STKM);
if (!player2.spwn && player2.spawnID >= 0)
create_part(-1, (int)parts[player2.spawnID].x, (int)parts[player2.spawnID].y, PT_STKM2);
// particle update happens right after this function (called separately)
}
}
void Simulation::AfterSim()
{
debug_mostRecentlyUpdated = -1;
if (emp_trigger_count)
{
// pitiful attempt at trying to keep code relating to a given element in the same file
void Element_EMP_Trigger(Simulation *sim, int triggerCount);
Element_EMP_Trigger(this, emp_trigger_count);
emp_trigger_count = 0;
}
frameCount += 1;
}
Simulation::~Simulation()
{
delete air;
}
Simulation::Simulation():
replaceModeSelected(0),
replaceModeFlags(0),
debug_nextToUpdate(0),
ISWIRE(0),
force_stacking_check(false),
emp_decor(0),
emp_trigger_count(0),
etrd_count_valid(false),
etrd_life0_count(0),
lightningRecreate(0),
gravWallChanged(false),
CGOL(0),
GSPEED(1),
edgeMode(0),
gravityMode(0),
customGravityX(0),
customGravityY(0),
legacy_enable(0),
aheat_enable(0),
water_equal_test(0),
sys_pause(0),
framerender(0),
pretty_powder(0),
sandcolour_frame(0),
deco_space(0)
{
int tportal_rx[] = {-1, 0, 1, 1, 1, 0,-1,-1};
int tportal_ry[] = {-1,-1,-1, 0, 1, 1, 1, 0};
memcpy(portal_rx, tportal_rx, sizeof(tportal_rx));
memcpy(portal_ry, tportal_ry, sizeof(tportal_ry));
currentTick = 0;
std::fill(elementCount, elementCount+PT_NUM, 0);
elementRecount = true;
//Create and attach gravity simulation
grav = Gravity::Create();
//Give air sim references to our data
grav->bmap = bmap;
//Gravity sim gives us maps to use
gravx = &grav->gravx[0];
gravy = &grav->gravy[0];
gravp = &grav->gravp[0];
gravmap = &grav->gravmap[0];
//Create and attach air simulation
air = new Air(*this);
//Give air sim references to our data
air->bmap = bmap;
air->emap = emap;
air->fvx = fvx;
air->fvy = fvy;
//Air sim gives us maps to use
vx = air->vx;
vy = air->vy;
pv = air->pv;
hv = air->hv;
msections = LoadMenus();
wtypes = LoadWalls();
platent = LoadLatent();
std::copy(GetElements().begin(), GetElements().end(), elements.begin());
tools = GetTools();
player.comm = 0;
player2.comm = 0;
init_can_move();
clear_sim();
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<CustomGOLData> newCustomGol)
{
std::sort(newCustomGol.begin(), newCustomGol.end());
customGol = newCustomGol;
}
String Simulation::ElementResolve(int type, int ctype) const
{
if (type == PT_LIFE)
{
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)
return elements[type].Name;
return "Empty";
}
String Simulation::BasicParticleInfo(Particle const &sample_part) const
{
StringBuilder sampleInfo;
int type = sample_part.type;
int ctype = sample_part.ctype;
int storedCtype = sample_part.tmp4;
if (type == PT_LAVA && IsElement(ctype))
{
sampleInfo << "Molten " << ElementResolve(ctype, -1);
}
else if ((type == PT_PIPE || type == PT_PPIP) && IsElement(ctype))
{
if (ctype == PT_LAVA && IsElement(storedCtype))
{
sampleInfo << ElementResolve(type, -1) << " with molten " << ElementResolve(storedCtype, -1);
}
else
{
sampleInfo << ElementResolve(type, -1) << " with " << ElementResolve(ctype, storedCtype);
}
}
else
{
sampleInfo << ElementResolve(type, ctype);
}
return sampleInfo.Build();
}
bool Simulation::InBounds(int x, int y)
{
return (x>=0 && y>=0 && x<XRES && y<YRES);
}
int Simulation::remainder_p(int x, int y)
{
return (x % y) + (x>=0 ? 0 : y);
}
float Simulation::remainder_p(float x, float y)
{
return std::fmod(x, y) + (x>=0 ? 0 : y);
}
constexpr size_t ce_log2(size_t n)
{
return ((n < 2) ? 1 : 1 + ce_log2(n / 2));
}
static_assert(PMAPBITS <= 16, "PMAPBITS is too large");
// * This will technically fail in some cases where (XRES * YRES) << PMAPBITS would
// fit in 31 bits but multiplication is evil and wraps around without you knowing it.
// * Whoever runs into a problem with this (e.g. with XRES = 612, YRES = 384 and
// PMAPBITS = 13) should just remove the check and take responsibility otherwise.
static_assert(ce_log2(XRES) + ce_log2(YRES) + PMAPBITS <= 31, "not enough space in pmap");