1
0
Fork 0
mirror of https://github.com/AquariaOSE/Aquaria.git synced 2025-04-11 09:16:38 +00:00

Rework and fix entity spawning and ID handling.

Bugfix: Actually call postInit() for entities spawned via "se" node.
Bugfix: Don't call postInit() twice for the active pet

Previously, f55a70b459 changed entity ID handling so that
script-spawned entities have negative IDs and entities placed
on the map have positive IDs.
Turns out that positive IDs were still flaky due to frequent renumbering
upon map load. Typically entity IDs are assigned sequentially, and if,
upon loading, an entity with an already existing ID was to be spawned,
that new entity would be assigned a new unique ID (often its ID+1).
That in turn would often cascade into a long string of IDs falsely
considered conflicting because one bad entity started the entire thing.

During lots of map editing I've noticed that sometimes after adding an
entity to an older map, saving, and loading, many entities could be shuffled
around on the map. I'm not 100% sure this bug is related (and it may or
may not be fixed, i don't know yet, time will tell)
but at least entity numbering should be fine now.

This change properly and permanently renumbers entities so that upon map save,
the problem will no longer occur on subsequent loads and entity IDs
will be stable from then on.
Duplicate entity IDs should also no longer happen.

Also kill Entity::entityTypeIdx because there's no need to store it.

And yes i do feel bad for nuking that funny comment block :<
This commit is contained in:
fgenesis 2023-05-09 04:49:06 +02:00
parent 33e8c0643d
commit aa067be1f7
10 changed files with 256 additions and 178 deletions

View file

@ -202,7 +202,6 @@ Entity::Entity()
{
entityProperties[i] = false;
}
entityTypeIdx = -1;
damageTime = vars->entityDamageTime;
slowingToStopPathTimer = 0;
slowingToStopPath = 0;
@ -2446,6 +2445,7 @@ void Entity::fillGrid()
}
}
/*
void Entity::assignUniqueID(bool temporary)
{
const int inc = temporary ? -1 : 1;
@ -2473,24 +2473,12 @@ void Entity::assignUniqueID(bool temporary)
}
entityID = id;
}
*/
// caller must make sure that the ID is unused
void Entity::setID(int id)
{
entityID = id;
FOR_ENTITIES(i)
{
Entity *e = *i;
if (e != this)
{
if (e->getID() == entityID)
{
std::ostringstream os;
os << "ID conflict between " << name << " and " << e->name;
debugLog(os.str());
e->assignUniqueID(e->getID() < 0);
}
}
}
}
int Entity::getID()

View file

