Parsing JSON in AGS

Started by eri0o, Sun 12/01/2020 03:12:51

Previous topic - Next topic

eri0o

So, I kinda was curious about parsing JSON on AGS, I had a need for reading and not writing. Still, I decided to go on and look on Google, and found this c library called JSMN: https://github.com/zserge/jsmn/blob/master/jsmn.h

The code looked simple enough, so I decided to try to port it to AGS Script...

jsmn.ash
Spoiler
Code: ags

// new module header

/**
 * JSON type identifier. Basic types are:
 * 	o Object
 * 	o Array
 * 	o String
 * 	o Other primitive: number, boolean (true/false) or null
 */
 enum jsmntype {
  eJSMN_UNDEFINED = 0,
  eJSMN_OBJECT = 1,
  eJSMN_ARRAY = 2,
  eJSMN_STRING = 3,
  eJSMN_PRIMITIVE = 4
};

enum jsmnerr {
  /* Not enough tokens were provided */
  eJSMN_ERROR_NOMEM = -1,
  /* Invalid character inside JSON string */
  eJSMN_ERROR_INVAL = -2,
  /* The string is not a full JSON packet, more bytes expected */
  eJSMN_ERROR_PART = -3
};

/**
 * JSON token description.
 * type		type (object, array, string etc.)
 * start	start position in JSON data string
 * end		end position in JSON data string
 */
managed struct jsmntok {
  jsmntype type;
  int start;
  int end;
  int size;
  int parent;
};

/**
 * JSON parser. Contains an array of token blocks available. Also stores
 * the string being parsed now and current position in that string.
 */
managed struct jsmn_parser{
  int pos;     /* offset in the JSON string */
  int toknext; /* next token to allocate */
  int toksuper;         /* superior token node, e.g. parent object or array */
};

/**
 * Create JSON parser over an array of tokens
 */
import void jsmn_init(jsmn_parser *parser);

/**
 * Run JSON parser. It parses a JSON data string into and array of tokens, each
 * describing
 * a single JSON object.
 */
import int jsmn_parse(jsmn_parser *parser, String js, int len, jsmntok *tokens[], int num_tokens);
[close]


jsmn.asc
Spoiler
Code: ags

// new module script

/**
 * Allocates a fresh unused token from the token pool.
 */
jsmntok *jsmn_alloc_token(jsmn_parser *parser, jsmntok *tokens[], int num_tokens) {
  jsmntok *tok;
  if (parser.toknext >= num_tokens) {
    return null;
  }
  parser.toknext = parser.toknext+1;
  tok = tokens[parser.toknext];
  tok.end = -1;
  tok.start = -1;
  tok.size = 0;
  tok.parent = -1;
  return tok;
}

/**
 * Fills token type and boundaries.
 */
void jsmn_fill_token(jsmntok *token, jsmntype type, int start, int end) {
  token.type = type;
  token.start = start;
  token.end = end;
  token.size = 0;
}


int _parse_found(int start, jsmn_parser *parser, String js, int len, jsmntok *tokens[], int num_tokens){
  jsmntok *token;
  if (tokens == null) {
    parser.pos--;
    return 0;
  }
  token = jsmn_alloc_token(parser, tokens, num_tokens);
  if (token == null) {
    parser.pos = start;
    return eJSMN_ERROR_NOMEM;
  }
  jsmn_fill_token(token, eJSMN_PRIMITIVE, start, parser.pos);

  token.parent = parser.toksuper;
  parser.pos--;
  return 0;
}

/**
 * Fills next available token with JSON primitive.
 */
int jsmn_parse_primitive(jsmn_parser *parser, String js, int len, jsmntok *tokens[], int num_tokens) {
  jsmntok *token;
  int start;

  start = parser.pos;

  for (; parser.pos < len && js.Chars[parser.pos] != 0; parser.pos++) {
    switch (js.Chars[parser.pos]) {

    /* In strict mode primitive must be followed by "," or "}" or "]" */
    case ':':
    case 20: /* '\t' */
    case 18: /* '\r' */
    case 14: /* '\n' */
    case ' ':
    case ',':
    case ']':
    case '}':
      return _parse_found(start, parser, js, len, tokens, num_tokens);
    default:
                   /* to quiet a warning from gcc*/
      break;
    }
    if (js.Chars[parser.pos] < 32 || js.Chars[parser.pos] >= 127) {
      parser.pos = start;
      return eJSMN_ERROR_INVAL;
    }
  }
  
  /* In strict mode primitive must be followed by a comma/object/array */
  parser.pos = start;
  return eJSMN_ERROR_PART;
}

