misc tests

This commit is contained in:
turleypol 2026-08-10 21:45:58 +02:00
parent c80bcebfd5
commit dfd27eaf26
11 changed files with 1597 additions and 0 deletions

View file

@ -0,0 +1,111 @@
use uo;
use os;
use datafile;
include "testutil";
const DF_NAME := ":TestMisc:dstest";
program testdatastore()
return 1;
endprogram
// The datafile and element handles are reference objects of their own, each with its own
// type name and integer.
exported function datafile_refs_report_types( unused resmngr )
var df := CreateDataFile( DF_NAME, DF_KEYTYPE_STRING );
if ( !df )
return ret_error( $"failed to create datafile: {df}" );
endif
var elem := df.CreateElement( "a" );
if ( !elem )
return ret_error( $"failed to create element: {elem}" );
endif
var res := 1;
if ( TypeOf( df ) != "DataFileRef" )
res := ret_error( $"unexpected datafile type name: {TypeOf( df )}" );
elseif ( TypeOf( elem ) != "DataElemRef" )
res := ret_error( $"unexpected element type name: {TypeOf( elem )}" );
elseif ( TypeOfInt( df ) == TypeOfInt( elem ) )
res := ret_error( "datafile and element share a type integer" );
endif
UnloadDataFile( DF_NAME );
return res;
endfunction
// A method neither handle recognises is looked up by name and answers falsy.
exported function datafile_unknown_methods( unused resmngr )
var df := CreateDataFile( DF_NAME, DF_KEYTYPE_STRING );
if ( !df )
return ret_error( $"failed to create datafile: {df}" );
endif
var elem := df.CreateElement( "a" );
if ( !elem )
return ret_error( $"failed to create element: {elem}" );
endif
var res := 1;
if ( df.no_such_method_at_all() )
res := ret_error( "datafile answered an unknown method" );
elseif ( elem.no_such_method_at_all() )
res := ret_error( "element answered an unknown method" );
endif
UnloadDataFile( DF_NAME );
return res;
endfunction
// DeleteElement removes a key, and deleting one that was never there is harmless.
exported function datafile_delete_element( unused resmngr )
var df := CreateDataFile( DF_NAME, DF_KEYTYPE_STRING );
if ( !df )
return ret_error( $"failed to create datafile: {df}" );
endif
df.CreateElement( "a" );
df.CreateElement( "b" );
var res := 1;
if ( df.Keys().size() != 2 )
res := ret_error( $"expected two keys, got {df.Keys()}" );
else
df.DeleteElement( "a" );
if ( df.Keys().size() != 1 )
res := ret_error( $"expected one key after delete, got {df.Keys()}" );
elseif ( df.FindElement( "a" ) )
res := ret_error( "the deleted element is still findable" );
else
df.DeleteElement( "no_such_key" );
if ( df.Keys().size() != 1 )
res := ret_error( "deleting a missing key changed the key set" );
endif
endif
endif
UnloadDataFile( DF_NAME );
return res;
endfunction
// ListDataFiles reports what is loaded, and UnloadDataFile takes it back out.
exported function datafile_list_and_unload( unused resmngr )
var df := CreateDataFile( DF_NAME, DF_KEYTYPE_STRING );
if ( !df )
return ret_error( $"failed to create datafile: {df}" );
endif
var listed := ListDataFiles();
if ( TypeOf( listed ) != "Array" )
UnloadDataFile( DF_NAME );
return ret_error( $"ListDataFiles did not answer an array: {TypeOf( listed )}" );
endif
var res := UnloadDataFile( DF_NAME );
if ( !res )
return ret_error( $"failed to unload datafile: {res}" );
endif
return 1;
endfunction

View file

@ -0,0 +1,161 @@
use uo;
use os;
include "testutil";
// A patch of map reserved for this file so the mobiles it spawns cannot be picked up by
// another package's listing test.
const LX := 150;
const LY := 150;
const LZ := 0;
program testlisting()
return 1;
endprogram
function find_serial( list, serial )
foreach entry in list
if ( entry.serial == serial )
return 1;
endif
endforeach
return 0;
endfunction
// ListMobilesNearLocation finds what is standing inside the range and nothing outside it.
exported function list_mobiles_near_location( resmngr )
var npc := resmngr.CreateNPCFromTemplate( ":TestNPC:probe_npc", LX, LY, LZ, 0,
_DEFAULT_REALM, 1 );
if ( !npc )
return ret_error( $"failed to create npc: {npc}" );
endif
var near := ListMobilesNearLocation( LX, LY, LZ, 2 );
if ( !find_serial( near, npc.serial ) )
return ret_error( $"the npc was not listed within range: {near.size()} found" );
endif
// A location well away from the npc does not list it.
var far := ListMobilesNearLocation( LX + 40, LY + 40, LZ, 2 );
if ( find_serial( far, npc.serial ) )
return ret_error( "the npc was listed from a location out of range" );
endif
return 1;
endfunction
// The Ex form takes two kinds of flag and needs one of each. The state flags -- NORMAL,
// HIDDEN, GHOST, CONCEALED -- decide which mobiles qualify at all, and the ONLY flags then
// narrow by kind. A mobile that matches no state flag is never listed, whatever else is asked
// for, so an ONLY flag on its own selects nothing at all.
exported function list_mobiles_near_ex( resmngr )
var npc := resmngr.CreateNPCFromTemplate( ":TestNPC:probe_npc", LX, LY, LZ, 0,
_DEFAULT_REALM, 1 );
if ( !npc )
return ret_error( $"failed to create npc: {npc}" );
endif
var npcs := ListMobilesNearLocationEx( LX, LY, LZ, 2,
LISTEX_FLAG_NORMAL + LISTEX_FLAG_NPC_ONLY );
if ( !find_serial( npcs, npc.serial ) )
return ret_error( "the npc was not listed with NORMAL and NPC_ONLY" );
endif
var players := ListMobilesNearLocationEx( LX, LY, LZ, 2,
LISTEX_FLAG_NORMAL + LISTEX_FLAG_PLAYERS_ONLY );
if ( find_serial( players, npc.serial ) )
return ret_error( "the npc was listed with PLAYERS_ONLY" );
endif
// NPC_ONLY without a state flag qualifies nobody.
var neither := ListMobilesNearLocationEx( LX, LY, LZ, 2, LISTEX_FLAG_NPC_ONLY );
if ( find_serial( neither, npc.serial ) )
return ret_error( "an ONLY flag without a state flag should list nothing" );
endif
return 1;
endfunction
// Line of sight is taken from an object rather than a bare location.
exported function list_mobiles_in_line_of_sight( resmngr )
var seer := resmngr.CreateNPCFromTemplate( ":TestNPC:probe_npc", LX, LY, LZ, 0,
_DEFAULT_REALM, 1 );
if ( !seer )
return ret_error( $"failed to create npc: {seer}" );
endif
var other := resmngr.CreateNPCFromTemplate( ":TestNPC:probe_npc", LX + 1, LY, LZ, 0,
_DEFAULT_REALM, 1 );
if ( !other )
return ret_error( $"failed to create second npc: {other}" );
endif
var seen := ListMobilesInLineOfSight( seer, 5 );
if ( !find_serial( seen, other.serial ) )
return ret_error( "the adjacent npc was not in line of sight" );
endif
return 1;
endfunction
// A box query filters by POL class rather than by object type.
exported function list_objects_in_box_of_class( resmngr )
var npc := resmngr.CreateNPCFromTemplate( ":TestNPC:probe_npc", LX, LY, LZ, 0,
_DEFAULT_REALM, 1 );
if ( !npc )
return ret_error( $"failed to create npc: {npc}" );
endif
var mobiles := ListObjectsInBoxOfClass( POLCLASS_NPC, LX - 2, LY - 2, -128,
LX + 2, LY + 2, 127 );
if ( !find_serial( mobiles, npc.serial ) )
return ret_error( "the npc was not found in the box query" );
endif
var items := ListObjectsInBoxOfClass( POLCLASS_ITEM, LX - 2, LY - 2, -128,
LX + 2, LY + 2, 127 );
if ( find_serial( items, npc.serial ) )
return ret_error( "the npc was listed as an item" );
endif
return 1;
endfunction
// Nobody is logged in during the test run, so the online list is empty while the offline
// list is not.
exported function online_and_offline_lists( unused resmngr )
var online := EnumerateOnlineCharacters();
if ( TypeOf( online ) != "Array" )
return ret_error( $"EnumerateOnlineCharacters did not answer an array: {TypeOf( online )}" );
endif
if ( online.size() != 0 )
return ret_error( $"expected nobody online, got {online.size()}" );
endif
var offline := ListOfflineMobilesInRealm( _DEFAULT_REALM );
if ( TypeOf( offline ) != "Array" )
return ret_error( $"ListOfflineMobilesInRealm did not answer an array: {TypeOf( offline )}" );
endif
return 1;
endfunction
// A circle of radius 0 is a single point, and a larger one is symmetric about its centre.
exported function midpoint_circle_coords( unused resmngr )
var single := GetMidpointCircleCoords( 100, 100, 0 );
if ( TypeOf( single ) != "Array" )
return ret_error( $"expected an array, got {TypeOf( single )}" );
endif
var ring := GetMidpointCircleCoords( 100, 100, 3 );
if ( ring.size() < 1 )
return ret_error( "a radius of 3 produced no coordinates" );
endif
foreach coord in ring
if ( coord.x < 97 || coord.x > 103 || coord.y < 97 || coord.y > 103 )
return ret_error( $"coordinate outside the circle bounds: {coord.x},{coord.y}" );
endif
endforeach
return 1;
endfunction

