ID:2079322
 
BYOND Version:509
Operating System:Windows 10 Pro 64-bit
Web Browser:Chrome 49.0.2623.112
Applies to:DM Language
Status: Open

Issue hasn't been assigned a status value.
Descriptive Problem Summary:
Sometimes Bump() is called on dense objects that did NOT return 0 on Cross() when one of the objects on the same tile did.
Occurs in tile-based movement, not sure about pixel-based.

Numbered Steps to Reproduce Problem:
In the below example, place an /obj/DontCrossMe/ and an /obj/CrossMe on the same tile.

Code Snippet (if applicable) to Reproduce Problem:
world
fps = 25 // 25 frames per second
icon_size = 32 // 32x32 icon size by default

view = 6 // show up to 6 tiles outward from center (13x13 view)


// Make objects move 8 pixels per tick when walking
atom
movable
Bump(var/atom/movable/A)
..()
world << "BUMPING [src]|[src.type] WITH [A]|[A.type]"

mob
icon = 'whoops.dmi'

obj
density = 1
icon = 'whoops.dmi'
CrossMe
icon_state = "1"
Cross()
return 1
Bump()
world << "Oh no, we're bumping and we shouldn't be!"
DontCrossMe
icon_state = "2"
Cross(var/atom/movable/A)
world << "no crossing!"
return 0
Bump()
world << "We should be getting bumped so this is okay!" // Never reached.
..()


Expected Results:
/atom/movable/Bump() would be called with the uncrossable DontCrossMe as an argument. i.e.:
BUMPING Guest-1964986568|/mob WITH DontCrossMe|/obj/DontCrossMe
Actual Results:
BUMPING Guest-1964986568|/mob WITH CrossMe|/obj/CrossMe


When does the problem NOT occur?
I believe that this wouldn't occur if CrossMe had density = 0, although I'll test that after writing this post and describe the results in a reply.


By the way, this problem occurs both in 509 and the current 510 beta. (510.1340)
As I said above, I tested if density is the deciding factor here. Here's the slightly modified version of the code:
world
fps = 25 // 25 frames per second
icon_size = 32 // 32x32 icon size by default

view = 6 // show up to 6 tiles outward from center (13x13 view)


// Make objects move 8 pixels per tick when walking
atom
movable
Bump(var/atom/movable/A)
..()
world << "BUMPING [src]|[src.type] WITH [A]|[A.type]"

mob
icon = 'whoops.dmi'

obj
density = 1
icon = 'whoops.dmi'
CrossMe
icon_state = "1"
Cross()
return 1
DontCrossMe
icon_state = "2"
Cross(var/atom/movable/A)
world << "no crossing!"
return 0
CrossMeUndense
density = 0
icon_state = "1"
Cross()
return 1
DontCrossMeUndense
density = 0
icon_state = "2"
Cross(var/atom/movable/A)
world << "no crossing!"
return 0


In this case, Bump() is NEVER called, despite the fact that movement onto the tile was prevented by an overriden Cross(). BYOND only seems to care about density when looking for an atom use as an arg for Bump().
Are you even using pixel movement? Move() and Cross() behave very differently in the absence of pixel movement, falling back on the old behavior where Enter() handles everything for compatibility. Enter() only returns pass/fail, not a bumped object, which is why the old method of checking for a dense object has to be used.

