summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/script_commands.txt73
-rw-r--r--npc/dev/test.txt18
-rw-r--r--src/map/script.c281
3 files changed, 275 insertions, 97 deletions
diff --git a/doc/script_commands.txt b/doc/script_commands.txt
index 05d075ac1..f9a628953 100644
--- a/doc/script_commands.txt
+++ b/doc/script_commands.txt
@@ -8067,10 +8067,75 @@ Example:
*sprintf(<format>{,param{,param{,...}}})
-C style sprintf. The resulting string is returned same as in PHP. All C
-format specifiers are supported except %n. For more info check sprintf
-function at www.cplusplus.com
-Number of params is only limited by Hercules' script engine.
+C style sprintf. The resulting string is returned.
+
+The format string can contain placeholders (format specifiers) using the
+following structure:
+
+ %[parameter][flags][width]type
+
+The following format specifier types are supported:
+
+%%: Prints a literal '%' (special case, doesn't support parameter, flag, width)
+%d, %i: Formats the specified value as a decimal signed number
+%u: Formats the specified value as a decimal unsigned number
+%x: Formats the specified value as a hexadecimal (lowercase) unsigned number
+%X: Formats the specified value as a hexadecimal (uppercase) unsigned number
+%o: Formats the specified value as an octal unsigned number
+%s: Formats the specified value as a string
+%c: Formats the specified value as a character (only uses the first character
+ of strings)
+
+The following format specifier types are not supported:
+
+%n (not implemented due to safety concerns)
+%f, %F, %e, %E, %g, %G (the script engine doesn't use floating point values)
+%p (the script engine doesn't use pointers)
+%a, %A (not supported, use 0x%x and 0x%X respectively instead)
+
+An ordinal parameter can be specified in the form 'x$' (where x is a number),
+to reorder the output (this may be useful in translated strings, where the
+sentence order may be different from the original order). Example:
+
+ // Name, level, job name
+ mes(sprintf("Hello, I'm %s, a level %d %s", strcharinfo(PC_NAME), BaseLevel, jobname(Class)));
+
+When translating the sentence to other languages (for example Italian),
+swapping some arguments may be appropriate, and it may be desirable to keep the
+actual arguments in the same order (i.e. when translating through the HULD):
+
+ // Job name is printed before the level, although they're specified in the opposite order.
+ // Name, job name, level
+ mes(sprintf("Ciao, io sono %1$s, un %3$s di livello %2$d", strcharinfo(PC_NAME), BaseLevel, jobname(Class)));
+
+The supported format specifier flags are:
+
+- (minus): Left-align the output of this format specifier. (the default is to
+ right-align the output).
++ (plus): Prepends a plus for positive signed-numeric types. positive = '+',
+ negative = '-'.
+(space): Prepends a space for positive signed-numeric types. positive = ' ',
+ negative = '-'. This flag is ignored if the '+' flag exists.
+0 (zero): When a field width option is specified, prepends zeros for numeric
+ types. (the default prepends spaces).
+A field width can be specified.
+
+ mes(sprintf("The temperature is %+d degrees Celsius", .@temperature)); // Keeps the '+' sign in front of positive values
+ .@map_name$ = sprintf("quiz_%02d", .@i); // Keeps the leading 0 in "quiz_00", etc
+
+A field width may be specified, to ensure that 'at least' that many characters
+are printed. If a star ('*') is specified as width, then the width is read as
+argument to the sprintf() function. This also supports positional arguments.
+
+ sprintf("%04d", 10) // Returns "0010"
+ sprintf("%0*d", 5, 10) // Returns "00010"
+ sprintf("%5d", 10) // Returns " 10"
+ sprintf("%-5d", 10) // Returns "10 "
+ sprintf("%10s", "Hello") // Returns " Hello";
+ sprintf("%-10s", "Hello") // Returns "Hello ";
+
+Precision ('.X') and length ('hh', 'h', 'l', 'll', 'L', 'z', 'j', 't')
+specifiers are not implemented (not necessary for the script engine purposes)
Example:
.@format$ = "The %s contains %d monkeys";
diff --git a/npc/dev/test.txt b/npc/dev/test.txt
index 72cf86616..ee2bda259 100644
--- a/npc/dev/test.txt
+++ b/npc/dev/test.txt
@@ -711,6 +711,24 @@ function script HerculesSelfTestHelper {
callsub(OnCheck, "Callfunc (return NPC variables from another NPC)", callfunc("F_TestVarOfAnotherNPC", "TestVarOfAnotherNPC"), 1);
callsub(OnCheck, "Callfunc (return NPC variables from another NPC - local variable overwrite check)", .x, 2);
+ callsub(OnCheckStr, "sprintf (%%)", sprintf("'%%'"), "'%'");
+ callsub(OnCheckStr, "sprintf (%d)", sprintf("'%d'", 5), "'5'");
+ callsub(OnCheckStr, "sprintf (neg. %d)", sprintf("'%d'", -5), "'-5'");
+ callsub(OnCheckStr, "sprintf (%u)", sprintf("'%u'", 5), "'5'");
+ callsub(OnCheckStr, "sprintf (%x)", sprintf("'%x'", 10), "'a'");
+ callsub(OnCheckStr, "sprintf (%X)", sprintf("'%X'", 31), "'1F'");
+ callsub(OnCheckStr, "sprintf (%s)", sprintf("'%s'", "Hello World!"), "'Hello World!'");
+ callsub(OnCheckStr, "sprintf (%c)", sprintf("'%c'", "Hello World!"), "'H'");
+ callsub(OnCheckStr, "sprintf (%+d)", sprintf("'%+d'", 5), "'+5'");
+ callsub(OnCheckStr, "sprintf (%{n}d)", sprintf("'%5d'", 5), "' 5'");
+ callsub(OnCheckStr, "sprintf (%-{n}d)", sprintf("'%-5d'", 5), "'5 '");
+ callsub(OnCheckStr, "sprintf (%-+{n}d)", sprintf("'%-+5d'", 5), "'+5 '");
+ callsub(OnCheckStr, "sprintf (%+0{n}d)", sprintf("'%+05d'", 5), "'+0005'");
+ callsub(OnCheckStr, "sprintf (%0*d)", sprintf("'%0*d'", 5, 10), "'00010'");
+ callsub(OnCheckStr, "sprintf (Two args)", sprintf("'%+05d' '%x'", 5, 0x7f), "'+0005' '7f'");
+ callsub(OnCheckStr, "sprintf (positional)", sprintf("'%2$+05d'", 5, 6), "'+0006'");
+ callsub(OnCheckStr, "sprintf (positional)", sprintf("'%2$s' '%1$c'", "First", "Second"), "'Second' 'F'");
+
if (.errors) {
debugmes "Script engine self-test [ \033[0;31mFAILED\033[0m ]";
debugmes "**** The test was completed with " + .errors + " errors. ****";
diff --git a/src/map/script.c b/src/map/script.c
index fa33c5d76..d97dacb89 100644
--- a/src/map/script.c
+++ b/src/map/script.c
@@ -15239,129 +15239,224 @@ BUILDIN(implode)
// Implements C sprintf, except format %n. The resulting string is
// returned, instead of being saved in variable by reference.
//-------------------------------------------------------
-BUILDIN(sprintf) {
- unsigned int argc = 0, arg = 0;
- const char* format;
- char* p;
- char* q;
- char* buf = NULL;
- char* buf2 = NULL;
- struct script_data* data;
- size_t len, buf2_len = 0;
+BUILDIN(sprintf)
+{
+ const char *format = script_getstr(st, 2);
+ const char *p = NULL, *np = NULL;
StringBuf final_buf;
+ char *buf = NULL;
+ int buf_len = 0;
+ int lastarg = 2;
+ int argc = script_lastdata(st) + 1;
- // Fetch init data
- format = script_getstr(st, 2);
- argc = script_lastdata(st)-2;
- len = strlen(format);
-
- // Skip parsing, where no parsing is required.
- if(len==0) {
- script_pushconststr(st,"");
- return true;
- }
-
- // Pessimistic alloc
- CREATE(buf, char, len+1);
-
- // Need not be parsed, just solve stuff like %%.
- if(argc==0) {
- memcpy(buf,format,len+1);
- script_pushstrcopy(st, buf);
- aFree(buf);
- return true;
- }
+ StrBuf->Init(&final_buf);
- safestrncpy(buf, format, len+1);
+ p = format;
- // Issue sprintf for each parameter
- StrBuf->Init(&final_buf);
- q = buf;
- while((p = strchr(q, '%'))!=NULL) {
- if(p!=q) {
- len = p-q+1;
- if(buf2_len<len) {
- RECREATE(buf2, char, len);
- buf2_len = len;
+ /*
+ * format-string = "" / *(text / placeholder)
+ * placeholder = "%%" / "%n" / std-placeholder
+ * std-placeholder = "%" [pos-parameter] [flags] [width] [precision] [length] type
+ * pos-parameter = number "$"
+ * flags = *("-" / "+" / "0" / SP)
+ * width = number / ("*" [pos-parameter])
+ * precision = "." (number / ("*" [pos-parameter]))
+ * length = "hh" / "h" / "l" / "ll" / "L" / "z" / "j" / "t"
+ * type = "d" / "i" / "u" / "f" / "F" / "e" / "E" / "g" / "G" / "x" / "X" / "o" / "s" / "c" / "p" / "a" / "A"
+ * number = digit-nonzero *DIGIT
+ * digit-nonzero = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9"
+ */
+
+ while ((np = strchr(p, '%')) != NULL) {
+ bool flag_plus = false, flag_minus = false, flag_zero = false, flag_space = false;
+ bool positional_arg = false;
+ int width = 0, nextarg = lastarg + 1, thisarg = nextarg;
+
+ if (p != np) {
+ int len = (int)(np - p + 1);
+ if (buf_len < len) {
+ RECREATE(buf, char, len);
+ buf_len = len;
}
- safestrncpy(buf2, q, len);
- StrBuf->AppendStr(&final_buf, buf2);
- q = p;
+ safestrncpy(buf, p, len);
+ StrBuf->AppendStr(&final_buf, buf);
}
- p = q+1;
- if(*p=='%') { // %%
+
+ p = np;
+ np++;
+
+ // placeholder = "%%" ; (special case)
+ if (*np == '%') {
StrBuf->AppendStr(&final_buf, "%");
- q+=2;
+ p = np + 1;
continue;
}
- if(*p=='n') { // %n
+ // placeholder = "%n" ; (ignored)
+ if (*np == 'n') {
ShowWarning("buildin_sprintf: Format %%n not supported! Skipping...\n");
script->reportsrc(st);
- q+=2;
+ lastarg = nextarg;
+ p = np + 1;
continue;
}
- if(arg>=argc) {
+
+ // std-placeholder = "%" [pos-parameter] [flags] [width] [precision] [length] type
+
+ // pos-parameter = number "$"
+ if (ISDIGIT(*np) && *np != '0') {
+ const char *pp = np;
+ while (ISDIGIT(*pp))
+ pp++;
+ if (*pp == '$') {
+ thisarg = atoi(np) + 2;
+ positional_arg = true;
+ np = pp + 1;
+ }
+ }
+
+ if (thisarg >= argc) {
ShowError("buildin_sprintf: Not enough arguments passed!\n");
- aFree(buf);
- if(buf2) aFree(buf2);
+ if (buf != NULL)
+ aFree(buf);
StrBuf->Destroy(&final_buf);
script_pushconststr(st,"");
return false;
}
- if((p = strchr(q+1, '%'))==NULL) {
- p = strchr(q, 0); // EOS
- }
- len = p-q+1;
- if(buf2_len<len) {
- RECREATE(buf2, char, len);
- buf2_len = len;
+
+ // flags = *("-" / "+" / "0" / SP)
+ while (true) {
+ if (*np == '-') {
+ flag_minus = true;
+ } else if (*np == '+') {
+ flag_plus = true;
+ } else if (*np == ' ') {
+ flag_space = true;
+ } else if (*np == '0') {
+ flag_zero = true;
+ } else {
+ break;
+ }
+ np++;
}
- safestrncpy(buf2, q, len);
- q = p;
- // Note: This assumes the passed value being the correct
- // type to the current format specifier. If not, the server
- // probably crashes or returns anything else, than expected,
- // but it would behave in normal code the same way so it's
- // the scripter's responsibility.
- data = script_getdata(st, arg+3);
- if(data_isstring(data)) { // String
- StrBuf->Printf(&final_buf, buf2, script_getstr(st, arg+3));
- } else if(data_isint(data)) { // Number
- StrBuf->Printf(&final_buf, buf2, script_getnum(st, arg+3));
- } else if(data_isreference(data)) { // Variable
- char* name = reference_getname(data);
- if(name[strlen(name)-1]=='$') { // var Str
- StrBuf->Printf(&final_buf, buf2, script_getstr(st, arg+3));
- } else { // var Int
- StrBuf->Printf(&final_buf, buf2, script_getnum(st, arg+3));
+ // width = number / ("*" [pos-parameter])
+ if (ISDIGIT(*np)) {
+ width = atoi(np);
+ while (ISDIGIT(*np))
+ np++;
+ } else if (*np == '*') {
+ bool positional_widtharg = false;
+ int width_arg;
+ np++;
+ // pos-parameter = number "$"
+ if (ISDIGIT(*np) && *np != '0') {
+ const char *pp = np;
+ while (ISDIGIT(*pp))
+ pp++;
+ if (*pp == '$') {
+ width_arg = atoi(np) + 2;
+ positional_widtharg = true;
+ np = pp + 1;
+ }
}
- } else { // Unsupported type
- ShowError("buildin_sprintf: Unknown argument type!\n");
- aFree(buf);
- if(buf2) aFree(buf2);
+ if (!positional_widtharg) {
+ width_arg = nextarg;
+ nextarg++;
+ if (!positional_arg)
+ thisarg++;
+ }
+
+ if (width_arg >= argc || thisarg >= argc) {
+ ShowError("buildin_sprintf: Not enough arguments passed!\n");
+ if (buf != NULL)
+ aFree(buf);
+ StrBuf->Destroy(&final_buf);
+ script_pushconststr(st,"");
+ return false;
+ }
+ width = script_getnum(st, width_arg);
+ }
+
+ // precision = "." (number / ("*" [pos-parameter])) ; (not needed/implemented)
+
+ // length = "hh" / "h" / "l" / "ll" / "L" / "z" / "j" / "t" ; (not needed/implemented)
+
+ // type = "d" / "i" / "u" / "f" / "F" / "e" / "E" / "g" / "G" / "x" / "X" / "o" / "s" / "c" / "p" / "a" / "A"
+ if (buf_len < 16) {
+ RECREATE(buf, char, 16);
+ buf_len = 16;
+ }
+ {
+ int i = 0;
+ memset(buf, '\0', buf_len);
+ buf[i++] = '%';
+ if (flag_minus)
+ buf[i++] = '-';
+ if (flag_plus)
+ buf[i++] = '+';
+ else if (flag_space) // ignored if '+' is specified
+ buf[i++] = ' ';
+ if (flag_zero)
+ buf[i++] = '0';
+ if (width > 0)
+ safesnprintf(buf + i, buf_len - i - 1, "%d", width);
+ }
+ buf[(int)strlen(buf)] = *np;
+ switch (*np) {
+ case 'd':
+ case 'i':
+ case 'u':
+ case 'x':
+ case 'X':
+ case 'o':
+ // Piggyback printf
+ StrBuf->Printf(&final_buf, buf, script_getnum(st, thisarg));
+ break;
+ case 's':
+ // Piggyback printf
+ StrBuf->Printf(&final_buf, buf, script_getstr(st, thisarg));
+ break;
+ case 'c':
+ {
+ const char *str = script_getstr(st, thisarg);
+ // Piggyback printf
+ StrBuf->Printf(&final_buf, buf, str[0]);
+ }
+ break;
+ case 'f':
+ case 'F':
+ case 'e':
+ case 'E':
+ case 'g':
+ case 'G':
+ case 'p':
+ case 'a':
+ case 'A':
+ ShowWarning("buildin_sprintf: Format %%%c not supported! Skipping...\n", *np);
+ script->reportsrc(st);
+ lastarg = nextarg;
+ p = np + 1;
+ continue;
+ default:
+ ShowError("buildin_sprintf: Invalid format string.\n");
+ if (buf != NULL)
+ aFree(buf);
StrBuf->Destroy(&final_buf);
script_pushconststr(st,"");
return false;
}
- arg++;
- }
-
- // Append anything left
- if(*q) {
- StrBuf->AppendStr(&final_buf, q);
+ lastarg = nextarg;
+ p = np + 1;
}
- // Passed more, than needed
- if(arg<argc) {
- ShowWarning("buildin_sprintf: Unused arguments passed.\n");
- script->reportsrc(st);
- }
+ // Append the remaining part
+ if (p != NULL)
+ StrBuf->AppendStr(&final_buf, p);
script_pushstrcopy(st, StrBuf->Value(&final_buf));
- aFree(buf);
- if(buf2) aFree(buf2);
+ if (buf != NULL)
+ aFree(buf);
StrBuf->Destroy(&final_buf);
return true;