View file

@ -0,0 +1,144 @@
use uo;
use os;
include "testutil";
program testosopts()
return 1;
endprogram
// Set_Script_Option answers the previous value of the option it changed, so each option can
// be set and put back. SCRIPTOPT_NO_INTERRUPT is the same flag Is_Critical() reports.
exported function script_option_round_trip( unused resmngr )
if ( Is_Critical() )
return ret_error( "the script should not start out critical" );
endif
var old := Set_Script_Option( SCRIPTOPT_NO_INTERRUPT, 1 );
if ( old != 0 )
return ret_error( $"expected the previous option value 0, got {old}" );
endif
if ( !Is_Critical() )
Set_Script_Option( SCRIPTOPT_NO_INTERRUPT, 0 );
return ret_error( "the script should be critical after setting NO_INTERRUPT" );
endif
old := Set_Script_Option( SCRIPTOPT_NO_INTERRUPT, 0 );
if ( old != 1 )
return ret_error( $"expected the previous option value 1, got {old}" );
endif
if ( Is_Critical() )
return ret_error( "the script should no longer be critical" );
endif
return 1;
endfunction
// These options each store the value given and report the one they replaced.
exported function script_options_each( unused resmngr )
var options := { SCRIPTOPT_CAN_ACCESS_OFFLINE_MOBILES, SCRIPTOPT_AUXSVC_ASSUME_STRING,
SCRIPTOPT_SURVIVE_ATTACHED_DISCONNECT };
foreach opt in options
var first := Set_Script_Option( opt, 1 );
if ( first.errortext )
return ret_error( $"option {opt} was refused: {first}" );
endif
var second := Set_Script_Option( opt, 0 );
if ( second != 1 )
return ret_error( $"option {opt} did not report the value just set, got {second}" );
endif
// Put the option back where it started.
Set_Script_Option( opt, first );
endforeach
return 1;
endfunction
// SCRIPTOPT_NO_RUNAWAY is the odd one out. The value it stores is the negation of the value
// passed, but the value it reports is the stored flag rather than the option, so setting it
// to 1 and reading it back answers 0 instead of 1.
exported function script_option_no_runaway( unused resmngr )
var original := Set_Script_Option( SCRIPTOPT_NO_RUNAWAY, 1 );
if ( original.errortext )
return ret_error( $"NO_RUNAWAY was refused: {original}" );
endif
var readback := Set_Script_Option( SCRIPTOPT_NO_RUNAWAY, 0 );
if ( readback != 0 )
Set_Script_Option( SCRIPTOPT_NO_RUNAWAY, 0 );
return ret_error( $"expected the inverted read-back 0, got {readback}" );
endif
// After passing 0 the flag is set, so the next read-back answers 1.
var second := Set_Script_Option( SCRIPTOPT_NO_RUNAWAY, 0 );
if ( second != 1 )
return ret_error( $"expected the flag to read back as 1, got {second}" );
endif
return 1;
endfunction
// An option number outside the known set is refused.
exported function script_option_unknown( unused resmngr )
var res := Set_Script_Option( 9999, 1 );
if ( res || res.errortext != "Unknown Script Option" )
return ret_error( $"expected Unknown Script Option, got {res}" );
endif
res := Set_Script_Option( "not_a_number", 1 );
if ( res )
return ret_error( $"expected a non-integer option number to be refused, got {res}" );
endif
return 1;
endfunction
// Set_Priority answers the previous priority and holds the range 1..255.
exported function set_priority_round_trip( unused resmngr )
var original := Set_Priority( 100 );
if ( original.errortext )
return ret_error( $"failed to set priority: {original}" );
endif
var previous := Set_Priority( 50 );
if ( previous != 100 )
return ret_error( $"expected the previous priority 100, got {previous}" );
endif
var res := Set_Priority( 0 );
if ( res )
return ret_error( $"a priority below 1 should be refused, got {res}" );
endif
res := Set_Priority( 256 );
if ( res )
return ret_error( $"a priority above 255 should be refused, got {res}" );
endif
res := Set_Priority( "high" );
if ( res )
return ret_error( $"a non-integer priority should be refused, got {res}" );
endif
Set_Priority( original );
return 1;
endfunction
// Unload_Scripts reports how many it unloaded. A name nobody has loaded unloads nothing.
// It is never called with an empty string here: that would unload every script on the shard.
exported function unload_scripts_by_name( unused resmngr )
var res := Unload_Scripts( ":TestMisc:no_such_script_at_all" );
if ( res != 0 )
return ret_error( $"expected nothing to unload, got {res}" );
endif
res := Unload_Scripts( 1 );
if ( res )
return ret_error( $"a non-string script name should be refused, got {res}" );
endif
return 1;
endfunction

