Improve documentation with properties of objects and behaviors

This commit is contained in:
Florian Rival
2025-11-06 17:19:37 +01:00
parent dad33c30a9
commit 3a9645a55f
8 changed files with 315 additions and 49 deletions
+2 -12
View File
@@ -203,6 +203,7 @@ interface Platform {
boolean IsExtensionLoaded([Const] DOMString name);
void RemoveExtension([Const] DOMString name);
void ReloadBuiltinExtensions();
[Value] UniquePtrObjectConfiguration CreateObjectConfiguration([Const] DOMString type);
[Const, Ref] VectorPlatformExtension GetAllPlatformExtensions();
};
@@ -210,19 +211,8 @@ interface Platform {
interface JsPlatform {
[Ref] JsPlatform STATIC_Get();
void AddNewExtension([Const, Ref] PlatformExtension extension);
// Inherited from Platform:
[Const, Value] DOMString GetName();
[Const, Value] DOMString GetFullName();
[Const, Value] DOMString GetSubtitle();
[Const, Value] DOMString GetDescription();
[Const, Ref] InstructionOrExpressionGroupMetadata GetInstructionOrExpressionGroupMetadata([Const] DOMString name);
boolean IsExtensionLoaded([Const] DOMString name);
void RemoveExtension([Const] DOMString name);
void ReloadBuiltinExtensions();
[Const, Ref] VectorPlatformExtension GetAllPlatformExtensions();
};
JsPlatform implements Platform;
interface PairStringVariable {
void PairStringVariable();
+2 -10
View File
@@ -268,21 +268,13 @@ export class Platform extends EmscriptenObject {
isExtensionLoaded(name: string): boolean;
removeExtension(name: string): void;
reloadBuiltinExtensions(): void;
createObjectConfiguration(type: string): UniquePtrObjectConfiguration;
getAllPlatformExtensions(): VectorPlatformExtension;
}
export class JsPlatform extends EmscriptenObject {
export class JsPlatform extends Platform {
static get(): JsPlatform;
addNewExtension(extension: PlatformExtension): void;
getName(): string;
getFullName(): string;
getSubtitle(): string;
getDescription(): string;
getInstructionOrExpressionGroupMetadata(name: string): InstructionOrExpressionGroupMetadata;
isExtensionLoaded(name: string): boolean;
removeExtension(name: string): void;
reloadBuiltinExtensions(): void;
getAllPlatformExtensions(): VectorPlatformExtension;
}
export class PairStringVariable extends EmscriptenObject {
-9
View File
@@ -2,15 +2,6 @@
declare class gdJsPlatform extends gdPlatform {
static get(): gdJsPlatform;
addNewExtension(extension: gdPlatformExtension): void;
getName(): string;
getFullName(): string;
getSubtitle(): string;
getDescription(): string;
getInstructionOrExpressionGroupMetadata(name: string): gdInstructionOrExpressionGroupMetadata;
isExtensionLoaded(name: string): boolean;
removeExtension(name: string): void;
reloadBuiltinExtensions(): void;
getAllPlatformExtensions(): gdVectorPlatformExtension;
delete(): void;
ptr: number;
};
+1
View File
@@ -8,6 +8,7 @@ declare class gdPlatform {
isExtensionLoaded(name: string): boolean;
removeExtension(name: string): void;
reloadBuiltinExtensions(): void;
createObjectConfiguration(type: string): gdUniquePtrObjectConfiguration;
getAllPlatformExtensions(): gdVectorPlatformExtension;
delete(): void;
ptr: number;
@@ -28,6 +28,9 @@ const { groupBy, sortKeys } = require('./lib/ArrayHelpers');
const { generateAllExtensionsSections } = require('./lib/WikiExtensionTable');
/** @typedef {import("./lib/ExtensionReferenceGenerator.js").RawText} RawText */
/** @typedef {import('../../../GDevelop.js/types').EventsFunctionsExtension} EventsFunctionsExtension */
/** @typedef {import('../../../GDevelop.js/types').Project} Project */
/** @typedef {import('../../../GDevelop.js/types').PlatformExtension} PlatformExtension */
/** @typedef {{ tier: 'community' | 'experimental' | 'reviewed', shortDescription: string, authorIds: Array<string>, authors?: Array<{id: string, username: string}>, extensionNamespace: string, fullName: string, name: string, version: string, gdevelopVersion?: string, url: string, headerUrl: string, tags: Array<string>, category: string, previewIconUrl: string, eventsBasedBehaviorsCount: number, eventsFunctionsCount: number}} ExtensionShortHeader */
@@ -60,9 +63,9 @@ const generateAuthorNamesWithLinks = authors => {
* (useful as containing author public profiles information).
*
* @param {any} gd
* @param {any} project (gdProject)
* @param {Project} project
*
* @returns {Promise<any>} A promise to all extensions (gdEventsFunctionsExtension)
* @returns {Promise<Array<EventsFunctionsExtension>>}
*/
const addAllExtensionsToProject = async (gd, project) => {
const response = await axios.get(extensionShortHeadersUrl);
@@ -149,23 +152,27 @@ const getAllExtensionShortHeaders = async () => {
* Create a page for an extension.
* @param {any} gd
* @param {any} project (gdProject)
* @param {any} extension The extension (gdEventsFunctionsExtension)
* @param {EventsFunctionsExtension} eventsFunctionsExtension
* @param {ExtensionShortHeader} extensionShortHeader
* @param {boolean} isExperimental The tier
*/
const createExtensionReferencePage = async (
gd,
project,
extension,
eventsFunctionsExtension,
extensionShortHeader,
isExperimental
) => {
const extensionMetadata = generateEventsFunctionExtensionMetadata(
const platformExtension = generateEventsFunctionExtensionMetadata(
gd,
project,
extension
eventsFunctionsExtension
);
const extensionReference = generateExtensionReference(extensionMetadata);
const extensionReference = generateExtensionReference({
platform: gd.JsPlatform.get(),
extension: platformExtension,
eventsFunctionsExtension,
});
const referencePageContent = rawTextsToString(
generateExtensionRawText(
extensionReference,
@@ -179,7 +186,7 @@ const createExtensionReferencePage = async (
)
);
const folderName = getExtensionFolderName(extension.getName());
const folderName = getExtensionFolderName(platformExtension.getName());
const extensionReferenceFilePath = path.join(
extensionsRootPath,
folderName,
@@ -246,7 +253,7 @@ const generateExtensionFooterText = ({ extension }) => {
`
---
*This page is an auto-generated reference page about the **${extension.getFullName()}** extension, made by the community of [GDevelop, the open-source, cross-platform game engine designed for everyone](https://gdevelop.io/).*` +
*This page is an auto-generated reference page about the **${extension.getFullName()}** extension for [GDevelop, the open-source, AI-powered, cross-platform game engine designed for everyone](https://gdevelop.io/).*` +
' ' +
'Learn more about [all GDevelop community-made extensions here](/gdevelop5/extensions).',
};
@@ -254,9 +261,9 @@ const generateExtensionFooterText = ({ extension }) => {
/**
* Generate the metadata for the events based extension
* @param {any} project A project containing of the extensions (gdProject)
* @param {any} eventsFunctionsExtension An extension (gdEventsFunctionsExtension)
* @returns {any} the extension metadata (gdPlatformExtension)
* @param {Project} project
* @param {EventsFunctionsExtension} eventsFunctionsExtension
* @returns {PlatformExtension}
*/
const generateEventsFunctionExtensionMetadata = (
gd,
@@ -82,7 +82,11 @@ const generateAllExtensionReferences = gd => {
/** @type {Array<ExtensionReference>} */
const extensionReferences = mapVector(
platformExtensions,
generateExtensionReference
(platformExtension) => generateExtensionReference({
platform: gd.JsPlatform.get(),
extension: platformExtension,
eventsFunctionsExtension: null,
})
);
return extensionReferences;
@@ -157,7 +161,7 @@ const generateExtensionFooterText = ({ extension }) => {
text:
`\n\n---\n\n` +
generateBuiltInExtensionNote({ extension }) +
`*This page is an auto-generated reference page about the **${extension.getFullName()}** feature of [GDevelop, the open-source, cross-platform game engine designed for everyone](https://gdevelop.io/).*` +
`*This page is an auto-generated reference page about the **${extension.getFullName()}** feature of [GDevelop, the open-source, AI-powered, cross-platform game engine designed for everyone](https://gdevelop.io/).*` +
' ' +
'Learn more about [all GDevelop features here](/gdevelop5/all-features).',
};
@@ -1,15 +1,23 @@
// @ts-check
const { mapFor } = require('./MapFor');
const { generateReadMoreLink } = require('./WikiHelpLink');
const { isNullPtr } = require('./IsNullPtr');
const { mapVector } = require('./MapFor');
// Types definitions used in this script:
/** @typedef {import('../../../../GDevelop.js/types').Platform} Platform */
/** @typedef {import('../../../../GDevelop.js/types').PlatformExtension} PlatformExtension */
/** @typedef {import('../../../../GDevelop.js/types').EventsFunctionsExtension} EventsFunctionsExtension */
/** @typedef {import('../../../../GDevelop.js/types').MapStringExpressionMetadata} MapStringExpressionMetadata */
/** @typedef {import('../../../../GDevelop.js/types').ExpressionMetadata} ExpressionMetadata */
/** @typedef {import('../../../../GDevelop.js/types').ObjectMetadata} ObjectMetadata */
/** @typedef {import('../../../../GDevelop.js/types').BehaviorMetadata} BehaviorMetadata */
/** @typedef {import('../../../../GDevelop.js/types').ParameterMetadata} ParameterMetadata */
/** @typedef {import('../../../../GDevelop.js/types').MapStringPropertyDescriptor} MapStringPropertyDescriptor */
/** @typedef {import('../../../../GDevelop.js/types').NamedPropertyDescriptor} NamedPropertyDescriptor */
/** @typedef {import('../../../../GDevelop.js/types').PropertiesContainer} PropertiesContainer */
/** @typedef {import('../../../../GDevelop.js/types').PropertyDescriptor} PropertyDescriptor */
/**
* @typedef {Object} RawText A text to be shown on a page
@@ -25,6 +33,7 @@ const { generateReadMoreLink } = require('./WikiHelpLink');
/**
* @typedef {Object} ObjectReference
* @prop {ObjectMetadata} objectMetadata The object.
* @prop {Array<ReferenceText>} propertiesReferenceTexts Reference texts for the object properties.
* @prop {Array<ReferenceText>} actionsReferenceTexts Reference texts for the object actions.
* @prop {Array<ReferenceText>} conditionsReferenceTexts Reference texts for the object conditions.
* @prop {Array<ReferenceText>} expressionsReferenceTexts Reference texts for the object expressions.
@@ -33,6 +42,8 @@ const { generateReadMoreLink } = require('./WikiHelpLink');
/**
* @typedef {Object} BehaviorReference
* @prop {BehaviorMetadata} behaviorMetadata The behavior.
* @prop {Array<ReferenceText>} propertiesReferenceTexts Reference texts for the behavior properties.
* @prop {Array<ReferenceText>} sharedPropertiesReferenceTexts Reference texts for the behavior shared properties.
* @prop {Array<ReferenceText>} actionsReferenceTexts Reference texts for the behavior actions.
* @prop {Array<ReferenceText>} conditionsReferenceTexts Reference texts for the behavior conditions.
* @prop {Array<ReferenceText>} expressionsReferenceTexts Reference texts for the behavior expressions.
@@ -41,6 +52,7 @@ const { generateReadMoreLink } = require('./WikiHelpLink');
/**
* @typedef {Object} ExtensionReference
* @prop {PlatformExtension} extension The extension.
* @prop {EventsFunctionsExtension | null} eventsFunctionsExtension The "source" events-based extension.
* @prop {Array<ReferenceText>} freeExpressionsReferenceTexts Reference texts for free expressions.
* @prop {Array<ReferenceText>} freeActionsReferenceTexts Reference texts for free actions.
* @prop {Array<ReferenceText>} freeConditionsReferenceTexts Reference texts for free conditions.
@@ -166,7 +178,9 @@ const sanitizeExpressionDescription = str => {
);
};
const translateTypeToHumanReadableDescription = type => {
const translateTypeToHumanReadableDescription = anyType => {
const type = anyType.toLowerCase();
if (type === 'number') return '🔢 Number';
if (type === 'expression') return '🔢 Number';
if (type === 'camera') return '🔢 Camera index (Number)';
@@ -208,6 +222,11 @@ const translateTypeToHumanReadableDescription = type => {
if (type === 'yesorno') return '❓ Yes or No';
if (type === 'trueorfalse') return '❓ True or False';
if (type === 'multilinestring') return '🔤 Multiline text (String)';
if (type === 'boolean') return '🔘 Boolean';
if (type === 'color') return '🎨 Color';
if (type === 'resource') return '🗂️ Resource';
return type;
};
@@ -575,10 +594,188 @@ const sortReferenceTexts = (referenceText1, referenceText2) => {
};
/**
* @param {PlatformExtension} extension
* @param {string} text
* @returns {string}
*/
const sanitizePropertyDescription = text => {
const sanitizedText = text.replace(/\n/g, ' ').trim();
if (sanitizedText.endsWith('.')) return sanitizedText;
return sanitizedText + '.';
};
/**
* @param {{ propertiesMetadata?: MapStringPropertyDescriptor, propertiesContainer?: PropertiesContainer }} options
* @returns {Array<ReferenceText>}
*/
const generatePropertiesReferenceTexts = ({
propertiesMetadata,
propertiesContainer,
}) => {
/**
* @param {string} name
* @param {PropertyDescriptor | NamedPropertyDescriptor} property
* @returns {ReferenceText | null}
*/
const generatePropertyReferenceText = (name, property) => {
if (name.startsWith('_')) return null;
if (property.isHidden()) return null;
if (property.getType() === 'Behavior') {
return null;
}
const type = property.getType();
const measurementUnit = property.getMeasurementUnit();
const measurementUnitText = measurementUnit.isUndefined()
? null
: measurementUnit.getLabel();
const choices =
type.toLowerCase() === 'choice'
? [
...mapVector(property.getChoices(), choice => choice.getValue()),
// TODO Remove this once we made sure no built-in extension still use `addExtraInfo` instead of `addChoice`.
...property.getExtraInfo().toJSArray(),
]
: null;
const information = [
translateTypeToHumanReadableDescription(type),
choices
? `one of: ${choices.map(choice => `"${choice}"`).join(', ')}`
: null,
measurementUnitText,
].filter(Boolean);
return {
orderKey: name,
text: [
`- **${property.getLabel() || name}** (${information.join(', ')}).`,
property.getDescription()
? `${sanitizePropertyDescription(property.getDescription())}`
: null,
property.getValue()
? `Default value is \`${property.getValue()}\`.`
: null,
]
.filter(Boolean)
.join(' '),
};
};
/**
* @param {string} name
* @param {PropertyDescriptor | NamedPropertyDescriptor} property
* @returns {ReferenceText | null}
*/
const generatePropertyInternalReferenceText = (name, property) => {
if (name.startsWith('_')) return null;
if (property.isHidden()) return null;
const padding = ' ';
if (property.getType() === 'Behavior') {
return {
orderKey: name,
text: `${padding}> This behavior must be used on an object also having a behavior with type "${property
.getExtraInfo()
.toJSArray()
.join(', ')}". This is stored on property \`${name}\`.\n`,
};
}
const type = property.getType();
const measurementUnit = property.getMeasurementUnit();
const measurementUnitText = measurementUnit.isUndefined()
? null
: measurementUnit.getName();
return {
orderKey: name,
text: [
`${padding}- **${property.getLabel() ||
name}** is stored as \`${name}\` (${type}).`,
measurementUnitText ? `Unit is ${measurementUnitText}.` : null,
`Default value is \`${property.getValue()}\`.`,
]
.filter(Boolean)
.join(' '),
};
};
const propertiesReferenceTexts = [
...(propertiesMetadata
? propertiesMetadata
.keys()
.toJSArray()
.map(propertyName => {
const propertyDescriptor = propertiesMetadata.get(propertyName);
return generatePropertyReferenceText(
propertyName,
propertyDescriptor
);
})
: []),
...(propertiesContainer
? mapVector(propertiesContainer, namedPropertyDescriptor => {
const propertyName = namedPropertyDescriptor.getName();
return generatePropertyReferenceText(
propertyName,
namedPropertyDescriptor
);
})
: []),
].filter(Boolean);
if (!propertiesReferenceTexts.length) return [];
const internalPropertiesReferenceTexts = [
...(propertiesMetadata
? propertiesMetadata
.keys()
.toJSArray()
.map(propertyName => {
const propertyDescriptor = propertiesMetadata.get(propertyName);
return generatePropertyInternalReferenceText(
propertyName,
propertyDescriptor
);
})
: []),
...(propertiesContainer
? mapVector(propertiesContainer, namedPropertyDescriptor => {
const propertyName = namedPropertyDescriptor.getName();
return generatePropertyInternalReferenceText(
propertyName,
namedPropertyDescriptor
);
})
: []),
].filter(Boolean);
return [
...propertiesReferenceTexts,
{
orderKey: '',
text: '\n??? quote "See internal technical details"\n\n',
},
...internalPropertiesReferenceTexts,
];
};
/**
* @param {{
* platform: Platform,
* extension: PlatformExtension,
* eventsFunctionsExtension: EventsFunctionsExtension | null
* }} options
* @returns {ExtensionReference}
*/
const generateExtensionReference = extension => {
const generateExtensionReference = ({
platform,
extension,
eventsFunctionsExtension,
}) => {
const extensionExpressions = extension.getAllExpressions();
const extensionStrExpressions = extension.getAllStrExpressions();
@@ -595,6 +792,45 @@ const generateExtensionReference = extension => {
if (objectMetadata.isPrivate()) {
return null;
}
const propertiesReferenceTexts = [];
if (eventsFunctionsExtension) {
// Objects from "events-based" extensions:
const eventsBasedObjects = eventsFunctionsExtension.getEventsBasedObjects();
const objectName =
objectType.split('::').pop() || 'Unrecognized object type format';
if (eventsBasedObjects.has(objectName)) {
const eventsBasedObject = eventsBasedObjects.get(objectName);
propertiesReferenceTexts.push(
...generatePropertiesReferenceTexts({
propertiesContainer: eventsBasedObject.getPropertyDescriptors(),
})
);
} else {
throw new Error(
`Object "${objectType}" not found in events-based extension "${eventsFunctionsExtension.getFullName()}".`
);
}
} else {
// "Built-in" objects:
const objectConfiguration = platform
.createObjectConfiguration(objectType)
.release();
if (isNullPtr(objectConfiguration)) {
throw new Error(
`Failed to create object configuration for object type "${objectType}".`
);
}
propertiesReferenceTexts.push(
...generatePropertiesReferenceTexts({
propertiesMetadata: objectConfiguration.getProperties(),
})
);
}
const actionsReferenceTexts = generateInstructionsReferenceRowsTexts({
areConditions: false,
instructionsMetadata: extension.getAllActionsForObject(objectType),
@@ -621,6 +857,7 @@ const generateExtensionReference = extension => {
return {
objectMetadata,
propertiesReferenceTexts,
actionsReferenceTexts,
conditionsReferenceTexts,
expressionsReferenceTexts,
@@ -638,6 +875,13 @@ const generateExtensionReference = extension => {
return null;
}
const propertiesReferenceTexts = generatePropertiesReferenceTexts({
propertiesMetadata: behaviorMetadata.getProperties(),
});
const sharedPropertiesReferenceTexts = generatePropertiesReferenceTexts({
propertiesMetadata: behaviorMetadata.getSharedProperties(),
});
const actionsReferenceTexts = generateInstructionsReferenceRowsTexts({
areConditions: false,
instructionsMetadata: extension.getAllActionsForBehavior(behaviorType),
@@ -668,6 +912,8 @@ const generateExtensionReference = extension => {
return {
behaviorMetadata,
propertiesReferenceTexts,
sharedPropertiesReferenceTexts,
actionsReferenceTexts,
conditionsReferenceTexts,
expressionsReferenceTexts,
@@ -696,6 +942,7 @@ const generateExtensionReference = extension => {
return {
extension,
eventsFunctionsExtension,
freeActionsReferenceTexts,
freeConditionsReferenceTexts,
freeExpressionsReferenceTexts,
@@ -727,7 +974,7 @@ const generateExtensionRawText = (
const withHeaderIfNotEmpty = (texts, { headerName, depth }) => {
if (!texts.length) return [];
return [generateHeader({ headerName, depth }), ...texts];
return [generateHeader({ headerName, depth }), ...texts, { text: '' }];
};
return [
@@ -750,6 +997,7 @@ const generateExtensionRawText = (
...objectReferences.flatMap(objectReference => {
const {
objectMetadata,
propertiesReferenceTexts,
actionsReferenceTexts,
conditionsReferenceTexts,
expressionsReferenceTexts,
@@ -761,6 +1009,10 @@ const generateExtensionRawText = (
showExtensionName: false,
showHelpLink: false,
}),
...withHeaderIfNotEmpty(propertiesReferenceTexts, {
headerName: 'Object properties',
depth: 3,
}),
...withHeaderIfNotEmpty(actionsReferenceTexts, {
headerName: 'Object actions',
depth: 3,
@@ -781,6 +1033,8 @@ const generateExtensionRawText = (
...behaviorReferences.flatMap(behaviorReference => {
const {
behaviorMetadata,
propertiesReferenceTexts,
sharedPropertiesReferenceTexts,
actionsReferenceTexts,
conditionsReferenceTexts,
expressionsReferenceTexts,
@@ -792,6 +1046,14 @@ const generateExtensionRawText = (
showExtensionName: false,
showHelpLink: false,
}),
...withHeaderIfNotEmpty(propertiesReferenceTexts, {
headerName: 'Behavior properties',
depth: 3,
}),
...withHeaderIfNotEmpty(sharedPropertiesReferenceTexts, {
headerName: 'Behavior shared properties',
depth: 3,
}),
...withHeaderIfNotEmpty(actionsReferenceTexts, {
headerName: 'Behavior actions',
depth: 3,
+19
View File
@@ -0,0 +1,19 @@
// @ts-check
/** @typedef {import('../../../../GDevelop.js/types').EmscriptenObject} EmscriptenObject */
/**
* Check if the Emscripten object is actually a null pointer.
*
* You should normally not use this - instead prefer the null/empty object pattern,
* with a function `IsBadXxx` to check if the object you're manipulating represents
* an null/empty object.
*
* @param {EmscriptenObject} object
* @returns {boolean}
*/
const isNullPtr = object => object.ptr === 0;
module.exports = {
isNullPtr,
};