Incidentally, your Bump() declaration is wrong; the argument is just an /atom type, not /atom/movable, since you can bump a turf. (Also you don't need the var keyword in proc arguments.)
I did say in the post that this "Occurs in tile-based movement, not sure about pixel-based." So no, I'm using tile-based movement.

I mostly assumed (or hoped) that this was unintended behavior, but seeing as how this is the old movement system, and your comment on turf/Enter, I'm guessing that's probably not the case.
Yep, this is intended behavior since it's the old system. If you can think of a way of using Cross/etc. in tile-movement games without breaking the old system, though, I'd very much like to do that. Obviously bumping the first object that denied a Cross() request would be the best way to go, and I think that could only enhance games.
You mean adding new behavior for tile-based games while retaining old behavior?

I'm not sure there's a good way to have the desired behavior here ('bumping' on Cross() failed atoms) without overriding old movement behavior in some way. You could add a proc that would fire when a Cross() is denied, though, and then continue along with other movecode behavior.

Possibly that would mean that the return value of that proc would determine whether to prevent Bump() from firing, if desired.

Let me try to pseudocode how that would work.
1. Atom1 attempts to enter a turf that contains an atom(atom2) with Cross() returning 0 always.
2. atom2 Cross() returns 0, meaning it cannot cross.
3. atom2 calls CrossDenied(atom1) AND/OR atom1 gets CannotCross(atom2) called (atom1.CannotCross(atom2))
4. Optionally, the return value of either those procs would determine whether to continue with old turf/Enter behavior, checking for dense objects to Bump()
Adding new procs is definitely not a way I would go.

It'd really be ideal if Enter() returned the blocking atom or null, instead of a 1 or 0. However, any true value returned is counted as a success.

The one bright spot is that maybe returning the blocker is an option, because I don't think there's any code in existence that returns a non-numeric result from Enter() in the non-blocking case. The trick is, that could only work on internal calls initiated by Move() directly, so it's really hacky.
I think the only real problem here is that it is completely unexpected that you would Bump() an object that has succeeded in Cross(). This causes problems by actively doing more than it should, and removing this let's people manually call bump in Cross() if they need to for nondense objects.

"Obviously bumping the first object that denied a Cross() request would be the best way to go, and I think that could only enhance games."

So I think what you described above would be sufficient. Though having Bump() based entirely on the Cross() return and not density at all would be fine as well as long as it is clear in the reference (it isn't entirely clear in the reference that Bump() is only done on dense objects at the moment)
Any hope for a modification of this before 510 becomes stable?
This would definitely not change in a minor build. I have no plans for this in 511 because there still isn't a clear solution.
In response to Lummox JR
Lummox JR wrote:
Obviously bumping the first object that denied a Cross() request would be the best way to go, and I think that could only enhance games.

Do you mean a solution code-side or the way to implement the feature for everyone to use?

Because I think your idea (quoted above) is perfectly acceptable.
The option I mentioned is really way too hacky. I don't think it's an acceptable solution.
Honestly, the whole movement system in BYOND is a lot more painful to work with than you'd think for a multitude of reasons.

This is what I wind up doing instead of using the built-in. I maintain this as a private library that I work with:

//DME-injections:
#define PIXEL_MOVEMENT
#define SUBPIXEL_MOVEMENT
#define SLIDING_MOVEMENT

#define PIXEL_LAYER 1

#define floor(x) round(x)
#define ceil(x) (-round(-(x)))
#define clamp(v,l,h) min(max(v,l),h)

var/list/__dir2ang = list(90,270,0,0,45,315,0,180,135,225,180,0,90,270,0)
#define dir2ang(d) __dir2ang[d]

#define DIR_HORIZONTAL 12
#define VIEW_WIDTH 31
#define VIEW_HEIGHT 17

#define TILE_WIDTH 32
#define TILE_HEIGHT 32
#define FPS 40
#define TICK_LAG (10/FPS)
#define ACCURATE_TIME (world.tick_usage/100*world.tick_lag + world.time)
#define WORLD_PLANE 1
#define HUD_PLANE 2


#define MOVE_TELEPORT 1
#define MOVE_STEP 2
#define MOVE_JUMP 4
#define MOVE_SELF 8
#define MOVE_SLIDE 16

var
move_manager/move_manager = new()
max_pixel_y = 0

INIT_EVENT(map_init)
max_pixel_y = world.maxy*TILE_WIDTH

move_manager
var
list/moved
proc
Update()
set waitfor = 0
while(src)
if(moved.len)
for(var/atom/movable/o in moved)
o.move_flags = 0
o.moving_dir = 0
if(o.moving_obstacles) o.moving_obstacles = null
o.moving_x = 0
o.moving_y = 0
moved.len = 0
sleep(TICK_LAG)
New()
moved = list()
Update()

atom
var
slide_stick = 0
standing = 0
tmp
base_layer = 0
proc
Bumped(atom/movable/o)

Enter(atom/movable/o,atom/oldloc)
return o.onEnter(src,oldloc,..())

Exit(atom/movable/o,atom/newloc)
return o.onExit(src,newloc,..())

Entered(atom/movable/o,atom/oldloc)
o.onEntered(src,oldloc)

Exited(atom/movable/o,atom/newloc)
o.onExited(src,newloc)

New()
if(!base_layer)
base_layer = layer
if(standing) layer = base_layer + 1 - ((y-1)*TILE_HEIGHT)/max_pixel_y*PIXEL_LAYER
..()

movable
var
substep_x = 0
substep_y = 0
move_speed = 0
move_delay = TICK_LAG
can_slide = 0
stationary = 0
face_dir = 0
tmp
move_flags = 0
moving_dir = 0
list/moving_obstacles
moving_slide = 0
moving_x = 0
moving_y = 0

last_move = -1#INF
next_move = 0
proc
onEnter(atom/o,atom/oldloc,retval)
return retval

onExit(atom/o,atom/newloc,retval)
return retval

onEntered(atom/o,atom/oldloc)

onExited(atom/o,atom/newloc)

onCross(atom/movable/o,retval)
return retval

onUncross(atom/movable/o,retval)
return retval

onCrossed(atom/movable/o)

onUncrossed(atom/movable/o)

//Move the movable by a pixel amount on each axis taking obstacles into account
Translate(x,y,Dir=0,Source,Delay=null)
if(!istype(loc,/turf)) return 0
return Move(loc,Dir,step_x+substep_x+x,step_y+substep_y+y,Source,ceil(max(abs(x),abs(y))),Delay)

//Move the movable in a direction taking obstacles into account
Step(Dir=0,Source,Speed=0,Delay=null)
if(!istype(loc,/turf)) return 0
if(!Dir) Dir = dir
if(!Speed) Speed = move_speed||step_size
var/ox = Dir&EAST ? Speed : Dir&WEST ? -Speed : 0
var/oy = Dir&NORTH ? Speed : Dir&SOUTH ? -Speed : 0
return Move(loc,Dir,step_x+substep_x+ox,step_y+substep_y+oy,Source,Speed,Delay)

#ifdef PIXEL_MOVEMENT
//Move the movable according to a vector taking obstacles into account
Project(Ang,Speed,Dir,Source,Delay=null)
if(!istype(loc,/turf)) return 0
if(!Dir) Dir = dir
if(!Speed) Speed = move_speed||step_size
var/ox = cos(Ang) * Speed
var/oy = sin(Ang) * Speed
return Move(loc,Dir,step_x+substep_x+ox,step_y+substep_y+oy,Source,Speed,Delay)
#endif

//Force the movable to move by a pixel amount on each axis. Will call Crossed/Uncrossed/Entered/Exited, but not Cross/Uncross/Enter/Exit
ForceTranslate(x,y,Dir=0,Source,Delay=null)
if(!istype(loc,/turf)) return
return ForceMove(loc,Dir,step_x+substep_x+x,step_y+substep_y+y,Source,Delay)

//Force the movable to move in a direction. Will call Crossed/Uncrossed/Entered/Exited, but not Cross/Uncross/Enter/Exit
ForceStep(Dir=0,Source,Speed=0,Delay=null)
if(!istype(loc,/turf)) return
if(!Dir) Dir = dir
if(!Speed) Speed = move_speed||step_size
var/ox = Dir&EAST ? Speed : Dir&WEST ? -Speed : 0
var/oy = Dir&NORTH ? Speed : Dir&SOUTH ? -Speed : 0
return ForceMove(loc,Dir,step_x+substep_x+ox,step_y+substep_y+oy,Source,Delay)

#ifdef PIXEL_MOVEMENT
//Force the movable to move according to a vector. Will call Crossed/Uncrossed/Entered/Exited, but not Cross/Uncross/Enter/Exit
ForceProject(Ang,Speed,Dir,Source,Delay=null)
if(!istype(loc,/turf)) return
if(!Dir) Dir = dir
if(!Speed) Speed = move_speed||step_size
var/ox = cos(Ang) * Speed
var/oy = sin(Ang) * Speed
return ForceMove(loc,Dir,step_x+substep_x+ox,step_y+substep_y+oy,Source,Delay)
#endif

//Force the movable to move to a set location. Will call Crossed/Uncrossed/Entered/Exited, but not Cross/Uncross/Enter/Exit
ForceMove(atom/NewLoc,Dir,Step_x,Step_y,Source,Delay=0)
var/worldx
var/worldy
if(NewLoc)
worldx = clamp(NewLoc.x*TILE_WIDTH + Step_x,TILE_WIDTH,(world.maxx+1)*TILE_WIDTH)
worldy = clamp(NewLoc.y*TILE_HEIGHT + Step_y,TILE_HEIGHT,(world.maxy+1)*TILE_HEIGHT)
if(Step_x<0||Step_x>TILE_WIDTH||Step_y<0||Step_y>TILE_HEIGHT)
NewLoc = locate(worldx/TILE_WIDTH,worldy/TILE_HEIGHT,NewLoc.z)
Step_x = worldx-NewLoc.x*TILE_WIDTH
Step_y = worldy-NewLoc.y*TILE_HEIGHT
var/curx = x*TILE_WIDTH+step_x+substep_x
var/cury = y*TILE_HEIGHT+step_y+substep_y
if(loc==NewLoc&&curx==worldx&&cury==worldy) return 0

if(Source&&Source!=src)
move_flags = MOVE_TELEPORT
else
move_flags = MOVE_TELEPORT|MOVE_SELF
moving_dir = 0
moving_x = 0
moving_y = 0
move_manager.moved[src] = 1

var/atom/OldLoc = loc
var/osx = step_x + substep_x
var/osy = step_y + substep_y
var/oDir = dir
var/list/olocs = locs, list/olaps = list(), list/oareas = list()
var/list/nlocs = list(), list/nlaps = list(), list/nareas = list()
if(istype(OldLoc,/turf))
olaps = obounds(src) - olocs
for(var/turf/t in olocs) oareas |= t.loc
loc = NewLoc
step_x = Step_x
step_y = Step_y
#ifdef SUBPIXEL_MOVEMENT
substep_x = Step_x-floor(Step_x)
substep_y = Step_y-floor(Step_y)
#endif
dir = Dir||dir
nlocs = locs
if(istype(NewLoc,/turf))
nlaps = obounds(src) - nlocs
for(var/turf/t in nlocs) nareas |= t.loc

var/xlocs = olocs - nlocs, xlaps = olaps-nlaps, xareas = oareas-nareas
nlocs -= olocs; nlaps -= olaps; nareas -= oareas
for(var/atom/movable/o in xlaps) o.Uncrossed(src)
for(var/atom/o in xlocs) o.Exited(src,loc)
for(var/area/a in xareas) a.Exited(src,loc)
for(var/area/a in nareas) a.Entered(src,OldLoc)
for(var/atom/o in nlocs) o.Entered(src,OldLoc)
for(var/atom/movable/o in nlaps) o.Crossed(src)

Moved(OldLoc,oDir,osx,osy,Source,Delay)

//called after a successful change of location or change of direction
Moved(atom/OldLoc,oDir,oStep_y,oStep_x,Source,Delay=0)
if(Source==src)
last_move = world.time
next_move = last_move+Delay
face_dir = dir&DIR_HORIZONTAL||dir
if(standing)
layer = base_layer + 1 - ((y-1)*TILE_HEIGHT+step_y)/max_pixel_y*PIXEL_LAYER

#ifdef PIXEL_MOVEMENT
CenterOn(atom/movable/ref,Dir=0,Source=null,Delay=0)
if(ref.z)
var/cx = ref.x*TILE_WIDTH
var/cy = ref.y*TILE_HEIGHT
if(istype(ref))
#ifdef SUBPIXEL_MOVEMENT
cx += ref.step_x+ref.bound_x+ref.bound_width/2+ref.substep_x-bound_width/2-bound_x
cy += ref.step_y+ref.bound_y+ref.bound_height/2+ref.substep_y-bound_height/2-bound_y
#elseif
cx += ref.step_x+ref.bound_x+ref.bound_width/2-bound_width/2-bound_x
cy += ref.step_y+ref.bound_y+ref.bound_height/2-bound_height/2-bound_y
#endif
else
cx += TILE_WIDTH/2
cy += TILE_HEIGHT/2
var/turf/t = locate(clamp(cx/TILE_WIDTH,1,world.maxx),clamp(cy/TILE_HEIGHT,1,world.maxy),ref.z)
ForceMove(t,Dir,cx-t.x*TILE_WIDTH,cy-t.y*TILE_HEIGHT,Source,Delay)

CenterTranslate(atom/movable/ref,ox,oy,Dir=0,Source=null,Delay=0)
if(ref.z)
var/cx = ref.x*TILE_WIDTH+ox
var/cy = ref.y*TILE_HEIGHT+oy
if(istype(ref))
#ifdef SUBPIXEL_MOVEMENT
cx += ref.step_x+ref.bound_x+ref.bound_width/2+ref.substep_x-bound_width/2-bound_x
cy += ref.step_y+ref.bound_y+ref.bound_height/2+ref.substep_y-bound_height/2-bound_y
#elseif
cx += ref.step_x+ref.bound_x+ref.bound_width/2-bound_width/2-bound_x
cy += ref.step_y+ref.bound_y+ref.bound_height/2-bound_height/2-bound_y
#endif
else
cx += TILE_WIDTH/2
cy += TILE_HEIGHT/2
var/turf/t = locate(clamp(cx/TILE_WIDTH,1,world.maxx),clamp(cy/TILE_HEIGHT,1,world.maxy),ref.z)
ForceMove(t,Dir,cx-t.x*TILE_WIDTH,cy-t.y*TILE_HEIGHT,Source,Delay)

CenterProject(atom/movable/ref,ang,dist,Dir=0,Source=null,Delay=0)
if(ref.z)
var/cx = ref.x*TILE_WIDTH+cos(ang)*dist
var/cy = ref.y*TILE_HEIGHT+sin(ang)*dist
if(istype(ref))
#ifdef SUBPIXEL_MOVEMENT
cx += ref.step_x+ref.bound_x+ref.bound_width/2+ref.substep_x-bound_width/2-bound_x
cy += ref.step_y+ref.bound_y+ref.bound_height/2+ref.substep_y-bound_height/2-bound_y
#elseif
cx += ref.step_x+ref.bound_x+ref.bound_width/2-bound_width/2-bound_x
cy += ref.step_y+ref.bound_y+ref.bound_height/2-bound_height/2-bound_y
#endif
else
cx += TILE_WIDTH/2
cy += TILE_HEIGHT/2
var/turf/t = locate(clamp(cx/TILE_WIDTH,1,world.maxx),clamp(cy/TILE_HEIGHT,1,world.maxy),ref.z)
ForceMove(t,Dir,cx-t.x*TILE_WIDTH,cy-t.y*TILE_HEIGHT,Source,Delay)
#endif

Bump(atom/o)
if(o)
if(moving_obstacles)
. = moving_obstacles.len
moving_obstacles |= o
. = .-moving_obstacles.len
else
moving_obstacles = list(o)
. = 1
#ifdef SLIDING_MOVEMENT
if(.&&o.slide_stick)
moving_slide = 0
#endif
..()
o.Bumped(src)

//changes to move to make movement code much more user friendly, less prone to bugs, and enable:
// movement delaying behavior
// directed/self movement differentiation
// edge sliding
// subpixel step preservation.
// obstacle information tracking.
Move(atom/NewLoc,Dir=0,Step_x=0,Step_y=0,Source,Speed=0,Delay=null)
if(stationary) return 0
/*NOTE: This makes calling a movement within (Un)Crossed(), Entered()/Exited(), Bump() or Bumped() potentially unsafe.
Always be sure to delay movement refiring until after the parent Move() function has had time to complete.*/

if(!Source) Source = src
if(Source==src)
if(next_move>world.time) return 0
move_flags = MOVE_SELF
else
move_flags = 0
if(!Speed) Speed = move_speed||step_size
if(Delay==null) Delay = move_delay
//calculate the movement delta
#ifdef SUBPIXEL_MOVEMENT
var/curx = x*TILE_WIDTH+step_x+substep_x
var/cury = y*TILE_HEIGHT+step_y+substep_y
#else
var/curx = x*TILE_WIDTH+step_x
var/cury = y*TILE_HEIGHT+step_y
#endif
//correct any misaligned values
if(!NewLoc)
NewLoc = loc
if(!loc) return 0
var/worldx = NewLoc.x*TILE_WIDTH + Step_x
var/worldy = NewLoc.y*TILE_HEIGHT + Step_y
moving_x = worldx-curx
moving_y = worldy-cury
if(Step_x<0||Step_x>=TILE_WIDTH||Step_y<0||Step_y>=TILE_HEIGHT)
NewLoc = locate(clamp(worldx/TILE_WIDTH,1,world.maxx),clamp(worldy/TILE_HEIGHT,1,world.maxy),NewLoc.z)
Step_x = worldx-NewLoc.x*TILE_WIDTH
Step_y = worldy-NewLoc.y*TILE_HEIGHT

moving_dir = (moving_x>0 ? EAST : moving_x<0 ? WEST : 0) + (moving_y>0 ? NORTH : moving_y<0 ? SOUTH : 0)
moving_slide = can_slide
if(moving_obstacles) moving_obstacles.len = 0
//determine if this is a jump or a step
if(max(abs(moving_x),abs(moving_y))>=Speed+1)
move_flags |= MOVE_JUMP
else
move_flags |= MOVE_STEP
//store current location data
var/oLoc = loc
var/oDir = dir
#ifdef SUBPIXEL_MOVEMENT
var/oSx = step_x + substep_x
var/oSy = step_y + substep_y
#else
var/oSx = step_x
var/oSy = step_y
#endif
//if the Speed is nonstandard, temporarily change the step size
move_manager.moved[src] = 1
var/ssz = move_speed ? floor(move_speed) : step_size
if(floor(Speed)!=ssz)
var/oss = step_size
step_size = ssz
. = ..()
if(step_size==ssz)
step_size = oss
else
. = ..()
#ifdef SUBPIXEL_MOVEMENT
curx = x*TILE_WIDTH+step_x+substep_x
cury = y*TILE_HEIGHT+step_y+substep_y
#else
curx = x*TILE_WIDTH+step_x
cury = y*TILE_HEIGHT+step_y
#endif
#ifdef SLIDING_MOVEMENT
var/remx = worldx-curx
var/remy = worldy-cury
if(abs(remx)>=1||abs(remy)>=1)
if(can_slide&&moving_slide&&moving_dir&moving_dir-1)
move_flags |= MOVE_SLIDE
NewLoc = locate(clamp(worldx/TILE_WIDTH,1,world.maxx),cury/TILE_HEIGHT,z)
Step_y = cury-floor(cury/TILE_WIDTH)*TILE_WIDTH
. += ..()
#ifdef SUBPIXEL_MOVEMENT
curx = x*TILE_WIDTH+step_x+substep_x
remx = worldx-curx
if(remx)
curx -= substep_x
substep_x = 0
else
curx -= substep_x
substep_x = Step_x-floor(Step_x)
curx += substep_x
#else
curx = x*TILE_HEIGHT+step_x
#endif
NewLoc = locate(curx/TILE_WIDTH,clamp(worldy/TILE_HEIGHT,1,world.maxy),z)
Step_x = curx-floor(curx/TILE_WIDTH)*TILE_WIDTH
Step_y = worldy-NewLoc.y*TILE_HEIGHT
. += ..()
#ifdef SUBPIXEL_MOVEMENT
cury = y*TILE_HEIGHT+step_y+substep_y
remy = worldy-cury
if(remy)
substep_y = 0
else
substep_y = Step_y-floor(Step_y)
#endif
else
if(remx) substep_x = 0
if(remy) substep_y = 0
else
substep_x = Step_x-floor(Step_x)
substep_y = Step_y-floor(Step_y)
#endif
//if the movement was successful or turned the mob's direction, call to Moved()
if(.||dir!=oDir)
Moved(oLoc,oDir,oSx,oSy,Source,Delay)
else if(!moving_obstacles||!moving_obstacles.len)
Bump(null)

New()
if(!base_layer)
base_layer = layer
if(standing) layer = base_layer + 1 - ((y-1)*TILE_HEIGHT+step_y)/max_pixel_y*PIXEL_LAYER
..()
if(!stationary)
if(!move_speed) move_speed = step_size
if(loc)
if(isturf(loc))
var/list/areas = list()
for(var/turf/t in locs)
areas |= t.loc
for(var/area/a in areas)
a.Entered(src)
for(var/turf/t in locs)
t.Entered(src)
for(var/atom/movable/o in obounds(src))
o.Crossed(src)
else
loc.Entered(src,null)
Moved(null,0,0,0,world,0)

Cross(atom/movable/o)
return o.onCross(src,..())

Uncross(atom/movable/o)
return o.onUncross(src,..())

Crossed(atom/movable/o)
return o.onCrossed(src)

Uncrossed(atom/movable/o)
return o.onUncrossed(src)


There are still cases where Bump() is called incorrectly, where Cross() and Crossed() are called incorrectly, and I honestly would like to implement a Wrap()/Unwrap()/Wrapped()/Unwrapped() function for detecting when a movable becomes fully envoloped in a turf/movable's bounding area.

Working with BYOND's movement in general is a massive pain, and a large part of it stems from nonbreaking changes made to the engine that work in loc/step_x/step_y.

Pixel movement just plain would have been simpler to work with had we implemented it without step_x/step_y and simply made x/y be integer absolute pixel coordinates when the world is in pixel mode.

But we're too far down that road now.

I've got a laundry-list of borderline unfixable annoyances with the movement system BYOND uses. A global define specifying a breaking coordinate system change between pixel and tile mode would outright fix so many problems.
Enter() being pass/fail doesn't have to be changed. The only thing that needs to be changed is how the object to be Bump()ed is selected. Right now it's just the first dense object in the turf's contents list. What about saving the object that Cross() failed on to a variable in the turf (even just an internal one) and have that get Bump()ed when Enter() fails?
In response to Exxion
Exxion wrote:
Enter() being pass/fail doesn't have to be changed. The only thing that needs to be changed is how the object to be Bump()ed is selected. Right now it's just the first dense object in the turf's contents list. What about saving the object that Cross() failed on to a variable in the turf (even just an internal one) and have that get Bump()ed when Enter() fails?

If Enter() is overridden, there would be no access to that internal var. Only once it got to the default Enter() via ..() would it work, and then the internal proc would have to have a way to know about the Move()-related state info in its stack. It's a thorny thing to set up.
This is still happening, and it's terrible.
In response to Shadowmech88
Of course it is, because I still don't know of a way to solve the problem. I'm wide open to suggestions.
So if I understand the problem, you are trying to effectively return two values because you have to both capture the object that causes a failure and the return value.

I know my head is on sideways but what I think you need is a data structure that solves that problem in the general case, which brings me back to function delegates and event hooks (I posted on this a long while ago, but it's mostly irrelevant).

Suppose a /hook is a way to call a given function on a list of objects, eg

AllowEnter = /hook(src.contents, /atom/movable/proc/Cross)
result = AllowEnter.RunUntilFail(O)

(If function delegates were a thing, a hook could also be considered a list of delegates each with maybe a different associated proc)

A /hook could be run one of a couple ways:
Run on all objects (return nothing)
Run until the first failure (return true if no failure)
Run until the first success (return false if no success)

Because it's a data structure, you could optionally store a list of returned values for the last run (presumably indexed by the object/delegate that produced it); in the case of the terminating hooks, the last value in the list will be the odd man out. Alternately, you could just store the terminating object.

The obvious problem is it probably runs way too slow to be used in low-level byond code unless you're very clever in optimizing it. In the case of movement, you know only /A/Ms are stored in the contents list, but the /hook data structure would have no such guarantee (maybe filter datums when added instead of at runtime?).

Whether this is the right or wrong, I think you won't be able to solve the problem without either some kind of new tool or a very ugly hack.
@Lummox:

My movement redux actually stores all of this information internally in the data structure itself. BYOND is a general solution, therefore has to assume that any relevant information the engine could generate could potentially be useful, and therefore should be hung on to.

How I keep this information from causing garbage collection issues is that I run a sweeper using a global collection and dirty-marking.

Basic walkthrough of the process:

1) Move is called:
a) Check if object is marked as dirty.
b) if dirty, do nothing. if not dirty, append obstacle to global dirty collection.
c) Empty existing obstacles collection.