View file

@ -0,0 +1,132 @@
use uo;
use os;
use polsys;
include "testutil";
const BASE_REALM := "britannia";
program testrealm()
return 1;
endprogram
// AddRealm needs two strings, an existing base realm, and a name nobody is using yet.
exported function add_realm_rejects_bad_arguments( resources )
var res := AddRealm( 1, BASE_REALM );
if ( res || res.errortext != "Invalid parameter" )
return ret_error( $"expected Invalid parameter for a non-string name, got {res}" );
endif
res := AddRealm( "shadow-bad-base", "no_such_realm" );
if ( res || res.errortext != "BaseRealm not found." )
return ret_error( $"expected BaseRealm not found., got {res}" );
endif
res := AddRealm( BASE_REALM, BASE_REALM );
if ( res || res.errortext != "Realmname already defined." )
return ret_error( $"expected Realmname already defined., got {res}" );
endif
// A shadow realm cannot itself be used as a base.
res := resources.AddRealm( "shadow-as-base", BASE_REALM );
if ( !res )
return ret_error( $"failed to create shadow realm: {res}" );
endif
res := AddRealm( "shadow-of-shadow", "shadow-as-base" );
if ( res || res.errortext != "BaseRealm is a ShadowRealm." )
return ret_error( $"expected BaseRealm is a ShadowRealm., got {res}" );
endif
return 1;
endfunction
// DeleteRealm only removes an empty shadow realm.
exported function delete_realm_rejects_bad_args( unused resources )
var res := DeleteRealm( 1 );
if ( res || res.errortext != "Invalid parameter" )
return ret_error( $"expected Invalid parameter for a non-string name, got {res}" );
endif
res := DeleteRealm( "no_such_realm" );
if ( res || res.errortext != "Realm not found." )
return ret_error( $"expected Realm not found., got {res}" );
endif
res := DeleteRealm( BASE_REALM );
if ( res || res.errortext != "Realm is not a ShadowRealm." )
return ret_error( $"expected Realm is not a ShadowRealm., got {res}" );
endif
return 1;
endfunction
// A shadow realm holding a top-level item refuses to be deleted until the item is gone.
exported function delete_realm_refuses_occupied( unused resources )
var realm_name := "shadow-occupied";
var res := AddRealm( realm_name, BASE_REALM );
if ( !res )
return ret_error( $"failed to create shadow realm: {res}" );
endif
var item := CreateItemAtLocation( 100, 100, 0, 0x3e3f, 1, realm_name );
if ( !item )
DeleteRealm( realm_name );
return ret_error( $"failed to create item in shadow realm: {item}" );
endif
res := DeleteRealm( realm_name );
var failure;
if ( res || res.errortext != "Items in Realm." )
failure := ret_error( $"expected Items in Realm., got {res}" );
endif
DestroyItem( item );
res := DeleteRealm( realm_name );
if ( !failure && !res )
failure := ret_error( $"an emptied shadow realm should delete, got {res}" );
endif
if ( failure )
return failure;
endif
return 1;
endfunction
// GetRealmDecay and SetRealmDecay share the same name lookup.
exported function realm_decay_round_trip( resources )
var res := GetRealmDecay( 1 );
if ( res || res.errortext != "Invalid parameter" )
return ret_error( $"expected Invalid parameter, got {res}" );
endif
res := GetRealmDecay( "no_such_realm" );
if ( res || res.errortext != "Realm not found." )
return ret_error( $"expected Realm not found., got {res}" );
endif
var realm_name := "shadow-decay";
res := resources.AddRealm( realm_name, BASE_REALM );
if ( !res )
return ret_error( $"failed to create shadow realm: {res}" );
endif
res := SetRealmDecay( realm_name, 0 );
if ( !res )
return ret_error( $"failed to turn decay off: {res}" );
endif
if ( GetRealmDecay( realm_name ) )
return ret_error( "decay should be off" );
endif
res := SetRealmDecay( realm_name, 1 );
if ( !res )
return ret_error( $"failed to turn decay on: {res}" );
endif
if ( !GetRealmDecay( realm_name ) )
return ret_error( "decay should be on" );
endif
return 1;
endfunction

View file

@ -0,0 +1,104 @@
use uo;
use os;
include "testutil";
// The shard defines one resource type, testres, over the whole 192x192 map with
// UnitsPerArea 50 (testsuite/pol/regions/resource.cfg and testres.cfg).
const RES := "testres";
// Coordinates used only by this file, so the depletion it causes cannot disturb other tests.
const FRESH_X := 180;
const FRESH_Y := 180;
const DRAIN_X := 182;
const DRAIN_Y := 182;
program testresource()
return 1;
endprogram
// Every resource entry point looks the type up by name first.
exported function resource_unknown_name( unused resmngr )
var res := HarvestResource( "no_such_resource", FRESH_X, FRESH_Y, 1, 1 );
if ( res || res.errortext != "No resource by that name" )
return ret_error( $"HarvestResource: expected No resource by that name, got {res}" );
endif
res := GetHarvestDifficulty( "no_such_resource", FRESH_X, FRESH_Y, 0 );
if ( res || res.errortext != "No resource by that name" )
return ret_error( $"GetHarvestDifficulty: expected No resource by that name, got {res}" );
endif
res := GetRegionString( "no_such_resource", FRESH_X, FRESH_Y, "UnitsPerArea" );
if ( res || res.errortext != "No resource by that name" )
return ret_error( $"GetRegionString: expected No resource by that name, got {res}" );
endif
return 1;
endfunction
// A harvest of b units, at most n times, answers b*a. With b=1 and n=1 that is a single unit.
exported function harvest_takes_one_unit( unused resmngr )
var res := HarvestResource( RES, FRESH_X, FRESH_Y, 1, 1 );
if ( res != 1 )
return ret_error( $"expected one unit harvested, got {res}" );
endif
res := HarvestResource( RES, FRESH_X, FRESH_Y, -1, 1 );
if ( res || res.errortext != "b must be >= 0" )
return ret_error( $"expected b must be >= 0, got {res}" );
endif
return 1;
endfunction
// An area holds UnitsPerArea units. Draining it answers everything that was left, and a
// further harvest answers nothing.
exported function harvest_drains_the_area( unused resmngr )
var first := HarvestResource( RES, DRAIN_X, DRAIN_Y, 1, 1000 );
if ( first < 1 )
return ret_error( $"expected the area to yield its units, got {first}" );
endif
var second := HarvestResource( RES, DRAIN_X, DRAIN_Y, 1, 1000 );
if ( second != 0 )
return ret_error( $"a drained area should yield nothing, got {second}" );
endif
return 1;
endfunction
// GetHarvestDifficulty additionally needs a resource-bearing landmark of the given tile type at
// the location. Grass is not one, so the middle of the map has none.
exported function difficulty_needs_a_landmark( unused resmngr )
var res := GetHarvestDifficulty( RES, FRESH_X, FRESH_Y, 0 );
if ( res || res.errortext != "No resource-bearing landmark there" )
return ret_error( $"expected No resource-bearing landmark there, got {res}" );
endif
// A tile type the resource does not list at all is refused the same way, before the map is
// ever consulted.
res := GetHarvestDifficulty( RES, SAND_X, SAND_Y, 0x3 );
if ( res || res.errortext != "No resource-bearing landmark there" )
return ret_error( $"expected No resource-bearing landmark there for grass, got {res}" );
endif
return 1;
endfunction
// testres lists LandTile 0x17, and the generated map lays a sand transition column of exactly
// that tile down the inside of its water border, so the landmark lookup succeeds there. It
// first asks the static server for a static of that type and only then falls back to the
// landtile, which is what makes this the one case that reaches findstatic.
const SAND_X := 29;
const SAND_Y := 100;
exported function difficulty_on_a_landmark( unused resmngr )
var res := GetHarvestDifficulty( RES, SAND_X, SAND_Y, 0x17 );
if ( res.errortext )
return ret_error( $"expected a difficulty on the sand column, got {res}" );
endif
if ( TypeOf( res ) != "Integer" )
return ret_error( $"expected an integer difficulty, got {TypeOf( res )} {res}" );
endif
return 1;
endfunction

