From 1932d6f02bf0bbd4216ef21b7c40119ca279ab7a Mon Sep 17 00:00:00 2001 From: Jackz Date: Tue, 16 May 2023 20:53:43 -0500 Subject: [PATCH] Add json include --- scripting/include/json.inc | 813 +++++++++++++++ scripting/include/json/array.inc | 955 ++++++++++++++++++ scripting/include/json/definitions.inc | 180 ++++ scripting/include/json/helpers/decode.inc | 573 +++++++++++ scripting/include/json/helpers/errors.inc | 64 ++ .../include/json/helpers/metastringmap.inc | 237 +++++ scripting/include/json/helpers/string.inc | 247 +++++ .../include/json/helpers/typedstringmap.inc | 222 ++++ scripting/include/json/helpers/unicode.inc | 185 ++++ scripting/include/json/object.inc | 724 +++++++++++++ scripting/l4d2_randomizer.sp | 332 ++++++ 11 files changed, 4532 insertions(+) create mode 100644 scripting/include/json.inc create mode 100644 scripting/include/json/array.inc create mode 100644 scripting/include/json/definitions.inc create mode 100644 scripting/include/json/helpers/decode.inc create mode 100644 scripting/include/json/helpers/errors.inc create mode 100644 scripting/include/json/helpers/metastringmap.inc create mode 100644 scripting/include/json/helpers/string.inc create mode 100644 scripting/include/json/helpers/typedstringmap.inc create mode 100644 scripting/include/json/helpers/unicode.inc create mode 100644 scripting/include/json/object.inc create mode 100644 scripting/l4d2_randomizer.sp diff --git a/scripting/include/json.inc b/scripting/include/json.inc new file mode 100644 index 0000000..a3ace6e --- /dev/null +++ b/scripting/include/json.inc @@ -0,0 +1,813 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program 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 + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _json_included + #endinput +#endif +#define _json_included + +#include +#include +#include +#include +#include +#include +#include + +/** + * Calculates the buffer size required to store an encoded JSON instance. + * + * @param obj Object to encode. + * @param options Bitwise combination of `JSON_ENCODE_*` options. + * @param depth The current depth of the encoder. + * @return The required buffer size. + */ +stock int json_encode_size(JSON_Object obj, int options = JSON_NONE, int depth = 0) +{ + bool pretty_print = (options & JSON_ENCODE_PRETTY) != 0; + + bool is_array = obj.IsArray; + + int size = 1; // for opening bracket + + // used in key iterator + int json_size = obj.Length; + JSON_Object child = null; + bool is_empty = true; + int str_length = 0; + + int key_length = 0; + for (int i = 0; i < json_size; i += 1) { + key_length = is_array ? JSON_INT_BUFFER_SIZE : obj.GetKeySize(i); + char[] key = new char[key_length]; + + if (is_array) { + IntToString(i, key, key_length); + } else { + obj.GetKey(i, key, key_length); + } + + // skip keys that are marked as hidden + if (obj.GetHidden(key)) { + continue; + } + + JSONCellType type = obj.GetType(key); + // skip keys of unknown type + if (type == JSON_Type_Invalid) { + continue; + } + + if (pretty_print) { + size += strlen(JSON_PP_NEWLINE); + size += (depth + 1) * strlen(JSON_PP_INDENT); + } + + if (! is_array) { + // add the size of the key and + 1 for : + size += json_cell_string_size(key) + 1; + + if (pretty_print) { + size += strlen(JSON_PP_AFTER_COLON); + } + } + + switch (type) { + case JSON_Type_String: { + str_length = obj.GetSize(key); + char[] value = new char[str_length]; + obj.GetString(key, value, str_length); + + size += json_cell_string_size(value); + } + case JSON_Type_Int: { + size += JSON_INT_BUFFER_SIZE; + } + #if SM_INT64_SUPPORTED + case JSON_Type_Int64: { + size += JSON_INT64_BUFFER_SIZE; + } + #endif + case JSON_Type_Float: { + size += JSON_FLOAT_BUFFER_SIZE; + } + case JSON_Type_Bool: { + size += JSON_BOOL_BUFFER_SIZE; + } + case JSON_Type_Object: { + child = obj.GetObject(key); + size += child != null ? json_encode_size(child, options, depth + 1) : JSON_NULL_BUFFER_SIZE; + } + } + + // increment for comma + size += 1; + + is_empty = false; + } + + if (! is_empty) { + // remove the final comma + size -= 1; + + if (pretty_print) { + size += strlen(JSON_PP_NEWLINE); + size += depth * strlen(JSON_PP_INDENT); + } + } + + size += 2; // closing bracket + NULL + + return size; +} + +/** + * Encodes a JSON instance into its string representation. + * + * @param obj Object to encode. + * @param output String buffer to store output. + * @param max_size Maximum size of string buffer. + * @param options Bitwise combination of `JSON_ENCODE_*` options. + * @param depth The current depth of the encoder. + */ +stock void json_encode( + JSON_Object obj, + char[] output, + int max_size, + int options = JSON_NONE, + int depth = 0 +) +{ + bool pretty_print = (options & JSON_ENCODE_PRETTY) != 0; + + bool is_array = obj.IsArray; + strcopy(output, max_size, is_array ? "[" : "{"); + + // used in key iterator + int json_size = obj.Length; + int builder_size = 0; + int str_length = 1; + JSON_Object child = null; + int cell_length = 0; + bool is_empty = true; + + int key_length = 0; + for (int i = 0; i < json_size; i += 1) { + key_length = is_array ? JSON_INT_BUFFER_SIZE : obj.GetKeySize(i); + char[] key = new char[key_length]; + + if (is_array) { + IntToString(i, key, key_length); + } else { + obj.GetKey(i, key, key_length); + } + + // skip keys that are marked as hidden + if (obj.GetHidden(key)) { + continue; + } + + JSONCellType type = obj.GetType(key); + // skip keys of unknown type + if (type == JSON_Type_Invalid) { + continue; + } + + // determine the length of the char[] needed to represent our cell data + cell_length = 0; + switch (type) { + case JSON_Type_String: { + str_length = obj.GetSize(key); + char[] value = new char[str_length]; + obj.GetString(key, value, str_length); + + cell_length = json_cell_string_size(value); + } + case JSON_Type_Int: { + cell_length = JSON_INT_BUFFER_SIZE; + } + #if SM_INT64_SUPPORTED + case JSON_Type_Int64: { + cell_length = JSON_INT64_BUFFER_SIZE; + } + #endif + case JSON_Type_Float: { + cell_length = JSON_FLOAT_BUFFER_SIZE; + } + case JSON_Type_Bool: { + cell_length = JSON_BOOL_BUFFER_SIZE; + } + case JSON_Type_Object: { + child = obj.GetObject(key); + cell_length = child != null ? max_size : JSON_NULL_BUFFER_SIZE; + } + } + + // fit the contents into the cell + char[] cell = new char[cell_length]; + switch (type) { + case JSON_Type_String: { + char[] value = new char[str_length]; + obj.GetString(key, value, str_length); + json_cell_string(value, cell, cell_length); + } + case JSON_Type_Int: { + int value = obj.GetInt(key); + IntToString(value, cell, cell_length); + } + #if SM_INT64_SUPPORTED + case JSON_Type_Int64: { + int value[2]; + obj.GetInt64(key, value); + Int64ToString(value, cell, cell_length); + } + #endif + case JSON_Type_Float: { + float value = obj.GetFloat(key); + FloatToString(value, cell, cell_length); + + // trim trailing 0s from float output up until decimal point + int last_char = strlen(cell) - 1; + while (cell[last_char] == '0' && cell[last_char - 1] != '.') { + cell[last_char--] = '\0'; + } + } + case JSON_Type_Bool: { + bool value = obj.GetBool(key); + strcopy(cell, cell_length, value ? "true" : "false"); + } + case JSON_Type_Object: { + if (child != null) { + json_encode(child, cell, cell_length, options, depth + 1); + } else { + strcopy(cell, cell_length, "null"); + } + } + } + + // make the builder fit our key:value + // use previously determined cell length and + 1 for , + builder_size = cell_length + 1; + if (! is_array) { + // get the length of the key and + 1 for : + builder_size += json_cell_string_size(key) + 1; + + if (pretty_print) { + builder_size += strlen(JSON_PP_AFTER_COLON); + } + } + + char[] builder = new char[builder_size]; + strcopy(builder, builder_size, ""); + + // add the key if we're working with an object + if (! is_array) { + json_cell_string(key, builder, builder_size); + StrCat(builder, builder_size, ":"); + + if (pretty_print) { + StrCat(builder, builder_size, JSON_PP_AFTER_COLON); + } + } + + // add the value and a trailing comma + StrCat(builder, builder_size, cell); + StrCat(builder, builder_size, ","); + + // prepare pretty printing then send builder to output afterwards + if (pretty_print) { + StrCat(output, max_size, JSON_PP_NEWLINE); + + for (int j = 0; j < depth + 1; j += 1) { + StrCat(output, max_size, JSON_PP_INDENT); + } + } + + StrCat(output, max_size, builder); + + is_empty = false; + } + + if (! is_empty) { + // remove the final comma + output[strlen(output) - 1] = '\0'; + + if (pretty_print) { + StrCat(output, max_size, JSON_PP_NEWLINE); + + for (int j = 0; j < depth; j += 1) { + StrCat(output, max_size, JSON_PP_INDENT); + } + } + } + + // append closing bracket + StrCat(output, max_size, is_array ? "]" : "}"); +} + +/** + * Decodes a JSON string into a JSON instance. + * + * @param buffer Buffer to decode. + * @param options Bitwise combination of `JSON_DECODE_*` options. + * @param pos Current position of the decoder as bytes + * offset into the buffer. + * @param depth Current nested depth of the decoder. + * @return JSON instance or null if decoding failed becase + * the buffer didn't contain valid JSON. + * @error If the buffer does not contain valid JSON, + * an error will be thrown. + */ +stock JSON_Object json_decode( + const char[] buffer, + int options = JSON_NONE, + int &pos = 0, + int depth = 0 +) +{ + int length = strlen(buffer); + // skip preceding whitespace + if (! json_skip_whitespace(buffer, length, pos)) { + json_set_last_error("buffer ended early at position %d", pos); + + return null; + } + + bool is_array = false; + JSON_Array arr = null; + JSON_Object obj = null; + if (buffer[pos] == '{') { + is_array = false; + obj = new JSON_Object(); + } else if (buffer[pos] == '[') { + is_array = true; + arr = new JSON_Array(); + } else { + json_set_last_error("no object or array found at position %d", pos); + + return null; + } + + bool allow_single_quotes = (options & JSON_DECODE_SINGLE_QUOTES) > 0; + + bool empty_checked = false; + + // while we haven't reached the end of our structure + while ( + (! is_array && buffer[pos] != '}') + || (is_array && buffer[pos] != ']') + ) { + // pos is either an opening structure or comma, so increment past it + pos += 1; + + // skip any whitespace preceding the element + if (! json_skip_whitespace(buffer, length, pos)) { + json_set_last_error("buffer ended early at position %d", pos); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + + // if we haven't checked for empty yet and we are at the end + // of an object or array, we can stop here (empty structure) + if (! empty_checked) { + if ( + (! is_array && buffer[pos] == '}') + || (is_array && buffer[pos] == ']') + ) { + break; + } + + empty_checked = true; + } + + int key_length = 1; + if (! is_array) { + // if dealing with an object, look for the key and determine length + if (! json_is_string(buffer[pos], allow_single_quotes)) { + json_set_last_error("expected key string at position %d", pos); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + + key_length = json_extract_string_size( + buffer, + length, + pos, + is_array + ); + } + + char[] key = new char[key_length]; + + if (! is_array) { + // extract the key from the buffer + json_extract_string(buffer, length, pos, key, key_length, is_array); + + // skip any whitespace following the key + if (! json_skip_whitespace(buffer, length, pos)) { + json_set_last_error("buffer ended early at position %d", pos); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + + // ensure that we find a colon + if (buffer[pos++] != ':') { + json_set_last_error( + "expected colon after key at position %d", + pos + ); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + + // skip any whitespace following the colon + if (! json_skip_whitespace(buffer, length, pos)) { + json_set_last_error("buffer ended early at position %d", pos); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + } + + int cell_length = 1; + JSONCellType cell_type = JSON_Type_Invalid; + if (buffer[pos] == '{' || buffer[pos] == '[') { + cell_type = JSON_Type_Object; + } else if (json_is_string(buffer[pos], allow_single_quotes)) { + cell_type = JSON_Type_String; + cell_length = json_extract_string_size( + buffer, + length, + pos, + is_array + ); + } else { + // in this particular instance, we use JSON_Type_Invalid to + // represent any type that isn't an object or string + cell_length = json_extract_until_end_size( + buffer, + length, + pos, + is_array + ); + } + + if (! is_array && obj.HasKey(key)) { + obj.Remove(key); + } + + char[] cell = new char[cell_length]; + switch (cell_type) { + case JSON_Type_Object: { + // if we are dealing with an object or array, decode recursively + JSON_Object value = json_decode( + buffer, + options, + pos, + depth + 1 + ); + + // decoding failed, error will be logged in json_decode + if (value == null) { + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + + if (is_array) { + arr.PushObject(value); + } else { + obj.SetObject(key, value); + } + } + case JSON_Type_String: { + // if we are dealing with a string, attempt to extract it + if (! json_extract_string( + buffer, + length, + pos, + cell, + cell_length, + is_array + )) { + json_set_last_error( + "couldn't extract string at position %d", + pos + ); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + + if (is_array) { + arr.PushString(cell); + } else { + obj.SetString(key, cell); + } + } + case JSON_Type_Invalid: { + if (! json_extract_until_end( + buffer, + length, + pos, + cell, + cell_length, + is_array + )) { + json_set_last_error( + "couldn't extract until end at position %d", + pos + ); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + + if (strlen(cell) == 0) { + json_set_last_error( + "empty cell encountered at position %d", + pos + ); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + + if (json_is_int(cell)) { + int value = StringToInt(cell); + #if SM_INT64_SUPPORTED + if (json_is_int64(cell, value)) { + int values[2]; + StringToInt64(cell, values); + + if (is_array) { + arr.PushInt64(values); + } else { + obj.SetInt64(key, values); + } + } else { + if (is_array) { + arr.PushInt(value); + } else { + obj.SetInt(key, value); + } + } + #else + if (is_array) { + arr.PushInt(value); + } else { + obj.SetInt(key, value); + } + #endif + } else if (json_is_float(cell)) { + float value = StringToFloat(cell); + if (is_array) { + arr.PushFloat(value); + } else { + obj.SetFloat(key, value); + } + } else if (StrEqual(cell, "true") || StrEqual(cell, "false")) { + bool value = StrEqual(cell, "true"); + if (is_array) { + arr.PushBool(value); + } else { + obj.SetBool(key, value); + } + } else if (StrEqual(cell, "null")) { + if (is_array) { + arr.PushObject(null); + } else { + obj.SetObject(key, null); + } + } else { + json_set_last_error( + "unknown type encountered at position %d: %s", + pos, + cell + ); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + } + } + + if (! json_skip_whitespace(buffer, length, pos)) { + json_set_last_error("buffer ended early at position %d", pos); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + } + + // skip remaining whitespace and ensure we're at the end of the buffer + pos += 1; + if (json_skip_whitespace(buffer, length, pos) && depth == 0) { + json_set_last_error( + "unexpected data after structure end at position %d", + pos + ); + json_cleanup_and_delete(obj); + json_cleanup_and_delete(arr); + + return null; + } + + return is_array ? view_as(arr) : obj; +} + +/** + * Encodes the object with the options provided and writes + * the output to the file at the path specified. + * + * @param obj Object to encode/write to file. + * @param path Path of file to write to. + * @param options Options to pass to `json_encode`. + * @return True on success, false otherwise. + */ +stock bool json_write_to_file( + JSON_Object obj, + const char[] path, + int options = JSON_NONE +) +{ + File f = OpenFile(path, "wb"); + if (f == null) { + return false; + } + + int size = json_encode_size(obj, options); + char[] buffer = new char[size]; + json_encode(obj, buffer, size, options); + + bool success = f.WriteString(buffer, false); + delete f; + + return success; +} + +/** + * Reads and decodes the contents of a JSON file. + * + * @param path Path of file to read from. + * @param options Options to pass to `json_decode`. + * @return The decoded object on success, null otherwise. + */ +stock JSON_Object json_read_from_file(const char[] path, int options = JSON_NONE) +{ + File f = OpenFile(path, "rb"); + if (f == null) { + return null; + } + + f.Seek(0, SEEK_END); + int size = f.Position + 1; + char[] buffer = new char[size]; + + f.Seek(0, SEEK_SET); + f.ReadString(buffer, size); + delete f; + + return json_decode(buffer, options); +} + +/** + * Creates a shallow copy of the specified object. + * + * @param obj Object to copy. + * @return A shallow copy of the specified object. + */ +stock JSON_Object json_copy_shallow(JSON_Object obj) +{ + bool isArray = obj.IsArray; + JSON_Object result = isArray + ? view_as(new JSON_Array()) + : new JSON_Object(); + + if (isArray) { + view_as(result).Concat(view_as(obj)); + } else { + result.Merge(obj); + } + + return result; +} + +/** + * Creates a deep copy of the specified object. + * + * @param obj Object to copy. + * @return A deep copy of the specified object. + */ +stock JSON_Object json_copy_deep(JSON_Object obj) +{ + JSON_Object result = json_copy_shallow(obj); + + int length = obj.Length; + int key_length = 0; + for (int i = 0; i < length; i += 1) { + key_length = obj.GetKeySize(i); + char[] key = new char[key_length]; + obj.GetKey(i, key, key_length); + + // only deep copy objects + JSONCellType type = obj.GetType(key); + if (type != JSON_Type_Object) { + continue; + } + + JSON_Object value = obj.GetObject(key); + result.SetObject(key, value != null ? json_copy_deep(value) : null); + } + + return result; +} + +/** + * Recursively cleans up the instance and any instances stored within. + * + * @param obj Object to clean up. + */ +stock void json_cleanup(JSON_Object obj) +{ + if (obj == null) { + return; + } + + int length = obj.Length; + int key_length = 0; + for (int i = 0; i < length; i += 1) { + key_length = obj.GetKeySize(i); + char[] key = new char[key_length]; + obj.GetKey(i, key, key_length); + + // only clean up objects + JSONCellType type = obj.GetType(key); + if (type != JSON_Type_Object) { + continue; + } + + JSON_Object value = obj.GetObject(key); + if (value != null) { + json_cleanup(value); + } + } + + obj.Super.Cleanup(); +} + +/** + * Cleans up an object and sets the passed variable to null. + * + * @param obj Object to clean up. + */ +stock void json_cleanup_and_delete(JSON_Object &obj) +{ + json_cleanup(obj); + obj = null; +} diff --git a/scripting/include/json/array.inc b/scripting/include/json/array.inc new file mode 100644 index 0000000..5f41162 --- /dev/null +++ b/scripting/include/json/array.inc @@ -0,0 +1,955 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program 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 + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _json_array_included + #endinput +#endif +#define _json_array_included + +#include +#include +#include +#include + +methodmap JSON_Array < JSON_Object +{ + /** + * @section Helpers + */ + + /** + * Views the instance as its superclass to access overridden methods. + */ + property JSON_Object Super + { + public get() + { + return view_as(this); + } + } + + /** + * The enforced type of the array. + */ + property JSONCellType Type + { + public get() + { + return view_as(this.Meta.GetOptionalValue( + JSON_ENFORCE_TYPE_KEY, + JSON_Type_Invalid + )); + } + + public set(JSONCellType value) + { + if (value == JSON_Type_Invalid) { + this.Meta.Remove(JSON_ENFORCE_TYPE_KEY); + } else { + this.Meta.SetValue(JSON_ENFORCE_TYPE_KEY, value); + } + } + } + + /** + * Checks whether the array accepts the type provided. + * + * @param type Type to check for enforcement. + * @return True if the type can be used, false otherwise. + */ + public bool CanUseType(JSONCellType type) + { + return this.Type == JSON_Type_Invalid || this.Type == type; + } + + /** + * Checks whether the object has an index. + * + * @param index Index to check existence of. + * @return True if the index exists, false otherwise. + */ + public bool HasKey(int index) + { + return index >= 0 && index < this.Length; + } + + /** + * @section Metadata Getters + */ + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * @internal + * + * @see MetaStringMap.GetMeta + */ + public any GetMeta(int index, JSONMetaInfo meta, any default_value) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return default_value; + } + + return this.Super.GetMeta(key, meta, default_value); + } + + /** + * Gets the cell type stored at an index. + * + * @param index Index to get value type for. + * @return Value type for index provided, + * or JSON_Type_Invalid if it does not exist. + */ + public JSONCellType GetType(int index) + { + return view_as( + this.GetMeta(index, JSON_Meta_Type, JSON_Type_Invalid) + ); + } + + /** + * Gets the length of the string stored at an index. + * + * @param index Index to get string length for. + * @return Length of string at index provided, + * or -1 if it is not a string/does not exist. + */ + public int GetSize(int index) + { + return view_as(this.GetMeta(index, JSON_Meta_Size, -1)); + } + + /** + * Gets whether the index should be hidden from encoding. + * + * @param index Index to get hidden state for. + * @return Whether or not the index should be hidden. + */ + public bool GetHidden(int index) + { + return view_as(this.GetMeta(index, JSON_Meta_Hidden, false)); + } + + /** + * @section Metadata Setters + */ + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * @internal + * + * @see JSON_Object.SetMeta + */ + public bool SetMeta(int index, JSONMetaInfo meta, any value) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.SetMeta(key, meta, value); + } + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * @internal + * + * @see JSON_Object.RemoveMeta + */ + public bool RemoveMeta(int index, JSONMetaInfo meta) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.RemoveMeta(key, meta); + } + + /** + * Sets whether the index should be hidden from encoding. + * + * @param index Index to set hidden state for. + * @param hidden Whether or not the index should be hidden. + * @return True on success, false otherwise. + */ + public bool SetHidden(int index, bool hidden) + { + return this.SetMeta(index, JSON_Meta_Hidden, hidden); + } + + /** + * @section Getters + */ + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see MetaStringMap.GetValue + */ + public bool GetValue(int index, any &value) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.GetValue(key, value); + } + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * @internal + * + * @see MetaStringMap.GetOptionalValue + */ + public any GetOptionalValue(int index, any default_value = -1) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.GetOptionalValue(key, default_value); + } + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see MetaStringMap.GetString + */ + public bool GetString(int index, char[] value, int max_size, int &size = 0) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.GetString(key, value, max_size, size); + } + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see MetaStringMap.GetInt + */ + public int GetInt(int index, int default_value = -1) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return default_value; + } + + return this.Super.GetInt(key, default_value); + } + + #if SM_INT64_SUPPORTED + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see JSON_Object.GetInt64 + */ + public bool GetInt64(int index, int value[2]) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.GetInt64(key, value); + } + #endif + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see MetaStringMap.GetFloat + */ + public float GetFloat(int index, float default_value = -1.0) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return default_value; + } + + return this.Super.GetFloat(key, default_value); + } + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see MetaStringMap.GetBool + */ + public bool GetBool(int index, bool default_value = false) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return default_value; + } + + return this.Super.GetBool(key, default_value); + } + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see MetaStringMap.GetObject + */ + public JSON_Object GetObject(int index, JSON_Object default_value = null) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return default_value; + } + + return this.Super.GetObject(key, default_value); + } + + /** + * @section Setters + */ + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see JSON_Object.SetString + */ + public bool SetString(int index, const char[] value) + { + if (! this.CanUseType(JSON_Type_String)) { + return false; + } + + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.SetString(key, value); + } + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see JSON_Object.SetInt + */ + public bool SetInt(int index, int value) + { + if (! this.CanUseType(JSON_Type_Int)) { + return false; + } + + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.SetInt(key, value); + } + + #if SM_INT64_SUPPORTED + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see JSON_Object.SetInt64 + */ + public bool SetInt64(int index, int value[2]) + { + if (! this.CanUseType(JSON_Type_Int64)) { + return false; + } + + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.SetInt64(key, value); + } + #endif + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see JSON_Object.SetFloat + */ + public bool SetFloat(int index, float value) + { + if (! this.CanUseType(JSON_Type_Float)) { + return false; + } + + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.SetFloat(key, value); + } + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see JSON_Object.SetBool + */ + public bool SetBool(int index, bool value) + { + if (! this.CanUseType(JSON_Type_Bool)) { + return false; + } + + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.SetBool(key, value); + } + + /** + * Converts index to a string ('key') and calls the relevant Super method. + * + * @see JSON_Object.SetObject + */ + public bool SetObject(int index, JSON_Object value) + { + if (! this.CanUseType(JSON_Type_Object)) { + return false; + } + + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + return this.Super.SetObject(key, value); + } + + /** + * @section Pushers + */ + + /** + * Pushes a string to the end of the array. + * + * @param value Value to push. + * @return The element's index on success, -1 otherwise. + */ + public int PushString(const char[] value) + { + int index = this.Length; + if (! this.SetString(index, value)) { + return -1; + } + + return index; + } + + /** + * Pushes an int to the end of the array. + * + * @param value Value to push. + * @return The element's index on success, -1 otherwise. + */ + public int PushInt(int value) + { + int index = this.Length; + if (! this.SetInt(index, value)) { + return -1; + } + + return index; + } + + #if SM_INT64_SUPPORTED + /** + * Pushes an int64 to the end of the array. + * + * @param value Value to push. + * @return The element's index on success, -1 otherwise. + */ + public int PushInt64(int value[2]) + { + int index = this.Length; + if (! this.SetInt64(index, value)) { + return -1; + } + + return index; + } + #endif + + /** + * Pushes a float to the end of the array. + * + * @param value Value to push. + * @return The element's index on success, -1 otherwise. + */ + public int PushFloat(float value) + { + int index = this.Length; + if (! this.SetFloat(index, value)) { + return -1; + } + + return index; + } + + /** + * Pushes a bool to the end of the array. + * + * @param value Value to push. + * @return The element's index on success, -1 otherwise. + */ + public int PushBool(bool value) + { + int index = this.Length; + if (! this.SetBool(index, value)) { + return -1; + } + + return index; + } + + /** + * Pushes a JSON object to the end of the array. + * + * @param value Value to push. + * @return The element's index on success, -1 otherwise. + */ + public int PushObject(JSON_Object value) + { + int index = this.Length; + if (! this.SetObject(index, value)) { + return -1; + } + + return index; + } + + /** + * @section Search Helpers + */ + + /** + * Finds the index of a value in the array. + * + * @param value Value to search for. + * @return The index of the value if it is found, -1 otherwise. + */ + public int IndexOf(any value) + { + any current; + int length = this.Length; + for (int i = 0; i < length; i += 1) { + if (this.GetValue(i, current) && value == current) { + return i; + } + } + + return -1; + } + + /** + * Finds the index of a string in the array. + * + * @param value String to search for. + * @return The index of the string if it is found, -1 otherwise. + */ + public int IndexOfString(const char[] value) + { + int length = this.Length; + for (int i = 0; i < length; i += 1) { + if (this.GetType(i) != JSON_Type_String) { + continue; + } + + int current_size = this.GetSize(i); + char[] current = new char[current_size]; + this.GetString(i, current, current_size); + if (StrEqual(value, current)) { + return i; + } + } + + return -1; + } + + /** + * Determines whether the array contains a value. + * + * @param value Value to search for. + * @return True if the value is found, false otherwise. + */ + public bool Contains(any value) + { + return this.IndexOf(value) != -1; + } + + /** + * Determines whether the array contains a string. + * + * @param value String to search for. + * @return True if the string is found, false otherwise. + */ + public bool ContainsString(const char[] value) + { + return this.IndexOfString(value) != -1; + } + + /** + * @section Misc + */ + + /** + * Removes an index and its related meta-keys from the array, + * and shifts down all following element indices. + * + * @param key Key to remove. + * @return True on success, false if the value was never set. + */ + public bool Remove(int index) + { + char key[JSON_INT_BUFFER_SIZE]; + if (! this.GetKey(index, key, sizeof(key))) { + return false; + } + + int length = this.Length; + + // remove existing value at index + if (! this.Super.Remove(key)) { + return false; + } + + // shift all following elements down + char current_key[JSON_INT_BUFFER_SIZE]; + for (int oldIndex = index + 1; oldIndex < length; oldIndex += 1) { + int newIndex = oldIndex - 1; + JSONCellType type = this.GetType(oldIndex); + + switch (type) { + case JSON_Type_String: { + int str_length = this.GetSize(oldIndex); + char[] str_value = new char[str_length]; + + this.GetString(oldIndex, str_value, str_length); + this.SetString(newIndex, str_value); + } + case JSON_Type_Int: { + this.SetInt(newIndex, this.GetInt(oldIndex)); + } + #if SM_INT64_SUPPORTED + case JSON_Type_Int64: { + int value[2]; + this.GetInt64(oldIndex, value); + this.SetInt64(newIndex, value); + } + #endif + case JSON_Type_Float: { + this.SetFloat(newIndex, this.GetFloat(oldIndex)); + } + case JSON_Type_Bool: { + this.SetBool(newIndex, this.GetBool(oldIndex)); + } + case JSON_Type_Object: { + this.SetObject(newIndex, this.GetObject(oldIndex)); + } + } + + this.SetHidden(newIndex, this.GetHidden(oldIndex)); + + if (this.GetKey( + oldIndex, + current_key, + sizeof(current_key) + )) { + this.Super.Remove(current_key); + } + } + + return true; + } + + /** + * Concatenates the entries from the specified array + * on to the end of this array. + * + * @param from Array to concat entries from. + * @return True on success, false otherwise. + * @error If the object being merged is an object or the + * arrays being merged don't have the same strict + * type set, an error will be thrown. + */ + public bool Concat(JSON_Array from) + { + if (! this.IsArray || ! from.IsArray) { + json_set_last_error("attempted to concat using object(s)"); + + return false; + } + + if (this.Type != from.Type) { + json_set_last_error( + "attempted to concat arrays with mismatched strict types" + ); + + return false; + } + + int current_length = this.Length; + int json_size = from.Length; + for (int i = 0; i < json_size; i += 1) { + JSONCellType type = from.GetType(i); + // skip keys of unknown type + if (type == JSON_Type_Invalid) { + continue; + } + + // push value onto array + switch (type) { + case JSON_Type_String: { + int length = from.GetSize(i); + char[] value = new char[length]; + from.GetString(i, value, length); + + this.PushString(value); + } + case JSON_Type_Int: { + this.PushInt(from.GetInt(i)); + } + #if SM_INT64_SUPPORTED + case JSON_Type_Int64: { + int value[2]; + from.GetInt64(i, value); + this.PushInt64(value); + } + #endif + case JSON_Type_Float: { + this.PushFloat(from.GetFloat(i)); + } + case JSON_Type_Bool: { + this.PushBool(from.GetBool(i)); + } + case JSON_Type_Object: { + this.PushObject(from.GetObject(i)); + } + } + + this.SetHidden(current_length + i, from.GetHidden(i)); + } + + return true; + } + + /** + * Typed Helpers + */ + + /** + * The length of the longest string in the array. + */ + property int MaxStringLength + { + public get() + { + int max = -1; + int current = -1; + int length = this.Length; + for (int i = 0; i < length; i += 1) { + if (this.GetType(i) != JSON_Type_String) { + continue; + } + + current = this.GetSize(i); + if (current > max) { + max = current; + } + } + + return max; + } + } + + /** + * Sets the array to enforce a specific type. + * This will fail if there are any existing elements + * in the array which are not of the same type. + * + * @param type Type to enforce. + * @return True if the type was enforced successfully, false otherwise. + */ + public bool EnforceType(JSONCellType type) + { + if (type == JSON_Type_Invalid) { + this.Type = type; + + return true; + } + + int length = this.Length; + for (int i = 0; i < length; i += 1) { + if (this.GetType(i) != type) { + return false; + } + } + + this.Type = type; + + return true; + } + + /** + * Imports a native array's values into the instance. + * + * @param type Type of native values. + * @param values Array of values. + * @param size Size of array. + * @return True on success, false otherwise. + */ + public bool ImportValues(JSONCellType type, any[] values, int size) + { + bool success = true; + for (int i = 0; i < size; i += 1) { + switch (type) { + case JSON_Type_Int: { + success = success && this.PushInt(values[i]) > -1; + } + case JSON_Type_Float: { + success = success && this.PushFloat(values[i]) > -1; + } + case JSON_Type_Bool: { + success = success && this.PushBool(values[i]) > -1; + } + case JSON_Type_Object: { + success = success && this.PushObject(values[i]) > -1; + } + } + } + + return success; + } + + /** + * Imports a native array's strings into the instance. + * + * @param strings Array of strings. + * @param size Size of array. + * @return True on success, false otherwise. + */ + public bool ImportStrings(const char[][] strings, int size) + { + bool success = true; + for (int i = 0; i < size; i += 1) { + success = success && this.PushString(strings[i]) > -1; + } + + return success; + } + + /** + * Exports the instance's values into a native array. + * + * @param values Array to export to. + * @param max_size Maximum size of array. + */ + public void ExportValues(any[] values, int max_size) + { + int length = this.Length; + if (length < max_size) { + max_size = length; + } + + for (int i = 0; i < max_size; i += 1) { + this.GetValue(i, values[i]); + } + } + + /** + * Exports the instance's strings into a native array. + * + * @param values Array to export to. + * @param max_size Maximum size of array. + * @param max_string_size Maximum size of array elements. + */ + public void ExportStrings( + char[][] values, + int max_size, + int max_string_size + ) { + int length = this.Length; + if (length < max_size) { + max_size = length; + } + + for (int i = 0; i < max_size; i += 1) { + this.GetString(i, values[i], max_string_size); + } + } + + /** + * json.inc Aliases + */ + + /** @see JSON_Object.ShallowCopy */ + public JSON_Array ShallowCopy() + { + return view_as(this.Super.ShallowCopy()); + } + + /** @see JSON_Object.DeepCopy */ + public JSON_Array DeepCopy() + { + return view_as(this.Super.DeepCopy()); + } + + /** + * @section Constructor + */ + + /** + * Creates a new JSON_Array. + * + * @param type The type to enforce for this array, or + * JSON_Type_Invalid for no enforced type. + * @return A new JSON_Array. + */ + public JSON_Array(JSONCellType type = JSON_Type_Invalid) + { + JSON_Array self = view_as(new JSON_Object()); + self.Meta.SetBool(JSON_ARRAY_KEY, true); + self.EnforceType(type); + + return self; + } +}; diff --git a/scripting/include/json/definitions.inc b/scripting/include/json/definitions.inc new file mode 100644 index 0000000..eba7b9c --- /dev/null +++ b/scripting/include/json/definitions.inc @@ -0,0 +1,180 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program 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 + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _json_definitions_included + #endinput +#endif +#define _json_definitions_included + +#include +#include + +#define SM_INT64_SUPPORTED SOURCEMOD_V_MAJOR >= 1 \ + && SOURCEMOD_V_MINOR >= 11 \ + && SOURCEMOD_V_REV >= 6861 + +/** + * @section Settings + */ + +/** Used when no options are desired. */ +#define JSON_NONE 0 + +/** + * @section json_encode settings + */ + +/** Should encoded output be pretty printed? */ +#define JSON_ENCODE_PRETTY 1 << 0 + +/** + * @section json_decode settings + */ + +/** Should single quote wrapped strings be accepted during decoding? */ +#define JSON_DECODE_SINGLE_QUOTES 1 << 0 + +/** + * @section json_merge settings + */ + +/** During merge, should existing keys be replaced if they exist in both objects? */ +#define JSON_MERGE_REPLACE 1 << 0 + +/** During merge, should existing objects be cleaned up if they exist in + * both objects? (only applies when JSON_MERGE_REPLACE is also set) */ +#define JSON_MERGE_CLEANUP 1 << 1 + +/** + * @section Pretty Print Constants + * + * Used to determine how pretty printed JSON should be formatted when encoded. + * You can modify these if you prefer your JSON formatted differently. + */ + +char JSON_PP_AFTER_COLON[32] = " "; +char JSON_PP_INDENT[32] = " "; +char JSON_PP_NEWLINE[32] = "\n"; + +/** + * @section Buffer Size Constants + */ + +/** The longest representable integer ("-2147483648") + NULL terminator */ +#define JSON_INT_BUFFER_SIZE 12 + +#if SM_INT64_SUPPORTED +/** The longest representable int64 ("-9223372036854775808") + NULL terminator */ +#define JSON_INT64_BUFFER_SIZE 21 +#endif + +/** You may need to change this if you are working with large floats. */ +#define JSON_FLOAT_BUFFER_SIZE 32 + +/** "true"|"false" + NULL terminator */ +#define JSON_BOOL_BUFFER_SIZE 6 + +/** "null" + NULL terminator */ +#define JSON_NULL_BUFFER_SIZE 5 + +/** + * @section Array/Object Constants + */ + +#define JSON_ARRAY_KEY "is_array" +#define JSON_ENFORCE_TYPE_KEY "enforce_type" + +/** + * Types of cells within a JSON object + */ +enum JSONCellType { + JSON_Type_Invalid = -1, + JSON_Type_String = 0, + JSON_Type_Int, + #if SM_INT64_SUPPORTED + JSON_Type_Int64, + #endif + JSON_Type_Float, + JSON_Type_Bool, + JSON_Type_Object +}; + +/** + * Types of metadata a JSON element can have + */ +enum JSONMetaInfo { + JSON_Meta_Type = 0, + JSON_Meta_Size, + JSON_Meta_Hidden, + JSON_Meta_Index +} + +/** + * An array of all possible meta info values. + */ +JSONMetaInfo JSON_ALL_METADATA[4] = { + JSON_Meta_Type, JSON_Meta_Size, JSON_Meta_Hidden, JSON_Meta_Index +}; + +/** + * Calculates the length required to store a meta key + * for a specified key/metainfo combination. + * @internal + * + * @param key + * @return The length required to store the meta key. + */ +stock int json_meta_key_length(const char[] key) +{ + // %s:%d + return strlen(key) + 1 + JSON_INT_BUFFER_SIZE; +} + +/** + * Formats the key/metainfo combination into a buffer. + * @internal + * + * @param output String buffer to store output. + * @param max_size Maximum size of string buffer. + * @param key Key to generate metakey for. + * @param meta Meta information to generate metakey for. + */ +stock void json_format_meta_key( + char[] output, + int max_size, + const char[] key, + JSONMetaInfo meta +) +{ + FormatEx(output, max_size, "k|%s:%d", key, view_as(meta)); +} diff --git a/scripting/include/json/helpers/decode.inc b/scripting/include/json/helpers/decode.inc new file mode 100644 index 0000000..c550de8 --- /dev/null +++ b/scripting/include/json/helpers/decode.inc @@ -0,0 +1,573 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program 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 + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _json_helpers_decode_included + #endinput +#endif +#define _json_helpers_decode_included + +#include +#include + +/** + * @section Determine Buffer Contents + */ + +/** + * Checks whether the character at the beginning of the buffer is whitespace. + * + * @param buffer String buffer of data. + * @return True if the first character in the buffer + * is whitespace, false otherwise. + */ +stock bool json_is_whitespace(const char[] buffer) +{ + return buffer[0] == ' ' + || buffer[0] == '\t' + || buffer[0] == '\r' + || buffer[0] == '\n'; +} + +/** + * Checks whether the character at the beginning + * of the buffer is the start of a string. + * + * @param buffer String buffer of data. + * @param allow_single_quotes Should strings using single quotes be accepted? + * @return True if the first character in the buffer + * is the start of a string, false otherwise. + */ +stock bool json_is_string(const char[] buffer, bool allow_single_quotes = false) +{ + return buffer[0] == '"' || (allow_single_quotes && buffer[0] == '\''); +} + +/** + * Checks whether the buffer provided contains an int. + * + * @param buffer String buffer of data. + * @return True if buffer contains an int, false otherwise. + */ +stock bool json_is_int(const char[] buffer) +{ + int length = strlen(buffer); + if (length == 0) { + return false; + } + + bool starts_with_zero = false; + bool has_digit_gt_zero = false; + + for (int i = 0; i < length; i += 1) { + // allow minus as first character only + if (i == 0 && buffer[i] == '-') { + continue; + } + + if (! IsCharNumeric(buffer[i])) { + return false; + } + + if (buffer[i] == '0') { + if (starts_with_zero) { + // detect repeating leading zeros + return false; + } else if (! has_digit_gt_zero) { + starts_with_zero = true; + } + } else if (starts_with_zero) { + // detect numbers with leading zero + return false; + } else { + has_digit_gt_zero = true; + } + } + + return true; +} + +#if SM_INT64_SUPPORTED +/** + * Checks whether the buffer provided contains an int64, assuming it has + * already been validated as an int and attempted to convert to an int32. + * + * @param buffer String buffer of data. + * @param value Converted int value to compare with. + * @return True if buffer contains an int64, false otherwise. + */ +stock bool json_is_int64(const char[] buffer, int value) +{ + if ( + (value == 0 && ! StrEqual(buffer, "0")) + || (value == -1 && ! StrEqual(buffer, "-1")) + ) { + // failed to produce output of validated int, must be 64bit + return true; + } + + if (buffer[0] != '-' && value < 0) { + // 32-bit unsigned positive int which is incorrectly + // interpreted as a negative signed int by sourcepawn + return true; + } + + if (buffer[0] == '-' && value > 0) { + return true; + } + + return false; +} +#endif + +/** + * Checks whether the buffer provided contains a float. + * + * @param buffer String buffer of data. + * @return True if buffer contains a float, false otherwise. + */ +stock bool json_is_float(const char[] buffer) +{ + int length = strlen(buffer); + if (length == 0) { + return false; + } + + bool starts_with_zero = false; + bool has_digit_gt_zero = false; + bool after_decimal = false; + bool has_digit_after_decimal = false; + bool after_exponent = false; + bool has_digit_after_exponent = false; + + for (int i = 0; i < length; i += 1) { + // allow minus as first character only + if (i == 0 && buffer[i] == '-') { + continue; + } + + // if we haven't encountered a decimal or exponent yet + if (! after_decimal && ! after_exponent) { + if (buffer[i] == '.') { + // if we encounter a decimal before any digits + if (! starts_with_zero && ! has_digit_gt_zero) { + return false; + } + + after_decimal = true; + } else if (buffer[i] == 'e' || buffer[i] == 'E') { + // if we encounter an exponent before any digits + if (! starts_with_zero && ! has_digit_gt_zero) { + return false; + } + + after_exponent = true; + } else if (IsCharNumeric(buffer[i])) { + if (buffer[i] == '0') { + if (starts_with_zero) { + // detect repeating leading zeros + return false; + } else if (! has_digit_gt_zero) { + starts_with_zero = true; + } + } else { + if (starts_with_zero) { + // detect numbers with leading zero + return false; + } + + has_digit_gt_zero = true; + } + } else { + return false; + } + } else if (after_decimal && ! after_exponent) { + // after decimal has been encountered, allow any numerics + if (IsCharNumeric(buffer[i])) { + has_digit_after_decimal = true; + } else if (buffer[i] == 'e' || buffer[i] == 'E') { + if (! has_digit_after_decimal) { + // detect exponents directly after decimal + return false; + } + + after_exponent = true; + } else { + return false; + } + } else if (after_exponent) { + if ( + (buffer[i] == '+' || buffer[i] == '-') + && (buffer[i - 1] == 'e' || buffer[i - 1] == 'E') + ) { + // allow + or - directly after exponent + continue; + } else if (IsCharNumeric(buffer[i])) { + has_digit_after_exponent = true; + } else { + return false; + } + } + } + + // if we have a decimal, there should be digit(s) after it + if (after_decimal && ! has_digit_after_decimal) { + return false; + } + + // if we have an exponent, there should be digit(s) after it + if (after_exponent && ! has_digit_after_exponent) { + return false; + } + + // we should have reached an exponent, decimal, or both + // otherwise this number can be handled by the int parser + return after_decimal || after_exponent; +} + +/** + * Checks whether the character at the beginning of the buffer + * is considered a valid 'end point' for an element, + * such as a colon (indicating the end of a key), + * a comma (indicating the end of an element), + * or the end of an object or array. + * + * @param buffer String buffer of data. + * @param is_array Whether the decoder is processing an array. + * @return True if the first character in the buffer + * is a valid element end point, false otherwise. + */ +stock bool json_is_at_end(const char[] buffer, bool is_array) +{ + return buffer[0] == ',' + || (! is_array && (buffer[0] == ':' || buffer[0] == '}')) + || (is_array && buffer[0] == ']'); +} + +/** + * @section Extract Contents from Buffer + */ + +/** + * Moves the position until it reaches a non-whitespace + * character or the end of the buffer's maximum size. + * + * @param buffer String buffer of data. + * @param max_size Maximum size of string buffer. + * @param pos Position to increment. + * @return True if pos has not reached the end + * of the buffer, false otherwise. + */ +stock bool json_skip_whitespace(const char[] buffer, int max_size, int &pos) +{ + while (json_is_whitespace(buffer[pos]) && pos < max_size) { + pos += 1; + } + + return pos < max_size; +} + +/** + * Calculates the size of the buffer required to store the next + * JSON cell stored in the provided buffer at the provided position. + * This function is quite forgiving of malformed input and shouldn't be + * relied upon as proof that the input is valid. + * + * @param buffer String buffer of data. + * @param max_size Maximum size of string buffer. + * @param pos Position to increment. + * @param is_array Whether the decoder is processing an array. + * @return The size of the buffer required to store the cell. + */ +stock int json_extract_until_end_size( + const char[] buffer, + int max_size, + int pos, + bool is_array +) +{ + int length = 1; // for NULL terminator + + // while we haven't hit whitespace, an end point or the end of the buffer + while ( + ! json_is_whitespace(buffer[pos]) + && ! json_is_at_end(buffer[pos], is_array) + && pos < max_size + ) { + pos += 1; + length += 1; + } + + return length; +} + +/** + * Extracts a JSON cell from the buffer until a valid end point is reached. + * + * @param buffer String buffer of data. + * @param max_size Maximum size of string buffer. + * @param pos Position to increment. + * @param output String buffer to store output. + * @param output_max_size Maximum size of output string buffer. + * @param is_array Whether the decoder is processing an array. + * @return True if pos has not reached the end + * of the buffer, false otherwise. + */ +stock bool json_extract_until_end( + const char[] buffer, + int max_size, + int &pos, + char[] output, + int output_max_size, + bool is_array +) { + strcopy(output, output_max_size, ""); + + // set start to position of first character in cell + int start = pos; + + // while we haven't hit whitespace, an end point or the end of the buffer + while ( + ! json_is_whitespace(buffer[pos]) + && ! json_is_at_end(buffer[pos], is_array) + && pos < max_size + ) { + pos += 1; + } + + // set end to the current position + int end = pos; + + // skip any following whitespace + json_skip_whitespace(buffer, max_size, pos); + + // if we aren't at a valid endpoint, extraction has failed + if (! json_is_at_end(buffer[pos], is_array)) { + return false; + } + + // copy only from start with length end - start + NULL terminator + strcopy(output, end - start + 1, buffer[start]); + + return pos < max_size; +} + +/** + * Calculates the size of the buffer required to store the next + * JSON string stored in the provided buffer at the provided position. + * This function is quite forgiving of malformed input and shouldn't be + * relied upon as proof that the input is valid. + * + * @param buffer String buffer of data. + * @param max_size Maximum size of string buffer. + * @param pos Position to increment. + * @param is_array Whether the decoder is processing an array. + * @return The size of the buffer required to store the string. + */ +stock int json_extract_string_size( + const char[] buffer, + int max_size, + int pos, + bool is_array +) +{ + int length = 1; // for NULL terminator + + // store initial quote + char quote = buffer[pos]; + + // increment past opening quote + pos += 1; + + // while we haven't hit the end of the buffer + int continuous_backslashes = 0; + while (pos < max_size) { + if (buffer[pos] == quote) { + // if we have an even number of preceding backslashes, + // the quote isn't escaped so this is the end of the string + if (continuous_backslashes % 2 == 0) { + break; + } + } + + if (buffer[pos] == '\\') { + continuous_backslashes += 1; + } else { + continuous_backslashes = 0; + } + + // pass over the character as it is part of the string + pos += 1; + length += 1; + } + + return length; +} + +/** + * Extracts a JSON string from the buffer until a valid end point is reached. + * + * @param buffer String buffer of data. + * @param max_size Maximum size of string buffer. + * @param pos Position to increment. + * @param output String buffer to store output. + * @param output_max_size Maximum size of output string buffer. + * @param is_array Whether the decoder is processing an array. + * @return True if pos has not reached the end + * of the buffer, false otherwise. + */ +stock bool json_extract_string( + const char[] buffer, + int max_size, + int &pos, + char[] output, + int output_max_size, + bool is_array +) { + strcopy(output, output_max_size, ""); + + // store initial quote + char quote = buffer[pos]; + + // increment past opening quote + pos += 1; + + // set start to position of first character in string + int start = pos; + + // while we haven't hit the end of the buffer + int continuous_backslashes = 0; + while (pos < max_size) { + // check for unescaped control characters + if ( + buffer[pos] == '\b' + || buffer[pos] == '\f' + || buffer[pos] == '\n' + || buffer[pos] == '\r' + || buffer[pos] == '\t' + ) { + return false; + } + + if (buffer[pos] == quote) { + // if we have an even number of preceding backslashes, + // the quote isn't escaped so this is the end of the string + if (continuous_backslashes % 2 == 0) { + break; + } + } + + if (buffer[pos] == '\\') { + continuous_backslashes += 1; + } else { + if (continuous_backslashes % 2 != 0) { + if (buffer[pos] == 'u') { + if (pos + 4 >= max_size) { + // less than 4 characters left in the buffer + return false; + } + + // ensure next 4 chars are hex and not a high surrogate + for (int i = 0; i < 4; i += 1) { + pos += 1; + + if (! json_char_is_hex(buffer[pos])) { + return false; + } + + if ( + i == 1 + && buffer[pos - 1] == 'D' + && buffer[pos] >= '8' + ) { + // detected a high surrogate value + return false; + } + } + + // jump back to the last hex char so it is safe to continue + pos -= 1; + } else if ( + buffer[pos] != '"' + && buffer[pos] != '\'' + && buffer[pos] != '/' + && buffer[pos] != 'b' + && buffer[pos] != 'f' + && buffer[pos] != 'n' + && buffer[pos] != 'r' + && buffer[pos] != 't' + ) { + // illegal escape detected + return false; + } + } + + continuous_backslashes = 0; + } + + // pass over the character as it is part of the string + pos += 1; + } + + // set end to the current position + int end = pos; + + // increment past closing quote + pos += 1; + + // skip trailing whitespace + if (! json_skip_whitespace(buffer, max_size, pos)) { + return false; + } + + // if we haven't reached an ending character at the end of the cell, + // there is likely junk data not encapsulated by a string + if (! json_is_at_end(buffer[pos], is_array)) { + return false; + } + + // copy only from start with length end - start + NULL terminator + int length = end - start + 1; + strcopy( + output, + length > output_max_size ? output_max_size : length, + buffer[start] + ); + + if (quote == '\'') { + ReplaceString(output, max_size, "\\'", "'"); + } + + json_unescape_string(output, max_size); + + return pos < max_size; +} diff --git a/scripting/include/json/helpers/errors.inc b/scripting/include/json/helpers/errors.inc new file mode 100644 index 0000000..7c3241c --- /dev/null +++ b/scripting/include/json/helpers/errors.inc @@ -0,0 +1,64 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program 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 + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _json_helpers_errors_included + #endinput +#endif +#define _json_helpers_errors_included + +static char g_jsonLastError[1024] = ""; + +/** + * Stores the error provided as the 'last error' for later access. + * @internal + * + * @param error Error to store. + * @param ... Further arguments to pass to message formatter. + */ +stock void json_set_last_error(const char[] error, any ...) +{ + VFormat(g_jsonLastError, sizeof(g_jsonLastError), error, 2); +} + +/** + * Retrieves the last error encountered and stores it in the buffer provided. + * + * @param buffer String buffer. + * @param max_size Maximum size of string buffer. + * @return True if the error was copied successfuly, + * false otherwise. + */ +stock bool json_get_last_error(char[] buffer, int max_size) +{ + return strcopy(buffer, max_size, g_jsonLastError) > 0; +} diff --git a/scripting/include/json/helpers/metastringmap.inc b/scripting/include/json/helpers/metastringmap.inc new file mode 100644 index 0000000..b89134a --- /dev/null +++ b/scripting/include/json/helpers/metastringmap.inc @@ -0,0 +1,237 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program 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 + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _metastringmap_included + #endinput +#endif +#define _metastringmap_included + +#include +#include + +/** + * A TypedStringMap which contains a nested `Data` `TypedStringMap` property. + * Standard methods and properties have been overridden to run against `Data`, + * but you can access the parent methods/properties using the `Meta` property. + */ +methodmap MetaStringMap < TypedStringMap +{ + /** + * @section Properties + */ + + /** + * Views the instance as its superclass to access overridden methods. + */ + property TypedStringMap Meta + { + public get() + { + return view_as(this); + } + } + + /** + * Gets the nested stringmap where data is stored. + */ + property TypedStringMap Data + { + public get() + { + return view_as(this.Meta.GetHandle("data")); + } + + public set(TypedStringMap value) + { + this.Meta.SetHandle("data", value); + } + } + + /** @see TypedStringMap.Length */ + property int Length { + public get() + { + return this.Data.Length; + } + } + + /** + * @section Getters + */ + + /** @see StringMap.GetValue */ + public bool GetValue(const char[] key, any &value) + { + return this.Data.GetValue(key, value); + } + + /** + * @see TypedStringMap.GetOptionalValue + * @internal + */ + public any GetOptionalValue(const char[] key, any default_value = -1) + { + return this.Data.GetOptionalValue(key, default_value); + } + + /** @see StringMap.GetString */ + public bool GetString( + const char[] key, + char[] value, + int max_size, + int &size = 0 + ) { + return this.Data.GetString(key, value, max_size, size); + } + + /** @see TypedStringMap.GetInt */ + public int GetInt(const char[] key, int default_value = -1) + { + return this.Data.GetInt(key, default_value); + } + + /** @see TypedStringMap.GetFloat */ + public float GetFloat(const char[] key, float default_value = -1.0) + { + return this.Data.GetFloat(key, default_value); + } + + /** @see TypedStringMap.GetBool */ + public bool GetBool(const char[] key, bool default_value = false) + { + return this.Data.GetBool(key, default_value); + } + + /** @see TypedStringMap.GetHandle */ + public Handle GetHandle(const char[] key, Handle default_value = null) + { + return this.Data.GetHandle(key, default_value); + } + + /** + * @section Setters + */ + + /** @see StringMap.SetValue */ + public bool SetValue(const char[] key, any value) + { + return this.Data.SetValue(key, value); + } + + /** @see StringMap.SetString */ + public bool SetString(const char[] key, const char[] value) + { + return this.Data.SetString(key, value); + } + + /** @see TypedStringMap.SetInt */ + public bool SetInt(const char[] key, int value) + { + return this.Data.SetInt(key, value); + } + + /** @see TypedStringMap.SetFloat */ + public bool SetFloat(const char[] key, float value) + { + return this.Data.SetFloat(key, value); + } + + /** @see TypedStringMap.SetBool */ + public bool SetBool(const char[] key, bool value) + { + return this.Data.SetBool(key, value); + } + + /** @see TypedStringMap.SetHandle */ + public bool SetHandle(const char[] key, Handle value) + { + return this.Data.SetHandle(key, value); + } + + /** @see StringMap.Remove */ + public bool Remove(const char[] key) + { + return this.Data.Remove(key); + } + + /** + * @section Misc + */ + + /** @see TypedStringMap.HasKey */ + public bool HasKey(const char[] key) + { + return this.Data.HasKey(key); + } + + /** @see StringMap.Clear */ + public void Clear() + { + TypedStringMap data = this.Data; + data.Clear(); + this.Meta.Clear(); + this.Data = data; + } + + /** + * Deletes the instance's data StringMap as well as the instance itself. + */ + public void Cleanup() + { + delete this.Data; + delete this; + } + + /** @see StringMap.Snapshot */ + public StringMapSnapshot Snapshot() + { + return this.Data.Snapshot(); + } + + /** + * @section Constructor + */ + + /** + * Creates a new MetaStringMap. + * + * @return A new MetaStringMap. + */ + public MetaStringMap() + { + MetaStringMap self = view_as(new TypedStringMap()); + self.Data = new TypedStringMap(); + + return self; + } +}; diff --git a/scripting/include/json/helpers/string.inc b/scripting/include/json/helpers/string.inc new file mode 100644 index 0000000..e01400c --- /dev/null +++ b/scripting/include/json/helpers/string.inc @@ -0,0 +1,247 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program 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 + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _json_helpers_string_included + #endinput +#endif +#define _json_helpers_string_included + +#include + +/** + * Mapping characters to their escaped form. + */ +char JSON_STRING_NORMAL[][] = { + "\\", "\"", "/", "\b", "\f", "\n", "\r", "\t" +}; +char JSON_STRING_ESCAPED[][] = { + "\\\\", "\\\"", "\\/", "\\b", "\\f", "\\n", "\\r", "\\t" +}; + +/** + * Escapes a string in-place in a buffer. + * + * @param buffer String buffer. + * @param max_size Maximum size of string buffer. + */ +stock void json_escape_string(char[] buffer, int max_size) +{ + for (int i = 0; i < sizeof(JSON_STRING_NORMAL); i += 1) { + ReplaceString( + buffer, + max_size, + JSON_STRING_NORMAL[i], + JSON_STRING_ESCAPED[i] + ); + } + + int length = strlen(buffer) + 1; + for (int pos = 0; pos < length && pos < max_size; pos += 1) { + if (buffer[pos] < 0x80) { + // skip standard ascii values + continue; + } + + // consume the ascii bytes of the next utf8 character + int ascii_size; + int utf8 = json_ascii_to_utf8(buffer[pos], length - pos, ascii_size); + if (ascii_size <= 0) { + continue; + } + + // convert the utf8 value to escaped format + char escaped[7]; + FormatEx(escaped, sizeof(escaped), "\\u%04x", utf8); + + // duplicate the consumed byte array + ascii_size += 1; + char[] ascii = new char[ascii_size]; + for (int i = 0; i < ascii_size; i += 1) { + ascii[i] = buffer[pos + i]; + } + ascii[ascii_size - 1] = '\0'; + + // replace bytes with the escaped value + int replacements = ReplaceString(buffer, max_size, ascii, escaped); + + // calculate new string length based on replacements made + length -= replacements * ascii_size - 1; + length += replacements * sizeof(escaped) - 1; + + // skip to the last of the bytes we just replaced + pos += sizeof(escaped) - 2; + } +} + +/** + * Unescapes a string in-place in a buffer. + * + * @param buffer String buffer. + * @param max_size Maximum size of string buffer. + */ +stock void json_unescape_string(char[] buffer, int max_size) +{ + int length = strlen(buffer) + 1; + int continuous_backslashes = 0; + for (int pos = 0; pos < length && pos < max_size; pos += 1) { + if (buffer[pos] == '\\') { + continuous_backslashes += 1; + } else { + if (continuous_backslashes % 2 != 0 && buffer[pos] == 'u') { + // consume the entire escape starting at backslash + pos -= 1; + char escaped[7]; + for (int i = 0; i < 6; i += 1) { + escaped[i] = buffer[pos + i]; + } + escaped[sizeof(escaped) - 1] = '\0'; + + // convert the hex to decimal + int utf8 = StringToInt(escaped[2], 16); + + // convert the utf8 to ascii + int ascii_size = json_utf8_to_ascii_size(utf8) + 1; + char[] ascii = new char[ascii_size]; + int written = json_utf8_to_ascii(utf8, ascii, ascii_size); + + // replace the escaped value with ascii bytes + int replacements = ReplaceString( + buffer, + max_size, + escaped, + ascii, + false + ); + + // calculate new string length based on replacements made + length -= replacements * sizeof(escaped) - 1; + length += replacements * written; + + // skip to the last of the bytes we just replaced + pos += written - 1; + } + + continuous_backslashes = 0; + } + } + + for (int i = 0; i < sizeof(JSON_STRING_NORMAL); i += 1) { + ReplaceString( + buffer, + max_size, + JSON_STRING_ESCAPED[i], + JSON_STRING_NORMAL[i] + ); + } +} + +/** + * Checks whether the provided character is a valid hexadecimal character. + * + * @param c Character to check. + * @return True if c is a hexadecimal character, false otherwise. + */ +stock bool json_char_is_hex(int c) +{ + return ( + (c >= '0' && c <= '9') + || (c >= 'a' && c <= 'f') + || (c >= 'A' && c <= 'F') + ); +} + +/** + * Calculates the maximum buffer length required to + * store the JSON cell representation of a string. + * + * @param length The length of the string. + * @return Maximum buffer length. + */ +stock int json_cell_string_size(const char[] input) +{ + int size = 3; // for outside quotes + NULL terminator + + bool foundEscapeTarget = false; + int length = strlen(input); + for (int pos = 0; pos < length; pos += 1) { + foundEscapeTarget = false; + for (int i = 0; i < sizeof(JSON_STRING_NORMAL); i += 1) { + if (input[pos] == JSON_STRING_NORMAL[i][0]) { + size += 2; + foundEscapeTarget = true; + break; + } + } + + if (foundEscapeTarget) { + continue; + } + + if (input[pos] < 0x80) { + size += 1; + continue; + } + + // consume the ascii bytes of the next utf8 character + int ascii_size; + json_ascii_to_utf8(input[pos], length - pos, ascii_size); + if (ascii_size <= 0) { + continue; + } + + pos += ascii_size - 1; + size += 6; // for unicode escape is \uXXXX + } + + return size; +} + +/** + * Generates the JSON cell representation of a string. + * + * @param input Value to generate output for. + * @param output String buffer to store output. + * @param max_size Maximum size of string buffer. + */ +stock void json_cell_string(const char[] input, char[] output, int max_size) +{ + // add input string to output, offset for start/end quotes + strcopy(output[1], max_size - 2, input); + + // escape the output + json_escape_string(output[1], max_size - 2); + + // surround output with quotations + output[0] = '"'; + StrCat(output, max_size, "\""); +} diff --git a/scripting/include/json/helpers/typedstringmap.inc b/scripting/include/json/helpers/typedstringmap.inc new file mode 100644 index 0000000..07ef8c0 --- /dev/null +++ b/scripting/include/json/helpers/typedstringmap.inc @@ -0,0 +1,222 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program 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 + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _typedstringmap_included + #endinput +#endif +#define _typedstringmap_included + +#include + +/** @see StringMap.ContainsKey */ +#define TRIE_SUPPORTS_CONTAINSKEY SOURCEMOD_V_MAJOR >= 1 \ + && SOURCEMOD_V_MINOR >= 11 \ + && SOURCEMOD_V_REV >= 6646 + +/** + * A StringMap with typed getters and setters. + */ +methodmap TypedStringMap < StringMap +{ + /** + * @section Properies + */ + + /** @see StringMap.Size */ + property int Length { + public get() + { + return this.Size; + } + } + + /** + * @section Misc + */ + + /** @see StringMap.ContainsKey */ + public bool HasKey(const char[] key) + { + #if TRIE_SUPPORTS_CONTAINSKEY + return this.ContainsKey(key); + #else + int dummy_int; + char dummy_str[1]; + + return this.GetValue(key, dummy_int) + || this.GetString(key, dummy_str, sizeof(dummy_str)); + #endif + } + + /** + * @section Getters + */ + + // GetValue is implemented natively by StringMap + + /** + * Retrieves the value stored at a key. + * @internal + * + * @param key Key to retrieve value for. + * @param default_value Value to return if the key does not exist. + * @return Value stored at key. + */ + public any GetOptionalValue(const char[] key, any default_value = -1) + { + any value; + return this.GetValue(key, value) ? value : default_value; + } + + // GetString is implemented natively by StringMap + + /** + * Retrieves the int stored at a key. + * + * @param key Key to retrieve int value for. + * @param default_value Value to return if the key does not exist. + * @return Value stored at key. + */ + public int GetInt(const char[] key, int default_value = -1) + { + return view_as(this.GetOptionalValue(key, default_value)); + } + + /** + * Retrieves the float stored at a key. + * + * @param key Key to retrieve float value for. + * @param default_value Value to return if the key does not exist. + * @return Value stored at key. + */ + public float GetFloat(const char[] key, float default_value = -1.0) + { + return view_as(this.GetOptionalValue(key, default_value)); + } + + /** + * Retrieves the bool stored at a key. + * + * @param key Key to retrieve bool value for. + * @param default_value Value to return if the key does not exist. + * @return Value stored at key. + */ + public bool GetBool(const char[] key, bool default_value = false) + { + return view_as(this.GetOptionalValue(key, default_value)); + } + + /** + * Retrieves the handle stored at a key. + * + * @param key Key to retrieve handle value for. + * @param default_value Value to return if the key does not exist. + * @return Value stored at key. + */ + public Handle GetHandle( + const char[] key, + Handle default_value = null + ) { + return view_as(this.GetOptionalValue(key, default_value)); + } + + /** + * @section Setters + */ + + // SetValue is implemented natively by StringMap + + // SetString is implemented natively by StringMap + + /** + * Sets the int stored at a key. + * + * @param key Key to set to int value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetInt(const char[] key, int value) + { + return this.SetValue(key, value); + } + + /** + * Sets the float stored at a key. + * + * @param key Key to set to float value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetFloat(const char[] key, float value) + { + return this.SetValue(key, value); + } + + /** + * Sets the bool stored at a key. + * + * @param key Key to set to bool value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetBool(const char[] key, bool value) + { + return this.SetValue(key, value); + } + + /** + * Sets the handle stored at a key. + * + * @param key Key to set to object value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetHandle(const char[] key, Handle value) + { + return this.SetValue(key, value); + } + + /** + * @section Constructor + */ + + /** + * Creates a new TypedStringMap. + * + * @return A new TypedStringMap. + */ + public TypedStringMap() + { + return view_as(CreateTrie()); + } +}; diff --git a/scripting/include/json/helpers/unicode.inc b/scripting/include/json/helpers/unicode.inc new file mode 100644 index 0000000..1c4bb43 --- /dev/null +++ b/scripting/include/json/helpers/unicode.inc @@ -0,0 +1,185 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program 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 + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _json_helpers_unicode_included + #endinput +#endif +#define _json_helpers_unicode_included + +// most of the code here is adapted from https://dev.w3.org/XML/encoding.c + +/** + * Calculates how many bytes will be required to store the ASCII + * representation of a UTF-8 character. + * + * @param c The UTF-8 character. + * @return The number of bytes required, or -1 if c is invalid. + */ +stock int json_utf8_to_ascii_size(int c) +{ + if (c < 0 || c > 0x10FFFF) { + return -1; + } + + if (c < 0x80) { + return 1; + } else if (c < 0x800) { + return 2; + } else if (c < 0x10000) { + if (c >= 0xD800 && c <= 0xDFFF) { + // high surrogate + return -1; + } + + return 3; + } + + return 4; +} + +/** + * Converts a UTF-8 character to its ASCII representation. + * + * @param c The UTF-8 character. + * @param output String buffer to store output. + * @param max_size Maximum size of string buffer. + * @return The number of bytes written, or -1 if c is invalid. + */ +stock int json_utf8_to_ascii(int c, char[] output, int max_size) +{ + if (max_size < 1) { + return 0; + } + + if (c < 0 || c > 0x10FFFF) { + return -1; + } + + int size = 0; + if (c < 0x80) { + size = 1; + output[0] = c; + } else if (c < 0x800) { + size = 2; + output[0] = ((c >> 6) & 0x1F) | 0xC0; + } else if (c < 0x10000) { + if (c >= 0xD800 && c <= 0xDFFF) { + // high surrogate + return -1; + } + + size = 3; + output[0] = ((c >> 12) & 0x0F) | 0xE0; + } else { + size = 4; + output[0] = ((c >> 18) & 0x07) | 0xF0; + } + + if (size >= max_size) { + return -1; + } + + // first byte has already been calculated, calculate the rest + int i; + for (i = 1; i < size; i += 1) { + output[i] = ((c >> ((size - i - 1) * 6)) & 0x3F) | 0x80; + } + + return i; +} + +/** + * Converts bytes to their UTF-8 int representation. + * + * @param ascii The ascii/bytes to convert. + * @param max_size Maximum size of ascii. + * @return The UTF-8 int representation. + */ +stock int json_ascii_to_utf8(const char[] ascii, int max_size, int &size) +{ + size = 0; + if (max_size < 1) { + return -1; + } + + int c = 0; + if ((ascii[0] & 0x80) != 0) { + if (max_size < 2) { + return -1; + } + + if ((ascii[1] & 0xC0) != 0x80) { + return -1; + } + + if ((ascii[0] & 0xE0) == 0xE0) { + if (max_size < 3) { + return -1; + } + + if ((ascii[2] & 0xC0) != 0x80) { + return -1; + } + + if ((ascii[0] & 0xF0) == 0xF0) { + if (max_size < 4) { + return -1; + } + + if ((ascii[0] & 0xF8) != 0xF0 || (ascii[3] & 0xC0) != 0x80) { + return -1; + } + + size = 4; + c = (ascii[0] & 0x07) << 18; + } else { + size = 3; + c = (ascii[0] & 0x0F) << 12; + } + } else { + size = 2; + c = (ascii[0] & 0x1F) << 6; + } + } else { + size = 1; + c = ascii[0]; + } + + // first byte has already been calculated, calculate the rest + int i; + for (i = 1; i < size; i += 1) { + c |= (ascii[i] & 0x3F) << ((size - i - 1) * 6); + } + + return c; +} diff --git a/scripting/include/json/object.inc b/scripting/include/json/object.inc new file mode 100644 index 0000000..71a0603 --- /dev/null +++ b/scripting/include/json/object.inc @@ -0,0 +1,724 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * A pure SourcePawn JSON encoder/decoder. + * https://github.com/clugg/sm-json + * + * sm-json (C)2022 James Dickens. (clug) + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program 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 + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + */ + +#if defined _json_object_included + #endinput +#endif +#define _json_object_included + +#include +#include +#include +#include + +methodmap JSON_Object < MetaStringMap +{ + /** + * @section Properties + */ + + /** + * Views the instance as its superclass to access overridden methods. + */ + property MetaStringMap Super + { + public get() + { + return view_as(this); + } + } + + /** + * Whether the current object is an array. + */ + property bool IsArray { + public get() + { + return this.Meta.GetBool(JSON_ARRAY_KEY); + } + } + + /** + * @section Iteration Helpers + */ + + /** + * @section Metadata Getters + */ + + /** + * Gets the requested meta info for a key. + * @internal + * + * @param key Key to get meta info for. + * @param meta Meta info to get. + * @param default_value Value to return if meta does not exist. + * @return The meta value. + */ + public any GetMeta( + const char[] key, + JSONMetaInfo meta, + any default_value + ) { + int max_size = json_meta_key_length(key); + char[] meta_key = new char[max_size]; + json_format_meta_key(meta_key, max_size, key, meta); + + return this.Meta.GetOptionalValue(meta_key, default_value); + } + + /** + * Gets the cell type stored at a key. + * + * @param key Key to get value type for. + * @return Value type for key provided, + * or JSON_Type_Invalid if it does not exist. + */ + public JSONCellType GetType(const char[] key) + { + return view_as( + this.GetMeta(key, JSON_Meta_Type, JSON_Type_Invalid) + ); + } + + /** + * Gets the size of the string stored at a key. + * + * @param key Key to get buffer size for. + * @return Buffer size for string at key provided, + * or -1 if it is not a string/does not exist. + */ + public int GetSize(const char[] key) + { + return view_as(this.GetMeta(key, JSON_Meta_Size, -1)); + } + + /** + * Gets whether the key should be hidden from encoding. + * + * @param key Key to get hidden state for. + * @return Whether or not the key should be hidden. + */ + public bool GetHidden(const char[] key) + { + return view_as(this.GetMeta(key, JSON_Meta_Hidden, false)); + } + + /** + * Gets the index of a key. + * + * @param key Key to get index of. + * @return Index of the key provided, or -1 if it does not exist. + */ + public int GetIndex(const char[] key) + { + return view_as(this.GetMeta(key, JSON_Meta_Index, -1)); + } + + /** + * Gets the key stored at an index. + * If an array, will convert the index to its string value. + * If an array, will return false if the index is not between [0, length]. + * + * @param index Index of key. + * @param value Buffer to store key at. + * @param max_size Maximum size of value buffer. + * @return True on success, false otherwise. + */ + public bool GetKey(int index, char[] value, int max_size) + { + char[] index_key = new char[JSON_INT_BUFFER_SIZE]; + if (IntToString(index, index_key, JSON_INT_BUFFER_SIZE) == 0) { + return false; + } + + if (this.IsArray) { + // allow access of one past last index for intermediary operations + if (index < 0 || index > this.Length) { + return false; + } + + strcopy(value, max_size, index_key); + + return true; + } + + return this.Meta.GetString(index_key, value, max_size); + } + + /** + * Returns the buffer size required to store the key at the specified index. + * + * @param index Index of key. + * @return Buffer size required to store key. + */ + public int GetKeySize(int index) + { + if (this.IsArray) { + return JSON_INT_BUFFER_SIZE; + } + + int max_size = JSON_INT_BUFFER_SIZE + 4; + char[] index_size_key = new char[max_size]; + FormatEx(index_size_key, max_size, "%d:len", index); + + return this.Meta.GetInt(index_size_key); + } + + /** + * @section Metadata Setters + */ + + /** + * Sets meta info on a key. + * @internal + * + * @param key Key to set meta info for. + * @param meta Meta info to set. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetMeta(const char[] key, JSONMetaInfo meta, any value) + { + int max_size = json_meta_key_length(key); + char[] meta_key = new char[max_size]; + json_format_meta_key(meta_key, max_size, key, meta); + + return this.Meta.SetValue(meta_key, value); + } + + /** + * Removes meta info from a key. + * @internal + * + * @param key Key to remove meta info from. + * @param meta Meta info to remove. + * @return True on success, false otherwise. + */ + public bool RemoveMeta(const char[] key, JSONMetaInfo meta) + { + int max_size = json_meta_key_length(key); + char[] meta_key = new char[max_size]; + json_format_meta_key(meta_key, max_size, key, meta); + + return this.Meta.Remove(meta_key); + } + + /** + * Sets whether the key should be hidden from encoding. + * + * @param key Key to set hidden state for. + * @param hidden Whether or not the key should be hidden. + * @return True on success, false otherwise. + */ + public bool SetHidden(const char[] key, bool hidden) + { + return hidden + ? this.SetMeta(key, JSON_Meta_Hidden, hidden) + : this.RemoveMeta(key, JSON_Meta_Hidden); + } + + /** + * Tracks a key, setting it's type and index where necessary. + * @internal + * + * @param key Key to track. If the key already + * exists, it's index will not be changed. + * @param type Type to set key to. If a valid type is + * provided, the key's type will be updated. + * @param index Index to set key to. If the index is not + * provided (-1), the object length will be used. + * @return True on success, false otherwise. + */ + public bool TrackKey( + const char[] key, + JSONCellType type = JSON_Type_Invalid, + int index = -1 + ) { + // track type if provided + if (type != JSON_Type_Invalid) { + this.SetMeta(key, JSON_Meta_Type, type); + } + + if (this.IsArray) { + return true; + } + + if (index == -1) { + index = this.Length; + + // skip tracking index if we're pushing to end & key already exists + if (this.HasKey(key)) { + if (type != JSON_Type_Invalid && type != JSON_Type_String) { + // remove any existing size + this.RemoveMeta(key, JSON_Meta_Size); + } + + return true; + } + } + + char[] index_key = new char[JSON_INT_BUFFER_SIZE]; + IntToString(index, index_key, JSON_INT_BUFFER_SIZE); + + int max_size = JSON_INT_BUFFER_SIZE + 8; + char[] index_size_key = new char[max_size]; + FormatEx(index_size_key, max_size, "%d:len", index); + + return this.Meta.SetString(index_key, key) + && this.Meta.SetInt(index_size_key, strlen(key) + 1) + && this.SetMeta(key, JSON_Meta_Index, index); + } + + /** + * Untracks a key, cleaning up all it's meta and indexing data. + * @internal + * + * @param key Key to untrack. + * @return True on success, false otherwise. + */ + public bool UntrackKey(const char[] key) + { + int index = this.GetIndex(key); + + for (int i = 0; i < sizeof(JSON_ALL_METADATA); i += 1) { + this.RemoveMeta(key, JSON_ALL_METADATA[i]); + } + + if (this.IsArray) { + return true; + } + + if (index == -1) { + return false; + } + + char[] index_key = new char[JSON_INT_BUFFER_SIZE]; + IntToString(index, index_key, JSON_INT_BUFFER_SIZE); + + int max_size = JSON_INT_BUFFER_SIZE + 8; + char[] index_size_key = new char[max_size]; + FormatEx(index_size_key, max_size, "%d:len", index); + + if (! this.Meta.Remove(index_key) || ! this.Meta.Remove(index_size_key)) { + return false; + } + + int length = this.Length; + int last_index = length - 1; + if (index < last_index) { + for (int i = index + 1; i < length; i += 1) { + int new_key_size = this.GetKeySize(i); + char[] new_key = new char[new_key_size]; + this.GetKey(i, new_key, new_key_size); + + this.TrackKey(new_key, JSON_Type_Invalid, i - 1); + } + + IntToString(last_index, index_key, JSON_INT_BUFFER_SIZE); + FormatEx(index_size_key, max_size, "%d:len", last_index); + + this.Meta.Remove(index_key); + this.Meta.Remove(index_size_key); + } + + return true; + } + + /** + * @section Getters + */ + + #if SM_INT64_SUPPORTED + /** + * Retrieves the int64 stored at a key. + * + * @param key Key to retrieve int64 value for. + * @param value Int buffer to store output. + * @return True on success, false otherwise. + */ + public bool GetInt64(const char[] key, int value[2]) + { + return this.Data.GetArray(key, value, 2); + } + #endif + + /** + * Retrieves the JSON object stored at a key. + * + * @param key Key to retrieve object value for. + * @param default_value Value to return if the key does not exist. + * @return Value stored at key. + */ + public JSON_Object GetObject( + const char[] key, + JSON_Object default_value = null + ) { + return view_as(this.GetHandle(key, default_value)); + } + + /** + * @section Setters + */ + + /** + * Sets the string stored at a key. + * + * @param key Key to set to string value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetString(const char[] key, const char[] value) + { + return this.TrackKey(key, JSON_Type_String) + && this.Super.SetString(key, value) + && this.SetMeta(key, JSON_Meta_Size, strlen(value) + 1); + } + + /** + * Sets the int stored at a key. + * + * @param key Key to set to int value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetInt(const char[] key, int value) + { + return this.TrackKey(key, JSON_Type_Int) + && this.Super.SetInt(key, value); + } + + #if SM_INT64_SUPPORTED + /** + * Sets the int64 stored at a key. + * + * @param key Key to set to int64 value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetInt64(const char[] key, int value[2]) + { + return this.TrackKey(key, JSON_Type_Int64) + && this.Data.SetArray(key, value, 2); + } + #endif + + /** + * Sets the float stored at a key. + * + * @param key Key to set to float value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetFloat(const char[] key, float value) + { + return this.TrackKey(key, JSON_Type_Float) + && this.Super.SetFloat(key, value); + } + + /** + * Sets the bool stored at a key. + * + * @param key Key to set to bool value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetBool(const char[] key, bool value) + { + return this.TrackKey(key, JSON_Type_Bool) + && this.Super.SetBool(key, value); + } + + /** + * Sets the JSON object stored at a key. + * + * @param key Key to set to object value. + * @param value Value to set. + * @return True on success, false otherwise. + */ + public bool SetObject(const char[] key, JSON_Object value) + { + return this.TrackKey(key, JSON_Type_Object) + && this.Super.SetHandle(key, value); + } + + /** + * @section Misc + */ + + /** + * Removes an item from the object by key. + * + * @param key Key of object to remove. + * @return True on success, false if the value was never set. + */ + public bool Remove(const char[] key) + { + return this.UntrackKey(key) && this.Super.Remove(key); + } + + /** + * Renames the key of an existing item in the object. + * + * @param from Existing key to rename. + * @param to New key. + * @param replace Should the 'to' key should be replaced if it exists? + * @return True on success, false otherwise. + */ + public bool Rename( + const char[] from, + const char[] to, + bool replace = true + ) { + JSONCellType type = this.GetType(from); + if (type == JSON_Type_Invalid) { + return false; + } + + if (StrEqual(from, to, true)) { + return true; + } + + bool toExists = this.HasKey(to); + if (toExists) { + if (! replace) { + return false; + } + + this.Remove(to); + } + + switch (type) { + case JSON_Type_String: { + int length = this.GetSize(from); + char[] value = new char[length]; + this.GetString(from, value, length); + this.SetString(to, value); + } + case JSON_Type_Int: { + this.SetInt(to, this.GetInt(from)); + } + #if SM_INT64_SUPPORTED + case JSON_Type_Int64: { + int value[2]; + this.GetInt64(from, value); + this.SetInt64(to, value); + } + #endif + case JSON_Type_Float: { + this.SetFloat(to, this.GetFloat(from)); + } + case JSON_Type_Bool: { + this.SetBool(to, this.GetBool(from)); + } + case JSON_Type_Object: { + this.SetObject(to, this.GetObject(from)); + } + } + + this.SetHidden(to, this.GetHidden(from)); + + this.Remove(from); + + return true; + } + + /** + * Merges in the entries from the specified object, + * optionally replacing existing entries with the same key. + * + * @param from Object to merge entries from. + * @param options Bitwise combination of `JSON_MERGE_*` options. + * @return True on success, false otherwise. + * @error If the object being merged is an array, + * an error will be thrown. + */ + public bool Merge(JSON_Object from, int options = JSON_MERGE_REPLACE) + { + if (this.IsArray || from.IsArray) { + json_set_last_error("attempted to merge using an array"); + + return false; + } + + bool replace = (options & JSON_MERGE_REPLACE) > 0; + bool autocleanup = (options & JSON_MERGE_CLEANUP) > 0; + + int json_size = from.Length; + int key_length = 0; + for (int i = 0; i < json_size; i += 1) { + key_length = from.GetKeySize(i); + char[] key = new char[key_length]; + from.GetKey(i, key, key_length); + + // skip already existing keys if we aren't in replace mode + bool key_already_exists = this.HasKey(key); + if (! replace && key_already_exists) { + continue; + } + + JSONCellType type = from.GetType(key); + // skip keys of unknown type + if (type == JSON_Type_Invalid) { + continue; + } + + // merge value onto structure + switch (type) { + case JSON_Type_String: { + int length = from.GetSize(key); + char[] value = new char[length]; + from.GetString(key, value, length); + + this.SetString(key, value); + } + case JSON_Type_Int: { + this.SetInt(key, from.GetInt(key)); + } + #if SM_INT64_SUPPORTED + case JSON_Type_Int64: { + int value[2]; + from.GetInt64(key, value); + this.SetInt64(key, value); + } + #endif + case JSON_Type_Float: { + this.SetFloat(key, from.GetFloat(key)); + } + case JSON_Type_Bool: { + this.SetBool(key, from.GetBool(key)); + } + case JSON_Type_Object: { + JSON_Object value = from.GetObject(key); + + if (autocleanup && key_already_exists) { + JSON_Object existing = this.GetObject(key); + if (existing != value) { + json_cleanup_and_delete(existing); + } + } + + this.SetObject(key, value); + } + } + + this.SetHidden(key, from.GetHidden(key)); + } + + return true; + } + + /** + * @section json.inc Aliases + */ + + /** + * Makes a global call with this + * instance passed as the object. + * + * @see json_encode_size + */ + public int EncodeSize(int options = JSON_NONE) + { + return json_encode_size(this, options); + } + + /** + * Makes a global call with this + * instance passed as the object. + * + * @see json_encode + */ + public void Encode(char[] output, int max_size, int options = JSON_NONE) + { + json_encode(this, output, max_size, options); + } + + /** + * Makes a global call with this + * instance passed as the object. + * + * @see json_write_to_file + */ + public bool WriteToFile(const char[] path, int options = JSON_NONE) + { + return json_write_to_file(this, path, options); + } + + /** + * Makes a global call with this + * instance passed as the object. + * + * @see json_copy_deep + */ + public JSON_Object ShallowCopy() + { + return json_copy_shallow(this); + } + + /** + * Makes a global call with this + * instance passed as the object. + * + * @see json_copy_deep + */ + public JSON_Object DeepCopy() + { + return json_copy_deep(this); + } + + /** + * Makes a global call with this + * instance passed as the object. + * + * @see json_cleanup + */ + public void Cleanup() + { + json_cleanup(this); + } + + /** + * @section Constructor + */ + + /** + * Creates a new JSON_Object. + * + * @return A new JSON_Object. + */ + public JSON_Object() + { + return view_as(new MetaStringMap()); + } +}; diff --git a/scripting/l4d2_randomizer.sp b/scripting/l4d2_randomizer.sp new file mode 100644 index 0000000..b877154 --- /dev/null +++ b/scripting/l4d2_randomizer.sp @@ -0,0 +1,332 @@ +#pragma semicolon 1 +#pragma newdecls required + +//#define DEBUG + +#define PLUGIN_VERSION "1.0" +#define DEBUG_SCENE_PARSE 1 + +#include +#include +//#include +#include +#include +#include +#define ENT_PROP_NAME "l4d2_randomizer" +#include + +public Plugin myinfo = +{ + name = "L4D2 Randomizer", + author = "jackzmc", + description = "", + version = PLUGIN_VERSION, + url = "https://github.com/Jackzmc/sourcemod-plugins" +}; + +ConVar cvarEnabled; + +public void OnPluginStart() { + EngineVersion g_Game = GetEngineVersion(); + if(g_Game != Engine_Left4Dead && g_Game != Engine_Left4Dead2) { + SetFailState("This plugin is for L4D/L4D2 only."); + } + + RegAdminCmd("sm_rcycle", Command_CycleRandom, ADMFLAG_CHEATS); + RegAdminCmd("sm_expent", Command_ExportEnt, ADMFLAG_GENERIC); + + cvarEnabled = CreateConVar("sm_randomizer_enabled", "0"); +} + +public void OnMapEnd() { + DeleteCustomEnts(); +} + +int GetLookingEntity(int client, TraceEntityFilter filter) { + static float pos[3], ang[3]; + GetClientEyePosition(client, pos); + GetClientEyeAngles(client, ang); + TR_TraceRayFilter(pos, ang, MASK_SOLID, RayType_Infinite, filter, client); + if(TR_DidHit()) { + return TR_GetEntityIndex(); + } + return -1; +} + +public Action Command_CycleRandom(int client, int args) { + DeleteCustomEnts(); + char map[64]; + GetCurrentMap(map, sizeof(map)); + LoadMap(map); + ReplyToCommand(client, "Done."); + return Plugin_Handled; +} + +public Action Command_ExportEnt(int client, int args) { + int entity = GetLookingEntity(client, Filter_IgnorePlayer); + if(entity > 0) { + float origin[3]; + float angles[3]; + float size[3]; + GetEntPropVector(entity, Prop_Send, "m_vecOrigin", origin); + GetEntPropVector(entity, Prop_Send, "m_angRotation", angles); + GetEntPropVector(entity, Prop_Send, "m_vecMaxs", size); + + char model[64]; + GetEntPropString(entity, Prop_Data, "m_ModelName", model, sizeof(model)); + + ReplyToCommand(client, "{"); + ReplyToCommand(client, "\t\"model\": \"%s\",", model); + ReplyToCommand(client, "\t\"origin\": [%.2f, %.2f, %.2f],", origin[0], origin[1], origin[2]); + ReplyToCommand(client, "\t\"angles\": [%.2f, %.2f, %.2f],", angles[0], angles[1], angles[2]); + ReplyToCommand(client, "\t\"size\": [%.2f, %.2f, %.2f]", size[0], size[1], size[2]); + ReplyToCommand(client, "}"); + } else { + PrintCenterText(client, "No entity found"); + } + return Plugin_Handled; +} + +public void OnMapStart() { + if(cvarEnabled.BoolValue) { + char map[64]; + GetCurrentMap(map, sizeof(map)); + LoadMap(map); + } +} + +#define MAX_SCENE_NAME_LENGTH 32 +enum struct SceneData { + char name[MAX_SCENE_NAME_LENGTH]; + float chance; + ArrayList exclusions; + ArrayList variants; + + void Cleanup() { + delete this.exclusions; + SceneVariantData choice; + for(int i = 0; i < this.variants.Length; i++) { + this.variants.GetArray(i, choice); + choice.Cleanup(); + } + delete this.variants; + } +} + +enum struct SceneVariantData { + int weight; + ArrayList entities; + + void Cleanup() { + delete this.entities; + } +} + +enum struct VariantEntityData { + char type[16]; + char model[64]; + float origin[3]; + float angles[3]; + float scale[3]; +} + +ArrayList scenes; + +// Parses (mapname).json and runs chances +public bool LoadMap(const char[] map) { + + char filePath[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, filePath, sizeof(filePath), "data/randomizer/%s.json", map); + if(!FileExists(filePath)) { + Log("[Randomizer] No map config file (data/randomizer/%s.json), not loading", map); + return false; + } + + char buffer[65536]; + File file = OpenFile(filePath, "r"); + if(file == null) { + LogError("[Randomizer] Could not open map config file (data/randomizer/%s.json)", map); + return false; + } + file.ReadString(buffer, sizeof(buffer)); + JSON_Object data = json_decode(buffer); + if(data == null) { + json_get_last_error(buffer, sizeof(buffer)); + LogError("[Randomizer] Could not parse map config file (data/randomizer/%s.json): %s", map, buffer); + delete file; + return false; + } + Cleanup(); + scenes = new ArrayList(sizeof(SceneData)); + + Profiler profiler = new Profiler(); + profiler.Start(); + + int length = data.Length; + char key[32]; + for (int i = 0; i < length; i += 1) { + data.GetKey(i, key, sizeof(key)); + if(data.GetType(key) != JSON_Type_Object) continue; + + JSON_Object scene = data.GetObject(key); + // Parses scene data and inserts to scenes + loadGroup(key, scene); + } + + profiler.Stop(); + Log("Loaded %d scenes in %.1f seconds", scenes.Length, profiler.Time); + profiler.Start(); + + processGroups(); + + profiler.Stop(); + Log("Done processing in %.1f seconds", scenes.Length, profiler.Time); + + json_cleanup_and_delete(data); + + delete profiler; + delete file; + return true; +} + +void loadGroup(const char key[MAX_SCENE_NAME_LENGTH], JSON_Object sceneData) { + SceneData scene; + scene.name = key; + scene.chance = sceneData.GetFloat("chance"); + if(scene.chance < 0.0 || scene.chance > 1.0) { + LogError("Scene \"%s\" has invalid chance (%f)", scene.name, scene.chance); + return; + } + scene.exclusions = new ArrayList(ByteCountToCells(MAX_SCENE_NAME_LENGTH)); + JSON_Array exclusions = view_as(sceneData.GetObject("exclusions")); + if(exclusions != null) { + char id[MAX_SCENE_NAME_LENGTH]; + for(int i = 0; i < exclusions.Length; i ++) { + exclusions.GetString(i, id, sizeof(id)); + scene.exclusions.PushString(id); + } + } + scene.variants = new ArrayList(sizeof(SceneVariantData)); + JSON_Array variants = view_as(sceneData.GetObject("variants")); + for(int i = 0; i < variants.Length; i++) { + // Parses choice and loads to scene.choices + loadChoice(scene, variants.GetObject(i)); + } + scenes.PushArray(scene); +} + +void loadChoice(SceneData scene, JSON_Object choiceData) { + SceneVariantData choice; + choice.weight = choiceData.GetInt("weight", 1); + choice.entities = new ArrayList(sizeof(VariantEntityData)); + JSON_Array entities = view_as(choiceData.GetObject("entities")); + for(int i = 0; i < entities.Length; i++) { + // Parses entities and loads to choice.entities + loadChoiceEntity(choice, entities.GetObject(i)); + } + scene.variants.PushArray(choice); +} + +void loadChoiceEntity(SceneVariantData choice, JSON_Object entityData) { + VariantEntityData entity; + entityData.GetString("model", entity.model, sizeof(entity.model)); + if(!entityData.GetString("type", entity.type, sizeof(entity.type))) { + entity.type = "prop_dynamic"; + } + GetVector(entityData, "origin", entity.origin); + GetVector(entityData, "angles", entity.angles); + GetVector(entityData, "scale", entity.scale); + choice.entities.PushArray(entity); +} + +void GetVector(JSON_Object obj, const char[] key, float out[3]) { + JSON_Array vecArray = view_as(obj.GetObject(key)); + if(vecArray != null) { + out[0] = vecArray.GetFloat(0); + out[1] = vecArray.GetFloat(1); + out[2] = vecArray.GetFloat(2); + } +} + +void processGroups() { + SceneData scene; + for(int i = 0; i < scenes.Length; i++) { + scenes.GetArray(i, scene); + // TODO: Exclusions + if(GetURandomFloat() < scene.chance) { + selectScene(scene); + } + } +} + +void selectScene(SceneData scene) { + // TODO: Weight + if(scene.variants.Length == 0) { + LogError("Warn: No variants were found for scene \"%s\"", scene.name); + return; + } + Debug("Selected scene: \"%s\"", scene.name); + + ArrayList choices = new ArrayList(); + SceneVariantData choice; + // Weighted random: Push N times dependent on weight + for(int i = 0; i < scene.variants.Length; i++) { + scene.variants.GetArray(i, choice); + for(int c = 0; c < choice.weight; c++) { + choices.Push(i); + } + + } + int index = GetURandomInt() % choices.Length; + index = choices.Get(index); + delete choices; + Debug("Selected variant: #%d", index); + scene.variants.GetArray(index, choice); + spawnVariant(choice); +} + +void spawnVariant(SceneVariantData choice) { + VariantEntityData entity; + // Weighted random: Push N times dependent on weight + for(int i = 0; i < choice.entities.Length; i++) { + choice.entities.GetArray(i, entity); + spawnEntity(entity); + } +} + +void spawnEntity(VariantEntityData entity) { + Debug("spawning \"%s\" at (%.1f %.1f %.1f) rot (%.0f %.0f %.0f)", entity.model, entity.origin[0], entity.origin[1], entity.origin[2], entity.angles[0], entity.angles[1], entity.angles[2]); + PrecacheModel(entity.model); + CreateProp(entity.type, entity.model, entity.origin, entity.angles); +} + +void Debug(const char[] format, any ...) { + #if defined DEBUG_SCENE_PARSE + char buffer[192]; + + VFormat(buffer, sizeof(buffer), format, 2); + + PrintToServer("[Randomizer::Debug] %s", buffer); + PrintToConsoleAll("[Randomizer::Debug] %s", buffer); + + #endif +} + +void Log(const char[] format, any ...) { + char buffer[192]; + + VFormat(buffer, sizeof(buffer), format, 2); + + PrintToServer("[Randomizer] %s", buffer); +} + +void Cleanup() { + if(scenes != null) { + SceneData scene; + for(int i = 0; i < scenes.Length; i++) { + scenes.GetArray(i, scene); + scene.Cleanup(); + } + delete scenes; + } +} \ No newline at end of file