blob: fdfbea293166fdbee837f7146e76b50cf8cccfb9 [file] [log] [blame]
import 'dart:convert';
import 'dart:io';
import 'package:colorize/colorize.dart';
import 'package:core_generator/src/comment.dart';
import 'package:core_generator/src/configuration.dart';
import 'package:core_generator/src/cpp_formatter.dart';
import 'package:core_generator/src/field_type.dart';
import 'package:core_generator/src/key.dart';
import 'package:core_generator/src/property.dart';
String stripExtension(String filename) {
var index = filename.lastIndexOf('.');
return index == -1 ? filename : filename.substring(0, index);
}
class Definition {
static final Map<String, Definition> definitions = <String, Definition>{};
final String _filename;
static final _formatter = CppFormatter();
String _name;
final List<Property> _properties = [];
List<Property> get properties => _properties
.where((property) => property.isRuntime)
.toList(growable: false);
Definition _extensionOf;
Key _key;
bool _isAbstract = false;
bool _editorOnly = false;
bool _forRuntime = true;
bool get forRuntime => _forRuntime;
factory Definition(String filename) {
var definition = definitions[filename];
if (definition != null) {
return definition;
}
var file = File(defsPath + filename);
var contents = file.readAsStringSync();
Map<String, dynamic> definitionData;
try {
dynamic parsedJson = json.decode(contents);
if (parsedJson is Map<String, dynamic>) {
definitionData = parsedJson;
}
} on FormatException catch (error) {
color('Invalid json data in $filename: $error', front: Styles.RED);
return null;
}
definitions[filename] =
definition = Definition.fromFilename(filename, definitionData);
return definition;
}
Definition.fromFilename(this._filename, Map<String, dynamic> data) {
dynamic extendsFilename = data['extends'];
if (extendsFilename is String) {
_extensionOf = Definition(extendsFilename);
}
dynamic nameValue = data['name'];
if (nameValue is String) {
_name = nameValue;
}
dynamic forRuntime = data['runtime'];
if (forRuntime is bool) {
_forRuntime = forRuntime;
}
dynamic abstractValue = data['abstract'];
if (abstractValue is bool) {
_isAbstract = abstractValue;
}
dynamic editorOnlyValue = data['editorOnly'];
if (editorOnlyValue is bool) {
_editorOnly = editorOnlyValue;
}
_key = Key.fromJSON(data['key']) ?? Key.forDefinition(this);
dynamic properties = data['properties'];
if (properties is Map<String, dynamic>) {
for (final MapEntry<String, dynamic> entry in properties.entries) {
if (entry.value is Map<String, dynamic>) {
var property =
Property(this, entry.key, entry.value as Map<String, dynamic>);
if (property == null) {
continue;
}
_properties.add(property);
}
}
}
}
String get localFilename => _filename.indexOf(defsPath) == 0
? _filename.substring(defsPath.length)
: _filename;
String get name => _name;
String get localCodeFilename => '${stripExtension(_filename)}_base.hpp';
String get concreteCodeFilename => 'rive/${stripExtension(_filename)}.hpp';
String get localCppCodeFilename => '${stripExtension(_filename)}_base.cpp';
/// Generates cpp header code based on the Definition
Future<void> generateCode() async {
if (!_forRuntime) {
return;
}
bool defineContextExtension = _extensionOf?._name == null;
StringBuffer code = StringBuffer();
var includes = <String>{
defineContextExtension
? 'rive/core.hpp'
: _extensionOf.concreteCodeFilename
};
for (final property in properties) {
if (property.type.include != null) {
includes.add(property.type.include);
}
includes.add('rive/core/field_types/' +
property.type.snakeRuntimeCoreName +
'.hpp');
}
var sortedIncludes = includes.toList()..sort();
for (final include in sortedIncludes) {
code.write('#include ');
if (include[0] == '<') {
code.write(include);
} else {
code.write('\"$include\"');
}
code.write('\n');
}
code.writeln('namespace rive {');
var superTypeName = defineContextExtension ? 'Core' : _extensionOf?._name;
code.writeln('class ${_name}Base : public $superTypeName {');
code.writeln('protected:');
code.writeln('typedef $superTypeName Super;');
code.writeln('public:');
code.writeln('static const uint16_t typeKey = ${_key.intValue};\n');
code.write(comment(
'Helper to quickly determine if a core object extends another '
'without RTTI at runtime.',
indent: 1));
code.writeln('bool isTypeOf(uint16_t typeKey) const override {');
code.writeln('switch(typeKey) {');
code.writeln('case ${_name}Base::typeKey:');
for (var p = _extensionOf; p != null; p = p._extensionOf) {
code.writeln('case ${p._name}Base::typeKey:');
}
code.writeln('return true;');
code.writeln('default: return false;}');
code.writeln('}\n');
code.writeln('uint16_t coreType() const override { return typeKey; }\n');
if (properties.isNotEmpty) {
for (final property in properties) {
code.writeln('static const uint16_t ${property.name}PropertyKey = '
'${property.key.intValue};');
}
if (properties.any((prop) => !prop.isEncoded)) {
code.writeln('private:');
}
// Write fields.
for (final property in properties) {
if (property.isEncoded) {
// Encoded properties don't store data, it's up to the implementation
// to decode and store what it needs.
continue;
}
code.writeln('${property.type.cppName} m_${property.capitalizedName}');
var initialize = property.initialValueRuntime ??
property.initialValue ??
property.type.defaultValue;
if (initialize != null) {
var converted = property.type.convertCpp(initialize);
if (converted != null) {
code.write(' = $converted');
}
}
code.write(';');
}
// Write getter/setters.
code.writeln('public:');
for (final property in properties) {
if (property.isEncoded) {
// Encoded properties just have a pure virtual decoder that needs to
// be implemented. Also requires an implemention of copyPropertyName
// as that will no longer automatically be copied by the generated
// code.
code.writeln((property.isSetOverride ? '' : 'virtual ') +
'void decode${property.capitalizedName}' +
'(${property.type.cppName} value) ' +
(property.isSetOverride ? 'override' : '') +
'= 0;');
code.writeln((property.isSetOverride ? '' : 'virtual ') +
'void copy${property.capitalizedName}' +
'(const ${_name}Base& object) ' +
(property.isSetOverride ? 'override' : '') +
'= 0;');
} else {
code.writeln((property.isVirtual ? 'virtual' : 'inline') +
' ${property.type.cppGetterName} ${property.name}() const ' +
(property.isGetOverride ? 'override' : '') +
'{ return m_${property.capitalizedName}; }');
code.writeln(
'void ${property.name}(${property.type.cppName} value) ' +
(property.isSetOverride ? 'override' : '') +
'{'
'if(m_${property.capitalizedName} == value)'
'{return;}'
'm_${property.capitalizedName} = value;'
'${property.name}Changed();'
'}');
}
code.writeln();
}
}
if (!_isAbstract) {
code.writeln('Core* clone() const override;');
}
if (properties.isNotEmpty || _extensionOf == null) {
code.writeln('void copy(const ${_name}Base& object) {');
for (final property in properties) {
if (property.isEncoded) {
code.writeln('copy${property.capitalizedName}(object);');
} else {
code.writeln('m_${property.capitalizedName} = '
'object.m_${property.capitalizedName};');
}
}
if (_extensionOf != null) {
code.writeln('${_extensionOf.name}::'
'copy(object); ');
}
code.writeln('}');
code.writeln();
code.writeln('bool deserialize(uint16_t propertyKey, '
'BinaryReader& reader) override {');
code.writeln('switch (propertyKey){');
for (final property in properties) {
code.writeln('case ${property.name}PropertyKey:');
if (property.isEncoded) {
code.writeln('decode${property.capitalizedName}'
'(${property.type.runtimeCoreType}::deserialize(reader));');
} else {
code.writeln('m_${property.capitalizedName} = '
'${property.type.runtimeCoreType}::deserialize(reader);');
}
code.writeln('return true;');
}
code.writeln('}');
if (_extensionOf != null) {
code.writeln('return ${_extensionOf.name}::'
'deserialize(propertyKey, reader); }');
} else {
code.writeln('return false; }');
}
}
code.writeln('protected:');
if (properties.isNotEmpty) {
for (final property in properties) {
code.writeln('virtual void ${property.name}Changed() {}');
}
}
code.writeln('};');
code.writeln('}');
var file = File('$generatedHppPath$localCodeFilename');
file.createSync(recursive: true);
var formattedCode =
await _formatter.formatAndGuard('${_name}Base', code.toString());
file.writeAsStringSync(formattedCode, flush: true);
// See if we need to stub out the concrete version...
var concreteFile = File('$concreteHppPath$concreteCodeFilename');
if (!concreteFile.existsSync()) {
StringBuffer concreteCode = StringBuffer();
concreteFile.createSync(recursive: true);
concreteCode.writeln('#include "rive/generated/$localCodeFilename"');
concreteCode.writeln('#include <stdio.h>');
concreteCode.writeln('namespace rive {');
concreteCode.writeln('''class $_name : public ${_name}Base {
public:
};''');
concreteCode.writeln('}');
var formattedCode =
await _formatter.formatAndGuard(_name, concreteCode.toString());
concreteFile.writeAsStringSync(formattedCode, flush: true);
}
if (!_isAbstract) {
StringBuffer cppCode = StringBuffer();
cppCode.writeln('#include "rive/generated/$localCodeFilename"');
cppCode.writeln('#include "$concreteCodeFilename"');
cppCode.writeln();
cppCode.writeln('using namespace rive;');
cppCode.writeln();
cppCode.writeln('Core* ${_name}Base::clone() const { '
'auto cloned = new $_name(); '
'cloned->copy(*this); '
'return cloned; '
'}');
var cppFile = File('$generatedCppPath$localCppCodeFilename');
cppFile.createSync(recursive: true);
var formattedCode = await _formatter.format(cppCode.toString());
cppFile.writeAsStringSync(formattedCode, flush: true);
}
}
@override
String toString() {
return '$_name[${_key?.intValue ?? '-'}]';
}
static const int minPropertyId = 3;
static Future<bool> generate() async {
// Check dupe ids.
bool runGenerator = true;
Map<int, Definition> ids = {};
Map<int, Property> properties = {};
for (final definition in definitions.values) {
if (definition._key?.intValue != null) {
var other = ids[definition._key.intValue];
if (other != null) {
color('Duplicate type ids for $definition and $other.',
front: Styles.RED);
runGenerator = false;
} else {
ids[definition._key.intValue] = definition;
}
}
for (final property in definition._properties) {
if (property.key.isMissing) {
continue;
}
var other = properties[property.key.intValue];
if (other != null) {
color(
'''Duplicate field ids for ${property.definition}.$property '''
'''and ${other.definition}.$other.''',
front: Styles.RED);
runGenerator = false;
} else if (property.key.intValue < minPropertyId) {
color(
'${property.definition}.$property: ids less than '
'$minPropertyId are reserved.',
front: Styles.RED);
runGenerator = false;
} else {
properties[property.key.intValue] = property;
}
}
}
// Find max id, we use this to assign to types that don't have ids yet.
int nextFieldId = minPropertyId - 1;
int nextId = 0;
for (final definition in definitions.values) {
if (definition._key != null &&
definition._key.intValue != null &&
definition._key.intValue > nextId) {
nextId = definition._key.intValue;
}
for (final field in definition._properties) {
if (field != null &&
field.key.intValue != null &&
field.key.intValue > nextFieldId) {
nextFieldId = field.key.intValue;
}
}
}
if (!runGenerator) {
color('Not running generator due to previous errors.',
front: Styles.YELLOW);
return false;
}
definitions.removeWhere((key, definition) => definition._editorOnly);
// Clear out previous generated code.
var dir = Directory(generatedHppPath);
if (dir.existsSync()) {
dir.deleteSync(recursive: true);
}
dir.createSync(recursive: true);
// Generate core context.
for (final definition in definitions.values) {
await definition.generateCode();
}
StringBuffer ctxCode = StringBuffer('');
var includes = <String>{};
var runtimeDefinitions =
definitions.values.where((definition) => definition.forRuntime);
for (final definition in runtimeDefinitions) {
includes.add(definition.concreteCodeFilename);
}
var includeList = includes.toList()..sort();
for (final include in includeList) {
ctxCode.writeln('#include "$include"');
}
ctxCode.writeln('namespace rive {class CoreRegistry {'
'public:');
ctxCode.writeln('static Core* makeCoreInstance(int typeKey) {'
'switch(typeKey) {');
for (final definition in runtimeDefinitions) {
if (definition._isAbstract) {
continue;
}
ctxCode.writeln('case ${definition.name}Base::typeKey:');
ctxCode.writeln('return new ${definition.name}();');
}
ctxCode.writeln('} return nullptr; }');
var usedFieldTypes = <FieldType, List<Property>>{};
var getSetFieldTypes = <FieldType, List<Property>>{};
for (final definition in runtimeDefinitions) {
for (final property in definition.properties) {
usedFieldTypes[property.type] ??= [];
usedFieldTypes[property.type].add(property);
if (!property.isEncoded) {
getSetFieldTypes[property.type] ??= [];
getSetFieldTypes[property.type].add(property);
}
}
}
for (final fieldType in getSetFieldTypes.keys) {
ctxCode
.writeln('static void set${fieldType.capitalizedName}(Core* object, '
'int propertyKey, ${fieldType.cppName} value){');
ctxCode.writeln('switch (propertyKey) {');
var properties = getSetFieldTypes[fieldType];
for (final property in properties) {
ctxCode.writeln('case ${property.definition.name}Base'
'::${property.name}PropertyKey:');
ctxCode.writeln('object->as<${property.definition.name}Base>()->'
'${property.name}(value);');
ctxCode.writeln('break;');
}
ctxCode.writeln('}}');
}
for (final fieldType in getSetFieldTypes.keys) {
ctxCode.writeln(
'static ${fieldType.cppName} get${fieldType.capitalizedName}('
'Core* object, int propertyKey){');
ctxCode.writeln('switch (propertyKey) {');
var properties = getSetFieldTypes[fieldType];
for (final property in properties) {
ctxCode.writeln('case ${property.definition.name}Base'
'::${property.name}PropertyKey:');
ctxCode.writeln('return object->as<${property.definition.name}Base>()->'
'${property.name}();');
}
ctxCode.writeln('}');
ctxCode.writeln('return ${fieldType.defaultValue ?? 'nullptr'};');
ctxCode.writeln('}');
}
ctxCode.writeln('static int propertyFieldId(int propertyKey) {');
ctxCode.writeln('switch(propertyKey) {');
for (final fieldType in usedFieldTypes.keys) {
var properties = usedFieldTypes[fieldType];
for (final property in properties) {
ctxCode.writeln('case ${property.definition.name}Base'
'::${property.name}PropertyKey:');
}
ctxCode.writeln('return Core${fieldType.capitalizedName}Type::id;');
}
ctxCode.writeln('default: return -1;}}');
ctxCode.writeln('};}');
var output = generatedHppPath;
var folder =
output != null && output.isNotEmpty && output[output.length - 1] == '/'
? output.substring(0, output.length - 1)
: output;
var file = File('$folder/core_registry.hpp');
file.createSync(recursive: true);
var formattedCode =
await _formatter.formatAndGuard('CoreRegistry', ctxCode.toString());
file.writeAsStringSync(formattedCode, flush: true);
return true;
}
}