diff --git a/.gitignore b/.gitignore index 0a509a626..58962ea21 100644 --- a/.gitignore +++ b/.gitignore @@ -236,3 +236,7 @@ spine-libgdx/spine-skeletonviewer/.settings/org.eclipse.jdt.apt.core.prefs spine-cpp/.cache .build spine-libgdx/.settings +docs/ +out.json +spine-cpp/extraction.log +extraction.log diff --git a/spine-cpp/extract-spine-cpp-types.js b/spine-cpp/extract-spine-cpp-types.js new file mode 100755 index 000000000..e0aeb326a --- /dev/null +++ b/spine-cpp/extract-spine-cpp-types.js @@ -0,0 +1,654 @@ +#!/usr/bin/env node + +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const scriptDir = path.dirname(path.resolve(__filename)); +const spineIncludeDir = path.join(scriptDir, 'spine-cpp', 'include'); + +function showHelp() { + console.log(` +extract-spine-cpp-types.js - Extract type information from spine-cpp header files + +USAGE: + extract-spine-cpp-types.js [] + extract-spine-cpp-types.js --help + +DESCRIPTION: + Extracts classes, structs, enums, and their public members from C++ headers. + Without arguments, processes all headers in spine-cpp/include. + + In all-files mode, performs two passes: + 1. Extract all type definitions + 2. Add inherited methods from supertypes (including template instantiations) + +OUTPUT FORMAT: + Single file mode: Array of type definitions + All files mode: Object with relative file paths as keys + +TYPE DEFINITION STRUCTURE: + { + "name": "ClassName", // Type name + "kind": "class" | "struct" | "enum", // Type kind + "loc": { // Source location + "line": 45, + "col": 15 + }, + "superTypes": ["BaseClass", "Interface"], // Base classes (optional) + "members": [...], // Public members (classes/structs) + "values": [...], // Enum constants (enums only) + "isTemplate": true, // Present if type is a template + "templateParams": ["T", "U"], // Template parameter names (templates only) + "isAbstract": true // Present if class has pure virtual methods + } + +MEMBER STRUCTURE: + Fields: + { + "kind": "field", + "name": "fieldName", + "type": "int" + } + + Methods: + { + "kind": "method", + "name": "methodName", + "returnType": "void", + "parameters": [ + {"name": "param1", "type": "int"}, + {"name": "param2", "type": "const String &"} + ], + "isStatic": false, + "isVirtual": true, + "isPure": false, + "fromSupertype": "BaseClass" // Present if inherited (all-files mode only) + } + + Constructors: + { + "kind": "constructor", + "name": "ClassName", + "parameters": [...] + } + +ENUM VALUES: + { + "name": "ENUM_VALUE", + "value": "1 << 0" // Present only if explicitly initialized + } + +INHERITANCE (all-files mode only): + - Methods inherited from non-template supertypes are included + - Methods inherited from template supertypes have parameters substituted + - SpineObject methods are excluded (considered noise) + - Methods are marked with "fromSupertype" field + - Template supertypes appear as "TemplateName" + +NOTES: + - Only public members are extracted + - Forward declarations are excluded + - Template instantiations (e.g., Vector) are excluded + - Friend declarations are excluded + - Implicit/compiler-generated methods are excluded + - Destructors and operator methods are excluded (not needed for C wrapper generation) + +EXAMPLES: + # Extract types from single file + extract-spine-cpp-types.js spine-cpp/include/spine/Animation.h > animation.json + + # Extract all types with inheritance resolution + extract-spine-cpp-types.js > all-spine-types.json + + # Query specific type + extract-spine-cpp-types.js > types.json + jq '."spine/Bone.h"[] | select(.name == "Bone")' types.json +`); +} + +function extractLocalTypes(headerFile, typeMap = null) { + const absHeaderPath = path.resolve(headerFile); + const sourceContent = fs.readFileSync(absHeaderPath, 'utf8'); + const sourceLines = sourceContent.split('\n'); + + // Get AST from clang + const cmd = `clang++ -Xclang -ast-dump=json -fsyntax-only -std=c++11 -I "${spineIncludeDir}" "${absHeaderPath}" 2>/dev/null`; + const maxBuffer = headerFile.includes('Debug.h') ? 500 : 200; // MB + + let astJson; + try { + const output = execSync(cmd, { encoding: 'utf8', maxBuffer: maxBuffer * 1024 * 1024 }); + astJson = JSON.parse(output); + } catch (error) { + throw new Error(error.code === 'ENOBUFS' + ? `AST output too large (>${maxBuffer}MB)` + : error.message); + } + + const types = []; + + function extractEnumValueFromSource(enumConstNode) { + if (!enumConstNode.loc) return undefined; + + const line = sourceLines[enumConstNode.loc.line - 1]; + if (!line) return undefined; + + // Find enum name and check for '=' + const nameMatch = line.match(new RegExp(`\\b${enumConstNode.name}\\b`)); + if (!nameMatch) return undefined; + + const afterName = line.substring(nameMatch.index + enumConstNode.name.length); + const equalIndex = afterName.indexOf('='); + if (equalIndex === -1) return null; // No explicit value + + // Extract value expression + let valueText = afterName.substring(equalIndex + 1); + + // Handle multi-line values + let currentLine = enumConstNode.loc.line; + while (currentLine < sourceLines.length && !valueText.match(/[,}]/)) { + valueText += '\n' + sourceLines[currentLine++]; + } + + // Extract up to comma or brace + const endMatch = valueText.match(/^(.*?)([,}])/s); + if (endMatch) valueText = endMatch[1]; + + // Clean up + return valueText + .replace(/\/\/.*$/gm, '') // Remove single-line comments + .replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments + .replace(/\s+/g, ' ') // Normalize whitespace + .trim(); + } + + function extractReturnType(methodNode) { + const fullType = methodNode.type?.qualType || ''; + const match = fullType.match(/^(.+?)\s*\(/); + return match ? match[1].trim() : 'void'; + } + + function extractParameters(methodNode) { + return (methodNode.inner || []) + .filter(n => n.kind === 'ParmVarDecl') + .map(n => ({ + name: n.name || '', + type: n.type?.qualType || '' + })); + } + + function isInTargetFile(node) { + if (!node.loc) return false; + + const targetPath = path.resolve(headerFile); + const loc = node.loc; + + // Check direct file location + if (loc.file) return path.resolve(loc.file) === targetPath; + + // Check macro locations + for (const locType of ['spellingLoc', 'expansionLoc']) { + if (loc[locType]) { + if (loc[locType].includedFrom) return false; + if (loc[locType].file) return path.resolve(loc[locType].file) === targetPath; + } + } + + // If included from another file, reject + if (loc.includedFrom) return false; + + // No location info - assume it's in the main file + return true; + } + + function extractTypeInfo(node) { + const info = { + name: node.name || '', + kind: node.kind === 'EnumDecl' ? 'enum' : (node.tagUsed || ''), + loc: { + line: node.loc?.line || 0, + col: node.loc?.col || 0 + } + }; + + // Extract base classes + if (node.bases?.length > 0) { + info.superTypes = node.bases.map(b => b.type?.qualType || '').filter(Boolean); + } + + // For enums, extract the values + if (node.kind === 'EnumDecl') { + info.values = (node.inner || []) + .filter(n => n.kind === 'EnumConstantDecl') + .map(n => { + const enumValue = { name: n.name || '' }; + const sourceValue = extractEnumValueFromSource(n); + + if (sourceValue === null) { + // Implicit value - no value property + } else if (sourceValue) { + enumValue.value = sourceValue; + } else if (n.inner?.length > 0) { + enumValue.value = "<>"; + } + + return enumValue; + }); + return info; + } + + // For classes/structs, extract public members + info.members = []; + let currentAccess = node.tagUsed === 'struct' ? 'public' : 'private'; + let hasPureVirtual = false; + + for (const inner of node.inner || []) { + if (inner.kind === 'AccessSpecDecl') { + currentAccess = inner.access || 'private'; + continue; + } + + if (inner.kind === 'FriendDecl' || currentAccess !== 'public') continue; + + const member = extractMember(inner, node); + if (member) { + info.members.push(member); + // Check if this is a pure virtual method + if (member.kind === 'method' && member.isPure) { + hasPureVirtual = true; + } + } + } + + // Mark as abstract if it has pure virtual methods + if (hasPureVirtual) { + info.isAbstract = true; + } + + return info; + } + + function extractMember(inner, parent) { + if (inner.isImplicit) return null; + + switch (inner.kind) { + case 'FieldDecl': + return { + kind: 'field', + name: inner.name || '', + type: inner.type?.qualType || '' + }; + + case 'CXXMethodDecl': + if (!inner.name) return null; + // Skip operators - not needed for C wrapper generation + if (inner.name.startsWith('operator')) return null; + + return { + kind: 'method', + name: inner.name, + returnType: extractReturnType(inner), + parameters: extractParameters(inner), + isStatic: inner.storageClass === 'static', + isVirtual: inner.virtual || false, + isPure: inner.pure || false + }; + + case 'CXXConstructorDecl': + return { + kind: 'constructor', + name: inner.name || parent.name || '', + parameters: extractParameters(inner) + }; + + case 'CXXDestructorDecl': + // Skip destructors - not needed for C wrapper generation + return null; + + default: + return null; + } + } + + function processNode(node, inSpineNamespace = false) { + if (!node || typeof node !== 'object') return; + + // Handle spine namespace + if (node.kind === 'NamespaceDecl' && node.name === 'spine') { + (node.inner || []).forEach(n => processNode(n, true)); + return; + } + + // Recurse to find spine namespace + if (!inSpineNamespace) { + (node.inner || []).forEach(n => processNode(n, false)); + return; + } + + // Process type declarations + const typeKinds = ['CXXRecordDecl', 'ClassTemplateDecl', 'EnumDecl', 'TypedefDecl', 'TypeAliasDecl']; + if (!typeKinds.includes(node.kind)) return; + + // Skip if not in target file or invalid + if (!isInTargetFile(node) || + node.isImplicit || + !node.name || + node.name.startsWith('_') || + node.name.includes('<')) return; + + // Skip forward declarations + if (node.previousDecl && (!node.inner || node.inner.length === 0)) return; + + // Extract type info + if (node.kind === 'ClassTemplateDecl') { + const classNode = (node.inner || []).find(n => n.kind === 'CXXRecordDecl'); + if (classNode) { + const typeInfo = extractTypeInfo(classNode); + typeInfo.isTemplate = true; + + // Extract template parameters + const templateParams = []; + for (const inner of node.inner || []) { + if (inner.kind === 'TemplateTypeParmDecl' && inner.name) { + templateParams.push(inner.name); + } + } + if (templateParams.length > 0) { + typeInfo.templateParams = templateParams; + } + + types.push(typeInfo); + } + } else if (node.kind === 'CXXRecordDecl' && node.inner?.length > 0) { + types.push(extractTypeInfo(node)); + } else if (node.kind === 'EnumDecl') { + types.push(extractTypeInfo(node)); + } else if (node.kind === 'TypedefDecl' || node.kind === 'TypeAliasDecl') { + types.push(extractTypeInfo(node)); + } + } + + processNode(astJson); + + // Filter out forward declarations and sort by line number + const filteredTypes = types + .filter(t => !(t.members && t.members.length === 0)) + .sort((a, b) => a.loc.line - b.loc.line); + + // Add inherited methods if we have a type map + if (typeMap) { + for (const type of filteredTypes) { + if (type.superTypes && type.members) { + addInheritedMethods(type, typeMap); + } + } + } + + return filteredTypes; +} + +function addInheritedMethods(type, typeMap) { + const inheritedMethods = []; + const ownMethodSignatures = new Set(); + + // Build a set of method signatures from this type + for (const member of type.members) { + if (member.kind === 'method') { + const sig = getMethodSignature(member); + ownMethodSignatures.add(sig); + } + } + + // Process each supertype + for (const superTypeName of type.superTypes) { + // Clean up the supertype name (remove namespaces, etc) + const cleanName = superTypeName.replace(/^.*::/, ''); + + // Skip SpineObject inheritance - it's just noise + if (cleanName === 'SpineObject') continue; + + // Check if this is a template supertype + const templateMatch = cleanName.match(/^([^<]+)<(.+)>$/); + if (templateMatch) { + const templateClassName = templateMatch[1]; + const templateArgs = templateMatch[2]; + + const templateType = typeMap.get(templateClassName); + if (templateType && templateType.isTemplate && templateType.members) { + // Process template inheritance + addTemplateInheritedMethods( + type, templateType, templateClassName, templateArgs, + inheritedMethods, ownMethodSignatures + ); + } else if (templateType && !templateType.isTemplate) { + // Template might not be marked as isTemplate, try anyway + addTemplateInheritedMethods( + type, templateType, templateClassName, templateArgs, + inheritedMethods, ownMethodSignatures + ); + } + } else { + // Non-template supertype + const superType = typeMap.get(cleanName); + + if (!superType || !superType.members) continue; + + // Add non-overridden methods from supertype + for (const member of superType.members) { + if (member.kind === 'method') { + const sig = getMethodSignature(member); + if (!ownMethodSignatures.has(sig)) { + const inheritedMember = { ...member, fromSupertype: cleanName }; + inheritedMethods.push(inheritedMember); + ownMethodSignatures.add(sig); // Prevent duplicates from multiple inheritance + } + } + } + } + } + + // Add inherited methods to the type + type.members.push(...inheritedMethods); +} + +function addTemplateInheritedMethods( + type, templateType, templateClassName, templateArgs, + inheritedMethods, ownMethodSignatures +) { + // Parse template arguments (handle multiple args) + const argsList = []; + let depth = 0; + let currentArg = ''; + + for (const char of templateArgs) { + if (char === '<') depth++; + else if (char === '>') depth--; + + if (char === ',' && depth === 0) { + argsList.push(currentArg.trim()); + currentArg = ''; + } else { + currentArg += char; + } + } + if (currentArg.trim()) { + argsList.push(currentArg.trim()); + } + + // Build a mapping of template params to actual types + const paramMap = new Map(); + + // Use the actual template parameters if we have them + if (templateType.templateParams && templateType.templateParams.length === argsList.length) { + templateType.templateParams.forEach((param, i) => { + paramMap.set(param, argsList[i]); + }); + } else { + // Fallback: if we don't have template param info, skip substitution + console.error(`Warning: Template ${templateClassName} missing parameter info, skipping substitution`); + return; + } + + // Process each member of the template + for (const member of templateType.members) { + if (member.kind === 'method') { + // Skip template constructors - they have weird names like "Pose