/**
 * Fills next token with JSON string.
 */
int jsmn_parse_string(jsmn_parser *parser, String js, int len, jsmntok *tokens[], int num_tokens) {
  jsmntok *token;

  int start = parser.pos;

  parser.pos++;

  /* Skip starting quote */
  for (; parser.pos < len && js.Chars[parser.pos] != 0; parser.pos++) {
    char c = js.Chars[parser.pos];

    /* Quote: end of string */
    if (c == '"') {
      if (tokens == null) {
        return 0;
      }
      token = jsmn_alloc_token(parser, tokens, num_tokens);
      if (token == null) {
        parser.pos = start;
        return eJSMN_ERROR_NOMEM;
      }
      jsmn_fill_token(token, eJSMN_STRING, start + 1, parser.pos);
      token.parent = parser.toksuper;
      return 0;
    }

    /* Backslash: Quoted symbol expected */
    if (c == 92 && parser.pos + 1 < len) {
      int i;
      parser.pos++;
      switch (js.Chars[parser.pos]) {
      /* Allowed escaped symbols */
      case '"':
      case '/':
      case 92:
      case 'b':
      case 'f':
      case 'r':
      case 'n':
      case 't':
        break;
      /* Allows escaped symbol \uXXXX */
      case 'u':
        parser.pos++;
        for (i = 0; i < 4 && parser.pos < len && js.Chars[parser.pos] != 0; i++) {
          /* If it isn't a hex character we have an error */
          if (!((js.Chars[parser.pos] >= 48 && js.Chars[parser.pos] <= 57) ||   /* 0-9 */
                (js.Chars[parser.pos] >= 65 && js.Chars[parser.pos] <= 70) ||   /* A-F */
                (js.Chars[parser.pos] >= 97 && js.Chars[parser.pos] <= 102))) { /* a-f */
            parser.pos = start;
            return eJSMN_ERROR_INVAL;
          }
          parser.pos++;
        }
        parser.pos--;
        break;
      /* Unexpected symbol */
      default:
        parser.pos = start;
        return eJSMN_ERROR_INVAL;
      }
    }
  }
  parser.pos = start;
  return eJSMN_ERROR_PART;
}


/**
 * Parse JSON string and fill tokens.
 */