View file

@ -0,0 +1,250 @@
use uo;
use os;
use cfgfile;
include "testutil";
// The fixture read by the constraint cases. Its element keys are integers, which
// is what ApplyConstraint matches the objtypes it is handed against.
const CONSTRAINT_CFG := "uoconstraint";
const CHEAP := 0x900001; // Amount 5
const DEAR := 0x900002; // Amount 20
const NO_AMOUNT := 0x900003; // has no Amount property at all
const AMOUNT_IS_TEXT := 0x900004; // Amount is a string, not a number
const ATTACH_SCRIPT := ":testmisc:uoattach";
const ATTACH_X := 148;
const ATTACH_Y := 148;
program testuomenu()
return 1;
endprogram
function list_holds( list, value )
foreach entry in list
if ( entry == value )
return 1;
endif
endforeach
return 0;
endfunction
// A dynamic menu is built up item by item and then read back. Its objtypes come
// out in the order they went in, and a submenu-free menu contributes them all.
exported function dynamic_menu_objtypes( unused resmngr )
var menu := CreateMenu( "test menu" );
if ( TypeOf( menu ) != "MenuRef" )
return ret_error( $"expected a menu, got {TypeOf( menu )}: {menu}" );
endif
if ( !AddMenuItem( menu, 0x0eed, "a gold coin", 0 ) )
return ret_error( "could not add an item by objtype" );
endif
// An objtype may also be given by the name itemdesc knows it under.
if ( !AddMenuItem( menu, "loadtest", "a container", 0x10 ) )
return ret_error( "could not add an item by objtype name" );
endif
var objtypes := GetMenuObjTypes( menu );
if ( TypeOf( objtypes ) != "Array" )
return ret_error( $"expected an array of objtypes, got {objtypes}" );
endif
if ( objtypes.size() != 2 )
return ret_error( $"expected two objtypes, got {objtypes.size()}" );
endif
if ( !list_holds( objtypes, 0x0eed ) || !list_holds( objtypes, 0x200001 ) )
return ret_error( $"the menu holds the wrong objtypes: {objtypes}" );
endif
return 1;
endfunction
exported function menu_rejects_bad_params( unused resmngr )
var no_title := CreateMenu( 0 );
if ( no_title )
return ret_error( $"expected a falsy result for a non-string title, got {no_title}" );
endif
var not_a_menu := AddMenuItem( 0, 0x0eed, "a gold coin", 0 );
if ( not_a_menu )
return ret_error( $"expected a falsy result adding to a non-menu, got {not_a_menu}" );
endif
// The read-back form also accepts the name of a static menu, and no static
// menus are configured on this shard.
var unknown := GetMenuObjTypes( "nosuchmenu" );
if ( unknown )
return ret_error( $"expected a falsy result for an unknown menu, got {unknown}" );
endif
return 1;
endfunction
// Copying an array copies each element, which is the only route to the menu
// object's own copy handler. The copy carries the menu's items with it.
exported function menu_copies_by_value( unused resmngr )
var menu := CreateMenu( "copy me" );
if ( !AddMenuItem( menu, 0x0eed, "a gold coin", 0 ) )
return ret_error( "could not add an item" );
endif
var holder := { menu };
var copied := holder;
if ( TypeOf( copied[1] ) != "MenuRef" )
return ret_error( $"copied element is not a menu: {TypeOf( copied[1] )}" );
endif
if ( TypeOfInt( copied[1] ) != TypeOfInt( menu ) )
return ret_error( "the copy has a different type integer" );
endif
if ( TypeOfInt( menu ) == TypeOfInt( holder ) )
return ret_error( "a menu and an array share a type integer" );
endif
var objtypes := GetMenuObjTypes( copied[1] );
if ( objtypes.size() != 1 || objtypes[1] != 0x0eed )
return ret_error( $"the copied menu lost its items: {objtypes}" );
endif
return 1;
endfunction
// ApplyConstraint keeps the objtypes a config file says are affordable. An
// entry is dropped for any of four reasons: it is not an integer, the file has
// no element for it, that element has no such property, or the property is not
// a number.
exported function apply_constraint_filters( unused resmngr )
var cfg := ReadConfigFile( CONSTRAINT_CFG );
if ( !cfg )
return ret_error( $"could not read {CONSTRAINT_CFG}.cfg: {cfg}" );
endif
var candidates := { CHEAP, DEAR, NO_AMOUNT, AMOUNT_IS_TEXT, 0x900099, "not an objtype" };
var affordable := ApplyConstraint( candidates, cfg, "Amount", 10 );
if ( TypeOf( affordable ) != "Array" )
return ret_error( $"expected an array, got {affordable}" );
endif
if ( affordable.size() != 1 || affordable[1] != CHEAP )
return ret_error( $"expected only the cheap objtype, got {affordable}" );
endif
var everything := ApplyConstraint( candidates, cfg, "Amount", 100 );
if ( everything.size() != 2 )
return ret_error( $"expected both priced objtypes, got {everything}" );
endif
if ( !list_holds( everything, CHEAP ) || !list_holds( everything, DEAR ) )
return ret_error( $"the wrong objtypes were affordable: {everything}" );
endif
var nothing := ApplyConstraint( candidates, cfg, "Amount", 4 );
if ( nothing.size() )
return ret_error( $"nothing should be affordable, got {nothing}" );
endif
// A property no element carries as a number matches nobody.
var by_name := ApplyConstraint( candidates, cfg, "Name", 100 );
if ( by_name.size() )
return ret_error( $"a non-numeric property should match nobody, got {by_name}" );
endif
return 1;
endfunction
exported function apply_constraint_bad_params( unused resmngr )
var cfg := ReadConfigFile( CONSTRAINT_CFG );
if ( !cfg )
return ret_error( $"could not read {CONSTRAINT_CFG}.cfg: {cfg}" );
endif
var not_a_list := ApplyConstraint( CHEAP, cfg, "Amount", 10 );
if ( not_a_list )
return ret_error( $"expected a falsy result for a non-array, got {not_a_list}" );
endif
var not_a_cfg := ApplyConstraint( { CHEAP }, 0, "Amount", 10 );
if ( not_a_cfg )
return ret_error( $"expected a falsy result for a non-config-file, got {not_a_cfg}" );
endif
var no_property := ApplyConstraint( { CHEAP }, cfg, 0, 10 );
if ( no_property )
return ret_error( $"expected a falsy result for a non-string property, got {no_property}" );
endif
return 1;
endfunction
// A character can be attached to one script at a time, and a script can hold
// one character at a time. Both halves of that are refusals rather than
// silent replacements.
exported function attach_and_detach( resmngr )
var npc := resmngr.CreateNPCFromTemplate( ":TestNPC:probe_npc", ATTACH_X, ATTACH_Y, 0, 0,
_DEFAULT_REALM, 1 );
if ( !npc )
return ret_error( $"failed to create npc: {npc}" );
endif
var other := resmngr.CreateNPCFromTemplate( ":TestNPC:probe_npc", ATTACH_X + 1, ATTACH_Y, 0, 0,
_DEFAULT_REALM, 1 );
if ( !other )
return ret_error( $"failed to create second npc: {other}" );
endif
if ( Detach() )
return ret_error( "detaching with nothing attached should answer 0" );
endif
var attached := Attach( npc );
if ( !attached )
return ret_error( $"could not attach the npc: {attached}" );
endif
var res := 1;
var second := Attach( other );
if ( second )
res := ret_error( $"attaching a second character should be refused, got {second}" );
endif
if ( res )
var bad := Attach( 0 );
if ( bad )
res := ret_error( $"expected an error for a non-character, got {bad}" );
endif
endif
if ( !Detach() )
res := ret_error( "detaching an attached character should answer 1" );
endif
return res;
endfunction
// The other refusal needs a second script, because the character remembers
// which script holds it rather than merely that it is held.
exported function attach_refuses_other_script( resmngr )
var npc := resmngr.CreateNPCFromTemplate( ":TestNPC:probe_npc", ATTACH_X + 2, ATTACH_Y, 0, 0,
_DEFAULT_REALM, 1 );
if ( !npc )
return ret_error( $"failed to create npc: {npc}" );
endif
var helper := resmngr.Start_Script( ATTACH_SCRIPT, struct{ script := GetProcess(), chr := npc } );
if ( !helper )
return ret_error( $"could not start {ATTACH_SCRIPT}: {helper}" );
endif
var ready := Wait_For_Event( 5 );
if ( !ready )
return ret_error( "the helper script never reported back" );
endif
if ( !ready.attached )
return ret_error( $"the helper script could not attach: {ready.attached}" );
endif
var refused := Attach( npc );
if ( refused )
return ret_error( $"attaching a held character should be refused, got {refused}" );
endif
return 1;
endfunction