" + if (member.name.includes('<')) continue; + + // Clone the member and substitute template parameters + const inheritedMember = JSON.parse(JSON.stringify(member)); + inheritedMember.fromSupertype = `${templateClassName}<${templateArgs}>`; + + // Replace template parameters in return type + if (inheritedMember.returnType) { + inheritedMember.returnType = substituteTemplateParams( + inheritedMember.returnType, paramMap + ); + } + + // Replace template parameters in parameters + if (inheritedMember.parameters) { + for (const param of inheritedMember.parameters) { + param.type = substituteTemplateParams(param.type, paramMap); + } + } + + // Check if this method is overridden + const sig = getMethodSignature(inheritedMember); + if (!ownMethodSignatures.has(sig)) { + inheritedMethods.push(inheritedMember); + ownMethodSignatures.add(sig); + } + } + } +} + +function substituteTemplateParams(typeStr, paramMap) { + let result = typeStr; + + // Replace template parameters in order of length (longest first) + // to avoid replacing substrings (e.g., V before V1) + const sortedParams = Array.from(paramMap.keys()).sort((a, b) => b.length - a.length); + + for (const param of sortedParams) { + const regex = new RegExp(`\\b${param}\\b`, 'g'); + result = result.replace(regex, paramMap.get(param)); + } + + return result; +} + +function getMethodSignature(method) { + // Create a signature that identifies method overrides + // For virtual methods, just use the name + // For non-virtual methods, include parameter types + let sig = method.name; + if (method.parameters && method.parameters.length > 0) { + sig += '(' + method.parameters.map(p => p.type).join(',') + ')'; + } else { + sig += '()'; + } + + // Add const qualifier if present + if (method.returnType && method.returnType.includes('const')) { + sig += ' const'; + } + + return sig; +} + +function findAllHeaderFiles() { + const headers = []; + + function walkDir(dir) { + fs.readdirSync(dir).forEach(file => { + const fullPath = path.join(dir, file); + if (fs.statSync(fullPath).isDirectory()) { + walkDir(fullPath); + } else if (file.endsWith('.h') && file !== 'spine.h') { + headers.push(fullPath); + } + }); + } + + walkDir(spineIncludeDir); + return headers.sort(); +} + +// Main execution +const arg = process.argv[2]; + +if (arg === '--help' || arg === '-h') { + showHelp(); + process.exit(0); +} + +if (arg) { + // Single file mode - no inheritance support + if (!fs.existsSync(arg)) { + console.error(`Error: File not found: ${arg}`); + process.exit(1); + } + + try { + const types = extractLocalTypes(arg); + if (types.length === 0) { + console.error('No types found in the specified file'); + process.exit(1); + } + console.log(JSON.stringify(types, null, 2)); + } catch (error) { + console.error(`Error: ${error.message}`); + process.exit(1); + } +} else { + // Process all files + const allHeaders = findAllHeaderFiles(); + const allTypes = {}; + let processed = 0, errors = 0; + + console.error(`Processing ${allHeaders.length} header files...`); + + // First pass: extract all types without inheritance + const typeMap = new Map(); + + for (const headerFile of allHeaders) { + const relPath = path.relative(spineIncludeDir, headerFile); + process.stderr.write(`\r\x1b[K Pass 1 - Processing ${++processed}/${allHeaders.length}: ${relPath}...`); + + try { + const types = extractLocalTypes(headerFile); + if (types.length > 0) { + allTypes[relPath] = types; + // Build type map + for (const type of types) { + typeMap.set(type.name, type); + } + } + } catch (error) { + errors++; + console.error(`\n ERROR processing ${relPath}: ${error.message}`); + } + } + + // Second pass: add inherited methods + console.error(`\n Pass 2 - Adding inherited methods...`); + processed = 0; + + for (const headerFile of allHeaders) { + const relPath = path.relative(spineIncludeDir, headerFile); + if (!allTypes[relPath]) continue; + + process.stderr.write(`\r\x1b[K Pass 2 - Processing ${++processed}/${Object.keys(allTypes).length}: ${relPath}...`); + + for (const type of allTypes[relPath]) { + if (type.superTypes && type.members) { + addInheritedMethods(type, typeMap); + } + } + } + + console.error(`\n Completed: ${Object.keys(allTypes).length} files processed, ${errors} errors`); + console.log(JSON.stringify(allTypes, null, 2)); +} \ No newline at end of file diff --git a/spine-cpp/spine-cpp/include/spine/Atlas.h b/spine-cpp/spine-cpp/include/spine/Atlas.h index f6c7931da..06acf555d 100644 --- a/spine-cpp/spine-cpp/include/spine/Atlas.h +++ b/spine-cpp/spine-cpp/include/spine/Atlas.h @@ -51,12 +51,7 @@ namespace spine { // Our TextureFilter collides with UE4's TextureFilter in unity builds. We rename // TextureFilter to SpineTextureFilter in UE4. #ifdef SPINE_UE4 - #define TEXTURE_FILTER_ENUM SpineTextureFilter -#else - #define TEXTURE_FILTER_ENUM TextureFilter -#endif - - enum TEXTURE_FILTER_ENUM { + enum SpineTextureFilter { TextureFilter_Unknown, TextureFilter_Nearest, TextureFilter_Linear, @@ -66,6 +61,18 @@ namespace spine { TextureFilter_MipMapNearestLinear, TextureFilter_MipMapLinearLinear }; +#else + enum TextureFilter { + TextureFilter_Unknown, + TextureFilter_Nearest, + TextureFilter_Linear, + TextureFilter_MipMap, + TextureFilter_MipMapNearestNearest, + TextureFilter_MipMapLinearNearest, + TextureFilter_MipMapNearestLinear, + TextureFilter_MipMapLinearLinear + }; +#endif enum TextureWrap { TextureWrap_MirroredRepeat, @@ -78,8 +85,13 @@ namespace spine { String name; String texturePath; Format format; - TEXTURE_FILTER_ENUM minFilter; - TEXTURE_FILTER_ENUM magFilter; +#ifdef SPINE_UE4 + SpineTextureFilter minFilter; + SpineTextureFilter magFilter; +#else + TextureFilter minFilter; + TextureFilter magFilter; +#endif TextureWrap uWrap; TextureWrap vWrap; int width, height; diff --git a/spine-cpp/spine-cpp/include/spine/Debug.h b/spine-cpp/spine-cpp/include/spine/Debug.h index 914afa55e..f2911c27e 100644 --- a/spine-cpp/spine-cpp/include/spine/Debug.h +++ b/spine-cpp/spine-cpp/include/spine/Debug.h @@ -32,8 +32,7 @@ #include #include - -#include +#include namespace spine { @@ -57,12 +56,14 @@ namespace spine { } void reportLeaks() { - for (std::map::iterator it = _allocated.begin(); it != _allocated.end(); it++) { - printf("\"%s:%i (%zu bytes at %p)\n", it->second.fileName, it->second.line, it->second.size, - it->second.address); + HashMap::Entries entries = _allocated.getEntries(); + while (entries.hasNext()) { + HashMap::Pair pair = entries.next(); + printf("\"%s:%i (%zu bytes at %p)\n", pair.value.fileName, pair.value.line, pair.value.size, + pair.value.address); } printf("allocations: %zu, reallocations: %zu, frees: %zu\n", _allocations, _reallocations, _frees); - if (_allocated.empty()) printf("No leaks detected\n"); + if (_allocated.size() == 0) printf("No leaks detected\n"); } void clearAllocations() { @@ -72,7 +73,7 @@ namespace spine { virtual void *_alloc(size_t size, const char *file, int line) { void *result = _extension->_alloc(size, file, line); - _allocated[result] = Allocation(result, size, file, line); + _allocated.put(result, Allocation(result, size, file, line)); _allocations++; _usedMemory += size; return result; @@ -80,28 +81,46 @@ namespace spine { virtual void *_calloc(size_t size, const char *file, int line) { void *result = _extension->_calloc(size, file, line); - _allocated[result] = Allocation(result, size, file, line); + _allocated.put(result, Allocation(result, size, file, line)); _allocations++; _usedMemory += size; return result; } virtual void *_realloc(void *ptr, size_t size, const char *file, int line) { - if (_allocated.count(ptr)) _usedMemory -= _allocated[ptr].size; - _allocated.erase(ptr); + if (_allocated.containsKey(ptr)) { + // Find and store the size before removing + HashMap::Entries entries = _allocated.getEntries(); + while (entries.hasNext()) { + HashMap::Pair pair = entries.next(); + if (pair.key == ptr) { + _usedMemory -= pair.value.size; + break; + } + } + _allocated.remove(ptr); + } void *result = _extension->_realloc(ptr, size, file, line); _reallocations++; - _allocated[result] = Allocation(result, size, file, line); + _allocated.put(result, Allocation(result, size, file, line)); _usedMemory += size; return result; } virtual void _free(void *mem, const char *file, int line) { - if (_allocated.count(mem)) { + if (_allocated.containsKey(mem)) { _extension->_free(mem, file, line); _frees++; - _usedMemory -= _allocated[mem].size; - _allocated.erase(mem); + // Find and store the size before removing + HashMap::Entries entries = _allocated.getEntries(); + while (entries.hasNext()) { + HashMap::Pair pair = entries.next(); + if (pair.key == mem) { + _usedMemory -= pair.value.size; + break; + } + } + _allocated.remove(mem); return; } @@ -112,8 +131,8 @@ namespace spine { virtual char *_readFile(const String &path, int *length) { auto data = _extension->_readFile(path, length); - if (_allocated.count(data) == 0) { - _allocated[data] = Allocation(data, sizeof(char) * (*length), nullptr, 0); + if (!_allocated.containsKey(data)) { + _allocated.put(data, Allocation(data, sizeof(char) * (*length), nullptr, 0)); _allocations++; _usedMemory += sizeof(char) * (*length); } @@ -127,7 +146,7 @@ namespace spine { private: SpineExtension *_extension; - std::map _allocated; + HashMap _allocated; size_t _allocations; size_t _reallocations; size_t _frees; diff --git a/spine-cpp/spine-cpp/include/spine/SkeletonBinary.h b/spine-cpp/spine-cpp/include/spine/SkeletonBinary.h index d2bca7335..65891fea0 100644 --- a/spine-cpp/spine-cpp/include/spine/SkeletonBinary.h +++ b/spine-cpp/spine-cpp/include/spine/SkeletonBinary.h @@ -127,7 +127,7 @@ namespace spine { void setScale(float scale) { _scale = scale; } - String &getError() { return _error; } + const String &getError() const { return _error; } private: struct DataInput : public SpineObject { diff --git a/spine-cpp/spine-cpp/include/spine/SkeletonJson.h b/spine-cpp/spine-cpp/include/spine/SkeletonJson.h index 8083236ad..764c6de53 100644 --- a/spine-cpp/spine-cpp/include/spine/SkeletonJson.h +++ b/spine-cpp/spine-cpp/include/spine/SkeletonJson.h @@ -79,7 +79,7 @@ namespace spine { void setScale(float scale) { _scale = scale; } - String &getError() { return _error; } + const String &getError() const { return _error; } private: AttachmentLoader *_attachmentLoader;