Compare commits

...

1 Commits

Author SHA1 Message Date
Florian Rival
0c2913bbff Allow AI to change layers/effects/scene and some game properties 2025-08-29 18:19:25 +02:00
26 changed files with 819 additions and 13 deletions

View File

@@ -277,6 +277,10 @@ class GD_CORE_API MetadataProvider {
return &metadata == &badObjectInfo;
}
static bool IsBadEffectMetadata(const gd::EffectMetadata& metadata) {
return &metadata == &badEffectMetadata;
}
virtual ~MetadataProvider();
private:

View File

@@ -1908,7 +1908,9 @@ module.exports = {
.addEffect('AmbientLight')
.setFullName(_('Ambient light'))
.setDescription(
_('A light that illuminates all objects from every direction.')
_(
'A light that illuminates all objects from every direction. Often used along with a Directional light (though a Hemisphere light can be used instead of an Ambient light).'
)
)
.markAsNotWorkingForObjects()
.markAsOnlyWorkingFor3D()
@@ -1929,7 +1931,11 @@ module.exports = {
const effect = extension
.addEffect('DirectionalLight')
.setFullName(_('Directional light'))
.setDescription(_('A very far light source like the sun.'))
.setDescription(
_(
"A very far light source like the sun. This is the light to use for casting shadows for 3D objects (other lights won't emit shadows). Often used along with a Hemisphere light."
)
)
.markAsNotWorkingForObjects()
.markAsOnlyWorkingFor3D()
.addIncludeFile('Extensions/3D/DirectionalLight.js');
@@ -2013,7 +2019,7 @@ module.exports = {
.setFullName(_('Hemisphere light'))
.setDescription(
_(
'A light that illuminates objects from every direction with a gradient.'
'A light that illuminates objects from every direction with a gradient. Often used along with a Directional light.'
)
)
.markAsNotWorkingForObjects()

View File

@@ -1143,6 +1143,7 @@ interface LayersContainer {
boolean HasLayerNamed([Const] DOMString name);
void RemoveLayer([Const] DOMString name);
unsigned long GetLayersCount();
unsigned long GetLayerPosition([Const] DOMString name);
void SwapLayers(unsigned long firstLayerIndex, unsigned long secondLayerIndex);
void MoveLayer(unsigned long oldIndex, unsigned long newIndex);
void SerializeLayersTo([Ref] SerializerElement element);
@@ -2932,6 +2933,7 @@ interface MetadataProvider {
boolean STATIC_IsBadInstructionMetadata([Const, Ref] InstructionMetadata metadata);
boolean STATIC_IsBadBehaviorMetadata([Const, Ref] BehaviorMetadata metadata);
boolean STATIC_IsBadObjectMetadata([Const, Ref] ObjectMetadata metadata);
boolean STATIC_IsBadEffectMetadata([Const, Ref] EffectMetadata metadata);
};
enum ProjectDiagnostic_ErrorType {

View File

@@ -606,6 +606,7 @@ typedef std::vector<gd::PropertyDescriptorChoice> VectorPropertyDescriptorChoice
#define STATIC_IsBadInstructionMetadata IsBadInstructionMetadata
#define STATIC_IsBadBehaviorMetadata IsBadBehaviorMetadata
#define STATIC_IsBadObjectMetadata IsBadObjectMetadata
#define STATIC_IsBadEffectMetadata IsBadEffectMetadata
#define STATIC_RenameObjectInEvents RenameObjectInEvents
#define STATIC_RemoveObjectInEvents RemoveObjectInEvents

View File

@@ -941,6 +941,7 @@ export class LayersContainer extends EmscriptenObject {
hasLayerNamed(name: string): boolean;
removeLayer(name: string): void;
getLayersCount(): number;
getLayerPosition(name: string): number;
swapLayers(firstLayerIndex: number, secondLayerIndex: number): void;
moveLayer(oldIndex: number, newIndex: number): void;
serializeLayersTo(element: SerializerElement): void;
@@ -2109,6 +2110,7 @@ export class MetadataProvider extends EmscriptenObject {
static isBadInstructionMetadata(metadata: InstructionMetadata): boolean;
static isBadBehaviorMetadata(metadata: BehaviorMetadata): boolean;
static isBadObjectMetadata(metadata: ObjectMetadata): boolean;
static isBadEffectMetadata(metadata: EffectMetadata): boolean;
}
export class ProjectDiagnostic extends EmscriptenObject {

View File

@@ -7,6 +7,7 @@ declare class gdLayersContainer {
hasLayerNamed(name: string): boolean;
removeLayer(name: string): void;
getLayersCount(): number;
getLayerPosition(name: string): number;
swapLayers(firstLayerIndex: number, secondLayerIndex: number): void;
moveLayer(oldIndex: number, newIndex: number): void;
serializeLayersTo(element: gdSerializerElement): void;

View File

@@ -26,6 +26,7 @@ declare class gdMetadataProvider {
static isBadInstructionMetadata(metadata: gdInstructionMetadata): boolean;
static isBadBehaviorMetadata(metadata: gdBehaviorMetadata): boolean;
static isBadObjectMetadata(metadata: gdObjectMetadata): boolean;
static isBadEffectMetadata(metadata: gdEffectMetadata): boolean;
delete(): void;
ptr: number;
};

View File

@@ -5,6 +5,7 @@ import { I18n } from '@lingui/react';
import {
type RenderEditorContainerPropsWithRef,
type SceneEventsOutsideEditorChanges,
type InstancesOutsideEditorChanges,
} from '../MainFrame/EditorContainers/BaseEditor';
import { type ObjectWithContext } from '../ObjectsList/EnumerateObjects';
import Paper from '../UI/Paper';
@@ -67,6 +68,7 @@ const useProcessFunctionCalls = ({
getEditorFunctionCallResults,
addEditorFunctionCallResults,
onSceneEventsModifiedOutsideEditor,
onInstancesModifiedOutsideEditor,
onExtensionInstalled,
}: {|
i18n: I18nType,
@@ -85,6 +87,9 @@ const useProcessFunctionCalls = ({
onSceneEventsModifiedOutsideEditor: (
changes: SceneEventsOutsideEditorChanges
) => void,
onInstancesModifiedOutsideEditor: (
changes: InstancesOutsideEditorChanges
) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|}) => {
const { ensureExtensionInstalled } = useEnsureExtensionInstalled({
@@ -159,6 +164,7 @@ const useProcessFunctionCalls = ({
});
},
onSceneEventsModifiedOutsideEditor,
onInstancesModifiedOutsideEditor,
ensureExtensionInstalled,
searchAndInstallAsset,
});
@@ -179,6 +185,7 @@ const useProcessFunctionCalls = ({
searchAndInstallAsset,
generateEvents,
onSceneEventsModifiedOutsideEditor,
onInstancesModifiedOutsideEditor,
triggerSendEditorFunctionCallResults,
editorCallbacks,
]
@@ -349,6 +356,9 @@ type Props = {|
onSceneEventsModifiedOutsideEditor: (
changes: SceneEventsOutsideEditorChanges
) => void,
onInstancesModifiedOutsideEditor: (
changes: InstancesOutsideEditorChanges
) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
initialMode: 'chat' | 'agent' | null,
initialAiRequestId: string | null,
@@ -372,6 +382,9 @@ export type AskAiEditorInterface = {|
onSceneEventsModifiedOutsideEditor: (
changes: SceneEventsOutsideEditorChanges
) => void,
onInstancesModifiedOutsideEditor: (
changes: InstancesOutsideEditorChanges
) => void,
startOrOpenChat: ({|
mode: 'chat' | 'agent',
aiRequestId: string | null,
@@ -401,6 +414,7 @@ export const AskAiEditor = React.memo<Props>(
onCreateProjectFromExample,
onOpenLayout,
onSceneEventsModifiedOutsideEditor,
onInstancesModifiedOutsideEditor,
onExtensionInstalled,
initialMode,
initialAiRequestId,
@@ -518,6 +532,7 @@ export const AskAiEditor = React.memo<Props>(
onSceneObjectEdited: noop,
onSceneObjectsDeleted: noop,
onSceneEventsModifiedOutsideEditor: noop,
onInstancesModifiedOutsideEditor: noop,
startOrOpenChat: onStartOrOpenChat,
}));
@@ -655,6 +670,7 @@ export const AskAiEditor = React.memo<Props>(
fileMetadata,
storageProviderName,
mode,
toolsVersion: 'v2',
aiConfiguration: {
presetId: aiConfigurationPresetId,
},
@@ -944,6 +960,7 @@ export const AskAiEditor = React.memo<Props>(
getEditorFunctionCallResults,
addEditorFunctionCallResults,
onSceneEventsModifiedOutsideEditor,
onInstancesModifiedOutsideEditor,
i18n,
onExtensionInstalled,
});
@@ -1044,6 +1061,9 @@ export const renderAskAiEditorContainer = (
onSceneEventsModifiedOutsideEditor={
props.onSceneEventsModifiedOutsideEditor
}
onInstancesModifiedOutsideEditor={
props.onInstancesModifiedOutsideEditor
}
onExtensionInstalled={props.onExtensionInstalled}
initialMode={
(props.extraEditorProps && props.extraEditorProps.mode) || null

View File

@@ -10,6 +10,7 @@ import {
type AssetSearchAndInstallOptions,
type AssetSearchAndInstallResult,
type SceneEventsOutsideEditorChanges,
type InstancesOutsideEditorChanges,
} from '.';
export type EditorFunctionCallResult =
@@ -39,6 +40,9 @@ export type ProcessEditorFunctionCallsOptions = {|
onSceneEventsModifiedOutsideEditor: (
changes: SceneEventsOutsideEditorChanges
) => void,
onInstancesModifiedOutsideEditor: (
changes: InstancesOutsideEditorChanges
) => void,
ensureExtensionInstalled: (options: {|
extensionName: string,
|}) => Promise<void>,
@@ -53,6 +57,7 @@ export const processEditorFunctionCalls = async ({
editorCallbacks,
generateEvents,
onSceneEventsModifiedOutsideEditor,
onInstancesModifiedOutsideEditor,
ignore,
ensureExtensionInstalled,
searchAndInstallAsset,
@@ -136,6 +141,7 @@ export const processEditorFunctionCalls = async ({
args,
generateEvents,
onSceneEventsModifiedOutsideEditor,
onInstancesModifiedOutsideEditor,
ensureExtensionInstalled,
searchAndInstallAsset,
}

View File

@@ -32,6 +32,12 @@ export type ExpressionSummary = {|
relevantForSceneEvents?: boolean,
|};
export type PropertySummary = {|
name: string,
description: string,
type: string,
|};
export type ObjectSummary = {|
name: string,
fullName: string,
@@ -51,6 +57,17 @@ export type BehaviorSummary = {|
expressions: Array<ExpressionSummary>,
|};
export type EffectSummary = {|
name: string,
fullName: string,
description: string,
notWorkingForObjects: boolean,
onlyWorkingFor2D: boolean,
onlyWorkingFor3D: boolean,
unique: boolean,
properties: Array<PropertySummary>,
|};
export type ExtensionSummary = {|
extensionName: string,
extensionFullName: string,
@@ -60,6 +77,7 @@ export type ExtensionSummary = {|
freeExpressions: Array<ExpressionSummary>,
objects: { [string]: ObjectSummary },
behaviors: { [string]: BehaviorSummary },
effects: { [string]: EffectSummary },
|};
const normalizeType = (parameterType: string) => {
@@ -102,6 +120,29 @@ const getParameterSummary = (
return parameterSummary;
};
const getPropertySummary = (
propertyName: string,
property: gdPropertyDescriptor
) => {
return {
name: propertyName,
description: property.getDescription(),
type: property.getType(),
};
};
const getPropertiesSummary = (
propertiesMetadata: gdMapStringPropertyDescriptor
) => {
return propertiesMetadata
.keys()
.toJSArray()
.map(propertyName => {
const property = propertiesMetadata.get(propertyName);
return getPropertySummary(propertyName, property);
});
};
export const buildExtensionSummary = ({
gd,
extension,
@@ -111,6 +152,7 @@ export const buildExtensionSummary = ({
}): ExtensionSummary => {
const objects: { [string]: ObjectSummary } = {};
const behaviors: { [string]: BehaviorSummary } = {};
const effects: { [string]: EffectSummary } = {};
const generateInstructionsSummaries = ({
instructionsMetadata,
@@ -254,6 +296,27 @@ export const buildExtensionSummary = ({
behaviors[behaviorType] = behaviorSummary;
});
extension
.getExtensionEffectTypes()
.toJSArray()
.forEach(effectType => {
const effectMetadata = extension.getEffectMetadata(effectType);
if (gd.MetadataProvider.isBadEffectMetadata(effectMetadata)) {
return;
}
const effectSummary: EffectSummary = {
name: effectMetadata.getType(),
fullName: effectMetadata.getFullName(),
description: effectMetadata.getDescription(),
notWorkingForObjects: effectMetadata.isMarkedAsNotWorkingForObjects(),
onlyWorkingFor2D: effectMetadata.isMarkedAsOnlyWorkingFor2D(),
onlyWorkingFor3D: effectMetadata.isMarkedAsOnlyWorkingFor3D(),
unique: effectMetadata.isMarkedAsUnique(),
properties: getPropertiesSummary(effectMetadata.getProperties()),
};
effects[effectType] = effectSummary;
});
return {
extensionName: extension.getName(),
@@ -275,5 +338,6 @@ export const buildExtensionSummary = ({
],
objects,
behaviors,
effects,
};
};

View File

@@ -894,6 +894,7 @@ describe('SimplifiedProject', () => {
},
},
"description": "A fake extension with a fake behavior containing 2 properties.",
"effects": Object {},
"extensionFullName": "Fake extension with a fake behavior",
"extensionName": "FakeBehavior",
"freeActions": Array [],

View File

@@ -17,6 +17,7 @@ import { Trans } from '@lingui/macro';
import Link from '../UI/Link';
import {
hexNumberToRGBArray,
rgbColorToHex,
rgbOrHexToHexNumber,
} from '../Utils/ColorTransformer';
import { type SimplifiedBehavior } from './SimplifiedProject/SimplifiedProject';
@@ -63,10 +64,13 @@ export type EditorFunctionGenericOutput = {|
properties?: any,
sharedProperties?: any,
instances?: any,
layers?: any,
behaviors?: Array<SimplifiedBehavior>,
animationNames?: string,
generatedEventsErrorDiagnostics?: string,
aiGeneratedEventId?: string,
propertiesLayersEffectsForSceneNamed?: string,
warnings?: string,
|};
export type EventsGenerationResult =
@@ -123,6 +127,10 @@ export type SceneEventsOutsideEditorChanges = {|
newOrChangedAiGeneratedEventIds: Set<string>,
|};
export type InstancesOutsideEditorChanges = {|
scene: gdLayout,
|};
/**
* A function that does something in the editor on the given project.
*/
@@ -146,6 +154,9 @@ export type EditorFunction = {|
onSceneEventsModifiedOutsideEditor: (
changes: SceneEventsOutsideEditorChanges
) => void,
onInstancesModifiedOutsideEditor: (
changes: InstancesOutsideEditorChanges
) => void,
ensureExtensionInstalled: (options: {|
extensionName: string,
|}) => Promise<void>,
@@ -1493,7 +1504,6 @@ const put2dInstances: EditorFunction = {
// Create the array of existing instances to move/modify, and new instances to create.
const modifiedAndCreatedInstances: Array<gdInitialInstance> = [];
iterateOnInstances(initialInstances, instance => {
if (instance.getLayer() !== layer_name) return;
if (instance.getObjectName() !== object_name) return;
if (
existingInstanceIds.some(id =>
@@ -1501,6 +1511,8 @@ const put2dInstances: EditorFunction = {
)
) {
modifiedAndCreatedInstances.push(instance);
// Take the opportunity to move to a new layer if specified.
instance.setLayer(layer_name);
}
});
for (let i = 0; i < newInstancesCount; i++) {
@@ -1846,7 +1858,6 @@ const put3dInstances: EditorFunction = {
// Create the array of existing instances to move/modify, and new instances to create.
const modifiedAndCreatedInstances: Array<gdInitialInstance> = [];
iterateOnInstances(initialInstances, instance => {
if (instance.getLayer() !== layer_name) return;
if (instance.getObjectName() !== object_name) return;
if (
existingInstanceIds.some(id =>
@@ -1854,6 +1865,8 @@ const put3dInstances: EditorFunction = {
)
) {
modifiedAndCreatedInstances.push(instance);
// Take the opportunity to move to a new layer if specified.
instance.setLayer(layer_name);
}
});
for (let i = 0; i < newInstancesCount; i++) {
@@ -2379,7 +2392,7 @@ const createScene: EditorFunction = {
if (project.hasLayoutNamed(scene_name)) {
const scene = project.getLayout(scene_name);
if (include_ui_layer && !scene.hasLayerNamed('UI')) {
scene.insertNewLayer('UI', 0);
scene.insertNewLayer('UI', scene.getLayersCount());
addDefaultLightToLayer(scene.getLayer('UI'));
return makeGenericSuccess(
`Scene with name "${scene_name}" already exists, no need to re-create it. A layer called "UI" was added to it.`
@@ -2394,7 +2407,7 @@ const createScene: EditorFunction = {
const scenesCount = project.getLayoutsCount();
const scene = project.insertNewLayout(scene_name, scenesCount);
if (include_ui_layer) {
scene.insertNewLayer('UI', 0);
scene.insertNewLayer('UI', scene.getLayersCount());
}
if (background_color) {
const colorAsRgb = hexNumberToRGBArray(
@@ -2438,6 +2451,581 @@ const deleteScene: EditorFunction = {
},
};
const serializeEffectProperties = (
effect: gdEffect,
effectMetadata: gdEffectMetadata
) => {
const effectProperties = effectMetadata.getProperties();
const propertyNames = effectProperties.keys().toJSArray();
return propertyNames
.map(name => {
const propertyDescriptor = effectProperties.get(name);
if (shouldHideProperty(propertyDescriptor)) return null;
// Set the value of the property to what is stored in the effect.
// If it's not set, none of these will be set and the "value" will be the default one
// serialized by the property descriptor.
let value = null;
if (effect.hasDoubleParameter(name)) {
value = effect.getDoubleParameter(name);
} else if (effect.hasStringParameter(name)) {
value = effect.getStringParameter(name);
} else if (effect.hasBooleanParameter(name)) {
value = effect.getBooleanParameter(name);
}
if (value === null) {
return serializeNamedProperty(name, propertyDescriptor);
}
return {
...serializeNamedProperty(name, propertyDescriptor),
value,
};
})
.filter(Boolean);
};
const inspectScenePropertiesLayersEffects: EditorFunction = {
renderForEditor: ({ args }) => {
const scene_name = extractRequiredString(args, 'scene_name');
return {
text: (
<Trans>
Inspecting scene properties, layers and effects for scene {scene_name}
.
</Trans>
),
};
},
launchFunction: async ({ project, args }) => {
const scene_name = extractRequiredString(args, 'scene_name');
if (!project.hasLayoutNamed(scene_name)) {
return makeGenericFailure(`Scene not found: "${scene_name}".`);
}
const scene = project.getLayout(scene_name);
const layersContainer = scene.getLayers();
return {
success: true,
propertiesLayersEffectsForSceneNamed: scene.getName(),
properties: {
backgroundColor: rgbColorToHex(
scene.getBackgroundColorRed(),
scene.getBackgroundColorGreen(),
scene.getBackgroundColorBlue()
),
stopSoundsOnStartup: scene.stopSoundsOnStartup(),
// Also include some project related properties:
gameResolutionWidth: project.getGameResolutionWidth(),
gameResolutionHeight: project.getGameResolutionHeight(),
gameOrientation: project.getOrientation(),
gameScaleMode: project.getScaleMode(),
gameName: project.getName(),
},
layers: mapFor(0, layersContainer.getLayersCount(), i => {
const layer = layersContainer.getLayerAt(i);
const effectsContainer = layer.getEffects();
return {
name: layer.getName(),
position: i,
effects: mapFor(0, effectsContainer.getEffectsCount(), j => {
const effect = effectsContainer.getEffectAt(j);
const effectMetadata = gd.MetadataProvider.getEffectMetadata(
project.getCurrentPlatform(),
effect.getEffectType()
);
if (gd.MetadataProvider.isBadEffectMetadata(effectMetadata)) {
return null;
}
return {
effectName: effect.getName(),
effectType: effect.getEffectType(),
effectProperties: serializeEffectProperties(
effect,
effectMetadata
),
};
}).filter(Boolean),
};
}),
};
},
};
const isFuzzyMatch = (string1: string, string2: string) => {
const simplifiedString1 = string1.toLowerCase().replace(/\s|_|-/g, '');
const simplifiedString2 = string2.toLowerCase().replace(/\s|_|-/g, '');
return simplifiedString1 === simplifiedString2;
};
const changeScenePropertiesLayersEffects: EditorFunction = {
renderForEditor: ({ args, shouldShowDetails }) => {
const scene_name = extractRequiredString(args, 'scene_name');
const changed_properties = SafeExtractor.extractArrayProperty(
args,
'changed_properties'
);
const changed_layers = SafeExtractor.extractArrayProperty(
args,
'changed_layers'
);
const changed_layer_effects = SafeExtractor.extractArrayProperty(
args,
'changed_layer_effects'
);
const changedPropertiesCount =
(changed_properties && changed_properties.length) || 0;
const changedLayersCount = (changed_layers && changed_layers.length) || 0;
const changedLayerEffectsCount =
(changed_layer_effects && changed_layer_effects.length) || 0;
return {
text:
changedPropertiesCount > 0 &&
changedLayersCount > 0 &&
changedLayerEffectsCount > 0 ? (
<Trans>
Changing some scene properties, layers and effects for scene{' '}
{scene_name}.
</Trans>
) : changedPropertiesCount > 0 && changedLayersCount > 0 ? (
<Trans>
Changing some scene properties and layers for scene {scene_name}.
</Trans>
) : changedPropertiesCount > 0 && changedLayerEffectsCount > 0 ? (
<Trans>
Changing some scene properties and effects for scene {scene_name}.
</Trans>
) : changedLayerEffectsCount > 0 && changedLayersCount > 0 ? (
<Trans>
Changing some scene effects and layers for scene {scene_name}.
</Trans>
) : changedPropertiesCount > 0 ? (
<Trans>Changing some scene properties for scene {scene_name}.</Trans>
) : changedLayersCount > 0 ? (
<Trans>Changing some scene layers for scene {scene_name}.</Trans>
) : changedLayerEffectsCount > 0 ? (
<Trans>Changing some scene effects for scene {scene_name}.</Trans>
) : (
<Trans>Unknown changes attempted for scene {scene_name}.</Trans>
),
};
},
launchFunction: async ({
project,
args,
onInstancesModifiedOutsideEditor,
}) => {
const scene_name = extractRequiredString(args, 'scene_name');
if (!project.hasLayoutNamed(scene_name)) {
return makeGenericFailure(`Scene not found: "${scene_name}".`);
}
const scene = project.getLayout(scene_name);
const changes = [];
const warnings = [];
const changed_properties = SafeExtractor.extractArrayProperty(
args,
'changed_properties'
);
const changed_layers = SafeExtractor.extractArrayProperty(
args,
'changed_layers'
);
const changed_layer_effects = SafeExtractor.extractArrayProperty(
args,
'changed_layer_effects'
);
if (changed_properties)
changed_properties.forEach(changed_property => {
const propertyName = SafeExtractor.extractStringProperty(
changed_property,
'property_name'
);
const newValue = SafeExtractor.extractStringProperty(
changed_property,
'new_value'
);
if (propertyName === null || newValue === null) {
warnings.push(
`Missing "property_name" or "new_value" in the changed_property object: ${JSON.stringify(
changed_property
)}. It was ignored and not changed.`
);
return;
}
if (isFuzzyMatch(propertyName, 'backgroundColor')) {
const colorAsRgb = hexNumberToRGBArray(rgbOrHexToHexNumber(newValue));
scene.setBackgroundColor(colorAsRgb[0], colorAsRgb[1], colorAsRgb[2]);
changes.push('Modified the scene background color.');
} else if (isFuzzyMatch(propertyName, 'gameResolutionWidth')) {
project.setGameResolutionSize(
parseInt(newValue),
project.getGameResolutionHeight()
);
changes.push('Modified the game resolution width.');
} else if (isFuzzyMatch(propertyName, 'stopSoundsOnStartup')) {
scene.setStopSoundsOnStartup(newValue.toLowerCase() === 'true');
changes.push(
'Modified whether sounds should be stopped on scene startup.'
);
} else if (isFuzzyMatch(propertyName, 'gameResolutionHeight')) {
project.setGameResolutionSize(
project.getGameResolutionWidth(),
parseInt(newValue)
);
changes.push('Modified the game resolution height.');
} else if (isFuzzyMatch(propertyName, 'gameOrientation')) {
project.setOrientation(newValue);
changes.push('Modified the game orientation.');
} else if (isFuzzyMatch(propertyName, 'gameScaleMode')) {
project.setScaleMode(newValue);
changes.push('Modified the game scale mode.');
} else if (isFuzzyMatch(propertyName, 'gameName')) {
project.setName(newValue);
changes.push('Modified the game name.');
} else {
warnings.push(
`Unknown property for the scene: "${propertyName}". It was ignored and not changed.`
);
}
});
if (changed_layers) {
changed_layers.forEach(changed_layer => {
const layerName = SafeExtractor.extractStringProperty(
changed_layer,
'layer_name'
);
if (layerName === null) {
warnings.push(
`Missing "layer_name" in an item of changed_layers. It was ignored and not changed.`
);
return;
}
const new_layer_name = SafeExtractor.extractStringProperty(
changed_layer,
'new_layer_name'
);
const new_layer_position = SafeExtractor.extractNumberProperty(
changed_layer,
'new_layer_position'
);
const delete_this_layer = SafeExtractor.extractBooleanProperty(
changed_layer,
'delete_this_layer'
);
const move_instances_to_layer = SafeExtractor.extractStringProperty(
changed_layer,
'move_instances_to_layer'
);
if (scene.hasLayerNamed(layerName)) {
if (delete_this_layer) {
if (move_instances_to_layer) {
gd.WholeProjectRefactorer.mergeLayersInScene(
project,
scene,
layerName,
move_instances_to_layer
);
} else {
// Note: some instances will be invalidated because of this.
gd.WholeProjectRefactorer.removeLayerInScene(
project,
scene,
layerName
);
}
scene.getLayers().removeLayer(layerName);
changes.push(
`Removed layer "${layerName}" for scene "${scene.getName()}".`
);
} else {
if (new_layer_name) {
gd.WholeProjectRefactorer.renameLayerInScene(
project,
scene,
layerName,
new_layer_name
);
changes.push(
`Renamed layer "${layerName}" to "${new_layer_name}" for scene "${scene.getName()}".`
);
}
}
if (new_layer_position !== null) {
scene
.getLayers()
.moveLayer(
scene.getLayers().getLayerPosition(layerName),
new_layer_position
);
changes.push(
`Moved layer "${layerName}" to position ${new_layer_position} for scene "${scene.getName()}".`
);
}
// /!\ Tell the editor that some instances have potentially been modified (and even removed).
// This will force the instances editor to destroy and mount again the
// renderers to avoid keeping any references to existing instances, and also drop any selection.
onInstancesModifiedOutsideEditor({
scene,
});
} else {
scene
.getLayers()
.insertNewLayer(
new_layer_name || layerName,
new_layer_position === null
? scene.getLayersCount()
: new_layer_position
);
changes.push(
`Created new layer "${new_layer_name ||
layerName}" for scene "${scene.getName()}" at position ${new_layer_position ||
0}.`
);
}
});
}
if (changed_layer_effects) {
changed_layer_effects.forEach(changed_layer_effect => {
const layerName = SafeExtractor.extractStringProperty(
changed_layer_effect,
'layer_name'
);
if (layerName === null) {
warnings.push(
`Missing "layer_name" in an item of changed_layer_effects. It was ignored and not changed.`
);
return;
}
if (!scene.hasLayerNamed(layerName)) {
warnings.push(
`Layer not found: "${layerName}". It was ignored and no effects on it were changed.`
);
return;
}
const layer = scene.getLayers().getLayer(layerName);
const effectsContainer = layer.getEffects();
const effectName = SafeExtractor.extractStringProperty(
changed_layer_effect,
'effect_name'
);
if (effectName === null) {
warnings.push(
`Missing "effect_name" in an item of changed_layer_effects. It was ignored and not changed.`
);
return;
}
const effect_type = SafeExtractor.extractStringProperty(
changed_layer_effect,
'effect_type'
);
const new_effect_name = SafeExtractor.extractStringProperty(
changed_layer_effect,
'new_effect_name'
);
const new_effect_position = SafeExtractor.extractNumberProperty(
changed_layer_effect,
'new_effect_position'
);
const delete_this_effect = SafeExtractor.extractBooleanProperty(
changed_layer_effect,
'delete_this_effect'
);
let newlyCreatedEffect: gdEffect | null = null;
if (effectsContainer.hasEffectNamed(effectName)) {
const effect = effectsContainer.getEffect(effectName);
if (delete_this_effect) {
effectsContainer.removeEffect(effectName);
changes.push(
`Removed "${effectName}" effect on layer "${layerName}".`
);
} else {
if (new_effect_name) {
effect.setName(new_effect_name);
changes.push(
`Renamed the "${effectName}" effect on layer "${layerName}" to "${new_effect_name}".`
);
}
if (new_effect_position !== null) {
effectsContainer.moveEffect(
effectsContainer.getEffectPosition(effectName),
new_effect_position
);
changes.push(
`Moved the "${effectName}" effect on layer "${layerName}" to position ${new_effect_position}.`
);
}
}
} else {
if (effect_type) {
const newEffectName = new_effect_name || effectName;
const effectMetadata = gd.MetadataProvider.getEffectMetadata(
project.getCurrentPlatform(),
effect_type
);
if (gd.MetadataProvider.isBadEffectMetadata(effectMetadata)) {
warnings.push(
`Effect type "${effect_type}" is not a valid effect type. Effect "${newEffectName}" was NOT added.`
);
} else {
newlyCreatedEffect = effectsContainer.insertNewEffect(
newEffectName,
new_effect_position || 0
);
newlyCreatedEffect.setEffectType(effect_type);
}
}
}
const changed_properties = SafeExtractor.extractArrayProperty(
changed_layer_effect,
'changed_properties'
);
if (changed_properties) {
if (!effectsContainer.hasEffectNamed(effectName)) {
warnings.push(
`Effect not found: "${effectName}". It was ignored and not changed.`
);
return;
}
const effect = effectsContainer.getEffect(effectName);
const effectMetadata = gd.MetadataProvider.getEffectMetadata(
project.getCurrentPlatform(),
effect.getEffectType()
);
if (gd.MetadataProvider.isBadEffectMetadata(effectMetadata)) {
warnings.push(
`Effect "${effectName}" is not a valid effect. It was ignored and not changed.`
);
return;
}
const effectProperties = effectMetadata.getProperties();
changed_properties.forEach(changed_property => {
const propertyName = SafeExtractor.extractStringProperty(
changed_property,
'property_name'
);
const newValue = SafeExtractor.extractStringProperty(
changed_property,
'new_value'
);
if (propertyName === null || newValue === null) {
warnings.push(
`Missing "property_name" or "new_value" in an item of changed_properties. It was ignored and not changed. Make sure you follow the exact format for changing effect properties.`
);
return;
}
const { foundProperty } = findPropertyByName({
properties: effectProperties,
name: propertyName,
});
if (!foundProperty) {
warnings.push(
`Property not found: "${propertyName}" in effect "${effectName}". It was ignored and not changed. Make sure you only change existing effect properties.`
);
return;
}
const lowercasedType = foundProperty.getType().toLowerCase();
if (lowercasedType === 'number') {
effect.setDoubleParameter(
propertyName,
parseFloat(newValue) || 0
);
} else if (lowercasedType === 'boolean') {
effect.setBooleanParameter(
propertyName,
newValue.toLowerCase() === 'true'
);
} else {
effect.setStringParameter(propertyName, newValue);
}
changes.push(
`Modified "${propertyName}" property of the "${effectName}" effect to "${newValue}".`
);
});
}
if (newlyCreatedEffect) {
const effectMetadata = gd.MetadataProvider.getEffectMetadata(
project.getCurrentPlatform(),
newlyCreatedEffect.getEffectType()
);
if (gd.MetadataProvider.isBadEffectMetadata(effectMetadata)) {
// Should not happen.
} else {
changes.push(
`Created new "${newlyCreatedEffect.getName()}" effect on layer "${layerName}" at position ${new_effect_position ||
0}. It properties are: ${serializeEffectProperties(
newlyCreatedEffect,
effectMetadata
)
// This stringify might not give the prettiest output, this could be improved.
.map(serializedProperty => JSON.stringify(serializedProperty))
.join(', ')}.`
);
}
}
});
}
if (changes.length === 0 && warnings.length === 0) {
return {
success: false,
message: 'No changes were made.',
};
} else if (changes.length === 0 && warnings.length > 0) {
return {
success: false,
message:
'No changes were made because of the issues listed in the warnings.',
warnings: warnings.join('\n'),
};
} else if (changes.length > 0 && warnings.length === 0) {
return {
success: true,
message: ['Successfully done the changes.', ...changes].join('\n'),
};
} else {
return {
success: true,
message: [
'Successfully done some changes but some issues were found - see the warnings.',
...changes,
].join('\n'),
warnings: warnings.join('\n'),
};
}
},
};
const addOrEditVariable: EditorFunction = {
renderForEditor: ({ args, shouldShowDetails }) => {
const variable_name_or_path = extractRequiredString(
@@ -2595,5 +3183,7 @@ export const editorFunctions: { [string]: EditorFunction } = {
add_scene_events: addSceneEvents,
create_scene: createScene,
delete_scene: deleteScene,
inspect_scene_properties_layers_effects: inspectScenePropertiesLayersEffects,
change_scene_properties_layers_effects: changeScenePropertiesLayersEffects,
add_or_edit_variable: addOrEditVariable,
};

View File

@@ -37,6 +37,10 @@ export type SceneEventsOutsideEditorChanges = {|
newOrChangedAiGeneratedEventIds: Set<string>,
|};
export type InstancesOutsideEditorChanges = {|
scene: gdLayout,
|};
export type RenderEditorContainerProps = {|
isActive: boolean,
projectItemName: ?string,
@@ -179,11 +183,14 @@ export type RenderEditorContainerProps = {|
) => void,
onSceneObjectsDeleted: (scene: gdLayout) => void,
onInstancesModifiedOutsideEditor: (
changes: InstancesOutsideEditorChanges
) => void,
// Events editing
onSceneEventsModifiedOutsideEditor: (
changes: SceneEventsOutsideEditorChanges
) => void,
onExtractAsExternalLayout: (name: string) => void,
onExtractAsEventBasedObject: (
extensionName: string,

View File

@@ -4,6 +4,7 @@ import {
type RenderEditorContainerProps,
type RenderEditorContainerPropsWithRef,
type SceneEventsOutsideEditorChanges,
type InstancesOutsideEditorChanges,
} from './BaseEditor';
import { prepareInstancesEditorSettings } from '../../InstancesEditor/InstancesEditorSettings';
import {
@@ -115,6 +116,10 @@ export class CustomObjectEditorContainer extends React.Component<RenderEditorCon
// No thing to be done.
}
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
// No thing to be done.
}
saveUiSettings = () => {
// const layout = this.getCustomObject();
// const editor = this.editor;

View File

@@ -7,6 +7,7 @@ import {
type RenderEditorContainerProps,
type RenderEditorContainerPropsWithRef,
type SceneEventsOutsideEditorChanges,
type InstancesOutsideEditorChanges,
} from './BaseEditor';
import SubscriptionChecker, {
type SubscriptionCheckerInterface,
@@ -66,6 +67,10 @@ export class DebuggerEditorContainer extends React.Component<
// No thing to be done.
}
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
// No thing to be done.
}
// To be updated, see https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops.
UNSAFE_componentWillReceiveProps() {
this._checkUserHasSubscription();

View File

@@ -6,6 +6,7 @@ import {
type RenderEditorContainerProps,
type RenderEditorContainerPropsWithRef,
type SceneEventsOutsideEditorChanges,
type InstancesOutsideEditorChanges,
} from './BaseEditor';
import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope';
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
@@ -68,6 +69,10 @@ export class EventsEditorContainer extends React.Component<RenderEditorContainer
}
}
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
// No thing to be done.
}
getLayout(): ?gdLayout {
const { project, projectItemName } = this.props;
if (

View File

@@ -5,6 +5,7 @@ import {
type RenderEditorContainerProps,
type RenderEditorContainerPropsWithRef,
type SceneEventsOutsideEditorChanges,
type InstancesOutsideEditorChanges,
} from './BaseEditor';
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
@@ -55,6 +56,10 @@ export class EventsFunctionsExtensionEditorContainer extends React.Component<Ren
// No thing to be done.
}
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
// No thing to be done.
}
shouldComponentUpdate(nextProps: RenderEditorContainerProps) {
// We stop updates when the component is inactive.
// If it's active, was active or becoming active again we let update propagate.

View File

@@ -8,6 +8,7 @@ import {
type RenderEditorContainerProps,
type RenderEditorContainerPropsWithRef,
type SceneEventsOutsideEditorChanges,
type InstancesOutsideEditorChanges,
} from './BaseEditor';
import ExternalPropertiesDialog, {
type ExternalProperties,
@@ -99,6 +100,10 @@ export class ExternalEventsEditorContainer extends React.Component<
// No thing to be done.
}
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
// No thing to be done.
}
getExternalEvents(): ?gdExternalEvents {
const { project, projectItemName } = this.props;
if (!project || !projectItemName) return null;

View File

@@ -13,6 +13,7 @@ import {
type RenderEditorContainerProps,
type RenderEditorContainerPropsWithRef,
type SceneEventsOutsideEditorChanges,
type InstancesOutsideEditorChanges,
} from './BaseEditor';
import ExternalPropertiesDialog, {
type ExternalProperties,
@@ -157,6 +158,16 @@ export class ExternalLayoutEditorContainer extends React.Component<
// No thing to be done.
}
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
if (changes.scene !== this.getLayout()) {
return;
}
if (this.editor) {
this.editor.onInstancesModifiedOutsideEditor();
}
}
getExternalLayout(): ?gdExternalLayout {
const { project, projectItemName } = this.props;
if (!project || !projectItemName) return null;

View File

@@ -5,6 +5,7 @@ import { type I18n as I18nType } from '@lingui/core';
import {
type RenderEditorContainerPropsWithRef,
type SceneEventsOutsideEditorChanges,
type InstancesOutsideEditorChanges,
} from '../BaseEditor';
import {
type FileMetadataAndStorageProviderName,
@@ -178,6 +179,9 @@ export type HomePageEditorInterface = {|
onSceneEventsModifiedOutsideEditor: (
scene: SceneEventsOutsideEditorChanges
) => void,
onInstancesModifiedOutsideEditor: (
changes: InstancesOutsideEditorChanges
) => void,
|};
export const HomePage = React.memo<Props>(
@@ -486,6 +490,13 @@ export const HomePage = React.memo<Props>(
[]
);
const onInstancesModifiedOutsideEditor = React.useCallback(
(changes: InstancesOutsideEditorChanges) => {
// No thing to be done.
},
[]
);
React.useImperativeHandle(ref, () => ({
getProject,
updateToolbar,
@@ -494,6 +505,7 @@ export const HomePage = React.memo<Props>(
onSceneObjectEdited,
onSceneObjectsDeleted,
onSceneEventsModifiedOutsideEditor,
onInstancesModifiedOutsideEditor,
}));
// As the homepage is never unmounted, we need to ensure the games platform

View File

@@ -4,6 +4,7 @@ import {
type RenderEditorContainerProps,
type RenderEditorContainerPropsWithRef,
type SceneEventsOutsideEditorChanges,
type InstancesOutsideEditorChanges,
} from './BaseEditor';
import ResourcesEditor from '../../ResourcesEditor';
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
@@ -50,6 +51,10 @@ export class ResourcesEditorContainer extends React.Component<RenderEditorContai
// No thing to be done.
}
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
// No thing to be done.
}
componentDidUpdate(prevProps: RenderEditorContainerProps) {
if (
this.editor &&

View File

@@ -10,6 +10,7 @@ import {
type RenderEditorContainerProps,
type RenderEditorContainerPropsWithRef,
type SceneEventsOutsideEditorChanges,
type InstancesOutsideEditorChanges,
} from './BaseEditor';
import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope';
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
@@ -98,6 +99,16 @@ export class SceneEditorContainer extends React.Component<RenderEditorContainerP
// No thing to be done.
}
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
if (changes.scene !== this.getLayout()) {
return;
}
if (this.editor) {
this.editor.onInstancesModifiedOutsideEditor();
}
}
getLayout(): ?gdLayout {
const { project, projectItemName } = this.props;
if (

View File

@@ -24,7 +24,10 @@ import {
saveUiSettings,
} from './EditorTabs/EditorTabsHandler';
import { type PreviewState } from './PreviewState';
import { type SceneEventsOutsideEditorChanges } from './EditorContainers/BaseEditor';
import {
type SceneEventsOutsideEditorChanges,
type InstancesOutsideEditorChanges,
} from './EditorContainers/BaseEditor';
import { type ResourceManagementProps } from '../ResourcesList/ResourceSource';
import { type HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewButton';
import { type GamesList } from '../GameDashboard/UseGamesList';
@@ -206,6 +209,9 @@ export type EditorTabsPaneCommonProps = {|
onSceneEventsModifiedOutsideEditor: (
changes: SceneEventsOutsideEditorChanges
) => void,
onInstancesModifiedOutsideEditor: (
changes: InstancesOutsideEditorChanges
) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
gamesList: GamesList,
@@ -294,6 +300,7 @@ const EditorTabsPane = React.forwardRef<Props, {||}>((props, ref) => {
onSceneObjectEdited,
onSceneObjectsDeleted,
onSceneEventsModifiedOutsideEditor,
onInstancesModifiedOutsideEditor,
onExtensionInstalled,
gamesList,
setEditorTabs,
@@ -681,6 +688,7 @@ const EditorTabsPane = React.forwardRef<Props, {||}>((props, ref) => {
onSceneObjectEdited: onSceneObjectEdited,
onSceneObjectsDeleted: onSceneObjectsDeleted,
onSceneEventsModifiedOutsideEditor: onSceneEventsModifiedOutsideEditor,
onInstancesModifiedOutsideEditor: onInstancesModifiedOutsideEditor,
onExtensionInstalled: onExtensionInstalled,
gamesList,
gamesPlatformFrameTools,

View File

@@ -58,6 +58,7 @@ import { renderResourcesEditorContainer } from './EditorContainers/ResourcesEdit
import {
type RenderEditorContainerPropsWithRef,
type SceneEventsOutsideEditorChanges,
type InstancesOutsideEditorChanges,
} from './EditorContainers/BaseEditor';
import { type Exporter } from '../ExportAndShare/ShareDialog';
import ResourcesLoader from '../ResourcesLoader/index';
@@ -2431,6 +2432,18 @@ const MainFrame = (props: Props) => {
[state.editorTabs]
);
const onInstancesModifiedOutsideEditor = React.useCallback(
(changes: InstancesOutsideEditorChanges) => {
for (const editor of getAllEditorTabs(state.editorTabs)) {
const { editorRef } = editor;
if (editorRef) {
editorRef.onInstancesModifiedOutsideEditor(changes);
}
}
},
[state.editorTabs]
);
const _onProjectItemModified = () => {
triggerUnsavedChanges();
forceUpdate();
@@ -3872,6 +3885,7 @@ const MainFrame = (props: Props) => {
onSceneObjectEdited: onSceneObjectEdited,
onSceneObjectsDeleted: onSceneObjectsDeleted,
onSceneEventsModifiedOutsideEditor: onSceneEventsModifiedOutsideEditor,
onInstancesModifiedOutsideEditor: onInstancesModifiedOutsideEditor,
onExtensionInstalled: onExtensionInstalled,
gamesList: gamesList,
};

View File

@@ -311,6 +311,18 @@ export default class SceneEditor extends React.Component<Props, State> {
}
};
onInstancesModifiedOutsideEditor = () => {
// /!\ Drop the selection to avoid keeping any references to deleted instances.
// This could be avoided if the selection used something like UUID to address instances.
this.instancesSelection.clearSelection();
// /!\ Force the instances editor to destroy and mount again the
// renderers to avoid keeping any references to existing instances
if (this.editorDisplay)
this.editorDisplay.instancesHandlers.forceRemountInstancesRenderers();
this.updateToolbar();
};
updateToolbar = () => {
const { editorDisplay } = this;
if (!editorDisplay) return;
@@ -590,10 +602,8 @@ export default class SceneEditor extends React.Component<Props, State> {
);
undo = () => {
// TODO: Do not clear selection so that the user can actually see
// the changes it is undoing (variable change, instance moved, etc.)
// or find a way to display a sumup of the change such as "Variable XXX
// in instance of Enemy changed to YYY"
// /!\ Drop the selection to avoid keeping any references to deleted instances.
// This could be avoided if the selection used something like UUID to address instances.
this.instancesSelection.clearSelection();
this.setState(
{
@@ -610,6 +620,8 @@ export default class SceneEditor extends React.Component<Props, State> {
};
redo = () => {
// /!\ Drop the selection to avoid keeping any references to deleted instances.
// This could be avoided if the selection used something like UUID to address instances.
this.instancesSelection.clearSelection();
this.setState(
{

View File

@@ -328,6 +328,7 @@ export const createAiRequest = async (
gameId,
fileMetadata,
storageProviderName,
toolsVersion,
}: {|
userId: string,
userRequest: string,
@@ -346,6 +347,7 @@ export const createAiRequest = async (
gameId?: string,
},
storageProviderName: ?string,
toolsVersion: string,
|}
): Promise<AiRequest> => {
const authorizationHeader = await getAuthorizationHeader();
@@ -363,6 +365,7 @@ export const createAiRequest = async (
gameId,
fileMetadata,
storageProviderName,
toolsVersion,
},
{
params: {