View file

@ -0,0 +1,406 @@
use uo;
use os;
include "testutil";
// The patch of map painted as the TestJustice region by
// testsuite/pol/regions/justice.cfg. It is grass at z 0 and nothing else on the
// shard stands here.
const JR_X := 134;
const JR_Y := 134;
const JR_NAME := "TestJustice";
// Well outside it, where the background region the core builds for itself
// applies. That one comes from an empty config element, so its name is the
// empty string rather than an error.
const BG_X := 90;
const BG_Y := 90;
// The house multis uoconvert generates for the test shard.
const MULTIID_HOUSE := 0x6c;
const MULTIID_MISSING := 0xfff0;
// The light regions from testsuite/pol/regions/light.cfg. TestLightDark covers
// 108..115 in both axes; the rest of the shard is TestLightBase.
const LIGHT_REGION := "TestLightDark";
const LIGHT_X := 110;
const LIGHT_Y := 110;
const LIGHT_DARK := 25;
const LIGHT_BASE := 10;
const LIGHT_MAX := 30;
// The weather region from testsuite/pol/regions/weather.cfg, and the patch of
// open water next to it that the assignment case moves about. Neither is
// readable back from a script -- nothing in uo.em reports a weather region's
// type or which region covers a location -- so these cases can only check the
// answers the two setters give.
const WEATHER_REGION := "TestWeatherStorm";
const WEATHER_TYPE_NONE := 255;
const WEATHER_RECT_X := 184;
const WEATHER_RECT_Y := 184;
program testuoregion()
return 1;
endprogram
function region_pc()
var pc := createAccountWithChar( "testuoregion", "pass" );
if ( !pc )
return pc;
endif
var moved := MoveObjectToLocation( pc, JR_X, JR_Y + 1, 0, flags := MOVEOBJECT_FORCELOCATION );
if ( !moved )
return ret_error( $"could not move the region pc: {moved}" );
endif
return pc;
endfunction
// GetRegionName reaches the justice region three different ways depending on
// what it is handed, because UObject::isa is exact class equality: an item is
// resolved through its top level owner, a player character through its own
// position (or its client's cached region while logged in), and anything else --
// an NPC is CLASS_NPC, a container is CLASS_CONTAINER -- straight from where it
// stands.
exported function region_name_of_objects( resmngr )
var npc := resmngr.CreateNPCFromTemplate( ":TestNPC:probe_npc", JR_X, JR_Y, 0, 0, _DEFAULT_REALM,
1 );
if ( !npc )
return ret_error( $"failed to create npc: {npc}" );
endif
var npc_region := GetRegionName( npc );
if ( npc_region != JR_NAME )
return ret_error( $"npc region: expected {JR_NAME}, got {npc_region}" );
endif
// An item inside a container answers for the container, which is the top
// level owner.
var cont := resmngr.CreateItemAtLocation( JR_X + 1, JR_Y, 0, 0x200001 );
if ( !cont )
return ret_error( $"failed to create container: {cont}" );
endif
var coin := resmngr.CreateItemInContainer( cont, 0x0eed, 1 );
if ( !coin )
return ret_error( $"failed to create coin: {coin}" );
endif
var coin_region := GetRegionName( coin );
if ( coin_region != JR_NAME )
return ret_error( $"contained item region: expected {JR_NAME}, got {coin_region}" );
endif
var pc := region_pc();
if ( !pc )
return pc;
endif
var pc_region := GetRegionName( pc );
if ( pc_region != JR_NAME )
return ret_error( $"pc region: expected {JR_NAME}, got {pc_region}" );
endif
var bad := GetRegionName( 0 );
if ( bad )
return ret_error( $"expected an error for a non-object, got {bad}" );
endif
return 1;
endfunction
// The by-location form needs no object at all, and answers the background
// region's empty name outside the painted range.
exported function region_name_at_location( unused resmngr )
var inside := GetRegionNameAtLocation( JR_X, JR_Y );
if ( inside != JR_NAME )
return ret_error( $"inside the region: expected {JR_NAME}, got {inside}" );
endif
var outside := GetRegionNameAtLocation( BG_X, BG_Y );
if ( TypeOf( outside ) != "String" )
return ret_error( $"outside the region: expected a string, got {TypeOf( outside )}" );
endif
if ( outside != "" )
return ret_error( $"the background region should be unnamed, got \"{outside}\"" );
endif
var bad_coord := GetRegionNameAtLocation( "not a number", JR_Y );
if ( bad_coord )
return ret_error( $"expected an error for a non-numeric x, got {bad_coord}" );
endif
var bad_realm := GetRegionNameAtLocation( JR_X, JR_Y, "nosuchrealm" );
if ( bad_realm )
return ret_error( $"expected an error for an unknown realm, got {bad_realm}" );
endif
return 1;
endfunction
// The light level comes from whichever light region covers the location, and
// the two on this shard hold different levels.
exported function region_light_level( unused resmngr )
var dark := GetRegionLightLevelAtLocation( LIGHT_X, LIGHT_Y );
if ( TypeOf( dark ) != "Integer" )
return ret_error( $"expected an integer light level, got {TypeOf( dark )}" );
endif
if ( dark != LIGHT_DARK )
return ret_error( $"inside {LIGHT_REGION}: expected {LIGHT_DARK}, got {dark}" );
endif
var base := GetRegionLightLevelAtLocation( BG_X, BG_Y );
if ( base != LIGHT_BASE )
return ret_error( $"outside {LIGHT_REGION}: expected {LIGHT_BASE}, got {base}" );
endif
// The realm is consulted as well as the coordinates, and the other realm has
// no dark patch at all.
var other_realm := GetRegionLightLevelAtLocation( LIGHT_X, LIGHT_Y, "britannia2" );
if ( other_realm != LIGHT_BASE )
return ret_error( $"the same spot in britannia2: expected {LIGHT_BASE}, got {other_realm}" );
endif
var bad_realm := GetRegionLightLevelAtLocation( LIGHT_X, LIGHT_Y, "nosuchrealm" );
if ( bad_realm )
return ret_error( $"expected an error for an unknown realm, got {bad_realm}" );
endif
return 1;
endfunction
// A light region is set by name rather than by location, and the change is
// visible to the by-location read straight away. Only the reserved dark patch
// is ever written to -- the base regions cover the whole shard, so moving one
// of those would change the light for every client on it.
exported function set_region_light_level( unused resmngr )
var res := 1;
if ( !SetRegionLightLevel( LIGHT_REGION, LIGHT_BASE ) )
return ret_error( $"could not set the light level of {LIGHT_REGION}" );
endif
var changed := GetRegionLightLevelAtLocation( LIGHT_X, LIGHT_Y );
if ( changed != LIGHT_BASE )
res := ret_error( $"after the change: expected {LIGHT_BASE}, got {changed}" );
endif
// Both ends of the accepted range are fine; one past the top is not, and a
// negative level is read as unsigned so it is out of range too.
if ( res )
if ( !SetRegionLightLevel( LIGHT_REGION, 0 ) )
res := ret_error( "a light level of 0 should be accepted" );
elseif ( !SetRegionLightLevel( LIGHT_REGION, LIGHT_MAX ) )
res := ret_error( $"a light level of {LIGHT_MAX} should be accepted" );
endif
endif
if ( res )
var too_bright := SetRegionLightLevel( LIGHT_REGION, LIGHT_MAX + 1 );
if ( too_bright )
res := ret_error( $"a light level past {LIGHT_MAX} should be refused, got {too_bright}" );
endif
endif
if ( res )
var negative := SetRegionLightLevel( LIGHT_REGION, -1 );
if ( negative )
res := ret_error( $"a negative light level should be refused, got {negative}" );
endif
endif
if ( res )
var unknown := SetRegionLightLevel( "nosuchlightregion", LIGHT_BASE );
if ( unknown )
res := ret_error( $"expected an error for an unknown region, got {unknown}" );
endif
endif
if ( res )
var bad := SetRegionLightLevel( 0, LIGHT_BASE );
if ( bad )
res := ret_error( $"expected an error for a non-string region name, got {bad}" );
endif
endif
var restored := SetRegionLightLevel( LIGHT_REGION, LIGHT_DARK );
if ( !restored )
return ret_error( $"could not restore the light level of {LIGHT_REGION}: {restored}" );
endif
return res;
endfunction
// Multi dimensions come from the multi definition rather than from any placed
// multi, so no house has to exist to ask for them.
exported function multi_dimensions( unused resmngr )
var dims := GetMultiDimensions( MULTIID_HOUSE );
if ( TypeOf( dims ) != "Struct" )
return ret_error( $"expected a struct, got {TypeOf( dims )}: {dims}" );
endif
if ( dims.xmin > dims.xmax )
return ret_error( $"xmin {dims.xmin} is past xmax {dims.xmax}" );
endif
if ( dims.ymin > dims.ymax )
return ret_error( $"ymin {dims.ymin} is past ymax {dims.ymax}" );
endif
var missing := GetMultiDimensions( MULTIID_MISSING );
if ( missing )
return ret_error( $"expected an error for an undefined multi, got {missing}" );
endif
var bad := GetMultiDimensions( "0x6c" );
if ( bad )
return ret_error( $"expected an error for a non-integer multi id, got {bad}" );
endif
return 1;
endfunction
// Command help is a file lookup down the character's own command level and
// every level below it, so the same command answers differently as the level
// changes. The shard has two levels, Player and Test, and only the Test one has
// a search directory.
exported function command_help( unused resmngr )
var pc := region_pc();
if ( !pc )
return pc;
endif
var res := 1;
pc.cmdlevel := 0;
var unprivileged := GetCommandHelp( pc, "dummycmd" );
if ( unprivileged )
res := ret_error( $"a player level character should find no help, got {unprivileged}" );
endif
if ( res )
pc.cmdlevel := 1;
var help := GetCommandHelp( pc, "dummycmd" );
if ( TypeOf( help ) != "String" )
res := ret_error( $"expected the help text, got {TypeOf( help )}: {help}" );
elseif ( !help.find( "dummycmd" ) )
res := ret_error( $"the help text is not the fixture's: \"{help}\"" );
endif
endif
if ( res )
// Reserved device names are refused before any directory is searched.
var device := GetCommandHelp( pc, "aux" );
if ( device )
res := ret_error( $"expected an error for a reserved device name, got {device}" );
endif
endif
if ( res )
var missing := GetCommandHelp( pc, "nosuchcommand" );
if ( missing )
res := ret_error( $"expected an error for an unknown command, got {missing}" );
endif
endif
if ( res )
var bad := GetCommandHelp( 0, "dummycmd" );
if ( bad )
res := ret_error( $"expected an error for a non-character, got {bad}" );
endif
endif
pc.cmdlevel := 0;
return res;
endfunction
// A weather region is set by name, and the four levels it carries are written
// as one. Nothing reads them back from a script, so what is checked here is
// which calls are accepted and which are refused; showing the weather actually
// reaching somebody needs a logged-in client.
//
// The region is put back to "no weather" at the end. TestWeatherStorm is out in
// open water where nobody stands, but a client inside a weather region is sent
// a weather packet whenever the region changes underneath it.
exported function set_region_weather_level( unused resmngr )
var res := 1;
// aux and lightoverride have defaults, so both arities are worth exercising.
if ( !SetRegionWeatherLevel( WEATHER_REGION, 2, 30 ) )
return ret_error( $"could not set the weather level of {WEATHER_REGION}" );
endif
if ( !SetRegionWeatherLevel( WEATHER_REGION, 2, 30, 5, LIGHT_BASE ) )
res := ret_error( "the five parameter form should be accepted" );
endif
if ( res )
var unknown := SetRegionWeatherLevel( "nosuchweatherregion", 2, 30 );
if ( unknown )
res := ret_error( $"expected an error for an unknown region, got {unknown}" );
endif
endif
if ( res )
var bad_name := SetRegionWeatherLevel( 0, 2, 30 );
if ( bad_name )
res := ret_error( $"expected an error for a non-string region name, got {bad_name}" );
endif
endif
if ( res )
var bad_type := SetRegionWeatherLevel( WEATHER_REGION, "rain", 30 );
if ( bad_type )
res := ret_error( $"expected an error for a non-numeric type, got {bad_type}" );
endif
endif
var restored := SetRegionWeatherLevel( WEATHER_REGION, WEATHER_TYPE_NONE, 0, 0, -1 );
if ( !restored )
return ret_error( $"could not restore {WEATHER_REGION}: {restored}" );
endif
return res;
endfunction
// Weather regions are the one region group whose painted zones can be moved at
// runtime. An empty region name is not an error but the undo: it puts the rect
// back to whichever region covered it when the shard loaded.
exported function assign_rect_to_weather( unused resmngr )
var assigned := AssignRectToWeatherRegion( WEATHER_REGION, WEATHER_RECT_X, WEATHER_RECT_Y,
WEATHER_RECT_X + 3, WEATHER_RECT_Y + 3 );
if ( !assigned )
return ret_error( $"could not assign the rect to {WEATHER_REGION}: {assigned}" );
endif
var res := 1;
var unknown := AssignRectToWeatherRegion( "nosuchweatherregion", WEATHER_RECT_X, WEATHER_RECT_Y,
WEATHER_RECT_X + 3, WEATHER_RECT_Y + 3 );
if ( unknown )
res := ret_error( $"expected an error for an unknown region, got {unknown}" );
endif
if ( res )
var bad_name := AssignRectToWeatherRegion( 0, WEATHER_RECT_X, WEATHER_RECT_Y, WEATHER_RECT_X + 3,
WEATHER_RECT_Y + 3 );
if ( bad_name )
res := ret_error( $"expected an error for a non-string region name, got {bad_name}" );
endif
endif
if ( res )
var off_map := AssignRectToWeatherRegion( WEATHER_REGION, WEATHER_RECT_X, WEATHER_RECT_Y, 500,
500 );
if ( off_map )
res := ret_error( $"expected an error for a rect off the map, got {off_map}" );
endif
endif
if ( res )
var bad_realm := AssignRectToWeatherRegion( WEATHER_REGION, WEATHER_RECT_X, WEATHER_RECT_Y,
WEATHER_RECT_X + 3, WEATHER_RECT_Y + 3,
"nosuchrealm" );
if ( bad_realm )
res := ret_error( $"expected an error for an unknown realm, got {bad_realm}" );
endif
endif
// The empty name is the documented way back to the load time layout.
var restored := AssignRectToWeatherRegion( "", WEATHER_RECT_X, WEATHER_RECT_Y, WEATHER_RECT_X + 3,
WEATHER_RECT_Y + 3 );
if ( !restored )
return ret_error( $"could not put the rect back: {restored}" );
endif
return res;
endfunction

