Codechange: add unit test against enum over optimisation
[openttd-github.git] / src / script / api / script_text.cpp
blob0d923034a052eb546c0acf9c3bb1a037fdc503b0
1 /*
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/>.
6 */
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;
27 if (nparam < 1) {
28 throw sq_throwerror(vm, "You need to pass at least a StringID to the constructor");
31 /* First resolve the StringID. */
32 SQInteger sqstring;
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. */
41 sq_push(vm, i + 3);
43 if (SQ_FAILED(this->_SetParam(i, vm))) {
44 this->~ScriptText();
45 throw sq_throwerror(vm, "Invalid parameter");
48 /* Pop the parameter again. */
49 sq_pop(vm, 1);
53 SQInteger ScriptText::_SetParam(int parameter, HSQUIRRELVM vm)
55 if (parameter >= SCRIPT_TEXT_MAX_PARAMETERS) return SQ_ERROR;
57 switch (sq_gettype(vm, -1)) {
58 case OT_STRING: {
59 const SQChar *value;
60 sq_getstring(vm, -1, &value);
62 this->param[parameter] = StrMakeValid(value);
63 break;
66 case OT_INTEGER: {
67 SQInteger value;
68 sq_getinteger(vm, -1, &value);
70 this->param[parameter] = value;
71 break;
74 case OT_INSTANCE: {
75 SQUserPointer real_instance = nullptr;
76 HSQOBJECT instance;
78 sq_getstackobj(vm, -1, &instance);
80 /* Validate if it is a GSText instance */
81 sq_pushroottable(vm);
82 sq_pushstring(vm, "GSText", -1);
83 sq_get(vm, -2);
84 sq_pushobject(vm, instance);
85 if (sq_instanceof(vm) != SQTrue) return SQ_ERROR;
86 sq_pop(vm, 3);
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);
94 break;
97 default: return SQ_ERROR;
100 if (this->paramc <= parameter) this->paramc = parameter + 1;
101 return 0;
104 SQInteger ScriptText::SetParam(HSQUIRRELVM vm)
106 if (sq_gettype(vm, 2) != OT_INTEGER) return SQ_ERROR;
108 SQInteger k;
109 sq_getinteger(vm, 2, &k);
111 if (k > SCRIPT_TEXT_MAX_PARAMETERS) return SQ_ERROR;
112 if (k < 1) return SQ_ERROR;
113 k--;
115 return this->_SetParam(k, vm);
118 SQInteger ScriptText::AddParam(HSQUIRRELVM vm)
120 SQInteger res;
121 res = this->_SetParam(this->paramc, vm);
122 if (res != 0) return res;
124 /* Push our own instance back on top of the stack */
125 sq_push(vm, 1);
126 return 1;
129 SQInteger ScriptText::_set(HSQUIRRELVM vm)
131 int32_t k;
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) {
142 SQInteger key;
143 sq_getinteger(vm, 2, &key);
144 k = (int32_t)key;
145 } else {
146 return SQ_ERROR;
149 if (k > SCRIPT_TEXT_MAX_PARAMETERS) return SQ_ERROR;
150 if (k < 1) return SQ_ERROR;
151 k--;
153 return this->_SetParam(k, vm);
156 std::string ScriptText::GetEncodedText()
158 ScriptTextList seen_texts;
159 ParamList params;
160 int param_count = 0;
161 std::string result;
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)));
166 return result;
169 void ScriptText::_FillParamList(ParamList &params, 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;
197 struct visitor {
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);
211 this->used = true;
214 void ScriptText::_GetEncodedText(std::back_insert_iterator<std::string> &output, int &param_count, ParamSpan args, bool first)
216 const std::string &name = GetGameStringName(this->string);
218 if (first) {
219 Utf8Encode(output, SCC_ENCODED);
220 fmt::format_to(output, "{:X}", this->string);
223 const StringParams &params = GetGameStringParams(this->string);
225 size_t idx = 0;
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));
230 return &pc;
232 auto skip_args = [&](size_t nb) { idx += nb; };
234 for (const StringParam &cur_param : params) {
235 try {
236 switch (cur_param.type) {
237 case StringParam::UNUSED:
238 skip_args(cur_param.consumes);
239 break;
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));
247 break;
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));
257 param_count++;
258 continue;
260 int count = 0;
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);
269 break;
272 default:
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;
282 } catch (int nb) {
283 param_count += nb;
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);