diff --git a/CHANGES b/CHANGES index dc155568a..67d1f0611 100644 --- a/CHANGES +++ b/CHANGES @@ -42,6 +42,7 @@ Misc: 0.11.0: (Future) Features: - Scripting: New `input` API for getting raw keyboard/mouse/controller state + - Scripting: New `storage` API for saving data for a script, e.g. settings - New unlicensed GB mappers: NT (older types 1 and 2), Li Cheng, GGB-81 - Debugger: Add range watchpoints Emulation fixes: @@ -57,11 +58,19 @@ Other fixes: - Qt: Fix crash when attempting to use OpenGL 2.1 to 3.1 (fixes mgba.io/i/2794) - Qt: Disable sync while running scripts from main thread (fixes mgba.io/i/2738) - Qt: Fix savestate preview sizes with different scales (fixes mgba.io/i/2560) + - Qt: Properly cap number of attached players by platform (fixes mgba.io/i/2807) + - Qt: Disable attempted linking betwen incompatible platforms (fixes mgba.io/i/2702) + - Qt: Fix modifier key names in shortcut editor (fixes mgba.io/i/2817) + - Qt: Fix a handful of edge cases with graphics viewers (fixes mgba.io/i/2827) + - Scripting: Fix receiving packets for client sockets + - Scripting: Fix empty receive calls returning unknown error on Windows Misc: - GB Serialize: Add missing savestate support for MBC6 and NT (newer) - GBA: Improve detection of valid ELF ROMs - Qt: Include wayland QPA in AppImage (fixes mgba.io/i/2796) - Qt: Stop eating boolean action key events (fixes mgba.io/i/2636) + - Qt: Automatically change video file extension as appropriate + - Scripting: Add `callbacks:oneshot` for single-call callbacks 0.10.1: (2023-01-10) Emulation fixes: diff --git a/CMakeLists.txt b/CMakeLists.txt index 656dc8f3a..fcec15990 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,7 @@ if(NOT LIBMGBA_ONLY) set(USE_SQLITE3 ON CACHE BOOL "Whether or not to enable SQLite3 support") set(USE_ELF ON CACHE BOOL "Whether or not to enable ELF support") set(USE_LUA ON CACHE BOOL "Whether or not to enable Lua scripting support") + set(USE_JSON_C ON CACHE BOOL "Whether or not to enable JSON-C support") set(M_CORE_GBA ON CACHE BOOL "Build Game Boy Advance core") set(M_CORE_GB ON CACHE BOOL "Build Game Boy core") set(M_CORE_DS ON CACHE BOOL "Build DS core") @@ -481,6 +482,7 @@ endif() if(DISABLE_DEPS) set(USE_GDB_STUB OFF) set(USE_DISCORD_RPC OFF) + set(USE_JSON_C OFF) set(USE_SQLITE3 OFF) set(USE_PNG OFF) set(USE_ZLIB OFF) @@ -782,11 +784,36 @@ endif() if(ENABLE_SCRIPTING) list(APPEND ENABLES SCRIPTING) + find_feature(USE_JSON_C "json-c") if(NOT USE_LUA VERSION_LESS 5.1) find_feature(USE_LUA "Lua" ${USE_LUA}) else() find_feature(USE_LUA "Lua") endif() + if(USE_JSON_C) + list(APPEND FEATURES JSON_C) + if(TARGET json-c::json-c) + list(APPEND DEPENDENCY_LIB json-c::json-c) + get_target_property(JSON_C_SONAME json-c::json-c IMPORTED_SONAME_NONE) + string(SUBSTRING "${JSON_C_SONAME}" 13 -1 JSON_C_SOVER) + + # This is only needed on 0.15, but the target unhelpfully does not contain version info + get_target_property(JSON_C_INCLUDE_DIR json-c::json-c INTERFACE_INCLUDE_DIRECTORIES) + if(NOT JSON_C_INCLUDE_DIR MATCHES json-c$) + include_directories(AFTER "${JSON_C_INCLUDE_DIR}/json-c") + endif() + else() + if(${json-c_VERSION} VERSION_LESS 0.13.0) + set(JSON_C_SOVER 3) + elseif(${json-c_VERSION} VERSION_LESS 0.15.0) + set(JSON_C_SOVER 4) + endif() + list(APPEND DEPENDENCY_LIB ${json-c_LIBRARIES}) + include_directories(AFTER ${json-c_INCLUDE_DIRS}) + link_directories(${json-c_LIBDIRS}) + endif() + set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS},libjson-c${JSON_C_SOVER}") + endif() if(USE_LUA) list(APPEND FEATURE_DEFINES USE_LUA) include_directories(AFTER ${LUA_INCLUDE_DIR}) @@ -1317,6 +1344,7 @@ if(NOT QUIET AND NOT LIBMGBA_ONLY) else() message(STATUS " Lua: ${USE_LUA}") endif() + message(STATUS " storage API: ${USE_JSON_C}") endif() message(STATUS "Frontends:") message(STATUS " Qt: ${BUILD_QT}") diff --git a/README.md b/README.md index 6311d6925..7f5026088 100644 --- a/README.md +++ b/README.md @@ -143,9 +143,9 @@ Compiling requires using CMake 3.1 or newer. GCC, Clang, and Visual Studio 2019 #### Docker building -The recommended way to build for most platforms is to use Docker. Several Docker images are provided that contain the requisite toolchain and dependencies for building mGBA across several platforms. +The recommended way to build for most platforms is to use Docker. Several Docker images are provided that contain the requisite toolchain and dependencies for building mGBA across several platforms. -Note: If you are on an older Windows system before Windows 10, you may need to configure your Docker to use VirtualBox shared folders to correctly map your current `mgba` checkout directory to the Docker image's working directory. (See issue [#1985](https://mgba.io/i/1985) for details.) +Note: If you are on an older Windows system before Windows 10, you may need to configure your Docker to use VirtualBox shared folders to correctly map your current `mgba` checkout directory to the Docker image's working directory. (See issue [#1985](https://mgba.io/i/1985) for details.) To use a Docker image to build mGBA, simply run the following command while in the root of an mGBA checkout: @@ -252,6 +252,7 @@ medusa has no hard dependencies, however, the following optional dependencies ar - SQLite3: for game databases. - libelf: for ELF loading. - Lua: for scripting. +- json-c: for the scripting `storage` API. SQLite3, libpng, and zlib are included with the emulator, so they do not need to be externally compiled first. @@ -311,7 +312,7 @@ Missing features on DS are Copyright --------- -medusa is Copyright © 2013 – 2022 Jeffrey Pfau. It is distributed under the [Mozilla Public License version 2.0](https://www.mozilla.org/MPL/2.0/). A copy of the license is available in the distributed LICENSE file. +medusa is Copyright © 2013 – 2023 Jeffrey Pfau. It is distributed under the [Mozilla Public License version 2.0](https://www.mozilla.org/MPL/2.0/). A copy of the license is available in the distributed LICENSE file. medusa contains the following third-party libraries: diff --git a/include/mgba/script/context.h b/include/mgba/script/context.h index dcec138e1..9e8e5ec23 100644 --- a/include/mgba/script/context.h +++ b/include/mgba/script/context.h @@ -98,6 +98,7 @@ void mScriptContextExportNamespace(struct mScriptContext* context, const char* n void mScriptContextTriggerCallback(struct mScriptContext*, const char* callback, struct mScriptList* args); uint32_t mScriptContextAddCallback(struct mScriptContext*, const char* callback, struct mScriptValue* value); +uint32_t mScriptContextAddOneshot(struct mScriptContext*, const char* callback, struct mScriptValue* value); void mScriptContextRemoveCallback(struct mScriptContext*, uint32_t cbid); void mScriptContextSetDocstring(struct mScriptContext*, const char* key, const char* docstring); diff --git a/include/mgba/script/macros.h b/include/mgba/script/macros.h index 42710f9e8..bbde17b07 100644 --- a/include/mgba/script/macros.h +++ b/include/mgba/script/macros.h @@ -423,7 +423,7 @@ CXX_GUARD_START #define mSCRIPT_DEFINE_STRUCT_DEINIT(TYPE) _mSCRIPT_DEFINE_STRUCT_BINDING(DEINIT, TYPE, _deinit, _deinit) #define mSCRIPT_DEFINE_STRUCT_DEINIT_NAMED(TYPE, NAME) _mSCRIPT_DEFINE_STRUCT_BINDING(DEINIT, TYPE, _deinit, NAME) #define mSCRIPT_DEFINE_STRUCT_DEFAULT_GET(TYPE) _mSCRIPT_DEFINE_STRUCT_BINDING(GET, TYPE, _get, _get) -#define mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(TYPE) _mSCRIPT_DEFINE_STRUCT_BINDING(SET, TYPE, _set, _set) +#define mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(TYPE, SETTER) _mSCRIPT_DEFINE_STRUCT_BINDING(SET, TYPE, SETTER, SETTER) #define mSCRIPT_DEFINE_DOC_STRUCT_METHOD(SCOPE, TYPE, NAME) mSCRIPT_DEFINE_STRUCT_METHOD_NAMED(doc_ ## TYPE, NAME, NAME) diff --git a/include/mgba/script/storage.h b/include/mgba/script/storage.h new file mode 100644 index 000000000..629158e83 --- /dev/null +++ b/include/mgba/script/storage.h @@ -0,0 +1,28 @@ +/* Copyright (c) 2013-2023 Jeffrey Pfau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef M_SCRIPT_STORAGE_H +#define M_SCRIPT_STORAGE_H + +#include + +CXX_GUARD_START + +#include +#include + +struct VFile; +void mScriptContextAttachStorage(struct mScriptContext* context); +void mScriptStorageFlushAll(struct mScriptContext* context); + +bool mScriptStorageSaveBucket(struct mScriptContext* context, const char* bucket); +bool mScriptStorageSaveBucketVF(struct mScriptContext* context, const char* bucket, struct VFile* vf); +bool mScriptStorageLoadBucket(struct mScriptContext* context, const char* bucket); +bool mScriptStorageLoadBucketVF(struct mScriptContext* context, const char* bucket, struct VFile* vf); +void mScriptStorageGetBucketPath(const char* bucket, char* out); + +CXX_GUARD_END + +#endif diff --git a/include/mgba/script/types.h b/include/mgba/script/types.h index ed8ae02e3..da13883d2 100644 --- a/include/mgba/script/types.h +++ b/include/mgba/script/types.h @@ -35,16 +35,17 @@ CXX_GUARD_START #define mSCRIPT_TYPE_C_PTR void* #define mSCRIPT_TYPE_C_CPTR const void* #define mSCRIPT_TYPE_C_LIST struct mScriptList* -#define mSCRIPT_TYPE_C_TABLE Table* +#define mSCRIPT_TYPE_C_TABLE struct Table* #define mSCRIPT_TYPE_C_WRAPPER struct mScriptValue* #define mSCRIPT_TYPE_C_WEAKREF uint32_t +#define mSCRIPT_TYPE_C_NUL void* #define mSCRIPT_TYPE_C_S(STRUCT) struct STRUCT* #define mSCRIPT_TYPE_C_CS(STRUCT) const struct STRUCT* -#define mSCRIPT_TYPE_C_S_METHOD(STRUCT, NAME) _mSTStructFunctionType_ ## STRUCT ## _ ## NAME #define mSCRIPT_TYPE_C_PS(X) void #define mSCRIPT_TYPE_C_PCS(X) void #define mSCRIPT_TYPE_C_WSTR struct mScriptValue* #define mSCRIPT_TYPE_C_WLIST struct mScriptValue* +#define mSCRIPT_TYPE_C_WTABLE struct mScriptValue* #define mSCRIPT_TYPE_C_W(X) struct mScriptValue* #define mSCRIPT_TYPE_C_CW(X) const struct mScriptValue* @@ -66,13 +67,14 @@ CXX_GUARD_START #define mSCRIPT_TYPE_FIELD_TABLE table #define mSCRIPT_TYPE_FIELD_WRAPPER opaque #define mSCRIPT_TYPE_FIELD_WEAKREF u32 +#define mSCRIPT_TYPE_FIELD_NUL opaque #define mSCRIPT_TYPE_FIELD_S(STRUCT) opaque #define mSCRIPT_TYPE_FIELD_CS(STRUCT) copaque -#define mSCRIPT_TYPE_FIELD_S_METHOD(STRUCT, NAME) copaque #define mSCRIPT_TYPE_FIELD_PS(STRUCT) opaque #define mSCRIPT_TYPE_FIELD_PCS(STRUCT) copaque #define mSCRIPT_TYPE_FIELD_WSTR opaque #define mSCRIPT_TYPE_FIELD_WLIST opaque +#define mSCRIPT_TYPE_FIELD_WTABLE opaque #define mSCRIPT_TYPE_FIELD_W(TYPE) opaque #define mSCRIPT_TYPE_FIELD_CW(TYPE) opaque @@ -94,13 +96,14 @@ CXX_GUARD_START #define mSCRIPT_TYPE_MS_TABLE (&mSTTable) #define mSCRIPT_TYPE_MS_WRAPPER (&mSTWrapper) #define mSCRIPT_TYPE_MS_WEAKREF (&mSTWeakref) +#define mSCRIPT_TYPE_MS_NUL mSCRIPT_TYPE_MS_VOID #define mSCRIPT_TYPE_MS_S(STRUCT) (&mSTStruct_ ## STRUCT) #define mSCRIPT_TYPE_MS_CS(STRUCT) (&mSTStructConst_ ## STRUCT) -#define mSCRIPT_TYPE_MS_S_METHOD(STRUCT, NAME) (&_mSTStructBindingType_ ## STRUCT ## _ ## NAME) #define mSCRIPT_TYPE_MS_PS(STRUCT) (&mSTStructPtr_ ## STRUCT) #define mSCRIPT_TYPE_MS_PCS(STRUCT) (&mSTStructPtrConst_ ## STRUCT) #define mSCRIPT_TYPE_MS_WSTR (&mSTStringWrapper) #define mSCRIPT_TYPE_MS_WLIST (&mSTListWrapper) +#define mSCRIPT_TYPE_MS_WTABLE (&mSTTableWrapper) #define mSCRIPT_TYPE_MS_W(TYPE) (&mSTWrapper_ ## TYPE) #define mSCRIPT_TYPE_MS_CW(TYPE) (&mSTWrapperConst_ ## TYPE) #define mSCRIPT_TYPE_MS_DS(STRUCT) (&mSTStruct_doc_ ## STRUCT) @@ -120,14 +123,16 @@ CXX_GUARD_START #define mSCRIPT_TYPE_CMP_STR(TYPE) mSCRIPT_TYPE_CMP_GENERIC(mSCRIPT_TYPE_MS_STR, TYPE) #define mSCRIPT_TYPE_CMP_CHARP(TYPE) mSCRIPT_TYPE_CMP_GENERIC(mSCRIPT_TYPE_MS_CHARP, TYPE) #define mSCRIPT_TYPE_CMP_LIST(TYPE) mSCRIPT_TYPE_CMP_GENERIC(mSCRIPT_TYPE_MS_LIST, TYPE) +#define mSCRIPT_TYPE_CMP_TABLE(TYPE) mSCRIPT_TYPE_CMP_GENERIC(mSCRIPT_TYPE_MS_TABLE, TYPE) #define mSCRIPT_TYPE_CMP_PTR(TYPE) ((TYPE)->base >= mSCRIPT_TYPE_OPAQUE) #define mSCRIPT_TYPE_CMP_WRAPPER(TYPE) (true) +#define mSCRIPT_TYPE_CMP_NUL(TYPE) mSCRIPT_TYPE_CMP_GENERIC(mSCRIPT_TYPE_MS_VOID, TYPE) #define mSCRIPT_TYPE_CMP_S(STRUCT) mSCRIPT_TYPE_MS_S(STRUCT)->name == _mSCRIPT_FIELD_NAME #define mSCRIPT_TYPE_CMP_CS(STRUCT) mSCRIPT_TYPE_MS_CS(STRUCT)->name == _mSCRIPT_FIELD_NAME -#define mSCRIPT_TYPE_CMP_S_METHOD(STRUCT, NAME) mSCRIPT_TYPE_MS_S_METHOD(STRUCT, NAME)->name == _mSCRIPT_FIELD_NAME +#define mSCRIPT_TYPE_CMP_WSTR(TYPE) (mSCRIPT_TYPE_CMP_GENERIC(mSCRIPT_TYPE_MS_WSTR, TYPE)) +#define mSCRIPT_TYPE_CMP_WLIST(TYPE) (mSCRIPT_TYPE_CMP_GENERIC(mSCRIPT_TYPE_MS_WLIST, TYPE)) +#define mSCRIPT_TYPE_CMP_WTABLE(TYPE) (mSCRIPT_TYPE_CMP_GENERIC(mSCRIPT_TYPE_MS_WTABLE, TYPE)) #define mSCRIPT_TYPE_CMP(TYPE0, TYPE1) mSCRIPT_TYPE_CMP_ ## TYPE0(TYPE1) -#define mSCRIPT_TYPE_CMP_WSTR(TYPE) (mSCRIPT_TYPE_CMP_GENERIC(mSCRIPT_TYPE_MS_STR, TYPE) || mSCRIPT_TYPE_CMP_GENERIC(mSCRIPT_TYPE_MS_CHARP, TYPE)) -#define mSCRIPT_TYPE_CMP_WLIST(TYPE) (mSCRIPT_TYPE_CMP_GENERIC(mSCRIPT_TYPE_MS_LIST, TYPE)) enum mScriptTypeBase { mSCRIPT_TYPE_VOID = 0, @@ -183,6 +188,7 @@ extern const struct mScriptType mSTWrapper; extern const struct mScriptType mSTWeakref; extern const struct mScriptType mSTStringWrapper; extern const struct mScriptType mSTListWrapper; +extern const struct mScriptType mSTTableWrapper; extern struct mScriptValue mScriptValueNull; @@ -250,10 +256,10 @@ struct mScriptTypeClass { bool internal; struct Table instanceMembers; struct Table castToMembers; + struct Table setters; struct mScriptClassMember* alloc; // TODO struct mScriptClassMember* free; struct mScriptClassMember* get; - struct mScriptClassMember* set; // TODO }; struct mScriptType { diff --git a/res/sgb-icon-128.png b/res/sgb-icon-128.png new file mode 100644 index 000000000..473adac4c Binary files /dev/null and b/res/sgb-icon-128.png differ diff --git a/res/sgb-icon-16.png b/res/sgb-icon-16.png new file mode 100644 index 000000000..6a76a156f Binary files /dev/null and b/res/sgb-icon-16.png differ diff --git a/res/sgb-icon-24.png b/res/sgb-icon-24.png new file mode 100644 index 000000000..5b8bd7a0d Binary files /dev/null and b/res/sgb-icon-24.png differ diff --git a/res/sgb-icon-256.png b/res/sgb-icon-256.png new file mode 100644 index 000000000..6e3132c57 Binary files /dev/null and b/res/sgb-icon-256.png differ diff --git a/res/sgb-icon-32.png b/res/sgb-icon-32.png new file mode 100644 index 000000000..ea28b979d Binary files /dev/null and b/res/sgb-icon-32.png differ diff --git a/res/sgb-icon-48.png b/res/sgb-icon-48.png new file mode 100644 index 000000000..3a801c3b9 Binary files /dev/null and b/res/sgb-icon-48.png differ diff --git a/res/sgb-icon-64.png b/res/sgb-icon-64.png new file mode 100644 index 000000000..1c9007dda Binary files /dev/null and b/res/sgb-icon-64.png differ diff --git a/res/sgb-icon.svg b/res/sgb-icon.svg new file mode 100644 index 000000000..ca922f1aa --- /dev/null +++ b/res/sgb-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/shaders/hq2x.shader/hq2x.fs b/res/shaders/hq2x.shader/hq2x.fs new file mode 100644 index 000000000..97019bd3c --- /dev/null +++ b/res/shaders/hq2x.shader/hq2x.fs @@ -0,0 +1,143 @@ +/* MIT License +* +* Copyright (c) 2015-2023 Lior Halphon +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ +/* Based on this (really good) article: http://blog.pkh.me/p/19-butchering-hqx-scaling-filters.html */ + +/* The colorspace used by the HQnx filters is not really YUV, despite the algorithm description claims it is. It is + also not normalized. Therefore, we shall call the colorspace used by HQnx "HQ Colorspace" to avoid confusion. */ +varying vec2 texCoord; +uniform sampler2D tex; +uniform vec2 texSize; + +vec3 rgb_to_hq_colospace(vec4 rgb) +{ + return vec3( 0.250 * rgb.r + 0.250 * rgb.g + 0.250 * rgb.b, + 0.250 * rgb.r - 0.000 * rgb.g - 0.250 * rgb.b, + -0.125 * rgb.r + 0.250 * rgb.g - 0.125 * rgb.b); +} + +bool is_different(vec4 a, vec4 b) +{ + vec3 diff = abs(rgb_to_hq_colospace(a) - rgb_to_hq_colospace(b)); + return diff.x > 0.018 || diff.y > 0.002 || diff.z > 0.005; +} + +#define P(m, r) ((pattern & (m)) == (r)) + +vec4 interp_2px(vec4 c1, float w1, vec4 c2, float w2) +{ + return (c1 * w1 + c2 * w2) / (w1 + w2); +} + +vec4 interp_3px(vec4 c1, float w1, vec4 c2, float w2, vec4 c3, float w3) +{ + return (c1 * w1 + c2 * w2 + c3 * w3) / (w1 + w2 + w3); +} + +vec4 scale(sampler2D image, vec2 position, vec2 input_resolution) +{ + // o = offset, the width of a pixel + vec2 o = vec2(1, 1) / input_resolution; + + /* We always calculate the top left pixel. If we need a different pixel, we flip the image */ + + // p = the position within a pixel [0...1] + vec2 p = fract(position * input_resolution); + + if (p.x > 0.5) o.x = -o.x; + if (p.y > 0.5) o.y = -o.y; + + vec4 w0 = texture2D(image, position + vec2( -o.x, -o.y)); + vec4 w1 = texture2D(image, position + vec2( 0, -o.y)); + vec4 w2 = texture2D(image, position + vec2( o.x, -o.y)); + vec4 w3 = texture2D(image, position + vec2( -o.x, 0)); + vec4 w4 = texture2D(image, position + vec2( 0, 0)); + vec4 w5 = texture2D(image, position + vec2( o.x, 0)); + vec4 w6 = texture2D(image, position + vec2( -o.x, o.y)); + vec4 w7 = texture2D(image, position + vec2( 0, o.y)); + vec4 w8 = texture2D(image, position + vec2( o.x, o.y)); + + int pattern = 0; + if (is_different(w0, w4)) pattern |= 1; + if (is_different(w1, w4)) pattern |= 2; + if (is_different(w2, w4)) pattern |= 4; + if (is_different(w3, w4)) pattern |= 8; + if (is_different(w5, w4)) pattern |= 16; + if (is_different(w6, w4)) pattern |= 32; + if (is_different(w7, w4)) pattern |= 64; + if (is_different(w8, w4)) pattern |= 128; + + if ((P(0xBF,0x37) || P(0xDB,0x13)) && is_different(w1, w5)) { + return interp_2px(w4, 3.0, w3, 1.0); + } + if ((P(0xDB,0x49) || P(0xEF,0x6D)) && is_different(w7, w3)) { + return interp_2px(w4, 3.0, w1, 1.0); + } + if ((P(0x0B,0x0B) || P(0xFE,0x4A) || P(0xFE,0x1A)) && is_different(w3, w1)) { + return w4; + } + if ((P(0x6F,0x2A) || P(0x5B,0x0A) || P(0xBF,0x3A) || P(0xDF,0x5A) || + P(0x9F,0x8A) || P(0xCF,0x8A) || P(0xEF,0x4E) || P(0x3F,0x0E) || + P(0xFB,0x5A) || P(0xBB,0x8A) || P(0x7F,0x5A) || P(0xAF,0x8A) || + P(0xEB,0x8A)) && is_different(w3, w1)) { + return interp_2px(w4, 3.0, w0, 1.0); + } + if (P(0x0B,0x08)) { + return interp_3px(w4, 2.0, w0, 1.0, w1, 1.0); + } + if (P(0x0B,0x02)) { + return interp_3px(w4, 2.0, w0, 1.0, w3, 1.0); + } + if (P(0x2F,0x2F)) { + return interp_3px(w4, 4.0, w3, 1.0, w1, 1.0); + } + if (P(0xBF,0x37) || P(0xDB,0x13)) { + return interp_3px(w4, 5.0, w1, 2.0, w3, 1.0); + } + if (P(0xDB,0x49) || P(0xEF,0x6D)) { + return interp_3px(w4, 5.0, w3, 2.0, w1, 1.0); + } + if (P(0x1B,0x03) || P(0x4F,0x43) || P(0x8B,0x83) || P(0x6B,0x43)) { + return interp_2px(w4, 3.0, w3, 1.0); + } + if (P(0x4B,0x09) || P(0x8B,0x89) || P(0x1F,0x19) || P(0x3B,0x19)) { + return interp_2px(w4, 3.0, w1, 1.0); + } + if (P(0x7E,0x2A) || P(0xEF,0xAB) || P(0xBF,0x8F) || P(0x7E,0x0E)) { + return interp_3px(w4, 2.0, w3, 3.0, w1, 3.0); + } + if (P(0xFB,0x6A) || P(0x6F,0x6E) || P(0x3F,0x3E) || P(0xFB,0xFA) || + P(0xDF,0xDE) || P(0xDF,0x1E)) { + return interp_2px(w4, 3.0, w0, 1.0); + } + if (P(0x0A,0x00) || P(0x4F,0x4B) || P(0x9F,0x1B) || P(0x2F,0x0B) || + P(0xBE,0x0A) || P(0xEE,0x0A) || P(0x7E,0x0A) || P(0xEB,0x4B) || + P(0x3B,0x1B)) { + return interp_3px(w4, 2.0, w3, 1.0, w1, 1.0); + } + + return interp_3px(w4, 6.0, w3, 1.0, w1, 1.0); +} + +void main() { + gl_FragColor = scale(tex, texCoord, texSize); +} diff --git a/res/shaders/hq2x.shader/manifest.ini b/res/shaders/hq2x.shader/manifest.ini new file mode 100644 index 000000000..c0ab0ef8a --- /dev/null +++ b/res/shaders/hq2x.shader/manifest.ini @@ -0,0 +1,11 @@ +[shader] +name=hq2x +author=Lior Halphon +description="High Quality" 2x scaling +passes=1 + +[pass.0] +fragmentShader=hq2x.fs +blend=0 +width=-2 +height=-2 diff --git a/res/shaders/omniscale.shader/manifest.ini b/res/shaders/omniscale.shader/manifest.ini new file mode 100644 index 000000000..a98e34ce2 --- /dev/null +++ b/res/shaders/omniscale.shader/manifest.ini @@ -0,0 +1,9 @@ +[shader] +name=OmniScale +author=Lior Halphon +description=Resolution-indepedent scaler inspired by the hqx family scalers +passes=1 + +[pass.0] +fragmentShader=omniscale.fs +blend=0 diff --git a/res/shaders/omniscale.shader/omniscale.fs b/res/shaders/omniscale.shader/omniscale.fs new file mode 100644 index 000000000..d6b71475b --- /dev/null +++ b/res/shaders/omniscale.shader/omniscale.fs @@ -0,0 +1,292 @@ +/* MIT License +* +* Copyright (c) 2015-2023 Lior Halphon +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ +/* OmniScale is derived from the pattern based design of HQnx, but with the following general differences: + - The actual output calculating was completely redesigned as resolution independent graphic generator. This allows + scaling to any factor. + - HQnx approximations that were good enough for a 2x/3x/4x factor were refined, creating smoother gradients. + - "Quarters" can be interpolated in more ways than in the HQnx filters + - If a pattern does not provide enough information to determine the suitable scaling interpolation, up to 16 pixels + per quarter are sampled (in contrast to the usual 9) in order to determine the best interpolation. + */ +varying vec2 texCoord; +uniform sampler2D tex; +uniform vec2 texSize; +uniform vec2 outputSize; + +/* We use the same colorspace as the HQ algorithms. */ +vec3 rgb_to_hq_colospace(vec4 rgb) +{ + return vec3( 0.250 * rgb.r + 0.250 * rgb.g + 0.250 * rgb.b, + 0.250 * rgb.r - 0.000 * rgb.g - 0.250 * rgb.b, + -0.125 * rgb.r + 0.250 * rgb.g - 0.125 * rgb.b); +} + + +bool is_different(vec4 a, vec4 b) +{ + vec3 diff = abs(rgb_to_hq_colospace(a) - rgb_to_hq_colospace(b)); + return diff.x > 0.018 || diff.y > 0.002 || diff.z > 0.005; +} + +#define P(m, r) ((pattern & (m)) == (r)) + +vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + // o = offset, the width of a pixel + vec2 o = vec2(1, 1) / input_resolution; + + /* We always calculate the top left quarter. If we need a different quarter, we flip our co-ordinates */ + + // p = the position within a pixel [0...1] + vec2 p = fract(position * input_resolution); + + if (p.x > 0.5) { + o.x = -o.x; + p.x = 1.0 - p.x; + } + if (p.y > 0.5) { + o.y = -o.y; + p.y = 1.0 - p.y; + } + + vec4 w0 = texture2D(image, position + vec2( -o.x, -o.y)); + vec4 w1 = texture2D(image, position + vec2( 0, -o.y)); + vec4 w2 = texture2D(image, position + vec2( o.x, -o.y)); + vec4 w3 = texture2D(image, position + vec2( -o.x, 0)); + vec4 w4 = texture2D(image, position + vec2( 0, 0)); + vec4 w5 = texture2D(image, position + vec2( o.x, 0)); + vec4 w6 = texture2D(image, position + vec2( -o.x, o.y)); + vec4 w7 = texture2D(image, position + vec2( 0, o.y)); + vec4 w8 = texture2D(image, position + vec2( o.x, o.y)); + + int pattern = 0; + if (is_different(w0, w4)) pattern |= 1 << 0; + if (is_different(w1, w4)) pattern |= 1 << 1; + if (is_different(w2, w4)) pattern |= 1 << 2; + if (is_different(w3, w4)) pattern |= 1 << 3; + if (is_different(w5, w4)) pattern |= 1 << 4; + if (is_different(w6, w4)) pattern |= 1 << 5; + if (is_different(w7, w4)) pattern |= 1 << 6; + if (is_different(w8, w4)) pattern |= 1 << 7; + + if ((P(0xBF,0x37) || P(0xDB,0x13)) && is_different(w1, w5)) { + return mix(w4, w3, 0.5 - p.x); + } + if ((P(0xDB,0x49) || P(0xEF,0x6D)) && is_different(w7, w3)) { + return mix(w4, w1, 0.5 - p.y); + } + if ((P(0x0B,0x0B) || P(0xFE,0x4A) || P(0xFE,0x1A)) && is_different(w3, w1)) { + return w4; + } + if ((P(0x6F,0x2A) || P(0x5B,0x0A) || P(0xBF,0x3A) || P(0xDF,0x5A) || + P(0x9F,0x8A) || P(0xCF,0x8A) || P(0xEF,0x4E) || P(0x3F,0x0E) || + P(0xFB,0x5A) || P(0xBB,0x8A) || P(0x7F,0x5A) || P(0xAF,0x8A) || + P(0xEB,0x8A)) && is_different(w3, w1)) { + return mix(w4, mix(w4, w0, 0.5 - p.x), 0.5 - p.y); + } + if (P(0x0B,0x08)) { + return mix(mix(w0 * 0.375 + w1 * 0.25 + w4 * 0.375, w4 * 0.5 + w1 * 0.5, p.x * 2.0), w4, p.y * 2.0); + } + if (P(0x0B,0x02)) { + return mix(mix(w0 * 0.375 + w3 * 0.25 + w4 * 0.375, w4 * 0.5 + w3 * 0.5, p.y * 2.0), w4, p.x * 2.0); + } + if (P(0x2F,0x2F)) { + float dist = length(p - vec2(0.5)); + float pixel_size = length(1.0 / (output_resolution / input_resolution)); + if (dist < 0.5 - pixel_size / 2) { + return w4; + } + vec4 r; + if (is_different(w0, w1) || is_different(w0, w3)) { + r = mix(w1, w3, p.y - p.x + 0.5); + } + else { + r = mix(mix(w1 * 0.375 + w0 * 0.25 + w3 * 0.375, w3, p.y * 2.0), w1, p.x * 2.0); + } + + if (dist > 0.5 + pixel_size / 2) { + return r; + } + return mix(w4, r, (dist - 0.5 + pixel_size / 2) / pixel_size); + } + if (P(0xBF,0x37) || P(0xDB,0x13)) { + float dist = p.x - 2.0 * p.y; + float pixel_size = length(1.0 / (output_resolution / input_resolution)) * sqrt(5.0); + if (dist > pixel_size / 2) { + return w1; + } + vec4 r = mix(w3, w4, p.x + 0.5); + if (dist < -pixel_size / 2) { + return r; + } + return mix(r, w1, (dist + pixel_size / 2) / pixel_size); + } + if (P(0xDB,0x49) || P(0xEF,0x6D)) { + float dist = p.y - 2.0 * p.x; + float pixel_size = length(1.0 / (output_resolution / input_resolution)) * sqrt(5.0); + if (p.y - 2.0 * p.x > pixel_size / 2) { + return w3; + } + vec4 r = mix(w1, w4, p.x + 0.5); + if (dist < -pixel_size / 2) { + return r; + } + return mix(r, w3, (dist + pixel_size / 2) / pixel_size); + } + if (P(0xBF,0x8F) || P(0x7E,0x0E)) { + float dist = p.x + 2.0 * p.y; + float pixel_size = length(1.0 / (output_resolution / input_resolution)) * sqrt(5.0); + + if (dist > 1.0 + pixel_size / 2) { + return w4; + } + + vec4 r; + if (is_different(w0, w1) || is_different(w0, w3)) { + r = mix(w1, w3, p.y - p.x + 0.5); + } + else { + r = mix(mix(w1 * 0.375 + w0 * 0.25 + w3 * 0.375, w3, p.y * 2.0), w1, p.x * 2.0); + } + + if (dist < 1.0 - pixel_size / 2) { + return r; + } + + return mix(r, w4, (dist + pixel_size / 2 - 1.0) / pixel_size); + } + + if (P(0x7E,0x2A) || P(0xEF,0xAB)) { + float dist = p.y + 2.0 * p.x; + float pixel_size = length(1.0 / (output_resolution / input_resolution)) * sqrt(5.0); + + if (p.y + 2.0 * p.x > 1.0 + pixel_size / 2) { + return w4; + } + + vec4 r; + + if (is_different(w0, w1) || is_different(w0, w3)) { + r = mix(w1, w3, p.y - p.x + 0.5); + } + else { + r = mix(mix(w1 * 0.375 + w0 * 0.25 + w3 * 0.375, w3, p.y * 2.0), w1, p.x * 2.0); + } + + if (dist < 1.0 - pixel_size / 2) { + return r; + } + + return mix(r, w4, (dist + pixel_size / 2 - 1.0) / pixel_size); + } + + if (P(0x1B,0x03) || P(0x4F,0x43) || P(0x8B,0x83) || P(0x6B,0x43)) { + return mix(w4, w3, 0.5 - p.x); + } + + if (P(0x4B,0x09) || P(0x8B,0x89) || P(0x1F,0x19) || P(0x3B,0x19)) { + return mix(w4, w1, 0.5 - p.y); + } + + if (P(0xFB,0x6A) || P(0x6F,0x6E) || P(0x3F,0x3E) || P(0xFB,0xFA) || + P(0xDF,0xDE) || P(0xDF,0x1E)) { + return mix(w4, w0, (1.0 - p.x - p.y) / 2.0); + } + + if (P(0x4F,0x4B) || P(0x9F,0x1B) || P(0x2F,0x0B) || + P(0xBE,0x0A) || P(0xEE,0x0A) || P(0x7E,0x0A) || P(0xEB,0x4B) || + P(0x3B,0x1B)) { + float dist = p.x + p.y; + float pixel_size = length(1.0 / (output_resolution / input_resolution)); + + if (dist > 0.5 + pixel_size / 2) { + return w4; + } + + vec4 r; + if (is_different(w0, w1) || is_different(w0, w3)) { + r = mix(w1, w3, p.y - p.x + 0.5); + } + else { + r = mix(mix(w1 * 0.375 + w0 * 0.25 + w3 * 0.375, w3, p.y * 2.0), w1, p.x * 2.0); + } + + if (dist < 0.5 - pixel_size / 2) { + return r; + } + + return mix(r, w4, (dist + pixel_size / 2 - 0.5) / pixel_size); + } + + if (P(0x0B,0x01)) { + return mix(mix(w4, w3, 0.5 - p.x), mix(w1, (w1 + w3) / 2.0, 0.5 - p.x), 0.5 - p.y); + } + + if (P(0x0B,0x00)) { + return mix(mix(w4, w3, 0.5 - p.x), mix(w1, w0, 0.5 - p.x), 0.5 - p.y); + } + + float dist = p.x + p.y; + float pixel_size = length(1.0 / (output_resolution / input_resolution)); + + if (dist > 0.5 + pixel_size / 2) { + return w4; + } + + /* We need more samples to "solve" this diagonal */ + vec4 x0 = texture2D(image, position + vec2( -o.x * 2.0, -o.y * 2.0)); + vec4 x1 = texture2D(image, position + vec2( -o.x , -o.y * 2.0)); + vec4 x2 = texture2D(image, position + vec2( 0.0 , -o.y * 2.0)); + vec4 x3 = texture2D(image, position + vec2( o.x , -o.y * 2.0)); + vec4 x4 = texture2D(image, position + vec2( -o.x * 2.0, -o.y )); + vec4 x5 = texture2D(image, position + vec2( -o.x * 2.0, 0.0 )); + vec4 x6 = texture2D(image, position + vec2( -o.x * 2.0, o.y )); + + if (is_different(x0, w4)) pattern |= 1 << 8; + if (is_different(x1, w4)) pattern |= 1 << 9; + if (is_different(x2, w4)) pattern |= 1 << 10; + if (is_different(x3, w4)) pattern |= 1 << 11; + if (is_different(x4, w4)) pattern |= 1 << 12; + if (is_different(x5, w4)) pattern |= 1 << 13; + if (is_different(x6, w4)) pattern |= 1 << 14; + + int diagonal_bias = -7; + while (pattern != 0) { + diagonal_bias += pattern & 1; + pattern >>= 1; + } + + if (diagonal_bias <= 0) { + vec4 r = mix(w1, w3, p.y - p.x + 0.5); + if (dist < 0.5 - pixel_size / 2) { + return r; + } + return mix(r, w4, (dist + pixel_size / 2 - 0.5) / pixel_size); + } + + return w4; +} + +void main() { + gl_FragColor = scale(tex, texCoord, texSize, outputSize); +} diff --git a/src/core/flags.h.in b/src/core/flags.h.in index db7f9c7c4..e10c06238 100644 --- a/src/core/flags.h.in +++ b/src/core/flags.h.in @@ -83,6 +83,10 @@ #cmakedefine USE_GDB_STUB #endif +#ifndef USE_JSON_C +#cmakedefine USE_JSON_C +#endif + #ifndef USE_LIBAV #cmakedefine USE_LIBAV #endif diff --git a/src/gba/sio.c b/src/gba/sio.c index d4df7e8da..9c47335dc 100644 --- a/src/gba/sio.c +++ b/src/gba/sio.c @@ -184,13 +184,13 @@ void GBASIOWriteSIOCNT(struct GBASIO* sio, uint16_t value) { switch (sio->mode) { case SIO_NORMAL_8: case SIO_NORMAL_32: - value |= 0x0004; + value = GBASIONormalFillSi(value); if ((value & 0x0081) == 0x0081) { - if (value & 0x4000) { + if (GBASIONormalIsIrq(value)) { // TODO: Test this on hardware to see if this is correct GBARaiseIRQ(sio->p, GBA_IRQ_SIO, 0); } - value &= ~0x0080; + value = GBASIONormalClearStart(value); } break; case SIO_MULTI: diff --git a/src/platform/libretro/libretro.c b/src/platform/libretro/libretro.c index 55a70cb2c..35c23918e 100644 --- a/src/platform/libretro/libretro.c +++ b/src/platform/libretro/libretro.c @@ -346,7 +346,7 @@ static void _doDeferredSetup(void) { // On the off-hand chance that a core actually expects its buffers to be populated when // you actually first get them, you're out of luck without workarounds. Yup, seriously. // Here's that workaround, but really the API needs to be thrown out and rewritten. - struct VFile* save = VFileFromMemory(savedata, SIZE_CART_FLASH1M); + struct VFile* save = VFileFromMemory(savedata, GBA_SIZE_FLASH1M); if (!core->loadSave(core, save)) { save->close(save); } @@ -931,8 +931,8 @@ bool retro_load_game(const struct retro_game_info* game) { core->setPeripheral(core, mPERIPH_RUMBLE, &rumble); core->setPeripheral(core, mPERIPH_ROTATION, &rotation); - savedata = anonymousMemoryMap(SIZE_CART_FLASH1M); - memset(savedata, 0xFF, SIZE_CART_FLASH1M); + savedata = anonymousMemoryMap(GBA_SIZE_FLASH1M); + memset(savedata, 0xFF, GBA_SIZE_FLASH1M); _reloadSettings(); core->loadROM(core, rom); @@ -1009,7 +1009,7 @@ void retro_unload_game(void) { core->deinit(core); mappedMemoryFree(data, dataSize); data = 0; - mappedMemoryFree(savedata, SIZE_CART_FLASH1M); + mappedMemoryFree(savedata, GBA_SIZE_FLASH1M); savedata = 0; } @@ -1164,7 +1164,7 @@ size_t retro_get_memory_size(unsigned id) { case mPLATFORM_GBA: switch (((struct GBA*) core->board)->memory.savedata.type) { case SAVEDATA_AUTODETECT: - return SIZE_CART_FLASH1M; + return GBA_SIZE_FLASH1M; default: return GBASavedataSize(&((struct GBA*) core->board)->memory.savedata); } @@ -1363,7 +1363,7 @@ static void _startImage(struct mImageSource* image, unsigned w, unsigned h, int static void _stopImage(struct mImageSource* image) { UNUSED(image); - cam.stop(); + cam.stop(); } static void _requestImage(struct mImageSource* image, const void** buffer, size_t* stride, enum mColorFormat* colorFormat) { diff --git a/src/platform/opengl/gles2.c b/src/platform/opengl/gles2.c index 4277b7b76..88ac000a5 100644 --- a/src/platform/opengl/gles2.c +++ b/src/platform/opengl/gles2.c @@ -301,7 +301,8 @@ void _drawShader(struct mGLES2Context* context, struct mGLES2Shader* shader) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, shader->filter ? GL_LINEAR : GL_NEAREST); glUseProgram(shader->program); glUniform1i(shader->texLocation, 0); - glUniform2f(shader->texSizeLocation, context->d.width - padW, context->d.height - padH); + glUniform2f(shader->texSizeLocation, context->d.width, context->d.height); + glUniform2f(shader->outputSizeLocation, drawW, drawH); #ifdef BUILD_GLES3 if (shader->vao != (GLuint) -1) { glBindVertexArray(shader->vao); @@ -532,6 +533,7 @@ void mGLES2ShaderInit(struct mGLES2Shader* shader, const char* vs, const char* f shader->texLocation = glGetUniformLocation(shader->program, "tex"); shader->texSizeLocation = glGetUniformLocation(shader->program, "texSize"); shader->positionLocation = glGetAttribLocation(shader->program, "position"); + shader->outputSizeLocation = glGetUniformLocation(shader->program, "outputSize"); size_t i; for (i = 0; i < shader->nUniforms; ++i) { shader->uniforms[i].location = glGetUniformLocation(shader->program, shader->uniforms[i].name); @@ -1010,8 +1012,11 @@ bool mGLES2ShaderLoad(struct VideoShader* shader, struct VDir* dir) { } } u = mGLES2UniformListSize(&uniformVector); - struct mGLES2Uniform* uniformBlock = calloc(u, sizeof(*uniformBlock)); - memcpy(uniformBlock, mGLES2UniformListGetPointer(&uniformVector, 0), sizeof(*uniformBlock) * u); + struct mGLES2Uniform* uniformBlock; + if (u) { + uniformBlock = calloc(u, sizeof(*uniformBlock)); + memcpy(uniformBlock, mGLES2UniformListGetPointer(&uniformVector, 0), sizeof(*uniformBlock) * u); + } mGLES2UniformListDeinit(&uniformVector); mGLES2ShaderInit(&shaderBlock[n], vssrc, fssrc, width, height, scaling, uniformBlock, u); diff --git a/src/platform/opengl/gles2.h b/src/platform/opengl/gles2.h index 144a64a13..b497b4bfa 100644 --- a/src/platform/opengl/gles2.h +++ b/src/platform/opengl/gles2.h @@ -70,6 +70,7 @@ struct mGLES2Shader { GLuint texLocation; GLuint texSizeLocation; GLuint positionLocation; + GLuint outputSizeLocation; struct mGLES2Uniform* uniforms; size_t nUniforms; diff --git a/src/platform/qt/KeyEditor.cpp b/src/platform/qt/KeyEditor.cpp index dcdf3f422..b25076f36 100644 --- a/src/platform/qt/KeyEditor.cpp +++ b/src/platform/qt/KeyEditor.cpp @@ -9,6 +9,7 @@ #include "input/GamepadButtonEvent.h" #include "InputIndex.h" #include "ShortcutController.h" +#include "utils.h" #include #include @@ -34,35 +35,7 @@ void KeyEditor::setValue(int key) { if (key < 0) { setText(tr("---")); } else { - QKeySequence seq(key); - switch (key) { -#ifndef Q_OS_MAC - case Qt::Key_Shift: - setText(QCoreApplication::translate("QShortcut", "Shift")); - break; - case Qt::Key_Control: - setText(QCoreApplication::translate("QShortcut", "Control")); - break; - case Qt::Key_Alt: - setText(QCoreApplication::translate("QShortcut", "Alt")); - break; - case Qt::Key_Meta: - setText(QCoreApplication::translate("QShortcut", "Meta")); - break; -#endif - case Qt::Key_Super_L: - setText(tr("Super (L)")); - break; - case Qt::Key_Super_R: - setText(tr("Super (R)")); - break; - case Qt::Key_Menu: - setText(tr("Menu")); - break; - default: - setText(QKeySequence(key).toString(QKeySequence::NativeText)); - break; - } + setText(keyName(key)); } } emit valueChanged(key); diff --git a/src/platform/qt/MapView.cpp b/src/platform/qt/MapView.cpp index da65abf9d..d565f2fea 100644 --- a/src/platform/qt/MapView.cpp +++ b/src/platform/qt/MapView.cpp @@ -42,7 +42,7 @@ MapView::MapView(std::shared_ptr controller, QWidget* parent) #ifdef M_CORE_GBA case mPLATFORM_GBA: m_boundary = 2048; - m_ui.tile->setMaxTile(3096); + m_ui.tile->setMaxTile(3072); m_addressBase = GBA_BASE_VRAM; m_addressWidth = 8; m_ui.bgInfo->addCustomProperty("priority", tr("Priority")); @@ -119,6 +119,9 @@ void MapView::selectMap(int map) { } m_map = map; m_mapStatus.fill({}); + // Different maps can have different max palette counts; set it to + // 0 immediately to avoid tile lookups with state palette IDs break + m_ui.tile->setPalette(0); updateTiles(true); } @@ -184,11 +187,18 @@ void MapView::updateTilesGBA(bool) { frame = GBARegisterDISPCNTGetFrameSelect(io[REG_DISPCNT >> 1]); } } + m_boundary = 1024; + m_ui.tile->setMaxTile(1536); priority = GBARegisterBGCNTGetPriority(io[(REG_BG0CNT >> 1) + m_map]); if (mode == 0 || (mode == 1 && m_map != 2)) { offset = QString("%1, %2") .arg(io[(REG_BG0HOFS >> 1) + (m_map << 1)]) .arg(io[(REG_BG0VOFS >> 1) + (m_map << 1)]); + + if (!GBARegisterBGCNTIs256Color(io[(REG_BG0CNT >> 1) + m_map])) { + m_boundary = 2048; + m_ui.tile->setMaxTile(3072); + } } else if ((mode > 0 && m_map == 2) || (mode == 2 && m_map == 3)) { int32_t refX = io[(REG_BG2X_LO >> 1) + ((m_map - 2) << 2)]; refX |= io[(REG_BG2X_HI >> 1) + ((m_map - 2) << 2)] << 16; diff --git a/src/platform/qt/MultiplayerController.cpp b/src/platform/qt/MultiplayerController.cpp index f44323603..618efd1f6 100644 --- a/src/platform/qt/MultiplayerController.cpp +++ b/src/platform/qt/MultiplayerController.cpp @@ -214,10 +214,6 @@ MultiplayerController::~MultiplayerController() { } bool MultiplayerController::attachGame(CoreController* controller) { - if (m_lockstep.attached == MAX_GBAS) { - return false; - } - if (m_lockstep.attached == 0) { switch (controller->platform()) { #ifdef M_CORE_GBA @@ -233,6 +229,9 @@ bool MultiplayerController::attachGame(CoreController* controller) { default: return false; } + m_platform = controller->platform(); + } else if (controller->platform() != m_platform) { + return false; } mCoreThread* thread = controller->thread(); @@ -243,6 +242,10 @@ bool MultiplayerController::attachGame(CoreController* controller) { switch (controller->platform()) { #ifdef M_CORE_GBA case mPLATFORM_GBA: { + if (m_lockstep.attached >= MAX_GBAS) { + return false; + } + GBA* gba = static_cast(thread->core->board); GBASIOLockstepNode* node = new GBASIOLockstepNode; @@ -259,6 +262,10 @@ bool MultiplayerController::attachGame(CoreController* controller) { #endif #ifdef M_CORE_GB case mPLATFORM_GB: { + if (m_lockstep.attached >= 2) { + return false; + } + GB* gb = static_cast(thread->core->board); GBSIOLockstepNode* node = new GBSIOLockstepNode; diff --git a/src/platform/qt/MultiplayerController.h b/src/platform/qt/MultiplayerController.h index 5ad6124db..35cae13bb 100644 --- a/src/platform/qt/MultiplayerController.h +++ b/src/platform/qt/MultiplayerController.h @@ -9,6 +9,7 @@ #include #include +#include #include #ifdef M_CORE_GBA #include @@ -77,6 +78,8 @@ private: GBASIOLockstep m_gbaLockstep; #endif }; + + mPlatform m_platform = mPLATFORM_NONE; QList m_players; QMutex m_lock; }; diff --git a/src/platform/qt/ShortcutModel.cpp b/src/platform/qt/ShortcutModel.cpp index 54fa83fcf..b73982cad 100644 --- a/src/platform/qt/ShortcutModel.cpp +++ b/src/platform/qt/ShortcutModel.cpp @@ -6,6 +6,7 @@ #include "ShortcutModel.h" #include "ShortcutController.h" +#include "utils.h" using namespace QGBA; @@ -33,7 +34,7 @@ QVariant ShortcutModel::data(const QModelIndex& index, int role) const { case 0: return m_controller->visibleName(item->name); case 1: - return shortcut ? QKeySequence(shortcut->shortcut()).toString(QKeySequence::NativeText) : QVariant(); + return shortcut ? keyName(shortcut->shortcut()) : QVariant(); case 2: if (!shortcut) { return QVariant(); @@ -134,4 +135,4 @@ void ShortcutModel::clearMenu(const QString&) { // TODO beginResetModel(); endResetModel(); -} \ No newline at end of file +} diff --git a/src/platform/qt/TileView.cpp b/src/platform/qt/TileView.cpp index 116935e29..7d8d1a71e 100644 --- a/src/platform/qt/TileView.cpp +++ b/src/platform/qt/TileView.cpp @@ -51,7 +51,7 @@ TileView::TileView(std::shared_ptr controller, QWidget* parent) #ifdef M_CORE_GBA case mPLATFORM_GBA: m_ui.tile->setBoundary(2048, 0, 2); - m_ui.tile->setMaxTile(3096); + m_ui.tile->setMaxTile(3072); break; #endif #ifdef M_CORE_GB @@ -76,7 +76,7 @@ TileView::TileView(std::shared_ptr controller, QWidget* parent) #ifdef M_CORE_GBA case mPLATFORM_GBA: m_ui.tile->setBoundary(2048 >> selected, selected, selected + 2); - m_ui.tile->setMaxTile(3096 >> selected); + m_ui.tile->setMaxTile(3072 >> selected); break; #endif #ifdef M_CORE_GB diff --git a/src/platform/qt/VideoView.cpp b/src/platform/qt/VideoView.cpp index 3548aa5d7..86700ceba 100644 --- a/src/platform/qt/VideoView.cpp +++ b/src/platform/qt/VideoView.cpp @@ -20,6 +20,7 @@ using namespace QGBA; QMap VideoView::s_acodecMap; QMap VideoView::s_vcodecMap; QMap VideoView::s_containerMap; +QMap VideoView::s_extensionMap; bool VideoView::Preset::compatible(const Preset& other) const { if (!other.container.isNull() && !container.isNull() && other.container != container) { @@ -71,6 +72,23 @@ VideoView::VideoView(QWidget* parent) if (s_containerMap.empty()) { s_containerMap["mkv"] = "matroska"; } + if (s_extensionMap.empty()) { + s_extensionMap["matroska"] += ".mkv"; + s_extensionMap["matroska"] += ".mka"; + s_extensionMap["webm"] += ".webm"; + s_extensionMap["avi"] += ".avi"; + s_extensionMap["mp4"] += ".mp4"; + s_extensionMap["mp4"] += ".m4v"; + s_extensionMap["mp4"] += ".m4a"; + + s_extensionMap["flac"] += ".flac"; + s_extensionMap["mpeg"] += ".mpg"; + s_extensionMap["mpeg"] += ".mpeg"; + s_extensionMap["mpegts"] += ".ts"; + s_extensionMap["mp3"] += ".mp3"; + s_extensionMap["ogg"] += ".ogg"; + s_extensionMap["ogv"] += ".ogv"; + } connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &VideoView::close); connect(m_ui.start, &QAbstractButton::clicked, this, &VideoView::startRecording); @@ -195,6 +213,9 @@ void VideoView::setController(std::shared_ptr controller) { } void VideoView::startRecording() { + if (QFileInfo(m_filename).suffix().isEmpty()) { + changeExtension(); + } if (!validateSettings()) { return; } @@ -242,6 +263,7 @@ void VideoView::selectFile() { QString filename = GBAApp::app()->getSaveFileName(this, tr("Select output file")); if (!filename.isEmpty()) { m_ui.filename->setText(filename); + changeExtension(); } } @@ -293,6 +315,7 @@ void VideoView::setContainer(const QString& container) { m_containerCstr = nullptr; m_container = QString(); } + changeExtension(); validateSettings(); uncheckIncompatible(); } @@ -462,6 +485,30 @@ void VideoView::uncheckIncompatible() { } } +void VideoView::changeExtension() { + if (m_filename.isEmpty()) { + return; + } + + if (!s_extensionMap.contains(m_container)) { + return; + } + + QStringList extensions = s_extensionMap.value(m_container); + QString filename = m_filename; + int index = m_filename.lastIndexOf("."); + if (index >= 0) { + if (extensions.contains(filename.mid(index))) { + // This extension is already valid + return; + } + filename.truncate(index); + } + filename += extensions.front(); + + m_ui.filename->setText(filename); +} + QString VideoView::sanitizeCodec(const QString& codec, const QMap& mapping) { QString sanitized = codec.toLower(); sanitized = sanitized.remove(QChar('.')); diff --git a/src/platform/qt/VideoView.h b/src/platform/qt/VideoView.h index 2d61cf64a..697892c93 100644 --- a/src/platform/qt/VideoView.h +++ b/src/platform/qt/VideoView.h @@ -7,6 +7,7 @@ #ifdef USE_FFMPEG +#include #include #include @@ -63,6 +64,8 @@ private slots: void uncheckIncompatible(); void updatePresets(); + void changeExtension(); + private: struct Preset { QString container; @@ -124,6 +127,7 @@ private: static QMap s_acodecMap; static QMap s_vcodecMap; static QMap s_containerMap; + static QMap s_extensionMap; }; } diff --git a/src/platform/qt/scripting/ScriptingController.cpp b/src/platform/qt/scripting/ScriptingController.cpp index b0fbd51ef..990cbca9b 100644 --- a/src/platform/qt/scripting/ScriptingController.cpp +++ b/src/platform/qt/scripting/ScriptingController.cpp @@ -20,6 +20,7 @@ #include "scripting/ScriptingTextBufferModel.h" #include +#include #include #include @@ -51,6 +52,9 @@ ScriptingController::ScriptingController(QObject* parent) m_bufferModel = new ScriptingTextBufferModel(this); QObject::connect(m_bufferModel, &ScriptingTextBufferModel::textBufferCreated, this, &ScriptingController::textBufferCreated); + connect(&m_storageFlush, &QTimer::timeout, this, &ScriptingController::flushStorage); + m_storageFlush.setInterval(5); + mScriptGamepadInit(&m_gamepad); init(); @@ -107,9 +111,11 @@ bool ScriptingController::load(VFileDevice& vf, const QString& name) { emit error(QString::fromUtf8(m_activeEngine->getError(m_activeEngine))); ok = false; } - if (m_controller && m_controller->isPaused()) { + if (m_controller) { m_controller->setSync(true); - m_controller->paused(); + if (m_controller->isPaused()) { + m_controller->paused(); + } } return ok; } @@ -144,6 +150,12 @@ void ScriptingController::runCode(const QString& code) { load(vf, "*prompt"); } +void ScriptingController::flushStorage() { +#ifdef USE_JSON_C + mScriptStorageFlushAll(&m_scriptContext); +#endif +} + bool ScriptingController::eventFilter(QObject* obj, QEvent* ev) { event(obj, ev); return false; @@ -293,6 +305,9 @@ void ScriptingController::detachGamepad() { void ScriptingController::init() { mScriptContextInit(&m_scriptContext); mScriptContextAttachStdlib(&m_scriptContext); +#ifdef USE_JSON_C + mScriptContextAttachStorage(&m_scriptContext); +#endif mScriptContextAttachSocket(&m_scriptContext); mScriptContextAttachInput(&m_scriptContext); mScriptContextRegisterEngines(&m_scriptContext); @@ -308,6 +323,10 @@ void ScriptingController::init() { if (m_engines.count() == 1) { m_activeEngine = *m_engines.begin(); } + +#ifdef USE_JSON_C + m_storageFlush.start(); +#endif } uint32_t ScriptingController::qtToScriptingKey(const QKeyEvent* event) { diff --git a/src/platform/qt/scripting/ScriptingController.h b/src/platform/qt/scripting/ScriptingController.h index 0e275561d..e34b34542 100644 --- a/src/platform/qt/scripting/ScriptingController.h +++ b/src/platform/qt/scripting/ScriptingController.h @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -55,6 +56,8 @@ public slots: void reset(); void runCode(const QString& code); + void flushStorage(); + protected: bool eventFilter(QObject*, QEvent*) override; @@ -84,6 +87,8 @@ private: std::shared_ptr m_controller; InputController* m_inputController = nullptr; + + QTimer m_storageFlush; }; } diff --git a/src/platform/qt/utils.cpp b/src/platform/qt/utils.cpp index ca0899b36..8f07cd799 100644 --- a/src/platform/qt/utils.cpp +++ b/src/platform/qt/utils.cpp @@ -6,6 +6,7 @@ #include "utils.h" #include +#include #include #include "VFileDevice.h" @@ -145,4 +146,27 @@ bool extractMatchingFile(VDir* dir, std::function filter) return false; } +QString keyName(int key) { + switch (key) { +#ifndef Q_OS_MAC + case Qt::Key_Shift: + return QCoreApplication::translate("QShortcut", "Shift"); + case Qt::Key_Control: + return QCoreApplication::translate("QShortcut", "Control"); + case Qt::Key_Alt: + return QCoreApplication::translate("QShortcut", "Alt"); + case Qt::Key_Meta: + return QCoreApplication::translate("QShortcut", "Meta"); +#endif + case Qt::Key_Super_L: + return QObject::tr("Super (L)"); + case Qt::Key_Super_R: + return QObject::tr("Super (R)"); + case Qt::Key_Menu: + return QObject::tr("Menu"); + default: + return QKeySequence(key).toString(QKeySequence::NativeText); + } +} + } diff --git a/src/platform/qt/utils.h b/src/platform/qt/utils.h index 86f669f41..cf3a53dc4 100644 --- a/src/platform/qt/utils.h +++ b/src/platform/qt/utils.h @@ -85,4 +85,6 @@ constexpr const T& clamp(const T& v, const T& lo, const T& hi) { QString romFilters(bool includeMvl = false); bool extractMatchingFile(VDir* dir, std::function filter); +QString keyName(int key); + } diff --git a/src/script/CMakeLists.txt b/src/script/CMakeLists.txt index d84659453..f741b9c05 100644 --- a/src/script/CMakeLists.txt +++ b/src/script/CMakeLists.txt @@ -10,6 +10,10 @@ set(TEST_FILES test/classes.c test/types.c) +if(USE_JSON_C) + list(APPEND SOURCE_FILES storage.c) +endif() + if(USE_LUA) list(APPEND SOURCE_FILES engines/lua.c) list(APPEND TEST_FILES @@ -17,6 +21,10 @@ if(USE_LUA) test/input.c test/lua.c test/stdlib.c) + + if(USE_JSON_C) + list(APPEND TEST_FILES test/storage.c) + endif() endif() source_group("Scripting" FILES ${SOURCE_FILES}) diff --git a/src/script/context.c b/src/script/context.c index bf5fcfa54..e49d2527b 100644 --- a/src/script/context.c +++ b/src/script/context.c @@ -17,8 +17,10 @@ struct mScriptFileInfo { }; struct mScriptCallbackInfo { + struct mScriptValue* fn; const char* callback; - size_t id; + uint32_t id; + bool oneshot; }; static void _engineContextDestroy(void* ctx) { @@ -56,13 +58,28 @@ static void _contextFindForFile(const char* key, void* value, void* user) { } } +static void _freeTable(void* data) { + struct Table* table = data; + + struct TableIterator iter; + if (TableIteratorStart(table, &iter)) { + do { + struct mScriptCallbackInfo* info = TableIteratorGetValue(table, &iter); + mScriptValueDeref(info->fn); + } while (TableIteratorNext(table, &iter)); + } + + TableDeinit(table); + free(table); +} + void mScriptContextInit(struct mScriptContext* context) { HashTableInit(&context->rootScope, 0, (void (*)(void*)) mScriptValueDeref); HashTableInit(&context->engines, 0, _engineContextDestroy); mScriptListInit(&context->refPool, 0); TableInit(&context->weakrefs, 0, (void (*)(void*)) mScriptValueDeref); context->nextWeakref = 1; - HashTableInit(&context->callbacks, 0, (void (*)(void*)) mScriptValueDeref); + HashTableInit(&context->callbacks, 0, _freeTable); TableInit(&context->callbackId, 0, free); context->nextCallbackId = 1; context->constants = NULL; @@ -84,14 +101,6 @@ void mScriptContextFillPool(struct mScriptContext* context, struct mScriptValue* if (value->refs == mSCRIPT_VALUE_UNREF) { return; } - switch (value->type->base) { - case mSCRIPT_TYPE_SINT: - case mSCRIPT_TYPE_UINT: - case mSCRIPT_TYPE_FLOAT: - return; - default: - break; - } struct mScriptValue* poolEntry = mScriptListAppend(&context->refPool); poolEntry->type = mSCRIPT_TYPE_MS_WRAPPER; @@ -212,45 +221,60 @@ void mScriptContextDisownWeakref(struct mScriptContext* context, uint32_t weakre } void mScriptContextTriggerCallback(struct mScriptContext* context, const char* callback, struct mScriptList* args) { - struct mScriptValue* list = HashTableLookup(&context->callbacks, callback); - if (!list) { + struct Table* table = HashTableLookup(&context->callbacks, callback); + if (!table) { return; } - size_t i; - for (i = 0; i < mScriptListSize(list->value.list); ++i) { + struct TableIterator iter; + if (!TableIteratorStart(table, &iter)) { + return; + } + + struct UInt32List oneshots; + UInt32ListInit(&oneshots, 0); + do { struct mScriptFrame frame; - struct mScriptValue* fn = mScriptListGetPointer(list->value.list, i); - if (!fn->type) { - continue; - } + struct mScriptCallbackInfo* info = TableIteratorGetValue(table, &iter); mScriptFrameInit(&frame); if (args) { mScriptListCopy(&frame.arguments, args); } - if (fn->type->base == mSCRIPT_TYPE_WRAPPER) { - fn = mScriptValueUnwrap(fn); - } - mScriptInvoke(fn, &frame); + mScriptInvoke(info->fn, &frame); mScriptFrameDeinit(&frame); + + if (info->oneshot) { + *UInt32ListAppend(&oneshots) = info->id; + } + } while (TableIteratorNext(table, &iter)); + + size_t i; + for (i = 0; i < UInt32ListSize(&oneshots); ++i) { + mScriptContextRemoveCallback(context, *UInt32ListGetPointer(&oneshots, i)); } + UInt32ListDeinit(&oneshots); } -uint32_t mScriptContextAddCallback(struct mScriptContext* context, const char* callback, struct mScriptValue* fn) { +static uint32_t mScriptContextAddCallbackInternal(struct mScriptContext* context, const char* callback, struct mScriptValue* fn, bool oneshot) { if (fn->type->base != mSCRIPT_TYPE_FUNCTION) { return 0; } - struct mScriptValue* list = HashTableLookup(&context->callbacks, callback); - if (!list) { - list = mScriptValueAlloc(mSCRIPT_TYPE_MS_LIST); - HashTableInsert(&context->callbacks, callback, list); + struct Table* table = HashTableLookup(&context->callbacks, callback); + if (!table) { + table = calloc(1, sizeof(*table)); + TableInit(table, 0, NULL); + HashTableInsert(&context->callbacks, callback, table); } struct mScriptCallbackInfo* info = malloc(sizeof(*info)); // Steal the string from the table key, since it's guaranteed to outlive this struct struct TableIterator iter; HashTableIteratorLookup(&context->callbacks, &iter, callback); info->callback = HashTableIteratorGetKey(&context->callbacks, &iter); - info->id = mScriptListSize(list->value.list); - mScriptValueWrap(fn, mScriptListAppend(list->value.list)); + info->oneshot = oneshot; + if (fn->type->base == mSCRIPT_TYPE_WRAPPER) { + fn = mScriptValueUnwrap(fn); + } + info->fn = fn; + mScriptValueRef(fn); while (true) { uint32_t id = context->nextCallbackId; ++context->nextCallbackId; @@ -258,8 +282,19 @@ uint32_t mScriptContextAddCallback(struct mScriptContext* context, const char* c continue; } TableInsert(&context->callbackId, id, info); - return id; + info->id = id; + break; } + TableInsert(table, info->id, info); + return info->id; +} + +uint32_t mScriptContextAddCallback(struct mScriptContext* context, const char* callback, struct mScriptValue* fn) { + return mScriptContextAddCallbackInternal(context, callback, fn, false); +} + +uint32_t mScriptContextAddOneshot(struct mScriptContext* context, const char* callback, struct mScriptValue* fn) { + return mScriptContextAddCallbackInternal(context, callback, fn, true); } void mScriptContextRemoveCallback(struct mScriptContext* context, uint32_t cbid) { @@ -267,16 +302,13 @@ void mScriptContextRemoveCallback(struct mScriptContext* context, uint32_t cbid) if (!info) { return; } - struct mScriptValue* list = HashTableLookup(&context->callbacks, info->callback); - if (!list) { + struct Table* table = HashTableLookup(&context->callbacks, info->callback); + if (!table) { return; } - if (info->id >= mScriptListSize(list->value.list)) { - return; - } - struct mScriptValue* fn = mScriptValueUnwrap(mScriptListGetPointer(list->value.list, info->id)); - mScriptValueDeref(fn); - mScriptListGetPointer(list->value.list, info->id)->type = NULL; + mScriptValueDeref(info->fn); + TableRemove(table, cbid); + TableRemove(&context->callbackId, cbid); } void mScriptContextExportConstants(struct mScriptContext* context, const char* nspace, struct mScriptKVPair* constants) { diff --git a/src/script/docgen.c b/src/script/docgen.c index 65fcc87ce..502baa705 100644 --- a/src/script/docgen.c +++ b/src/script/docgen.c @@ -9,6 +9,7 @@ #include #include #include +#include #include struct mScriptContext context; @@ -469,6 +470,7 @@ int main(int argc, char* argv[]) { mScriptContextInit(&context); mScriptContextAttachStdlib(&context); mScriptContextAttachSocket(&context); + mScriptContextAttachStorage(&context); mScriptContextAttachInput(&context); mScriptContextSetTextBufferFactory(&context, NULL, NULL); diff --git a/src/script/engines/lua.c b/src/script/engines/lua.c index bac841f1b..58c13d96f 100644 --- a/src/script/engines/lua.c +++ b/src/script/engines/lua.c @@ -36,8 +36,11 @@ static const char* _luaGetError(struct mScriptEngineContext*); static bool _luaCall(struct mScriptFrame*, void* context); +static void _freeFrame(struct mScriptList* frame); +static void _autofreeFrame(struct mScriptContext* context, struct mScriptList* frame); + struct mScriptEngineContextLua; -static bool _luaPushFrame(struct mScriptEngineContextLua*, struct mScriptList*, bool internal); +static bool _luaPushFrame(struct mScriptEngineContextLua*, struct mScriptList*); static bool _luaPopFrame(struct mScriptEngineContextLua*, struct mScriptList*); static bool _luaInvoke(struct mScriptEngineContextLua*, struct mScriptFrame*); @@ -131,7 +134,7 @@ static const char* _socketLuaSource = " end,\n" " connect = function(self, address, port)\n" " local status = self._s:connect(address, port)\n" - " return socket._wrap(status)\n" + " return self:_hook(status)\n" " end,\n" " listen = function(self, backlog)\n" " local status = self._s:listen(backlog or 1)\n" @@ -520,6 +523,8 @@ struct mScriptValue* _luaRootScope(struct mScriptEngineContext* ctx) { lua_pop(luaContext->lua, 1); key = _luaCoerce(luaContext, false); mScriptValueWrap(key, mScriptListAppend(list->value.list)); + mScriptValueRef(key); + mScriptContextFillPool(luaContext->d.context, key); } lua_pop(luaContext->lua, 1); @@ -538,11 +543,13 @@ struct mScriptValue* _luaCoerceFunction(struct mScriptEngineContextLua* luaConte return value; } -struct mScriptValue* _luaCoerceTable(struct mScriptEngineContextLua* luaContext) { +struct mScriptValue* _luaCoerceTable(struct mScriptEngineContextLua* luaContext, struct Table* markedObjects) { struct mScriptValue* table = mScriptValueAlloc(mSCRIPT_TYPE_MS_TABLE); bool isList = true; lua_pushnil(luaContext->lua); + + const void* tablePointer; while (lua_next(luaContext->lua, -2) != 0) { struct mScriptValue* value = NULL; int type = lua_type(luaContext->lua, -1); @@ -553,14 +560,20 @@ struct mScriptValue* _luaCoerceTable(struct mScriptEngineContextLua* luaContext) case LUA_TFUNCTION: value = _luaCoerce(luaContext, true); break; + case LUA_TTABLE: + tablePointer = lua_topointer(luaContext->lua, -1); + // Ensure this table doesn't contain any cycles + if (!HashTableLookupBinary(markedObjects, &tablePointer, sizeof(tablePointer))) { + HashTableInsertBinary(markedObjects, &tablePointer, sizeof(tablePointer), (void*) tablePointer); + value = _luaCoerceTable(luaContext, markedObjects); + } default: - // Don't let values be something that could contain themselves break; } if (!value) { - lua_pop(luaContext->lua, 3); + lua_pop(luaContext->lua, type == LUA_TTABLE ? 2 : 3); mScriptValueDeref(table); - return false; + return NULL; } struct mScriptValue* key = NULL; @@ -584,18 +597,13 @@ struct mScriptValue* _luaCoerceTable(struct mScriptEngineContextLua* luaContext) return false; } mScriptTableInsert(table, key, value); - if (key->type != mSCRIPT_TYPE_MS_STR) { - // Strings are added to the ref pool, so we need to keep it - // ref'd to prevent it from being collected prematurely - mScriptValueDeref(key); - } + mScriptValueDeref(key); mScriptValueDeref(value); } lua_pop(luaContext->lua, 1); size_t len = mScriptTableSize(table); if (!isList || !len) { - mScriptContextFillPool(luaContext->d.context, table); return table; } @@ -605,18 +613,15 @@ struct mScriptValue* _luaCoerceTable(struct mScriptEngineContextLua* luaContext) struct mScriptValue* value = mScriptTableLookup(table, &mSCRIPT_MAKE_S64(i)); if (!value) { mScriptValueDeref(list); - mScriptContextFillPool(luaContext->d.context, table); return table; } mScriptValueWrap(value, mScriptListAppend(list->value.list)); } if (i != len + 1) { mScriptValueDeref(list); - mScriptContextFillPool(luaContext->d.context, table); return table; } mScriptValueDeref(table); - mScriptContextFillPool(luaContext->d.context, list); return list; } @@ -628,6 +633,7 @@ struct mScriptValue* _luaCoerce(struct mScriptEngineContextLua* luaContext, bool size_t size; const void* buffer; + struct Table markedObjects; struct mScriptValue* value = NULL; switch (lua_type(luaContext->lua, -1)) { case LUA_TNIL: @@ -651,7 +657,6 @@ struct mScriptValue* _luaCoerce(struct mScriptEngineContextLua* luaContext, bool case LUA_TSTRING: buffer = lua_tolstring(luaContext->lua, -1, &size); value = mScriptStringCreateFromBytes(buffer, size); - mScriptContextFillPool(luaContext->d.context, value); break; case LUA_TFUNCTION: // This function pops the value internally via luaL_ref @@ -664,19 +669,36 @@ struct mScriptValue* _luaCoerce(struct mScriptEngineContextLua* luaContext, bool if (!pop) { break; } - return _luaCoerceTable(luaContext); + HashTableInit(&markedObjects, 0, NULL); + value = _luaCoerceTable(luaContext, &markedObjects); + HashTableDeinit(&markedObjects); + return value; case LUA_TUSERDATA: if (!lua_getmetatable(luaContext->lua, -1)) { break; } luaL_getmetatable(luaContext->lua, "mSTStruct"); if (!lua_rawequal(luaContext->lua, -1, -2)) { - lua_pop(luaContext->lua, 2); - break; + lua_pop(luaContext->lua, 1); + luaL_getmetatable(luaContext->lua, "mSTList"); + if (!lua_rawequal(luaContext->lua, -1, -2)) { + lua_pop(luaContext->lua, 1); + luaL_getmetatable(luaContext->lua, "mSTTable"); + if (!lua_rawequal(luaContext->lua, -1, -2)) { + lua_pop(luaContext->lua, 2); + break; + } + } } lua_pop(luaContext->lua, 2); value = lua_touserdata(luaContext->lua, -1); value = mScriptContextAccessWeakref(luaContext->d.context, value); + if (value->type->base == mSCRIPT_TYPE_WRAPPER) { + value = mScriptValueUnwrap(value); + } + if (value) { + mScriptValueRef(value); + } break; } if (pop) { @@ -698,6 +720,7 @@ bool _luaWrap(struct mScriptEngineContextLua* luaContext, struct mScriptValue* v lua_pushnil(luaContext->lua); return true; } + mScriptContextFillPool(luaContext->d.context, value); } struct mScriptValue derefPtr; if (value->type->base == mSCRIPT_TYPE_OPAQUE) { @@ -788,6 +811,7 @@ bool _luaWrap(struct mScriptEngineContextLua* luaContext, struct mScriptValue* v if (needsWeakref) { *newValue = mSCRIPT_MAKE(WEAKREF, weakref); } else { + mScriptValueRef(value); mScriptValueWrap(value, newValue); } lua_getfield(luaContext->lua, LUA_REGISTRYINDEX, "mSTList"); @@ -798,6 +822,7 @@ bool _luaWrap(struct mScriptEngineContextLua* luaContext, struct mScriptValue* v if (needsWeakref) { *newValue = mSCRIPT_MAKE(WEAKREF, weakref); } else { + mScriptValueRef(value); mScriptValueWrap(value, newValue); } lua_getfield(luaContext->lua, LUA_REGISTRYINDEX, "mSTTable"); @@ -809,7 +834,6 @@ bool _luaWrap(struct mScriptEngineContextLua* luaContext, struct mScriptValue* v newValue->refs = mSCRIPT_VALUE_UNREF; newValue->type->alloc(newValue); lua_pushcclosure(luaContext->lua, _luaThunk, 1); - mScriptValueDeref(value); break; case mSCRIPT_TYPE_OBJECT: if (!value->value.opaque) { @@ -820,6 +844,7 @@ bool _luaWrap(struct mScriptEngineContextLua* luaContext, struct mScriptValue* v if (needsWeakref) { *newValue = mSCRIPT_MAKE(WEAKREF, weakref); } else { + mScriptValueRef(value); mScriptValueWrap(value, newValue); } lua_getfield(luaContext->lua, LUA_REGISTRYINDEX, "mSTStruct"); @@ -920,16 +945,12 @@ const char* _luaGetError(struct mScriptEngineContext* context) { return luaContext->lastError; } -bool _luaPushFrame(struct mScriptEngineContextLua* luaContext, struct mScriptList* frame, bool internal) { +bool _luaPushFrame(struct mScriptEngineContextLua* luaContext, struct mScriptList* frame) { bool ok = true; if (frame) { size_t i; for (i = 0; i < mScriptListSize(frame); ++i) { struct mScriptValue* value = mScriptListGetPointer(frame, i); - if (internal && value->type->base == mSCRIPT_TYPE_WRAPPER) { - value = mScriptValueUnwrap(value); - mScriptContextFillPool(luaContext->d.context, value); - } if (!_luaWrap(luaContext, value)) { ok = false; break; @@ -953,8 +974,11 @@ bool _luaPopFrame(struct mScriptEngineContextLua* luaContext, struct mScriptList ok = false; break; } - mScriptValueWrap(value, mScriptListAppend(frame)); - mScriptValueDeref(value); + struct mScriptValue* tail = mScriptListAppend(frame); + mScriptValueWrap(value, tail); + if (tail->type == value->type) { + mScriptValueDeref(value); + } } if (count > i) { lua_pop(luaContext->lua, count - i); @@ -981,6 +1005,26 @@ bool _luaCall(struct mScriptFrame* frame, void* context) { return true; } +void _freeFrame(struct mScriptList* frame) { + size_t i; + for (i = 0; i < mScriptListSize(frame); ++i) { + struct mScriptValue* val = mScriptValueUnwrap(mScriptListGetPointer(frame, i)); + if (val) { + mScriptValueDeref(val); + } + } +} + +void _autofreeFrame(struct mScriptContext* context, struct mScriptList* frame) { + size_t i; + for (i = 0; i < mScriptListSize(frame); ++i) { + struct mScriptValue* val = mScriptValueUnwrap(mScriptListGetPointer(frame, i)); + if (val) { + mScriptContextFillPool(context, val); + } + } +} + bool _luaInvoke(struct mScriptEngineContextLua* luaContext, struct mScriptFrame* frame) { int nargs = 0; if (frame) { @@ -992,7 +1036,7 @@ bool _luaInvoke(struct mScriptEngineContextLua* luaContext, struct mScriptFrame* luaContext->lastError = NULL; } - if (frame && !_luaPushFrame(luaContext, &frame->arguments, false)) { + if (frame && !_luaPushFrame(luaContext, &frame->arguments)) { return false; } @@ -1057,6 +1101,7 @@ int _luaThunk(lua_State* lua) { struct mScriptFrame frame; mScriptFrameInit(&frame); if (!_luaPopFrame(luaContext, &frame.arguments)) { + _freeFrame(&frame.arguments); mScriptContextDrainPool(luaContext->d.context); mScriptFrameDeinit(&frame); luaL_traceback(lua, lua, "Error calling function (translating arguments into runtime)", 1); @@ -1064,19 +1109,21 @@ int _luaThunk(lua_State* lua) { } struct mScriptValue* fn = lua_touserdata(lua, lua_upvalueindex(1)); + _autofreeFrame(luaContext->d.context, &frame.arguments); if (!fn || !mScriptInvoke(fn, &frame)) { + mScriptContextDrainPool(luaContext->d.context); mScriptFrameDeinit(&frame); luaL_traceback(lua, lua, "Error calling function (invoking failed)", 1); return lua_error(lua); } - if (!_luaPushFrame(luaContext, &frame.returnValues, true)) { - mScriptFrameDeinit(&frame); + bool ok = _luaPushFrame(luaContext, &frame.returnValues); + mScriptContextDrainPool(luaContext->d.context); + mScriptFrameDeinit(&frame); + if (!ok) { luaL_traceback(lua, lua, "Error calling function (translating return values from runtime)", 1); return lua_error(lua); } - mScriptContextDrainPool(luaContext->d.context); - mScriptFrameDeinit(&frame); return lua_gettop(luaContext->lua); } @@ -1131,19 +1178,22 @@ int _luaSetObject(lua_State* lua) { strlcpy(key, keyPtr, sizeof(key)); lua_pop(lua, 2); - obj = mScriptContextAccessWeakref(luaContext->d.context, obj); - if (!obj) { - luaL_traceback(lua, lua, "Invalid object", 1); - return lua_error(lua); - } - if (!val) { luaL_traceback(lua, lua, "Error translating value to runtime", 1); return lua_error(lua); } + obj = mScriptContextAccessWeakref(luaContext->d.context, obj); + if (!obj) { + mScriptValueDeref(val); + mScriptContextDrainPool(luaContext->d.context); + luaL_traceback(lua, lua, "Invalid object", 1); + return lua_error(lua); + } + if (!mScriptObjectSet(obj, key, val)) { mScriptValueDeref(val); + mScriptContextDrainPool(luaContext->d.context); char error[MAX_KEY_SIZE + 16]; snprintf(error, sizeof(error), "Invalid key '%s'", key); luaL_traceback(lua, lua, "Invalid key", 1); @@ -1187,8 +1237,11 @@ int _luaGetTable(lua_State* lua) { } lua_pop(lua, 2); - obj = mScriptContextAccessWeakref(luaContext->d.context, obj); - if (!obj) { + obj = mScriptContextAccessWeakref(luaContext->d.context, obj); + if (obj->type->base == mSCRIPT_TYPE_WRAPPER) { + obj = mScriptValueUnwrap(obj); + } + if (!obj || obj->type != mSCRIPT_TYPE_MS_TABLE) { luaL_traceback(lua, lua, "Invalid table", 1); return lua_error(lua); } @@ -1220,7 +1273,10 @@ int _luaLenTable(lua_State* lua) { lua_pop(lua, 1); obj = mScriptContextAccessWeakref(luaContext->d.context, obj); - if (!obj) { + if (obj->type->base == mSCRIPT_TYPE_WRAPPER) { + obj = mScriptValueUnwrap(obj); + } + if (!obj || obj->type != mSCRIPT_TYPE_MS_TABLE) { luaL_traceback(lua, lua, "Invalid table", 1); return lua_error(lua); } @@ -1256,7 +1312,10 @@ static int _luaNextTable(lua_State* lua) { lua_pop(lua, 2); table = mScriptContextAccessWeakref(luaContext->d.context, table); - if (!table) { + if (table->type->base == mSCRIPT_TYPE_WRAPPER) { + table = mScriptValueUnwrap(table); + } + if (!table || table->type != mSCRIPT_TYPE_MS_TABLE) { luaL_traceback(lua, lua, "Invalid table", 1); return lua_error(lua); } diff --git a/src/script/socket.c b/src/script/socket.c index 818c758f6..5b2e95b60 100644 --- a/src/script/socket.c +++ b/src/script/socket.c @@ -36,6 +36,7 @@ static const struct _mScriptSocketErrorMapping { #ifndef USE_GETHOSTBYNAME #ifdef _WIN32 { WSATRY_AGAIN, mSCRIPT_SOCKERR_AGAIN }, + { WSAEWOULDBLOCK, mSCRIPT_SOCKERR_AGAIN }, { WSANO_RECOVERY, mSCRIPT_SOCKERR_FAILED }, { WSANO_DATA, mSCRIPT_SOCKERR_NO_DATA }, { WSAHOST_NOT_FOUND, mSCRIPT_SOCKERR_NOT_FOUND }, diff --git a/src/script/stdlib.c b/src/script/stdlib.c index 746d82c1d..1c65994b7 100644 --- a/src/script/stdlib.c +++ b/src/script/stdlib.c @@ -27,7 +27,14 @@ static uint32_t _mScriptCallbackAdd(struct mScriptCallbackManager* adapter, stru fn = mScriptValueUnwrap(fn); } uint32_t id = mScriptContextAddCallback(adapter->context, name->buffer, fn); - mScriptValueDeref(fn); + return id; +} + +static uint32_t _mScriptCallbackOneshot(struct mScriptCallbackManager* adapter, struct mScriptString* name, struct mScriptValue* fn) { + if (fn->type->base == mSCRIPT_TYPE_WRAPPER) { + fn = mScriptValueUnwrap(fn); + } + uint32_t id = mScriptContextAddOneshot(adapter->context, name->buffer, fn); return id; } @@ -37,6 +44,7 @@ static void _mScriptCallbackRemove(struct mScriptCallbackManager* adapter, uint3 mSCRIPT_DECLARE_STRUCT(mScriptCallbackManager); mSCRIPT_DECLARE_STRUCT_METHOD(mScriptCallbackManager, U32, add, _mScriptCallbackAdd, 2, STR, callback, WRAPPER, function); +mSCRIPT_DECLARE_STRUCT_METHOD(mScriptCallbackManager, U32, oneshot, _mScriptCallbackOneshot, 2, STR, callback, WRAPPER, function); mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptCallbackManager, remove, _mScriptCallbackRemove, 1, U32, cbid); static uint64_t mScriptMakeBitmask(struct mScriptList* list) { @@ -87,6 +95,8 @@ mSCRIPT_DEFINE_STRUCT(mScriptCallbackManager) ) mSCRIPT_DEFINE_DOCSTRING("Add a callback of the named type. The returned id can be used to remove it later") mSCRIPT_DEFINE_STRUCT_METHOD(mScriptCallbackManager, add) + mSCRIPT_DEFINE_DOCSTRING("Add a one-shot callback of the named type that will be automatically removed after called. The returned id can be used to remove it early") + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptCallbackManager, oneshot) mSCRIPT_DEFINE_DOCSTRING("Remove a callback with the previously retuned id") mSCRIPT_DEFINE_STRUCT_METHOD(mScriptCallbackManager, remove) mSCRIPT_DEFINE_END; diff --git a/src/script/storage.c b/src/script/storage.c new file mode 100644 index 000000000..b1b740162 --- /dev/null +++ b/src/script/storage.c @@ -0,0 +1,551 @@ +/* Copyright (c) 2013-2023 Jeffrey Pfau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include + +#include +#include + +#include +#include + +#define STORAGE_LEN_MAX 64 + +struct mScriptStorageBucket { + char* name; + struct mScriptValue* root; + bool autoflush; + bool dirty; +}; + +struct mScriptStorageContext { + struct Table buckets; +}; + +void mScriptStorageBucketDeinit(void*); +struct mScriptValue* mScriptStorageBucketGet(struct mScriptStorageBucket* bucket, const char* key); +static void mScriptStorageBucketSet(struct mScriptStorageBucket* bucket, const char* key, struct mScriptValue* value); +static void mScriptStorageBucketSetVoid(struct mScriptStorageBucket* bucket, const char* key, struct mScriptValue* value); +static void mScriptStorageBucketSetSInt(struct mScriptStorageBucket* bucket, const char* key, int64_t value); +static void mScriptStorageBucketSetUInt(struct mScriptStorageBucket* bucket, const char* key, uint64_t value); +static void mScriptStorageBucketSetFloat(struct mScriptStorageBucket* bucket, const char* key, double value); +static void mScriptStorageBucketSetBool(struct mScriptStorageBucket* bucket, const char* key, bool value); +static bool mScriptStorageBucketReload(struct mScriptStorageBucket* bucket); +static bool mScriptStorageBucketFlush(struct mScriptStorageBucket* bucket); +static void mScriptStorageBucketEnableAutoFlush(struct mScriptStorageBucket* bucket, bool enable); + +static void mScriptStorageContextDeinit(struct mScriptStorageContext*); +static void mScriptStorageContextFlushAll(struct mScriptStorageContext*); +struct mScriptStorageBucket* mScriptStorageGetBucket(struct mScriptStorageContext*, const char* name); + +static bool mScriptStorageToJson(struct mScriptValue* value, struct json_object** out); +static struct mScriptValue* mScriptStorageFromJson(struct json_object* json); + +mSCRIPT_DECLARE_STRUCT(mScriptStorageBucket); +mSCRIPT_DECLARE_STRUCT_METHOD(mScriptStorageBucket, WRAPPER, _get, mScriptStorageBucketGet, 1, CHARP, key); +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageBucket, setSInt, mScriptStorageBucketSetSInt, 2, CHARP, key, S64, value); +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageBucket, setUInt, mScriptStorageBucketSetUInt, 2, CHARP, key, U64, value); +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageBucket, setFloat, mScriptStorageBucketSetFloat, 2, CHARP, key, F64, value); +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageBucket, setBool, mScriptStorageBucketSetBool, 2, CHARP, key, BOOL, value); +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageBucket, setStr, mScriptStorageBucketSet, 2, CHARP, key, WSTR, value); +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageBucket, setList, mScriptStorageBucketSet, 2, CHARP, key, WLIST, value); +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageBucket, setTable, mScriptStorageBucketSet, 2, CHARP, key, WTABLE, value); +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageBucket, setVoid, mScriptStorageBucketSetVoid, 2, CHARP, key, NUL, value); +mSCRIPT_DECLARE_STRUCT_METHOD(mScriptStorageBucket, BOOL, reload, mScriptStorageBucketReload, 0); +mSCRIPT_DECLARE_STRUCT_METHOD(mScriptStorageBucket, BOOL, flush, mScriptStorageBucketFlush, 0); +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageBucket, enableAutoFlush, mScriptStorageBucketEnableAutoFlush, 1, BOOL, enable); + +mSCRIPT_DEFINE_STRUCT(mScriptStorageBucket) + mSCRIPT_DEFINE_CLASS_DOCSTRING( + "A single 'bucket' of stored data, appropriate for a single script to store its data. " + "Fields can be set directly on the bucket objct, e.g. if you want to store a value called " + "`foo` on a bucket named `bucket`, you can directly assign to it as `bucket.foo = value`, " + "and retrieve it in the same way later. Primitive types (numbers, strings, lists and tables) " + "can be stored in buckets, but complex data types (e.g. a bucket itself) cannot. Data " + "stored in a bucket is periodically flushed to disk and persists between sessions." + ) + mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(mScriptStorageBucket, setSInt) + mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(mScriptStorageBucket, setUInt) + mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(mScriptStorageBucket, setFloat) + mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(mScriptStorageBucket, setBool) + mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(mScriptStorageBucket, setStr) + mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(mScriptStorageBucket, setList) + mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(mScriptStorageBucket, setTable) + mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(mScriptStorageBucket, setVoid) + mSCRIPT_DEFINE_STRUCT_DEFAULT_GET(mScriptStorageBucket) + mSCRIPT_DEFINE_DOCSTRING("Reload the state of the bucket from disk") + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptStorageBucket, reload) + mSCRIPT_DEFINE_DOCSTRING("Flush the bucket to disk manually") + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptStorageBucket, flush) + mSCRIPT_DEFINE_DOCSTRING( + "Enable or disable the automatic flushing of this bucket. This is good for ensuring buckets " + "don't get flushed in an inconsistent state. It will also disable flushing to disk when the " + "emulator is shut down, so make sure to either manually flush the bucket or re-enable " + "automatic flushing whenever you're done updating it if you do disable it prior." + ) + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptStorageBucket, enableAutoFlush) +mSCRIPT_DEFINE_END; + +mSCRIPT_DECLARE_STRUCT(mScriptStorageContext); +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageContext, _deinit, mScriptStorageContextDeinit, 0); +mSCRIPT_DECLARE_STRUCT_METHOD(mScriptStorageContext, S(mScriptStorageBucket), getBucket, mScriptStorageGetBucket, 1, CHARP, key); +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageContext, flushAll, mScriptStorageContextFlushAll, 0); + +mSCRIPT_DEFINE_STRUCT(mScriptStorageContext) + mSCRIPT_DEFINE_STRUCT_DEINIT(mScriptStorageContext) + mSCRIPT_DEFINE_DOCSTRING( + "Get a bucket with the given name. Names can contain letters, numbers, " + "underscores and periods. If a given bucket doesn't exist, it is created." + ) + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptStorageContext, getBucket) + mSCRIPT_DEFINE_DOCSTRING("Flush all buckets to disk manually") + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptStorageContext, flushAll) +mSCRIPT_DEFINE_END; + +struct mScriptValue* mScriptStorageBucketGet(struct mScriptStorageBucket* bucket, const char* key) { + struct mScriptValue* val = mScriptTableLookup(bucket->root, &mSCRIPT_MAKE_CHARP(key)); + if (val) { + mScriptValueRef(val); + } + return val; +} + +void mScriptStorageBucketSet(struct mScriptStorageBucket* bucket, const char* key, struct mScriptValue* value) { + struct mScriptValue* vkey = mScriptStringCreateFromUTF8(key); + if (value->type->base == mSCRIPT_TYPE_WRAPPER) { + value = mScriptValueUnwrap(value); + } + mScriptTableInsert(bucket->root, vkey, value); + mScriptValueDeref(vkey); + bucket->dirty = true; +} + +void mScriptStorageBucketSetVoid(struct mScriptStorageBucket* bucket, const char* key, struct mScriptValue* value) { + UNUSED(value); + struct mScriptValue* vkey = mScriptStringCreateFromUTF8(key); + mScriptTableInsert(bucket->root, vkey, &mScriptValueNull); + mScriptValueDeref(vkey); + bucket->dirty = true; +} + +#define MAKE_SCALAR_SETTER(NAME, TYPE) \ + void mScriptStorageBucketSet ## NAME (struct mScriptStorageBucket* bucket, const char* key, mSCRIPT_TYPE_C_ ## TYPE value) { \ + struct mScriptValue* vkey = mScriptStringCreateFromUTF8(key); \ + struct mScriptValue* vval = mScriptValueAlloc(mSCRIPT_TYPE_MS_ ## TYPE); \ + vval->value.mSCRIPT_TYPE_FIELD_ ## TYPE = value; \ + mScriptTableInsert(bucket->root, vkey, vval); \ + mScriptValueDeref(vkey); \ + mScriptValueDeref(vval); \ + bucket->dirty = true; \ + } + +MAKE_SCALAR_SETTER(SInt, S64) +MAKE_SCALAR_SETTER(UInt, U64) +MAKE_SCALAR_SETTER(Float, F64) +MAKE_SCALAR_SETTER(Bool, BOOL) + +void mScriptStorageGetBucketPath(const char* bucket, char* out) { + mCoreConfigDirectory(out, PATH_MAX); + + strncat(out, PATH_SEP "storage" PATH_SEP, PATH_MAX); +#ifdef _WIN32 + // TODO: Move this to vfs somewhere + WCHAR wout[MAX_PATH]; + MultiByteToWideChar(CP_UTF8, 0, out, -1, wout, MAX_PATH); + CreateDirectoryW(wout, NULL); +#else + mkdir(out, 0755); +#endif + + char suffix[STORAGE_LEN_MAX + 6]; + snprintf(suffix, sizeof(suffix), "%s.json", bucket); + strncat(out, suffix, PATH_MAX); +} + +static struct json_object* _tableToJson(struct mScriptValue* rootVal) { + bool ok = true; + + struct TableIterator iter; + struct json_object* rootObj = json_object_new_object(); + if (mScriptTableIteratorStart(rootVal, &iter)) { + do { + struct mScriptValue* key = mScriptTableIteratorGetKey(rootVal, &iter); + struct mScriptValue* value = mScriptTableIteratorGetValue(rootVal, &iter); + const char* ckey; + if (key->type == mSCRIPT_TYPE_MS_CHARP) { + ckey = key->value.copaque; + } else if (key->type == mSCRIPT_TYPE_MS_STR) { + ckey = key->value.string->buffer; + } else { + ok = false; + break; + } + + struct json_object* obj; + ok = mScriptStorageToJson(value, &obj); + + if (ok) { +#if JSON_C_VERSION_NUM >= (13 << 8) + ok = json_object_object_add(rootObj, ckey, obj) >= 0; +#else + json_object_object_add(rootObj, ckey, obj); +#endif + } + } while (mScriptTableIteratorNext(rootVal, &iter) && ok); + } + if (!ok) { + json_object_put(rootObj); + return NULL; + } + return rootObj; +} + +bool mScriptStorageToJson(struct mScriptValue* value, struct json_object** out) { + if (value->type->base == mSCRIPT_TYPE_WRAPPER) { + value = mScriptValueUnwrap(value); + } + + size_t i; + bool ok = true; + struct json_object* obj = NULL; + switch (value->type->base) { + case mSCRIPT_TYPE_SINT: + obj = json_object_new_int64(value->value.s64); + break; + case mSCRIPT_TYPE_UINT: + if (value->type == mSCRIPT_TYPE_MS_BOOL) { + obj = json_object_new_boolean(value->value.u32); + break; + } +#if JSON_C_VERSION_NUM >= (14 << 8) + obj = json_object_new_uint64(value->value.u64); +#else + if (value->value.u64 < (uint64_t) INT64_MAX) { + obj = json_object_new_int64(value->value.u64); + } else { + obj = json_object_new_double(value->value.u64); + } +#endif + break; + case mSCRIPT_TYPE_FLOAT: + obj = json_object_new_double(value->value.f64); + break; + case mSCRIPT_TYPE_STRING: + obj = json_object_new_string_len(value->value.string->buffer, value->value.string->size); + break; + case mSCRIPT_TYPE_LIST: +#if JSON_C_VERSION_NUM >= (15 << 8) + obj = json_object_new_array_ext(mScriptListSize(value->value.list)); +#else + obj = json_object_new_array(); +#endif + for (i = 0; i < mScriptListSize(value->value.list); ++i) { + struct json_object* listObj; + ok = mScriptStorageToJson(mScriptListGetPointer(value->value.list, i), &listObj); + if (!ok) { + break; + } + json_object_array_add(obj, listObj); + } + break; + case mSCRIPT_TYPE_TABLE: + obj = _tableToJson(value); + if (!obj) { + ok = false; + } + break; + case mSCRIPT_TYPE_VOID: + obj = NULL; + break; + default: + ok = false; + break; + } + + if (!ok) { + if (obj) { + json_object_put(obj); + } + *out = NULL; + } else { + *out = obj; + } + return ok; +} + +#ifndef JSON_C_TO_STRING_PRETTY_TAB +#define JSON_C_TO_STRING_PRETTY_TAB 0 +#endif + +static bool _mScriptStorageBucketFlushVF(struct mScriptStorageBucket* bucket, struct VFile* vf) { + struct json_object* rootObj; + bool ok = mScriptStorageToJson(bucket->root, &rootObj); + if (!ok) { + vf->close(vf); + return false; + } + + const char* json = json_object_to_json_string_ext(rootObj, JSON_C_TO_STRING_PRETTY | JSON_C_TO_STRING_PRETTY_TAB); + if (!json) { + json_object_put(rootObj); + vf->close(vf); + return false; + } + + vf->write(vf, json, strlen(json)); + vf->close(vf); + + bucket->dirty = false; + + json_object_put(rootObj); + return true; +} + +bool mScriptStorageBucketFlush(struct mScriptStorageBucket* bucket) { + char path[PATH_MAX]; + mScriptStorageGetBucketPath(bucket->name, path); + struct VFile* vf = VFileOpen(path, O_WRONLY | O_CREAT | O_TRUNC); + return _mScriptStorageBucketFlushVF(bucket, vf); +} + +void mScriptStorageBucketEnableAutoFlush(struct mScriptStorageBucket* bucket, bool enable) { + bucket->autoflush = enable; +} + +bool mScriptStorageSaveBucketVF(struct mScriptContext* context, const char* bucketName, struct VFile* vf) { + struct mScriptValue* value = mScriptContextGetGlobal(context, "storage"); + if (!value) { + vf->close(vf); + return false; + } + struct mScriptStorageContext* storage = value->value.opaque; + struct mScriptStorageBucket* bucket = mScriptStorageGetBucket(storage, bucketName); + return _mScriptStorageBucketFlushVF(bucket, vf); +} + +bool mScriptStorageSaveBucket(struct mScriptContext* context, const char* bucketName) { + char path[PATH_MAX]; + mScriptStorageGetBucketPath(bucketName, path); + struct VFile* vf = VFileOpen(path, O_WRONLY | O_CREAT | O_TRUNC); + return mScriptStorageSaveBucketVF(context, bucketName, vf); +} + +struct mScriptValue* mScriptStorageFromJson(struct json_object* json) { + enum json_type type = json_object_get_type(json); + struct mScriptValue* value = NULL; + switch (type) { + case json_type_null: + return &mScriptValueNull; + case json_type_int: + value = mScriptValueAlloc(mSCRIPT_TYPE_MS_S64); + value->value.s64 = json_object_get_int64(json); + break; + case json_type_double: + value = mScriptValueAlloc(mSCRIPT_TYPE_MS_F64); + value->value.f64 = json_object_get_double(json); + break; + case json_type_boolean: + value = mScriptValueAlloc(mSCRIPT_TYPE_MS_BOOL); + value->value.u32 = json_object_get_boolean(json); + break; + case json_type_string: + value = mScriptStringCreateFromBytes(json_object_get_string(json), json_object_get_string_len(json)); + break; + case json_type_array: + value = mScriptValueAlloc(mSCRIPT_TYPE_MS_LIST); + { + size_t i; + for (i = 0; i < json_object_array_length(json); ++i) { + struct mScriptValue* vval = mScriptStorageFromJson(json_object_array_get_idx(json, i)); + if (!vval) { + mScriptValueDeref(value); + value = NULL; + break; + } + mScriptValueWrap(vval, mScriptListAppend(value->value.list)); + mScriptValueDeref(vval); + } + } + break; + case json_type_object: + value = mScriptValueAlloc(mSCRIPT_TYPE_MS_TABLE); + { + json_object_object_foreach(json, jkey, jval) { + struct mScriptValue* vval = mScriptStorageFromJson(jval); + if (!vval) { + mScriptValueDeref(value); + value = NULL; + break; + } + struct mScriptValue* vkey = mScriptStringCreateFromUTF8(jkey); + mScriptTableInsert(value, vkey, vval); + mScriptValueDeref(vkey); + mScriptValueDeref(vval); + } + } + break; + } + return value; +} + +static struct mScriptValue* _mScriptStorageLoadJson(struct VFile* vf) { + ssize_t size = vf->size(vf); + if (size < 2) { + vf->close(vf); + return NULL; + } + char* json = calloc(1, size + 1); + if (vf->read(vf, json, size) != size) { + vf->close(vf); + return NULL; + } + vf->close(vf); + + struct json_object* obj = json_tokener_parse(json); + free(json); + if (!obj) { + return NULL; + } + + struct mScriptValue* root = mScriptStorageFromJson(obj); + json_object_put(obj); + return root; +} + +bool mScriptStorageBucketReload(struct mScriptStorageBucket* bucket) { + char path[PATH_MAX]; + mScriptStorageGetBucketPath(bucket->name, path); + struct VFile* vf = VFileOpen(path, O_RDONLY); + if (!vf) { + return false; + } + struct mScriptValue* root = _mScriptStorageLoadJson(vf); + if (!root) { + return false; + } + if (bucket->root) { + mScriptValueDeref(bucket->root); + } + bucket->root = root; + + bucket->dirty = false; + + return true; +} + +bool mScriptStorageLoadBucketVF(struct mScriptContext* context, const char* bucketName, struct VFile* vf) { + struct mScriptValue* value = mScriptContextGetGlobal(context, "storage"); + if (!value) { + vf->close(vf); + return false; + } + struct mScriptStorageContext* storage = value->value.opaque; + struct mScriptValue* root = _mScriptStorageLoadJson(vf); + if (!root) { + return false; + } + struct mScriptStorageBucket* bucket = mScriptStorageGetBucket(storage, bucketName); + mScriptValueDeref(bucket->root); + bucket->root = root; + + bucket->dirty = false; + + return true; +} + +bool mScriptStorageLoadBucket(struct mScriptContext* context, const char* bucketName) { + char path[PATH_MAX]; + mScriptStorageGetBucketPath(bucketName, path); + struct VFile* vf = VFileOpen(path, O_RDONLY); + if (!vf) { + return false; + } + return mScriptStorageLoadBucketVF(context, bucketName, vf); +} + +void mScriptContextAttachStorage(struct mScriptContext* context) { + struct mScriptStorageContext* storage = calloc(1, sizeof(*storage)); + struct mScriptValue* value = mScriptValueAlloc(mSCRIPT_TYPE_MS_S(mScriptStorageContext)); + value->flags = mSCRIPT_VALUE_FLAG_FREE_BUFFER; + value->value.opaque = storage; + + HashTableInit(&storage->buckets, 0, mScriptStorageBucketDeinit); + + mScriptContextSetGlobal(context, "storage", value); + mScriptContextSetDocstring(context, "storage", "Singleton instance of struct::mScriptStorageContext"); +} + +void mScriptStorageFlushAll(struct mScriptContext* context) { + struct mScriptValue* value = mScriptContextGetGlobal(context, "storage"); + if (!value) { + return; + } + struct mScriptStorageContext* storage = value->value.opaque; + mScriptStorageContextFlushAll(storage); +} + +void mScriptStorageContextDeinit(struct mScriptStorageContext* storage) { + HashTableDeinit(&storage->buckets); +} + +void mScriptStorageContextFlushAll(struct mScriptStorageContext* storage) { + struct TableIterator iter; + if (HashTableIteratorStart(&storage->buckets, &iter)) { + do { + struct mScriptStorageBucket* bucket = HashTableIteratorGetValue(&storage->buckets, &iter); + if (bucket->autoflush) { + mScriptStorageBucketFlush(bucket); + } + } while (HashTableIteratorNext(&storage->buckets, &iter)); + } +} + +struct mScriptStorageBucket* mScriptStorageGetBucket(struct mScriptStorageContext* storage, const char* name) { + if (!name) { + return NULL; + } + + // Check if name is allowed + // Currently only names matching /[0-9A-Za-z][0-9A-Za-z_.]*/ are allowed + size_t i; + for (i = 0; name[i]; ++i) { + if (i >= STORAGE_LEN_MAX) { + return NULL; + } + if (isalnum(name[i])) { + continue; + } + if (name[i] == '_') { + continue; + } + if (i > 0 && name[i] == '.') { + continue; + } + return NULL; + } + struct mScriptStorageBucket* bucket = HashTableLookup(&storage->buckets, name); + if (bucket) { + return bucket; + } + + bucket = calloc(1, sizeof(*bucket)); + bucket->name = strdup(name); + bucket->autoflush = true; + if (!mScriptStorageBucketReload(bucket)) { + bucket->root = mScriptValueAlloc(mSCRIPT_TYPE_MS_TABLE); + } + HashTableInsert(&storage->buckets, name, bucket); + return bucket; +} + +void mScriptStorageBucketDeinit(void* data) { + struct mScriptStorageBucket* bucket = data; + if (bucket->dirty) { + mScriptStorageBucketFlush(bucket); + } + mScriptValueDeref(bucket->root); + free(bucket->name); + free(bucket); +} diff --git a/src/script/test/classes.c b/src/script/test/classes.c index 85afb627c..f4dec7722 100644 --- a/src/script/test/classes.c +++ b/src/script/test/classes.c @@ -46,6 +46,14 @@ struct TestF { int* ref; }; +struct TestG { + const char* name; + int64_t s; + uint64_t u; + double f; + const char* c; +}; + static int32_t testAi0(struct TestA* a) { return a->i; } @@ -83,6 +91,26 @@ static void testDeinit(struct TestF* f) { ++*f->ref; } +static void testSetS(struct TestG* g, const char* name, int64_t val) { + g->name = name; + g->s = val; +} + +static void testSetU(struct TestG* g, const char* name, uint64_t val) { + g->name = name; + g->u = val; +} + +static void testSetF(struct TestG* g, const char* name, double val) { + g->name = name; + g->f = val; +} + +static void testSetC(struct TestG* g, const char* name, const char* val) { + g->name = name; + g->c = val; +} + #define MEMBER_A_DOCSTRING "Member a" mSCRIPT_DECLARE_STRUCT(TestA); @@ -157,6 +185,19 @@ mSCRIPT_DEFINE_STRUCT(TestF) mSCRIPT_DEFINE_STRUCT_DEINIT(TestF) mSCRIPT_DEFINE_END; +mSCRIPT_DECLARE_STRUCT(TestG); +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(TestG, setS, testSetS, 2, CHARP, name, S64, value); +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(TestG, setU, testSetU, 2, CHARP, name, U64, value); +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(TestG, setF, testSetF, 2, CHARP, name, F64, value); +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(TestG, setC, testSetC, 2, CHARP, name, CHARP, value); + +mSCRIPT_DEFINE_STRUCT(TestG) + mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(TestG, setS) + mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(TestG, setU) + mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(TestG, setF) + mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(TestG, setC) +mSCRIPT_DEFINE_END; + M_TEST_DEFINE(testALayout) { struct mScriptTypeClass* cls = mSCRIPT_TYPE_MS_S(TestA)->details.cls; assert_false(cls->init); @@ -1032,6 +1073,67 @@ M_TEST_DEFINE(testFDeinit) { assert_false(cls->init); } +M_TEST_DEFINE(testGSet) { + struct mScriptTypeClass* cls = mSCRIPT_TYPE_MS_S(TestG)->details.cls; + + struct TestG s = { + }; + + assert_int_equal(s.s, 0); + assert_int_equal(s.u, 0); + assert_float_equal(s.f, 0, 0); + assert_null(s.c); + + struct mScriptValue sval = mSCRIPT_MAKE_S(TestG, &s); + struct mScriptValue val; + struct mScriptValue* pval; + + val = mSCRIPT_MAKE_S64(1); + assert_true(mScriptObjectSet(&sval, "a", &val)); + assert_int_equal(s.s, 1); + assert_string_equal(s.name, "a"); + + val = mSCRIPT_MAKE_U64(2); + assert_true(mScriptObjectSet(&sval, "b", &val)); + assert_int_equal(s.u, 2); + assert_string_equal(s.name, "b"); + + val = mSCRIPT_MAKE_F64(1.5); + assert_true(mScriptObjectSet(&sval, "c", &val)); + assert_float_equal(s.f, 1.5, 0); + assert_string_equal(s.name, "c"); + + val = mSCRIPT_MAKE_CHARP("hello"); + assert_true(mScriptObjectSet(&sval, "d", &val)); + assert_string_equal(s.c, "hello"); + assert_string_equal(s.name, "d"); + + val = mSCRIPT_MAKE_S32(3); + assert_true(mScriptObjectSet(&sval, "a", &val)); + assert_int_equal(s.s, 3); + + val = mSCRIPT_MAKE_S16(4); + assert_true(mScriptObjectSet(&sval, "a", &val)); + assert_int_equal(s.s, 4); + + val = mSCRIPT_MAKE_S8(5); + assert_true(mScriptObjectSet(&sval, "a", &val)); + assert_int_equal(s.s, 5); + + val = mSCRIPT_MAKE_BOOL(false); + assert_true(mScriptObjectSet(&sval, "a", &val)); + assert_int_equal(s.u, 0); + + pval = mScriptStringCreateFromASCII("goodbye"); + assert_true(mScriptObjectSet(&sval, "a", pval)); + assert_string_equal(s.c, "goodbye"); + mScriptValueDeref(pval); + + assert_true(cls->init); + mScriptClassDeinit(cls); + assert_false(cls->init); +} + M_TEST_SUITE_DEFINE(mScriptClasses, cmocka_unit_test(testALayout), cmocka_unit_test(testASignatures), @@ -1047,4 +1149,5 @@ M_TEST_SUITE_DEFINE(mScriptClasses, cmocka_unit_test(testDSet), cmocka_unit_test(testEGet), cmocka_unit_test(testFDeinit), + cmocka_unit_test(testGSet), ) diff --git a/src/script/test/lua.c b/src/script/test/lua.c index 5731b0705..49c5521d7 100644 --- a/src/script/test/lua.c +++ b/src/script/test/lua.c @@ -66,9 +66,14 @@ static int32_t sum(struct mScriptList* list) { return sum; } +static unsigned tableSize(struct Table* table) { + return TableSize(table); +} + mSCRIPT_BIND_FUNCTION(boundIdentityInt, S32, identityInt, 1, S32, a); mSCRIPT_BIND_FUNCTION(boundAddInts, S32, addInts, 2, S32, a, S32, b); mSCRIPT_BIND_FUNCTION(boundSum, S32, sum, 1, LIST, list); +mSCRIPT_BIND_FUNCTION(boundTableSize, U32, tableSize, 1, TABLE, table); mSCRIPT_DECLARE_STRUCT(Test); mSCRIPT_DECLARE_STRUCT_D_METHOD(Test, S32, ifn0, 0); @@ -371,13 +376,51 @@ M_TEST_DEFINE(callCFunc) { assert_true(a.type->equal(&a, val)); mScriptValueDeref(val); + LOAD_PROGRAM("b('a')"); + assert_false(lua->run(lua)); + mScriptContextDeinit(&context); } + +M_TEST_DEFINE(callCTable) { + SETUP_LUA; + + assert_true(lua->setGlobal(lua, "b", &boundTableSize)); + + TEST_PROGRAM("assert(b({}) == 0)"); + assert_null(lua->getError(lua)); + + TEST_PROGRAM("assert(b({[2]=1}) == 1)"); + assert_null(lua->getError(lua)); + + TEST_PROGRAM("assert(b({a=1}) == 1)"); + assert_null(lua->getError(lua)); + + TEST_PROGRAM("assert(b({a={}}) == 1)"); + assert_null(lua->getError(lua)); + + LOAD_PROGRAM( + "a = {}\n" + "a.b = a\n" + "assert(b(a) == 1)\n" + ); + assert_false(lua->run(lua)); + + LOAD_PROGRAM( + "a = {}\n" + "a.b = {}\n" + "a.b.c = a\n" + "assert(b(a) == 1)\n" + ); + assert_false(lua->run(lua)); + + mScriptContextDeinit(&context); +} + M_TEST_DEFINE(globalNull) { SETUP_LUA; struct Test s = {}; - struct mScriptValue* val; struct mScriptValue a; LOAD_PROGRAM("assert(a)"); @@ -764,6 +807,48 @@ M_TEST_DEFINE(linkedList) { mScriptContextDeinit(&context); } +M_TEST_DEFINE(listConvert) { + SETUP_LUA; + + struct mScriptValue* list = mScriptValueAlloc(mSCRIPT_TYPE_MS_LIST); + + assert_true(lua->setGlobal(lua, "l", list)); + TEST_PROGRAM("assert(l)"); + + struct mScriptValue* val = lua->getGlobal(lua, "l"); + assert_non_null(val); + if (val->type->base == mSCRIPT_TYPE_WRAPPER) { + val = mScriptValueUnwrap(val); + } + assert_ptr_equal(val->type, mSCRIPT_TYPE_MS_LIST); + assert_ptr_equal(val->value.list, list->value.list); + mScriptValueDeref(val); + mScriptValueDeref(list); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(tableConvert) { + SETUP_LUA; + + struct mScriptValue* list = mScriptValueAlloc(mSCRIPT_TYPE_MS_TABLE); + + assert_true(lua->setGlobal(lua, "l", list)); + TEST_PROGRAM("assert(l)"); + + struct mScriptValue* val = lua->getGlobal(lua, "l"); + assert_non_null(val); + if (val->type->base == mSCRIPT_TYPE_WRAPPER) { + val = mScriptValueUnwrap(val); + } + assert_ptr_equal(val->type, mSCRIPT_TYPE_MS_TABLE); + assert_ptr_equal(val->value.table, list->value.table); + mScriptValueDeref(val); + mScriptValueDeref(list); + + mScriptContextDeinit(&context); +} + M_TEST_SUITE_DEFINE_SETUP_TEARDOWN(mScriptLua, cmocka_unit_test(create), cmocka_unit_test(loadGood), @@ -774,6 +859,7 @@ M_TEST_SUITE_DEFINE_SETUP_TEARDOWN(mScriptLua, cmocka_unit_test(rootScope), cmocka_unit_test(callLuaFunc), cmocka_unit_test(callCFunc), + cmocka_unit_test(callCTable), cmocka_unit_test(globalNull), cmocka_unit_test(globalStructFieldGet), cmocka_unit_test(globalStructFieldSet), @@ -782,5 +868,7 @@ M_TEST_SUITE_DEFINE_SETUP_TEARDOWN(mScriptLua, cmocka_unit_test(tableLookup), cmocka_unit_test(tableIterate), cmocka_unit_test(callList), - cmocka_unit_test(linkedList) + cmocka_unit_test(linkedList), + cmocka_unit_test(listConvert), + cmocka_unit_test(tableConvert), ) diff --git a/src/script/test/stdlib.c b/src/script/test/stdlib.c index 82a4c425b..0f821dbb4 100644 --- a/src/script/test/stdlib.c +++ b/src/script/test/stdlib.c @@ -96,8 +96,41 @@ M_TEST_DEFINE(callbacks) { mScriptContextDeinit(&context); } +M_TEST_DEFINE(oneshot) { + SETUP_LUA; + + TEST_PROGRAM( + "val = 0\n" + "function cb()\n" + " val = val + 1\n" + "end\n" + "id = callbacks:oneshot('test', cb)\n" + "assert(id)" + ); + + TEST_VALUE(S32, "val", 0); + + mScriptContextTriggerCallback(&context, "test", NULL); + TEST_VALUE(S32, "val", 1); + + mScriptContextTriggerCallback(&context, "test", NULL); + TEST_VALUE(S32, "val", 1); + + TEST_PROGRAM( + "id = callbacks:oneshot('test', cb)\n" + "assert(id)\n" + "callbacks:remove(id)" + ); + + mScriptContextTriggerCallback(&context, "test", NULL); + TEST_VALUE(S32, "val", 1); + + mScriptContextDeinit(&context); +} + M_TEST_SUITE_DEFINE_SETUP_TEARDOWN(mScriptStdlib, cmocka_unit_test(bitMask), cmocka_unit_test(bitUnmask), cmocka_unit_test(callbacks), + cmocka_unit_test(oneshot), ) diff --git a/src/script/test/storage.c b/src/script/test/storage.c new file mode 100644 index 000000000..e9bb535ba --- /dev/null +++ b/src/script/test/storage.c @@ -0,0 +1,585 @@ +/* Copyright (c) 2013-2023 Jeffrey Pfau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "util/test/suite.h" + +#include +#include +#include + +#include "script/test.h" + +#define SETUP_LUA \ + struct mScriptContext context; \ + mScriptContextInit(&context); \ + struct mScriptEngineContext* lua = mScriptContextRegisterEngine(&context, mSCRIPT_ENGINE_LUA); \ + mScriptContextAttachStdlib(&context); \ + mScriptContextAttachStorage(&context); \ + char bucketPath[PATH_MAX]; \ + mScriptStorageGetBucketPath("xtest", bucketPath); \ + remove(bucketPath) + +M_TEST_SUITE_SETUP(mScriptStorage) { + if (mSCRIPT_ENGINE_LUA->init) { + mSCRIPT_ENGINE_LUA->init(mSCRIPT_ENGINE_LUA); + } + return 0; +} + +M_TEST_SUITE_TEARDOWN(mScriptStorage) { + if (mSCRIPT_ENGINE_LUA->deinit) { + mSCRIPT_ENGINE_LUA->deinit(mSCRIPT_ENGINE_LUA); + } + return 0; +} + +M_TEST_DEFINE(basicInt) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + TEST_PROGRAM("assert(not bucket.a)"); + TEST_PROGRAM("bucket.a = 1"); + TEST_PROGRAM("assert(bucket.a == 1)"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(basicFloat) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + TEST_PROGRAM("assert(not bucket.a)"); + TEST_PROGRAM("bucket.a = 0.5"); + TEST_PROGRAM("assert(bucket.a == 0.5)"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(basicBool) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + TEST_PROGRAM("assert(not bucket.a)"); + TEST_PROGRAM("bucket.a = true"); + TEST_PROGRAM("assert(bucket.a == true)"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(basicNil) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + TEST_PROGRAM("assert(not bucket.a)"); + TEST_PROGRAM("bucket.a = nil"); + TEST_PROGRAM("assert(bucket.a == nil)"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(basicString) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + TEST_PROGRAM("assert(not bucket.a)"); + TEST_PROGRAM("bucket.a = 'hello'"); + TEST_PROGRAM("assert(bucket.a == 'hello')"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(basicList) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + TEST_PROGRAM("assert(not bucket.a)"); + TEST_PROGRAM("bucket.a = {1}"); + TEST_PROGRAM("assert(#bucket.a == 1)"); + TEST_PROGRAM("assert(bucket.a[1] == 1)"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(basicTable) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + TEST_PROGRAM("assert(not bucket.a)"); + TEST_PROGRAM("bucket.a = {['a']=1}"); + TEST_PROGRAM("assert(#bucket.a == 1)"); + TEST_PROGRAM("assert(bucket.a.a == 1)"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(nullByteString) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + TEST_PROGRAM("assert(not bucket.a)"); + TEST_PROGRAM("bucket.a = 'a\\x00b'"); + TEST_PROGRAM("assert(bucket.a == 'a\\x00b')"); + TEST_PROGRAM("assert(#bucket.a == 3)"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(structured) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + TEST_PROGRAM("assert(not bucket.a)"); + TEST_PROGRAM( + "bucket.a = {\n" + " ['a'] = 1,\n" + " ['b'] = {1},\n" + " ['c'] = {\n" + " ['d'] = 1\n" + " }\n" + "}" + ); + TEST_PROGRAM("assert(bucket.a)"); + TEST_PROGRAM("assert(bucket.a.a == 1)"); + TEST_PROGRAM("assert(#bucket.a.b == 1)"); + TEST_PROGRAM("assert(bucket.a.b[1] == 1)"); + TEST_PROGRAM("assert(#bucket.a.c == 1)"); + TEST_PROGRAM("assert(bucket.a.c.d == 1)"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(invalidObject) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + TEST_PROGRAM("assert(not bucket.a)"); + LOAD_PROGRAM("bucket.a = bucket"); + assert_false(lua->run(lua)); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(serializeInt) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + + TEST_PROGRAM("bucket.a = 1"); + struct VFile* vf = VFileOpen("test.json", O_CREAT | O_TRUNC | O_WRONLY); + assert_true(mScriptStorageSaveBucketVF(&context, "xtest", vf)); + vf = VFileOpen("test.json", O_RDONLY); + assert_non_null(vf); + ssize_t size = vf->size(vf); + char* buf = calloc(1, size + 1); + assert_int_equal(vf->read(vf, buf, size), size); + assert_string_equal(buf, "{\n\t\"a\":1\n}"); + free(buf); + vf->close(vf); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(serializeFloat) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + + TEST_PROGRAM("bucket.a = 0.5"); + struct VFile* vf = VFileOpen("test.json", O_CREAT | O_TRUNC | O_WRONLY); + assert_true(mScriptStorageSaveBucketVF(&context, "xtest", vf)); + vf = VFileOpen("test.json", O_RDONLY); + assert_non_null(vf); + ssize_t size = vf->size(vf); + char* buf = calloc(1, size + 1); + assert_int_equal(vf->read(vf, buf, size), size); + assert_string_equal(buf, "{\n\t\"a\":0.5\n}"); + free(buf); + vf->close(vf); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(serializeBool) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + + TEST_PROGRAM("bucket.a = true"); + struct VFile* vf = VFileOpen("test.json", O_CREAT | O_TRUNC | O_WRONLY); + assert_true(mScriptStorageSaveBucketVF(&context, "xtest", vf)); + vf = VFileOpen("test.json", O_RDONLY); + assert_non_null(vf); + ssize_t size = vf->size(vf); + char* buf = calloc(1, size + 1); + assert_int_equal(vf->read(vf, buf, size), size); + assert_string_equal(buf, "{\n\t\"a\":true\n}"); + free(buf); + vf->close(vf); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(serializeNil) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + + TEST_PROGRAM("bucket.a = nil"); + struct VFile* vf = VFileOpen("test.json", O_CREAT | O_TRUNC | O_WRONLY); + assert_true(mScriptStorageSaveBucketVF(&context, "xtest", vf)); + vf = VFileOpen("test.json", O_RDONLY); + assert_non_null(vf); + ssize_t size = vf->size(vf); + char* buf = calloc(1, size + 1); + assert_int_equal(vf->read(vf, buf, size), size); + assert_string_equal(buf, "{\n\t\"a\":null\n}"); + free(buf); + vf->close(vf); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(serializeString) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + + TEST_PROGRAM("bucket.a = 'hello'"); + struct VFile* vf = VFileOpen("test.json", O_CREAT | O_TRUNC | O_WRONLY); + assert_true(mScriptStorageSaveBucketVF(&context, "xtest", vf)); + vf = VFileOpen("test.json", O_RDONLY); + assert_non_null(vf); + ssize_t size = vf->size(vf); + char* buf = calloc(1, size + 1); + assert_int_equal(vf->read(vf, buf, size), size); + assert_string_equal(buf, "{\n\t\"a\":\"hello\"\n}"); + free(buf); + vf->close(vf); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(serializeList) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + + TEST_PROGRAM("bucket.a = {1, 2}"); + struct VFile* vf = VFileOpen("test.json", O_CREAT | O_TRUNC | O_WRONLY); + assert_true(mScriptStorageSaveBucketVF(&context, "xtest", vf)); + vf = VFileOpen("test.json", O_RDONLY); + assert_non_null(vf); + ssize_t size = vf->size(vf); + char* buf = calloc(1, size + 1); + assert_int_equal(vf->read(vf, buf, size), size); + assert_string_equal(buf, "{\n\t\"a\":[\n\t\t1,\n\t\t2\n\t]\n}"); + free(buf); + vf->close(vf); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(serializeTable) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + + TEST_PROGRAM("bucket.a = {['b']=1}"); + struct VFile* vf = VFileOpen("test.json", O_CREAT | O_TRUNC | O_WRONLY); + assert_true(mScriptStorageSaveBucketVF(&context, "xtest", vf)); + vf = VFileOpen("test.json", O_RDONLY); + assert_non_null(vf); + ssize_t size = vf->size(vf); + char* buf = calloc(1, size + 1); + assert_int_equal(vf->read(vf, buf, size), size); + assert_string_equal(buf, "{\n\t\"a\":{\n\t\t\"b\":1\n\t}\n}"); + free(buf); + vf->close(vf); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(serializeNullByteString) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + + TEST_PROGRAM("bucket.a = 'a\\x00b'"); + struct VFile* vf = VFileOpen("test.json", O_CREAT | O_TRUNC | O_WRONLY); + assert_true(mScriptStorageSaveBucketVF(&context, "xtest", vf)); + vf = VFileOpen("test.json", O_RDONLY); + assert_non_null(vf); + ssize_t size = vf->size(vf); + char* buf = calloc(1, size + 1); + assert_int_equal(vf->read(vf, buf, size), size); + assert_string_equal(buf, "{\n\t\"a\":\"a\\u0000b\"\n}"); + free(buf); + vf->close(vf); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(deserializeInt) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + + TEST_PROGRAM("assert(not bucket.a)"); + + static const char* json = "{\"a\":1}"; + struct VFile* vf = VFileFromConstMemory(json, strlen(json)); + assert_true(mScriptStorageLoadBucketVF(&context, "xtest", vf)); + TEST_PROGRAM("assert(bucket.a == 1)"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(deserializeFloat) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + + TEST_PROGRAM("assert(not bucket.a)"); + + static const char* json = "{\"a\":0.5}"; + struct VFile* vf = VFileFromConstMemory(json, strlen(json)); + assert_true(mScriptStorageLoadBucketVF(&context, "xtest", vf)); + TEST_PROGRAM("assert(bucket.a == 0.5)"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(deserializeBool) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + + TEST_PROGRAM("assert(not bucket.a)"); + + static const char* json = "{\"a\":true}"; + struct VFile* vf = VFileFromConstMemory(json, strlen(json)); + assert_true(mScriptStorageLoadBucketVF(&context, "xtest", vf)); + TEST_PROGRAM("assert(bucket.a == true)"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(deserializeNil) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + + TEST_PROGRAM("assert(not bucket.a)"); + + static const char* json = "{\"a\":null}"; + struct VFile* vf = VFileFromConstMemory(json, strlen(json)); + assert_true(mScriptStorageLoadBucketVF(&context, "xtest", vf)); + TEST_PROGRAM("assert(bucket.a == nil)"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(deserializeString) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + + TEST_PROGRAM("assert(not bucket.a)"); + + static const char* json = "{\"a\":\"hello\"}"; + struct VFile* vf = VFileFromConstMemory(json, strlen(json)); + assert_true(mScriptStorageLoadBucketVF(&context, "xtest", vf)); + TEST_PROGRAM("assert(bucket.a == 'hello')"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(deserializeList) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + + TEST_PROGRAM("assert(not bucket.a)"); + + static const char* json = "{\"a\":[1,2]}"; + struct VFile* vf = VFileFromConstMemory(json, strlen(json)); + assert_true(mScriptStorageLoadBucketVF(&context, "xtest", vf)); + TEST_PROGRAM("assert(#bucket.a == 2)"); + TEST_PROGRAM("assert(bucket.a[1] == 1)"); + TEST_PROGRAM("assert(bucket.a[2] == 2)"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(deserializeTable) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + + TEST_PROGRAM("assert(not bucket.a)"); + + static const char* json = "{\"a\":{\"b\":1}}"; + struct VFile* vf = VFileFromConstMemory(json, strlen(json)); + assert_true(mScriptStorageLoadBucketVF(&context, "xtest", vf)); + TEST_PROGRAM("assert(bucket.a.b == 1)"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(deserializeNullByteString) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + + TEST_PROGRAM("assert(not bucket.a)"); + + static const char* json = "{\"a\":\"a\\u0000b\"}"; + struct VFile* vf = VFileFromConstMemory(json, strlen(json)); + assert_true(mScriptStorageLoadBucketVF(&context, "xtest", vf)); + TEST_PROGRAM("assert(bucket.a == 'a\\x00b')"); + TEST_PROGRAM("assert(bucket.a ~= 'a\\x00c')"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(deserializeError) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + + TEST_PROGRAM("assert(not bucket.a)"); + + static const char* json = "{a:1}"; + struct VFile* vf = VFileFromConstMemory(json, strlen(json)); + assert_false(mScriptStorageLoadBucketVF(&context, "xtest", vf)); + TEST_PROGRAM("assert(not bucket.a)"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(structuredRoundTrip) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + TEST_PROGRAM("assert(not bucket.a)"); + TEST_PROGRAM( + "bucket.a = {\n" + " ['a'] = 1,\n" + " ['b'] = {1},\n" + " ['c'] = {\n" + " ['d'] = 1\n" + " }\n" + "}" + ); + + struct VFile* vf = VFileOpen("test.json", O_CREAT | O_TRUNC | O_WRONLY); + assert_true(mScriptStorageSaveBucketVF(&context, "xtest", vf)); + + TEST_PROGRAM("bucket.a = nil") + TEST_PROGRAM("assert(not bucket.a)"); + + vf = VFileOpen("test.json", O_RDONLY); + assert_non_null(vf); + assert_true(mScriptStorageLoadBucketVF(&context, "xtest", vf)); + + TEST_PROGRAM("assert(bucket.a)"); + TEST_PROGRAM("assert(bucket.a.a == 1)"); + TEST_PROGRAM("assert(#bucket.a.b == 1)"); + TEST_PROGRAM("assert(bucket.a.b[1] == 1)"); + TEST_PROGRAM("assert(#bucket.a.c == 1)"); + TEST_PROGRAM("assert(bucket.a.c.d == 1)"); + + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(autoflush) { + SETUP_LUA; + + TEST_PROGRAM("bucket = storage:getBucket('xtest')"); + TEST_PROGRAM("assert(bucket)"); + TEST_PROGRAM("assert(not bucket.a)"); + + TEST_PROGRAM("bucket:enableAutoFlush(true)") + TEST_PROGRAM("bucket.a = 1"); + TEST_PROGRAM("storage:flushAll()"); + TEST_PROGRAM("assert(bucket:reload())") + TEST_PROGRAM("assert(bucket.a == 1)"); + + TEST_PROGRAM("bucket:enableAutoFlush(false)") + TEST_PROGRAM("bucket.a = 2"); + TEST_PROGRAM("storage:flushAll()"); + TEST_PROGRAM("assert(bucket:reload())") + TEST_PROGRAM("assert(bucket.a == 1)"); + + TEST_PROGRAM("bucket:enableAutoFlush(false)") + TEST_PROGRAM("bucket.a = 3"); + TEST_PROGRAM("storage:flushAll()"); + TEST_PROGRAM("bucket:enableAutoFlush(true)") + TEST_PROGRAM("storage:flushAll()"); + TEST_PROGRAM("assert(bucket:reload())") + TEST_PROGRAM("assert(bucket.a == 3)"); + + mScriptContextDeinit(&context); +} + +M_TEST_SUITE_DEFINE_SETUP_TEARDOWN(mScriptStorage, + cmocka_unit_test(basicInt), + cmocka_unit_test(basicFloat), + cmocka_unit_test(basicBool), + cmocka_unit_test(basicNil), + cmocka_unit_test(basicString), + cmocka_unit_test(basicList), + cmocka_unit_test(basicTable), + cmocka_unit_test(nullByteString), + cmocka_unit_test(invalidObject), + cmocka_unit_test(structured), + cmocka_unit_test(serializeInt), + cmocka_unit_test(serializeFloat), + cmocka_unit_test(serializeBool), + cmocka_unit_test(serializeNil), + cmocka_unit_test(serializeString), + cmocka_unit_test(serializeList), + cmocka_unit_test(serializeTable), + cmocka_unit_test(serializeNullByteString), + cmocka_unit_test(deserializeInt), + cmocka_unit_test(deserializeFloat), + cmocka_unit_test(deserializeBool), + cmocka_unit_test(deserializeNil), + cmocka_unit_test(deserializeString), + cmocka_unit_test(deserializeList), + cmocka_unit_test(deserializeTable), + cmocka_unit_test(deserializeNullByteString), + cmocka_unit_test(deserializeError), + cmocka_unit_test(structuredRoundTrip), + cmocka_unit_test(autoflush), +) diff --git a/src/script/types.c b/src/script/types.c index 0237cd4cb..dd50f7968 100644 --- a/src/script/types.c +++ b/src/script/types.c @@ -27,6 +27,10 @@ static bool _stringCast(const struct mScriptValue*, const struct mScriptType*, s static bool _castScalar(const struct mScriptValue*, const struct mScriptType*, struct mScriptValue*); static uint32_t _hashScalar(const struct mScriptValue*); +static bool _wstrCast(const struct mScriptValue*, const struct mScriptType*, struct mScriptValue*); +static bool _wlistCast(const struct mScriptValue*, const struct mScriptType*, struct mScriptValue*); +static bool _wtableCast(const struct mScriptValue*, const struct mScriptType*, struct mScriptValue*); + static uint32_t _valHash(const void* val, size_t len, uint32_t seed); static bool _valEqual(const void* a, const void* b); static void* _valRef(void*); @@ -232,6 +236,7 @@ const struct mScriptType mSTStringWrapper = { .alloc = NULL, .free = NULL, .hash = NULL, + .cast = _wstrCast, }; const struct mScriptType mSTListWrapper = { @@ -241,6 +246,17 @@ const struct mScriptType mSTListWrapper = { .alloc = NULL, .free = NULL, .hash = NULL, + .cast = _wlistCast, +}; + +const struct mScriptType mSTTableWrapper = { + .base = mSCRIPT_TYPE_WRAPPER, + .size = sizeof(struct mScriptValue), + .name = "wrapper table", + .alloc = NULL, + .free = NULL, + .hash = NULL, + .cast = _wtableCast, }; const struct mScriptType mSTWeakref = { @@ -366,6 +382,45 @@ uint32_t _hashScalar(const struct mScriptValue* val) { return x; } +bool _wstrCast(const struct mScriptValue* input, const struct mScriptType* type, struct mScriptValue* output) { + if (input->type->base != mSCRIPT_TYPE_WRAPPER) { + return false; + } + const struct mScriptValue* unwrapped = mScriptValueUnwrapConst(input); + if (unwrapped->type != mSCRIPT_TYPE_MS_STR) { + return false; + } + memcpy(output, input, sizeof(*output)); + output->type = type; + return true; +} + +bool _wlistCast(const struct mScriptValue* input, const struct mScriptType* type, struct mScriptValue* output) { + if (input->type->base != mSCRIPT_TYPE_WRAPPER) { + return false; + } + const struct mScriptValue* unwrapped = mScriptValueUnwrapConst(input); + if (unwrapped->type != mSCRIPT_TYPE_MS_LIST) { + return false; + } + memcpy(output, input, sizeof(*output)); + output->type = type; + return true; +} + +bool _wtableCast(const struct mScriptValue* input, const struct mScriptType* type, struct mScriptValue* output) { + if (input->type->base != mSCRIPT_TYPE_WRAPPER) { + return false; + } + const struct mScriptValue* unwrapped = mScriptValueUnwrapConst(input); + if (unwrapped->type != mSCRIPT_TYPE_MS_TABLE) { + return false; + } + memcpy(output, input, sizeof(*output)); + output->type = type; + return true; +} + #define AS(NAME, TYPE) \ bool _as ## NAME(const struct mScriptValue* input, mSCRIPT_TYPE_C_ ## TYPE * T) { \ switch (input->type->base) { \ @@ -837,7 +892,6 @@ void mScriptValueWrap(struct mScriptValue* value, struct mScriptValue* out) { out->type = mSCRIPT_TYPE_MS_WRAPPER; out->value.opaque = value; - mScriptValueRef(value); } struct mScriptValue* mScriptValueUnwrap(struct mScriptValue* value) { @@ -944,14 +998,11 @@ bool mScriptTableRemove(struct mScriptValue* table, struct mScriptValue* key) { } struct mScriptValue* mScriptTableLookup(struct mScriptValue* table, struct mScriptValue* key) { - if (table->type->base == mSCRIPT_TYPE_WRAPPER) { - table = mScriptValueUnwrap(table); - } if (table->type != mSCRIPT_TYPE_MS_TABLE) { - return false; + return NULL; } if (!key->type->hash) { - return false; + return NULL; } return HashTableLookupCustom(table->value.table, key); } @@ -1097,12 +1148,16 @@ static void _mScriptClassInit(struct mScriptTypeClass* cls, const struct mScript } break; case mSCRIPT_CLASS_INIT_SET: - cls->set = calloc(1, sizeof(*member)); - memcpy(cls->set, &detail->info.member, sizeof(*member)); + member = calloc(1, sizeof(*member)); + memcpy(member, &detail->info.member, sizeof(*member)); if (docstring) { - cls->set->docstring = docstring; + member->docstring = docstring; docstring = NULL; } + if (detail->info.member.type->details.function.parameters.count != 3) { + abort(); + } + HashTableInsert(&cls->setters, detail->info.member.type->details.function.parameters.entries[2]->name, member); break; case mSCRIPT_CLASS_INIT_INTERNAL: cls->internal = true; @@ -1117,11 +1172,11 @@ void mScriptClassInit(struct mScriptTypeClass* cls) { } HashTableInit(&cls->instanceMembers, 0, free); HashTableInit(&cls->castToMembers, 0, NULL); + HashTableInit(&cls->setters, 0, free); cls->alloc = NULL; cls->free = NULL; cls->get = NULL; - cls->set = NULL; _mScriptClassInit(cls, cls->details, false); cls->init = true; @@ -1133,6 +1188,7 @@ void mScriptClassDeinit(struct mScriptTypeClass* cls) { } HashTableDeinit(&cls->instanceMembers); HashTableDeinit(&cls->castToMembers); + HashTableDeinit(&cls->setters); cls->init = false; } @@ -1265,7 +1321,7 @@ bool mScriptObjectGet(struct mScriptValue* obj, const char* member, struct mScri this->type = obj->type; this->refs = mSCRIPT_VALUE_UNREF; this->flags = 0; - this->value.opaque = obj; + this->value.opaque = obj->value.opaque; mSCRIPT_PUSH(&frame.arguments, CHARP, member); if (!mScriptInvoke(&getMember, &frame) || mScriptListSize(&frame.returnValues) != 1) { mScriptFrameDeinit(&frame); @@ -1300,6 +1356,91 @@ bool mScriptObjectGetConst(const struct mScriptValue* obj, const char* member, s return _accessRawMember(m, obj->value.opaque, true, val); } +static struct mScriptClassMember* _findSetter(const struct mScriptTypeClass* cls, const struct mScriptType* type) { + struct mScriptClassMember* m = HashTableLookup(&cls->setters, type->name); + if (m) { + return m; + } + + switch (type->base) { + case mSCRIPT_TYPE_SINT: + if (type->size < 2) { + m = HashTableLookup(&cls->setters, mSCRIPT_TYPE_MS_S16->name); + if (m) { + return m; + } + } + if (type->size < 4) { + m = HashTableLookup(&cls->setters, mSCRIPT_TYPE_MS_S32->name); + if (m) { + return m; + } + } + if (type->size < 8) { + m = HashTableLookup(&cls->setters, mSCRIPT_TYPE_MS_S64->name); + if (m) { + return m; + } + } + break; + case mSCRIPT_TYPE_UINT: + if (type->size < 2) { + m = HashTableLookup(&cls->setters, mSCRIPT_TYPE_MS_U16->name); + if (m) { + return m; + } + } + if (type->size < 4) { + m = HashTableLookup(&cls->setters, mSCRIPT_TYPE_MS_U32->name); + if (m) { + return m; + } + } + if (type->size < 8) { + m = HashTableLookup(&cls->setters, mSCRIPT_TYPE_MS_U64->name); + if (m) { + return m; + } + } + break; + case mSCRIPT_TYPE_FLOAT: + if (type->size < 8) { + m = HashTableLookup(&cls->setters, mSCRIPT_TYPE_MS_F64->name); + if (m) { + return m; + } + } + break; + case mSCRIPT_TYPE_STRING: + if (type == mSCRIPT_TYPE_MS_STR) { + m = HashTableLookup(&cls->setters, mSCRIPT_TYPE_MS_CHARP->name); + if (m) { + return m; + } + m = HashTableLookup(&cls->setters, mSCRIPT_TYPE_MS_WSTR->name); + if (m) { + return m; + } + } + break; + case mSCRIPT_TYPE_LIST: + m = HashTableLookup(&cls->setters, mSCRIPT_TYPE_MS_WLIST->name); + if (m) { + return m; + } + break; + case mSCRIPT_TYPE_TABLE: + m = HashTableLookup(&cls->setters, mSCRIPT_TYPE_MS_WTABLE->name); + if (m) { + return m; + } + break; + default: + break; + } + return NULL; +} + bool mScriptObjectSet(struct mScriptValue* obj, const char* member, struct mScriptValue* val) { if (obj->type->base != mSCRIPT_TYPE_OBJECT || obj->type->isConst) { return false; @@ -1314,7 +1455,29 @@ bool mScriptObjectSet(struct mScriptValue* obj, const char* member, struct mScri struct mScriptClassMember* m = HashTableLookup(&cls->instanceMembers, member); if (!m) { - return false; + if (val->type->base == mSCRIPT_TYPE_WRAPPER) { + val = mScriptValueUnwrap(val); + } + struct mScriptValue setMember; + m = _findSetter(cls, val->type); + if (!m || !_accessRawMember(m, obj->value.opaque, obj->type->isConst, &setMember)) { + return false; + } + struct mScriptFrame frame; + mScriptFrameInit(&frame); + struct mScriptValue* this = mScriptListAppend(&frame.arguments); + this->type = obj->type; + this->refs = mSCRIPT_VALUE_UNREF; + this->flags = 0; + this->value.opaque = obj->value.opaque; + mSCRIPT_PUSH(&frame.arguments, CHARP, member); + mScriptValueWrap(val, mScriptListAppend(&frame.arguments)); + if (!mScriptInvoke(&setMember, &frame) || mScriptListSize(&frame.returnValues) != 0) { + mScriptFrameDeinit(&frame); + return false; + } + mScriptFrameDeinit(&frame); + return true; } void* rawMember = (void *)((uintptr_t) obj->value.opaque + m->offset); @@ -1478,7 +1641,7 @@ bool mScriptPopPointer(struct mScriptList* list, void** out) { } bool mScriptCast(const struct mScriptType* type, const struct mScriptValue* input, struct mScriptValue* output) { - if (input->type->base == mSCRIPT_TYPE_WRAPPER) { + if (input->type->base == mSCRIPT_TYPE_WRAPPER && type->base != mSCRIPT_TYPE_WRAPPER) { input = mScriptValueUnwrapConst(input); } if (type->cast && type->cast(input, type, output)) {