From 0d00a675d034aeb7b7747bfa80da180ee7a5f0de Mon Sep 17 00:00:00 2001 From: turleypol Date: Mon, 8 Jul 2024 11:35:25 +0200 Subject: [PATCH] Boat refactor Part 1 of N (#654) * test for traveller movement due to turn/move check relative position of a component * refactor BoatComponents, turn/move --- pol-core/pol/multi/boat.cpp | 329 ++++++++-------------- pol-core/pol/multi/boat.h | 23 +- testsuite/pol/testpkgs/boat/test_boat.src | 79 ++++++ 3 files changed, 213 insertions(+), 218 deletions(-) diff --git a/pol-core/pol/multi/boat.cpp b/pol-core/pol/multi/boat.cpp index 786ef5135..ec20148b1 100644 --- a/pol-core/pol/multi/boat.cpp +++ b/pol-core/pol/multi/boat.cpp @@ -82,15 +82,13 @@ BoatShape::ComponentShape::ComponentShape( const std::string& str, unsigned char graphic = static_cast( strtoul( tmp.c_str(), nullptr, 0 ) ); if ( graphic ) { - unsigned short xd, yd; + s16 xd, yd; if ( is >> xd >> yd ) { - xdelta = xd; - ydelta = yd; - zdelta = 0; - signed short zd; + delta = Core::Vec3d( xd, yd, 0 ); + s16 zd; if ( is >> zd ) - zdelta = zd; + delta.z( zd ); return; } } @@ -113,15 +111,13 @@ BoatShape::ComponentShape::ComponentShape( const std::string& str, const std::st graphic = static_cast( strtoul( tmp.c_str(), nullptr, 0 ) ); if ( graphic ) { - unsigned short xd, yd; + s16 xd, yd; if ( is >> xd >> yd ) { - xdelta = xd; - ydelta = yd; - zdelta = 0; - signed short zd; + delta = Core::Vec3d( xd, yd, 0 ); + s16 zd; if ( is >> zd ) - zdelta = zd; + delta.z( zd ); ok = true; } } @@ -282,13 +278,13 @@ bool BoatShapeExists( u16 multiid ) return Core::gamestate.boatshapes.count( multiid ) != 0; } -void UBoat::send_smooth_move( Network::Client* client, Core::UFACING move_dir, u8 speed, u16 newx, - u16 newy, bool relative ) +// TODO: why not send after actual movement like the other pkts +void UBoat::send_smooth_move( Network::Client* client, Core::UFACING move_dir, u8 speed, + const Core::Pos4d& newpos, bool relative ) const { Network::PktHelper::PacketOut msg; - u16 xmod = newx - x(); - u16 ymod = newy - y(); + auto posdelta = newpos.xyz() - pos3d(); Core::UFACING b_facing = boat_facing(); if ( relative == false ) @@ -301,10 +297,10 @@ void UBoat::send_smooth_move( Network::Client* client, Core::UFACING move_dir, u msg->Write( move_dir ); msg->Write( b_facing ); - msg->WriteFlipped( newx ); - msg->WriteFlipped( newy ); - msg->WriteFlipped( ( z() < 0 ) ? static_cast( 0x10000 + z() ) - : static_cast( z() ) ); + msg->WriteFlipped( newpos.x() ); + msg->WriteFlipped( newpos.y() ); + msg->WriteFlipped( ( newpos.z() < 0 ) ? static_cast( 0x10000 + newpos.z() ) + : static_cast( newpos.z() ) ); // 0xf000 encoding room, huffman can take more space then the maximum of 0xffff const u16 max_count = ( 0xf000 - 18 ) / 10; @@ -322,10 +318,11 @@ void UBoat::send_smooth_move( Network::Client* client, Core::UFACING move_dir, u if ( component == nullptr || component->orphan() ) continue; msg->Write( component->serial_ext ); - msg->WriteFlipped( static_cast( component->x() + xmod ) ); - msg->WriteFlipped( static_cast( component->y() + ymod ) ); - msg->WriteFlipped( static_cast( ( component->z() < 0 ) ? ( 0x10000 + component->z() ) - : ( component->z() ) ) ); + const auto comppos = component->pos() + posdelta; + msg->WriteFlipped( comppos.x() ); + msg->WriteFlipped( comppos.y() ); + msg->WriteFlipped( + static_cast( ( comppos.z() < 0 ) ? ( 0x10000 + comppos.z() ) : ( comppos.z() ) ) ); ++object_count; } for ( auto& travellerRef : travellers_ ) @@ -353,10 +350,11 @@ void UBoat::send_smooth_move( Network::Client* client, Core::UFACING move_dir, u continue; } msg->Write( obj->serial_ext ); - msg->WriteFlipped( static_cast( obj->x() + xmod ) ); - msg->WriteFlipped( static_cast( obj->y() + ymod ) ); + const auto objpos = obj->pos() + posdelta; + msg->WriteFlipped( objpos.x() ); + msg->WriteFlipped( objpos.y() ); msg->WriteFlipped( - static_cast( ( obj->z() < 0 ) ? ( 0x10000 + obj->z() ) : ( obj->z() ) ) ); + static_cast( ( objpos.z() < 0 ) ? ( 0x10000 + objpos.z() ) : ( objpos.z() ) ) ); ++object_count; } u16 len = msg->offset; @@ -368,12 +366,11 @@ void UBoat::send_smooth_move( Network::Client* client, Core::UFACING move_dir, u msg.Send( client, len ); } -void UBoat::send_smooth_move_to_inrange( Core::UFACING move_dir, u8 speed, u16 newx, u16 newy, - bool relative ) +void UBoat::send_smooth_move_to_inrange( Core::UFACING move_dir, u8 speed, + const Core::Pos4d& newpos, bool relative ) const { - const Core::Pos2d newpos = Core::Pos2d( newx, newy ); Core::WorldIterator::InMaxVisualRange( - newpos, realm(), + newpos, [&]( Mobile::Character* zonechr ) { Network::Client* client = zonechr->client; @@ -381,7 +378,7 @@ void UBoat::send_smooth_move_to_inrange( Core::UFACING move_dir, u8 speed, u16 n if ( zonechr->in_visual_range( this, newpos ) && zonechr->in_visual_range( this ) && client->ClientType & Network::CLIENTTYPE_7090 ) // send this only to those who see the // old location aswell - send_smooth_move( client, move_dir, speed, newx, newy, relative ); + send_smooth_move( client, move_dir, speed, newpos, relative ); } ); } @@ -760,7 +757,7 @@ bool UBoat::on_ship( const BoatContext& bc, const UObject* obj ) if ( item->container != nullptr ) return false; } - Core::Vec2d rxy = obj->pos2d() - Core::Pos2d( bc.x, bc.y ); + Core::Vec2d rxy = obj->pos2d() - bc.oldpos.xy(); return bc.mdef.body_contains( rxy ); } @@ -768,6 +765,7 @@ bool UBoat::on_ship( const BoatContext& bc, const UObject* obj ) void UBoat::move_travellers( Core::UFACING move_dir, const BoatContext& oldlocation, unsigned short newx, unsigned short newy, Realms::Realm* oldrealm ) { + Core::Pos2d newpos2d{ newx, newy }; bool any_orphans = false; for ( auto& travellerRef : travellers_ ) @@ -783,28 +781,27 @@ void UBoat::move_travellers( Core::UFACING move_dir, const BoatContext& oldlocat } obj->set_dirty(); + auto oldpos = obj->pos(); + auto newtravellerpos = obj->pos(); + if ( newx != USHRT_MAX && + newy != USHRT_MAX ) // dave added 3/27/3, if move_xy was used, dont use facing + { + // keeps relative distance from boat mast + auto delta = newtravellerpos.xy() - oldlocation.oldpos.xy(); + newtravellerpos.xy( newpos2d + delta ); + } + else + { + newtravellerpos.move_to( move_dir ); + } if ( obj->ismobile() ) { Mobile::Character* chr = static_cast( obj ); + chr->lastpos = oldpos; + chr->setposition( newtravellerpos ); if ( chr->logged_in() ) { - Core::Pos4d oldpos = chr->pos(); - chr->lastpos = oldpos; - - if ( newx != USHRT_MAX && - newy != USHRT_MAX ) // dave added 3/27/3, if move_xy was used, dont use facing - { - s16 dx, dy; - dx = chr->x() - oldlocation.x; // keeps relative distance from boat mast - dy = chr->y() - oldlocation.y; - chr->setposition( Core::Pos4d( chr->pos() ).x( newx + dx ).y( newy + dy ) ); - } - else - { - chr->setposition( chr->pos().move( move_dir ) ); - } - MoveCharacterWorldPosition( oldpos, chr ); chr->position_changed(); if ( chr->client != nullptr ) @@ -836,57 +833,16 @@ void UBoat::move_travellers( Core::UFACING move_dir, const BoatContext& oldlocat } chr->move_reason = Mobile::Character::MULTIMOVE; } - else - { - // characters that are logged out move with the boat - // they aren't in the worldzones so this is real easy. - chr->lastpos = chr->pos(); // I think in this case setting last? isn't - // necessary, but I'll do it anyway. - - if ( newx != USHRT_MAX && - newy != USHRT_MAX ) // dave added 3/27/3, if move_xy was used, dont use facing - { - s16 dx, dy; - dx = chr->x() - oldlocation.x; // keeps relative distance from boat mast - dy = chr->y() - oldlocation.y; - chr->setposition( Core::Pos4d( chr->pos() ).x( newx + dx ).y( newy + dy ) ); - } - else - { - chr->setposition( chr->pos().move( move_dir ) ); - } - } } else { Items::Item* item = static_cast( obj ); - Core::Pos4d oldpos = item->pos(); - if ( newx != USHRT_MAX && - newy != USHRT_MAX ) // dave added 4/9/3, if move_xy was used, dont use facing - { - s16 dx, dy; - dx = item->x() - oldlocation.x; // keeps relative distance from boat mast - dy = item->y() - oldlocation.y; + item->setposition( newtravellerpos ); - item->set_dirty(); - - item->setposition( Core::Pos4d( item->pos() ).x( newx + dx ).y( newy + dy ) ); - - if ( Core::settingsManager.ssopt.refresh_decay_after_boat_moves ) - item->restart_decay_timer(); - MoveItemWorldPosition( oldpos, item ); - } - else - { - item->set_dirty(); - - item->setposition( item->pos().move( move_dir ) ); - - if ( Core::settingsManager.ssopt.refresh_decay_after_boat_moves ) - item->restart_decay_timer(); - MoveItemWorldPosition( oldpos, item ); - } + if ( Core::settingsManager.ssopt.refresh_decay_after_boat_moves ) + item->restart_decay_timer(); + MoveItemWorldPosition( oldpos, item ); Core::WorldIterator::InMaxVisualRange( item, @@ -917,30 +873,29 @@ void UBoat::move_travellers( Core::UFACING move_dir, const BoatContext& oldlocat remove_orphans(); } -void UBoat::turn_traveller_coords( Mobile::Character* chr, RELATIVE_DIR dir ) +Core::Pos4d UBoat::turn_coords( const Core::Pos4d& oldpos, RELATIVE_DIR dir ) const { - chr->lastpos = chr->pos(); - - s16 xd = chr->x() - x(); - s16 yd = chr->y() - y(); - + Core::Vec2d delta = oldpos.xy() - pos2d(); switch ( dir ) { case LEFT: - chr->setposition( Core::Pos4d( x() + yd, y() - xd, chr->z(), chr->realm() ) ); - chr->facing = static_cast( ( chr->facing + 6 ) & 7 ); + delta = Core::Vec2d( delta.y(), -delta.x() ); break; case AROUND: - chr->setposition( Core::Pos4d( x() - xd, y() - yd, chr->z(), chr->realm() ) ); - chr->facing = static_cast( ( chr->facing + 4 ) & 7 ); + delta.x( -delta.x() ).y( -delta.y() ); break; case RIGHT: - chr->setposition( Core::Pos4d( x() - yd, y() + xd, chr->z(), chr->realm() ) ); - chr->facing = static_cast( ( chr->facing + 2 ) & 7 ); + delta = Core::Vec2d( -delta.y(), delta.x() ); break; case NO_TURN: - break; + return oldpos; } + return pos() + delta; +} + +u8 UBoat::turn_facing( u8 oldfacing, RELATIVE_DIR dir ) const +{ + return ( ( dir * 2 ) + oldfacing ) & 7; } void UBoat::turn_travellers( RELATIVE_DIR dir, const BoatContext& oldlocation ) @@ -960,17 +915,17 @@ void UBoat::turn_travellers( RELATIVE_DIR dir, const BoatContext& oldlocation ) } obj->set_dirty(); + Core::Pos4d oldpos = obj->pos(); + obj->setposition( turn_coords( oldpos, dir ) ); if ( obj->ismobile() ) { Mobile::Character* chr = static_cast( obj ); + chr->lastpos = oldpos; + chr->setfacing( turn_facing( chr->facing, dir ) ); if ( chr->logged_in() ) { // send_remove_character_to_nearby( chr ); - Core::Pos4d oldpos = chr->pos(); - turn_traveller_coords( chr, dir ); - - Core::MoveCharacterWorldPosition( oldpos, chr ); chr->position_changed(); if ( chr->client != nullptr ) @@ -999,41 +954,10 @@ void UBoat::turn_travellers( RELATIVE_DIR dir, const BoatContext& oldlocation ) // deletes. // chr->lasty = ~ (unsigned short) 0; } - else - { - turn_traveller_coords( chr, dir ); - } } else { Items::Item* item = static_cast( obj ); - s16 xd = item->x() - x(); - s16 yd = item->y() - y(); - u16 newx( 0 ); - u16 newy( 0 ); - switch ( dir ) - { - case NO_TURN: - newx = item->x(); - newy = item->y(); - break; - case LEFT: - newx = x() + yd; - newy = y() - xd; - break; - case AROUND: - newx = x() - xd; - newy = y() - yd; - break; - case RIGHT: - newx = x() - yd; - newy = y() + xd; - break; - } - item->set_dirty(); - - Core::Pos4d oldpos = item->pos(); - item->setposition( Core::Pos4d( item->pos() ).x( newx ).y( newy ) ); if ( Core::settingsManager.ssopt.refresh_decay_after_boat_moves ) item->restart_decay_timer(); @@ -1290,64 +1214,60 @@ bool UBoat::move( Core::UFACING dir, u8 speed, bool relative ) auto newpos = pos().move( move_dir ); - if ( navigable( multidef(), newpos ) ) - { - BoatContext bc( *this ); - - send_smooth_move_to_inrange( move_dir, speed, newpos.x(), newpos.y(), relative ); - - set_dirty(); - - move_multi_in_world( x(), y(), newpos.x(), newpos.y(), this, realm() ); - - const Core::Pos4d oldpos = pos(); - setposition( newpos ); - - // NOTE, send_boat_to_inrange pauses those it sends to. - // send_boat_to_inrange( this, oldx, oldy ); - move_travellers( move_dir, bc, x(), y(), realm() ); - move_components( realm() ); - - Core::WorldIterator::InMaxVisualRange( - this, - [&]( Mobile::Character* zonechr ) - { - Network::Client* client = zonechr->client; - if ( !zonechr->in_visual_range( this ) ) - return; - if ( client->ClientType & Network::CLIENTTYPE_7090 ) - { - if ( zonechr->in_visual_range( this, oldpos ) ) - return; - else - send_boat_newly_inrange( client ); // send HSA packet only for newly inrange - } - else - { - if ( client->ClientType & Network::CLIENTTYPE_7000 ) - send_boat( client ); // Send - else - send_boat_old( client ); - } - } ); - - Core::WorldIterator::InMaxVisualRange( - oldpos, - [&]( Mobile::Character* zonechr ) - { - if ( zonechr->in_visual_range( this, oldpos ) && - !zonechr->in_visual_range( this ) ) // send remove to chrs only seeing the old loc - send_remove_boat( zonechr->client ); - } ); - - do_tellmoves(); - unpause_paused(); - return true; - } - else - { + if ( !navigable( multidef(), newpos ) ) return false; - } + + BoatContext bc( *this ); + + send_smooth_move_to_inrange( move_dir, speed, newpos, relative ); + + set_dirty(); + + move_multi_in_world( x(), y(), newpos.x(), newpos.y(), this, realm() ); + + const Core::Pos4d oldpos = pos(); + setposition( newpos ); + + // NOTE, send_boat_to_inrange pauses those it sends to. + // send_boat_to_inrange( this, oldx, oldy ); + move_travellers( move_dir, bc, x(), y(), realm() ); + move_components( realm() ); + + Core::WorldIterator::InMaxVisualRange( + this, + [&]( Mobile::Character* zonechr ) + { + Network::Client* client = zonechr->client; + if ( !zonechr->in_visual_range( this ) ) + return; + if ( client->ClientType & Network::CLIENTTYPE_7090 ) + { + if ( zonechr->in_visual_range( this, oldpos ) ) + return; // TODO POS: better place for smooth move + else + send_boat_newly_inrange( client ); // send HSA packet only for newly inrange + } + else + { + if ( client->ClientType & Network::CLIENTTYPE_7000 ) + send_boat( client ); // Send + else + send_boat_old( client ); + } + } ); + + Core::WorldIterator::InMaxVisualRange( + oldpos, + [&]( Mobile::Character* zonechr ) + { + if ( zonechr->in_visual_range( this, oldpos ) && + !zonechr->in_visual_range( this ) ) // send remove to chrs only seeing the old loc + send_remove_boat( zonechr->client ); + } ); + + do_tellmoves(); + unpause_paused(); + return true; } inline unsigned short UBoat::multiid_ifturn( RELATIVE_DIR dir ) @@ -1418,8 +1338,7 @@ void UBoat::transform_components( const BoatShape& old_boatshape, Realms::Realm* Core::Pos4d oldpos = item->pos(); - item->setposition( - pos() + Core::Vec3d( itr2->xdelta, itr2->ydelta, static_cast( itr2->zdelta ) ) ); + item->setposition( pos() + itr2->delta ); MoveItemWorldPosition( oldpos, item ); @@ -1483,8 +1402,7 @@ void UBoat::move_components( Realms::Realm* /*oldrealm*/ ) item->set_dirty(); Core::Pos4d oldpos = item->pos(); - item->setposition( - pos() + Core::Vec3d( itr2->xdelta, itr2->ydelta, static_cast( itr2->zdelta ) ) ); + item->setposition( pos() + itr2->delta ); MoveItemWorldPosition( oldpos, item ); @@ -1535,7 +1453,7 @@ bool UBoat::turn( RELATIVE_DIR dir ) send_display_boat_to_inrange( x(), y() ); do_tellmoves(); unpause_paused(); - facing = ( ( dir * 2 ) + facing ) & 7; + facing = turn_facing( facing, dir ); result = true; } else @@ -1830,8 +1748,7 @@ void UBoat::create_components() component->graphic = itr->graphic; // component itemdesc entries generally have graphic=1, so they don't get their height set. component->height = Plib::tileheight( component->graphic ); - component->setposition( - pos() + Core::Vec3d( itr->xdelta, itr->ydelta, static_cast( itr->zdelta ) ) ); + component->setposition( pos() + itr->delta ); component->disable_decay(); component->movable( false ); add_item_to_world( component ); diff --git a/pol-core/pol/multi/boat.h b/pol-core/pol/multi/boat.h index ccbd1067b..8ec4e1867 100644 --- a/pol-core/pol/multi/boat.h +++ b/pol-core/pol/multi/boat.h @@ -77,9 +77,7 @@ struct BoatShape unsigned int objtype; unsigned short graphic; unsigned short altgraphic; - unsigned short xdelta; - unsigned short ydelta; - signed short zdelta; + Core::Vec3d delta; ComponentShape( const std::string& str, const std::string& altstr, unsigned char type ); ComponentShape( const std::string& str, unsigned char type ); }; @@ -99,10 +97,9 @@ class UBoat final : public UMulti class BoatContext { const MultiDef& mdef; - unsigned short x; // TODO Pos2d - unsigned short y; + Core::Pos4d oldpos; - explicit BoatContext( const UBoat& ub ) : mdef( ub.multidef() ), x( ub.x() ), y( ub.y() ){}; + explicit BoatContext( const UBoat& ub ) : mdef( ub.multidef() ), oldpos( ub.pos() ){}; friend class UBoat; BoatContext& operator=( const BoatContext& ) { return *this; } }; @@ -130,7 +127,7 @@ public: bool move( Core::UFACING dir, u8 speed, bool relative ); bool move_xy( const Core::Pos2d& newp, int flags, Realms::Realm* oldrealm ); - enum RELATIVE_DIR + enum RELATIVE_DIR // order matters! facing = ( ( dir * 2 ) + facing ) & 7; { NO_TURN, RIGHT, @@ -143,10 +140,6 @@ public: virtual void unregister_object( Core::UObject* obj ) override; Core::UFACING boat_facing() const; - void send_smooth_move( Network::Client* client, Core::UFACING move_dir, u8 speed, u16 newx, - u16 newy, bool relative ); - void send_smooth_move_to_inrange( Core::UFACING move_dir, u8 speed, u16 newx, u16 newy, - bool relative ); void send_display_boat( Network::Client* client ); void send_display_boat_to_inrange( u16 oldx = USHRT_MAX, u16 oldy = USHRT_MAX ); void send_boat( Network::Client* client ); @@ -192,7 +185,6 @@ protected: unsigned short x = USHRT_MAX, unsigned short y = USHRT_MAX, Realms::Realm* oldrealm = nullptr ); void turn_travellers( RELATIVE_DIR dir, const BoatContext& oldlocation ); - void turn_traveller_coords( Mobile::Character* chr, RELATIVE_DIR dir ); static bool on_ship( const BoatContext& bc, const Core::UObject* obj ); void move_offline_mobiles( const Core::Pos4d& newpos ); const MultiDef& multi_ifturn( RELATIVE_DIR dir ); @@ -226,6 +218,13 @@ protected: friend struct BoatMoveGuard; private: + void send_smooth_move( Network::Client* client, Core::UFACING move_dir, u8 speed, + const Core::Pos4d& newpos, bool relative ) const; + void send_smooth_move_to_inrange( Core::UFACING move_dir, u8 speed, const Core::Pos4d& newpos, + bool relative ) const; + + Core::Pos4d turn_coords( const Core::Pos4d& oldpos, RELATIVE_DIR dir ) const; + u8 turn_facing( u8 oldfacing, RELATIVE_DIR dir ) const; void create_components(); typedef Core::UObjectRef Traveller; typedef std::vector Travellers; diff --git a/testsuite/pol/testpkgs/boat/test_boat.src b/testsuite/pol/testpkgs/boat/test_boat.src index 2822e2da0..ab95c4304 100644 --- a/testsuite/pol/testpkgs/boat/test_boat.src +++ b/testsuite/pol/testpkgs/boat/test_boat.src @@ -20,6 +20,12 @@ exported function create() DestroyMulti(res); return ret_error(er); endif + if (res.portplank.x != 10 || res.portplank.y != 48 || res.portplank.z != -4) + var er:="Wrong portplank pos: {},{},{}".format(res.portplank.x, res.portplank.y, res.portplank.z); + DestroyMulti(res); + return ret_error(er); + endif + for i:=1 to 5 sleepms(10); if (res.getprop("started")) @@ -269,3 +275,76 @@ endfunction DestroyMulti(boat); return 1; endfunction*/ + +exported function test_boat_turn_travellers() + var boat:=CreateMultiAtLocation(10,50,-4,0x11000, CRMULTI_FACING_EAST); + if (!boat) + return ret_error("Failed to create boat "+boat); + endif + var item:=CreateItemAtLocation(10,51,0,0xf3f); + if (!item) + DestroyMulti(boat); + return ret_error($"Failed to create item {item}"); + endif + var npc := CreateNPCFromTemplate( ":testnpc:probe_npc", 9, 50, 0 ); + if ( !npc ) + DestroyItem(item); + DestroyMulti(boat); + return ret_error( $"Failed to create npc: {npc}" ); + endif + var checks:={ + {1,4, 9,50, 10,49}, // right + {3,2, 10,51, 9,50}, // left + {2,6, 10,49, 11,50}, // around + {0,6, 10,49, 11,50} // nothing + }; + var result:=1; + foreach check in checks + var res:=TurnBoat(boat,check[1]); + if (!res) + result:=ret_error($"Failed to turn {check} {res}"); + break; + endif + if (boat.facing != check[2] || item.x != check[3] || item.y != check[4] || npc.x != check[5] || npc.y != check[6]) + result := ret_error($"Check failed {check}: facing {boat.facing} item {item.x},{item.y} npc {npc.x},{npc.y}"); + break; + endif + endforeach + MoveObjectToLocation( npc, 100, 100, 0, flags := MOVEOBJECT_FORCELOCATION ); + npc.kill(); + DestroyItem(item); + DestroyMulti(boat); + return result; +endfunction + +exported function test_boat_move_travellers() + var boat:=CreateMultiAtLocation(10,50,-4,0x11000, CRMULTI_FACING_EAST); + if (!boat) + return ret_error("Failed to create boat "+boat); + endif + var item:=CreateItemAtLocation(10,51,0,0xf3f); + if (!item) + DestroyMulti(boat); + return ret_error($"Failed to create item {item}"); + endif + var npc := CreateNPCFromTemplate( ":testnpc:probe_npc", 9, 50, 0 ); + if ( !npc ) + DestroyItem(item); + DestroyMulti(boat); + return ret_error( $"Failed to create npc: {npc}" ); + endif + var result:=1; + var res:=MoveBoat(boat,0); + if (!res) + result:=ret_error($"Failed to move {res}"); + else + if ( item.x != 10 || item.y != 50 || npc.x != 9 || npc.y != 49) + result := ret_error($"Move failed: item {item.x},{item.y} npc {npc.x},{npc.y}"); + endif + endif + MoveObjectToLocation( npc, 100, 100, 0, flags := MOVEOBJECT_FORCELOCATION ); + npc.kill(); + DestroyItem(item); + DestroyMulti(boat); + return result; +endfunction