/*
* This file is part of OpenTTD .
* OpenTTD is free software ; you can redistribute it and / or modify it under the terms of the GNU General Public License as published by the Free Software Foundation , version 2.
* OpenTTD is distributed in the hope that it will be useful , but WITHOUT ANY WARRANTY ; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE .
* See the GNU General Public License for more details . You should have received a copy of the GNU General Public License along with OpenTTD . If not , see < http : //www.gnu.org/licenses/>.
*/
/** @file script_text.cpp Implementation of ScriptText. */
# include "../../stdafx.h"
# include "../../string_func.h"
# include "../../strings_func.h"
# include "../../game/game_text.hpp"
# include "script_text.hpp"
# include "script_log.hpp"
# include "../script_fatalerror.hpp"
# include "../script_instance.hpp"
# include "script_log.hpp"
# include "../../table/control_codes.h"
# include "../../core/format.hpp"
# include "table/strings.h"
# include "../../safeguards.h"
RawText : : RawText ( const std : : string & text ) : text ( text )
{
}
ScriptText : : ScriptText ( HSQUIRRELVM vm ) :
string ( STR_NULL ) , param ( ) , paramc ( 0 )
{
int nparam = sq_gettop ( vm ) - 1 ;
if ( nparam < 1 ) {
throw sq_throwerror ( vm , " You need to pass at least a StringID to the constructor " ) ;
}
/* First resolve the StringID. */
SQInteger sqstring ;
if ( SQ_FAILED ( sq_getinteger ( vm , 2 , & sqstring ) ) ) {
throw sq_throwerror ( vm , " First argument must be a valid StringID " ) ;
}
this - > string = sqstring ;
/* The rest of the parameters must be arguments. */
for ( int i = 0 ; i < nparam - 1 ; i + + ) {
/* Push the parameter to the top of the stack. */
sq_push ( vm , i + 3 ) ;
if ( SQ_FAILED ( this - > _SetParam ( i , vm ) ) ) {
this - > ~ ScriptText ( ) ;
throw sq_throwerror ( vm , " Invalid parameter " ) ;
}
/* Pop the parameter again. */
sq_pop ( vm , 1 ) ;
}
}
SQInteger ScriptText : : _SetParam ( int parameter , HSQUIRRELVM vm )
{
if ( parameter > = SCRIPT_TEXT_MAX_PARAMETERS ) return SQ_ERROR ;
switch ( sq_gettype ( vm , - 1 ) ) {
case OT_STRING : {
const SQChar * value ;
sq_getstring ( vm , - 1 , & value ) ;
this - > param [ parameter ] = StrMakeValid ( value ) ;
break ;
}
case OT_INTEGER : {
SQInteger value ;
sq_getinteger ( vm , - 1 , & value ) ;
this - > param [ parameter ] = value ;
break ;
}
case OT_INSTANCE : {
SQUserPointer real_instance = nullptr ;
HSQOBJECT instance ;
sq_getstackobj ( vm , - 1 , & instance ) ;
/* Validate if it is a GSText instance */
sq_pushroottable ( vm ) ;
sq_pushstring ( vm , " GSText " , - 1 ) ;
sq_get ( vm , - 2 ) ;
sq_pushobject ( vm , instance ) ;
if ( sq_instanceof ( vm ) ! = SQTrue ) return SQ_ERROR ;
sq_pop ( vm , 3 ) ;
/* Get the 'real' instance of this class */
sq_getinstanceup ( vm , - 1 , & real_instance , nullptr ) ;
if ( real_instance = = nullptr ) return SQ_ERROR ;
ScriptText * value = static_cast < ScriptText * > ( real_instance ) ;
this - > param [ parameter ] = ScriptTextRef ( value ) ;
break ;
}
default : return SQ_ERROR ;
}
if ( this - > paramc < = parameter ) this - > paramc = parameter + 1 ;
return 0 ;
}
SQInteger ScriptText : : SetParam ( HSQUIRRELVM vm )
{
if ( sq_gettype ( vm , 2 ) ! = OT_INTEGER ) return SQ_ERROR ;
SQInteger k ;
sq_getinteger ( vm , 2 , & k ) ;
if ( k > SCRIPT_TEXT_MAX_PARAMETERS ) return SQ_ERROR ;
if ( k < 1 ) return SQ_ERROR ;
k - - ;
return this - > _SetParam ( k , vm ) ;
}
SQInteger ScriptText : : AddParam ( HSQUIRRELVM vm )
{
SQInteger res ;
res = this - > _SetParam ( this - > paramc , vm ) ;
if ( res ! = 0 ) return res ;
/* Push our own instance back on top of the stack */
sq_push ( vm , 1 ) ;
return 1 ;
}
SQInteger ScriptText : : _set ( HSQUIRRELVM vm )
{
int32_t k ;
if ( sq_gettype ( vm , 2 ) = = OT_STRING ) {
const SQChar * key_string ;
sq_getstring ( vm , 2 , & key_string ) ;
std : : string str = StrMakeValid ( key_string ) ;
if ( ! str . starts_with ( " param_ " ) | | str . size ( ) > 8 ) return SQ_ERROR ;
k = stoi ( str . substr ( 6 ) ) ;
} else if ( sq_gettype ( vm , 2 ) = = OT_INTEGER ) {
SQInteger key ;
sq_getinteger ( vm , 2 , & key ) ;
k = ( int32_t ) key ;
} else {
return SQ_ERROR ;
}
if ( k > SCRIPT_TEXT_MAX_PARAMETERS ) return SQ_ERROR ;
if ( k < 1 ) return SQ_ERROR ;
k - - ;
return this - > _SetParam ( k , vm ) ;
}
std : : string ScriptText : : GetEncodedText ( )
{
int param_count = 0 ;
std : : string result ;
auto output = std : : back_inserter ( result ) ;
if ( this - > GetActiveInstance ( ) - > IsTextParamMismatchAllowed ( ) ) {
static StringIDList seen_ids ;
seen_ids . clear ( ) ;
this - > _GetEncodedTextTraditional ( output , param_count , seen_ids ) ;
} else {
static ScriptTextList seen_texts ;
seen_texts . clear ( ) ;
ParamList params ;
this - > _FillParamList ( params , seen_texts ) ;
this - > _GetEncodedText ( output , param_count , params , true ) ;
}
if ( param_count > SCRIPT_TEXT_MAX_PARAMETERS ) throw Script_FatalError ( fmt : : format ( " {}: Too many parameters " , GetGameStringName ( this - > string ) ) ) ;
return result ;
}
void ScriptText : : _TextParamError ( std : : string msg )
{
if ( this - > GetActiveInstance ( ) - > IsTextParamMismatchAllowed ( ) ) {
ScriptLog : : LogOnce ( ScriptLogTypes : : LOG_ERROR , std : : move ( msg ) ) ;
} else {
throw Script_FatalError ( std : : move ( msg ) ) ;
}
}
void ScriptText : : _GetEncodedTextTraditional ( std : : back_insert_iterator < std : : string > & output , int & param_count , StringIDList & seen_ids )
{
const std : : string & name = GetGameStringName ( this - > string ) ;
if ( std : : find ( seen_ids . begin ( ) , seen_ids . end ( ) , this - > string ) ! = seen_ids . end ( ) ) throw Script_FatalError ( fmt : : format ( " {}: Circular reference detected " , name ) ) ;
seen_ids . push_back ( this - > string ) ;
Utf8Encode ( output , SCC_ENCODED ) ;
fmt : : format_to ( output , " {:X} " , this - > string ) ;
auto write_param_fallback = [ & ] ( int idx ) {
if ( std : : holds_alternative < ScriptTextRef > ( this - > param [ idx ] ) ) {
int count = 1 ; // 1 because the string id is included in consumed parameters
fmt : : format_to ( output , " : " ) ;
std : : get < ScriptTextRef > ( this - > param [ idx ] ) - > _GetEncodedTextTraditional ( output , count , seen_ids ) ;
param_count + = count ;
} else if ( std : : holds_alternative < SQInteger > ( this - > param [ idx ] ) ) {
fmt : : format_to ( output , " :{:X} " , std : : get < SQInteger > ( this - > param [ idx ] ) ) ;
param_count + + ;
} else {
/* Fallback value */
fmt : : format_to ( output , " :0 " ) ;
param_count + + ;
}
} ;
const StringParams & params = GetGameStringParams ( this - > string ) ;
int cur_idx = 0 ;
for ( const StringParam & cur_param : params ) {
if ( cur_idx > = this - > paramc ) {
this - > _TextParamError ( fmt : : format ( " {}: Not enough parameters " , name ) ) ;
break ;
}
switch ( cur_param . type ) {
case StringParam : : RAW_STRING :
if ( ! std : : holds_alternative < std : : string > ( this - > param [ cur_idx ] ) ) {
this - > _TextParamError ( fmt : : format ( " {}: Parameter {} expects a raw string " , name , cur_idx ) ) ;
write_param_fallback ( cur_idx + + ) ;
break ;
}
fmt : : format_to ( output , " : \" {} \" " , std : : get < std : : string > ( this - > param [ cur_idx + + ] ) ) ;
param_count + + ;
break ;
case StringParam : : STRING : {
if ( ! std : : holds_alternative < ScriptTextRef > ( this - > param [ cur_idx ] ) ) {
this - > _TextParamError ( fmt : : format ( " {}: Parameter {} expects a substring " , name , cur_idx ) ) ;
write_param_fallback ( cur_idx + + ) ;
break ;
}
int count = 1 ; // 1 because the string id is included in consumed parameters
fmt : : format_to ( output , " : " ) ;
std : : get < ScriptTextRef > ( this - > param [ cur_idx + + ] ) - > _GetEncodedTextTraditional ( output , count , seen_ids ) ;
if ( count ! = cur_param . consumes ) {
this - > _TextParamError ( fmt : : format ( " {}: Parameter {} substring consumes {}, but expected {} to be consumed " , name , cur_idx , count - 1 , cur_param . consumes - 1 ) ) ;
}
param_count + = count ;
break ;
}
default :
if ( cur_idx + cur_param . consumes > this - > paramc ) {
this - > _TextParamError ( fmt : : format ( " {}: Not enough parameters " , name ) ) ;
}
for ( int i = 0 ; i < cur_param . consumes & & cur_idx < this - > paramc ; i + + ) {
if ( ! std : : holds_alternative < SQInteger > ( this - > param [ cur_idx ] ) ) {
this - > _TextParamError ( fmt : : format ( " {}: Parameter {} expects an integer " , name , cur_idx ) ) ;
write_param_fallback ( cur_idx + + ) ;
continue ;
}
fmt : : format_to ( output , " :{:X} " , std : : get < SQInteger > ( this - > param [ cur_idx + + ] ) ) ;
param_count + + ;
}
break ;
}
}
for ( int i = cur_idx ; i < this - > paramc ; i + + ) {
write_param_fallback ( i ) ;
}
seen_ids . pop_back ( ) ;
}
void ScriptText : : _FillParamList ( ParamList & params , ScriptTextList & seen_texts )
{
if ( std : : find ( seen_texts . begin ( ) , seen_texts . end ( ) , this ) ! = seen_texts . end ( ) ) throw Script_FatalError ( fmt : : format ( " {}: Circular reference detected " , GetGameStringName ( this - > string ) ) ) ;
seen_texts . push_back ( this ) ;
for ( int i = 0 ; i < this - > paramc ; i + + ) {
Param * p = & this - > param [ i ] ;
params . emplace_back ( this - > string , i , p ) ;
if ( ! std : : holds_alternative < ScriptTextRef > ( * p ) ) continue ;
std : : get < ScriptTextRef > ( * p ) - > _FillParamList ( params , seen_texts ) ;
}
seen_texts . pop_back ( ) ;
/* Fill with dummy parameters to match FormatString() behaviour. */
if ( seen_texts . empty ( ) ) {
static Param dummy = 0 ;
int nb_extra = SCRIPT_TEXT_MAX_PARAMETERS - ( int ) params . size ( ) ;
for ( int i = 0 ; i < nb_extra ; i + + )
params . emplace_back ( - 1 , i , & dummy ) ;
}
}
void ScriptText : : ParamCheck : : Encode ( std : : back_insert_iterator < std : : string > & output , const char * cmd )
{
if ( this - > cmd = = nullptr ) this - > cmd = cmd ;
if ( this - > used ) return ;
if ( std : : holds_alternative < std : : string > ( * this - > param ) ) fmt : : format_to ( output , " : \" {} \" " , std : : get < std : : string > ( * this - > param ) ) ;
if ( std : : holds_alternative < SQInteger > ( * this - > param ) ) fmt : : format_to ( output , " :{:X} " , std : : get < SQInteger > ( * this - > param ) ) ;
if ( std : : holds_alternative < ScriptTextRef > ( * this - > param ) ) {
fmt : : format_to ( output , " : " ) ;
Utf8Encode ( output , SCC_ENCODED ) ;
fmt : : format_to ( output , " {:X} " , std : : get < ScriptTextRef > ( * this - > param ) - > string ) ;
}
this - > used = true ;
}
void ScriptText : : _GetEncodedText ( std : : back_insert_iterator < std : : string > & output , int & param_count , ParamSpan args , bool first )
{
const std : : string & name = GetGameStringName ( this - > string ) ;
if ( first ) {
Utf8Encode ( output , SCC_ENCODED ) ;
fmt : : format_to ( output , " {:X} " , this - > string ) ;
}
const StringParams & params = GetGameStringParams ( this - > string ) ;
size_t idx = 0 ;
auto get_next_arg = [ & ] ( ) {
if ( idx > = args . size ( ) ) throw Script_FatalError ( fmt : : format ( " {}({}): Not enough parameters " , name , param_count + 1 ) ) ;
ParamCheck & pc = args [ idx + + ] ;
if ( pc . owner ! = this - > string ) ScriptLog : : Warning ( fmt : : format ( " {}({}): Consumes {}({}) " , name , param_count + 1 , GetGameStringName ( pc . owner ) , pc . idx + 1 ) ) ;
return & pc ;
} ;
auto skip_args = [ & ] ( size_t nb ) { idx + = nb ; } ;
for ( const StringParam & cur_param : params ) {
try {
switch ( cur_param . type ) {
case StringParam : : UNUSED :
skip_args ( cur_param . consumes ) ;
break ;
case StringParam : : RAW_STRING :
{
ParamCheck & p = * get_next_arg ( ) ;
p . Encode ( output , cur_param . cmd ) ;
if ( p . cmd ! = cur_param . cmd ) throw 1 ;
if ( ! std : : holds_alternative < std : : string > ( * p . param ) ) ScriptLog : : Error ( fmt : : format ( " {}({}): {{{}}} expects a raw string " , name , param_count + 1 , cur_param . cmd ) ) ;
break ;
}
case StringParam : : STRING :
{
ParamCheck & p = * get_next_arg ( ) ;
p . Encode ( output , cur_param . cmd ) ;
if ( p . cmd ! = cur_param . cmd ) throw 1 ;
if ( ! std : : holds_alternative < ScriptTextRef > ( * p . param ) ) {
ScriptLog : : Error ( fmt : : format ( " {}({}): {{{}}} expects a GSText " , name , param_count + 1 , cur_param . cmd ) ) ;
param_count + + ;
continue ;
}
int count = 0 ;
ScriptTextRef & ref = std : : get < ScriptTextRef > ( * p . param ) ;
ref - > _GetEncodedText ( output , count , args . subspan ( idx ) , false ) ;
if ( + + count ! = cur_param . consumes ) {
ScriptLog : : Warning ( fmt : : format ( " {}({}): {{{}}} expects {} to be consumed, but {} consumes {} " , name , param_count + 1 , cur_param . cmd , cur_param . consumes - 1 , GetGameStringName ( ref - > string ) , count - 1 ) ) ;
/* Fill missing params if needed. */
for ( int i = count ; i < cur_param . consumes ; i + + ) fmt : : format_to ( output , " :0 " ) ;
}
skip_args ( cur_param . consumes - 1 ) ;
break ;
}
default :
for ( int i = 0 ; i < cur_param . consumes ; i + + ) {
ParamCheck & p = * get_next_arg ( ) ;
p . Encode ( output , i = = 0 ? cur_param . cmd : nullptr ) ;
if ( i = = 0 & & p . cmd ! = cur_param . cmd ) throw 1 ;
if ( ! std : : holds_alternative < SQInteger > ( * p . param ) ) ScriptLog : : Error ( fmt : : format ( " {}({}): {{{}}} expects an integer " , name , param_count + i + 1 , cur_param . cmd ) ) ;
}
}
param_count + = cur_param . consumes ;
} catch ( int nb ) {
param_count + = nb ;
ScriptLog : : Warning ( fmt : : format ( " {}({}): Invalid parameter " , name , param_count ) ) ;
}
}
}
const std : : string Text : : GetDecodedText ( )
{
: : SetDParamStr ( 0 , this - > GetEncodedText ( ) ) ;
return : : GetString ( STR_JUST_RAW_STRING ) ;
}