@ -70,8 +70,6 @@ public:
virtual void postInit(){}
Vector lastPosition;
// postInit gets called after the entity IDs are determined
int entityTypeIdx;
enum ActivationType
{
ACT_NONE = -1,

View file

@ -164,7 +164,7 @@ Ingredient *Game::spawnIngredient(const std::string &ing, const Vector &pos, int
i->velocity.x = 0;
i->velocity.y = -500;
}
establishEntity(i);
establishEntity(i, findUnusedEntityID(true), pos);
//addRenderObject(i, LR_ENTITIES);
}
else
@ -179,7 +179,7 @@ void Game::spawnIngredientFromEntity(Entity *ent, IngredientData *data)
{
Ingredient *i = new Ingredient(ent->position, data);
ingredients.push_back(i);
establishEntity(i);
establishEntity(i, findUnusedEntityID(true), ent->position);
//addRenderObject(i, LR_ENTITIES);
}
@ -892,32 +892,17 @@ int Game::getIdxForEntityType(std::string type)
return -1;
}
Entity *Game::createEntity(int idx, int id, Vector position, int rot, bool createSaveData, std::string name, EntityType et, bool doPostInit)
{
std::string type;
for (size_t i = 0; i < dsq->game->entityTypeList.size(); i++)
{
EntityClass *ec = &dsq->game->entityTypeList[i];
if (ec->idx == idx)
{
type = ec->name;
return createEntity(type, id, position, rot, createSaveData, name, et, doPostInit);
}
}
return 0;
}
// ensure a limit of entity types in the current level
// older entities with be culled if state is set to 0
// otherwise, the older entities will have the state set
void Game::ensureLimit(Entity *e, int num, int state)
void Game::ensureLimit(Entity *me, int num, int state)
{
int idx = e->entityTypeIdx;
int c = 0;
std::list<Entity*> entityList;
std::vector<Entity*> entityList;
FOR_ENTITIES(i)
{
if ((*i)->entityTypeIdx == idx && (state == 0 || (*i)->getState() != state))
Entity *e = *i;
if ((state == 0 || e->getState() != state) && !nocasecmp(me->name, e->name))
{
entityList.push_back(*i);
c++;
@ -927,7 +912,7 @@ void Game::ensureLimit(Entity *e, int num, int state)
int numDelete = c-(num+1);
if (numDelete >= 0)
{
for (std::list<Entity*>::iterator i = entityList.begin(); i != entityList.end(); i++)
for (std::vector<Entity*>::iterator i = entityList.begin(); i != entityList.end(); i++)
{
if (state == 0)
(*i)->safeKill();
@ -937,88 +922,40 @@ void Game::ensureLimit(Entity *e, int num, int state)
if (numDelete <= 0)
break;
}
}
}
Entity* Game::establishEntity(Entity *e, int id, Vector position, int rot, bool createSaveData, std::string name, EntityType et, bool doPostInit)
void Game::establishEntity(Entity *e, int id, Vector startPos)
{
// e->layer must be set BEFORE calling this function!
std::string type = e->name;
stringToLower(type);
assert(id); // 0 is invalid/reserved
assert(!getEntityByID(id)); // must not already exist
e->setID(id);
e->position = startPos;
e->startPos = startPos;
// i'm thinking this *should* hold up for new files
// it will mess up if you're importing an old file
// the logic being that you're not going to load in an non-ID-specified entity in new files
// so assignUniqueID should never be called
// so what i'm going to do is have it bitch
// note that when not loading a scene, it is valid to call assignUniqueID here
if (id != 0)
{
e->setID(id);
}
else
{
if (loadingScene)
{
std::ostringstream os;
os << "ERROR: Assigning Unique ID to a loaded Entity... if this is called from loadScene then Entity IDs may be invalid";
os << "\nEntityName: " << e->name;
errorLog(os.str());
}
else
{
e->assignUniqueID(!createSaveData); // when entity is placed on map, give positive ID; otherwise, if script-spawned, give negative ID
}
}
// NOTE: init cannot be called after "addRenderObject" for some unknown reason
// most scripts call setupEntity() in init(), which also sets the layer.
// otherwise the script is expected to set the render layer here if not using the default.
e->init();
Vector usePos = position;
e->startPos = usePos;
if (!name.empty())
e->name = name;
e->rotation.z = rot;
int idx = getIdxForEntityType(type);
e->entityTypeIdx = idx;
if (createSaveData)
{
int idx = dsq->game->getIdxForEntityType(type);
entitySaveData.push_back(EntitySaveData(e, idx, usePos.x, usePos.y, rot, e->getID(), e->name));
}
addRenderObject(e, e->layer);
if (doPostInit)
{
e->postInit();
}
return e;
addRenderObject(e, e->layer); // layer was just set in init()
}
Entity *Game::createEntity(const std::string &t, int id, Vector position, int rot, bool createSaveData, std::string name, EntityType et, bool doPostInit)
Entity* Game::getEntityByID(int id) const
{
std::string type = t;
stringToLower(type);
ScriptedEntity *e;
e = new ScriptedEntity(type, position, et);
return establishEntity(e, id, position, rot, createSaveData, name, et, doPostInit);
FOR_ENTITIES(i)
{
Entity *e = *i;
if (e->getID() == id)
return e;
}
return NULL;
}
EntitySaveData *Game::getEntitySaveDataForEntity(Entity *e, Vector pos)
EntitySaveData *Game::getEntitySaveDataForEntity(Entity *e)
{
for (size_t i = 0; i < entitySaveData.size(); i++)
@ -1031,6 +968,95 @@ EntitySaveData *Game::getEntitySaveDataForEntity(Entity *e, Vector pos)
return 0;
}
int Game::findUnusedEntityID(bool temporary) const
{
const int inc = temporary ? -1 : 1;
int id = 0;
retry:
id += inc;
FOR_ENTITIES(i)
{
Entity *e = *i;
if (e->getID() == id)
goto retry;
}
return id;
}
// caller must do e->postInit() when all map entities have been created
Entity* Game::createEntityOnMap(const EntitySaveData& sav)
{
assert(sav.id > 0);
std::string type = sav.name;
// legacy entities have no name recorded and instead use the idx specified in scripts/entities.txt
// newer entities and all those added by mods have idx==-1 and use the name directly
if(type.empty())
{
for (size_t i = 0; i < entityTypeList.size(); i++)
{
const EntityClass& ec = entityTypeList[i];
if (ec.idx == sav.idx)
{
type = ec.name;
break;
}
}
if(type.empty())
{
std::ostringstream os;
os << "Game::createEntityOnMap: Don't know entity type name for idx " << sav.idx;
errorLog(os.str());
return NULL;
}
}
if(Entity *e = getEntityByID(sav.id))
{
assert(false);
return NULL; // can't spawn, entity with that ID is already present
}
stringToLower(type);
Vector pos(sav.x, sav.y);
ScriptedEntity *e = new ScriptedEntity(type, pos, ET_ENEMY);
e->rotation.z = sav.rot;
int idx = getIdxForEntityType(type);
entitySaveData.push_back(EntitySaveData(sav.id, sav.x, sav.y, sav.rot, sav.idx, type, e));
establishEntity(e, sav.id, pos);
return e;
}
Entity *Game::createEntityOnMap(const char * type, const Vector pos)
{
int id = findUnusedEntityID(false);
EntitySaveData data;
data.id = id;
data.idx = -1;
data.name = type;
data.rot = 0;
data.x = pos.x;
data.y = pos.y;
return createEntityOnMap(data);
}
Entity* Game::createEntityTemp(const char* type, Vector pos, bool doPostInit)
{
int id = findUnusedEntityID(true);
ScriptedEntity *e = new ScriptedEntity(type, pos, ET_ENEMY);
establishEntity(e, id, pos);
// it's possible that we're loading a map, and an entity spawned via createEntityOnMap()
// calls createEntity() in its script's init() function. That's when we end up here.
// Delay postInit() until we're sure that the map has been loaded, then call postInit() for all.
if(doPostInit && !loadingScene)
e->postInit(); // if we're already running the map, do postInit() now
return e;
}
void Game::setTimerTextAlpha(float a, float t)
{
timerText->alpha.interpolateTo(a, t);
@ -1965,30 +1991,31 @@ bool Game::loadSceneXML(std::string scene)
this->reconstructGrid(true);
std::vector<EntitySaveData> toSpawn;
XMLElement *entitiesNode = doc.FirstChildElement("Entities");
while(entitiesNode)
{
if (entitiesNode->Attribute("j"))
{
SimpleIStringStream is(entitiesNode->Attribute("j"));
int idx, x, y, rot, groupID, id;
std::string name;
while (is >> idx)
EntitySaveData sav;
int unusedGroupID;
while (is >> sav.idx)
{
name="";
if (idx == -1)
is >> name;
is >> x >> y >> rot >> groupID >> id;
if (!name.empty())
dsq->game->createEntity(name, id, Vector(x,y), rot, true, "", ET_ENEMY);
else
dsq->game->createEntity(idx, id, Vector(x,y), rot, true, "", ET_ENEMY);
sav.name.clear();
if (sav.idx == -1)
is >> sav.name;
if(is >> sav.x >> sav.y >> sav.rot >> unusedGroupID >> sav.id)
toSpawn.push_back(sav);
}
}
entitiesNode = entitiesNode->NextSiblingElement("Entities");
}
if(toSpawn.size())
spawnEntities(&toSpawn[0], toSpawn.size());
this->reconstructGrid(true);
rebuildElementUpdateList();
@ -1997,6 +2024,90 @@ bool Game::loadSceneXML(std::string scene)
return true;
}
void Game::spawnEntities(const EntitySaveData *sav, size_t n)
{
std::vector<size_t> conflicting, usable;
for(size_t i = 0; i < n; ++i)
{
const EntitySaveData& es = sav[i];
// check for ID conflicts
int id = es.id;
bool renumber = id <= 0; // entities spawned on map load must have id > 0
if(!renumber)
{
for(size_t k = 0; k < i; ++k)
{
if(sav[k].id == id)
{
renumber = true;
break;
}
}
}
if(!renumber)
usable.push_back(i);
else
conflicting.push_back(i);
}
{
std::ostringstream os;
os << "Game::spawnEntities: Spawning " << usable.size() << " entities";
if(conflicting.size())
os << " without issues, another " << conflicting.size() << " have ID conflicts and need to be renumbered";
debugLog(os.str());
}
size_t failed = 0;
// create all entities that are possible to spawn without ID conflicts
for(size_t i = 0; i < usable.size(); ++i)
{
const EntitySaveData& es = sav[usable[i]];
failed += !createEntityOnMap(es);
}
// spawn and renumber the rest
int lastid = 0;
for(size_t i = 0; i < conflicting.size(); ++i)
{
// find an unused ID
int id = lastid; // assume any ID up to this is already taken...
bool ok;
do
{
++id; // ... which is why the first thing we do is to increment this
ok = true;
for(size_t k = 0; k < n; ++k)
{
if(sav[k].id == id)
{
ok = false;
break;
}
}
}
while(!ok);
lastid = id;
EntitySaveData es = sav[conflicting[i]];
std::ostringstream os;
os << "Renumbering entity [" << es.idx << ", " << es.name << "] id " << es.id << " to " << id;
debugLog(os.str());
es.id = id;
failed += !createEntityOnMap(es);
}
if(failed)
{
std::ostringstream os;
os << "Game::spawnEntities: Failed to spawn " << failed << " entities, on map [" << sceneName << "]";
errorLog(os.str());
}
}
void Game::setMusicToPlay(const std::string &m)
{
musicToPlay = m;
@ -2269,6 +2380,7 @@ bool Game::isValidTarget(Entity *e, Entity *me)
void Game::createPets()
{
debugLog("createPets()");
setActivePet(dsq->continuity.getFlag(FLAG_PET_ACTIVE));
}
@ -2290,9 +2402,9 @@ Entity* Game::setActivePet(int flag)
PetData *p = dsq->continuity.getPetData(petv);
if (p)
{
std::string name = p->namePart;
std::string name = "Pet_" + p->namePart;
Entity *e = createEntity("Pet_" + name, -1, avatar->position, 0, false, "");
Entity *e = createEntityTemp(name.c_str(), avatar->position, false);
if (e)
{
currentPet = e;
@ -2315,7 +2427,7 @@ void Game::createLi()
if (liFlag == 100)
{
debugLog("Creating Li");
li = createEntity("Li", 0, Vector(0,0), 0, false, "");
li = createEntityTemp("Li", Vector(0,0), false);
//li->skeletalSprite.animate("idle");
}
}
@ -2416,6 +2528,7 @@ void Game::entityDied(Entity *eDead)
void Game::postInitEntities()
{
debugLog("postInitEntities()");
FOR_ENTITIES(i)
{
Entity *e = *i;

View file

@ -130,10 +130,11 @@ typedef std::vector<Element*> ElementUpdateList;
struct EntitySaveData
{
public:
EntitySaveData(Entity *e, int idx, int x, int y, int rot, int id, const std::string &name) : e(e), idx(idx), x(x), y(y), rot(rot), id(id), name(name) {}
Entity *e;
int idx, x, y, rot, id;
EntitySaveData() : id(0), x(0), y(0), rot(0), idx(0), e(0) {}
EntitySaveData(int id, int x, int y, int rot, int idx, const std::string &name, Entity *e) : name(name), id(id), x(x), y(y), rot(rot), idx(idx), e(e) {}
std::string name;
int id, x, y, rot, idx;
Entity *e;
};
class Game : public StateObject
@ -240,10 +241,13 @@ public:
Vector getWallNormal(Vector pos, int sampleArea = 5, int obs = -1);
void updateMiniMapHintPosition();
EntitySaveData *getEntitySaveDataForEntity(Entity *e, Vector pos);
Entity *createEntity(int idx, int id, Vector position, int rot, bool createSaveData, std::string name, EntityType = ET_ENEMY, bool doPostInit=false);
Entity *createEntity(const std::string &type, int id, Vector position, int rot, bool createSaveData, std::string name, EntityType = ET_ENEMY, bool doPostInit=false);
Entity *establishEntity(Entity *e, int id=0, Vector position=Vector(0,0), int rot=0, bool createSaveData=false, std::string name="", EntityType = ET_ENEMY,bool doPostInit=false);
EntitySaveData *getEntitySaveDataForEntity(Entity *e);
Entity *createEntityOnMap(const EntitySaveData& sav); // when loading from save (saved to map). Caller must postInit().
Entity *createEntityOnMap(const char *type, Vector pos); // when spawning in the editor (saved to map). Caller must postInit().
Entity *createEntityTemp(const char *type, Vector pos, bool doPostInit); // when spawning via script (not saved to map). Never does postInit() if we're currently loading a map.
void establishEntity(Entity *e, int id, Vector startPos);
Entity *getEntityByID(int id) const;
int findUnusedEntityID(bool temporary) const; // pass temporary=true for script-spawned entities, false for entities that are spawned on the map
void setCameraFollow(RenderObject *r);
void setCameraFollowEntity(Entity *e);
@ -505,6 +509,7 @@ protected:
RenderObject *cameraFollowObject;
Entity *cameraFollowEntity;
bool loadSceneXML(std::string scene);
void spawnEntities(const EntitySaveData *sav, size_t n);
void toggleSceneEditor();

View file

@ -532,7 +532,7 @@ void Path::update(float dt)
if (neverSpawned || !(nodes[0].position - dsq->game->avatar->position).isLength2DIn(spawnEnemyDistance))
{
neverSpawned = false;
spawnedEntity = dsq->game->createEntity(spawnEnemyName, 0, nodes[0].position, 0, false, "");
spawnedEntity = dsq->game->createEntityTemp(spawnEnemyName.c_str(), nodes[0].position, true);
}
}
if (spawnedEntity && spawnedEntity->life < 1.0f)

View file

@ -1218,12 +1218,11 @@ void SceneEditor::updateEntitySaveData(Entity *editingEntity)
if (editingEntity)
{
EntitySaveData *d = dsq->game->getEntitySaveDataForEntity(editingEntity, oldPosition);
EntitySaveData *d = dsq->game->getEntitySaveDataForEntity(editingEntity);
if (d)
{
std::ostringstream os;
os << "idx1: " << d->idx << " ";
os << "idx2: " << editingEntity->entityTypeIdx << " ";
os << "idx: " << d->idx << " ";
os << "name: " << editingEntity->name;
debugLog(os.str());
@ -2429,10 +2428,9 @@ void SceneEditor::placeElement()
}
else if (editType == ET_ENTITIES)
{
if (!selectedEntity.nameBased)
dsq->game->createEntity(selectedEntity.index, 0, dsq->getGameCursorPosition(), 0, true, "", ET_ENEMY, true);
else
dsq->game->createEntity(selectedEntity.name, 0, dsq->getGameCursorPosition(), 0, true, "", ET_ENEMY, true);
Entity *e = dsq->game->createEntityOnMap(selectedEntity.name.c_str(), dsq->getGameCursorPosition());
if(e)
e->postInit();
}
else if (editType == ET_PATHS)
{

View file

@ -4172,8 +4172,9 @@ luaFunc(entity_createEntity)
{
Entity *e = entity(L);
Entity *ret = NULL;
if (e)
ret = dsq->game->createEntity(dsq->getEntityTypeIndexByName(getString(L, 2)), 0, e->position, 0, false, "", ET_ENEMY, true);
const char *name = lua_tostring(L, 2);
if (e && name)
ret = dsq->game->createEntityTemp(name, e->position, true);
luaReturnPtr(ret);
}
@ -4779,15 +4780,16 @@ luaFunc(spawnAroundEntity)
float radius = lua_tonumber(L, 3);
std::string entType = getString(L, 4);
std::string name = getString(L, 5);
int idx = dsq->game->getIdxForEntityType(entType);
if (e)
{
Vector pos = e->position;
const Vector center = e->position;
for (int i = 0; i < num; i++)
{
float angle = i*((2*PI)/float(num));
e = dsq->game->createEntity(idx, 0, pos + Vector(sinf(angle)*radius, cosf(angle)*radius), 0, false, name);
Vector spawnPos = center + Vector(sinf(angle)*radius, cosf(angle)*radius);
Entity *spawned = dsq->game->createEntityTemp(entType.c_str(), spawnPos, true);
if(spawned && !name.empty())
spawned->setName(name);
}
}
luaReturnNil();
@ -4905,8 +4907,9 @@ luaFunc(createEntity)
int x = lua_tointeger(L, 3);
int y = lua_tointeger(L, 4);
Entity *e = 0;
e = dsq->game->createEntity(type, 0, Vector(x, y), 0, false, name, ET_ENEMY, true);
Entity *e = dsq->game->createEntityTemp(type.c_str(), Vector(x, y), true);
if(e && !name.empty())
e->setName(name);
luaReturnPtr(e);
}
@ -7602,40 +7605,8 @@ luaFunc(entity_getID)
luaFunc(getEntityByID)
{
//debugLog("Calling getEntityByID");
int v = lua_tointeger(L, 1);
Entity *found = 0;
if (v)
{
//std::ostringstream os;
//os << "searching for entity with id: " << v;
//debugLog(os.str());
FOR_ENTITIES(i)
{
Entity *e = *i;
if (e->getID() == v)
{
found = e;
break;
}
}
/*if (!found)
{
std::ostringstream os;
os << "entity with id: " << v << " not found!";
debugLog(os.str());
}
else
{
std::ostringstream os;
os << "Found: " << found->name;
debugLog(os.str());
}*/
}
/*else
{
debugLog("entity ID was 0");
}*/
Entity *found = dsq->game->getEntityByID(v);
luaReturnPtr(found);
}

View file

@ -39,6 +39,7 @@ ScriptedEntity::ScriptedEntity(const std::string &scriptName, Vector position, E
strandSpacing = 10;
animKeyFunc = true;
canShotHitFunc = true;
postInitDone = false;
setEntityType(et);
@ -120,6 +121,9 @@ void ScriptedEntity::init()
void ScriptedEntity::postInit()
{
if(postInitDone)
return;
postInitDone = true;
if (script)
{
if (!script->call("postInit", this))

View file

@ -112,6 +112,7 @@ protected:
void onExitState(int action);
virtual void deathNotify(RenderObject *r);
bool canShotHitFunc;
bool postInitDone;
};
#endif

View file

@ -487,7 +487,7 @@ bool Shot::onHitWall(bool reflect)
{
if (!shotData->spawnEntity.empty())
{
dsq->game->createEntity(shotData->spawnEntity, 0, position, 0, false, "", ET_ENEMY, true);
dsq->game->createEntityTemp(shotData->spawnEntity.c_str(), position, true);
if (shotData->spawnEntity == "NatureFormFlowers")
{