int jsmn_parse(jsmn_parser *parser, String js, int len, jsmntok *tokens[], int num_tokens) {
  int r;
  int i;
  jsmntok *t;
  jsmntok *token;
  int count = parser.toknext;
  
  for (; parser.pos < len && js.Chars[parser.pos] != 0; parser.pos++) {
    char c;
    jsmntype type;
    
    c = js.Chars[parser.pos];
    
    switch (c) {
    case '{':
    case '[':
      count++;
      if (tokens == null) {
        break;
      }
      token = jsmn_alloc_token(parser, tokens, num_tokens);
      if (token == null) {
        return eJSMN_ERROR_NOMEM;
      }
      if (parser.toksuper != -1) {
        t = tokens[parser.toksuper];

        /* In strict mode an object or array can't become a key */
        if (t.type == eJSMN_OBJECT) {
          return eJSMN_ERROR_INVAL;
        }

        t.size++;
        token.parent = parser.toksuper;
      }
      if(c == '{') token.type = eJSMN_OBJECT;
      else token.type = eJSMN_ARRAY;
      token.start = parser.pos;
      parser.toksuper = parser.toknext - 1;
      break;
    case '}':
    case ']':
      if (tokens == null) {
        break;
      }
      
      if(c == '}') type = eJSMN_OBJECT;
      else type = eJSMN_ARRAY;

      if (parser.toknext < 1) {
        return eJSMN_ERROR_INVAL;
      }
      token = tokens[parser.toknext - 1];
      for (;;) {
        if (token.start != -1 && token.end == -1) {
          if (token.type != type) {
            return eJSMN_ERROR_INVAL;
          }
          token.end = parser.pos + 1;
          parser.toksuper = token.parent;
          break;
        }
        if (token.parent == -1) {
          if (token.type != type || parser.toksuper == -1) {
            return eJSMN_ERROR_INVAL;
          }
          break;
        }
        token = tokens[token.parent];
      }
      break;
    case '"': /* '\"' */
      r = jsmn_parse_string(parser, js, len, tokens, num_tokens);
      if (r < 0) {
        return r;
      }
      count++;
      if (parser.toksuper != -1 && tokens != null) {
        tokens[parser.toksuper].size++;
      }
      break;
    case 20: /* '\t' */
    case 18: /* '\r' */
    case 14: /* '\n' */
    case ' ':
      break;
    case ':':
      parser.toksuper = parser.toknext - 1;
      break;
    case ',':
      if (tokens != null && parser.toksuper != -1 &&
          tokens[parser.toksuper].type != eJSMN_ARRAY &&
          tokens[parser.toksuper].type != eJSMN_OBJECT) {

        parser.toksuper = tokens[parser.toksuper].parent;


      }
      break;
      
    /* In strict mode primitives are: numbers and booleans */
    case '-':
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
    case 't':
    case 'f':
    case 'n':
      /* And they must not be keys of the object */
      if (tokens != null && parser.toksuper != -1) {
        t = null;
        t = tokens[parser.toksuper];
        if (t.type == eJSMN_OBJECT ||
            (t.type == eJSMN_STRING && t.size != 0)) {
          return eJSMN_ERROR_INVAL;
        }
      }

      r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens);
      if (r < 0) {
        return r;
      }
      count++;
      if (parser.toksuper != -1 && tokens != null) {
        tokens[parser.toksuper].size++;
      }
      break;

    /* Unexpected char in strict mode */
    default:
      return eJSMN_ERROR_INVAL;
    }
    
  }
  
}

/**
 * Creates a new parser based over a given buffer with an array of tokens
 * available.
 */
void jsmn_init(jsmn_parser *parser) {
  parser.pos = 0;
  parser.toknext = 0;
  parser.toksuper = -1;
}

[close]

Now things appear to compile, so I decided to try it, I created a new room and tried to follow the example in the library here: https://github.com/zserge/jsmn/blob/master/example/simple.c

Room1.asc
Spoiler
Code: ags

// room script file

int jsoneq(String json, jsmntok *tok, String s) {
  if (tok.type == eJSMN_STRING && s.Length == (tok.end - tok.start) &&
      s == json.Substring(tok.start, tok.end-tok.start) ) {
    return 0;
  }
  return -1;
}

#define MAX_TOKENS 256

function room_AfterFadeIn()
{
  String simple_json = "{ \"name\":\"John\", \"age\":30, \"car\":null }";
  
  jsmn_parser* parser = new jsmn_parser;
  jsmn_init(parser);
  jsmntok* t[] = new jsmntok[MAX_TOKENS];
  for(int i=0; i<MAX_TOKENS; i++) t[i] = new jsmntok;
  int r;
  
  r = jsmn_parse(parser, simple_json, simple_json.Length, t, MAX_TOKENS);
  
  if (r < 0) {
    Display("Failed to parse JSON: %d\n", r);
    return 1;
  }
  
    /* Assume the top-level element is an object */
  if (r < 1 || t[0].type != eJSMN_OBJECT) {
    Display("Object expected\n");
    return 1;
  }
  
  
  //jsmn_
}
[close]

But I kinda couldn't advance much, I think I don't really understand how it's supposed to be used to parse things. :/

Does it expected usage makes sense to anybody?

The project can be downloaded here.

Snarky

Yeah, just from the header it seems to make sense. It should create a parse tree in the tokens array. Why don't you try and traverse the tree to see how it looks?

