2 * This file is part of OpenTTD.
3 * 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.
4 * 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.
5 * 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/>.
8 /** @file script_text.cpp Implementation of ScriptText. */
10 #include "../../stdafx.h"
11 #include "../../string_func.h"
12 #include "../../strings_func.h"
13 #include "../../game/game_text.hpp"
14 #include "script_text.hpp"
15 #include "script_log.hpp"
16 #include "../script_fatalerror.hpp"
17 #include "../../table/control_codes.h"
19 #include "table/strings.h"
21 #include "../../safeguards.h"
24 ScriptText::ScriptText(HSQUIRRELVM vm
)
26 int nparam
= sq_gettop(vm
) - 1;
28 throw sq_throwerror(vm
, "You need to pass at least a StringID to the constructor");
31 /* First resolve the StringID. */
33 if (SQ_FAILED(sq_getinteger(vm
, 2, &sqstring
))) {
34 throw sq_throwerror(vm
, "First argument must be a valid StringID");
36 this->string
= StringIndexInTab(sqstring
);
38 /* The rest of the parameters must be arguments. */
39 for (int i
= 0; i
< nparam
- 1; i
++) {
40 /* Push the parameter to the top of the stack. */
43 if (SQ_FAILED(this->_SetParam(i
, vm
))) {
45 throw sq_throwerror(vm
, "Invalid parameter");
48 /* Pop the parameter again. */
53 SQInteger
ScriptText::_SetParam(int parameter
, HSQUIRRELVM vm
)
55 if (parameter
>= SCRIPT_TEXT_MAX_PARAMETERS
) return SQ_ERROR
;
57 switch (sq_gettype(vm
, -1)) {
60 sq_getstring(vm
, -1, &value
);
62 this->param
[parameter
] = StrMakeValid(value
);
68 sq_getinteger(vm
, -1, &value
);
70 this->param
[parameter
] = value
;
75 SQUserPointer real_instance
= nullptr;
78 sq_getstackobj(vm
, -1, &instance
);
80 /* Validate if it is a GSText instance */
82 sq_pushstring(vm
, "GSText", -1);
84 sq_pushobject(vm
, instance
);
85 if (sq_instanceof(vm
) != SQTrue
) return SQ_ERROR
;
88 /* Get the 'real' instance of this class */
89 sq_getinstanceup(vm
, -1, &real_instance
, nullptr);
90 if (real_instance
== nullptr) return SQ_ERROR
;
92 ScriptText
*value
= static_cast<ScriptText
*>(real_instance
);
93 this->param
[parameter
] = ScriptTextRef(value
);
97 default: return SQ_ERROR
;
100 if (this->paramc
<= parameter
) this->paramc
= parameter
+ 1;
104 SQInteger
ScriptText::SetParam(HSQUIRRELVM vm
)
106 if (sq_gettype(vm
, 2) != OT_INTEGER
) return SQ_ERROR
;
109 sq_getinteger(vm
, 2, &k
);
111 if (k
> SCRIPT_TEXT_MAX_PARAMETERS
) return SQ_ERROR
;
112 if (k
< 1) return SQ_ERROR
;
115 return this->_SetParam(k
, vm
);
118 SQInteger
ScriptText::AddParam(HSQUIRRELVM vm
)
121 res
= this->_SetParam(this->paramc
, vm
);
122 if (res
!= 0) return res
;
124 /* Push our own instance back on top of the stack */
129 SQInteger
ScriptText::_set(HSQUIRRELVM vm
)
133 if (sq_gettype(vm
, 2) == OT_STRING
) {
134 const SQChar
*key_string
;
135 sq_getstring(vm
, 2, &key_string
);
137 std::string str
= StrMakeValid(key_string
);
138 if (!str
.starts_with("param_") || str
.size() > 8) return SQ_ERROR
;
140 k
= stoi(str
.substr(6));
141 } else if (sq_gettype(vm
, 2) == OT_INTEGER
) {
143 sq_getinteger(vm
, 2, &key
);
149 if (k
> SCRIPT_TEXT_MAX_PARAMETERS
) return SQ_ERROR
;
150 if (k
< 1) return SQ_ERROR
;
153 return this->_SetParam(k
, vm
);
156 std::string
ScriptText::GetEncodedText()
158 ScriptTextList seen_texts
;
162 auto output
= std::back_inserter(result
);
163 this->_FillParamList(params
, seen_texts
);
164 this->_GetEncodedText(output
, param_count
, params
, true);
165 if (param_count
> SCRIPT_TEXT_MAX_PARAMETERS
) throw Script_FatalError(fmt::format("{}: Too many parameters", GetGameStringName(this->string
)));
169 void ScriptText::_FillParamList(ParamList
¶ms
, ScriptTextList
&seen_texts
)
171 if (std::ranges::find(seen_texts
, this) != seen_texts
.end()) throw Script_FatalError(fmt::format("{}: Circular reference detected", GetGameStringName(this->string
)));
172 seen_texts
.push_back(this);
174 for (int i
= 0; i
< this->paramc
; i
++) {
175 Param
*p
= &this->param
[i
];
176 params
.emplace_back(this->string
, i
, p
);
177 if (!std::holds_alternative
<ScriptTextRef
>(*p
)) continue;
178 std::get
<ScriptTextRef
>(*p
)->_FillParamList(params
, seen_texts
);
181 seen_texts
.pop_back();
183 /* Fill with dummy parameters to match FormatString() behaviour. */
184 if (seen_texts
.empty()) {
185 static Param dummy
= 0;
186 int nb_extra
= SCRIPT_TEXT_MAX_PARAMETERS
- (int)params
.size();
187 for (int i
= 0; i
< nb_extra
; i
++)
188 params
.emplace_back(StringIndexInTab(-1), i
, &dummy
);
192 void ScriptText::ParamCheck::Encode(std::back_insert_iterator
<std::string
> &output
, const char *cmd
)
194 if (this->cmd
== nullptr) this->cmd
= cmd
;
195 if (this->used
) return;
198 std::back_insert_iterator
<std::string
> &output
;
200 void operator()(const std::string
&value
) { fmt::format_to(this->output
, ":\"{}\"", value
); }
201 void operator()(const SQInteger
&value
) { fmt::format_to(this->output
, ":{:X}", value
); }
202 void operator()(const ScriptTextRef
&value
)
204 fmt::format_to(this->output
, ":");
205 Utf8Encode(this->output
, SCC_ENCODED
);
206 fmt::format_to(this->output
, "{:X}", value
->string
);
210 std::visit(visitor
{output
}, *this->param
);
214 void ScriptText::_GetEncodedText(std::back_insert_iterator
<std::string
> &output
, int ¶m_count
, ParamSpan args
, bool first
)
216 const std::string
&name
= GetGameStringName(this->string
);
219 Utf8Encode(output
, SCC_ENCODED
);
220 fmt::format_to(output
, "{:X}", this->string
);
223 const StringParams
¶ms
= GetGameStringParams(this->string
);
226 auto get_next_arg
= [&]() {
227 if (idx
>= args
.size()) throw Script_FatalError(fmt::format("{}({}): Not enough parameters", name
, param_count
+ 1));
228 ParamCheck
&pc
= args
[idx
++];
229 if (pc
.owner
!= this->string
) ScriptLog::Warning(fmt::format("{}({}): Consumes {}({})", name
, param_count
+ 1, GetGameStringName(pc
.owner
), pc
.idx
+ 1));
232 auto skip_args
= [&](size_t nb
) { idx
+= nb
; };
234 for (const StringParam
&cur_param
: params
) {
236 switch (cur_param
.type
) {
237 case StringParam::UNUSED
:
238 skip_args(cur_param
.consumes
);
241 case StringParam::RAW_STRING
:
243 ParamCheck
&p
= *get_next_arg();
244 p
.Encode(output
, cur_param
.cmd
);
245 if (p
.cmd
!= cur_param
.cmd
) throw 1;
246 if (!std::holds_alternative
<std::string
>(*p
.param
)) ScriptLog::Error(fmt::format("{}({}): {{{}}} expects a raw string", name
, param_count
+ 1, cur_param
.cmd
));
250 case StringParam::STRING
:
252 ParamCheck
&p
= *get_next_arg();
253 p
.Encode(output
, cur_param
.cmd
);
254 if (p
.cmd
!= cur_param
.cmd
) throw 1;
255 if (!std::holds_alternative
<ScriptTextRef
>(*p
.param
)) {
256 ScriptLog::Error(fmt::format("{}({}): {{{}}} expects a GSText", name
, param_count
+ 1, cur_param
.cmd
));
261 ScriptTextRef
&ref
= std::get
<ScriptTextRef
>(*p
.param
);
262 ref
->_GetEncodedText(output
, count
, args
.subspan(idx
), false);
263 if (++count
!= cur_param
.consumes
) {
264 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));
265 /* Fill missing params if needed. */
266 for (int i
= count
; i
< cur_param
.consumes
; i
++) fmt::format_to(output
, ":0");
268 skip_args(cur_param
.consumes
- 1);
273 for (int i
= 0; i
< cur_param
.consumes
; i
++) {
274 ParamCheck
&p
= *get_next_arg();
275 p
.Encode(output
, i
== 0 ? cur_param
.cmd
: nullptr);
276 if (i
== 0 && p
.cmd
!= cur_param
.cmd
) throw 1;
277 if (!std::holds_alternative
<SQInteger
>(*p
.param
)) ScriptLog::Error(fmt::format("{}({}): {{{}}} expects an integer", name
, param_count
+ i
+ 1, cur_param
.cmd
));
281 param_count
+= cur_param
.consumes
;
284 ScriptLog::Warning(fmt::format("{}({}): Invalid parameter", name
, param_count
));
289 const std::string
Text::GetDecodedText()
291 ::SetDParamStr(0, this->GetEncodedText());
292 return ::GetString(STR_JUST_RAW_STRING
);