View file

@ -0,0 +1,237 @@
use uo;
use os;
use vitals;
include "testutil";
// The generated test map is the whole terrain the shard has: grass at z 0
// everywhere inside 28..163, water at z -5 outside it. Grass is MoveLand only
// and water is MoveSea only, so a movemode decides which of the two a query can
// stand on. See FileGenerator::modifyMap in pol-core/poltool/testfiles.cpp.
const LAND_X := 140;
const LAND_Y := 140;
const LAND_Z := 0;
const SEA_X := 10;
const SEA_Y := 100;
const SEA_Z := -5;
// uo.em declares no names for the eight facings, so these are spelled out here.
// An odd one is diagonal, which CanWalk treats differently.
const DIR_NORTH := 0;
const DIR_NORTHEAST := 1;
// Off the edge of the 192x192 realm.
const OFF_X := 500;
const OFF_Y := 500;
program testuowalk()
return 1;
endprogram
// A direction is a single step from the starting point, and the answer is the z
// the walker would end up at rather than a plain yes. That z is 0 on this map,
// which is falsy, so the result has to be told apart from an error by its type.
exported function can_walk_on_land( unused resmngr )
var north := CanWalk( "L", LAND_X, LAND_Y, LAND_Z, DIR_NORTH );
if ( TypeOf( north ) != "Integer" )
return ret_error( $"walking north: expected an integer z, got {north}" );
endif
if ( north != LAND_Z )
return ret_error( $"walking north: expected z {LAND_Z}, got {north}" );
endif
// An odd facing is diagonal, which is only allowed if at least one of the two
// orthogonals beside it is walkable too.
var northeast := CanWalk( "L", LAND_X, LAND_Y, LAND_Z, DIR_NORTHEAST );
if ( TypeOf( northeast ) != "Integer" )
return ret_error( $"walking northeast: expected an integer z, got {northeast}" );
endif
return 1;
endfunction
// Given a second pair of coordinates instead of a facing, the direction is
// worked out from the two positions -- and that pair has to be on the map.
exported function can_walk_to_coordinates( unused resmngr )
var east := CanWalk( "L", LAND_X, LAND_Y, LAND_Z, LAND_X + 1, LAND_Y );
if ( TypeOf( east ) != "Integer" )
return ret_error( $"walking east: expected an integer z, got {east}" );
endif
if ( east != LAND_Z )
return ret_error( $"walking east: expected z {LAND_Z}, got {east}" );
endif
var off_map := CanWalk( "L", LAND_X, LAND_Y, LAND_Z, OFF_X, OFF_Y );
if ( off_map )
return ret_error( $"expected an error for a target off the map, got {off_map}" );
endif
return 1;
endfunction
// Land and sea are exclusive on this map, so each movemode walks on exactly one
// of the two.
exported function can_walk_movemode_matters( unused resmngr )
var sea_on_land := CanWalk( "S", LAND_X, LAND_Y, LAND_Z, DIR_NORTH );
if ( sea_on_land )
return ret_error( $"sea movemode should not walk on grass, got {sea_on_land}" );
endif
var sea_on_water := CanWalk( "S", SEA_X, SEA_Y, SEA_Z, DIR_NORTH );
if ( TypeOf( sea_on_water ) != "Integer" )
return ret_error( $"sea movemode on water: expected an integer z, got {sea_on_water}" );
endif
if ( sea_on_water != SEA_Z )
return ret_error( $"sea movemode on water: expected z {SEA_Z}, got {sea_on_water}" );
endif
var land_on_water := CanWalk( "L", SEA_X, SEA_Y, SEA_Z, DIR_NORTH );
if ( land_on_water )
return ret_error( $"land movemode should not walk on water, got {land_on_water}" );
endif
var bad := CanWalk( 5, LAND_X, LAND_Y, LAND_Z, DIR_NORTH );
if ( bad )
return ret_error( $"expected an error for a non-string movemode, got {bad}" );
endif
return 1;
endfunction
// A path is the steps after the starting point, so it ends on the goal.
exported function find_path_across_land( unused resmngr )
var path := FindPath( LAND_X, LAND_Y, LAND_Z, LAND_X + 4, LAND_Y + 4, LAND_Z );
if ( TypeOf( path ) != "Array" )
return ret_error( $"expected an array of steps, got {path}" );
endif
if ( !path.size() )
return ret_error( "the path across open grass is empty" );
endif
var last := path[path.size()];
if ( last.x != LAND_X + 4 || last.y != LAND_Y + 4 )
return ret_error( $"the path ends at {last.x},{last.y}, not at the goal" );
endif
// A negative search skirt is clamped to nothing rather than refused, and
// ignoring doors takes the other branch of the blocker setup.
var skirtless := FindPath( LAND_X, LAND_Y, LAND_Z, LAND_X + 2, LAND_Y + 2, LAND_Z, _DEFAULT_REALM,
FP_IGNORE_DOORS, -1 );
if ( TypeOf( skirtless ) != "Array" )
return ret_error( $"expected an array with a negative skirt, got {skirtless}" );
endif
return 1;
endfunction
exported function find_path_errors( unused resmngr )
// The range limit is checked before the coordinates are, so it wins even for
// two perfectly good locations.
var too_far := FindPath( 40, 40, 0, 120, 120, 0 );
if ( too_far )
return ret_error( $"expected a range error, got {too_far}" );
endif
var no_movemode := FindPath( LAND_X, LAND_Y, LAND_Z, LAND_X + 2, LAND_Y, LAND_Z, _DEFAULT_REALM,
FP_IGNORE_MOBILES, 5, "" );
if ( no_movemode )
return ret_error( $"expected a movemode error, got {no_movemode}" );
endif
var bad_start := FindPath( OFF_X, 40, 0, OFF_X + 5, 45, 0 );
if ( bad_start )
return ret_error( $"expected a start coordinate error, got {bad_start}" );
endif
var bad_end := FindPath( 188, 188, 0, 193, 191, 0 );
if ( bad_end )
return ret_error( $"expected an end coordinate error, got {bad_end}" );
endif
// Grass and water are not connected for a land walker, so this one gets past
// every precondition and the search itself runs to exhaustion. The two ends
// are close enough together to stay inside the range limit.
var unreachable := FindPath( SEA_X + 20, SEA_Y, 0, SEA_X + 10, SEA_Y, SEA_Z );
if ( unreachable )
return ret_error( $"expected no path from grass to water, got {unreachable}" );
endif
var bad_param := FindPath( "here", 40, 0, 45, 45, 0 );
if ( bad_param )
return ret_error( $"expected an error for a non-numeric coordinate, got {bad_param}" );
endif
return 1;
endfunction
function ghost_listed( list, serial )
foreach entry in list
if ( entry.serial == serial )
return 1;
endif
endforeach
return 0;
endfunction
// A ghost is a dead player character, and this listing walks the world zones to
// find one. A player character is only ever filed in a zone when it logs in --
// create.cpp and pol.cpp register one on PlayerEnter, and loading it from
// pcs.txt only bumps a counter (uimport.cpp) -- so an offline character is in no
// zone at all and no location query can reach it, alive or dead. That is why
// ListOfflineMobilesInRealm exists as a separate scan of the object hash.
//
// The listing therefore cannot be shown finding anybody without a logged-in
// client, which belongs to the client tests. What is checked here is the shape
// of the answer and that an offline character stays out of it even once it is
// dead: this shard works a hit point maximum out through a hook that only runs
// for a mobile that is online, so an offline one has a maximum of zero and a
// single point of raw damage takes it all the way to dead.
exported function list_ghosts_near_location( unused resmngr )
var pc := createAccountWithChar( "testuowalk", "pass" );
if ( !pc )
return pc;
endif
var alive := ListGhostsNearLocation( pc.x, pc.y, pc.z, 2 );
if ( TypeOf( alive ) != "Array" )
return ret_error( $"expected an array, got {alive}" );
endif
if ( ghost_listed( alive, pc.serial ) )
return ret_error( "a living character was listed as a ghost" );
endif
var damaged := ApplyRawDamage( pc, 1, DAMAGE_NO_REPSYS, DAMAGE_NO_SHOW );
if ( damaged == error )
return ret_error( $"could not damage the ghost pc: {damaged}" );
endif
if ( !pc.dead )
return ret_error( "one point of raw damage did not kill an offline character" );
endif
var res := 1;
var ghosts := ListGhostsNearLocation( pc.x, pc.y, pc.z, 2 );
if ( TypeOf( ghosts ) != "Array" )
res := ret_error( $"expected an array, got {ghosts}" );
elseif ( ghost_listed( ghosts, pc.serial ) )
res := ret_error( "an offline character is in no zone and cannot be listed" );
endif
if ( res )
var bad := ListGhostsNearLocation( "not a number", pc.y, pc.z, 2 );
if ( bad )
res := ret_error( $"expected an error for a non-numeric x, got {bad}" );
endif
endif
var revived := Resurrect( pc, RESURRECT_FORCELOCATION );
if ( !revived )
return ret_error( $"could not resurrect the ghost pc: {revived}" );
endif
var corpse := pc.getcorpse();
if ( corpse )
DestroyItem( corpse );
endif
return res;
endfunction

