From 99297659ee9387b0e40d23db44e9b16dc5715113 Mon Sep 17 00:00:00 2001 From: Misa Date: Fri, 9 Oct 2020 17:09:11 -0700 Subject: [PATCH] Restore platform evaluation order to 2.2 This commit restores the evaluation order of moving platforms and conveyors to be what it was in 2.2. The evaluation order changed in 2.3 after the patchset to improve the handling of the `obj.entities` and `obj.blocks` vectors (#191). By evaluation order, I'm talking about the order in which platforms and conveyors will be evaluated (and thus will take priority) if Viridian stands on both a conveyor or platform at once, and they either have different speeds or are pointing in different directions. Nowhere in the main game is there a place where you can stand on two different conveyors/platforms at once, so this is solely within the territory of custom levels, which is my specialty. So what caused this evaluation order to change? Well, every moving platform and conveyor in the game is actually made up of two objects: an entity, and a block. The entity is the part that moves around, and the block is the part that actually has the collision. But if the entity is the part that moves around, and entities and blocks are in entirely separate vectors, how is the block part going to move along with it? Well, maybe you'd guess some sort of unique ID system, but spend some time digging around the code and you won't find any trace of any (there's no attribute on an entity to store such an ID, for starters). Instead, what the game does is actually remove all blocks that coincide with the exact top-left corner of the entity, and then create a new one. Destroying and creating blocks like this all the time is hugely wasteful, but hey, it worked. So why did the evaluation order change in 2.3? Well, to understand that, you'll need to understand 2.2's `active` system. Instead of having an object be real simply by virtue of it existing, 2.2 had this system where the object was only real if it had its `active` attribute set to true. In other words, you would be looking at a fake object that didn't actually exist if its `active` attribute was false. On the surface, this doesn't seem that bad. But this can lead to "holes" in a given vector of objects. A hole is simply an inactive object neighbored by active objects (or the inactive object could be the first one in the vector, but then have an active object immediately following it). If you have a vector of 3 objects, all of them active, then removing the second one will result in the vector containing an active object, followed by an inactive object, followed by an active one. However, since the switch to more properly use vectors instead of relying on this `active` system, there's no longer any way for holes to exist in a vector. Properly removing an object from a vector will just shift the rest of the objects down, so if we remove the second object after the vector fix, then this will simply move the third object into the slot of where the second object used to be. So, what happens if you destroy a block and then create a new one in the `active` system? Let's say that your `obj.blocks` looks like this, and here I'm denoting each block by writing out its coordinates: [30,60] [70,90] [80,100] and that you want to update the position of the second one, because the entity that that blocks belongs to has been updated. Okay, so, you delete that block, which then makes things look like this: [30,60] [-] [80,100] and then afterwards, you create a new block with the updated position, resulting in this: [30,60] [74,90] [80,100] Since `entityclass::createblock()` will find the first block slot that has a false `active` attribute, it puts the new object in the same slot as the old one. What has been essentially done here is that the slot of the block has basically been reserved for the new block with the new position. Here, the evaluation order of each block will stay the same. But then 2.3 comes along and changes things up. So we start with an `obj.blocks` like this again: [30,60] [70,90] [80,100] and we want to update the second block, like before. So we remove the second block, resulting in this: [30,60] [80,100] It should be obvious that unlike before, where the third block stayed in the third slot, the third block has now been moved to the second slot. But continuing on; we are now going to create the new block with its updated position, resulting in this: [30,60] [80,100] [70,90] At this point, we can see that the evaluation order of these blocks has been changed due to the fact that the third block has now been moved to the slot that was previously the slot of the second block. So what can we do about this? Well, we can basically emulate what VVVVVV did in 2.2, which is disable a block without actually removing it - except I'm not going to reintroduce an `active` attribute or anything. I'll disable the collision of all blocks at a certain position by setting their widths and heights to 0, and then re-enable them later by finding the first block at that same position, updating its position, and re-assigning its width and height again. The former is what `entityclass::nocollisionat()` does; the latter is what `entityclass::moveblockto()` does. The former mimicks turning off the `active` attribute of all blocks sharing a certain top-left corner; the latter mimicks creating a new block - and it will only do this for one block, because `entityclass::createblock()` in 2.2 only looked for the first block with a false `active` attribute. Now, some quirks relied on the previous behavior of destroying and creating blocks, but all of these quirks have been preserved with the way I implemented this fix. The first quirk is that platforms passing through 0,0 will destroy all spike hitboxes, script boxes, activity zones, and one-way hitboxes in the room. The hitboxes of moving platforms, disappearing platforms, 1x1 quicksand, and conveyors will not be affected. This is a consequence of the fact that the former group uses the `x` and `y` of their `rect`, while the latter group uses the `xp` and `yp` attributes. So the `xp` and `yp` of the former are both 0. Meaning, a platform passing through 0,0 destroys them all. Having these separate coordinates seems like an artifact from the Flash days. (And furthermore, there's an unused `x` and `y` attribute on all blocks, making for technically three separate sets of coordinates! This should probably be cleaned up, except for what I'm about to say...) But actually, if you merge both sets of coordinates into one, this lets moving platforms destroy script boxes and activity zones if it passes through the top-left corner of them, which is probably far worse than the destruction being localized to a specific coordinate that would never likely be reached normally. This quirk is preserved just fine without any special-casing, because instead of destroying all blocks at 0,0, they just get disabled, which does the same job. This quirk seems trivial to fix if I made it so that the position of a platform's block was updated instantaneously instead of having one step to disable it and another step to re-enable it, but I aim to preserve as much quirks as possible. The second quirk is that a moving platform passing through the top-left corner of a disappearing platform or 1x1 quicksand will destroy the block of that disappearing platform. This is because, again, when a moving platform updates, it destroys all blocks at its previous position, not just only one block. This is automatically preserved because this commit just disables the block of the disappearing platform instead of removing it. Just like the last one, this quirk seems extremely trivial to fix, and this time by simply making it so `entityclass::nocollisionat()` would have a `break` statement, i.e. only disabling the first block it finds instead of all blocks it finds, but I want to keep all quirks that are possible to keep. The last quirk is that, apparently, in order to prevent pushing the player vertically out of a moving platform if they get inside of one, the game destroys the block of the moving platform. If I had missed this edge case, then the block would've been destroyed, leaving the moving platform with no collision. But I caught it in my testing, so the block gets disabled instead of destroyed. Also, it seems obtuse for those who don't understand why a platform's block gets destroyed or disabled whenever the player collides with it in `entityclass::collisioncheck()`, so I've put up a comment documenting it as well. The different platform evaluation order desyncs my Nova TAS, but after applying this patchset and #504, my TAS syncs fine (save for the different walkingframe from starting immediately on the ground instead of in the air (#502), but that's minor and can be easily fixed). I've attached a test level to the pull request for this commit (#503) to demonstrate that this patchset not only fixes platform evaluation order, but preserves some bugs and quirks with the existing block system. The first room demonstrates the fixed platform evaluation order, by stepping on the conveyors that both point into each other. In 2.2, Viridian will move to the right of the background pillar, but in 2.3, Viridian will move to the left of the pillar. However, after applying this patch, Viridian will now move to the right of the pillar once again. The second room demonstrates that the platform-passing-through-0,0 trick still works (as explained above). The last room demonstrates that platforms passing through the top-left corners of disappearing platforms or 1x1 quicksand will remove the blocks of those entities, causing Viridian to immediately pass through them. This trick is still preserved after my patchset is applied. --- desktop_version/src/Entity.cpp | 7 ++++- desktop_version/src/Logic.cpp | 52 ++++++++++++++++++++++++++-------- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/desktop_version/src/Entity.cpp b/desktop_version/src/Entity.cpp index f9c264aa..9d43985e 100644 --- a/desktop_version/src/Entity.cpp +++ b/desktop_version/src/Entity.cpp @@ -4684,7 +4684,12 @@ void entityclass::collisioncheck(int i, int j, bool scm /*= false*/) } break; case 2: //Moving platforms - if (entitycollide(i, j)) removeblockat(entities[j].xp, entities[j].yp); + if (entitycollide(i, j)) + { + //Disable collision temporarily so we don't push the person out! + //Collision will be restored at end of platform update loop in gamelogic + nocollisionat(entities[j].xp, entities[j].yp); + } break; case 3: //Entity to entity if(entities[j].onentity>0) diff --git a/desktop_version/src/Logic.cpp b/desktop_version/src/Logic.cpp index 3c409f12..3b5984db 100644 --- a/desktop_version/src/Logic.cpp +++ b/desktop_version/src/Logic.cpp @@ -894,14 +894,16 @@ void gamelogic() continue; } - obj.removeblockat(obj.entities[i].xp, obj.entities[i].yp); + int prevx = obj.entities[i].xp; + int prevy = obj.entities[i].yp; + obj.nocollisionat(prevx, prevy); bool entitygone = obj.updateentities(i); // Behavioral logic if (entitygone) continue; obj.updateentitylogic(i); // Basic Physics obj.entitymapcollision(i); // Collisions with walls - obj.createblock(0, obj.entities[i].xp, obj.entities[i].yp, obj.entities[i].w, obj.entities[i].h); + obj.moveblockto(prevx, prevy, obj.entities[i].xp, obj.entities[i].yp, obj.entities[i].w, obj.entities[i].h); obj.movingplatformfix(i, obj.getplayer()); if (game.supercrewmate) { @@ -920,14 +922,16 @@ void gamelogic() continue; } - obj.removeblockat(obj.entities[ie].xp, obj.entities[ie].yp); + int prevx = obj.entities[ie].xp; + int prevy = obj.entities[ie].yp; + obj.nocollisionat(prevx, prevy); bool entitygone = obj.updateentities(ie); // Behavioral logic if (entitygone) continue; obj.updateentitylogic(ie); // Basic Physics obj.entitymapcollision(ie); // Collisions with walls - obj.hormovingplatformfix(ie); + obj.moveblockto(prevx, prevy, obj.entities[ie].xp, obj.entities[ie].yp, obj.entities[ie].w, obj.entities[ie].h); } //is the player standing on a moving platform? int i = obj.getplayer(); @@ -1061,13 +1065,19 @@ void gamelogic() //ascii snakes if (obj.entities[i].xp <= -80) { - if (obj.entities[i].isplatform) obj.removeblockat(obj.entities[i].xp, obj.entities[i].yp); + if (obj.entities[i].isplatform) + { + obj.moveblockto(obj.entities[i].xp, obj.entities[i].yp, obj.entities[i].xp + 400, obj.entities[i].yp, obj.entities[i].w, obj.entities[i].h); + } obj.entities[i].xp += 400; obj.entities[i].oldxp += 400; } else if (obj.entities[i].xp > 320) { - if (obj.entities[i].isplatform) obj.removeblockat(obj.entities[i].xp, obj.entities[i].yp); + if (obj.entities[i].isplatform) + { + obj.moveblockto(obj.entities[i].xp, obj.entities[i].yp, obj.entities[i].xp - 400, obj.entities[i].yp, obj.entities[i].w, obj.entities[i].h); + } obj.entities[i].xp -= 400; obj.entities[i].oldxp -= 400; } @@ -1076,13 +1086,19 @@ void gamelogic() { if (obj.entities[i].xp <= -10) { - if (obj.entities[i].isplatform) obj.removeblockat(obj.entities[i].xp, obj.entities[i].yp); + if (obj.entities[i].isplatform) + { + obj.moveblockto(obj.entities[i].xp, obj.entities[i].yp, obj.entities[i].xp + 320, obj.entities[i].yp, obj.entities[i].w, obj.entities[i].h); + } obj.entities[i].xp += 320; obj.entities[i].oldxp += 320; } else if (obj.entities[i].xp > 310) { - if (obj.entities[i].isplatform) obj.removeblockat(obj.entities[i].xp, obj.entities[i].yp); + if (obj.entities[i].isplatform) + { + obj.moveblockto(obj.entities[i].xp, obj.entities[i].yp, obj.entities[i].xp - 320, obj.entities[i].yp, obj.entities[i].w, obj.entities[i].h); + } obj.entities[i].xp -= 320; obj.entities[i].oldxp -= 320; } @@ -1098,13 +1114,19 @@ void gamelogic() if(obj.entities[i].type<50){ //Don't warp warp lines if (obj.entities[i].yp <= -12) { - if (obj.entities[i].isplatform) obj.removeblockat(obj.entities[i].xp, obj.entities[i].yp); + if (obj.entities[i].isplatform) + { + obj.moveblockto(obj.entities[i].xp, obj.entities[i].yp, obj.entities[i].xp, obj.entities[i].yp + 232, obj.entities[i].w, obj.entities[i].h); + } obj.entities[i].yp += 232; obj.entities[i].oldyp += 232; } else if (obj.entities[i].yp > 226) { - if (obj.entities[i].isplatform) obj.removeblockat(obj.entities[i].xp, obj.entities[i].yp); + if (obj.entities[i].isplatform) + { + obj.moveblockto(obj.entities[i].xp, obj.entities[i].yp, obj.entities[i].xp, obj.entities[i].yp - 232, obj.entities[i].w, obj.entities[i].h); + } obj.entities[i].yp -= 232; obj.entities[i].oldyp -= 232; } @@ -1122,13 +1144,19 @@ void gamelogic() { if (obj.entities[i].xp <= -30) { - if (obj.entities[i].isplatform) obj.removeblockat(obj.entities[i].xp, obj.entities[i].yp); + if (obj.entities[i].isplatform) + { + obj.moveblockto(obj.entities[i].xp, obj.entities[i].yp, obj.entities[i].xp + 350, obj.entities[i].yp, obj.entities[i].w, obj.entities[i].h); + } obj.entities[i].xp += 350; obj.entities[i].oldxp += 350; } else if (obj.entities[i].xp > 320) { - if (obj.entities[i].isplatform) obj.removeblockat(obj.entities[i].xp, obj.entities[i].yp); + if (obj.entities[i].isplatform) + { + obj.moveblockto(obj.entities[i].xp, obj.entities[i].yp, obj.entities[i].xp - 350, obj.entities[i].yp, obj.entities[i].w, obj.entities[i].h); + } obj.entities[i].xp -= 350; obj.entities[i].oldxp -= 350; }