diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dcdab0ef..a17e9b5fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix issue where adding a new song wouldn't warn about unsaved changes in current song - Fix issue where adding a song with an already existing name wouldn't auto select the newly created song - Fix issue where scene connection lines could get stuck in place if custom scripts that change scenes are called multiple times from the same scene +- Fix issue where "Replace Script" confirmation alert would appear when pasting sometimes even if the custom script hadn't been modified ## [4.1.2] - 2024-09-09 diff --git a/src/shared/lib/entities/entitiesHelpers.ts b/src/shared/lib/entities/entitiesHelpers.ts index deb9559bc..ce30014f0 100644 --- a/src/shared/lib/entities/entitiesHelpers.ts +++ b/src/shared/lib/entities/entitiesHelpers.ts @@ -720,7 +720,6 @@ const extractEntityStateSymbols = (state: EntitiesState) => { ...extractEntitySymbols(state.customEvents), ...extractEntitySymbols(state.music), ...extractEntitySymbols(state.sounds), - ...extractEntitySymbols(state.scriptEvents), ]; }; @@ -758,7 +757,6 @@ export const ensureSymbolsUnique = (state: EntitiesState) => { ensureEntitySymbolsUnique(state.customEvents, symbols); ensureEntitySymbolsUnique(state.music, symbols); ensureEntitySymbolsUnique(state.sounds, symbols); - ensureEntitySymbolsUnique(state.scriptEvents, symbols); }; export const mergeAssetEntity = ( diff --git a/src/shared/lib/entities/entitiesTypes.ts b/src/shared/lib/entities/entitiesTypes.ts index f896a198b..aecafec84 100644 --- a/src/shared/lib/entities/entitiesTypes.ts +++ b/src/shared/lib/entities/entitiesTypes.ts @@ -53,7 +53,6 @@ export type ScriptEventArgs = Record; export type ScriptEvent = { id: string; command: string; - symbol?: string | undefined; args?: ScriptEventArgs | undefined; children?: Record | undefined; }; diff --git a/src/shared/lib/resources/types.ts b/src/shared/lib/resources/types.ts index 1566f882a..936614d90 100644 --- a/src/shared/lib/resources/types.ts +++ b/src/shared/lib/resources/types.ts @@ -59,7 +59,6 @@ const ScriptEvent = Type.Recursive((This) => Type.Object({ id: Type.String(), command: Type.String(), - symbol: Type.Optional(Type.String()), // Include symbol property to match TypeScript args: Type.Optional(ScriptEventArgs), // Matches ScriptEventArgs children: Type.Optional( Type.Record( diff --git a/src/shared/lib/scripts/scriptHelpers.ts b/src/shared/lib/scripts/scriptHelpers.ts index cf9dc18a8..ca9fe023c 100644 --- a/src/shared/lib/scripts/scriptHelpers.ts +++ b/src/shared/lib/scripts/scriptHelpers.ts @@ -33,7 +33,24 @@ export const isNormalizedScriptEqual = ( const { args, command } = scriptEvent; scriptBEvents.push({ args, command }); }); - return isEqual(scriptAEvents, scriptBEvents); + + // Exit early if script lengths differ + if (scriptAEvents.length !== scriptBEvents.length) { + return false; + } + // Otherwise check that every script event is equivalent + for (let i = 0; i < scriptAEvents.length; i++) { + const scriptEventA = scriptAEvents[i]; + const scriptEventB = scriptBEvents[i]; + if (scriptEventA.command !== scriptEventB.command) { + return false; + } + if (!isArgsEqual(scriptEventA.args ?? {}, scriptEventB.args ?? {})) { + return false; + } + } + + return true; }; export const generateScriptHash = (script: ScriptEvent[]): string => { @@ -43,3 +60,32 @@ export const generateScriptHash = (script: ScriptEvent[]): string => { }); return SparkMD5.hash(JSON.stringify(data)); }; + +// Compare args with undefined and missing args as equivalent +const isArgsEqual = ( + a: Record, + b: Record +): boolean => { + const keys = new Set([...Object.keys(a), ...Object.keys(b)]); + for (const key of keys) { + const hasKeyA = Object.prototype.hasOwnProperty.call(a, key); + const hasKeyB = Object.prototype.hasOwnProperty.call(b, key); + const valA = a[key]; + const valB = b[key]; + + if (hasKeyA && hasKeyB) { + // Both objects have the key + if (!isEqual(valA, valB)) { + return false; + } + } else if (hasKeyA || hasKeyB) { + // One object has the key; check if its value is undefined + const val = hasKeyA ? valA : valB; + if (val !== undefined) { + // Values are different since one is not undefined + return false; + } + } + } + return true; +}; diff --git a/src/store/features/entities/entitiesState.ts b/src/store/features/entities/entitiesState.ts index e8fde5de3..394f476b0 100644 --- a/src/store/features/entities/entitiesState.ts +++ b/src/store/features/entities/entitiesState.ts @@ -3416,7 +3416,6 @@ const addScriptEvents: CaseReducer< const newScriptEvent: ScriptEventNormalized = { ...scriptEventData, id: action.payload.scriptEventIds[scriptEventIndex], - symbol: undefined, }; if (scriptEventData.children) { newScriptEvent.children = Object.keys(scriptEventData.children).reduce( diff --git a/test/helpers/scriptHelpers.test.ts b/test/helpers/scriptHelpers.test.ts new file mode 100644 index 000000000..1d58a44b5 --- /dev/null +++ b/test/helpers/scriptHelpers.test.ts @@ -0,0 +1,120 @@ +import { ScriptEventNormalized } from "shared/lib/entities/entitiesTypes"; +import { isNormalizedScriptEqual } from "shared/lib/scripts/scriptHelpers"; + +describe("isNormalizedScriptEqual", () => { + test("should consider empty scripts equal", () => { + const scriptA: string[] = []; + const lookupA: Record = {}; + const scriptB: string[] = []; + const lookupB: Record = {}; + expect(isNormalizedScriptEqual(scriptA, lookupA, scriptB, lookupB)).toEqual( + true + ); + }); + + test("should consider identical scripts equal", () => { + const scriptA: string[] = ["event1"]; + const lookupA: Record = { + event1: { + id: "event1", + command: "EVENT_TEST", + args: { + hello: "WORLD", + }, + }, + }; + const scriptB: string[] = ["event1"]; + const lookupB: Record = { + event1: { + id: "event1", + command: "EVENT_TEST", + args: { + hello: "WORLD", + }, + }, + }; + expect(isNormalizedScriptEqual(scriptA, lookupA, scriptB, lookupB)).toEqual( + true + ); + }); + + test("should consider functionally identical scripts equal even if ids dont match", () => { + const scriptA: string[] = ["event1"]; + const lookupA: Record = { + event1: { + id: "event1", + command: "EVENT_TEST", + args: { + hello: "WORLD", + }, + }, + }; + const scriptB: string[] = ["event2"]; + const lookupB: Record = { + event2: { + id: "event2", + command: "EVENT_TEST", + args: { + hello: "WORLD", + }, + }, + }; + expect(isNormalizedScriptEqual(scriptA, lookupA, scriptB, lookupB)).toEqual( + true + ); + }); + + test("should consider functionally different scripts to not be equal", () => { + const scriptA: string[] = ["event1"]; + const lookupA: Record = { + event1: { + id: "event1", + command: "EVENT_TEST", + args: { + hello: "WORLD", + }, + }, + }; + const scriptB: string[] = ["event1"]; + const lookupB: Record = { + event1: { + id: "event1", + command: "EVENT_TEST", + args: { + hello: "THERE", + }, + }, + }; + expect(isNormalizedScriptEqual(scriptA, lookupA, scriptB, lookupB)).toEqual( + false + ); + }); + + test("should consider missing args to be identical to undefined args", () => { + const scriptA: string[] = ["event1"]; + const lookupA: Record = { + event1: { + id: "event1", + command: "EVENT_TEST", + args: { + hello: "WORLD", + goodbye: undefined, + }, + }, + }; + const scriptB: string[] = ["event1"]; + const lookupB: Record = { + event1: { + id: "event1", + command: "EVENT_TEST", + args: { + hello: "WORLD", + foo: undefined, + }, + }, + }; + expect(isNormalizedScriptEqual(scriptA, lookupA, scriptB, lookupB)).toEqual( + true + ); + }); +});