View file

@ -0,0 +1 @@
dummycmd: the help text GetCommandHelp reads back.

View file

@ -0,0 +1,17 @@
use os;
use uo;
// Holds a character attached on behalf of test_uomenu.src, so that the caller
// can watch Attach refuse a character another script already owns. Attach is
// per script, so that refusal can only be reached from a second one.
//
// The caller's resource manager kills this script at the end of the case; the
// timeout is only there so a failing run cannot leave it behind.
program uoattach( args )
var { script, chr } := args;
script.sendevent( struct{ attached := Attach( chr ) } );
Wait_For_Event( 30 );
return 1;
endprogram

View file

@ -0,0 +1,34 @@
#############################################################################
## ApplyConstraint fixture
#############################################################################
#
# Read by testpkgs/misc/test_uomenu.src. ApplyConstraint looks each objtype in
# the list it is given up as an *integer* key in a config file, so the element
# names here are numbers; the leading 0x is honoured by strtoul.
#
# The objtypes are made up. ApplyConstraint never consults itemdesc -- it only
# matches the numbers it is handed against the keys below -- so nothing here
# has to be a real item.
#
Ingredient 0x900001
{
Amount 5
}
Ingredient 0x900002
{
Amount 20
}
// No Amount at all: skipped whatever the caller asks for.
Ingredient 0x900003
{
Name nothing
}
// Amount is a string, not an integer, so the value comparison never happens.
Ingredient 0x900004
{
Amount plenty
}