I don't think your jsoneq() function will work, though, since it seems to rely on s being an out parameter (so the function can modify the string provided as argument): in AGS you'd need to use a global variable. You should look out for similar problems in other functions.

eri0o

#2
Hey Snarky! I traversed and learned a few things!
Traverse code
Spoiler
Code: ags

  File* log = File.Open("$SAVEGAMEDIR$/log.log",eFileAppend);
  log.WriteRawLine("i ; tok_text ; tok.size ; tok.type;  tok.start ; tok.end ; tok.parent ");
  for(int i=0; i<MAX_TOKENS  ; i++){
    jsmntok* toke = t[i];
    log.WriteRawLine(String.Format("%d ; %s ; %d ; %d ; %d ; %d ; %d", i, simple_json.Substring(toke.start, toke.end-toke.start), toke.size , toke.type ,  toke.start ,  toke.end ,  toke.parent ));
    if(i>8) break;
  }
  log.Close();
[close]

for reference, on type, we need other table below for the type enum
Code: ags

 enum jsmntype {
  eJSMN_UNDEFINED = 0, //
  eJSMN_OBJECT = 1,       // JSON Object
  eJSMN_ARRAY = 2,         // Array
  eJSMN_STRING = 3,       // String
  eJSMN_PRIMITIVE = 4    // Other primitive: number, boolean (true/false) or null
};


The json is this -> { "name":"John", "age":30, "car":null }. The output log was this (I realigned things for readability):
Code: ags

i ; tok_text                                ; tok.size ; tok.type ;  tok.start ; tok.end ; tok.parent 
0 ;                                         ;        1 ;        0 ;          0 ;       0 ;    0
1 ; { "name":"John", "age":30, "car":null } ;        3 ;        1 ;          0 ;      39 ;   -1
2 ; name                                    ;        0 ;        3 ;          3 ;       7 ;    0
3 ; John                                    ;        1 ;        3 ;         10 ;      14 ;    1
4 ; age                                     ;        0 ;        3 ;         18 ;      21 ;    1
5 ; 30                                      ;        1 ;        4 ;         23 ;      25 ;    3
6 ; car                                     ;        0 ;        3 ;         28 ;      31 ;    1
7 ; null                                    ;        0 ;        4 ;         33 ;      37 ;    5
8 ;                                         ;        0 ;        0 ;          0 ;       0 ;    0
9 ;                                         ;        0 ;        0 ;          0 ;       0 ;    0