2) Enter is called:
a) check for collision. If so, add self to obstacles collection.
b) check for overlap.

3) Cross is called:
a) check for collision. If so, add self to obstacles collection.

4) Move is returning:
a) check if collisions has length
b) loop through collisions. Call Bump()


?) Tick is called:
a) loop through all dirty marked objects.
b) zero-len the collection of obstacles on all objects marked dirty.
c) wait for next tick.


Tick() is my sleeper function that gets called once per tick. It ensures any hanging references are cleared within a single frame of a movement failing, causing only deliberate caching of this data to hang the garbage collector.

In order to use the information in atom.obstacles, I would have to Copy() the collection of obstacles in order to hang the garbage collector with that list. Otherwise, just like args lists already are in BYOND, it is emptied just after the data is no longer possibly relevant unless explicitly handled by the user.


Stack-only/class-only/instance-only programming is fucking stupid. Global variables exist. The global sweep pattern is useful. Don't limit yourself to a pattern just because it can't live alone in the stack.
Hey, here's a "solution" that you might find acceptable:
Either allow gliding to be forced on even with pixel movement, or make pixel gliding more flexible to allow for pretty much the same thing.
Clearly that doesn't ACTUALLY solve this problem with tile movement, but it does allow tile movement games to get around this problem without having to rewrite all of their movement code.
Page: 1 2