@ -1,136 +1,160 @@
/*
* Tailor plugin . Automatically manages keeping your dorfs clothed .
* For best effect , place " tailor enable " in your dfhack . init configuration ,
* or set AUTOENABLE to true .
*/
# include "Core.h"
# include "DataDefs.h"
# include "Debug.h"
# include "PluginManager.h"
# include <string>
# include <vector>
# include "df/creature_raw.h"
# include "df/global_objects.h"
# include "df/historical_entity.h"
# include "df/item.h"
# include "df/item_flags.h"
# include "df/itemdef_armorst.h"
# include "df/itemdef_glovesst.h"
# include "df/itemdef_helmst.h"
# include "df/itemdef_pantsst.h"
# include "df/itemdef_shoesst.h"
# include "df/items_other_id.h"
# include "df/job.h"
# include "df/job_type.h"
# include "df/manager_order.h"
# include "df/plotinfost.h"
# include "df/world.h"
# include "modules/Maps.h"
# include "modules/Units.h"
# include "Core.h"
# include "Debug.h"
# include "LuaTools.h"
# include "PluginManager.h"
# include "modules/Materials.h"
# include "modules/Persistence.h"
# include "modules/Translation.h"
# include "modules/Units.h"
# include "modules/World.h"
using namespace DFHack ;
using std : : string ;
using std : : vector ;
using df : : global : : world ;
using df : : global : : plotinfo ;
using namespace DFHack ;
DFHACK_PLUGIN ( " tailor " ) ;
DFHACK_PLUGIN_IS_ENABLED ( is_enabled ) ;
# define AUTOENABLE false
DFHACK_PLUGIN_IS_ENABLED ( enabled ) ;
REQUIRE_GLOBAL ( world ) ;
REQUIRE_GLOBAL ( plotinfo ) ;
REQUIRE_GLOBAL ( standing_orders_use_dyed_cloth ) ;
REQUIRE_GLOBAL ( world ) ;
namespace DFHack {
DBG_DECLARE ( tailor , cycle , DebugCategory : : LINFO ) ;
DBG_DECLARE ( tailor , config , DebugCategory : : LINFO ) ;
}
class Tailor {
// ARMOR, SHOES, HELM, GLOVES, PANTS
static const string CONFIG_KEY = string ( plugin_name ) + " /config " ;
static PersistentDataItem config ;
// ah, if only STL had a bimap
enum ConfigValues {
CONFIG_IS_ENABLED = 0 ,
CONFIG_SILK_IDX = 1 ,
CONFIG_CLOTH_IDX = 2 ,
CONFIG_YARN_IDX = 3 ,
CONFIG_LEATHER_IDX = 4 ,
} ;
private :
static int get_config_val ( PersistentDataItem & c , int index ) {
if ( ! c . isValid ( ) )
return - 1 ;
return c . ival ( index ) ;
}
static bool get_config_bool ( PersistentDataItem & c , int index ) {
return get_config_val ( c , index ) = = 1 ;
}
static void set_config_val ( PersistentDataItem & c , int index , int value ) {
if ( c . isValid ( ) )
c . ival ( index ) = value ;
}
static void set_config_bool ( PersistentDataItem & c , int index , bool value ) {
set_config_val ( c , index , value ? 1 : 0 ) ;
}
const std : : map < df : : job_type , df : : item_type > jobTypeMap = {
{ df : : job_type : : MakeArmor , df : : item_type : : ARMOR } ,
{ df : : job_type : : MakePants , df : : item_type : : PANTS } ,
{ df : : job_type : : MakeHelm , df : : item_type : : HELM } ,
{ df : : job_type : : MakeGloves , df : : item_type : : GLOVES } ,
{ df : : job_type : : MakeShoes , df : : item_type : : SHOES }
} ;
const std : : map < df : : item_type , df : : job_type > itemTypeMap = {
{ df : : item_type : : ARMOR , df : : job_type : : MakeArmor } ,
{ df : : item_type : : PANTS , df : : job_type : : MakePants } ,
{ df : : item_type : : HELM , df : : job_type : : MakeHelm } ,
{ df : : item_type : : GLOVES , df : : job_type : : MakeGloves } ,
{ df : : item_type : : SHOES , df : : job_type : : MakeShoes }
} ;
# define F(x) df::item_flags::mask_##x
const df : : item_flags bad_flags = {
(
F ( dump ) | F ( forbid ) | F ( garbage_collect ) |
F ( hostile ) | F ( on_fire ) | F ( rotten ) | F ( trader ) |
F ( in_building ) | F ( construction ) | F ( owned )
)
# undef F
} ;
class MatType {
public :
std : : string name ;
df : : job_material_category job_material ;
df : : armor_general_flags armor_flag ;
bool operator = = ( const MatType & m ) const
{
return name = = m . name ;
}
static const int32_t CYCLE_TICKS = 1200 ; // one day
static int32_t cycle_timestamp = 0 ; // world->frame_counter at last cycle
// operator< is required to use this as a std::map key
bool operator < ( const MatType & m ) const
{
return name < m . name ;
}
// ah, if only STL had a bimap
static const std : : map < df : : job_type , df : : item_type > jobTypeMap = {
{ df : : job_type : : MakeArmor , df : : item_type : : ARMOR } ,
{ df : : job_type : : MakePants , df : : item_type : : PANTS } ,
{ df : : job_type : : MakeHelm , df : : item_type : : HELM } ,
{ df : : job_type : : MakeGloves , df : : item_type : : GLOVES } ,
{ df : : job_type : : MakeShoes , df : : item_type : : SHOES }
} ;
MatType ( std : : string & n , df : : job_material_category jm , df : : armor_general_flags af )
: name ( n ) , job_material ( jm ) , armor_flag ( af ) { } ;
MatType ( const char * n , df : : job_material_category jm , df : : armor_general_flags af )
: name ( std : : string ( n ) ) , job_material ( jm ) , armor_flag ( af ) { } ;
static const std : : map < df : : item_type , df : : job_type > itemTypeMap = {
{ df : : item_type : : ARMOR , df : : job_type : : MakeArmor } ,
{ df : : item_type : : PANTS , df : : job_type : : MakePants } ,
{ df : : item_type : : HELM , df : : job_type : : MakeHelm } ,
{ df : : item_type : : GLOVES , df : : job_type : : MakeGloves } ,
{ df : : item_type : : SHOES , df : : job_type : : MakeShoes }
} ;
class MatType {
public :
const std : : string name ;
const df : : job_material_category job_material ;
const df : : armor_general_flags armor_flag ;
bool operator = = ( const MatType & m ) const {
return name = = m . name ;
}
} ;
// operator< is required to use this as a std::map key
bool operator < ( const MatType & m ) const {
return name < m . name ;
}
const MatType
M_SILK = MatType ( " silk " , df : : job_material_category : : mask_silk , df : : armor_general_flags : : SOFT ) ,
M_CLOTH = MatType ( " cloth " , df : : job_material_category : : mask_cloth , df : : armor_general_flags : : SOFT ) ,
M_YARN = MatType ( " yarn " , df : : job_material_category : : mask_yarn , df : : armor_general_flags : : SOFT ) ,
M_LEATHER = MatType ( " leather " , df : : job_material_category : : mask_leather , df : : armor_general_flags : : LEATHER ) ;
MatType ( std : : string & n , df : : job_material_category jm , df : : armor_general_flags af )
: name ( n ) , job_material ( jm ) , armor_flag ( af ) { } ;
MatType ( const char * n , df : : job_material_category jm , df : : armor_general_flags af )
: name ( std : : string ( n ) ) , job_material ( jm ) , armor_flag ( af ) { } ;
} ;
std : : list < MatType > all_materials = { M_SILK , M_CLOTH , M_YARN , M_LEATHER } ;
static const MatType
M_SILK = MatType ( " silk " , df : : job_material_category : : mask_silk , df : : armor_general_flags : : SOFT ) ,
M_CLOTH = MatType ( " cloth " , df : : job_material_category : : mask_cloth , df : : armor_general_flags : : SOFT ) ,
M_YARN = MatType ( " yarn " , df : : job_material_category : : mask_yarn , df : : armor_general_flags : : SOFT ) ,
M_LEATHER = MatType ( " leather " , df : : job_material_category : : mask_leather , df : : armor_general_flags : : LEATHER ) ;
static const std : : list < MatType > all_materials = { M_SILK , M_CLOTH , M_YARN , M_LEATHER } ;
static std : : list < MatType > material_order = all_materials ;
static struct BadFlags {
uint32_t whole ;
BadFlags ( ) {
df : : item_flags flags ;
# define F(x) flags.bits.x = true;
F ( dump ) ; F ( forbid ) ; F ( garbage_collect ) ;
F ( hostile ) ; F ( on_fire ) ; F ( rotten ) ; F ( trader ) ;
F ( in_building ) ; F ( construction ) ; F ( owned ) ;
F ( in_chest ) ; F ( removed ) ; F ( encased ) ;
F ( spider_web ) ;
# undef F
whole = flags . whole ;
}
} badFlags ;
class Tailor {
private :
std : : map < std : : pair < df : : item_type , int > , int > available ; // key is item type & size
std : : map < std : : pair < df : : item_type , int > , int > needed ; // same
std : : map < std : : pair < df : : item_type , int > , int > queued ; // same
std : : map < int , int > sizes ; // this maps body size to races
std : : map < std : : tuple < df : : job_type , int , int > , int > orders ; // key is item type, item subtype, size
std : : map < MatType , int > supply ;
color_ostream * out ;
std : : list < MatType > material_order = { M_SILK , M_CLOTH , M_YARN , M_LEATHER } ;
std : : map < MatType , int > reserves ;
int default_reserve = 10 ;
public :
void reset ( )
{
available . clear ( ) ;
@ -145,9 +169,7 @@ private:
{
for ( auto i : world - > items . other [ df : : items_other_id : : ANY_GENERIC37 ] ) // GENERIC37 is "clothing"
{
if ( i - > flags . whole & bad_flags . whole )
continue ;
if ( i - > flags . bits . owned )
if ( i - > flags . whole & badFlags . whole )
continue ;
if ( i - > getWear ( ) > = 1 )
continue ;
@ -164,7 +186,7 @@ private:
for ( auto i : world - > items . other [ df : : items_other_id : : CLOTH ] )
{
if ( i - > flags . whole & bad _f lags. whole )
if ( i - > flags . whole & bad F lags. whole )
continue ;
if ( require_dyed & & ! i - > hasImprovements ( ) )
@ -197,7 +219,7 @@ private:
for ( auto i : world - > items . other [ df : : items_other_id : : SKIN_TANNED ] )
{
if ( i - > flags . whole & bad _f lags. whole )
if ( i - > flags . whole & bad F lags. whole )
continue ;
supply [ M_LEATHER ] + = i - > getStackSize ( ) ;
}
@ -369,8 +391,9 @@ private:
}
void place_orders ( )
int place_orders ( )
{
int ordered = 0 ;
auto entity = world - > entities . all [ plotinfo - > civ_id ] ;
for ( auto & o : orders )
@ -477,6 +500,7 @@ private:
) ;
count - = c ;
ordered + = c ;
}
else
{
@ -486,215 +510,217 @@ private:
}
}
}
return ordered ;
}
} ;
public :
void do_scan ( color_ostream & o )
{
out = & o ;
reset ( ) ;
// scan for useable clothing
static std : : unique_ptr < Tailor > tailor_instance ;
scan_clothing ( ) ;
static command_result do_command ( color_ostream & out , vector < string > & parameters ) ;
static int do_cycle ( color_ostream & out ) ;
// scan for clothing raw materials
DFhackCExport command_result plugin_init ( color_ostream & out , std : : vector < PluginCommand > & commands ) {
DEBUG ( config , out ) . print ( " initializing %s \n " , plugin_name ) ;
scan_materials ( ) ;
tailor_instance = dts : : make_unique < Tailor > ( ) ;
// scan for units who need replacement clothing
// provide a configuration interface for the plugin
commands . push_back ( PluginCommand (
plugin_name ,
" Automatically keep your dwarves in fresh clothing. " ,
do_command ) ) ;
scan_replacements ( ) ;
return CR_OK ;
}
// create new orders
DFhackCExport command_result plugin_enable ( color_ostream & out , bool enable ) {
if ( ! Core : : getInstance ( ) . isWorldLoaded ( ) ) {
out . printerr ( " Cannot enable %s without a loaded world. \n " , plugin_name ) ;
return CR_FAILURE ;
}
create_orders ( ) ;
if ( enable ! = is_enabled ) {
is_enabled = enable ;
DEBUG ( config , out ) . print ( " %s from the API; persisting \n " ,
is_enabled ? " enabled " : " disabled " ) ;
set_config_bool ( config , CONFIG_IS_ENABLED , is_enabled ) ;
if ( enable )
do_cycle ( out ) ;
} else {
DEBUG ( config , out ) . print ( " %s from the API, but already %s; no action \n " ,
is_enabled ? " enabled " : " disabled " ,
is_enabled ? " enabled " : " disabled " ) ;
}
return CR_OK ;
}
// scan existing orders and subtract
DFhackCExport command_result plugin_shutdown ( color_ostream & out ) {
DEBUG ( config , out ) . print ( " shutting down %s \n " , plugin_name ) ;
scan_existing_orders ( ) ;
tailor_instance . release ( ) ;
// place orders
return CR_OK ;
}
place_orders ( ) ;
static void set_material_order ( ) {
material_order . clear ( ) ;
for ( size_t i = 0 ; i < all_materials . size ( ) ; + + i ) {
if ( i = = ( size_t ) get_config_val ( config , CONFIG_SILK_IDX ) )
material_order . push_back ( M_SILK ) ;
else if ( i = = ( size_t ) get_config_val ( config , CONFIG_CLOTH_IDX ) )
material_order . push_back ( M_CLOTH ) ;
else if ( i = = ( size_t ) get_config_val ( config , CONFIG_YARN_IDX ) )
material_order . push_back ( M_YARN ) ;
else if ( i = = ( size_t ) get_config_val ( config , CONFIG_LEATHER_IDX ) )
material_order . push_back ( M_LEATHER ) ;
}
if ( ! material_order . size ( ) )
std : : copy ( all_materials . begin ( ) , all_materials . end ( ) , std : : back_inserter ( material_order ) ) ;
}
public :
command_result set_materials ( color_ostream & out , std : : vector < std : : string > & parameters )
{
std : : list < MatType > newmat ;
newmat . clear ( ) ;
for ( auto m = parameters . begin ( ) + 1 ; m ! = parameters . end ( ) ; m + + )
{
auto nameMatch = [ m ] ( MatType & m1 ) { return * m = = m1 . name ; } ;
auto mm = std : : find_if ( all_materials . begin ( ) , all_materials . end ( ) , nameMatch ) ;
if ( mm = = all_materials . end ( ) )
{
WARN ( config , out ) . print ( " tailor: material %s not recognized \n " , m - > c_str ( ) ) ;
return CR_WRONG_USAGE ;
}
else {
newmat . push_back ( * mm ) ;
}
}
material_order = newmat ;
INFO ( config , out ) . print ( " tailor: material list set to %s \n " , get_material_list ( ) . c_str ( ) ) ;
DFhackCExport command_result plugin_load_data ( color_ostream & out ) {
cycle_timestamp = 0 ;
config = World : : GetPersistentData ( CONFIG_KEY ) ;
return CR_OK ;
if ( ! config . isValid ( ) ) {
DEBUG ( config , out ) . print ( " no config found in this save; initializing \n " ) ;
config = World : : AddPersistentData ( CONFIG_KEY ) ;
set_config_bool ( config , CONFIG_IS_ENABLED , is_enabled ) ;
}
public :
std : : string get_material_list ( )
{
std : : string s ;
for ( const auto & m : material_order )
{
if ( ! s . empty ( ) ) s + = " , " ;
s + = m . name ;
}
return s ;
}
is_enabled = get_config_bool ( config , CONFIG_IS_ENABLED ) ;
DEBUG ( config , out ) . print ( " loading persisted enabled state: %s \n " ,
is_enabled ? " true " : " false " ) ;
set_material_order ( ) ;
public :
void process ( color_ostream & out )
{
bool found = false ;
return CR_OK ;
}
for ( df : : job_list_link * link = & world - > jobs . list ; link ! = NULL ; link = link - > next )
{
if ( link - > item = = NULL ) continue ;
if ( link - > item - > job_type = = df : : enums : : job_type : : UpdateStockpileRecords )
{
found = true ;
break ;
}
DFhackCExport command_result plugin_onstatechange ( color_ostream & out , state_change_event event ) {
if ( event = = DFHack : : SC_WORLD_UNLOADED ) {
if ( is_enabled ) {
DEBUG ( config , out ) . print ( " world unloaded; disabling %s \n " ,
plugin_name ) ;
is_enabled = false ;
}
}
return CR_OK ;
}
if ( found )
{
do_scan ( out ) ;
}
DFhackCExport command_result plugin_onupdate ( color_ostream & out ) {
if ( is_enabled & & world - > frame_counter - cycle_timestamp > = CYCLE_TICKS ) {
int ordered = do_cycle ( out ) ;
if ( 0 < ordered )
out . print ( " tailor: ordered %d items of clothing \n " , ordered ) ;
}
} ;
return CR_OK ;
}
static std : : unique_ptr < Tailor > tailor_instance ;
static bool call_tailor_lua ( color_ostream * out , const char * fn_name ,
int nargs = 0 , int nres = 0 ,
Lua : : LuaLambda & & args_lambda = Lua : : DEFAULT_LUA_LAMBDA ,
Lua : : LuaLambda & & res_lambda = Lua : : DEFAULT_LUA_LAMBDA ) {
DEBUG ( config ) . print ( " calling tailor lua function: '%s' \n " , fn_name ) ;
# define DELTA_TICKS 50
CoreSuspender guard ;
DFhackCExport command_result plugin_onupdate ( color_ostream & out )
{
if ( ! enabled | | ! tailor_instance )
return CR_OK ;
auto L = Lua : : Core : : State ;
Lua : : StackUnwinder top ( L ) ;
if ( ! Maps: : IsValid ( ) )
return CR_OK ;
if ( ! out )
out = & Core : : getInstance ( ) . getConsole ( ) ;
if ( DFHack : : World : : ReadPauseState ( ) )
return CR_OK ;
return Lua : : CallLuaModuleFunction ( * out , L , " plugins.tailor " , fn_name ,
nargs , nres ,
std : : forward < Lua : : LuaLambda & & > ( args_lambda ) ,
std : : forward < Lua : : LuaLambda & & > ( res_lambda ) ) ;
}
if ( world - > frame_counter % DELTA_TICKS ! = 0 )
return CR_OK ;
static command_result do_command ( color_ostream & out , vector < string > & parameters ) {
CoreSuspender suspend ;
{
CoreSuspender suspend ;
tailor_instance - > process ( out ) ;
if ( ! Core : : getInstance ( ) . isWorldLoaded ( ) ) {
out. printerr ( " Cannot run %s without a loaded world. \n " , plugin_name ) ;
return CR_FAILURE ;
}
return CR_OK ;
bool show_help = false ;
if ( ! call_tailor_lua ( & out , " parse_commandline " , parameters . size ( ) , 1 ,
[ & ] ( lua_State * L ) {
for ( const string & param : parameters )
Lua : : Push ( L , param ) ;
} ,
[ & ] ( lua_State * L ) {
show_help = ! lua_toboolean ( L , - 1 ) ;
} ) ) {
return CR_FAILURE ;
}
return show_help ? CR_WRONG_USAGE : CR_OK ;
}
static command_result tailor_cmd ( color_ostream & out , std : : vector < std : : string > & parameters ) {
bool desired = enabled ;
if ( parameters . size ( ) = = 1 & & ( parameters [ 0 ] = = " enable " | | parameters [ 0 ] = = " on " | | parameters [ 0 ] = = " 1 " ) )
{
desired = true ;
}
else if ( parameters . size ( ) = = 1 & & ( parameters [ 0 ] = = " disable " | | parameters [ 0 ] = = " off " | | parameters [ 0 ] = = " 0 " ) )
{
desired = false ;
}
else if ( parameters . size ( ) = = 1 & & ( parameters [ 0 ] = = " usage " | | parameters [ 0 ] = = " help " | | parameters [ 0 ] = = " ? " ) )
{
return CR_WRONG_USAGE ;
}
else if ( parameters . size ( ) = = 1 & & parameters [ 0 ] = = " test " )
{
if ( tailor_instance )
{
tailor_instance - > do_scan ( out ) ;
return CR_OK ;
}
else
{
out . print ( " %s: not instantiated \n " , plugin_name ) ;
return CR_FAILURE ;
}
}
else if ( parameters . size ( ) > 1 & & parameters [ 0 ] = = " materials " )
{
if ( tailor_instance )
{
return tailor_instance - > set_materials ( out , parameters ) ;
}
else
{
out . print ( " %s: not instantiated \n " , plugin_name ) ;
return CR_FAILURE ;
}
}
else if ( parameters . size ( ) = = 1 & & parameters [ 0 ] ! = " status " )
{
return CR_WRONG_USAGE ;
}
/////////////////////////////////////////////////////
// cycle logic
//
out . print ( " Tailor is %s %s. \n " , ( desired = = enabled ) ? " currently " : " now " , desired ? " enabled " : " disabled " ) ;
if ( tailor_instance )
{
out . print ( " Material list is: %s \n " , tailor_instance - > get_material_list ( ) . c_str ( ) ) ;
}
else
{
out . print ( " %s: not instantiated \n " , plugin_name ) ;
}
static int do_cycle ( color_ostream & out ) {
// mark that we have recently run
cycle_timestamp = world - > frame_counter ;
enabled = desired ;
DEBUG ( cycle , out ) . print ( " running %s cycle \n " , plugin_name ) ;
return CR_OK ;
tailor_instance - > reset ( ) ;
tailor_instance - > scan_clothing ( ) ;
tailor_instance - > scan_materials ( ) ;
tailor_instance - > scan_replacements ( ) ;
tailor_instance - > create_orders ( ) ;
tailor_instance - > scan_existing_orders ( ) ;
return tailor_instance - > place_orders ( ) ;
}
/////////////////////////////////////////////////////
// Lua API
//
DFhackCExport command_result plugin_onstatechange ( color_ostream & out , state_change_event event )
{
return CR_OK ;
static void tailor_doCycle ( color_ostream & out ) {
DEBUG ( config , out ) . print ( " entering tailor_doCycle \n " ) ;
out . print ( " ordered %d items of clothing \n " , do_cycle ( out ) ) ;
}
DFhackCExport command_result plugin_enable ( color_ostream & out , bool enable )
{
enabled = enable ;
return CR_OK ;
}
// remember, these are ONE-based indices from Lua
static void tailor_setMaterialPreferences ( color_ostream & out , int32_t silkIdx ,
int32_t clothIdx , int32_t yarnIdx , int32_t leatherIdx ) {
DEBUG ( config , out ) . print ( " entering tailor_setMaterialPreferences \n " ) ;
DFhackCExport command_result plugin_init ( color_ostream & out , std : : vector < PluginCommand > & commands )
{
tailor_instance = std : : move ( dts : : make_unique < Tailor > ( ) ) ;
// it doesn't really matter if these are invalid. set_material_order will do
// the right thing.
set_config_val ( config , CONFIG_SILK_IDX , silkIdx - 1 ) ;
set_config_val ( config , CONFIG_CLOTH_IDX , clothIdx - 1 ) ;
set_config_val ( config , CONFIG_YARN_IDX , yarnIdx - 1 ) ;
set_config_val ( config , CONFIG_LEATHER_IDX , leatherIdx - 1 ) ;
if ( AUTOENABLE ) {
enabled = true ;
}
set_material_order ( ) ;
}
commands . push_back ( PluginCommand (
plugin_name ,
" Automatically keep your dwarves in fresh clothing. " ,
tailor_cmd ) ) ;
return CR_OK ;
static int tailor_getMaterialPreferences ( lua_State * L ) {
color_ostream * out = Lua : : GetOutput ( L ) ;
if ( ! out )
out = & Core : : getInstance ( ) . getConsole ( ) ;
DEBUG ( config , * out ) . print ( " entering tailor_getMaterialPreferences \n " ) ;
vector < string > names ;
for ( const auto & m : material_order )
names . emplace_back ( m . name ) ;
Lua : : PushVector ( L , names ) ;
return 1 ;
}
DFhackCExport command_result plugin_shutdown ( color_ostream & out )
{
tailor_instance . release ( ) ;
DFHACK_PLUGIN_LUA_FUNCTIONS {
DFHACK_LUA_FUNCTION ( tailor_doCycle ) ,
DFHACK_LUA_FUNCTION ( tailor_setMaterialPreferences ) ,
DFHACK_LUA_END
} ;
return plugin_enable ( out , false ) ;
}
DFHACK_PLUGIN_LUA_COMMANDS {
DFHACK_LUA_COMMAND ( tailor_getMaterialPreferences ) ,
DFHACK_LUA_END
} ;