This gives us some precious information. The first time the curly braces is found, we can see the first element is undefined, but with size 1, implying one element following.
Then next element is a big json object, we know from the text, this json object represents all our data, it's size is 3, so we can expect 3 children, and it's parente is index 1 -1 (parent value is relative but I don't know exactly to what).
From there on, I am still having a bit of trouble analyzing the result before I jump to a JSON with nested JSON Objects...

My idea was to eventually generate an array of dynamic sprites from a Aseprite Export like below, where it has a json to describe the animation behavior:

CaptainClown.png
CaptainClown.json
Spoiler
Code: json

{ "frames": [
   {
    "filename": "Captain Clown Nose 0.aseprite",
    "frame": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 1.aseprite",
    "frame": { "x": 96, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 2.aseprite",
    "frame": { "x": 192, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 3.aseprite",
    "frame": { "x": 288, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 4.aseprite",
    "frame": { "x": 384, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 5.aseprite",
    "frame": { "x": 480, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 6.aseprite",
    "frame": { "x": 576, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 7.aseprite",
    "frame": { "x": 672, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 8.aseprite",
    "frame": { "x": 768, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 9.aseprite",
    "frame": { "x": 864, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 10.aseprite",
    "frame": { "x": 960, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 11.aseprite",
    "frame": { "x": 1056, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 12.aseprite",
    "frame": { "x": 1152, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 13.aseprite",
    "frame": { "x": 1248, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 5000
   },
   {
    "filename": "Captain Clown Nose 14.aseprite",
    "frame": { "x": 1344, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 15.aseprite",
    "frame": { "x": 1440, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 16.aseprite",
    "frame": { "x": 1536, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 17.aseprite",
    "frame": { "x": 1632, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 18.aseprite",
    "frame": { "x": 1728, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 19.aseprite",
    "frame": { "x": 1824, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 20.aseprite",
    "frame": { "x": 1920, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 5000
   },
   {
    "filename": "Captain Clown Nose 21.aseprite",
    "frame": { "x": 2016, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 22.aseprite",
    "frame": { "x": 2112, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 23.aseprite",
    "frame": { "x": 2208, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 24.aseprite",
    "frame": { "x": 2304, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 5000
   },
   {
    "filename": "Captain Clown Nose 25.aseprite",
    "frame": { "x": 2400, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 26.aseprite",
    "frame": { "x": 2496, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 27.aseprite",
    "frame": { "x": 2592, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 28.aseprite",
    "frame": { "x": 2688, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 5000
   },
   {
    "filename": "Captain Clown Nose 29.aseprite",
    "frame": { "x": 2784, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 5000
   },
   {
    "filename": "Captain Clown Nose 30.aseprite",
    "frame": { "x": 2880, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 31.aseprite",
    "frame": { "x": 2976, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 32.aseprite",
    "frame": { "x": 3072, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 33.aseprite",
    "frame": { "x": 3168, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 34.aseprite",
    "frame": { "x": 3264, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 35.aseprite",
    "frame": { "x": 3360, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   },
   {
    "filename": "Captain Clown Nose 36.aseprite",
    "frame": { "x": 3456, "y": 0, "w": 96, "h": 96 },
    "rotated": false,
    "trimmed": false,
    "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 96 },
    "sourceSize": { "w": 96, "h": 96 },
    "duration": 100
   }
 ],
 "meta": {
  "app": "http://www.aseprite.org/",
  "version": "1.2.16.3-x64",
  "image": "Captain Clown Nose.png",
  "format": "RGBA8888",
  "size": { "w": 3552, "h": 96 },
  "scale": "1",
  "frameTags": [
   { "name": "Idle S", "from": 0, "to": 4, "direction": "forward" },
   { "name": "Run S", "from": 5, "to": 10, "direction": "forward" },
   { "name": "Jump S", "from": 11, "to": 13, "direction": "forward" },
   { "name": "Fall S", "from": 14, "to": 14, "direction": "forward" },
   { "name": "Ground S", "from": 15, "to": 16, "direction": "forward" },
   { "name": "Dead Hit S", "from": 17, "to": 20, "direction": "forward" },
   { "name": "Dead Hit", "from": 21, "to": 24, "direction": "forward" },
   { "name": "Dead Ground", "from": 25, "to": 28, "direction": "forward" },
   { "name": "Sword", "from": 29, "to": 29, "direction": "forward" },
   { "name": "Attack 2", "from": 30, "to": 32, "direction": "forward" },
   { "name": "Underwater", "from": 33, "to": 36, "direction": "forward" }
  ],
  "layers": [
   { "name": "Grid", "opacity": 255, "blendMode": "normal" },
   { "name": "Captain", "opacity": 255, "blendMode": "normal" },
   { "name": "Effect", "opacity": 255, "blendMode": "normal" }
  ],
  "slices": [
  ]
 }
}
[close]

Anyway, but I am very far... Here is a test string that is throwing an error for me.

Code: ags
simple_json = "{\"squadName\":\"Super squad\",\"formed\":2016,\"active\":true,\"members\":[{\"name\":\"Molecule Man\",\"age\":29,\"secretIdentity\":\"Dan Jukes\",\"powers\":[\"Radiation resistance\",\"Radiation blast\"]},{\"name\":\"Madam Uppercut\",\"age\":39,\"secretIdentity\":\"Jane Wilson\",\"powers\":[\"Million punch\",\"Super reflexes\"]},{\"name\":\"Eternal Flame\",\"age\":100,\"secretIdentity\":\"Unknown\",\"powers\":[\"Immortality\",\"Heat Immunity\",\"Interdimensional jump\"]}]}";


The original implementation had both an strict and non-strict mode. I only translated the strict mode to AGS Script, I may need to support both for arrays to work here.

SMF spam blocked by CleanTalk