feat: ship HyperTwist release 19 player experience

This commit is contained in:
axiomlogicnexus 2026-07-23 17:23:24 +00:00
parent 49422acf8f
commit 02ad8fa781
129 changed files with 54134 additions and 772 deletions

View file

@ -33,6 +33,9 @@
"devDependencies": { "devDependencies": {
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.1.11" "vite": "^7.1.11"
},
"optionalDependencies": {
"@rollup/rollup-win32-x64-msvc": "4.62.2"
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
@ -749,6 +752,19 @@
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
"integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@stitches/react": { "node_modules/@stitches/react": {
"version": "1.2.8", "version": "1.2.8",
"license": "MIT", "license": "MIT",

View file

@ -36,5 +36,8 @@
"devDependencies": { "devDependencies": {
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.1.11" "vite": "^7.1.11"
},
"optionalDependencies": {
"@rollup/rollup-win32-x64-msvc": "4.62.2"
} }
} }

View file

@ -8,6 +8,10 @@ const bootstrapPath = path.join(browserRoot, 'src', 'browser-runtime-bootstrap.j
const fallbackPath = path.join(browserRoot, 'src', 'browser-spatial-runtime-fallback.js'); const fallbackPath = path.join(browserRoot, 'src', 'browser-spatial-runtime-fallback.js');
const bootStatePath = path.join(browserRoot, 'src', 'runtime', 'boot-state.ts'); const bootStatePath = path.join(browserRoot, 'src', 'runtime', 'boot-state.ts');
const bundledShellSourcePath = path.join(browserRoot, 'src', 'runtime', 'shell.ts'); const bundledShellSourcePath = path.join(browserRoot, 'src', 'runtime', 'shell.ts');
const registryPath = path.join(browserRoot, 'src', 'runtime', 'registry.ts');
const packagePath = path.join(browserRoot, 'package.json');
const packageLockPath = path.join(browserRoot, 'package-lock.json');
const repoRoot = path.resolve(browserRoot, '..', '..');
const failures = []; const failures = [];
@ -19,11 +23,161 @@ function assert(condition, message)
} }
} }
function escapeRegExp(value)
{
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function verifyPhaseOneQueue(sourcePath)
{
if (!existsSync(sourcePath))
{
return;
}
const source = readFileSync(sourcePath, 'utf8');
const positions = [...source.matchAll(/queuePosition:\s*(\d+)/g)]
.map((match) => Number.parseInt(match[1], 10))
.sort((left, right) => left - right);
const expectedPositions = Array.from({ length: 29 }, (_, index) => index + 1);
assert(
JSON.stringify(positions) === JSON.stringify(expectedPositions),
`${path.basename(sourcePath)} must expose each Phase 1 queue position exactly once from 1 through 29.`
);
for (const row of phaseOneQueue)
{
const rowPattern = new RegExp(
`queuePosition:\\s*${row.position},[\\s\\S]{0,240}repo:\\s*'${escapeRegExp(row.repo)}'`
);
assert(
rowPattern.test(source),
`${path.basename(sourcePath)} is missing Phase 1 queue row ${row.position}: ${row.repo}.`
);
}
}
const phaseOneQueue = [
{ position: 1, repo: 'cahidenes/rubiks-cube-solver' },
{ position: 2, repo: 'tentone/rubix-solver' },
{ position: 3, repo: 'NuiLab/code-vr' },
{ position: 4, repo: 'pissang/claygl' },
{ position: 5, repo: 'pissang/clay-viewer' },
{ position: 6, repo: 'pmndrs/postprocessing' },
{ position: 7, repo: 'pmndrs/react-postprocessing' },
{ position: 8, repo: 'pmndrs/drei' },
{ position: 9, repo: 'pmndrs/uikit' },
{ position: 10, repo: 'pmndrs/three-stdlib' },
{ position: 11, repo: 'pmndrs/maath' },
{ position: 12, repo: 'pmndrs/zustand' },
{ position: 13, repo: 'pmndrs/leva' },
{ position: 14, repo: 'pmndrs/use-gesture' },
{ position: 15, repo: 'pmndrs/react-spring' },
{ position: 16, repo: 'google/model-viewer/packages/model-viewer-effects' },
{ position: 17, repo: 'google/model-viewer/packages/modelviewer.dev' },
{ position: 18, repo: 'google/model-viewer/packages/render-fidelity-tools' },
{ position: 19, repo: 'google/model-viewer/packages/space-opera' },
{ position: 20, repo: 'ecomfe/zrender' },
{ position: 21, repo: 'ecomfe/echarts-gl' },
{ position: 22, repo: 'KhronosGroup/glTF-Sample-Viewer' },
{ position: 23, repo: 'KhronosGroup/glTF-Sample-Renderer' },
{ position: 24, repo: '@react-spring/core' },
{ position: 25, repo: '@react-spring/shared' },
{ position: 26, repo: '@react-spring/types' },
{ position: 27, repo: '@react-spring/parallax' },
{ position: 28, repo: '@react-spring/rafz' },
{ position: 29, repo: '@react-spring/animated' }
];
assert(existsSync(shellPath), `Missing authoritative browser shell: ${shellPath}`); assert(existsSync(shellPath), `Missing authoritative browser shell: ${shellPath}`);
assert(existsSync(bootstrapPath), `Missing bootstrap runtime script: ${bootstrapPath}`); assert(existsSync(bootstrapPath), `Missing bootstrap runtime script: ${bootstrapPath}`);
assert(existsSync(fallbackPath), `Missing clean-checkout fallback runtime: ${fallbackPath}`); assert(existsSync(fallbackPath), `Missing clean-checkout fallback runtime: ${fallbackPath}`);
assert(existsSync(bootStatePath), `Missing browser boot-state helper: ${bootStatePath}`); assert(existsSync(bootStatePath), `Missing browser boot-state helper: ${bootStatePath}`);
assert(existsSync(bundledShellSourcePath), `Missing bundled runtime shell source: ${bundledShellSourcePath}`); assert(existsSync(bundledShellSourcePath), `Missing bundled runtime shell source: ${bundledShellSourcePath}`);
assert(existsSync(registryPath), `Missing browser adapter registry: ${registryPath}`);
assert(existsSync(packagePath), `Missing browser package manifest: ${packagePath}`);
assert(existsSync(packageLockPath), `Missing browser package lock: ${packageLockPath}`);
verifyPhaseOneQueue(registryPath);
verifyPhaseOneQueue(fallbackPath);
const requiredExternalPaths = [
'.external/rubiks-cube-solver',
'.external/rubix-solver',
'.external/code-vr',
'.external/claygl/dist/claygl.js',
'.external/clay-viewer/dist/clay-viewer.js',
'.external/glTF-Sample-Viewer',
'.external/glTF-Sample-Renderer',
'.external/model-viewer/packages/modelviewer.dev',
'.external/model-viewer/packages/model-viewer-effects',
'.external/model-viewer/packages/render-fidelity-tools',
'.external/model-viewer/packages/space-opera',
'.external/react-spring/packages/animated',
'.external/react-spring/packages/core',
'.external/react-spring/packages/parallax',
'.external/react-spring/packages/rafz',
'.external/react-spring/packages/shared',
'.external/react-spring/packages/types'
];
for (const relativePath of requiredExternalPaths)
{
assert(
existsSync(path.join(repoRoot, relativePath)),
`Missing retained Phase 1 source or sidecar evidence: ${relativePath}`
);
}
if (existsSync(packagePath))
{
const packageManifest = JSON.parse(readFileSync(packagePath, 'utf8'));
const dependencies = packageManifest.dependencies ?? {};
const requiredDirectDependencies = [
'@google/model-viewer-effects',
'@khronosgroup/gltf-viewer',
'@pmndrs/uikit',
'@react-spring/three',
'@react-spring/web',
'@react-three/drei',
'@react-three/postprocessing',
'@use-gesture/react',
'echarts-gl',
'leva',
'maath',
'postprocessing',
'three-stdlib',
'zrender',
'zustand'
];
for (const packageName of requiredDirectDependencies)
{
assert(
typeof dependencies[packageName] === 'string',
`Missing direct browser runtime dependency: ${packageName}`
);
}
}
if (existsSync(packageLockPath))
{
const packageLock = readFileSync(packageLockPath, 'utf8');
for (const packageName of [
'@react-spring/animated',
'@react-spring/core',
'@react-spring/rafz',
'@react-spring/shared',
'@react-spring/types'
])
{
assert(
packageLock.includes(`"node_modules/${packageName}"`),
`Browser package lock is missing the retained transitive dependency ${packageName}.`
);
}
}
if (existsSync(shellPath)) if (existsSync(shellPath))
{ {

View file

@ -1,9 +1,19 @@
const BROWSER_SUPPORT_ADAPTERS = [ const BROWSER_SUPPORT_ADAPTERS = [
{
activation: 'native-sidecar',
description: 'Python solver and visualizer retained behind the first-party comparison-process boundary.',
id: '1f-cahidenes-native-solver',
phase: '1F',
queuePosition: 1,
repo: 'cahidenes/rubiks-cube-solver',
sourcePathHint: '.external/rubiks-cube-solver'
},
{ {
activation: 'bundled-module', activation: 'bundled-module',
description: 'Reference glTF renderer and viewer module wired into the first-party browser runtime.', description: 'Reference glTF renderer and viewer module wired into the first-party browser runtime.',
id: '1e-gltf-sample-renderer', id: '1e-gltf-sample-renderer',
phase: '1E', phase: '1E',
queuePosition: 23,
repo: 'KhronosGroup/glTF-Sample-Renderer', repo: 'KhronosGroup/glTF-Sample-Renderer',
sourcePathHint: '.external/glTF-Sample-Renderer' sourcePathHint: '.external/glTF-Sample-Renderer'
}, },
@ -12,6 +22,7 @@ const BROWSER_SUPPORT_ADAPTERS = [
description: 'Native/OpenCV solver adjunct retained as a browser-shell fallback lane rather than a direct npm package.', description: 'Native/OpenCV solver adjunct retained as a browser-shell fallback lane rather than a direct npm package.',
id: '1g-tentone-native-solver', id: '1g-tentone-native-solver',
phase: '1G', phase: '1G',
queuePosition: 2,
repo: 'tentone/rubix-solver', repo: 'tentone/rubix-solver',
sourcePathHint: '.external/rubix-solver' sourcePathHint: '.external/rubix-solver'
}, },
@ -20,6 +31,7 @@ const BROWSER_SUPPORT_ADAPTERS = [
description: 'Rust donor retained as a source-sidecar surface for follow-up browser spatial composition.', description: 'Rust donor retained as a source-sidecar surface for follow-up browser spatial composition.',
id: '1h-code-vr', id: '1h-code-vr',
phase: '1H', phase: '1H',
queuePosition: 3,
repo: 'NuiLab/code-vr', repo: 'NuiLab/code-vr',
sourcePathHint: '.external/code-vr' sourcePathHint: '.external/code-vr'
}, },
@ -28,6 +40,7 @@ const BROWSER_SUPPORT_ADAPTERS = [
description: 'Prebuilt ClayGL runtime script bridged into the browser shell for viewer and QA surfaces.', description: 'Prebuilt ClayGL runtime script bridged into the browser shell for viewer and QA surfaces.',
id: '1h-claygl', id: '1h-claygl',
phase: '1H', phase: '1H',
queuePosition: 4,
repo: 'pissang/claygl', repo: 'pissang/claygl',
sourcePathHint: '.external/claygl/dist/claygl.js' sourcePathHint: '.external/claygl/dist/claygl.js'
}, },
@ -36,6 +49,7 @@ const BROWSER_SUPPORT_ADAPTERS = [
description: 'Prebuilt Clay Viewer shell loaded beside ClayGL for bounded browser inspection.', description: 'Prebuilt Clay Viewer shell loaded beside ClayGL for bounded browser inspection.',
id: '1h-clay-viewer', id: '1h-clay-viewer',
phase: '1H', phase: '1H',
queuePosition: 5,
repo: 'pissang/clay-viewer', repo: 'pissang/clay-viewer',
sourcePathHint: '.external/clay-viewer/dist/clay-viewer.js' sourcePathHint: '.external/clay-viewer/dist/clay-viewer.js'
}, },
@ -44,6 +58,7 @@ const BROWSER_SUPPORT_ADAPTERS = [
description: 'Three.js post-processing core bundled into the runtime module graph.', description: 'Three.js post-processing core bundled into the runtime module graph.',
id: '1h-postprocessing', id: '1h-postprocessing',
phase: '1H', phase: '1H',
queuePosition: 6,
repo: 'pmndrs/postprocessing', repo: 'pmndrs/postprocessing',
sourcePathHint: '.external/postprocessing' sourcePathHint: '.external/postprocessing'
}, },
@ -52,6 +67,7 @@ const BROWSER_SUPPORT_ADAPTERS = [
description: 'React bridge for post-processing loaded into the browser spatial stack.', description: 'React bridge for post-processing loaded into the browser spatial stack.',
id: '1h-react-postprocessing', id: '1h-react-postprocessing',
phase: '1H', phase: '1H',
queuePosition: 7,
repo: 'pmndrs/react-postprocessing', repo: 'pmndrs/react-postprocessing',
sourcePathHint: '.external/react-postprocessing' sourcePathHint: '.external/react-postprocessing'
}, },
@ -60,6 +76,7 @@ const BROWSER_SUPPORT_ADAPTERS = [
description: 'Drei helper surface bundled for browser-spatial utility composition.', description: 'Drei helper surface bundled for browser-spatial utility composition.',
id: '1h-drei', id: '1h-drei',
phase: '1H', phase: '1H',
queuePosition: 8,
repo: 'pmndrs/drei', repo: 'pmndrs/drei',
sourcePathHint: '.external/drei' sourcePathHint: '.external/drei'
}, },
@ -68,6 +85,7 @@ const BROWSER_SUPPORT_ADAPTERS = [
description: 'World-anchored UI kit bundled for browser-spatial HUD and control surfaces.', description: 'World-anchored UI kit bundled for browser-spatial HUD and control surfaces.',
id: '1h-uikit', id: '1h-uikit',
phase: '1H', phase: '1H',
queuePosition: 9,
repo: 'pmndrs/uikit', repo: 'pmndrs/uikit',
sourcePathHint: '.external/uikit' sourcePathHint: '.external/uikit'
}, },
@ -76,6 +94,7 @@ const BROWSER_SUPPORT_ADAPTERS = [
description: 'Three.js helper library bundled for browser runtime support.', description: 'Three.js helper library bundled for browser runtime support.',
id: '1h-three-stdlib', id: '1h-three-stdlib',
phase: '1H', phase: '1H',
queuePosition: 10,
repo: 'pmndrs/three-stdlib', repo: 'pmndrs/three-stdlib',
sourcePathHint: '.external/three-stdlib' sourcePathHint: '.external/three-stdlib'
}, },
@ -84,6 +103,7 @@ const BROWSER_SUPPORT_ADAPTERS = [
description: 'Animation and math helper pack bundled into the runtime.', description: 'Animation and math helper pack bundled into the runtime.',
id: '1h-maath', id: '1h-maath',
phase: '1H', phase: '1H',
queuePosition: 11,
repo: 'pmndrs/maath', repo: 'pmndrs/maath',
sourcePathHint: '.external/maath' sourcePathHint: '.external/maath'
}, },
@ -92,6 +112,7 @@ const BROWSER_SUPPORT_ADAPTERS = [
description: 'State-store runtime bundled for browser shell state and derived panel demos.', description: 'State-store runtime bundled for browser shell state and derived panel demos.',
id: '1h-zustand', id: '1h-zustand',
phase: '1H', phase: '1H',
queuePosition: 12,
repo: 'pmndrs/zustand', repo: 'pmndrs/zustand',
sourcePathHint: '.external/zustand' sourcePathHint: '.external/zustand'
}, },
@ -100,6 +121,7 @@ const BROWSER_SUPPORT_ADAPTERS = [
description: 'Parameter-control runtime bundled for future tuning overlays.', description: 'Parameter-control runtime bundled for future tuning overlays.',
id: '1h-leva', id: '1h-leva',
phase: '1H', phase: '1H',
queuePosition: 13,
repo: 'pmndrs/leva', repo: 'pmndrs/leva',
sourcePathHint: '.external/leva' sourcePathHint: '.external/leva'
}, },
@ -108,6 +130,7 @@ const BROWSER_SUPPORT_ADAPTERS = [
description: 'Gesture capture hooks bundled for browser interaction surfaces.', description: 'Gesture capture hooks bundled for browser interaction surfaces.',
id: '1h-use-gesture', id: '1h-use-gesture',
phase: '1H', phase: '1H',
queuePosition: 14,
repo: 'pmndrs/use-gesture', repo: 'pmndrs/use-gesture',
sourcePathHint: '.external/use-gesture' sourcePathHint: '.external/use-gesture'
}, },
@ -116,14 +139,70 @@ const BROWSER_SUPPORT_ADAPTERS = [
description: 'Motion and interpolation runtime bundled for browser spatial transitions.', description: 'Motion and interpolation runtime bundled for browser spatial transitions.',
id: '1h-react-spring', id: '1h-react-spring',
phase: '1H', phase: '1H',
queuePosition: 15,
repo: 'pmndrs/react-spring', repo: 'pmndrs/react-spring',
sourcePathHint: '.external/react-spring' sourcePathHint: '.external/react-spring'
}, },
{
activation: 'dependency-module',
description: 'Core spring controller is bundled transitively through the owned React Spring runtime entrypoints.',
id: '1h-react-spring-core',
phase: '1H',
queuePosition: 24,
repo: '@react-spring/core',
sourcePathHint: '.external/react-spring/packages/core'
},
{
activation: 'dependency-module',
description: 'Shared React Spring utilities are retained and locked as transitive runtime dependencies.',
id: '1h-react-spring-shared',
phase: '1H',
queuePosition: 25,
repo: '@react-spring/shared',
sourcePathHint: '.external/react-spring/packages/shared'
},
{
activation: 'reference-only',
description: 'Compile-time type declarations remain dependency evidence and are not presented as a player runtime.',
id: '1h-react-spring-types',
phase: '1H',
queuePosition: 26,
repo: '@react-spring/types',
sourcePathHint: '.external/react-spring/packages/types'
},
{
activation: 'dependency-module',
description: 'Parallax behavior is supplied through the bundled React Spring web entrypoint rather than a fabricated standalone package.',
id: '1h-react-spring-parallax',
phase: '1H',
queuePosition: 27,
repo: '@react-spring/parallax',
sourcePathHint: '.external/react-spring/packages/parallax'
},
{
activation: 'dependency-module',
description: 'The request-animation-frame scheduler is locked as a transitive React Spring runtime dependency.',
id: '1h-react-spring-rafz',
phase: '1H',
queuePosition: 28,
repo: '@react-spring/rafz',
sourcePathHint: '.external/react-spring/packages/rafz'
},
{
activation: 'dependency-module',
description: 'Animated-value primitives are locked as a transitive React Spring runtime dependency.',
id: '1h-react-spring-animated',
phase: '1H',
queuePosition: 29,
repo: '@react-spring/animated',
sourcePathHint: '.external/react-spring/packages/animated'
},
{ {
activation: 'bundled-module', activation: 'bundled-module',
description: '2D render engine bundled beneath the analytics surface.', description: '2D render engine bundled beneath the analytics surface.',
id: '1i-zrender', id: '1i-zrender',
phase: '1I', phase: '1I',
queuePosition: 20,
repo: 'ecomfe/zrender', repo: 'ecomfe/zrender',
sourcePathHint: '.external/zrender' sourcePathHint: '.external/zrender'
}, },
@ -132,6 +211,7 @@ const BROWSER_SUPPORT_ADAPTERS = [
description: '3D analytics extension bundled beneath the ECharts runtime.', description: '3D analytics extension bundled beneath the ECharts runtime.',
id: '1i-echarts-gl', id: '1i-echarts-gl',
phase: '1I', phase: '1I',
queuePosition: 21,
repo: 'ecomfe/echarts-gl', repo: 'ecomfe/echarts-gl',
sourcePathHint: '.external/echarts-gl' sourcePathHint: '.external/echarts-gl'
}, },
@ -148,14 +228,25 @@ const BROWSER_SUPPORT_ADAPTERS = [
description: 'Effects package bundled beside model-viewer for decorated previews.', description: 'Effects package bundled beside model-viewer for decorated previews.',
id: '1j-model-viewer-effects', id: '1j-model-viewer-effects',
phase: '1J', phase: '1J',
queuePosition: 16,
repo: 'google/model-viewer/packages/model-viewer-effects', repo: 'google/model-viewer/packages/model-viewer-effects',
sourcePathHint: '.external/model-viewer/packages/model-viewer-effects' sourcePathHint: '.external/model-viewer/packages/model-viewer-effects'
}, },
{
activation: 'reference-only',
description: 'The documentation application is retained as reference evidence and is intentionally absent from the player runtime.',
id: '1j-model-viewer-docs',
phase: '1J',
queuePosition: 17,
repo: 'google/model-viewer/packages/modelviewer.dev',
sourcePathHint: '.external/model-viewer/packages/modelviewer.dev'
},
{ {
activation: 'editor-sidecar', activation: 'editor-sidecar',
description: 'Editor-oriented inspection UI retained as a sidecar instead of a default runtime export.', description: 'Editor-oriented inspection UI retained as a sidecar instead of a default runtime export.',
id: '1j-space-opera', id: '1j-space-opera',
phase: '1J', phase: '1J',
queuePosition: 19,
repo: 'google/model-viewer/packages/space-opera', repo: 'google/model-viewer/packages/space-opera',
sourcePathHint: '.external/model-viewer/packages/space-opera' sourcePathHint: '.external/model-viewer/packages/space-opera'
}, },
@ -164,12 +255,26 @@ const BROWSER_SUPPORT_ADAPTERS = [
description: 'Renderer comparison and fidelity tooling kept in the QA lane rather than loaded by default.', description: 'Renderer comparison and fidelity tooling kept in the QA lane rather than loaded by default.',
id: '1j-render-fidelity-tools', id: '1j-render-fidelity-tools',
phase: '1J', phase: '1J',
queuePosition: 18,
repo: 'google/model-viewer/packages/render-fidelity-tools', repo: 'google/model-viewer/packages/render-fidelity-tools',
sourcePathHint: '.external/model-viewer/packages/render-fidelity-tools' sourcePathHint: '.external/model-viewer/packages/render-fidelity-tools'
},
{
activation: 'reference-only',
description: 'The reference viewer remains a standards and QA comparison surface rather than a second embedded player renderer.',
id: '1e-gltf-sample-viewer',
phase: '1E',
queuePosition: 22,
repo: 'KhronosGroup/glTF-Sample-Viewer',
sourcePathHint: '.external/glTF-Sample-Viewer'
} }
]; ];
const BUNDLED_ADAPTER_ACTIVATIONS = new Set(['bundled-module', 'prebuilt-script']); const BUNDLED_ADAPTER_ACTIVATIONS = new Set([
'bundled-module',
'dependency-module',
'prebuilt-script'
]);
function resolveFallbackBootState() function resolveFallbackBootState()
{ {
@ -404,6 +509,16 @@ async function loadAdapter(AdapterId)
}; };
} }
if (Adapter.activation === 'reference-only')
{
return {
details: [Adapter.sourcePathHint],
id: AdapterId,
note: 'This row is retained as dependency or reference evidence and is intentionally not loaded as a player runtime.',
status: 'deferred'
};
}
return { return {
details: [Adapter.sourcePathHint], details: [Adapter.sourcePathHint],
id: AdapterId, id: AdapterId,

View file

@ -1,15 +1,18 @@
interface BrowserSupportAdapter { interface BrowserSupportAdapter {
activation: activation:
| 'bundled-module' | 'bundled-module'
| 'dependency-module'
| 'prebuilt-script' | 'prebuilt-script'
| 'source-sidecar' | 'source-sidecar'
| 'native-sidecar' | 'native-sidecar'
| 'editor-sidecar' | 'editor-sidecar'
| 'qa-sidecar'; | 'qa-sidecar'
| 'reference-only';
description: string; description: string;
id: string; id: string;
packageName?: string; packageName?: string;
phase: '1E' | '1G' | '1H' | '1I' | '1J'; phase: '1E' | '1F' | '1G' | '1H' | '1I' | '1J';
queuePosition?: number;
repo: string; repo: string;
sourcePathHint: string; sourcePathHint: string;
} }
@ -203,9 +206,19 @@ async function loadClayViewerStack(): Promise<LoadedAdapter>
} }
export const BrowserSupportAdapters: BrowserSupportAdapter[] = [ export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
{
id: '1f-cahidenes-native-solver',
phase: '1F',
queuePosition: 1,
repo: 'cahidenes/rubiks-cube-solver',
activation: 'native-sidecar',
sourcePathHint: '.external/rubiks-cube-solver',
description: 'Python solver and visualizer retained behind the first-party comparison-process boundary.'
},
{ {
id: '1e-gltf-sample-renderer', id: '1e-gltf-sample-renderer',
phase: '1E', phase: '1E',
queuePosition: 23,
repo: 'KhronosGroup/glTF-Sample-Renderer', repo: 'KhronosGroup/glTF-Sample-Renderer',
activation: 'bundled-module', activation: 'bundled-module',
packageName: '@khronosgroup/gltf-viewer', packageName: '@khronosgroup/gltf-viewer',
@ -215,6 +228,7 @@ export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
{ {
id: '1g-tentone-native-solver', id: '1g-tentone-native-solver',
phase: '1G', phase: '1G',
queuePosition: 2,
repo: 'tentone/rubix-solver', repo: 'tentone/rubix-solver',
activation: 'native-sidecar', activation: 'native-sidecar',
sourcePathHint: '.external/rubix-solver', sourcePathHint: '.external/rubix-solver',
@ -223,6 +237,7 @@ export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
{ {
id: '1h-code-vr', id: '1h-code-vr',
phase: '1H', phase: '1H',
queuePosition: 3,
repo: 'NuiLab/code-vr', repo: 'NuiLab/code-vr',
activation: 'source-sidecar', activation: 'source-sidecar',
sourcePathHint: '.external/code-vr', sourcePathHint: '.external/code-vr',
@ -231,6 +246,7 @@ export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
{ {
id: '1h-claygl', id: '1h-claygl',
phase: '1H', phase: '1H',
queuePosition: 4,
repo: 'pissang/claygl', repo: 'pissang/claygl',
activation: 'prebuilt-script', activation: 'prebuilt-script',
sourcePathHint: '.external/claygl/dist/claygl.js', sourcePathHint: '.external/claygl/dist/claygl.js',
@ -239,6 +255,7 @@ export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
{ {
id: '1h-clay-viewer', id: '1h-clay-viewer',
phase: '1H', phase: '1H',
queuePosition: 5,
repo: 'pissang/clay-viewer', repo: 'pissang/clay-viewer',
activation: 'prebuilt-script', activation: 'prebuilt-script',
sourcePathHint: '.external/clay-viewer/dist/clay-viewer.js', sourcePathHint: '.external/clay-viewer/dist/clay-viewer.js',
@ -247,6 +264,7 @@ export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
{ {
id: '1h-postprocessing', id: '1h-postprocessing',
phase: '1H', phase: '1H',
queuePosition: 6,
repo: 'pmndrs/postprocessing', repo: 'pmndrs/postprocessing',
activation: 'bundled-module', activation: 'bundled-module',
packageName: 'postprocessing', packageName: 'postprocessing',
@ -256,6 +274,7 @@ export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
{ {
id: '1h-react-postprocessing', id: '1h-react-postprocessing',
phase: '1H', phase: '1H',
queuePosition: 7,
repo: 'pmndrs/react-postprocessing', repo: 'pmndrs/react-postprocessing',
activation: 'bundled-module', activation: 'bundled-module',
packageName: '@react-three/postprocessing', packageName: '@react-three/postprocessing',
@ -265,6 +284,7 @@ export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
{ {
id: '1h-drei', id: '1h-drei',
phase: '1H', phase: '1H',
queuePosition: 8,
repo: 'pmndrs/drei', repo: 'pmndrs/drei',
activation: 'bundled-module', activation: 'bundled-module',
packageName: '@react-three/drei', packageName: '@react-three/drei',
@ -274,6 +294,7 @@ export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
{ {
id: '1h-uikit', id: '1h-uikit',
phase: '1H', phase: '1H',
queuePosition: 9,
repo: 'pmndrs/uikit', repo: 'pmndrs/uikit',
activation: 'bundled-module', activation: 'bundled-module',
packageName: '@pmndrs/uikit', packageName: '@pmndrs/uikit',
@ -283,6 +304,7 @@ export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
{ {
id: '1h-three-stdlib', id: '1h-three-stdlib',
phase: '1H', phase: '1H',
queuePosition: 10,
repo: 'pmndrs/three-stdlib', repo: 'pmndrs/three-stdlib',
activation: 'bundled-module', activation: 'bundled-module',
packageName: 'three-stdlib', packageName: 'three-stdlib',
@ -292,6 +314,7 @@ export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
{ {
id: '1h-maath', id: '1h-maath',
phase: '1H', phase: '1H',
queuePosition: 11,
repo: 'pmndrs/maath', repo: 'pmndrs/maath',
activation: 'bundled-module', activation: 'bundled-module',
packageName: 'maath', packageName: 'maath',
@ -301,6 +324,7 @@ export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
{ {
id: '1h-zustand', id: '1h-zustand',
phase: '1H', phase: '1H',
queuePosition: 12,
repo: 'pmndrs/zustand', repo: 'pmndrs/zustand',
activation: 'bundled-module', activation: 'bundled-module',
packageName: 'zustand', packageName: 'zustand',
@ -310,6 +334,7 @@ export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
{ {
id: '1h-leva', id: '1h-leva',
phase: '1H', phase: '1H',
queuePosition: 13,
repo: 'pmndrs/leva', repo: 'pmndrs/leva',
activation: 'bundled-module', activation: 'bundled-module',
packageName: 'leva', packageName: 'leva',
@ -319,6 +344,7 @@ export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
{ {
id: '1h-use-gesture', id: '1h-use-gesture',
phase: '1H', phase: '1H',
queuePosition: 14,
repo: 'pmndrs/use-gesture', repo: 'pmndrs/use-gesture',
activation: 'bundled-module', activation: 'bundled-module',
packageName: '@use-gesture/react', packageName: '@use-gesture/react',
@ -328,15 +354,77 @@ export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
{ {
id: '1h-react-spring', id: '1h-react-spring',
phase: '1H', phase: '1H',
queuePosition: 15,
repo: 'pmndrs/react-spring', repo: 'pmndrs/react-spring',
activation: 'bundled-module', activation: 'bundled-module',
packageName: '@react-spring/three and @react-spring/web', packageName: '@react-spring/three and @react-spring/web',
sourcePathHint: '.external/react-spring', sourcePathHint: '.external/react-spring',
description: 'Motion and interpolation runtime bundled for browser spatial transitions.' description: 'Motion and interpolation runtime bundled for browser spatial transitions.'
}, },
{
id: '1h-react-spring-core',
phase: '1H',
queuePosition: 24,
repo: '@react-spring/core',
activation: 'dependency-module',
packageName: '@react-spring/core',
sourcePathHint: '.external/react-spring/packages/core',
description: 'Core spring controller is bundled transitively through the owned React Spring runtime entrypoints.'
},
{
id: '1h-react-spring-shared',
phase: '1H',
queuePosition: 25,
repo: '@react-spring/shared',
activation: 'dependency-module',
packageName: '@react-spring/shared',
sourcePathHint: '.external/react-spring/packages/shared',
description: 'Shared React Spring utilities are retained and locked as transitive runtime dependencies.'
},
{
id: '1h-react-spring-types',
phase: '1H',
queuePosition: 26,
repo: '@react-spring/types',
activation: 'reference-only',
packageName: '@react-spring/types',
sourcePathHint: '.external/react-spring/packages/types',
description: 'Compile-time type declarations remain dependency evidence and are not presented as a player runtime.'
},
{
id: '1h-react-spring-parallax',
phase: '1H',
queuePosition: 27,
repo: '@react-spring/parallax',
activation: 'dependency-module',
packageName: '@react-spring/web (Parallax export)',
sourcePathHint: '.external/react-spring/packages/parallax',
description: 'Parallax behavior is supplied through the bundled React Spring web entrypoint rather than a fabricated standalone package.'
},
{
id: '1h-react-spring-rafz',
phase: '1H',
queuePosition: 28,
repo: '@react-spring/rafz',
activation: 'dependency-module',
packageName: '@react-spring/rafz',
sourcePathHint: '.external/react-spring/packages/rafz',
description: 'The request-animation-frame scheduler is locked as a transitive React Spring runtime dependency.'
},
{
id: '1h-react-spring-animated',
phase: '1H',
queuePosition: 29,
repo: '@react-spring/animated',
activation: 'dependency-module',
packageName: '@react-spring/animated',
sourcePathHint: '.external/react-spring/packages/animated',
description: 'Animated-value primitives are locked as a transitive React Spring runtime dependency.'
},
{ {
id: '1i-zrender', id: '1i-zrender',
phase: '1I', phase: '1I',
queuePosition: 20,
repo: 'ecomfe/zrender', repo: 'ecomfe/zrender',
activation: 'bundled-module', activation: 'bundled-module',
packageName: 'zrender', packageName: 'zrender',
@ -346,6 +434,7 @@ export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
{ {
id: '1i-echarts-gl', id: '1i-echarts-gl',
phase: '1I', phase: '1I',
queuePosition: 21,
repo: 'ecomfe/echarts-gl', repo: 'ecomfe/echarts-gl',
activation: 'bundled-module', activation: 'bundled-module',
packageName: 'echarts-gl', packageName: 'echarts-gl',
@ -364,15 +453,26 @@ export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
{ {
id: '1j-model-viewer-effects', id: '1j-model-viewer-effects',
phase: '1J', phase: '1J',
queuePosition: 16,
repo: 'google/model-viewer/packages/model-viewer-effects', repo: 'google/model-viewer/packages/model-viewer-effects',
activation: 'bundled-module', activation: 'bundled-module',
packageName: '@google/model-viewer-effects', packageName: '@google/model-viewer-effects',
sourcePathHint: '.external/model-viewer/packages/model-viewer-effects', sourcePathHint: '.external/model-viewer/packages/model-viewer-effects',
description: 'Effects package bundled beside model-viewer for decorated previews.' description: 'Effects package bundled beside model-viewer for decorated previews.'
}, },
{
id: '1j-model-viewer-docs',
phase: '1J',
queuePosition: 17,
repo: 'google/model-viewer/packages/modelviewer.dev',
activation: 'reference-only',
sourcePathHint: '.external/model-viewer/packages/modelviewer.dev',
description: 'The documentation application is retained as reference evidence and is intentionally absent from the player runtime.'
},
{ {
id: '1j-space-opera', id: '1j-space-opera',
phase: '1J', phase: '1J',
queuePosition: 19,
repo: 'google/model-viewer/packages/space-opera', repo: 'google/model-viewer/packages/space-opera',
activation: 'editor-sidecar', activation: 'editor-sidecar',
sourcePathHint: '.external/model-viewer/packages/space-opera', sourcePathHint: '.external/model-viewer/packages/space-opera',
@ -381,10 +481,20 @@ export const BrowserSupportAdapters: BrowserSupportAdapter[] = [
{ {
id: '1j-render-fidelity-tools', id: '1j-render-fidelity-tools',
phase: '1J', phase: '1J',
queuePosition: 18,
repo: 'google/model-viewer/packages/render-fidelity-tools', repo: 'google/model-viewer/packages/render-fidelity-tools',
activation: 'qa-sidecar', activation: 'qa-sidecar',
sourcePathHint: '.external/model-viewer/packages/render-fidelity-tools', sourcePathHint: '.external/model-viewer/packages/render-fidelity-tools',
description: 'Renderer comparison and fidelity tooling kept in the QA lane rather than loaded by default.' description: 'Renderer comparison and fidelity tooling kept in the QA lane rather than loaded by default.'
},
{
id: '1e-gltf-sample-viewer',
phase: '1E',
queuePosition: 22,
repo: 'KhronosGroup/glTF-Sample-Viewer',
activation: 'reference-only',
sourcePathHint: '.external/glTF-Sample-Viewer',
description: 'The reference viewer remains a standards and QA comparison surface rather than a second embedded player renderer.'
} }
]; ];
@ -400,11 +510,12 @@ export async function loadAdapter(AdapterId: string): Promise<LoadedAdapter>
case '1e-gltf-sample-renderer': case '1e-gltf-sample-renderer':
return loadViewerStack(); return loadViewerStack();
case '1f-cahidenes-native-solver':
case '1g-tentone-native-solver': case '1g-tentone-native-solver':
return { return {
id: AdapterId, id: AdapterId,
status: 'deferred', status: 'deferred',
note: 'Use the browser shell fallback form or Unreal IPC to route into the native tentone/OpenCV lane.' note: 'Use the first-party process boundary to route into this native or Python solver adjunct.'
}; };
case '1h-code-vr': case '1h-code-vr':
@ -428,6 +539,11 @@ export async function loadAdapter(AdapterId: string): Promise<LoadedAdapter>
case '1h-leva': case '1h-leva':
case '1h-use-gesture': case '1h-use-gesture':
case '1h-react-spring': case '1h-react-spring':
case '1h-react-spring-core':
case '1h-react-spring-shared':
case '1h-react-spring-parallax':
case '1h-react-spring-rafz':
case '1h-react-spring-animated':
return loadSpatialStack(); return loadSpatialStack();
case '1i-zrender': case '1i-zrender':
@ -452,6 +568,15 @@ export async function loadAdapter(AdapterId: string): Promise<LoadedAdapter>
note: 'render-fidelity-tools stays in the QA lane and is not auto-loaded in the browser shell.' note: 'render-fidelity-tools stays in the QA lane and is not auto-loaded in the browser shell.'
}; };
case '1h-react-spring-types':
case '1j-model-viewer-docs':
case '1e-gltf-sample-viewer':
return {
id: AdapterId,
status: 'deferred',
note: 'This row is retained as dependency or reference evidence and is intentionally not loaded as a player runtime.'
};
default: default:
throw new Error(`Unknown browser support adapter: ${AdapterId}`); throw new Error(`Unknown browser support adapter: ${AdapterId}`);
} }

View file

@ -78,6 +78,8 @@ namespace HyperTwistHttpVisionClientInternal
const FString& RequestJson, const FString& RequestJson,
FHttpJsonResponse& OutResponse) FHttpJsonResponse& OutResponse)
{ {
const TSharedRef<FHttpJsonResponse, ESPMode::ThreadSafe> ResponseState =
MakeShared<FHttpJsonResponse, ESPMode::ThreadSafe>();
OutResponse = FHttpJsonResponse(); OutResponse = FHttpJsonResponse();
if (Url.IsEmpty()) if (Url.IsEmpty())
@ -103,35 +105,36 @@ namespace HyperTwistHttpVisionClientInternal
} }
Request->OnProcessRequestComplete().BindLambda( Request->OnProcessRequestComplete().BindLambda(
[&OutResponse](FHttpRequestPtr, FHttpResponsePtr Response, bool bWasSuccessful) [ResponseState](FHttpRequestPtr, FHttpResponsePtr Response, const bool bWasSuccessful)
{ {
OutResponse.bCompleted = true; ResponseState->bCompleted = true;
if (Response.IsValid()) if (Response.IsValid())
{ {
OutResponse.StatusCode = Response->GetResponseCode(); ResponseState->StatusCode = Response->GetResponseCode();
OutResponse.ResponseBody = Response->GetContentAsString(); ResponseState->ResponseBody = Response->GetContentAsString();
} }
if (!bWasSuccessful) if (!bWasSuccessful)
{ {
OutResponse.Error = TEXT("request-failed"); ResponseState->Error = TEXT("request-failed");
return; return;
} }
if (!Response.IsValid()) if (!Response.IsValid())
{ {
OutResponse.Error = TEXT("response-missing"); ResponseState->Error = TEXT("response-missing");
return; return;
} }
if (!EHttpResponseCodes::IsOk(OutResponse.StatusCode)) if (!EHttpResponseCodes::IsOk(ResponseState->StatusCode))
{ {
OutResponse.Error = FString::Printf(TEXT("http-%d"), OutResponse.StatusCode); ResponseState->Error =
FString::Printf(TEXT("http-%d"), ResponseState->StatusCode);
return; return;
} }
OutResponse.bSucceeded = true; ResponseState->bSucceeded = true;
}); });
if (!Request->ProcessRequest()) if (!Request->ProcessRequest())
@ -142,19 +145,22 @@ namespace HyperTwistHttpVisionClientInternal
const double TimeoutSeconds = FMath::Max(static_cast<double>(Client.RequestTimeoutSeconds), 0.1); const double TimeoutSeconds = FMath::Max(static_cast<double>(Client.RequestTimeoutSeconds), 0.1);
const double Deadline = FPlatformTime::Seconds() + TimeoutSeconds; const double Deadline = FPlatformTime::Seconds() + TimeoutSeconds;
while (!OutResponse.bCompleted && FPlatformTime::Seconds() < Deadline) while (!ResponseState->bCompleted && FPlatformTime::Seconds() < Deadline)
{ {
FHttpModule::Get().GetHttpManager().Tick(0.01f); FHttpModule::Get().GetHttpManager().Tick(0.01f);
FPlatformProcess::Sleep(0.01f); FPlatformProcess::Sleep(0.01f);
} }
if (!OutResponse.bCompleted) if (!ResponseState->bCompleted)
{ {
Request->OnProcessRequestComplete().Unbind();
Request->CancelRequest(); Request->CancelRequest();
OutResponse.Error = TEXT("request-timeout"); ResponseState->Error = TEXT("request-timeout");
OutResponse = *ResponseState;
return false; return false;
} }
OutResponse = *ResponseState;
return OutResponse.bSucceeded; return OutResponse.bSucceeded;
} }
} }

View file

@ -64,18 +64,20 @@ namespace HyperTwistHttpVoiceClientInternal
FString Error; FString Error;
}; };
bool ExecuteRequest( bool ExecuteRequest(
const UHyperTwistHttpVoiceClient& Client, const UHyperTwistHttpVoiceClient& Client,
const FString& Verb, const FString& Verb,
const FString& Url, const FString& Url,
const FString& AcceptHeader, const FString& AcceptHeader,
const FString& ContentType, const FString& ContentType,
const FString& RequestBody, const FString& RequestBody,
FHttpResponseState& OutResponse) FHttpResponseState& OutResponse)
{ {
OutResponse = FHttpResponseState(); const TSharedRef<FHttpResponseState, ESPMode::ThreadSafe> ResponseState =
MakeShared<FHttpResponseState, ESPMode::ThreadSafe>();
OutResponse = FHttpResponseState();
if (Url.IsEmpty()) if (Url.IsEmpty())
{ {
OutResponse.Error = TEXT("service-base-url-missing"); OutResponse.Error = TEXT("service-base-url-missing");
return false; return false;
@ -100,38 +102,43 @@ namespace HyperTwistHttpVoiceClientInternal
Request->SetContentAsString(RequestBody); Request->SetContentAsString(RequestBody);
} }
Request->OnProcessRequestComplete().BindLambda( Request->OnProcessRequestComplete().BindLambda(
[&OutResponse](FHttpRequestPtr, FHttpResponsePtr Response, bool bWasSuccessful) [ResponseState](
{ FHttpRequestPtr,
OutResponse.bCompleted = true; FHttpResponsePtr Response,
const bool bWasSuccessful)
if (Response.IsValid())
{ {
OutResponse.StatusCode = Response->GetResponseCode(); ResponseState->bCompleted = true;
OutResponse.ResponseBody = Response->GetContentAsString();
OutResponse.ResponseBytes = Response->GetContent();
}
if (!bWasSuccessful) if (Response.IsValid())
{ {
OutResponse.Error = TEXT("request-failed"); ResponseState->StatusCode = Response->GetResponseCode();
return; ResponseState->ResponseBody = Response->GetContentAsString();
} ResponseState->ResponseBytes = Response->GetContent();
}
if (!Response.IsValid()) if (!bWasSuccessful)
{ {
OutResponse.Error = TEXT("response-missing"); ResponseState->Error = TEXT("request-failed");
return; return;
} }
if (!EHttpResponseCodes::IsOk(OutResponse.StatusCode)) if (!Response.IsValid())
{ {
OutResponse.Error = FString::Printf(TEXT("http-%d"), OutResponse.StatusCode); ResponseState->Error = TEXT("response-missing");
return; return;
} }
OutResponse.bSucceeded = true; if (!EHttpResponseCodes::IsOk(ResponseState->StatusCode))
}); {
ResponseState->Error = FString::Printf(
TEXT("http-%d"),
ResponseState->StatusCode);
return;
}
ResponseState->bSucceeded = true;
});
if (!Request->ProcessRequest()) if (!Request->ProcessRequest())
{ {
@ -139,23 +146,26 @@ namespace HyperTwistHttpVoiceClientInternal
return false; return false;
} }
const double TimeoutSeconds = FMath::Max(static_cast<double>(Client.RequestTimeoutSeconds), 0.1); const double TimeoutSeconds = FMath::Max(static_cast<double>(Client.RequestTimeoutSeconds), 0.1);
const double Deadline = FPlatformTime::Seconds() + TimeoutSeconds; const double Deadline = FPlatformTime::Seconds() + TimeoutSeconds;
while (!OutResponse.bCompleted && FPlatformTime::Seconds() < Deadline) while (!ResponseState->bCompleted && FPlatformTime::Seconds() < Deadline)
{ {
FHttpModule::Get().GetHttpManager().Tick(0.01f); FHttpModule::Get().GetHttpManager().Tick(0.01f);
FPlatformProcess::Sleep(0.01f); FPlatformProcess::Sleep(0.01f);
} }
if (!OutResponse.bCompleted) if (!ResponseState->bCompleted)
{ {
Request->CancelRequest(); Request->OnProcessRequestComplete().Unbind();
OutResponse.Error = TEXT("request-timeout"); Request->CancelRequest();
return false; ResponseState->Error = TEXT("request-timeout");
} OutResponse = *ResponseState;
return false;
}
return OutResponse.bSucceeded; OutResponse = *ResponseState;
} return OutResponse.bSucceeded;
}
bool ReadLe16(const TArray<uint8>& Bytes, const int32 Offset, uint16& OutValue) bool ReadLe16(const TArray<uint8>& Bytes, const int32 Offset, uint16& OutValue)
{ {

View file

@ -7,18 +7,23 @@
#include "HyperTwistBootstrap/HyperTwistContractLibrary.h" #include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
#include "HyperTwistRecognition/HyperTwistVoiceClient.h" #include "HyperTwistRecognition/HyperTwistVoiceClient.h"
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h" #include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"
#include "HyperTwistUX/HyperTwistPlayerSettings.h"
#include "TimerManager.h" #include "TimerManager.h"
namespace HyperTwistSpeechLibraryInternal namespace HyperTwistSpeechLibraryInternal
{ {
const FName SpeechAudioComponentTag(TEXT("HyperTwistSpeechAudio")); const FName SpeechAudioComponentTag(TEXT("HyperTwistSpeechAudio"));
const FName MusicAudioComponentTag(TEXT("HyperTwistMusicAudio"));
const FName EffectsAudioComponentTag(TEXT("HyperTwistEffectsAudio"));
constexpr float DefaultDuckVolumeMultiplier = 0.15f; constexpr float DefaultDuckVolumeMultiplier = 0.15f;
constexpr float RestoreDelaySeconds = 0.2f; constexpr float RestoreDelaySeconds = 0.2f;
struct FManagedAudioComponentEntry struct FManagedAudioComponentEntry
{ {
TWeakObjectPtr<UAudioComponent> AudioComponent; TWeakObjectPtr<UAudioComponent> AudioComponent;
bool bTreatAsSpeechAudio = false; EHyperTwistManagedAudioCategory Category =
EHyperTwistManagedAudioCategory::Effects;
float AuthoredVolumeMultiplier = 1.0f;
}; };
struct FManagedAudioRegistry struct FManagedAudioRegistry
@ -26,6 +31,10 @@ namespace HyperTwistSpeechLibraryInternal
TArray<FManagedAudioComponentEntry> RegisteredComponents; TArray<FManagedAudioComponentEntry> RegisteredComponents;
TMap<TWeakObjectPtr<UAudioComponent>, float> OriginalVolumeMap; TMap<TWeakObjectPtr<UAudioComponent>, float> OriginalVolumeMap;
FTimerHandle RestoreTimerHandle; FTimerHandle RestoreTimerHandle;
float MusicVolume = 1.0f;
float EffectsVolume = 1.0f;
float VoiceVolume = 1.0f;
float ActiveDuckVolumeMultiplier = DefaultDuckVolumeMultiplier;
bool bDuckingActive = false; bool bDuckingActive = false;
}; };
@ -62,9 +71,23 @@ namespace HyperTwistSpeechLibraryInternal
UObject* CreateVoiceClient(UObject* ContextObject, const bool bUseMockVoiceClient) UObject* CreateVoiceClient(UObject* ContextObject, const bool bUseMockVoiceClient)
{ {
UObject* Outer = ContextObject != nullptr ? ContextObject : GetTransientPackage(); UObject* Outer = ContextObject != nullptr ? ContextObject : GetTransientPackage();
return bUseMockVoiceClient if (bUseMockVoiceClient)
? static_cast<UObject*>(NewObject<UHyperTwistMockVoiceClient>(Outer)) {
: static_cast<UObject*>(NewObject<UHyperTwistHttpVoiceClient>(Outer)); return NewObject<UHyperTwistMockVoiceClient>(Outer);
}
UHyperTwistHttpVoiceClient* Client =
NewObject<UHyperTwistHttpVoiceClient>(Outer);
Client->LoadConfig();
const FHyperTwistPlayerPreferences Preferences =
UHyperTwistPlayerSettingsLibrary::LoadPreferences();
Client->ProviderLabel = Preferences.VoiceProviderId;
Client->ServiceBaseUrl = Preferences.VoiceEndpoint;
Client->AuthorizationToken.Reset();
UHyperTwistPlayerSettingsLibrary::LoadProviderCredential(
TEXT("voice-api-key"),
Client->AuthorizationToken);
return Client;
} }
void CompactManagedAudioRegistry() void CompactManagedAudioRegistry()
@ -85,27 +108,104 @@ namespace HyperTwistSpeechLibraryInternal
} }
} }
bool IsSpeechAudioComponent(const UAudioComponent* AudioComponent) EHyperTwistManagedAudioCategory ResolveTaggedCategory(
const UAudioComponent* AudioComponent
)
{ {
if (AudioComponent == nullptr) if (AudioComponent == nullptr)
{ {
return false; return EHyperTwistManagedAudioCategory::Effects;
} }
if (AudioComponent->ComponentHasTag(SpeechAudioComponentTag)) if (AudioComponent->ComponentHasTag(SpeechAudioComponentTag))
{ {
return true; return EHyperTwistManagedAudioCategory::Voice;
}
if (AudioComponent->ComponentHasTag(MusicAudioComponentTag))
{
return EHyperTwistManagedAudioCategory::Music;
}
if (AudioComponent->ComponentHasTag(EffectsAudioComponentTag))
{
return EHyperTwistManagedAudioCategory::Effects;
} }
for (const FManagedAudioComponentEntry& Entry : GManagedAudioRegistry.RegisteredComponents) for (const FManagedAudioComponentEntry& Entry : GManagedAudioRegistry.RegisteredComponents)
{ {
if (Entry.AudioComponent.Get() == AudioComponent) if (Entry.AudioComponent.Get() == AudioComponent)
{ {
return Entry.bTreatAsSpeechAudio; return Entry.Category;
} }
} }
return false; return EHyperTwistManagedAudioCategory::Effects;
}
float ResolveCategoryVolume(const EHyperTwistManagedAudioCategory Category)
{
switch (Category)
{
case EHyperTwistManagedAudioCategory::Music:
return GManagedAudioRegistry.MusicVolume;
case EHyperTwistManagedAudioCategory::Voice:
return GManagedAudioRegistry.VoiceVolume;
default:
return GManagedAudioRegistry.EffectsVolume;
}
}
void ApplyCategoryTag(
UAudioComponent* AudioComponent,
const EHyperTwistManagedAudioCategory Category
)
{
if (AudioComponent == nullptr)
{
return;
}
AudioComponent->ComponentTags.Remove(SpeechAudioComponentTag);
AudioComponent->ComponentTags.Remove(MusicAudioComponentTag);
AudioComponent->ComponentTags.Remove(EffectsAudioComponentTag);
switch (Category)
{
case EHyperTwistManagedAudioCategory::Music:
AudioComponent->ComponentTags.AddUnique(MusicAudioComponentTag);
break;
case EHyperTwistManagedAudioCategory::Voice:
AudioComponent->ComponentTags.AddUnique(SpeechAudioComponentTag);
break;
default:
AudioComponent->ComponentTags.AddUnique(EffectsAudioComponentTag);
break;
}
}
void ApplyCategoryVolume(FManagedAudioComponentEntry& Entry)
{
UAudioComponent* AudioComponent = Entry.AudioComponent.Get();
if (AudioComponent == nullptr)
{
return;
}
const float UnduckedVolume =
Entry.AuthoredVolumeMultiplier * ResolveCategoryVolume(Entry.Category);
const TWeakObjectPtr<UAudioComponent> AudioComponentKey(AudioComponent);
if (GManagedAudioRegistry.bDuckingActive
&& Entry.Category != EHyperTwistManagedAudioCategory::Voice)
{
GManagedAudioRegistry.OriginalVolumeMap.Add(
AudioComponentKey,
UnduckedVolume);
AudioComponent->SetVolumeMultiplier(
UnduckedVolume
* GManagedAudioRegistry.ActiveDuckVolumeMultiplier);
return;
}
GManagedAudioRegistry.OriginalVolumeMap.Remove(AudioComponentKey);
AudioComponent->SetVolumeMultiplier(UnduckedVolume);
} }
TArray<UAudioComponent*> ResolveManagedAudioComponents() TArray<UAudioComponent*> ResolveManagedAudioComponents()
@ -121,11 +221,6 @@ namespace HyperTwistSpeechLibraryInternal
} }
} }
if (Components.Num() > 0)
{
return Components;
}
if (GEngine == nullptr) if (GEngine == nullptr)
{ {
return Components; return Components;
@ -146,6 +241,15 @@ namespace HyperTwistSpeechLibraryInternal
{ {
if (AudioComponent != nullptr) if (AudioComponent != nullptr)
{ {
if (!Components.Contains(AudioComponent))
{
FManagedAudioComponentEntry Entry;
Entry.AudioComponent = AudioComponent;
Entry.Category = ResolveTaggedCategory(AudioComponent);
Entry.AuthoredVolumeMultiplier =
AudioComponent->VolumeMultiplier;
GManagedAudioRegistry.RegisteredComponents.Add(Entry);
}
Components.AddUnique(AudioComponent); Components.AddUnique(AudioComponent);
} }
} }
@ -165,21 +269,29 @@ namespace HyperTwistSpeechLibraryInternal
for (UAudioComponent* AudioComponent : AudioComponents) for (UAudioComponent* AudioComponent : AudioComponents)
{ {
if (AudioComponent == nullptr || IsSpeechAudioComponent(AudioComponent)) if (AudioComponent == nullptr
|| ResolveTaggedCategory(AudioComponent)
== EHyperTwistManagedAudioCategory::Voice)
{ {
continue; continue;
} }
const TWeakObjectPtr<UAudioComponent> AudioComponentKey(AudioComponent); const TWeakObjectPtr<UAudioComponent> AudioComponentKey(AudioComponent);
if (!GManagedAudioRegistry.OriginalVolumeMap.Contains(AudioComponentKey)) float UnduckedVolume = AudioComponent->VolumeMultiplier;
if (const float* ExistingUnduckedVolume =
GManagedAudioRegistry.OriginalVolumeMap.Find(AudioComponentKey))
{ {
GManagedAudioRegistry.OriginalVolumeMap.Add(AudioComponentKey, AudioComponent->VolumeMultiplier); UnduckedVolume = *ExistingUnduckedVolume;
} }
GManagedAudioRegistry.OriginalVolumeMap.Add(
AudioComponent->SetVolumeMultiplier(TargetVolume); AudioComponentKey,
UnduckedVolume);
AudioComponent->SetVolumeMultiplier(
UnduckedVolume * TargetVolume);
++DuckedComponentCount; ++DuckedComponentCount;
} }
GManagedAudioRegistry.ActiveDuckVolumeMultiplier = TargetVolume;
GManagedAudioRegistry.bDuckingActive = DuckedComponentCount > 0; GManagedAudioRegistry.bDuckingActive = DuckedComponentCount > 0;
return DuckedComponentCount; return DuckedComponentCount;
} }
@ -372,6 +484,18 @@ void UHyperTwistSpeechLibrary::RegisterManagedAudioComponent(
UAudioComponent* AudioComponent, UAudioComponent* AudioComponent,
const bool bTreatAsSpeechAudio const bool bTreatAsSpeechAudio
) )
{
RegisterCategorizedAudioComponent(
AudioComponent,
bTreatAsSpeechAudio
? EHyperTwistManagedAudioCategory::Voice
: EHyperTwistManagedAudioCategory::Effects);
}
void UHyperTwistSpeechLibrary::RegisterCategorizedAudioComponent(
UAudioComponent* AudioComponent,
const EHyperTwistManagedAudioCategory Category
)
{ {
if (AudioComponent == nullptr) if (AudioComponent == nullptr)
{ {
@ -384,25 +508,23 @@ void UHyperTwistSpeechLibrary::RegisterManagedAudioComponent(
{ {
if (Entry.AudioComponent.Get() == AudioComponent) if (Entry.AudioComponent.Get() == AudioComponent)
{ {
Entry.bTreatAsSpeechAudio = bTreatAsSpeechAudio; Entry.Category = Category;
if (bTreatAsSpeechAudio) HyperTwistSpeechLibraryInternal::ApplyCategoryTag(
{ AudioComponent,
AudioComponent->ComponentTags.AddUnique( Category);
HyperTwistSpeechLibraryInternal::SpeechAudioComponentTag HyperTwistSpeechLibraryInternal::ApplyCategoryVolume(Entry);
);
}
return; return;
} }
} }
HyperTwistSpeechLibraryInternal::FManagedAudioComponentEntry Entry; HyperTwistSpeechLibraryInternal::FManagedAudioComponentEntry Entry;
Entry.AudioComponent = AudioComponent; Entry.AudioComponent = AudioComponent;
Entry.bTreatAsSpeechAudio = bTreatAsSpeechAudio; Entry.Category = Category;
Entry.AuthoredVolumeMultiplier = AudioComponent->VolumeMultiplier;
HyperTwistSpeechLibraryInternal::GManagedAudioRegistry.RegisteredComponents.Add(Entry); HyperTwistSpeechLibraryInternal::GManagedAudioRegistry.RegisteredComponents.Add(Entry);
if (bTreatAsSpeechAudio) HyperTwistSpeechLibraryInternal::ApplyCategoryTag(AudioComponent, Category);
{ HyperTwistSpeechLibraryInternal::ApplyCategoryVolume(
AudioComponent->ComponentTags.AddUnique(HyperTwistSpeechLibraryInternal::SpeechAudioComponentTag); HyperTwistSpeechLibraryInternal::GManagedAudioRegistry.RegisteredComponents.Last());
}
} }
void UHyperTwistSpeechLibrary::UnregisterManagedAudioComponent(UAudioComponent* AudioComponent) void UHyperTwistSpeechLibrary::UnregisterManagedAudioComponent(UAudioComponent* AudioComponent)
@ -418,7 +540,32 @@ void UHyperTwistSpeechLibrary::UnregisterManagedAudioComponent(UAudioComponent*
return Entry.AudioComponent.Get() == AudioComponent; return Entry.AudioComponent.Get() == AudioComponent;
} }
); );
HyperTwistSpeechLibraryInternal::GManagedAudioRegistry.OriginalVolumeMap.Remove(AudioComponent); HyperTwistSpeechLibraryInternal::GManagedAudioRegistry.OriginalVolumeMap.Remove(
TWeakObjectPtr<UAudioComponent>(AudioComponent));
}
int32 UHyperTwistSpeechLibrary::ApplyManagedAudioCategoryVolumes(
const float MusicVolume,
const float EffectsVolume,
const float VoiceVolume
)
{
using namespace HyperTwistSpeechLibraryInternal;
GManagedAudioRegistry.MusicVolume = FMath::Clamp(MusicVolume, 0.0f, 1.0f);
GManagedAudioRegistry.EffectsVolume = FMath::Clamp(EffectsVolume, 0.0f, 1.0f);
GManagedAudioRegistry.VoiceVolume = FMath::Clamp(VoiceVolume, 0.0f, 1.0f);
ResolveManagedAudioComponents();
int32 AppliedComponentCount = 0;
for (FManagedAudioComponentEntry& Entry : GManagedAudioRegistry.RegisteredComponents)
{
if (Entry.AudioComponent.IsValid())
{
ApplyCategoryVolume(Entry);
++AppliedComponentCount;
}
}
return AppliedComponentCount;
} }
int32 UHyperTwistSpeechLibrary::DuckManagedAudioComponents(const float DuckVolumeMultiplier) int32 UHyperTwistSpeechLibrary::DuckManagedAudioComponents(const float DuckVolumeMultiplier)

View file

@ -23,6 +23,7 @@
#include "HyperTwistSimulation/HyperTwistPuzzleViewerComponent.h" #include "HyperTwistSimulation/HyperTwistPuzzleViewerComponent.h"
#include "HyperTwistSolverLibrary.h" #include "HyperTwistSolverLibrary.h"
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h" #include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"
#include "HyperTwistUX/HyperTwistPlayerSettings.h"
#include "Interfaces/VoiceCapture.h" #include "Interfaces/VoiceCapture.h"
#include "JsonObjectConverter.h" #include "JsonObjectConverter.h"
#include "Kismet/GameplayStatics.h" #include "Kismet/GameplayStatics.h"
@ -1169,6 +1170,25 @@ void AHyperTwistClassicCubeGameMode::RecordSolvedAttemptToLocalLeaderboard()
bool AHyperTwistClassicCubeGameMode::BeginVoiceCommandCapture() bool AHyperTwistClassicCubeGameMode::BeginVoiceCommandCapture()
{ {
const FHyperTwistPlayerPreferences Preferences =
UHyperTwistPlayerSettingsLibrary::LoadPreferences();
if (!Preferences.bSpeechInputEnabled)
{
ResultLineOverride =
TEXT("result: speech input is disabled in Settings > Voice & AI");
return false;
}
const bool bCloudSpeechProvider =
Preferences.SpeechProviderId == TEXT("openai")
|| Preferences.SpeechProviderId == TEXT("groq")
|| Preferences.SpeechProviderId == TEXT("deepgram")
|| Preferences.SpeechProviderId == TEXT("elevenlabs");
if (bCloudSpeechProvider && !Preferences.bAllowCloudProviders)
{
ResultLineOverride =
TEXT("result: enable cloud providers before using this speech route");
return false;
}
if (!bEnableVoiceCommandCapture || bVoiceCommandCaptureActive) if (!bEnableVoiceCommandCapture || bVoiceCommandCaptureActive)
{ {
return bVoiceCommandCaptureActive; return bVoiceCommandCaptureActive;
@ -1200,7 +1220,11 @@ bool AHyperTwistClassicCubeGameMode::BeginVoiceCommandCapture()
} }
ActiveVoiceCapture = FVoiceModule::Get().CreateVoiceCapture( ActiveVoiceCapture = FVoiceModule::Get().CreateVoiceCapture(
TEXT(""), Preferences.SpeechMicrophoneId.Equals(
TEXT("system-default"),
ESearchCase::IgnoreCase)
? TEXT("")
: *Preferences.SpeechMicrophoneId,
VoiceCaptureSampleRateHz, VoiceCaptureSampleRateHz,
VoiceCaptureChannelCount VoiceCaptureChannelCount
); );
@ -1515,21 +1539,37 @@ void AHyperTwistClassicCubeGameMode::RefreshHud()
? TEXT("mode: follow-along") ? TEXT("mode: follow-along")
: TEXT("mode: free play")) : TEXT("mode: free play"))
: ModeLineOverride; : ModeLineOverride;
FString VoiceLine = VoiceLineOverride.IsEmpty()
? FString::Printf(TEXT("voice: %s"), *SelectedVoiceProfileId)
: VoiceLineOverride;
FString ReplayLine = ReplayLineOverride.IsEmpty() FString ReplayLine = ReplayLineOverride.IsEmpty()
? TEXT("replay: armed for local capture") ? TEXT("replay: armed for local capture")
: ReplayLineOverride; : ReplayLineOverride;
FString LeaderboardLine = LeaderboardLineOverride.IsEmpty() FString LeaderboardLine = LeaderboardLineOverride.IsEmpty()
? TEXT("leaderboard: no local best yet") ? TEXT("leaderboard: no local best yet")
: LeaderboardLineOverride; : LeaderboardLineOverride;
FString TrainingLine;
const FString ControlsLine = const FString ControlsLine =
TEXT("controls: LMB clockwise, RMB counter-clockwise, touch clockwise, MMB drag orbit, wheel zoom, R scramble, H hint, Enter submit, F mode, V hold-to-talk, C cycle voice"); TEXT("controls: LMB clockwise, RMB counter-clockwise, touch clockwise, MMB drag orbit, wheel zoom, R scramble, H hint, Enter submit, F mode, F3 coach, Esc settings");
UHyperTwistTrainingSubsystem* TrainingSubsystem = ResolveTrainingSubsystem(); UHyperTwistTrainingSubsystem* TrainingSubsystem = ResolveTrainingSubsystem();
FHyperTwistTrainingLiveTimerState LiveTimerState; FHyperTwistTrainingLiveTimerState LiveTimerState;
const bool bHasLiveTimer = TrainingSubsystem != nullptr && TrainingSubsystem->HasActiveLiveTimer(); const bool bHasLiveTimer = TrainingSubsystem != nullptr && TrainingSubsystem->HasActiveLiveTimer();
if (TrainingSubsystem != nullptr)
{
const FHyperTwistTrainingRunState RunState = TrainingSubsystem->GetActiveRunState();
if (RunState.IsStructurallyValid()
&& RunState.CurrentSelection.TrainingCase.IsStructurallyValid())
{
const FHyperTwistTrainingCase& TrainingCase =
RunState.CurrentSelection.TrainingCase;
TrainingLine = FString::Printf(
TEXT("training: %s | %s | %d case%s remain"),
*RunState.ActiveDeck.Title,
TrainingCase.PromptLabel.IsEmpty()
? *TrainingCase.CaseId
: *TrainingCase.PromptLabel,
RunState.RemainingCaseIds.Num(),
RunState.RemainingCaseIds.Num() == 1 ? TEXT("") : TEXT("s"));
}
}
if (bHasLiveTimer) if (bHasLiveTimer)
{ {
LiveTimerState = TrainingSubsystem->GetActiveLiveTimerState(); LiveTimerState = TrainingSubsystem->GetActiveLiveTimerState();
@ -1613,16 +1653,13 @@ void AHyperTwistClassicCubeGameMode::RefreshHud()
ReplayLine, ReplayLine,
LeaderboardLine, LeaderboardLine,
ModeLine, ModeLine,
VoiceLine TrainingLine
); );
ActiveHudWidget->SetModeToggleButtonLabel( ActiveHudWidget->SetModeToggleButtonLabel(
ActiveSessionMode == EHyperTwistClassicCubeSessionMode::FollowAlong ActiveSessionMode == EHyperTwistClassicCubeSessionMode::FollowAlong
? TEXT("Switch To Free Play") ? TEXT("Switch To Free Play")
: TEXT("Switch To Follow Along") : TEXT("Switch To Follow Along")
); );
ActiveHudWidget->SetVoiceCycleButtonLabel(
FString::Printf(TEXT("Voice: %s"), *SelectedVoiceProfileId)
);
} }
FString AHyperTwistClassicCubeGameMode::BuildSessionId() const FString AHyperTwistClassicCubeGameMode::BuildSessionId() const
@ -1876,6 +1913,22 @@ TArray<FString> AHyperTwistClassicCubeGameMode::ResolveScrambleMovesForCurrentSe
} }
} }
if (const UHyperTwistTrainingSubsystem* TrainingSubsystem = ResolveTrainingSubsystem())
{
const FHyperTwistTrainingRunState RunState = TrainingSubsystem->GetActiveRunState();
if (RunState.IsStructurallyValid()
&& RunState.CurrentSelection.TrainingCase.IsStructurallyValid())
{
const TArray<FString> CaseScrambleMoves =
HyperTwistClassicCubeGameModeInternal::ParseNotationSequence(
RunState.CurrentSelection.TrainingCase.ScrambleNotation);
if (!CaseScrambleMoves.IsEmpty())
{
return CaseScrambleMoves;
}
}
}
return ActiveCubeActor != nullptr return ActiveCubeActor != nullptr
? ActiveCubeActor->GenerateScramble(ScrambleLength) ? ActiveCubeActor->GenerateScramble(ScrambleLength)
: TArray<FString>(); : TArray<FString>();
@ -2046,12 +2099,27 @@ void AHyperTwistClassicCubeGameMode::QueueNarrationEvent(
const FString& ScriptText const FString& ScriptText
) )
{ {
if (!bEnableCoachNarration || ScriptText.IsEmpty()) const FHyperTwistPlayerPreferences Preferences =
UHyperTwistPlayerSettingsLibrary::LoadPreferences();
if (!bEnableCoachNarration
|| !Preferences.bCoachNarrationEnabled
|| ScriptText.IsEmpty())
{
return;
}
const bool bCloudVoiceProvider =
Preferences.VoiceProviderId == TEXT("openai")
|| Preferences.VoiceProviderId == TEXT("elevenlabs");
if (bCloudVoiceProvider && !Preferences.bAllowCloudProviders)
{ {
return; return;
} }
RefreshVoiceProfiles(); RefreshVoiceProfiles();
if (!Preferences.VoiceId.IsEmpty())
{
SelectedVoiceProfileId = Preferences.VoiceId;
}
FHyperTwistNarrationSynthesisRequest Request = FHyperTwistNarrationSynthesisRequest Request =
UHyperTwistContractLibrary::MakeSampleNarrationSynthesisRequest(); UHyperTwistContractLibrary::MakeSampleNarrationSynthesisRequest();
@ -2063,6 +2131,8 @@ void AHyperTwistClassicCubeGameMode::QueueNarrationEvent(
Request.ScriptText = ScriptText; Request.ScriptText = ScriptText;
Request.SubtitleSeedText = ScriptText; Request.SubtitleSeedText = ScriptText;
Request.VoiceProfileId = SelectedVoiceProfileId; Request.VoiceProfileId = SelectedVoiceProfileId;
Request.LengthScale = 1.0f / FMath::Max(Preferences.VoiceSpeed, 0.1f);
Request.OrchestrationProfile.ModelBindingId = Preferences.VoiceModel;
const FHyperTwistNarrationSynthesisResult NarrationResult = const FHyperTwistNarrationSynthesisResult NarrationResult =
UHyperTwistSpeechLibrary::SynthesizeNarration(this, Request, bUseMockSpeechClient); UHyperTwistSpeechLibrary::SynthesizeNarration(this, Request, bUseMockSpeechClient);
if (TryPlayNarrationResult(NarrationResult)) if (TryPlayNarrationResult(NarrationResult))

View file

@ -89,7 +89,7 @@ void UHyperTwistClassicCubeHUDWidget::SetHudLines(
const FString& InReplayLine, const FString& InReplayLine,
const FString& InLeaderboardLine, const FString& InLeaderboardLine,
const FString& InModeLine, const FString& InModeLine,
const FString& InVoiceLine const FString& InTrainingLine
) )
{ {
EnsureWidgetTreeBuilt(); EnsureWidgetTreeBuilt();
@ -138,9 +138,13 @@ void UHyperTwistClassicCubeHUDWidget::SetHudLines(
{ {
ModeTextBlock->SetText(FText::FromString(InModeLine)); ModeTextBlock->SetText(FText::FromString(InModeLine));
} }
if (VoiceTextBlock != nullptr) if (TrainingTextBlock != nullptr)
{ {
VoiceTextBlock->SetText(FText::FromString(InVoiceLine)); TrainingTextBlock->SetText(FText::FromString(InTrainingLine));
TrainingTextBlock->SetVisibility(
InTrainingLine.IsEmpty()
? ESlateVisibility::Collapsed
: ESlateVisibility::HitTestInvisible);
} }
} }
@ -159,9 +163,7 @@ bool UHyperTwistClassicCubeHUDWidget::IsAnyActionButtonHovered() const
return IsHovered(NewScrambleButton) return IsHovered(NewScrambleButton)
|| IsHovered(HintButton) || IsHovered(HintButton)
|| IsHovered(SubmitSolveButton) || IsHovered(SubmitSolveButton)
|| IsHovered(ModeToggleButton) || IsHovered(ModeToggleButton);
|| IsHovered(VoiceHoldButton)
|| IsHovered(VoiceCycleButton);
} }
void UHyperTwistClassicCubeHUDWidget::SetModeToggleButtonLabel(const FString& InLabel) void UHyperTwistClassicCubeHUDWidget::SetModeToggleButtonLabel(const FString& InLabel)
@ -173,15 +175,6 @@ void UHyperTwistClassicCubeHUDWidget::SetModeToggleButtonLabel(const FString& In
} }
} }
void UHyperTwistClassicCubeHUDWidget::SetVoiceCycleButtonLabel(const FString& InLabel)
{
EnsureWidgetTreeBuilt();
if (VoiceCycleButtonLabel != nullptr)
{
VoiceCycleButtonLabel->SetText(FText::FromString(InLabel));
}
}
void UHyperTwistClassicCubeHUDWidget::EnsureWidgetTreeBuilt() void UHyperTwistClassicCubeHUDWidget::EnsureWidgetTreeBuilt()
{ {
if (WidgetTree == nullptr || WidgetTree->RootWidget != nullptr) if (WidgetTree == nullptr || WidgetTree->RootWidget != nullptr)
@ -244,12 +237,12 @@ void UHyperTwistClassicCubeHUDWidget::EnsureWidgetTreeBuilt()
InspectionTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudInspection"), 14, HyperTwistClassicCubeHUDWidgetInternal::AccentColor); InspectionTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudInspection"), 14, HyperTwistClassicCubeHUDWidgetInternal::AccentColor);
ScrambleTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudScramble"), 13, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor); ScrambleTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudScramble"), 13, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor);
ResultTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudResult"), 14, HyperTwistClassicCubeHUDWidgetInternal::PrimaryTextColor); ResultTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudResult"), 14, HyperTwistClassicCubeHUDWidgetInternal::PrimaryTextColor);
TrainingTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudTraining"), 13, HyperTwistClassicCubeHUDWidgetInternal::AccentColor);
HintTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudHint"), 13, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor); HintTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudHint"), 13, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor);
SolutionTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudSolution"), 13, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor); SolutionTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudSolution"), 13, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor);
ModeTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudMode"), 13, HyperTwistClassicCubeHUDWidgetInternal::PrimaryTextColor); ModeTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudMode"), 13, HyperTwistClassicCubeHUDWidgetInternal::PrimaryTextColor);
ReplayTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudReplay"), 12, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor); ReplayTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudReplay"), 12, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor);
LeaderboardTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudLeaderboard"), 12, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor); LeaderboardTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudLeaderboard"), 12, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor);
VoiceTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudVoice"), 12, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor);
ControlsTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudControls"), 12, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor); ControlsTextBlock = AddLine(RootLayout, TEXT("ClassicCubeHudControls"), 12, HyperTwistClassicCubeHUDWidgetInternal::MutedTextColor);
UUniformGridPanel* ActionGrid = WidgetTree->ConstructWidget<UUniformGridPanel>( UUniformGridPanel* ActionGrid = WidgetTree->ConstructWidget<UUniformGridPanel>(
@ -298,25 +291,6 @@ void UHyperTwistClassicCubeHUDWidget::EnsureWidgetTreeBuilt()
1, 1,
1 1
); );
VoiceHoldButton = CreateActionButton(
ActionGrid,
TEXT("ClassicCubeHudVoiceHoldButton"),
TEXT("ClassicCubeHudVoiceHoldButtonLabel"),
VoiceHoldButtonLabel,
TEXT("Hold To Talk"),
2,
0
);
VoiceCycleButton = CreateActionButton(
ActionGrid,
TEXT("ClassicCubeHudVoiceCycleButton"),
TEXT("ClassicCubeHudVoiceCycleButtonLabel"),
VoiceCycleButtonLabel,
TEXT("Voice"),
2,
1
);
if (NewScrambleButton != nullptr) if (NewScrambleButton != nullptr)
{ {
NewScrambleButton->OnClicked.AddDynamic(this, &UHyperTwistClassicCubeHUDWidget::HandleNewScrambleClicked); NewScrambleButton->OnClicked.AddDynamic(this, &UHyperTwistClassicCubeHUDWidget::HandleNewScrambleClicked);
@ -333,29 +307,19 @@ void UHyperTwistClassicCubeHUDWidget::EnsureWidgetTreeBuilt()
{ {
ModeToggleButton->OnClicked.AddDynamic(this, &UHyperTwistClassicCubeHUDWidget::HandleModeToggleClicked); ModeToggleButton->OnClicked.AddDynamic(this, &UHyperTwistClassicCubeHUDWidget::HandleModeToggleClicked);
} }
if (VoiceHoldButton != nullptr)
{
VoiceHoldButton->OnPressed.AddDynamic(this, &UHyperTwistClassicCubeHUDWidget::HandleVoicePressed);
VoiceHoldButton->OnReleased.AddDynamic(this, &UHyperTwistClassicCubeHUDWidget::HandleVoiceReleased);
}
if (VoiceCycleButton != nullptr)
{
VoiceCycleButton->OnClicked.AddDynamic(this, &UHyperTwistClassicCubeHUDWidget::HandleVoiceCycleClicked);
}
SetHudLines( SetHudLines(
TEXT("status: preparing classic cube lane"), TEXT("status: preparing classic cube lane"),
TEXT("scramble: pending"), TEXT("scramble: pending"),
TEXT("timer: idle"), TEXT("timer: idle"),
TEXT("inspection: pending"), TEXT("inspection: pending"),
TEXT("result: waiting for first attempt"), TEXT("result: waiting for first attempt"),
TEXT("controls: LMB clockwise, RMB counter-clockwise, touch clockwise, MMB drag orbit, wheel zoom, R scramble, H hint, Enter submit, F mode, V hold-to-talk, C cycle voice"), TEXT("controls: LMB clockwise, RMB counter-clockwise, touch clockwise, MMB drag orbit, wheel zoom, R scramble, H hint, Enter submit, F mode, F3 coach, Esc settings"),
TEXT("hint: request solver guidance"), TEXT("hint: request solver guidance"),
TEXT("solution: unavailable"), TEXT("solution: unavailable"),
TEXT("replay: armed for local capture"), TEXT("replay: armed for local capture"),
TEXT("leaderboard: no local best yet"), TEXT("leaderboard: no local best yet"),
TEXT("mode: free play"), TEXT("mode: free play"),
TEXT("voice: en_US-lessac-medium") FString()
); );
} }
@ -483,36 +447,3 @@ void UHyperTwistClassicCubeHUDWidget::HandleModeToggleClicked()
); );
} }
} }
void UHyperTwistClassicCubeHUDWidget::HandleVoicePressed()
{
if (UObject* OperatorSurface =
HyperTwistClassicCubeHUDWidgetInternal::ResolveOperatorSurfaceObject(this))
{
IHyperTwistClassicCubeOperatorSurface::Execute_RequestClassicCubeBeginVoiceCommandCapture(
OperatorSurface
);
}
}
void UHyperTwistClassicCubeHUDWidget::HandleVoiceReleased()
{
if (UObject* OperatorSurface =
HyperTwistClassicCubeHUDWidgetInternal::ResolveOperatorSurfaceObject(this))
{
IHyperTwistClassicCubeOperatorSurface::Execute_RequestClassicCubeEndVoiceCommandCapture(
OperatorSurface
);
}
}
void UHyperTwistClassicCubeHUDWidget::HandleVoiceCycleClicked()
{
if (UObject* OperatorSurface =
HyperTwistClassicCubeHUDWidgetInternal::ResolveOperatorSurfaceObject(this))
{
IHyperTwistClassicCubeOperatorSurface::Execute_RequestClassicCubeCycleVoiceProfile(
OperatorSurface
);
}
}

View file

@ -5,6 +5,7 @@
#include "GameFramework/PlayerController.h" #include "GameFramework/PlayerController.h"
#include "GameFramework/SpringArmComponent.h" #include "GameFramework/SpringArmComponent.h"
#include "HyperTwistSimulation/HyperTwistClassicCubeActor.h" #include "HyperTwistSimulation/HyperTwistClassicCubeActor.h"
#include "HyperTwistUX/HyperTwistPlayerControllerBase.h"
#include "InputCoreTypes.h" #include "InputCoreTypes.h"
#include "Components/SceneComponent.h" #include "Components/SceneComponent.h"
@ -58,6 +59,20 @@ void AHyperTwistClassicCubeOrbitPawn::Tick(const float DeltaSeconds)
{ {
return; return;
} }
const AHyperTwistPlayerControllerBase* HyperTwistController =
Cast<AHyperTwistPlayerControllerBase>(PlayerController);
const FHyperTwistPlayerPreferences* Preferences = HyperTwistController != nullptr
? &HyperTwistController->GetPlayerPreferences()
: nullptr;
const float ZoomMultiplier = Preferences != nullptr
? Preferences->ZoomSensitivity
: 1.0f;
const float OrbitMultiplier = Preferences != nullptr
? Preferences->OrbitSensitivity
: 1.0f;
const float VerticalDirection = Preferences != nullptr && Preferences->bInvertOrbitY
? -1.0f
: 1.0f;
if (SpringArm != nullptr) if (SpringArm != nullptr)
{ {
@ -65,7 +80,7 @@ void AHyperTwistClassicCubeOrbitPawn::Tick(const float DeltaSeconds)
if (!FMath::IsNearlyZero(MouseWheelDelta)) if (!FMath::IsNearlyZero(MouseWheelDelta))
{ {
SpringArm->TargetArmLength = FMath::Clamp( SpringArm->TargetArmLength = FMath::Clamp(
SpringArm->TargetArmLength - (MouseWheelDelta * ZoomStep), SpringArm->TargetArmLength - (MouseWheelDelta * ZoomStep * ZoomMultiplier),
MinimumArmLength, MinimumArmLength,
MaximumArmLength MaximumArmLength
); );
@ -79,9 +94,10 @@ void AHyperTwistClassicCubeOrbitPawn::Tick(const float DeltaSeconds)
PlayerController->GetInputMouseDelta(MouseDeltaX, MouseDeltaY); PlayerController->GetInputMouseDelta(MouseDeltaX, MouseDeltaY);
if (!FMath::IsNearlyZero(MouseDeltaX) || !FMath::IsNearlyZero(MouseDeltaY)) if (!FMath::IsNearlyZero(MouseDeltaX) || !FMath::IsNearlyZero(MouseDeltaY))
{ {
CurrentYawDegrees += MouseDeltaX * OrbitYawDegreesPerPixel; CurrentYawDegrees += MouseDeltaX * OrbitYawDegreesPerPixel * OrbitMultiplier;
CurrentPitchDegrees = FMath::Clamp( CurrentPitchDegrees = FMath::Clamp(
CurrentPitchDegrees - (MouseDeltaY * OrbitPitchDegreesPerPixel), CurrentPitchDegrees
- (MouseDeltaY * OrbitPitchDegreesPerPixel * OrbitMultiplier * VerticalDirection),
MinimumPitchDegrees, MinimumPitchDegrees,
MaximumPitchDegrees MaximumPitchDegrees
); );

View file

@ -2,8 +2,10 @@
#include "EngineUtils.h" #include "EngineUtils.h"
#include "GameFramework/GameModeBase.h" #include "GameFramework/GameModeBase.h"
#include "HyperTwistAlgorithm/HyperTwistAlgorithmKeyboard.h"
#include "HyperTwistSimulation/HyperTwistClassicCubeActor.h" #include "HyperTwistSimulation/HyperTwistClassicCubeActor.h"
#include "HyperTwistSimulation/HyperTwistClassicCubeOperatorSurface.h" #include "HyperTwistSimulation/HyperTwistClassicCubeOperatorSurface.h"
#include "HyperTwistUX/HyperTwistPlayerSettings.h"
#include "InputCoreTypes.h" #include "InputCoreTypes.h"
namespace HyperTwistClassicCubePlayerControllerInternal namespace HyperTwistClassicCubePlayerControllerInternal
@ -39,6 +41,28 @@ void AHyperTwistClassicCubePlayerController::BeginPlay()
ApplyClassicCubeInputMode(); ApplyClassicCubeInputMode();
} }
FString AHyperTwistClassicCubePlayerController::GetPauseMenuTitle() const
{
return TEXT("Classic Cube");
}
FString AHyperTwistClassicCubePlayerController::GetPauseMenuSubtitle() const
{
return TEXT("3x3x3 free play, timing, hints, replay, and guided practice");
}
void AHyperTwistClassicCubePlayerController::PrepareForModalUi()
{
Super::PrepareForModalUi();
if (UObject* OperatorSurface =
HyperTwistClassicCubePlayerControllerInternal::ResolveOperatorSurfaceObject(this))
{
IHyperTwistClassicCubeOperatorSurface::
Execute_RequestClassicCubeEndVoiceCommandCapture(OperatorSurface);
}
bToggleVoiceCaptureActive = false;
}
void AHyperTwistClassicCubePlayerController::SetupInputComponent() void AHyperTwistClassicCubePlayerController::SetupInputComponent()
{ {
Super::SetupInputComponent(); Super::SetupInputComponent();
@ -64,7 +88,7 @@ void AHyperTwistClassicCubePlayerController::SetupInputComponent()
); );
} }
if (bEnableTouchTurnInput) if (bEnableTouchTurnInput && PlayerPreferences.bTouchInputEnabled)
{ {
InputComponent->BindTouch( InputComponent->BindTouch(
IE_Pressed, IE_Pressed,
@ -76,7 +100,9 @@ void AHyperTwistClassicCubePlayerController::SetupInputComponent()
if (bBindFreshAttemptShortcut) if (bBindFreshAttemptShortcut)
{ {
InputComponent->BindKey( InputComponent->BindKey(
EKeys::R, UHyperTwistPlayerSettingsLibrary::ResolveKeyBinding(
PlayerPreferences,
TEXT("puzzle.new")),
IE_Pressed, IE_Pressed,
this, this,
&AHyperTwistClassicCubePlayerController::HandleFreshAttemptShortcut &AHyperTwistClassicCubePlayerController::HandleFreshAttemptShortcut
@ -86,7 +112,9 @@ void AHyperTwistClassicCubePlayerController::SetupInputComponent()
if (bBindHintShortcut) if (bBindHintShortcut)
{ {
InputComponent->BindKey( InputComponent->BindKey(
EKeys::H, UHyperTwistPlayerSettingsLibrary::ResolveKeyBinding(
PlayerPreferences,
TEXT("puzzle.hint")),
IE_Pressed, IE_Pressed,
this, this,
&AHyperTwistClassicCubePlayerController::HandleHintShortcut &AHyperTwistClassicCubePlayerController::HandleHintShortcut
@ -96,7 +124,9 @@ void AHyperTwistClassicCubePlayerController::SetupInputComponent()
if (bBindSubmitSolveShortcut) if (bBindSubmitSolveShortcut)
{ {
InputComponent->BindKey( InputComponent->BindKey(
EKeys::Enter, UHyperTwistPlayerSettingsLibrary::ResolveKeyBinding(
PlayerPreferences,
TEXT("puzzle.submit")),
IE_Pressed, IE_Pressed,
this, this,
&AHyperTwistClassicCubePlayerController::HandleSubmitSolveShortcut &AHyperTwistClassicCubePlayerController::HandleSubmitSolveShortcut
@ -106,33 +136,43 @@ void AHyperTwistClassicCubePlayerController::SetupInputComponent()
if (bBindModeToggleShortcut) if (bBindModeToggleShortcut)
{ {
InputComponent->BindKey( InputComponent->BindKey(
EKeys::F, UHyperTwistPlayerSettingsLibrary::ResolveKeyBinding(
PlayerPreferences,
TEXT("lesson.toggle")),
IE_Pressed, IE_Pressed,
this, this,
&AHyperTwistClassicCubePlayerController::HandleModeToggleShortcut &AHyperTwistClassicCubePlayerController::HandleModeToggleShortcut
); );
} }
if (bBindVoiceHoldShortcut) if (bBindVoiceHoldShortcut && PlayerPreferences.bSpeechInputEnabled)
{ {
const FKey PushToTalkKey =
UHyperTwistPlayerSettingsLibrary::ResolveKeyBinding(
PlayerPreferences,
TEXT("speech.push-to-talk"));
InputComponent->BindKey( InputComponent->BindKey(
EKeys::V, PushToTalkKey,
IE_Pressed, IE_Pressed,
this, this,
&AHyperTwistClassicCubePlayerController::HandleVoicePressed &AHyperTwistClassicCubePlayerController::HandleVoicePressed
); );
InputComponent->BindKey( InputComponent->BindKey(
EKeys::V, PushToTalkKey,
IE_Released, IE_Released,
this, this,
&AHyperTwistClassicCubePlayerController::HandleVoiceReleased &AHyperTwistClassicCubePlayerController::HandleVoiceReleased
); );
} }
if (bBindVoiceCycleShortcut) if (bBindVoiceCycleShortcut
&& PlayerPreferences.bSpeechInputEnabled
&& PlayerPreferences.bCoachNarrationEnabled)
{ {
InputComponent->BindKey( InputComponent->BindKey(
EKeys::C, UHyperTwistPlayerSettingsLibrary::ResolveKeyBinding(
PlayerPreferences,
TEXT("speech.cycle-voice")),
IE_Pressed, IE_Pressed,
this, this,
&AHyperTwistClassicCubePlayerController::HandleVoiceCycleShortcut &AHyperTwistClassicCubePlayerController::HandleVoiceCycleShortcut
@ -140,6 +180,18 @@ void AHyperTwistClassicCubePlayerController::SetupInputComponent()
} }
} }
bool AHyperTwistClassicCubePlayerController::InputKey(
const FInputKeyEventArgs& Params
)
{
const bool bHandledByBase = Super::InputKey(Params);
if (bHandledByBase)
{
return true;
}
return TryApplyKeyboardPuzzleMove(Params);
}
bool AHyperTwistClassicCubePlayerController::TryProcessCubeClickFromCursor(const bool bCounterClockwise) bool AHyperTwistClassicCubePlayerController::TryProcessCubeClickFromCursor(const bool bCounterClockwise)
{ {
float ScreenX = 0.0f; float ScreenX = 0.0f;
@ -195,19 +247,62 @@ void AHyperTwistClassicCubePlayerController::RequestFreshAttempt()
void AHyperTwistClassicCubePlayerController::ApplyClassicCubeInputMode() void AHyperTwistClassicCubePlayerController::ApplyClassicCubeInputMode()
{ {
bShowMouseCursor = true;
bEnableClickEvents = true;
bEnableMouseOverEvents = true;
if (bUseGameAndUiInputMode) if (bUseGameAndUiInputMode)
{ {
FInputModeGameAndUI InputMode; ApplyGameAndUiInputMode();
InputMode.SetHideCursorDuringCapture(false);
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
SetInputMode(InputMode);
} }
} }
bool AHyperTwistClassicCubePlayerController::TryApplyKeyboardPuzzleMove(
const FInputKeyEventArgs& Params
)
{
if (Params.Event != IE_Pressed
|| ActivePauseMenuWidget != nullptr
|| (GetWorld() != nullptr && GetWorld()->IsPaused()))
{
return false;
}
if (UObject* OperatorSurface =
HyperTwistClassicCubePlayerControllerInternal::ResolveOperatorSurfaceObject(this))
{
if (!IHyperTwistClassicCubeOperatorSurface::
Execute_CanClassicCubeAcceptGameplayMoveInput(OperatorSurface))
{
return false;
}
}
FHyperTwistAlgorithmKeyboardEvent KeyboardEvent;
KeyboardEvent.Key = Params.Key;
KeyboardEvent.bCtrlPressed =
IsInputKeyDown(EKeys::LeftControl) || IsInputKeyDown(EKeys::RightControl);
KeyboardEvent.bAltPressed =
IsInputKeyDown(EKeys::LeftAlt) || IsInputKeyDown(EKeys::RightAlt);
KeyboardEvent.bShiftPressed =
IsInputKeyDown(EKeys::LeftShift) || IsInputKeyDown(EKeys::RightShift);
KeyboardEvent.bMetaPressed =
IsInputKeyDown(EKeys::LeftCommand) || IsInputKeyDown(EKeys::RightCommand);
KeyboardEvent.bIsRepeat = false;
const FHyperTwistAlgorithmKeyboardMappingResult Mapping =
UHyperTwistAlgorithmKeyboardLibrary::MapKeyboardEvent(
KeyboardEvent,
UHyperTwistPlayerSettingsLibrary::BuildClassicKeyboardProfile(
PlayerPreferences));
if (!Mapping.bHasMoveIntent || Mapping.CanonicalText.IsEmpty())
{
return false;
}
if (AHyperTwistClassicCubeActor* CubeActor = ResolveClassicCubeActor())
{
return CubeActor->TryQueueMoveNotation(Mapping.CanonicalText, true);
}
return false;
}
AHyperTwistClassicCubeActor* AHyperTwistClassicCubePlayerController::ResolveClassicCubeActor() const AHyperTwistClassicCubeActor* AHyperTwistClassicCubePlayerController::ResolveClassicCubeActor() const
{ {
if (GetWorld() == nullptr) if (GetWorld() == nullptr)
@ -299,14 +394,32 @@ void AHyperTwistClassicCubePlayerController::HandleVoicePressed()
if (UObject* OperatorSurface = if (UObject* OperatorSurface =
HyperTwistClassicCubePlayerControllerInternal::ResolveOperatorSurfaceObject(this)) HyperTwistClassicCubePlayerControllerInternal::ResolveOperatorSurfaceObject(this))
{ {
IHyperTwistClassicCubeOperatorSurface::Execute_RequestClassicCubeBeginVoiceCommandCapture( if (PlayerPreferences.SpeechActivationMode == TEXT("toggle"))
OperatorSurface {
); if (bToggleVoiceCaptureActive)
{
IHyperTwistClassicCubeOperatorSurface::
Execute_RequestClassicCubeEndVoiceCommandCapture(OperatorSurface);
}
else
{
IHyperTwistClassicCubeOperatorSurface::
Execute_RequestClassicCubeBeginVoiceCommandCapture(OperatorSurface);
}
bToggleVoiceCaptureActive = !bToggleVoiceCaptureActive;
return;
}
IHyperTwistClassicCubeOperatorSurface::
Execute_RequestClassicCubeBeginVoiceCommandCapture(OperatorSurface);
} }
} }
void AHyperTwistClassicCubePlayerController::HandleVoiceReleased() void AHyperTwistClassicCubePlayerController::HandleVoiceReleased()
{ {
if (PlayerPreferences.SpeechActivationMode == TEXT("toggle"))
{
return;
}
if (UObject* OperatorSurface = if (UObject* OperatorSurface =
HyperTwistClassicCubePlayerControllerInternal::ResolveOperatorSurfaceObject(this)) HyperTwistClassicCubePlayerControllerInternal::ResolveOperatorSurfaceObject(this))
{ {

View file

@ -0,0 +1,601 @@
#include "HyperTwistSimulation/HyperTwistMagic120CellRuntimeLibrary.h"
#include "Generated/HyperTwistMagic120CellPermutationData.inl"
#include "JsonObjectConverter.h"
#include "Misc/Base64.h"
#include "Misc/Compression.h"
namespace HyperTwistMagic120CellRuntimeLibraryInternal
{
constexpr int32 CellCount = 120;
constexpr int32 StickersPerCell = 63;
constexpr int32 TwistStickerCount = StickersPerCell - 1;
constexpr int32 StateSize = CellCount * StickersPerCell;
constexpr int32 MoveCount = CellCount * TwistStickerCount;
constexpr int32 HeaderSize = 24;
constexpr uint8 RawMagic[8] = {'H', 'T', 'M', '1', '2', '0', 'P', '1'};
uint16 ReadUInt16(const uint8* Bytes)
{
return static_cast<uint16>(Bytes[0])
| (static_cast<uint16>(Bytes[1]) << 8);
}
uint32 ReadUInt32(const uint8* Bytes)
{
return static_cast<uint32>(Bytes[0])
| (static_cast<uint32>(Bytes[1]) << 8)
| (static_cast<uint32>(Bytes[2]) << 16)
| (static_cast<uint32>(Bytes[3]) << 24);
}
struct FPermutationTable
{
TArray<uint8> Raw;
TArray<int32> MoveOffsets;
FString Error;
bool bValid = false;
};
FPermutationTable BuildPermutationTable()
{
FPermutationTable Table;
FString Encoded;
Encoded.Reserve(
HyperTwistMagic120CellGenerated::CompressedPermutationTableSize
* 4 / 3 + 8);
for (const TCHAR* Chunk :
HyperTwistMagic120CellGenerated::CompressedPermutationTableBase64Chunks)
{
Encoded.Append(Chunk);
}
TArray<uint8> Compressed;
if (!FBase64::Decode(Encoded, Compressed)
|| Compressed.Num()
!= HyperTwistMagic120CellGenerated::CompressedPermutationTableSize)
{
Table.Error = TEXT("compressed permutation table failed Base64 validation");
return Table;
}
Table.Raw.SetNumUninitialized(
HyperTwistMagic120CellGenerated::RawPermutationTableSize);
if (!FCompression::UncompressMemory(
NAME_Zlib,
Table.Raw.GetData(),
Table.Raw.Num(),
Compressed.GetData(),
Compressed.Num()))
{
Table.Raw.Reset();
Table.Error = TEXT("compressed permutation table failed zlib validation");
return Table;
}
if (Table.Raw.Num() < HeaderSize
|| FMemory::Memcmp(Table.Raw.GetData(), RawMagic, UE_ARRAY_COUNT(RawMagic)) != 0
|| ReadUInt32(Table.Raw.GetData() + 8) != CellCount
|| ReadUInt32(Table.Raw.GetData() + 12) != StickersPerCell
|| ReadUInt32(Table.Raw.GetData() + 16) != MoveCount
|| ReadUInt32(Table.Raw.GetData() + 20) != StateSize)
{
Table.Raw.Reset();
Table.Error = TEXT("permutation table header is invalid");
return Table;
}
Table.MoveOffsets.Reserve(MoveCount);
int32 Cursor = HeaderSize;
for (int32 MoveIndex = 0; MoveIndex < MoveCount; ++MoveIndex)
{
if (Cursor + static_cast<int32>(sizeof(uint16)) > Table.Raw.Num())
{
Table.Raw.Reset();
Table.MoveOffsets.Reset();
Table.Error = TEXT("permutation table ended before a move header");
return Table;
}
Table.MoveOffsets.Add(Cursor);
const int32 PairCount = ReadUInt16(Table.Raw.GetData() + Cursor);
Cursor += sizeof(uint16);
const int32 PairBytes = PairCount * sizeof(uint16) * 2;
if (PairCount < 1
|| PairCount > StateSize
|| Cursor + PairBytes > Table.Raw.Num())
{
Table.Raw.Reset();
Table.MoveOffsets.Reset();
Table.Error = FString::Printf(
TEXT("permutation table move %d is malformed"),
MoveIndex);
return Table;
}
for (int32 PairIndex = 0; PairIndex < PairCount; ++PairIndex)
{
const uint8* PairBytesAddress =
Table.Raw.GetData() + Cursor + PairIndex * 4;
if (ReadUInt16(PairBytesAddress) >= StateSize
|| ReadUInt16(PairBytesAddress + 2) >= StateSize)
{
Table.Raw.Reset();
Table.MoveOffsets.Reset();
Table.Error = FString::Printf(
TEXT("permutation table move %d references an invalid slot"),
MoveIndex);
return Table;
}
}
Cursor += PairBytes;
}
if (Cursor != Table.Raw.Num())
{
Table.Raw.Reset();
Table.MoveOffsets.Reset();
Table.Error = TEXT("permutation table contains trailing bytes");
return Table;
}
Table.bValid = true;
return Table;
}
const FPermutationTable& GetPermutationTable()
{
static const FPermutationTable Table = BuildPermutationTable();
return Table;
}
FHyperTwistPuzzleDefinitionRef BuildDefinition()
{
FHyperTwistPuzzleDefinitionRef Definition;
Definition.PuzzleId = TEXT("polychoron/magic120cell/full-color");
Definition.PuzzleFamily = EHyperTwistPuzzleFamily::Other;
Definition.Dimension = 4;
Definition.DefinitionVersion = TEXT("2026.07");
Definition.NotationProfile = TEXT("magic120cell-cell-sticker-axis-v1");
Definition.SizeVector = {CellCount, StickersPerCell};
Definition.Variant = TEXT("exact-7560-facelet-permutation");
return Definition;
}
bool IsSupportedDefinition(const FHyperTwistPuzzleDefinitionRef& Definition)
{
return Definition.IsStructurallyValid()
&& Definition.PuzzleId == TEXT("polychoron/magic120cell/full-color")
&& Definition.PuzzleFamily == EHyperTwistPuzzleFamily::Other
&& Definition.Dimension == 4
&& Definition.SizeVector.Num() == 2
&& Definition.SizeVector[0] == CellCount
&& Definition.SizeVector[1] == StickersPerCell;
}
bool IsSolvedState(const FHyperTwistMagic120CellRuntimeState& State)
{
if (State.StickerColorIndices.Num() != StateSize)
{
return false;
}
for (int32 SlotIndex = 0; SlotIndex < StateSize; ++SlotIndex)
{
if (State.StickerColorIndices[SlotIndex]
!= SlotIndex / StickersPerCell)
{
return false;
}
}
return true;
}
bool IsStateShapeValid(const FHyperTwistMagic120CellRuntimeState& State)
{
if (State.StateProfile != TEXT("magic120cell-7560-facelet-permutation-v1")
|| !IsSupportedDefinition(State.Definition)
|| State.StickerColorIndices.Num() != StateSize
|| State.AppliedMoveCount < 0)
{
return false;
}
int32 ColorCounts[CellCount] = {};
for (const int32 ColorIndex : State.StickerColorIndices)
{
if (ColorIndex < 0 || ColorIndex >= CellCount)
{
return false;
}
++ColorCounts[ColorIndex];
}
for (const int32 ColorCount : ColorCounts)
{
if (ColorCount != StickersPerCell)
{
return false;
}
}
return true;
}
int32 GetMoveIndex(const FHyperTwistMagic120CellTurnRequest& Request)
{
return Request.CellIndex * TwistStickerCount + Request.StickerIndex - 1;
}
FString BuildNotation(const FHyperTwistMagic120CellTurnRequest& Request)
{
return FString::Printf(
TEXT("C%03d/S%02d%s"),
Request.CellIndex + 1,
Request.StickerIndex,
Request.bInverse ? TEXT("'") : TEXT(""));
}
}
bool FHyperTwistMagic120CellRuntimeState::IsStructurallyValid() const
{
using namespace HyperTwistMagic120CellRuntimeLibraryInternal;
return IsStateShapeValid(*this)
&& bIsSolved == IsSolvedState(*this);
}
bool FHyperTwistMagic120CellTurnRequest::IsStructurallyValid() const
{
return CellIndex >= 0
&& CellIndex <
HyperTwistMagic120CellRuntimeLibraryInternal::CellCount
&& StickerIndex >= 1
&& StickerIndex <
HyperTwistMagic120CellRuntimeLibraryInternal::StickersPerCell;
}
bool FHyperTwistMagic120CellProjectedCell::IsStructurallyValid() const
{
using namespace HyperTwistMagic120CellRuntimeLibraryInternal;
if (CellIndex < 0
|| CellIndex >= CellCount
|| StickerColorIndices.Num() != StickersPerCell
|| RepresentativeColorIndex < 0
|| RepresentativeColorIndex >= CellCount
|| DisplacedStickerCount < 0
|| DisplacedStickerCount > StickersPerCell
|| bCellSolved != (DisplacedStickerCount == 0))
{
return false;
}
for (const int32 ColorIndex : StickerColorIndices)
{
if (ColorIndex < 0 || ColorIndex >= CellCount)
{
return false;
}
}
return true;
}
bool FHyperTwistMagic120CellProjection::IsStructurallyValid() const
{
using namespace HyperTwistMagic120CellRuntimeLibraryInternal;
if (ProjectionProfile
!= TEXT("magic120cell-120-cell-7560-facelet-projection-v1")
|| Cells.Num() != CellCount)
{
return false;
}
TBitArray<> SeenCells(false, CellCount);
int32 ProjectedStickerCount = 0;
for (const FHyperTwistMagic120CellProjectedCell& Cell : Cells)
{
if (!Cell.IsStructurallyValid() || SeenCells[Cell.CellIndex])
{
return false;
}
SeenCells[Cell.CellIndex] = true;
ProjectedStickerCount += Cell.StickerColorIndices.Num();
}
return ProjectedStickerCount == StateSize;
}
FHyperTwistPuzzleDefinitionRef
UHyperTwistMagic120CellRuntimeLibrary::MakePuzzleDefinition()
{
return HyperTwistMagic120CellRuntimeLibraryInternal::BuildDefinition();
}
FHyperTwistMagic120CellRuntimeState
UHyperTwistMagic120CellRuntimeLibrary::BuildSolvedState()
{
using namespace HyperTwistMagic120CellRuntimeLibraryInternal;
FHyperTwistMagic120CellRuntimeState State;
State.Definition = MakePuzzleDefinition();
State.StickerColorIndices.Reserve(StateSize);
for (int32 SlotIndex = 0; SlotIndex < StateSize; ++SlotIndex)
{
State.StickerColorIndices.Add(SlotIndex / StickersPerCell);
}
State.AppliedMoveCount = 0;
State.bIsSolved = true;
return State;
}
FHyperTwistPuzzleState
UHyperTwistMagic120CellRuntimeLibrary::BuildPuzzleStateEnvelope(
const FHyperTwistMagic120CellRuntimeState& RuntimeState
)
{
const FHyperTwistMagic120CellRuntimeState SafeState =
RuntimeState.IsStructurallyValid()
? RuntimeState
: BuildSolvedState();
FHyperTwistPuzzleState PuzzleState;
PuzzleState.Definition = SafeState.Definition;
PuzzleState.StateEncodingKind = EHyperTwistStateEncodingKind::Facelet;
PuzzleState.StateEncoding.EncodingProfile = SafeState.StateProfile;
PuzzleState.StateEncoding.PayloadJson =
SerializeRuntimeStateToJson(SafeState);
PuzzleState.OrientationFrame.Reference =
TEXT("magic120cell-canonical-cell-sticker-frame-v1");
PuzzleState.bIsSolved = SafeState.bIsSolved;
PuzzleState.Source = EHyperTwistStateSource::Runtime;
return PuzzleState;
}
FHyperTwistMagic120CellTurnResult
UHyperTwistMagic120CellRuntimeLibrary::ApplyTurn(
const FHyperTwistMagic120CellRuntimeState& State,
const FHyperTwistMagic120CellTurnRequest& Request
)
{
using namespace HyperTwistMagic120CellRuntimeLibraryInternal;
FHyperTwistMagic120CellTurnResult Result;
Result.State = State;
if (!State.IsStructurallyValid())
{
Result.Warnings.Add(TEXT("invalid-magic120cell-runtime-state"));
return Result;
}
if (!Request.IsStructurallyValid())
{
Result.Warnings.Add(TEXT("invalid-magic120cell-turn-request"));
return Result;
}
const FPermutationTable& Table = GetPermutationTable();
if (!Table.bValid)
{
Result.Warnings.Add(FString::Printf(
TEXT("magic120cell-permutation-table-unavailable: %s"),
*Table.Error));
return Result;
}
const int32 MoveIndex = GetMoveIndex(Request);
const int32 MoveOffset = Table.MoveOffsets[MoveIndex];
const int32 PairCount =
ReadUInt16(Table.Raw.GetData() + MoveOffset);
const uint8* PairData =
Table.Raw.GetData() + MoveOffset + sizeof(uint16);
Result.State.StickerColorIndices = State.StickerColorIndices;
for (int32 PairIndex = 0; PairIndex < PairCount; ++PairIndex)
{
const uint8* Pair = PairData + PairIndex * 4;
const int32 Destination = ReadUInt16(Pair);
const int32 Source = ReadUInt16(Pair + 2);
if (Request.bInverse)
{
Result.State.StickerColorIndices[Source] =
State.StickerColorIndices[Destination];
}
else
{
Result.State.StickerColorIndices[Destination] =
State.StickerColorIndices[Source];
}
}
Result.State.AppliedMoveCount = State.AppliedMoveCount + 1;
Result.State.bIsSolved = IsSolvedState(Result.State);
Result.AppliedNotation = BuildNotation(Request);
Result.MovedStickerCount = PairCount;
Result.bApplied = true;
Result.bExactStateUpdate = Result.State.IsStructurallyValid();
if (!Result.bExactStateUpdate)
{
Result.Warnings.Add(TEXT("magic120cell-state-became-invalid"));
}
return Result;
}
FHyperTwistMagic120CellScrambleResult
UHyperTwistMagic120CellRuntimeLibrary::GenerateScramble(
const int32 MoveCount,
const int32 RandomSeed
)
{
using namespace HyperTwistMagic120CellRuntimeLibraryInternal;
FHyperTwistMagic120CellScrambleResult Result;
if (MoveCount < 1 || MoveCount > 1000)
{
Result.Warnings.Add(TEXT("invalid-magic120cell-scramble-request"));
return Result;
}
FString TableError;
if (!IsPermutationTableReady(TableError))
{
Result.Warnings.Add(FString::Printf(
TEXT("magic120cell-permutation-table-unavailable: %s"),
*TableError));
return Result;
}
FRandomStream Random(RandomSeed);
Result.State = BuildSolvedState();
Result.Moves.Reserve(MoveCount);
Result.AppliedNotation.Reserve(MoveCount);
FHyperTwistMagic120CellTurnRequest PreviousRequest;
bool bHasPreviousRequest = false;
for (int32 MoveIndex = 0; MoveIndex < MoveCount; ++MoveIndex)
{
FHyperTwistMagic120CellTurnRequest Request;
do
{
Request.CellIndex = Random.RandRange(0, CellCount - 1);
Request.StickerIndex = Random.RandRange(1, StickersPerCell - 1);
Request.bInverse = Random.RandRange(0, 1) == 1;
}
while (bHasPreviousRequest
&& Request.CellIndex == PreviousRequest.CellIndex
&& Request.StickerIndex == PreviousRequest.StickerIndex);
const FHyperTwistMagic120CellTurnResult Turn =
ApplyTurn(Result.State, Request);
if (!Turn.bApplied || !Turn.bExactStateUpdate)
{
Result.Warnings = Turn.Warnings;
Result.Warnings.Add(TEXT("magic120cell-scramble-turn-failed"));
Result.State = FHyperTwistMagic120CellRuntimeState();
Result.Moves.Reset();
Result.AppliedNotation.Reset();
return Result;
}
Result.State = Turn.State;
Result.Moves.Add(Request);
Result.AppliedNotation.Add(Turn.AppliedNotation);
PreviousRequest = Request;
bHasPreviousRequest = true;
}
Result.bExactStateUpdate = Result.State.IsStructurallyValid();
Result.bGenerated = Result.bExactStateUpdate
&& Result.Moves.Num() == MoveCount
&& !Result.State.bIsSolved;
if (!Result.bGenerated)
{
Result.Warnings.Add(TEXT("magic120cell-scramble-result-invalid"));
}
return Result;
}
FHyperTwistMagic120CellProjectionBuildResult
UHyperTwistMagic120CellRuntimeLibrary::BuildProjection(
const FHyperTwistMagic120CellRuntimeState& State
)
{
using namespace HyperTwistMagic120CellRuntimeLibraryInternal;
FHyperTwistMagic120CellProjectionBuildResult Result;
if (!State.IsStructurallyValid())
{
Result.Warnings.Add(TEXT("invalid-magic120cell-runtime-state"));
return Result;
}
Result.Projection.Cells.Reserve(CellCount);
for (int32 CellIndex = 0; CellIndex < CellCount; ++CellIndex)
{
FHyperTwistMagic120CellProjectedCell Cell;
Cell.CellIndex = CellIndex;
Cell.StickerColorIndices.Reserve(StickersPerCell);
int32 ForeignColorCounts[CellCount] = {};
for (int32 StickerIndex = 0;
StickerIndex < StickersPerCell;
++StickerIndex)
{
const int32 ColorIndex =
State.StickerColorIndices[
CellIndex * StickersPerCell + StickerIndex];
Cell.StickerColorIndices.Add(ColorIndex);
if (ColorIndex != CellIndex)
{
++Cell.DisplacedStickerCount;
++ForeignColorCounts[ColorIndex];
}
}
Cell.RepresentativeColorIndex = CellIndex;
int32 StrongestForeignColorCount = 0;
for (int32 ColorIndex = 0; ColorIndex < CellCount; ++ColorIndex)
{
if (ForeignColorCounts[ColorIndex] > StrongestForeignColorCount)
{
StrongestForeignColorCount = ForeignColorCounts[ColorIndex];
Cell.RepresentativeColorIndex = ColorIndex;
}
}
Cell.bCellSolved = Cell.DisplacedStickerCount == 0;
Result.Projection.Cells.Add(MoveTemp(Cell));
}
Result.bProjected = Result.Projection.IsStructurallyValid();
Result.bExactProjection = Result.bProjected;
if (!Result.bProjected)
{
Result.Warnings.Add(TEXT("magic120cell-projection-invalid"));
}
return Result;
}
int32 UHyperTwistMagic120CellRuntimeLibrary::GetTurnOrderForSticker(
const int32 StickerIndex
)
{
if (StickerIndex >= 1 && StickerIndex <= 12)
{
return 5;
}
if (StickerIndex >= 13 && StickerIndex <= 42)
{
return 2;
}
if (StickerIndex >= 43 && StickerIndex <= 62)
{
return 3;
}
return 0;
}
FString UHyperTwistMagic120CellRuntimeLibrary::SerializeRuntimeStateToJson(
const FHyperTwistMagic120CellRuntimeState& RuntimeState
)
{
FString Json;
FJsonObjectConverter::UStructToJsonObjectString(
FHyperTwistMagic120CellRuntimeState::StaticStruct(),
&RuntimeState,
Json,
0,
0);
return Json;
}
bool UHyperTwistMagic120CellRuntimeLibrary::
TryDeserializeRuntimeStateFromJson(
const FString& Json,
FHyperTwistMagic120CellRuntimeState& OutRuntimeState
)
{
FHyperTwistMagic120CellRuntimeState ParsedState;
if (Json.IsEmpty()
|| !FJsonObjectConverter::JsonObjectStringToUStruct(
Json,
&ParsedState,
0,
0)
|| !ParsedState.IsStructurallyValid())
{
return false;
}
OutRuntimeState = MoveTemp(ParsedState);
return true;
}
bool UHyperTwistMagic120CellRuntimeLibrary::IsPermutationTableReady(
FString& OutError
)
{
const HyperTwistMagic120CellRuntimeLibraryInternal::FPermutationTable& Table =
HyperTwistMagic120CellRuntimeLibraryInternal::GetPermutationTable();
OutError = Table.Error;
return Table.bValid;
}

View file

@ -0,0 +1,973 @@
#include "HyperTwistSimulation/HyperTwistMagicCube5DRuntimeLibrary.h"
#include "JsonObjectConverter.h"
namespace HyperTwistMagicCube5DRuntimeLibraryInternal
{
constexpr int32 AxisCount = 5;
constexpr int32 MinimumOrder = 2;
constexpr int32 MaximumOrder = 6;
constexpr int32 ColorCount = AxisCount * 2;
int32 IntegerPower(const int32 Base, const int32 Exponent)
{
int32 Result = 1;
for (int32 Index = 0; Index < Exponent; ++Index)
{
Result *= Base;
}
return Result;
}
int32 GetPieceCount(const int32 Order)
{
return IntegerPower(Order, AxisCount);
}
int32 GetFaceletCount(const int32 Order)
{
return ColorCount * IntegerPower(Order, AxisCount - 1);
}
int32 GetBoundaryCubieCount(const int32 Order)
{
const int32 InteriorOrder = FMath::Max(Order - 2, 0);
return GetPieceCount(Order) - IntegerPower(InteriorOrder, AxisCount);
}
int32 CoordinateFromDigit(const int32 Digit, const int32 Order)
{
return Order % 2 == 0
? (Digit * 2) - (Order - 1)
: Digit - (Order / 2);
}
int32 DigitFromCoordinate(const int32 Coordinate, const int32 Order)
{
return Order % 2 == 0
? (Coordinate + (Order - 1)) / 2
: Coordinate + (Order / 2);
}
int32 GetBoundaryCoordinateMagnitude(const int32 Order)
{
return Order % 2 == 0 ? Order - 1 : Order / 2;
}
bool IsCoordinateValueValid(const int32 Coordinate, const int32 Order)
{
if (Order < MinimumOrder || Order > MaximumOrder)
{
return false;
}
const int32 Boundary = GetBoundaryCoordinateMagnitude(Order);
if (Coordinate < -Boundary || Coordinate > Boundary)
{
return false;
}
return Order % 2 != 0 || (Coordinate + Boundary) % 2 == 0;
}
int32 ToAxisIndex(const EHyperTwistMagicCube5DAxis Axis)
{
return static_cast<int32>(Axis);
}
EHyperTwistMagicCube5DAxis ToAxisEnum(const int32 AxisIndex)
{
switch (AxisIndex)
{
case 0:
return EHyperTwistMagicCube5DAxis::X;
case 1:
return EHyperTwistMagicCube5DAxis::Y;
case 2:
return EHyperTwistMagicCube5DAxis::Z;
case 3:
return EHyperTwistMagicCube5DAxis::W;
default:
return EHyperTwistMagicCube5DAxis::V;
}
}
FString GetAxisLabel(const EHyperTwistMagicCube5DAxis Axis)
{
switch (Axis)
{
case EHyperTwistMagicCube5DAxis::X:
return TEXT("X");
case EHyperTwistMagicCube5DAxis::Y:
return TEXT("Y");
case EHyperTwistMagicCube5DAxis::Z:
return TEXT("Z");
case EHyperTwistMagicCube5DAxis::W:
return TEXT("W");
default:
return TEXT("V");
}
}
int32 GetCoordinateValue(
const FHyperTwistMagicCube5DGridCoordinate& Coordinate,
const EHyperTwistMagicCube5DAxis Axis
)
{
const int32 AxisIndex = ToAxisIndex(Axis);
return Coordinate.Components.IsValidIndex(AxisIndex)
? Coordinate.Components[AxisIndex]
: 0;
}
void SetCoordinateValue(
FHyperTwistMagicCube5DGridCoordinate& Coordinate,
const EHyperTwistMagicCube5DAxis Axis,
const int32 Value
)
{
const int32 AxisIndex = ToAxisIndex(Axis);
if (Coordinate.Components.Num() != AxisCount)
{
Coordinate.Components.Init(0, AxisCount);
}
Coordinate.Components[AxisIndex] = Value;
}
FHyperTwistMagicCube5DGridCoordinate DecodeCoordinate(
const int32 PositionIndex,
const int32 Order
)
{
FHyperTwistMagicCube5DGridCoordinate Coordinate;
Coordinate.Components.Init(0, AxisCount);
int32 Remainder = PositionIndex;
for (int32 AxisIndex = 0; AxisIndex < AxisCount; ++AxisIndex)
{
Coordinate.Components[AxisIndex] =
CoordinateFromDigit(Remainder % Order, Order);
Remainder /= Order;
}
return Coordinate;
}
int32 EncodeCoordinate(
const FHyperTwistMagicCube5DGridCoordinate& Coordinate,
const int32 Order
)
{
int32 PositionIndex = 0;
int32 PlaceValue = 1;
for (int32 AxisIndex = 0; AxisIndex < AxisCount; ++AxisIndex)
{
PositionIndex +=
DigitFromCoordinate(Coordinate.Components[AxisIndex], Order) * PlaceValue;
PlaceValue *= Order;
}
return PositionIndex;
}
FHyperTwistMagicCube5DSignedAxis MakeSignedAxis(
const EHyperTwistMagicCube5DAxis Axis,
const bool bPositiveDirection
)
{
FHyperTwistMagicCube5DSignedAxis SignedAxis;
SignedAxis.Axis = Axis;
SignedAxis.bPositiveDirection = bPositiveDirection;
return SignedAxis;
}
FHyperTwistMagicCube5DPieceOrientation BuildIdentityOrientation()
{
FHyperTwistMagicCube5DPieceOrientation Orientation;
Orientation.Basis.Reserve(AxisCount);
for (int32 AxisIndex = 0; AxisIndex < AxisCount; ++AxisIndex)
{
Orientation.Basis.Add(MakeSignedAxis(ToAxisEnum(AxisIndex), true));
}
return Orientation;
}
bool IsIdentityOrientation(
const FHyperTwistMagicCube5DPieceOrientation& Orientation
)
{
if (Orientation.Basis.Num() != AxisCount)
{
return false;
}
for (int32 AxisIndex = 0; AxisIndex < AxisCount; ++AxisIndex)
{
if (Orientation.Basis[AxisIndex].Axis != ToAxisEnum(AxisIndex)
|| !Orientation.Basis[AxisIndex].bPositiveDirection)
{
return false;
}
}
return true;
}
FHyperTwistPuzzleDefinitionRef BuildDefinition(const int32 Order)
{
FHyperTwistPuzzleDefinitionRef Definition;
Definition.PuzzleId =
FString::Printf(TEXT("magiccube5d/%d-order"), Order);
Definition.PuzzleFamily = EHyperTwistPuzzleFamily::Hypercube;
Definition.Dimension = AxisCount;
Definition.DefinitionVersion = TEXT("2026.07");
Definition.NotationProfile = TEXT("magiccube5d-signed-face-plane-v1");
Definition.SizeVector.Init(Order, AxisCount);
Definition.Variant = TEXT("family-owned-exact-piece-orientation");
return Definition;
}
bool IsSupportedDefinition(const FHyperTwistPuzzleDefinitionRef& Definition)
{
if (!Definition.IsStructurallyValid()
|| Definition.PuzzleFamily != EHyperTwistPuzzleFamily::Hypercube
|| Definition.Dimension != AxisCount
|| Definition.SizeVector.Num() != AxisCount)
{
return false;
}
const int32 Order = Definition.SizeVector[0];
if (Order < MinimumOrder || Order > MaximumOrder)
{
return false;
}
for (const int32 Size : Definition.SizeVector)
{
if (Size != Order)
{
return false;
}
}
return Definition.PuzzleId ==
FString::Printf(TEXT("magiccube5d/%d-order"), Order);
}
bool IsRuntimeStateShapeValid(const FHyperTwistMagicCube5DRuntimeState& State)
{
const int32 PieceCount = GetPieceCount(State.Order);
if (State.StateProfile != FString::Printf(
TEXT("magiccube5d-order-%d-piece-orientation-v1"),
State.Order)
|| !IsSupportedDefinition(State.Definition)
|| State.Definition.SizeVector[0] != State.Order
|| State.PositionToPiece.Num() != PieceCount
|| State.PieceOrientations.Num() != PieceCount
|| State.AppliedMoveCount < 0)
{
return false;
}
TBitArray<> SeenPieces(false, PieceCount);
for (const int32 PieceId : State.PositionToPiece)
{
if (PieceId < 0 || PieceId >= PieceCount || SeenPieces[PieceId])
{
return false;
}
SeenPieces[PieceId] = true;
}
for (const FHyperTwistMagicCube5DPieceOrientation& Orientation :
State.PieceOrientations)
{
if (!Orientation.IsStructurallyValid())
{
return false;
}
}
return true;
}
bool IsSolvedRuntimeState(const FHyperTwistMagicCube5DRuntimeState& State)
{
if (!IsRuntimeStateShapeValid(State))
{
return false;
}
for (int32 PositionIndex = 0;
PositionIndex < State.PositionToPiece.Num();
++PositionIndex)
{
if (State.PositionToPiece[PositionIndex] != PositionIndex
|| !IsIdentityOrientation(State.PieceOrientations[PositionIndex]))
{
return false;
}
}
return true;
}
FHyperTwistMagicCube5DGridCoordinate RotateCoordinateInPlane(
const FHyperTwistMagicCube5DGridCoordinate& Coordinate,
const EHyperTwistMagicCube5DAxis AxisA,
const EHyperTwistMagicCube5DAxis AxisB,
const EHyperTwistMagicCube5DTurnDirection Direction
)
{
FHyperTwistMagicCube5DGridCoordinate Rotated = Coordinate;
const int32 ValueA = GetCoordinateValue(Coordinate, AxisA);
const int32 ValueB = GetCoordinateValue(Coordinate, AxisB);
if (Direction == EHyperTwistMagicCube5DTurnDirection::PositiveQuarterTurn)
{
SetCoordinateValue(Rotated, AxisA, -ValueB);
SetCoordinateValue(Rotated, AxisB, ValueA);
}
else
{
SetCoordinateValue(Rotated, AxisA, ValueB);
SetCoordinateValue(Rotated, AxisB, -ValueA);
}
return Rotated;
}
FHyperTwistMagicCube5DSignedAxis RotateSignedAxisInPlane(
const FHyperTwistMagicCube5DSignedAxis& SignedAxis,
const EHyperTwistMagicCube5DAxis AxisA,
const EHyperTwistMagicCube5DAxis AxisB,
const EHyperTwistMagicCube5DTurnDirection Direction
)
{
if (SignedAxis.Axis != AxisA && SignedAxis.Axis != AxisB)
{
return SignedAxis;
}
FHyperTwistMagicCube5DGridCoordinate UnitVector;
UnitVector.Components.Init(0, AxisCount);
SetCoordinateValue(
UnitVector,
SignedAxis.Axis,
SignedAxis.bPositiveDirection ? 1 : -1);
const FHyperTwistMagicCube5DGridCoordinate Rotated =
RotateCoordinateInPlane(UnitVector, AxisA, AxisB, Direction);
for (int32 AxisIndex = 0; AxisIndex < AxisCount; ++AxisIndex)
{
const int32 Value = Rotated.Components[AxisIndex];
if (Value != 0)
{
return MakeSignedAxis(ToAxisEnum(AxisIndex), Value > 0);
}
}
return SignedAxis;
}
FHyperTwistMagicCube5DPieceOrientation RotateOrientationInPlane(
const FHyperTwistMagicCube5DPieceOrientation& Orientation,
const EHyperTwistMagicCube5DAxis AxisA,
const EHyperTwistMagicCube5DAxis AxisB,
const EHyperTwistMagicCube5DTurnDirection Direction
)
{
FHyperTwistMagicCube5DPieceOrientation Rotated = Orientation;
for (FHyperTwistMagicCube5DSignedAxis& BasisAxis : Rotated.Basis)
{
BasisAxis = RotateSignedAxisInPlane(BasisAxis, AxisA, AxisB, Direction);
}
return Rotated;
}
TSet<int32> GetSelectedSliceCoordinates(
const FHyperTwistMagicCube5DTurnRequest& Request,
const int32 Order
)
{
TSet<int32> Coordinates;
for (int32 LayerIndex = 0; LayerIndex < Order; ++LayerIndex)
{
if ((Request.SliceMask & (1 << LayerIndex)) == 0)
{
continue;
}
const int32 Digit = Request.bPositiveFace
? Order - 1 - LayerIndex
: LayerIndex;
Coordinates.Add(CoordinateFromDigit(Digit, Order));
}
return Coordinates;
}
int32 ResolveColorForWorldSide(
const FHyperTwistMagicCube5DGridCoordinate& PieceHomeCoordinate,
const FHyperTwistMagicCube5DPieceOrientation& Orientation,
const EHyperTwistMagicCube5DAxis WorldAxis,
const bool bPositiveWorldSide,
const int32 Order
)
{
const int32 Boundary = GetBoundaryCoordinateMagnitude(Order);
for (int32 LocalAxisIndex = 0; LocalAxisIndex < AxisCount; ++LocalAxisIndex)
{
const FHyperTwistMagicCube5DSignedAxis& BasisAxis =
Orientation.Basis[LocalAxisIndex];
if (BasisAxis.Axis != WorldAxis)
{
continue;
}
const bool bPositiveLocalSide =
BasisAxis.bPositiveDirection == bPositiveWorldSide;
const int32 RequiredLocalCoordinate =
bPositiveLocalSide ? Boundary : -Boundary;
if (PieceHomeCoordinate.Components[LocalAxisIndex]
!= RequiredLocalCoordinate)
{
return INDEX_NONE;
}
return LocalAxisIndex * 2 + (bPositiveLocalSide ? 1 : 0);
}
return INDEX_NONE;
}
FString BuildNotation(const FHyperTwistMagicCube5DTurnRequest& Request)
{
return FString::Printf(
TEXT("%s%s{%02X}:%s%s:%s"),
Request.bPositiveFace ? TEXT("+") : TEXT("-"),
*GetAxisLabel(Request.FaceAxis),
Request.SliceMask,
*GetAxisLabel(Request.RotationAxisA),
*GetAxisLabel(Request.RotationAxisB),
Request.Direction ==
EHyperTwistMagicCube5DTurnDirection::PositiveQuarterTurn
? TEXT("+90")
: TEXT("-90"));
}
}
bool FHyperTwistMagicCube5DPieceOrientation::IsStructurallyValid() const
{
using namespace HyperTwistMagicCube5DRuntimeLibraryInternal;
if (Basis.Num() != AxisCount)
{
return false;
}
bool bSeenAxes[AxisCount] = {false, false, false, false, false};
int32 NegativeDirectionCount = 0;
int32 PermutationInversionCount = 0;
for (const FHyperTwistMagicCube5DSignedAxis& SignedAxis : Basis)
{
const int32 AxisIndex = ToAxisIndex(SignedAxis.Axis);
if (AxisIndex < 0 || AxisIndex >= AxisCount || bSeenAxes[AxisIndex])
{
return false;
}
bSeenAxes[AxisIndex] = true;
NegativeDirectionCount += SignedAxis.bPositiveDirection ? 0 : 1;
}
for (int32 LeftIndex = 0; LeftIndex < Basis.Num(); ++LeftIndex)
{
for (int32 RightIndex = LeftIndex + 1;
RightIndex < Basis.Num();
++RightIndex)
{
if (ToAxisIndex(Basis[LeftIndex].Axis)
> ToAxisIndex(Basis[RightIndex].Axis))
{
++PermutationInversionCount;
}
}
}
// A legal 5D quarter turn is a proper signed permutation, never a reflection.
return (NegativeDirectionCount + PermutationInversionCount) % 2 == 0;
}
bool FHyperTwistMagicCube5DGridCoordinate::IsValidForOrder(const int32 Order) const
{
using namespace HyperTwistMagicCube5DRuntimeLibraryInternal;
if (Components.Num() != AxisCount)
{
return false;
}
for (const int32 Component : Components)
{
if (!IsCoordinateValueValid(Component, Order))
{
return false;
}
}
return true;
}
int32 FHyperTwistMagicCube5DRuntimeState::GetPieceCount() const
{
return UHyperTwistMagicCube5DRuntimeLibrary::IsSupportedOrder(Order)
? HyperTwistMagicCube5DRuntimeLibraryInternal::GetPieceCount(Order)
: 0;
}
bool FHyperTwistMagicCube5DRuntimeState::IsStructurallyValid() const
{
using namespace HyperTwistMagicCube5DRuntimeLibraryInternal;
return IsRuntimeStateShapeValid(*this)
&& bIsSolved == IsSolvedRuntimeState(*this);
}
bool FHyperTwistMagicCube5DTurnRequest::IsValidForOrder(const int32 Order) const
{
using namespace HyperTwistMagicCube5DRuntimeLibraryInternal;
const int32 FaceAxisIndex = ToAxisIndex(FaceAxis);
const int32 RotationAxisAIndex = ToAxisIndex(RotationAxisA);
const int32 RotationAxisBIndex = ToAxisIndex(RotationAxisB);
return Order >= MinimumOrder
&& Order <= MaximumOrder
&& SliceMask > 0
&& SliceMask < (1 << Order)
&& FaceAxisIndex >= 0
&& FaceAxisIndex < AxisCount
&& RotationAxisAIndex >= 0
&& RotationAxisAIndex < AxisCount
&& RotationAxisBIndex >= 0
&& RotationAxisBIndex < AxisCount
&& RotationAxisA != RotationAxisB
&& RotationAxisA != FaceAxis
&& RotationAxisB != FaceAxis;
}
bool FHyperTwistMagicCube5DProjectedCubie::IsStructurallyValid(
const int32 Order
) const
{
const int32 PieceCount =
HyperTwistMagicCube5DRuntimeLibraryInternal::GetPieceCount(Order);
return PositionIndex >= 0
&& PositionIndex < PieceCount
&& PieceId >= 0
&& PieceId < PieceCount
&& Position.IsValidForOrder(Order)
&& RepresentativeColorIndex >= 0
&& RepresentativeColorIndex <
HyperTwistMagicCube5DRuntimeLibraryInternal::ColorCount
&& bPieceInSolvedPosition == (PositionIndex == PieceId);
}
bool FHyperTwistMagicCube5DProjectedFacelet::IsStructurallyValid(
const int32 PieceCount
) const
{
const int32 AxisIndex =
HyperTwistMagicCube5DRuntimeLibraryInternal::ToAxisIndex(WorldAxis);
return PositionIndex >= 0
&& PositionIndex < PieceCount
&& PieceId >= 0
&& PieceId < PieceCount
&& AxisIndex >= 0
&& AxisIndex <
HyperTwistMagicCube5DRuntimeLibraryInternal::AxisCount
&& ColorIndex >= 0
&& ColorIndex <
HyperTwistMagicCube5DRuntimeLibraryInternal::ColorCount;
}
bool FHyperTwistMagicCube5DProjection::IsStructurallyValid() const
{
using namespace HyperTwistMagicCube5DRuntimeLibraryInternal;
if (ProjectionProfile != TEXT("magiccube5d-boundary-facelet-projection-v1")
|| !UHyperTwistMagicCube5DRuntimeLibrary::IsSupportedOrder(Order))
{
return false;
}
const int32 PieceCount = GetPieceCount(Order);
const int32 ExpectedCubieCount = GetBoundaryCubieCount(Order);
if (Cubies.Num() != ExpectedCubieCount
|| Facelets.Num() != GetFaceletCount(Order))
{
return false;
}
TBitArray<> SeenCubiePositions(false, PieceCount);
for (const FHyperTwistMagicCube5DProjectedCubie& Cubie : Cubies)
{
if (!Cubie.IsStructurallyValid(Order)
|| SeenCubiePositions[Cubie.PositionIndex])
{
return false;
}
SeenCubiePositions[Cubie.PositionIndex] = true;
}
TSet<int64> SeenFacelets;
SeenFacelets.Reserve(Facelets.Num());
for (const FHyperTwistMagicCube5DProjectedFacelet& Facelet : Facelets)
{
if (!Facelet.IsStructurallyValid(PieceCount))
{
return false;
}
const int64 Key =
(static_cast<int64>(Facelet.PositionIndex) * ColorCount)
+ (ToAxisIndex(Facelet.WorldAxis) * 2)
+ (Facelet.bPositiveWorldSide ? 1 : 0);
if (SeenFacelets.Contains(Key))
{
return false;
}
SeenFacelets.Add(Key);
}
return true;
}
bool UHyperTwistMagicCube5DRuntimeLibrary::IsSupportedOrder(const int32 Order)
{
return Order >= HyperTwistMagicCube5DRuntimeLibraryInternal::MinimumOrder
&& Order <= HyperTwistMagicCube5DRuntimeLibraryInternal::MaximumOrder;
}
FHyperTwistPuzzleDefinitionRef
UHyperTwistMagicCube5DRuntimeLibrary::MakePuzzleDefinition(const int32 Order)
{
const int32 SafeOrder = IsSupportedOrder(Order) ? Order : 3;
return HyperTwistMagicCube5DRuntimeLibraryInternal::BuildDefinition(SafeOrder);
}
FHyperTwistMagicCube5DRuntimeState
UHyperTwistMagicCube5DRuntimeLibrary::BuildSolvedState(const int32 Order)
{
using namespace HyperTwistMagicCube5DRuntimeLibraryInternal;
const int32 SafeOrder = IsSupportedOrder(Order) ? Order : 3;
const int32 PieceCount = GetPieceCount(SafeOrder);
FHyperTwistMagicCube5DRuntimeState State;
State.StateProfile = FString::Printf(
TEXT("magiccube5d-order-%d-piece-orientation-v1"),
SafeOrder);
State.Definition = MakePuzzleDefinition(SafeOrder);
State.Order = SafeOrder;
State.PositionToPiece.Reserve(PieceCount);
State.PieceOrientations.Reserve(PieceCount);
for (int32 PieceId = 0; PieceId < PieceCount; ++PieceId)
{
State.PositionToPiece.Add(PieceId);
State.PieceOrientations.Add(BuildIdentityOrientation());
}
State.AppliedMoveCount = 0;
State.bIsSolved = true;
return State;
}
FHyperTwistPuzzleState
UHyperTwistMagicCube5DRuntimeLibrary::BuildPuzzleStateEnvelope(
const FHyperTwistMagicCube5DRuntimeState& RuntimeState
)
{
const FHyperTwistMagicCube5DRuntimeState SafeState =
RuntimeState.IsStructurallyValid()
? RuntimeState
: BuildSolvedState(3);
FHyperTwistPuzzleState PuzzleState;
PuzzleState.Definition = SafeState.Definition;
PuzzleState.StateEncodingKind = EHyperTwistStateEncodingKind::FamilySpecific;
PuzzleState.StateEncoding.EncodingProfile = SafeState.StateProfile;
PuzzleState.StateEncoding.PayloadJson =
SerializeRuntimeStateToJson(SafeState);
PuzzleState.OrientationFrame.Reference =
TEXT("magiccube5d-canonical-signed-axis-frame-v1");
PuzzleState.bIsSolved = SafeState.bIsSolved;
PuzzleState.Source = EHyperTwistStateSource::Runtime;
return PuzzleState;
}
FHyperTwistMagicCube5DTurnResult
UHyperTwistMagicCube5DRuntimeLibrary::ApplyTurn(
const FHyperTwistMagicCube5DRuntimeState& State,
const FHyperTwistMagicCube5DTurnRequest& Request
)
{
using namespace HyperTwistMagicCube5DRuntimeLibraryInternal;
FHyperTwistMagicCube5DTurnResult Result;
Result.State = State;
if (!State.IsStructurallyValid())
{
Result.Warnings.Add(TEXT("invalid-magiccube5d-runtime-state"));
return Result;
}
if (!Request.IsValidForOrder(State.Order))
{
Result.Warnings.Add(TEXT("invalid-magiccube5d-turn-request"));
return Result;
}
const TSet<int32> SelectedCoordinates =
GetSelectedSliceCoordinates(Request, State.Order);
if (SelectedCoordinates.IsEmpty())
{
Result.Warnings.Add(TEXT("magiccube5d-turn-selects-no-layers"));
return Result;
}
Result.State.PositionToPiece = State.PositionToPiece;
Result.State.PieceOrientations = State.PieceOrientations;
for (int32 PositionIndex = 0;
PositionIndex < State.PositionToPiece.Num();
++PositionIndex)
{
const FHyperTwistMagicCube5DGridCoordinate Coordinate =
DecodeCoordinate(PositionIndex, State.Order);
if (!SelectedCoordinates.Contains(
GetCoordinateValue(Coordinate, Request.FaceAxis)))
{
continue;
}
const int32 PieceId = State.PositionToPiece[PositionIndex];
const FHyperTwistMagicCube5DGridCoordinate RotatedCoordinate =
RotateCoordinateInPlane(
Coordinate,
Request.RotationAxisA,
Request.RotationAxisB,
Request.Direction);
const int32 NewPositionIndex =
EncodeCoordinate(RotatedCoordinate, State.Order);
Result.State.PositionToPiece[NewPositionIndex] = PieceId;
Result.State.PieceOrientations[PieceId] = RotateOrientationInPlane(
State.PieceOrientations[PieceId],
Request.RotationAxisA,
Request.RotationAxisB,
Request.Direction);
}
Result.State.AppliedMoveCount = State.AppliedMoveCount + 1;
Result.State.bIsSolved = IsSolvedRuntimeState(Result.State);
Result.AppliedNotation = BuildNotation(Request);
Result.bApplied = true;
Result.bExactStateUpdate = Result.State.IsStructurallyValid();
if (!Result.bExactStateUpdate)
{
Result.Warnings.Add(TEXT("magiccube5d-state-became-invalid"));
}
return Result;
}
FHyperTwistMagicCube5DScrambleResult
UHyperTwistMagicCube5DRuntimeLibrary::GenerateScramble(
const int32 Order,
const int32 MoveCount,
const int32 RandomSeed
)
{
using namespace HyperTwistMagicCube5DRuntimeLibraryInternal;
FHyperTwistMagicCube5DScrambleResult Result;
if (!IsSupportedOrder(Order) || MoveCount < 1 || MoveCount > 500)
{
Result.Warnings.Add(TEXT("invalid-magiccube5d-scramble-request"));
return Result;
}
FRandomStream Random(RandomSeed);
Result.State = BuildSolvedState(Order);
Result.Moves.Reserve(MoveCount);
Result.AppliedNotation.Reserve(MoveCount);
FHyperTwistMagicCube5DTurnRequest PreviousRequest;
bool bHasPreviousRequest = false;
for (int32 MoveIndex = 0; MoveIndex < MoveCount; ++MoveIndex)
{
FHyperTwistMagicCube5DTurnRequest Request;
bool bDistinct = false;
for (int32 Attempt = 0; Attempt < 32 && !bDistinct; ++Attempt)
{
Request.FaceAxis =
ToAxisEnum(Random.RandRange(0, AxisCount - 1));
Request.bPositiveFace = Random.RandRange(0, 1) == 1;
Request.SliceMask = 1 << Random.RandRange(0, Order - 1);
TArray<EHyperTwistMagicCube5DAxis> OrthogonalAxes;
OrthogonalAxes.Reserve(AxisCount - 1);
for (int32 AxisIndex = 0; AxisIndex < AxisCount; ++AxisIndex)
{
const EHyperTwistMagicCube5DAxis Axis = ToAxisEnum(AxisIndex);
if (Axis != Request.FaceAxis)
{
OrthogonalAxes.Add(Axis);
}
}
const int32 AxisAIndex =
Random.RandRange(0, OrthogonalAxes.Num() - 1);
int32 AxisBIndex =
Random.RandRange(0, OrthogonalAxes.Num() - 2);
if (AxisBIndex >= AxisAIndex)
{
++AxisBIndex;
}
Request.RotationAxisA = OrthogonalAxes[AxisAIndex];
Request.RotationAxisB = OrthogonalAxes[AxisBIndex];
Request.Direction = Random.RandRange(0, 1) == 0
? EHyperTwistMagicCube5DTurnDirection::PositiveQuarterTurn
: EHyperTwistMagicCube5DTurnDirection::NegativeQuarterTurn;
bDistinct = !bHasPreviousRequest
|| Request.FaceAxis != PreviousRequest.FaceAxis
|| Request.bPositiveFace != PreviousRequest.bPositiveFace
|| Request.SliceMask != PreviousRequest.SliceMask
|| Request.RotationAxisA != PreviousRequest.RotationAxisA
|| Request.RotationAxisB != PreviousRequest.RotationAxisB;
}
const FHyperTwistMagicCube5DTurnResult TurnResult =
ApplyTurn(Result.State, Request);
if (!TurnResult.bApplied || !TurnResult.bExactStateUpdate)
{
Result.Warnings = TurnResult.Warnings;
Result.Warnings.Add(TEXT("magiccube5d-scramble-turn-failed"));
Result.State = FHyperTwistMagicCube5DRuntimeState();
Result.Moves.Reset();
Result.AppliedNotation.Reset();
return Result;
}
Result.State = TurnResult.State;
Result.Moves.Add(Request);
Result.AppliedNotation.Add(TurnResult.AppliedNotation);
PreviousRequest = Request;
bHasPreviousRequest = true;
}
Result.bExactStateUpdate = Result.State.IsStructurallyValid();
Result.bGenerated = Result.bExactStateUpdate
&& Result.Moves.Num() == MoveCount
&& !Result.State.bIsSolved;
if (!Result.bGenerated)
{
Result.Warnings.Add(TEXT("magiccube5d-scramble-result-invalid"));
}
return Result;
}
FHyperTwistMagicCube5DProjectionBuildResult
UHyperTwistMagicCube5DRuntimeLibrary::BuildProjection(
const FHyperTwistMagicCube5DRuntimeState& State
)
{
using namespace HyperTwistMagicCube5DRuntimeLibraryInternal;
FHyperTwistMagicCube5DProjectionBuildResult Result;
Result.Projection.Order = State.Order;
if (!State.IsStructurallyValid())
{
Result.Warnings.Add(TEXT("invalid-magiccube5d-runtime-state"));
return Result;
}
const int32 PieceCount = State.GetPieceCount();
const int32 Boundary = GetBoundaryCoordinateMagnitude(State.Order);
Result.Projection.Cubies.Reserve(GetBoundaryCubieCount(State.Order));
Result.Projection.Facelets.Reserve(GetFaceletCount(State.Order));
for (int32 PositionIndex = 0; PositionIndex < PieceCount; ++PositionIndex)
{
const FHyperTwistMagicCube5DGridCoordinate Position =
DecodeCoordinate(PositionIndex, State.Order);
const int32 PieceId = State.PositionToPiece[PositionIndex];
const FHyperTwistMagicCube5DGridCoordinate PieceHomeCoordinate =
DecodeCoordinate(PieceId, State.Order);
const FHyperTwistMagicCube5DPieceOrientation& Orientation =
State.PieceOrientations[PieceId];
int32 RepresentativeColorIndex = INDEX_NONE;
for (int32 AxisIndex = 0; AxisIndex < AxisCount; ++AxisIndex)
{
const int32 Coordinate = Position.Components[AxisIndex];
if (FMath::Abs(Coordinate) != Boundary)
{
continue;
}
const bool bPositiveWorldSide = Coordinate > 0;
const int32 ColorIndex = ResolveColorForWorldSide(
PieceHomeCoordinate,
Orientation,
ToAxisEnum(AxisIndex),
bPositiveWorldSide,
State.Order);
if (ColorIndex == INDEX_NONE)
{
Result.Warnings.Add(FString::Printf(
TEXT("missing-facelet-at-position-%d-axis-%d"),
PositionIndex,
AxisIndex));
return Result;
}
FHyperTwistMagicCube5DProjectedFacelet Facelet;
Facelet.PositionIndex = PositionIndex;
Facelet.PieceId = PieceId;
Facelet.WorldAxis = ToAxisEnum(AxisIndex);
Facelet.bPositiveWorldSide = bPositiveWorldSide;
Facelet.ColorIndex = ColorIndex;
Result.Projection.Facelets.Add(Facelet);
if (RepresentativeColorIndex == INDEX_NONE)
{
RepresentativeColorIndex = ColorIndex;
}
}
if (RepresentativeColorIndex == INDEX_NONE)
{
continue;
}
FHyperTwistMagicCube5DProjectedCubie Cubie;
Cubie.PositionIndex = PositionIndex;
Cubie.PieceId = PieceId;
Cubie.Position = Position;
Cubie.RepresentativeColorIndex = RepresentativeColorIndex;
Cubie.bPieceInSolvedPosition = PositionIndex == PieceId;
Result.Projection.Cubies.Add(Cubie);
}
Result.bProjected = Result.Projection.IsStructurallyValid();
Result.bExactProjection = Result.bProjected;
if (!Result.bProjected)
{
Result.Warnings.Add(TEXT("magiccube5d-projection-invalid"));
}
return Result;
}
FString UHyperTwistMagicCube5DRuntimeLibrary::SerializeRuntimeStateToJson(
const FHyperTwistMagicCube5DRuntimeState& RuntimeState
)
{
FString Json;
FJsonObjectConverter::UStructToJsonObjectString(
FHyperTwistMagicCube5DRuntimeState::StaticStruct(),
&RuntimeState,
Json,
0,
0);
return Json;
}
bool UHyperTwistMagicCube5DRuntimeLibrary::TryDeserializeRuntimeStateFromJson(
const FString& Json,
FHyperTwistMagicCube5DRuntimeState& OutRuntimeState
)
{
FHyperTwistMagicCube5DRuntimeState ParsedState;
if (Json.IsEmpty()
|| !FJsonObjectConverter::JsonObjectStringToUStruct(
Json,
&ParsedState,
0,
0)
|| !ParsedState.IsStructurallyValid())
{
return false;
}
OutRuntimeState = MoveTemp(ParsedState);
return true;
}
FString UHyperTwistMagicCube5DRuntimeLibrary::GetAxisLabel(
const EHyperTwistMagicCube5DAxis Axis
)
{
return HyperTwistMagicCube5DRuntimeLibraryInternal::GetAxisLabel(Axis);
}

View file

@ -4,8 +4,12 @@
#include "Engine/World.h" #include "Engine/World.h"
#include "HyperTwistBootstrap/HyperTwistContractLibrary.h" #include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
#include "HyperTwistCore/HyperTwistCoreLibrary.h" #include "HyperTwistCore/HyperTwistCoreLibrary.h"
#include "HyperTwistSimulation/HyperTwistFourDimensionalSaveGame.h"
#include "Kismet/GameplayStatics.h"
#include "Materials/MaterialInterface.h" #include "Materials/MaterialInterface.h"
#include "Misc/DateTime.h"
#include "ProceduralMeshComponent/Public/ProceduralMeshComponent.h" #include "ProceduralMeshComponent/Public/ProceduralMeshComponent.h"
#include "UObject/ConstructorHelpers.h"
namespace HyperTwistMelindaProjectionActorInternal namespace HyperTwistMelindaProjectionActorInternal
{ {
@ -25,6 +29,13 @@ AHyperTwistMelindaProjectionActor::AHyperTwistMelindaProjectionActor()
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot")); SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
RootComponent = SceneRoot; RootComponent = SceneRoot;
static ConstructorHelpers::FObjectFinder<UMaterialInterface> VertexColorMaterialFinder(
TEXT("/Game/HyperTwistTraining/Materials/M_HT_ProjectionVertexColor.M_HT_ProjectionVertexColor"));
if (VertexColorMaterialFinder.Succeeded())
{
VertexColorMaterial = VertexColorMaterialFinder.Object;
}
} }
void AHyperTwistMelindaProjectionActor::OnConstruction(const FTransform& Transform) void AHyperTwistMelindaProjectionActor::OnConstruction(const FTransform& Transform)
@ -213,6 +224,103 @@ bool AHyperTwistMelindaProjectionActor::RefreshProjection()
return ProjectionResult.bExactProjection; return ProjectionResult.bExactProjection;
} }
bool AHyperTwistMelindaProjectionActor::SaveRuntimeState()
{
const FHyperTwistMelindaCellFirstProjectionBuildResult ProjectionResult =
UHyperTwistMelindaProjectionLibrary::BuildCellFirstProjection(
CurrentState,
CellCenterSpacing);
if (!CurrentState.IsStructurallyValid()
|| !ProjectionResult.bProjected
|| !ProjectionResult.bExactProjection
|| !ProjectionResult.Projection.IsStructurallyValid())
{
LastPersistenceStatus = TEXT("save rejected: active 2x state is invalid");
return false;
}
UHyperTwistFourDimensionalSaveGame* SaveGame =
Cast<UHyperTwistFourDimensionalSaveGame>(
UGameplayStatics::CreateSaveGameObject(
UHyperTwistFourDimensionalSaveGame::StaticClass()));
if (SaveGame == nullptr)
{
LastPersistenceStatus = TEXT("save failed: save object unavailable");
return false;
}
SaveGame->PuzzleOrder = 2;
SaveGame->bUsesCellFirstState = true;
SaveGame->CellFirstState = CurrentState;
SaveGame->LastAppliedNotation = LastAppliedNotation;
SaveGame->SavedAtUtc = FDateTime::UtcNow().ToIso8601();
if (!UGameplayStatics::SaveGameToSlot(SaveGame, GetRuntimeSaveSlotName(), 0))
{
LastPersistenceStatus = TEXT("save failed: slot could not be written");
return false;
}
LastPersistenceStatus = TEXT("saved 2x2x2x2 session");
return true;
}
bool AHyperTwistMelindaProjectionActor::LoadRuntimeState()
{
const FString SlotName = GetRuntimeSaveSlotName();
if (!UGameplayStatics::DoesSaveGameExist(SlotName, 0))
{
LastPersistenceStatus = TEXT("no saved 2x2x2x2 session");
return false;
}
const UHyperTwistFourDimensionalSaveGame* SaveGame =
Cast<UHyperTwistFourDimensionalSaveGame>(
UGameplayStatics::LoadGameFromSlot(SlotName, 0));
if (SaveGame == nullptr
|| SaveGame->SchemaVersion != 1
|| SaveGame->PuzzleOrder != 2
|| !SaveGame->bUsesCellFirstState
|| !SaveGame->CellFirstState.IsStructurallyValid())
{
LastPersistenceStatus = TEXT("load rejected: invalid 2x2x2x2 save");
return false;
}
const FHyperTwistMelindaCellFirstProjectionBuildResult ProjectionResult =
UHyperTwistMelindaProjectionLibrary::BuildCellFirstProjection(
SaveGame->CellFirstState,
CellCenterSpacing);
if (!ProjectionResult.bProjected
|| !ProjectionResult.bExactProjection
|| !ProjectionResult.Projection.IsStructurallyValid())
{
LastPersistenceStatus = TEXT("load rejected: invalid 2x projection payload");
return false;
}
const FHyperTwistPuzzleState PreviousState = CurrentState;
const FString PreviousNotation = LastAppliedNotation;
CurrentState = SaveGame->CellFirstState;
LastAppliedNotation = SaveGame->LastAppliedNotation;
LastWarnings.Reset();
if (!RefreshProjection())
{
CurrentState = PreviousState;
LastAppliedNotation = PreviousNotation;
RefreshProjection();
LastPersistenceStatus = TEXT("load rejected: projection rebuild failed");
return false;
}
LastPersistenceStatus = TEXT("loaded 2x2x2x2 session");
return true;
}
FString AHyperTwistMelindaProjectionActor::GetRuntimeSaveSlotName()
{
return TEXT("HyperTwist_4D_2x2x2x2_v1");
}
void AHyperTwistMelindaProjectionActor::ClearRenderedCells() void AHyperTwistMelindaProjectionActor::ClearRenderedCells()
{ {
DisplayedCubieMetadata.Empty(); DisplayedCubieMetadata.Empty();

View file

@ -2,10 +2,96 @@
#include "Engine/World.h" #include "Engine/World.h"
#include "EngineUtils.h" #include "EngineUtils.h"
#include "HyperTwistCore/HyperTwistCoreLibrary.h"
#include "HyperTwistDiagnostics/HyperTwistRuntimeDiagnostics.h"
#include "HyperTwistSimulation/HyperTwistMelindaProjectionActor.h" #include "HyperTwistSimulation/HyperTwistMelindaProjectionActor.h"
#include "HyperTwistSimulation/HyperTwistMelindaProjectionOrbitPawn.h" #include "HyperTwistSimulation/HyperTwistMelindaProjectionOrbitPawn.h"
#include "HyperTwistSimulation/HyperTwistMelindaProjectionPlayerController.h" #include "HyperTwistSimulation/HyperTwistMelindaProjectionPlayerController.h"
namespace HyperTwistMelindaProjectionGameModeInternal
{
constexpr int32 PuzzleOrder = 2;
constexpr int32 ExpectedStatePieceCount = 16;
constexpr int32 ExpectedProjectedCellCount = 8;
constexpr int32 ExpectedRenderableCubieViewCount = 64;
int32 ResolveStatePieceCount(const FHyperTwistPuzzleState& State)
{
if (State.Definition.Dimension != 4 || State.Definition.SizeVector.Num() != 4)
{
return 0;
}
int32 PieceCount = 1;
for (const int32 AxisSize : State.Definition.SizeVector)
{
if (AxisSize != PuzzleOrder)
{
return 0;
}
PieceCount *= AxisSize;
}
return PieceCount;
}
void AppendPresentationReadiness(
const AHyperTwistMelindaProjectionActor* ProjectionActor,
const bool bStartupStatePrepared)
{
if (ProjectionActor == nullptr)
{
HyperTwistRuntimeDiagnostics::AppendEvent(
TEXT("FourDimensionalPresentation"),
TEXT("Four-dimensional presentation failed for order 2: projection actor unavailable."));
return;
}
const FHyperTwistPuzzleStateValidationResult StateValidation =
UHyperTwistCoreLibrary::ValidatePuzzleState(ProjectionActor->CurrentState);
const int32 StatePieceCount = ResolveStatePieceCount(ProjectionActor->CurrentState);
const int32 ProjectedCellCount = ProjectionActor->CurrentProjection.Cells.Num();
const int32 RenderableCubieViewCount =
ProjectionActor->GetRenderableCubieViewCount();
const bool bStateValid =
bStartupStatePrepared
&& StatePieceCount == ExpectedStatePieceCount
&& StateValidation.bStateSupported
&& StateValidation.bStructureValid
&& StateValidation.bIsSolvable;
const bool bProjectionValid =
ProjectedCellCount == ExpectedProjectedCellCount
&& RenderableCubieViewCount == ExpectedRenderableCubieViewCount
&& ProjectionActor->HasValidProjection();
if (bStateValid && bProjectionValid)
{
HyperTwistRuntimeDiagnostics::AppendEvent(
TEXT("FourDimensionalPresentation"),
FString::Printf(
TEXT(
"Four-dimensional presentation initialized for order %d with %d exact "
"state pieces and %d visible projected piece views; state valid, projection valid."),
PuzzleOrder,
StatePieceCount,
RenderableCubieViewCount));
return;
}
HyperTwistRuntimeDiagnostics::AppendEvent(
TEXT("FourDimensionalPresentation"),
FString::Printf(
TEXT(
"Four-dimensional presentation failed for order %d with %d state pieces, "
"%d projected cells, and %d visible projected piece views; state %s, projection %s."),
PuzzleOrder,
StatePieceCount,
ProjectedCellCount,
RenderableCubieViewCount,
bStateValid ? TEXT("valid") : TEXT("invalid"),
bProjectionValid ? TEXT("valid") : TEXT("invalid")));
}
}
AHyperTwistMelindaProjectionGameMode::AHyperTwistMelindaProjectionGameMode() AHyperTwistMelindaProjectionGameMode::AHyperTwistMelindaProjectionGameMode()
{ {
PlayerControllerClass = AHyperTwistMelindaProjectionPlayerController::StaticClass(); PlayerControllerClass = AHyperTwistMelindaProjectionPlayerController::StaticClass();
@ -16,6 +102,12 @@ void AHyperTwistMelindaProjectionGameMode::BeginPlay()
{ {
Super::BeginPlay(); Super::BeginPlay();
HyperTwistRuntimeDiagnostics::AppendEvent(
TEXT("MapReady"),
FString::Printf(
TEXT("Runtime map ready: %s."),
GetWorld() != nullptr ? *GetWorld()->GetPathName() : TEXT("unknown")));
if (APlayerController* PlayerController = if (APlayerController* PlayerController =
GetWorld() != nullptr ? GetWorld()->GetFirstPlayerController() : nullptr) GetWorld() != nullptr ? GetWorld()->GetFirstPlayerController() : nullptr)
{ {
@ -27,16 +119,30 @@ void AHyperTwistMelindaProjectionGameMode::BeginPlay()
AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveOrSpawnProjectionActor(); AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveOrSpawnProjectionActor();
if (ProjectionActor == nullptr) if (ProjectionActor == nullptr)
{ {
HyperTwistMelindaProjectionGameModeInternal::AppendPresentationReadiness(
nullptr,
false);
return; return;
} }
bool bStartupStatePrepared = true;
if (bRandomizeOnBeginPlay) if (bRandomizeOnBeginPlay)
{ {
ProjectionActor->GenerateRandomState(StartupRandomSeed); bStartupStatePrepared =
return; ProjectionActor->GenerateRandomState(StartupRandomSeed);
}
else
{
ProjectionActor->ResetToSolvedState();
if (bLoadSavedStateOnBeginPlay)
{
ProjectionActor->LoadRuntimeState();
}
} }
ProjectionActor->ResetToSolvedState(); HyperTwistMelindaProjectionGameModeInternal::AppendPresentationReadiness(
ProjectionActor,
bStartupStatePrepared);
} }
void AHyperTwistMelindaProjectionGameMode::ResetProjectionToSolved() void AHyperTwistMelindaProjectionGameMode::ResetProjectionToSolved()

View file

@ -6,6 +6,7 @@
#include "GameFramework/PlayerController.h" #include "GameFramework/PlayerController.h"
#include "GameFramework/SpringArmComponent.h" #include "GameFramework/SpringArmComponent.h"
#include "HyperTwistSimulation/HyperTwistMelindaProjectionActor.h" #include "HyperTwistSimulation/HyperTwistMelindaProjectionActor.h"
#include "HyperTwistUX/HyperTwistPlayerControllerBase.h"
#include "InputCoreTypes.h" #include "InputCoreTypes.h"
AHyperTwistMelindaProjectionOrbitPawn::AHyperTwistMelindaProjectionOrbitPawn() AHyperTwistMelindaProjectionOrbitPawn::AHyperTwistMelindaProjectionOrbitPawn()
@ -62,6 +63,20 @@ void AHyperTwistMelindaProjectionOrbitPawn::Tick(const float DeltaSeconds)
{ {
return; return;
} }
const AHyperTwistPlayerControllerBase* HyperTwistController =
Cast<AHyperTwistPlayerControllerBase>(PlayerController);
const FHyperTwistPlayerPreferences* Preferences = HyperTwistController != nullptr
? &HyperTwistController->GetPlayerPreferences()
: nullptr;
const float ZoomMultiplier = Preferences != nullptr
? Preferences->ZoomSensitivity
: 1.0f;
const float OrbitMultiplier = Preferences != nullptr
? Preferences->OrbitSensitivity
: 1.0f;
const float VerticalDirection = Preferences != nullptr && Preferences->bInvertOrbitY
? -1.0f
: 1.0f;
if (SpringArm != nullptr) if (SpringArm != nullptr)
{ {
@ -69,7 +84,7 @@ void AHyperTwistMelindaProjectionOrbitPawn::Tick(const float DeltaSeconds)
if (!FMath::IsNearlyZero(MouseWheelDelta)) if (!FMath::IsNearlyZero(MouseWheelDelta))
{ {
SpringArm->TargetArmLength = FMath::Clamp( SpringArm->TargetArmLength = FMath::Clamp(
SpringArm->TargetArmLength - (MouseWheelDelta * ZoomStep), SpringArm->TargetArmLength - (MouseWheelDelta * ZoomStep * ZoomMultiplier),
MinimumArmLength, MinimumArmLength,
MaximumArmLength MaximumArmLength
); );
@ -86,9 +101,10 @@ void AHyperTwistMelindaProjectionOrbitPawn::Tick(const float DeltaSeconds)
PlayerController->GetInputMouseDelta(MouseDeltaX, MouseDeltaY); PlayerController->GetInputMouseDelta(MouseDeltaX, MouseDeltaY);
if (!FMath::IsNearlyZero(MouseDeltaX) || !FMath::IsNearlyZero(MouseDeltaY)) if (!FMath::IsNearlyZero(MouseDeltaX) || !FMath::IsNearlyZero(MouseDeltaY))
{ {
CurrentYawDegrees += MouseDeltaX * OrbitYawDegreesPerPixel; CurrentYawDegrees += MouseDeltaX * OrbitYawDegreesPerPixel * OrbitMultiplier;
CurrentPitchDegrees = FMath::Clamp( CurrentPitchDegrees = FMath::Clamp(
CurrentPitchDegrees - (MouseDeltaY * OrbitPitchDegreesPerPixel), CurrentPitchDegrees
- (MouseDeltaY * OrbitPitchDegreesPerPixel * OrbitMultiplier * VerticalDirection),
MinimumPitchDegrees, MinimumPitchDegrees,
MaximumPitchDegrees MaximumPitchDegrees
); );

View file

@ -2,6 +2,7 @@
#include "EngineUtils.h" #include "EngineUtils.h"
#include "HyperTwistSimulation/HyperTwistMelindaProjectionActor.h" #include "HyperTwistSimulation/HyperTwistMelindaProjectionActor.h"
#include "HyperTwistUX/HyperTwistFourDimensionalHUDWidget.h"
#include "InputCoreTypes.h" #include "InputCoreTypes.h"
AHyperTwistMelindaProjectionPlayerController::AHyperTwistMelindaProjectionPlayerController() AHyperTwistMelindaProjectionPlayerController::AHyperTwistMelindaProjectionPlayerController()
@ -15,6 +16,104 @@ void AHyperTwistMelindaProjectionPlayerController::BeginPlay()
{ {
Super::BeginPlay(); Super::BeginPlay();
ApplyInputMode(); ApplyInputMode();
ShowPuzzleHud();
}
FString AHyperTwistMelindaProjectionPlayerController::GetPauseMenuTitle() const
{
return TEXT("4D Cube 2x2x2x2");
}
FString AHyperTwistMelindaProjectionPlayerController::GetPauseMenuSubtitle() const
{
if (const AHyperTwistMelindaProjectionActor* ProjectionActor =
ResolveProjectionActor())
{
return ProjectionActor->CurrentState.bIsSolved
? TEXT("Solved cell-first projection")
: FString::Printf(
TEXT("Active exact state | last turn: %s"),
*ProjectionActor->LastAppliedNotation);
}
return TEXT("Cell-first projection with exact four-dimensional turns");
}
bool AHyperTwistMelindaProjectionPlayerController::ShowPuzzleHud()
{
if (!bShowPuzzleHud)
{
return false;
}
if (ActivePuzzleHudWidget == nullptr)
{
TSubclassOf<UHyperTwistFourDimensionalHUDWidget> ResolvedClass =
PuzzleHudWidgetClass;
if (*ResolvedClass == nullptr)
{
ResolvedClass = UHyperTwistFourDimensionalHUDWidget::StaticClass();
}
ActivePuzzleHudWidget =
CreateWidget<UHyperTwistFourDimensionalHUDWidget>(
this,
ResolvedClass);
if (ActivePuzzleHudWidget == nullptr
|| !ActivePuzzleHudWidget->PrepareHudSurface())
{
ActivePuzzleHudWidget = nullptr;
return false;
}
ActivePuzzleHudWidget->OnScrambleRequested.AddDynamic(
this,
&AHyperTwistMelindaProjectionPlayerController::HandleHudScramble);
ActivePuzzleHudWidget->OnResetRequested.AddDynamic(
this,
&AHyperTwistMelindaProjectionPlayerController::HandleHudReset);
ActivePuzzleHudWidget->OnSaveRequested.AddDynamic(
this,
&AHyperTwistMelindaProjectionPlayerController::HandleHudSave);
ActivePuzzleHudWidget->OnLoadRequested.AddDynamic(
this,
&AHyperTwistMelindaProjectionPlayerController::HandleHudLoad);
ActivePuzzleHudWidget->AddToViewport(PuzzleHudZOrder);
}
else if (!ActivePuzzleHudWidget->IsInViewport())
{
ActivePuzzleHudWidget->AddToViewport(PuzzleHudZOrder);
}
RefreshPuzzleHud();
return true;
}
void AHyperTwistMelindaProjectionPlayerController::RefreshPuzzleHud()
{
if (ActivePuzzleHudWidget == nullptr)
{
return;
}
const AHyperTwistMelindaProjectionActor* ProjectionActor =
ResolveProjectionActor();
if (ProjectionActor == nullptr)
{
ActivePuzzleHudWidget->ConfigureSurface(
TEXT("4D Cube 2x2x2x2"),
TEXT("WAITING FOR RUNTIME"),
TEXT("Cell-first projection actor is not ready."),
TEXT("waiting"),
TEXT("not loaded"),
true);
return;
}
ActivePuzzleHudWidget->ConfigureSurface(
TEXT("4D Cube 2x2x2x2"),
FString::Printf(
TEXT("%s | 16 exact pieces | 8 projected cells"),
ProjectionActor->CurrentState.bIsSolved
? TEXT("SOLVED")
: TEXT("ACTIVE")),
TEXT("Direct cell projection | click any visible cell to turn"),
ProjectionActor->LastAppliedNotation,
ProjectionActor->LastPersistenceStatus,
true);
} }
void AHyperTwistMelindaProjectionPlayerController::SetupInputComponent() void AHyperTwistMelindaProjectionPlayerController::SetupInputComponent()
@ -42,7 +141,7 @@ void AHyperTwistMelindaProjectionPlayerController::SetupInputComponent()
); );
} }
if (bEnableTouchTurnInput) if (bEnableTouchTurnInput && PlayerPreferences.bTouchInputEnabled)
{ {
InputComponent->BindTouch( InputComponent->BindTouch(
IE_Pressed, IE_Pressed,
@ -70,6 +169,17 @@ void AHyperTwistMelindaProjectionPlayerController::SetupInputComponent()
&AHyperTwistMelindaProjectionPlayerController::HandleRandomizeShortcut &AHyperTwistMelindaProjectionPlayerController::HandleRandomizeShortcut
); );
} }
InputComponent->BindKey(
EKeys::S,
IE_Pressed,
this,
&AHyperTwistMelindaProjectionPlayerController::HandleSaveShortcut);
InputComponent->BindKey(
EKeys::L,
IE_Pressed,
this,
&AHyperTwistMelindaProjectionPlayerController::HandleLoadShortcut);
} }
bool AHyperTwistMelindaProjectionPlayerController::TryProcessProjectionClickFromCursor( bool AHyperTwistMelindaProjectionPlayerController::TryProcessProjectionClickFromCursor(
@ -103,7 +213,12 @@ bool AHyperTwistMelindaProjectionPlayerController::TryProcessProjectionClickFrom
if (AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveProjectionActor()) if (AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveProjectionActor())
{ {
return ProjectionActor->ProcessClick(RayOrigin, RayDirection, bCounterClockwise); const bool bApplied = ProjectionActor->ProcessClick(
RayOrigin,
RayDirection,
bCounterClockwise);
RefreshPuzzleHud();
return bApplied;
} }
return false; return false;
@ -115,13 +230,16 @@ void AHyperTwistMelindaProjectionPlayerController::ResetProjectionToSolved()
{ {
ProjectionActor->ResetToSolvedState(); ProjectionActor->ResetToSolvedState();
} }
RefreshPuzzleHud();
} }
bool AHyperTwistMelindaProjectionPlayerController::GenerateProjectionRandomState() bool AHyperTwistMelindaProjectionPlayerController::GenerateProjectionRandomState()
{ {
if (AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveProjectionActor()) if (AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveProjectionActor())
{ {
return ProjectionActor->GenerateRandomState(NextRandomSeed++); const bool bGenerated = ProjectionActor->GenerateRandomState(NextRandomSeed++);
RefreshPuzzleHud();
return bGenerated;
} }
return false; return false;
@ -129,16 +247,9 @@ bool AHyperTwistMelindaProjectionPlayerController::GenerateProjectionRandomState
void AHyperTwistMelindaProjectionPlayerController::ApplyInputMode() void AHyperTwistMelindaProjectionPlayerController::ApplyInputMode()
{ {
bShowMouseCursor = true;
bEnableClickEvents = true;
bEnableMouseOverEvents = true;
if (bUseGameAndUiInputMode) if (bUseGameAndUiInputMode)
{ {
FInputModeGameAndUI InputMode; ApplyGameAndUiInputMode();
InputMode.SetHideCursorDuringCapture(false);
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
SetInputMode(InputMode);
} }
} }
@ -182,6 +293,44 @@ void AHyperTwistMelindaProjectionPlayerController::HandleRandomizeShortcut()
GenerateProjectionRandomState(); GenerateProjectionRandomState();
} }
void AHyperTwistMelindaProjectionPlayerController::HandleSaveShortcut()
{
if (AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveProjectionActor())
{
ProjectionActor->SaveRuntimeState();
}
RefreshPuzzleHud();
}
void AHyperTwistMelindaProjectionPlayerController::HandleLoadShortcut()
{
if (AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveProjectionActor())
{
ProjectionActor->LoadRuntimeState();
}
RefreshPuzzleHud();
}
void AHyperTwistMelindaProjectionPlayerController::HandleHudScramble()
{
GenerateProjectionRandomState();
}
void AHyperTwistMelindaProjectionPlayerController::HandleHudReset()
{
ResetProjectionToSolved();
}
void AHyperTwistMelindaProjectionPlayerController::HandleHudSave()
{
HandleSaveShortcut();
}
void AHyperTwistMelindaProjectionPlayerController::HandleHudLoad()
{
HandleLoadShortcut();
}
void AHyperTwistMelindaProjectionPlayerController::HandleTouchPressed( void AHyperTwistMelindaProjectionPlayerController::HandleTouchPressed(
const ETouchIndex::Type FingerIndex, const ETouchIndex::Type FingerIndex,
const FVector Location const FVector Location

View file

@ -1,8 +1,12 @@
#include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionActor.h" #include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionActor.h"
#include "Components/SceneComponent.h" #include "Components/SceneComponent.h"
#include "HyperTwistSimulation/HyperTwistFourDimensionalSaveGame.h"
#include "Kismet/GameplayStatics.h"
#include "Materials/MaterialInterface.h" #include "Materials/MaterialInterface.h"
#include "Misc/DateTime.h"
#include "ProceduralMeshComponent/Public/ProceduralMeshComponent.h" #include "ProceduralMeshComponent/Public/ProceduralMeshComponent.h"
#include "UObject/ConstructorHelpers.h"
AHyperTwistVirtual3333ProjectionActor::AHyperTwistVirtual3333ProjectionActor() AHyperTwistVirtual3333ProjectionActor::AHyperTwistVirtual3333ProjectionActor()
{ {
@ -10,6 +14,13 @@ AHyperTwistVirtual3333ProjectionActor::AHyperTwistVirtual3333ProjectionActor()
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot")); SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
RootComponent = SceneRoot; RootComponent = SceneRoot;
static ConstructorHelpers::FObjectFinder<UMaterialInterface> VertexColorMaterialFinder(
TEXT("/Game/HyperTwistTraining/Materials/M_HT_ProjectionVertexColor.M_HT_ProjectionVertexColor"));
if (VertexColorMaterialFinder.Succeeded())
{
VertexColorMaterial = VertexColorMaterialFinder.Object;
}
} }
void AHyperTwistVirtual3333ProjectionActor::OnConstruction(const FTransform& Transform) void AHyperTwistVirtual3333ProjectionActor::OnConstruction(const FTransform& Transform)
@ -44,13 +55,68 @@ void AHyperTwistVirtual3333ProjectionActor::BeginPlay()
void AHyperTwistVirtual3333ProjectionActor::ResetToSolvedState() void AHyperTwistVirtual3333ProjectionActor::ResetToSolvedState()
{ {
CurrentState = UHyperTwistVirtual3333ProjectionLibrary::BuildSolvedState(); PuzzleOrder = UHyperTwistVirtual3333ProjectionLibrary::IsSupportedOrder(PuzzleOrder)
? PuzzleOrder
: 3;
CurrentState = UHyperTwistVirtual3333ProjectionLibrary::BuildSolvedState(PuzzleOrder);
CurrentSliceCoordinate =
UHyperTwistVirtual3333ProjectionLibrary::GetSliceCoordinateForLayer(
PuzzleOrder,
PuzzleOrder);
LastAppliedNotation = TEXT("solved"); LastAppliedNotation = TEXT("solved");
LastScramble.Reset();
LastWarnings.Reset(); LastWarnings.Reset();
EnsureSelectionIsValid(); EnsureSelectionIsValid();
RefreshProjection(); RefreshProjection();
} }
bool AHyperTwistVirtual3333ProjectionActor::GenerateScramble(
const int32 MoveCount,
const int32 RandomSeed
)
{
const FHyperTwistVirtual3333ScrambleResult ScrambleResult =
UHyperTwistVirtual3333ProjectionLibrary::GenerateScramble(
PuzzleOrder,
MoveCount,
RandomSeed);
LastWarnings = ScrambleResult.Warnings;
if (!ScrambleResult.bGenerated || !ScrambleResult.bExactStateUpdate)
{
return false;
}
CurrentState = ScrambleResult.State;
CurrentSliceAxis = ScrambleResult.FinalSelection.SliceAxis;
CurrentSliceCoordinate = ScrambleResult.FinalSelection.SliceCoordinate;
CurrentRotationAxis = ScrambleResult.FinalSelection.RotationAxis;
LastScramble = ScrambleResult.AppliedMoves;
LastAppliedNotation = FString::Printf(
TEXT("scrambled %d moves (seed %d)"),
MoveCount,
RandomSeed);
return RefreshProjection();
}
bool AHyperTwistVirtual3333ProjectionActor::SetPuzzleOrder(const int32 NewOrder)
{
if (!UHyperTwistVirtual3333ProjectionLibrary::IsSupportedOrder(NewOrder))
{
LastWarnings = {TEXT("virtual-nxnxnxn-order-unsupported")};
return false;
}
if (PuzzleOrder == NewOrder
&& CurrentState.IsStructurallyValid()
&& CurrentState.GetOrder() == NewOrder)
{
return true;
}
PuzzleOrder = NewOrder;
ResetToSolvedState();
return CurrentState.IsStructurallyValid() && CurrentProjection.IsStructurallyValid();
}
bool AHyperTwistVirtual3333ProjectionActor::ApplyTurnRequest( bool AHyperTwistVirtual3333ProjectionActor::ApplyTurnRequest(
const FHyperTwistVirtual3333SliceTurnRequest& Request const FHyperTwistVirtual3333SliceTurnRequest& Request
) )
@ -60,9 +126,9 @@ bool AHyperTwistVirtual3333ProjectionActor::ApplyTurnRequest(
LastWarnings = TurnResult.Warnings; LastWarnings = TurnResult.Warnings;
if (!TurnResult.bApplied || !TurnResult.bExactStateUpdate) if (!TurnResult.bApplied || !TurnResult.bExactStateUpdate)
{ {
if (!LastWarnings.Contains(TEXT("virtual-3x3x3x3-turn-apply-failed"))) if (!LastWarnings.Contains(TEXT("virtual-nxnxnxn-turn-apply-failed")))
{ {
LastWarnings.Add(TEXT("virtual-3x3x3x3-turn-apply-failed")); LastWarnings.Add(TEXT("virtual-nxnxnxn-turn-apply-failed"));
} }
return false; return false;
} }
@ -107,12 +173,56 @@ bool AHyperTwistVirtual3333ProjectionActor::SetSliceAxis(
bool AHyperTwistVirtual3333ProjectionActor::SetSliceCoordinate(const int32 SliceCoordinate) bool AHyperTwistVirtual3333ProjectionActor::SetSliceCoordinate(const int32 SliceCoordinate)
{ {
CurrentSliceCoordinate = FMath::Clamp(SliceCoordinate, -1, 1); const TArray<int32> Coordinates =
UHyperTwistVirtual3333ProjectionLibrary::GetSliceCoordinatesForOrder(PuzzleOrder);
if (!Coordinates.Contains(SliceCoordinate))
{
LastWarnings = {TEXT("virtual-nxnxnxn-slice-coordinate-invalid")};
return false;
}
CurrentSliceCoordinate = SliceCoordinate;
EnsureSelectionIsValid(); EnsureSelectionIsValid();
LastWarnings.Reset(); LastWarnings.Reset();
return CurrentState.IsStructurallyValid() ? RefreshProjection() : true; return CurrentState.IsStructurallyValid() ? RefreshProjection() : true;
} }
bool AHyperTwistVirtual3333ProjectionActor::SetSliceLayer(const int32 OneBasedLayer)
{
if (OneBasedLayer < 1 || OneBasedLayer > PuzzleOrder)
{
LastWarnings = {TEXT("virtual-nxnxnxn-slice-layer-invalid")};
return false;
}
return SetSliceCoordinate(
UHyperTwistVirtual3333ProjectionLibrary::GetSliceCoordinateForLayer(
PuzzleOrder,
OneBasedLayer));
}
bool AHyperTwistVirtual3333ProjectionActor::CycleSliceCoordinate(
const int32 DirectionStep
)
{
const TArray<int32> Coordinates =
UHyperTwistVirtual3333ProjectionLibrary::GetSliceCoordinatesForOrder(PuzzleOrder);
if (Coordinates.IsEmpty())
{
LastWarnings = {TEXT("virtual-nxnxnxn-no-slice-layers")};
return false;
}
int32 CurrentIndex = Coordinates.IndexOfByKey(CurrentSliceCoordinate);
if (CurrentIndex == INDEX_NONE)
{
CurrentIndex = Coordinates.Num() - 1;
}
const int32 Step = DirectionStep >= 0
? DirectionStep % Coordinates.Num()
: -((-DirectionStep) % Coordinates.Num());
CurrentIndex = (CurrentIndex + Step + Coordinates.Num()) % Coordinates.Num();
return SetSliceCoordinate(Coordinates[CurrentIndex]);
}
bool AHyperTwistVirtual3333ProjectionActor::SetRotationAxis( bool AHyperTwistVirtual3333ProjectionActor::SetRotationAxis(
const EHyperTwistVirtual3333Axis RotationAxis const EHyperTwistVirtual3333Axis RotationAxis
) )
@ -178,9 +288,9 @@ bool AHyperTwistVirtual3333ProjectionActor::RefreshProjection()
{ {
CurrentProjection = FHyperTwistVirtual3333VisibleProjection(); CurrentProjection = FHyperTwistVirtual3333VisibleProjection();
ClearRenderedTesseracts(); ClearRenderedTesseracts();
if (!LastWarnings.Contains(TEXT("virtual-3x3x3x3-projection-failed"))) if (!LastWarnings.Contains(TEXT("virtual-nxnxnxn-projection-failed")))
{ {
LastWarnings.Add(TEXT("virtual-3x3x3x3-projection-failed")); LastWarnings.Add(TEXT("virtual-nxnxnxn-projection-failed"));
} }
return false; return false;
} }
@ -190,10 +300,155 @@ bool AHyperTwistVirtual3333ProjectionActor::RefreshProjection()
return ProjectionResult.bExactProjection; return ProjectionResult.bExactProjection;
} }
bool AHyperTwistVirtual3333ProjectionActor::SaveRuntimeState()
{
if (!CurrentState.IsStructurallyValid()
|| CurrentState.GetOrder() != PuzzleOrder
|| !CurrentProjection.IsStructurallyValid())
{
LastPersistenceStatus = TEXT("save rejected: active 4D state is invalid");
return false;
}
UHyperTwistFourDimensionalSaveGame* SaveGame =
Cast<UHyperTwistFourDimensionalSaveGame>(
UGameplayStatics::CreateSaveGameObject(
UHyperTwistFourDimensionalSaveGame::StaticClass()));
if (SaveGame == nullptr)
{
LastPersistenceStatus = TEXT("save failed: save object unavailable");
return false;
}
SaveGame->PuzzleOrder = PuzzleOrder;
SaveGame->bUsesCellFirstState = false;
SaveGame->VisibleSliceState = CurrentState;
SaveGame->SliceAxis = CurrentSliceAxis;
SaveGame->SliceCoordinate = CurrentSliceCoordinate;
SaveGame->RotationAxis = CurrentRotationAxis;
SaveGame->bRenderCellShells = bRenderCellShells;
SaveGame->LastAppliedNotation = LastAppliedNotation;
SaveGame->SavedAtUtc = FDateTime::UtcNow().ToIso8601();
if (!UGameplayStatics::SaveGameToSlot(
SaveGame,
GetRuntimeSaveSlotName(),
0))
{
LastPersistenceStatus = TEXT("save failed: slot could not be written");
return false;
}
LastPersistenceStatus = FString::Printf(
TEXT("saved %dx%dx%dx%d session"),
PuzzleOrder,
PuzzleOrder,
PuzzleOrder,
PuzzleOrder);
return true;
}
bool AHyperTwistVirtual3333ProjectionActor::LoadRuntimeState()
{
const FString SlotName = GetRuntimeSaveSlotName();
if (!UGameplayStatics::DoesSaveGameExist(SlotName, 0))
{
LastPersistenceStatus = FString::Printf(
TEXT("no saved %dx%dx%dx%d session"),
PuzzleOrder,
PuzzleOrder,
PuzzleOrder,
PuzzleOrder);
return false;
}
const UHyperTwistFourDimensionalSaveGame* SaveGame =
Cast<UHyperTwistFourDimensionalSaveGame>(
UGameplayStatics::LoadGameFromSlot(SlotName, 0));
const TArray<int32> ValidCoordinates =
UHyperTwistVirtual3333ProjectionLibrary::GetSliceCoordinatesForOrder(
PuzzleOrder);
if (SaveGame == nullptr
|| SaveGame->SchemaVersion != 1
|| SaveGame->PuzzleOrder != PuzzleOrder
|| SaveGame->bUsesCellFirstState
|| !SaveGame->VisibleSliceState.IsStructurallyValid()
|| SaveGame->VisibleSliceState.GetOrder() != PuzzleOrder
|| !ValidCoordinates.Contains(SaveGame->SliceCoordinate)
|| !UHyperTwistVirtual3333ProjectionLibrary::IsRotationAxisAvailable(
SaveGame->SliceAxis,
SaveGame->RotationAxis))
{
LastPersistenceStatus = TEXT("load rejected: invalid or cross-order 4D save");
return false;
}
const FHyperTwistVirtual3333ProjectionBuildResult ProjectionResult =
UHyperTwistVirtual3333ProjectionLibrary::BuildVisibleProjection(
SaveGame->VisibleSliceState,
SaveGame->SliceAxis,
SaveGame->SliceCoordinate);
if (!ProjectionResult.bProjected
|| !ProjectionResult.bExactProjection
|| !ProjectionResult.Projection.IsStructurallyValid())
{
LastPersistenceStatus = TEXT("load rejected: invalid 4D projection payload");
return false;
}
const FHyperTwistVirtual3333RuntimeState PreviousState = CurrentState;
const EHyperTwistVirtual3333Axis PreviousSliceAxis = CurrentSliceAxis;
const int32 PreviousSliceCoordinate = CurrentSliceCoordinate;
const EHyperTwistVirtual3333Axis PreviousRotationAxis = CurrentRotationAxis;
const bool bPreviousRenderCellShells = bRenderCellShells;
const FString PreviousNotation = LastAppliedNotation;
CurrentState = SaveGame->VisibleSliceState;
CurrentSliceAxis = SaveGame->SliceAxis;
CurrentSliceCoordinate = SaveGame->SliceCoordinate;
CurrentRotationAxis = SaveGame->RotationAxis;
bRenderCellShells = SaveGame->bRenderCellShells;
LastAppliedNotation = SaveGame->LastAppliedNotation;
LastWarnings.Reset();
if (!RefreshProjection())
{
CurrentState = PreviousState;
CurrentSliceAxis = PreviousSliceAxis;
CurrentSliceCoordinate = PreviousSliceCoordinate;
CurrentRotationAxis = PreviousRotationAxis;
bRenderCellShells = bPreviousRenderCellShells;
LastAppliedNotation = PreviousNotation;
RefreshProjection();
LastPersistenceStatus = TEXT("load rejected: projection rebuild failed");
return false;
}
LastPersistenceStatus = FString::Printf(
TEXT("loaded %dx%dx%dx%d session"),
PuzzleOrder,
PuzzleOrder,
PuzzleOrder,
PuzzleOrder);
return true;
}
FString AHyperTwistVirtual3333ProjectionActor::GetRuntimeSaveSlotName() const
{
return FString::Printf(
TEXT("HyperTwist_4D_%dx%dx%dx%d_v1"),
PuzzleOrder,
PuzzleOrder,
PuzzleOrder,
PuzzleOrder);
}
FString AHyperTwistVirtual3333ProjectionActor::GetSelectionLabel() const FString AHyperTwistVirtual3333ProjectionActor::GetSelectionLabel() const
{ {
return FString::Printf( return FString::Printf(
TEXT("slice %s[%+d], hold %s"), TEXT("%dx%dx%dx%d | slice %s[%+d], hold %s"),
PuzzleOrder,
PuzzleOrder,
PuzzleOrder,
PuzzleOrder,
*UHyperTwistVirtual3333ProjectionLibrary::GetAxisLabel(CurrentSliceAxis), *UHyperTwistVirtual3333ProjectionLibrary::GetAxisLabel(CurrentSliceAxis),
CurrentSliceCoordinate, CurrentSliceCoordinate,
*UHyperTwistVirtual3333ProjectionLibrary::GetAxisLabel(CurrentRotationAxis) *UHyperTwistVirtual3333ProjectionLibrary::GetAxisLabel(CurrentRotationAxis)
@ -202,7 +457,15 @@ FString AHyperTwistVirtual3333ProjectionActor::GetSelectionLabel() const
void AHyperTwistVirtual3333ProjectionActor::EnsureSelectionIsValid() void AHyperTwistVirtual3333ProjectionActor::EnsureSelectionIsValid()
{ {
CurrentSliceCoordinate = FMath::Clamp(CurrentSliceCoordinate, -1, 1); PuzzleOrder = UHyperTwistVirtual3333ProjectionLibrary::IsSupportedOrder(PuzzleOrder)
? PuzzleOrder
: 3;
const TArray<int32> Coordinates =
UHyperTwistVirtual3333ProjectionLibrary::GetSliceCoordinatesForOrder(PuzzleOrder);
if (!Coordinates.Contains(CurrentSliceCoordinate))
{
CurrentSliceCoordinate = Coordinates.IsEmpty() ? 0 : Coordinates.Last();
}
const TArray<EHyperTwistVirtual3333Axis> AvailableAxes = const TArray<EHyperTwistVirtual3333Axis> AvailableAxes =
UHyperTwistVirtual3333ProjectionLibrary::GetAvailableRotationAxesForSliceAxis( UHyperTwistVirtual3333ProjectionLibrary::GetAvailableRotationAxesForSliceAxis(
@ -257,7 +520,7 @@ void AHyperTwistVirtual3333ProjectionActor::BuildProjectedTesseract(
const FName ComponentName = MakeUniqueObjectName( const FName ComponentName = MakeUniqueObjectName(
this, this,
UProceduralMeshComponent::StaticClass(), UProceduralMeshComponent::StaticClass(),
*FString::Printf(TEXT("Virtual3333Tesseract_%02d"), TesseractProjection.PositionIndex) *FString::Printf(TEXT("VirtualNTesseract_%04d"), TesseractProjection.PositionIndex)
); );
UProceduralMeshComponent* Mesh = NewObject<UProceduralMeshComponent>( UProceduralMeshComponent* Mesh = NewObject<UProceduralMeshComponent>(
this, this,
@ -278,11 +541,14 @@ void AHyperTwistVirtual3333ProjectionActor::BuildProjectedTesseract(
Mesh->bUseComplexAsSimpleCollision = false; Mesh->bUseComplexAsSimpleCollision = false;
Mesh->SetCastShadow(false); Mesh->SetCastShadow(false);
// Even orders use doubled half-step lattice coordinates to preserve exact turns.
// Convert them back to one visual cell step so 4x and 6x do not render at 2x spacing.
const float DisplayCoordinateScale = PuzzleOrder % 2 == 0 ? 0.5f : 1.0f;
const FVector Center = FVector( const FVector Center = FVector(
static_cast<float>(TesseractProjection.LocalGridCoordinate.X), static_cast<float>(TesseractProjection.LocalGridCoordinate.X),
static_cast<float>(TesseractProjection.LocalGridCoordinate.Y), static_cast<float>(TesseractProjection.LocalGridCoordinate.Y),
static_cast<float>(TesseractProjection.LocalGridCoordinate.Z) static_cast<float>(TesseractProjection.LocalGridCoordinate.Z)
) * (TesseractSize + TesseractGap); ) * DisplayCoordinateScale * (TesseractSize + TesseractGap);
const float HalfSize = TesseractSize * 0.5f; const float HalfSize = TesseractSize * 0.5f;
for (int32 FaceIndex = 0; FaceIndex < TesseractProjection.VisibleCells.Num(); ++FaceIndex) for (int32 FaceIndex = 0; FaceIndex < TesseractProjection.VisibleCells.Num(); ++FaceIndex)

View file

@ -2,10 +2,87 @@
#include "Engine/World.h" #include "Engine/World.h"
#include "EngineUtils.h" #include "EngineUtils.h"
#include "HyperTwistDiagnostics/HyperTwistRuntimeDiagnostics.h"
#include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionActor.h" #include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionActor.h"
#include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionOrbitPawn.h" #include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionOrbitPawn.h"
#include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionPlayerController.h" #include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionPlayerController.h"
namespace HyperTwistVirtual3333ProjectionGameModeInternal
{
int32 GetExpectedStatePieceCount(const int32 PuzzleOrder)
{
return PuzzleOrder * PuzzleOrder * PuzzleOrder * PuzzleOrder;
}
int32 GetExpectedVisiblePieceViewCount(const int32 PuzzleOrder)
{
return PuzzleOrder * PuzzleOrder * PuzzleOrder;
}
void AppendPresentationReadiness(
const AHyperTwistVirtual3333ProjectionActor* ProjectionActor,
const int32 PuzzleOrder,
const bool bStartupStatePrepared)
{
if (ProjectionActor == nullptr)
{
HyperTwistRuntimeDiagnostics::AppendEvent(
TEXT("FourDimensionalPresentation"),
FString::Printf(
TEXT(
"Four-dimensional presentation failed for order %d: "
"projection actor unavailable."),
PuzzleOrder));
return;
}
const int32 ExpectedStatePieceCount = GetExpectedStatePieceCount(PuzzleOrder);
const int32 ExpectedVisiblePieceViewCount =
GetExpectedVisiblePieceViewCount(PuzzleOrder);
const int32 StatePieceCount = ProjectionActor->CurrentState.GetPieceCount();
const int32 ProjectedPieceViewCount =
ProjectionActor->CurrentProjection.Tesseracts.Num();
const int32 RenderablePieceViewCount =
ProjectionActor->GetRenderableTesseractCount();
const bool bStateValid =
bStartupStatePrepared
&& ProjectionActor->CurrentState.GetOrder() == PuzzleOrder
&& StatePieceCount == ExpectedStatePieceCount
&& ProjectionActor->CurrentState.IsStructurallyValid();
const bool bProjectionValid =
ProjectedPieceViewCount == ExpectedVisiblePieceViewCount
&& RenderablePieceViewCount == ExpectedVisiblePieceViewCount
&& ProjectionActor->HasValidProjection();
if (bStateValid && bProjectionValid)
{
HyperTwistRuntimeDiagnostics::AppendEvent(
TEXT("FourDimensionalPresentation"),
FString::Printf(
TEXT(
"Four-dimensional presentation initialized for order %d with %d exact "
"state pieces and %d visible projected piece views; state valid, projection valid."),
PuzzleOrder,
StatePieceCount,
RenderablePieceViewCount));
return;
}
HyperTwistRuntimeDiagnostics::AppendEvent(
TEXT("FourDimensionalPresentation"),
FString::Printf(
TEXT(
"Four-dimensional presentation failed for order %d with %d state pieces, "
"%d projected piece views, and %d rendered piece views; state %s, projection %s."),
PuzzleOrder,
StatePieceCount,
ProjectedPieceViewCount,
RenderablePieceViewCount,
bStateValid ? TEXT("valid") : TEXT("invalid"),
bProjectionValid ? TEXT("valid") : TEXT("invalid")));
}
}
AHyperTwistVirtual3333ProjectionGameMode::AHyperTwistVirtual3333ProjectionGameMode() AHyperTwistVirtual3333ProjectionGameMode::AHyperTwistVirtual3333ProjectionGameMode()
{ {
PlayerControllerClass = AHyperTwistVirtual3333ProjectionPlayerController::StaticClass(); PlayerControllerClass = AHyperTwistVirtual3333ProjectionPlayerController::StaticClass();
@ -16,6 +93,12 @@ void AHyperTwistVirtual3333ProjectionGameMode::BeginPlay()
{ {
Super::BeginPlay(); Super::BeginPlay();
HyperTwistRuntimeDiagnostics::AppendEvent(
TEXT("MapReady"),
FString::Printf(
TEXT("Runtime map ready: %s."),
GetWorld() != nullptr ? *GetWorld()->GetPathName() : TEXT("unknown")));
if (APlayerController* PlayerController = if (APlayerController* PlayerController =
GetWorld() != nullptr ? GetWorld()->GetFirstPlayerController() : nullptr) GetWorld() != nullptr ? GetWorld()->GetFirstPlayerController() : nullptr)
{ {
@ -25,13 +108,33 @@ void AHyperTwistVirtual3333ProjectionGameMode::BeginPlay()
AHyperTwistVirtual3333ProjectionActor* ProjectionActor = ResolveOrSpawnProjectionActor(); AHyperTwistVirtual3333ProjectionActor* ProjectionActor = ResolveOrSpawnProjectionActor();
if (ProjectionActor == nullptr) if (ProjectionActor == nullptr)
{ {
HyperTwistVirtual3333ProjectionGameModeInternal::AppendPresentationReadiness(
nullptr,
PuzzleOrder,
false);
return; return;
} }
ProjectionActor->SetSliceAxis(StartupSliceAxis); bool bStartupStatePrepared = ProjectionActor->SetPuzzleOrder(PuzzleOrder);
ProjectionActor->SetSliceCoordinate(StartupSliceCoordinate);
ProjectionActor->SetRotationAxis(StartupRotationAxis);
ProjectionActor->ResetToSolvedState(); ProjectionActor->ResetToSolvedState();
bStartupStatePrepared =
ProjectionActor->SetSliceAxis(StartupSliceAxis) && bStartupStatePrepared;
if (!ProjectionActor->SetSliceCoordinate(StartupSliceCoordinate))
{
bStartupStatePrepared =
ProjectionActor->SetSliceLayer(PuzzleOrder) && bStartupStatePrepared;
}
bStartupStatePrepared =
ProjectionActor->SetRotationAxis(StartupRotationAxis) && bStartupStatePrepared;
if (bLoadSavedStateOnBeginPlay)
{
ProjectionActor->LoadRuntimeState();
}
HyperTwistVirtual3333ProjectionGameModeInternal::AppendPresentationReadiness(
ProjectionActor,
PuzzleOrder,
bStartupStatePrepared);
} }
void AHyperTwistVirtual3333ProjectionGameMode::ResetProjectionToSolved() void AHyperTwistVirtual3333ProjectionGameMode::ResetProjectionToSolved()
@ -83,3 +186,21 @@ AHyperTwistVirtual3333ProjectionGameMode::ResolveOrSpawnProjectionActor()
); );
return ActiveProjectionActor; return ActiveProjectionActor;
} }
AHyperTwistVirtual4444ProjectionGameMode::AHyperTwistVirtual4444ProjectionGameMode()
{
PuzzleOrder = 4;
StartupSliceCoordinate = 3;
}
AHyperTwistVirtual5555ProjectionGameMode::AHyperTwistVirtual5555ProjectionGameMode()
{
PuzzleOrder = 5;
StartupSliceCoordinate = 2;
}
AHyperTwistVirtual6666ProjectionGameMode::AHyperTwistVirtual6666ProjectionGameMode()
{
PuzzleOrder = 6;
StartupSliceCoordinate = 5;
}

View file

@ -4,8 +4,56 @@
namespace HyperTwistVirtual3333ProjectionLibraryInternal namespace HyperTwistVirtual3333ProjectionLibraryInternal
{ {
constexpr int32 PieceCount = 81;
constexpr int32 AxisCount = 4; constexpr int32 AxisCount = 4;
constexpr int32 MinimumOrder = 2;
constexpr int32 MaximumOrder = 6;
int32 GetPieceCount(const int32 Order)
{
return Order * Order * Order * Order;
}
int32 GetVisiblePieceCount(const int32 Order)
{
return Order * Order * Order;
}
int32 CoordinateFromDigit(const int32 Digit, const int32 Order)
{
return Order % 2 == 0
? (Digit * 2) - (Order - 1)
: Digit - (Order / 2);
}
int32 DigitFromCoordinate(const int32 Coordinate, const int32 Order)
{
return Order % 2 == 0
? (Coordinate + (Order - 1)) / 2
: Coordinate + (Order / 2);
}
int32 GetBoundaryCoordinateMagnitude(const int32 Order)
{
return Order % 2 == 0 ? Order - 1 : Order / 2;
}
bool IsCoordinateValueValid(const int32 Coordinate, const int32 Order)
{
if (Order < MinimumOrder || Order > MaximumOrder)
{
return false;
}
if (Order % 2 != 0)
{
const int32 Boundary = GetBoundaryCoordinateMagnitude(Order);
return Coordinate >= -Boundary && Coordinate <= Boundary;
}
const int32 Shifted = Coordinate + (Order - 1);
return Coordinate >= -(Order - 1)
&& Coordinate <= (Order - 1)
&& Shifted % 2 == 0;
}
template <typename TStruct> template <typename TStruct>
FString SerializeStructToJson(const TStruct& Value) FString SerializeStructToJson(const TStruct& Value)
@ -76,24 +124,33 @@ namespace HyperTwistVirtual3333ProjectionLibraryInternal
} }
} }
FHyperTwistVirtual3333GridCoordinate DecodeCoordinate(const int32 PositionIndex) FHyperTwistVirtual3333GridCoordinate DecodeCoordinate(
const int32 PositionIndex,
const int32 Order
)
{ {
FHyperTwistVirtual3333GridCoordinate Coordinate; FHyperTwistVirtual3333GridCoordinate Coordinate;
int32 Remainder = PositionIndex; int32 Remainder = PositionIndex;
Coordinate.X = (Remainder % 3) - 1; Coordinate.X = CoordinateFromDigit(Remainder % Order, Order);
Remainder /= 3; Remainder /= Order;
Coordinate.Y = (Remainder % 3) - 1; Coordinate.Y = CoordinateFromDigit(Remainder % Order, Order);
Remainder /= 3; Remainder /= Order;
Coordinate.Z = (Remainder % 3) - 1; Coordinate.Z = CoordinateFromDigit(Remainder % Order, Order);
Remainder /= 3; Remainder /= Order;
Coordinate.W = (Remainder % 3) - 1; Coordinate.W = CoordinateFromDigit(Remainder % Order, Order);
return Coordinate; return Coordinate;
} }
int32 EncodeCoordinate(const FHyperTwistVirtual3333GridCoordinate& Coordinate) int32 EncodeCoordinate(
const FHyperTwistVirtual3333GridCoordinate& Coordinate,
const int32 Order
)
{ {
return (((Coordinate.W + 1) * 3 + (Coordinate.Z + 1)) * 3 + (Coordinate.Y + 1)) * 3 return (((
+ (Coordinate.X + 1); DigitFromCoordinate(Coordinate.W, Order) * Order
+ DigitFromCoordinate(Coordinate.Z, Order)) * Order
+ DigitFromCoordinate(Coordinate.Y, Order)) * Order
+ DigitFromCoordinate(Coordinate.X, Order));
} }
FString GetAxisLabel(const EHyperTwistVirtual3333Axis Axis) FString GetAxisLabel(const EHyperTwistVirtual3333Axis Axis)
@ -111,26 +168,53 @@ namespace HyperTwistVirtual3333ProjectionLibraryInternal
} }
} }
FHyperTwistPuzzleDefinitionRef BuildDefinition() FHyperTwistPuzzleDefinitionRef BuildDefinition(const int32 Order)
{ {
FHyperTwistPuzzleDefinitionRef Definition; FHyperTwistPuzzleDefinitionRef Definition;
Definition.PuzzleId = TEXT("hypercube/3x3x3x3"); Definition.PuzzleId = FString::Printf(
TEXT("hypercube/%dx%dx%dx%d"),
Order,
Order,
Order,
Order);
Definition.PuzzleFamily = EHyperTwistPuzzleFamily::Hypercube; Definition.PuzzleFamily = EHyperTwistPuzzleFamily::Hypercube;
Definition.Dimension = 4; Definition.Dimension = 4;
Definition.DefinitionVersion = TEXT("2026.06"); Definition.DefinitionVersion = TEXT("2026.07");
Definition.NotationProfile = TEXT("virtual-3x3x3x3-slice-v1"); Definition.NotationProfile =
Definition.SizeVector = {3, 3, 3, 3}; FString::Printf(TEXT("virtual-%dx%dx%dx%d-slice-v1"), Order, Order, Order, Order);
Definition.SizeVector = {Order, Order, Order, Order};
Definition.Variant = TEXT("virtual-slice-projection"); Definition.Variant = TEXT("virtual-slice-projection");
return Definition; return Definition;
} }
bool IsSupportedDefinition(const FHyperTwistPuzzleDefinitionRef& Definition) bool IsSupportedDefinition(const FHyperTwistPuzzleDefinitionRef& Definition)
{ {
return Definition.IsStructurallyValid() if (!Definition.IsStructurallyValid()
&& Definition.PuzzleId == TEXT("hypercube/3x3x3x3") || Definition.PuzzleFamily != EHyperTwistPuzzleFamily::Hypercube
&& Definition.PuzzleFamily == EHyperTwistPuzzleFamily::Hypercube || Definition.Dimension != 4
&& Definition.Dimension == 4 || Definition.SizeVector.Num() != 4)
&& Definition.SizeVector == TArray<int32>({3, 3, 3, 3}); {
return false;
}
const int32 Order = Definition.SizeVector[0];
if (Order < MinimumOrder || Order > MaximumOrder)
{
return false;
}
for (const int32 Size : Definition.SizeVector)
{
if (Size != Order)
{
return false;
}
}
return Definition.PuzzleId == FString::Printf(
TEXT("hypercube/%dx%dx%dx%d"),
Order,
Order,
Order,
Order);
} }
FHyperTwistVirtual3333SignedAxis MakeSignedAxis( FHyperTwistVirtual3333SignedAxis MakeSignedAxis(
@ -175,14 +259,58 @@ namespace HyperTwistVirtual3333ProjectionLibraryInternal
return true; return true;
} }
bool IsSolvedRuntimeState(const FHyperTwistVirtual3333RuntimeState& State) bool IsRuntimeStateShapeValid(const FHyperTwistVirtual3333RuntimeState& State)
{ {
if (!State.IsStructurallyValid()) const int32 Order = State.GetOrder();
const int32 PieceCount = State.GetPieceCount();
const FString ExpectedStateProfile = FString::Printf(
TEXT("hypercube-%dx%dx%dx%d-runtime-v1"),
Order,
Order,
Order,
Order);
if (State.StateProfile != ExpectedStateProfile
|| !State.Definition.IsStructurallyValid()
|| !IsSupportedDefinition(State.Definition)
|| State.Definition.PuzzleFamily != EHyperTwistPuzzleFamily::Hypercube
|| State.Definition.Dimension != 4
|| Order < MinimumOrder
|| Order > MaximumOrder
|| State.PositionToPiece.Num() != PieceCount
|| State.PieceOrientations.Num() != PieceCount)
{ {
return false; return false;
} }
for (int32 PositionIndex = 0; PositionIndex < PieceCount; ++PositionIndex) TBitArray<> SeenPieces(false, PieceCount);
for (const int32 PieceId : State.PositionToPiece)
{
if (PieceId < 0 || PieceId >= PieceCount || SeenPieces[PieceId])
{
return false;
}
SeenPieces[PieceId] = true;
}
for (const FHyperTwistVirtual3333PieceOrientation& Orientation :
State.PieceOrientations)
{
if (!Orientation.IsStructurallyValid())
{
return false;
}
}
return true;
}
bool IsSolvedRuntimeState(const FHyperTwistVirtual3333RuntimeState& State)
{
if (!IsRuntimeStateShapeValid(State))
{
return false;
}
for (int32 PositionIndex = 0; PositionIndex < State.GetPieceCount(); ++PositionIndex)
{ {
if (State.PositionToPiece[PositionIndex] != PositionIndex if (State.PositionToPiece[PositionIndex] != PositionIndex
|| !IsIdentityOrientation(State.PieceOrientations[PositionIndex])) || !IsIdentityOrientation(State.PieceOrientations[PositionIndex]))
@ -359,7 +487,8 @@ namespace HyperTwistVirtual3333ProjectionLibraryInternal
FHyperTwistVirtual3333ProjectedCell BuildProjectedCell( FHyperTwistVirtual3333ProjectedCell BuildProjectedCell(
const FHyperTwistVirtual3333GridCoordinate& PieceHomeCoordinate, const FHyperTwistVirtual3333GridCoordinate& PieceHomeCoordinate,
const FHyperTwistVirtual3333PieceOrientation& Orientation, const FHyperTwistVirtual3333PieceOrientation& Orientation,
const EHyperTwistVirtual3333Axis WorldAxis const EHyperTwistVirtual3333Axis WorldAxis,
const int32 Order
) )
{ {
FHyperTwistVirtual3333ProjectedCell Cell; FHyperTwistVirtual3333ProjectedCell Cell;
@ -376,7 +505,7 @@ namespace HyperTwistVirtual3333ProjectionLibraryInternal
} }
const int32 LocalStickerSign = GetCoordinateValue(PieceHomeCoordinate, LocalAxis); const int32 LocalStickerSign = GetCoordinateValue(PieceHomeCoordinate, LocalAxis);
if (LocalStickerSign == 0) if (FMath::Abs(LocalStickerSign) != GetBoundaryCoordinateMagnitude(Order))
{ {
return Cell; return Cell;
} }
@ -385,7 +514,7 @@ namespace HyperTwistVirtual3333ProjectionLibraryInternal
const int32 CorrespondingLocalSide = BasisAxis.bPositiveDirection == (VisibleWorldSign > 0) const int32 CorrespondingLocalSide = BasisAxis.bPositiveDirection == (VisibleWorldSign > 0)
? 1 ? 1
: -1; : -1;
if (LocalStickerSign == CorrespondingLocalSide) if (FMath::Sign(LocalStickerSign) == CorrespondingLocalSide)
{ {
Cell.bHasSticker = true; Cell.bHasSticker = true;
Cell.Color = ResolveLocalStickerColor(LocalAxis, LocalStickerSign); Cell.Color = ResolveLocalStickerColor(LocalAxis, LocalStickerSign);
@ -397,66 +526,75 @@ namespace HyperTwistVirtual3333ProjectionLibraryInternal
} }
} }
bool FHyperTwistVirtual3333GridCoordinate::IsValidForOrder(const int32 Order) const
{
using namespace HyperTwistVirtual3333ProjectionLibraryInternal;
return Order >= MinimumOrder
&& Order <= MaximumOrder
&& IsCoordinateValueValid(X, Order)
&& IsCoordinateValueValid(Y, Order)
&& IsCoordinateValueValid(Z, Order)
&& IsCoordinateValueValid(W, Order);
}
int32 FHyperTwistVirtual3333RuntimeState::GetOrder() const
{
if (Definition.SizeVector.Num() != 4)
{
return 0;
}
const int32 Order = Definition.SizeVector[0];
for (const int32 Size : Definition.SizeVector)
{
if (Size != Order)
{
return 0;
}
}
return Order;
}
int32 FHyperTwistVirtual3333RuntimeState::GetPieceCount() const
{
const int32 Order = GetOrder();
return UHyperTwistVirtual3333ProjectionLibrary::IsSupportedOrder(Order)
? HyperTwistVirtual3333ProjectionLibraryInternal::GetPieceCount(Order)
: 0;
}
bool FHyperTwistVirtual3333RuntimeState::IsStructurallyValid() const bool FHyperTwistVirtual3333RuntimeState::IsStructurallyValid() const
{ {
const TArray<int32> SupportedSize = {3, 3, 3, 3}; using namespace HyperTwistVirtual3333ProjectionLibraryInternal;
if (StateProfile.IsEmpty() if (!IsRuntimeStateShapeValid(*this))
|| !Definition.IsStructurallyValid()
|| Definition.PuzzleId != TEXT("hypercube/3x3x3x3")
|| Definition.PuzzleFamily != EHyperTwistPuzzleFamily::Hypercube
|| Definition.Dimension != 4
|| Definition.SizeVector != SupportedSize
|| PositionToPiece.Num() != 81
|| PieceOrientations.Num() != 81)
{ {
return false; return false;
} }
bool bSeenPieces[81] = { return bIsSolved == IsSolvedRuntimeState(*this);
false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false
};
for (int32 PositionIndex = 0; PositionIndex < PositionToPiece.Num(); ++PositionIndex)
{
const int32 PieceId = PositionToPiece[PositionIndex];
if (PieceId < 0 || PieceId >= 81 || bSeenPieces[PieceId])
{
return false;
}
bSeenPieces[PieceId] = true;
}
for (const FHyperTwistVirtual3333PieceOrientation& Orientation : PieceOrientations)
{
if (!Orientation.IsStructurallyValid())
{
return false;
}
}
return true;
} }
bool FHyperTwistVirtual3333ProjectedTesseract::IsStructurallyValid() const bool FHyperTwistVirtual3333ProjectedTesseract::IsStructurallyValid() const
{ {
if (!UHyperTwistVirtual3333ProjectionLibrary::IsSupportedOrder(PuzzleOrder))
{
return false;
}
const int32 PieceCount =
HyperTwistVirtual3333ProjectionLibraryInternal::GetPieceCount(PuzzleOrder);
if (PositionIndex < 0 if (PositionIndex < 0
|| PositionIndex >= 81 || PositionIndex >= PieceCount
|| PieceId < 0 || PieceId < 0
|| PieceId >= 81 || PieceId >= PieceCount
|| LocalGridCoordinate.X < -1 || !HyperTwistVirtual3333ProjectionLibraryInternal::IsCoordinateValueValid(
|| LocalGridCoordinate.X > 1 LocalGridCoordinate.X,
|| LocalGridCoordinate.Y < -1 PuzzleOrder)
|| LocalGridCoordinate.Y > 1 || !HyperTwistVirtual3333ProjectionLibraryInternal::IsCoordinateValueValid(
|| LocalGridCoordinate.Z < -1 LocalGridCoordinate.Y,
|| LocalGridCoordinate.Z > 1 PuzzleOrder)
|| !HyperTwistVirtual3333ProjectionLibraryInternal::IsCoordinateValueValid(
LocalGridCoordinate.Z,
PuzzleOrder)
|| SlotLabel.IsEmpty() || SlotLabel.IsEmpty()
|| PieceLabel.IsEmpty() || PieceLabel.IsEmpty()
|| bPieceInSolvedPosition != (PositionIndex == PieceId) || bPieceInSolvedPosition != (PositionIndex == PieceId)
@ -485,12 +623,24 @@ bool FHyperTwistVirtual3333ProjectedTesseract::IsStructurallyValid() const
bool FHyperTwistVirtual3333VisibleProjection::IsStructurallyValid() const bool FHyperTwistVirtual3333VisibleProjection::IsStructurallyValid() const
{ {
if (Definition.SizeVector.Num() != 4)
{
return false;
}
const int32 Order = Definition.SizeVector[0];
const int32 PieceCount =
HyperTwistVirtual3333ProjectionLibraryInternal::GetPieceCount(Order);
const int32 VisiblePieceCount =
HyperTwistVirtual3333ProjectionLibraryInternal::GetVisiblePieceCount(Order);
if (ProjectionProfile.IsEmpty() if (ProjectionProfile.IsEmpty()
|| !Definition.IsStructurallyValid() || !Definition.IsStructurallyValid()
|| VisibleSliceCoordinate < -1 || !HyperTwistVirtual3333ProjectionLibraryInternal::IsSupportedDefinition(Definition)
|| VisibleSliceCoordinate > 1 || !UHyperTwistVirtual3333ProjectionLibrary::IsSupportedOrder(Order)
|| !HyperTwistVirtual3333ProjectionLibraryInternal::IsCoordinateValueValid(
VisibleSliceCoordinate,
Order)
|| DisplayAxes.Num() != 3 || DisplayAxes.Num() != 3
|| Tesseracts.Num() != 27) || Tesseracts.Num() != VisiblePieceCount)
{ {
return false; return false;
} }
@ -510,52 +660,55 @@ bool FHyperTwistVirtual3333VisibleProjection::IsStructurallyValid() const
bSeenDisplayAxes[AxisIndex] = true; bSeenDisplayAxes[AxisIndex] = true;
} }
bool bSeenPositions[81] = { TBitArray<> SeenPositions(false, PieceCount);
false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false
};
int32 VisibleCellCount = 0; int32 VisibleCellCount = 0;
for (const FHyperTwistVirtual3333ProjectedTesseract& Tesseract : Tesseracts) for (const FHyperTwistVirtual3333ProjectedTesseract& Tesseract : Tesseracts)
{ {
if (!Tesseract.IsStructurallyValid() || bSeenPositions[Tesseract.PositionIndex]) if (Tesseract.PuzzleOrder != Order
|| !Tesseract.IsStructurallyValid()
|| SeenPositions[Tesseract.PositionIndex])
{ {
return false; return false;
} }
bSeenPositions[Tesseract.PositionIndex] = true; SeenPositions[Tesseract.PositionIndex] = true;
VisibleCellCount += Tesseract.VisibleCells.Num(); VisibleCellCount += Tesseract.VisibleCells.Num();
} }
return VisibleCellCount == 81; return VisibleCellCount == VisiblePieceCount * 3;
} }
FHyperTwistPuzzleDefinitionRef UHyperTwistVirtual3333ProjectionLibrary::MakePuzzleDefinition() FHyperTwistPuzzleDefinitionRef UHyperTwistVirtual3333ProjectionLibrary::MakePuzzleDefinition(
const int32 Order
)
{ {
return HyperTwistVirtual3333ProjectionLibraryInternal::BuildDefinition(); const int32 SafeOrder = IsSupportedOrder(Order) ? Order : 3;
return HyperTwistVirtual3333ProjectionLibraryInternal::BuildDefinition(SafeOrder);
} }
FHyperTwistVirtual3333RuntimeState UHyperTwistVirtual3333ProjectionLibrary::BuildSolvedState() FHyperTwistVirtual3333RuntimeState UHyperTwistVirtual3333ProjectionLibrary::BuildSolvedState(
const int32 Order
)
{ {
using namespace HyperTwistVirtual3333ProjectionLibraryInternal;
const int32 SafeOrder = IsSupportedOrder(Order) ? Order : 3;
const int32 PieceCount =
HyperTwistVirtual3333ProjectionLibraryInternal::GetPieceCount(SafeOrder);
FHyperTwistVirtual3333RuntimeState State; FHyperTwistVirtual3333RuntimeState State;
State.Definition = MakePuzzleDefinition(); State.StateProfile = FString::Printf(
State.PositionToPiece.Reserve(HyperTwistVirtual3333ProjectionLibraryInternal::PieceCount); TEXT("hypercube-%dx%dx%dx%d-runtime-v1"),
State.PieceOrientations.Reserve(HyperTwistVirtual3333ProjectionLibraryInternal::PieceCount); SafeOrder,
for (int32 PieceId = 0; SafeOrder,
PieceId < HyperTwistVirtual3333ProjectionLibraryInternal::PieceCount; SafeOrder,
++PieceId) SafeOrder);
State.Definition = MakePuzzleDefinition(SafeOrder);
State.PositionToPiece.Reserve(PieceCount);
State.PieceOrientations.Reserve(PieceCount);
for (int32 PieceId = 0; PieceId < PieceCount; ++PieceId)
{ {
State.PositionToPiece.Add(PieceId); State.PositionToPiece.Add(PieceId);
State.PieceOrientations.Add( State.PieceOrientations.Add(BuildIdentityOrientation());
HyperTwistVirtual3333ProjectionLibraryInternal::BuildIdentityOrientation()
);
} }
State.bIsSolved = true; State.bIsSolved = true;
return State; return State;
@ -565,15 +718,24 @@ FHyperTwistPuzzleState UHyperTwistVirtual3333ProjectionLibrary::BuildPuzzleState
const FHyperTwistVirtual3333RuntimeState& RuntimeState const FHyperTwistVirtual3333RuntimeState& RuntimeState
) )
{ {
const FHyperTwistVirtual3333RuntimeState SafeRuntimeState =
RuntimeState.IsStructurallyValid()
? RuntimeState
: BuildSolvedState(3);
FHyperTwistPuzzleState PuzzleState; FHyperTwistPuzzleState PuzzleState;
PuzzleState.Definition = RuntimeState.IsStructurallyValid() PuzzleState.Definition = SafeRuntimeState.Definition;
? RuntimeState.Definition
: MakePuzzleDefinition();
PuzzleState.StateEncodingKind = EHyperTwistStateEncodingKind::FamilySpecific; PuzzleState.StateEncodingKind = EHyperTwistStateEncodingKind::FamilySpecific;
PuzzleState.StateEncoding.EncodingProfile = TEXT("hypercube-3x3x3x3-runtime-v1"); PuzzleState.StateEncoding.EncodingProfile = SafeRuntimeState.StateProfile;
PuzzleState.StateEncoding.PayloadJson = SerializeRuntimeStateToJson(RuntimeState); PuzzleState.StateEncoding.PayloadJson =
PuzzleState.OrientationFrame.Reference = TEXT("hypercube-3x3x3x3-canonical-v1"); SerializeRuntimeStateToJson(SafeRuntimeState);
PuzzleState.bIsSolved = RuntimeState.bIsSolved; const int32 Order = SafeRuntimeState.GetOrder();
PuzzleState.OrientationFrame.Reference = FString::Printf(
TEXT("hypercube-%dx%dx%dx%d-canonical-v1"),
Order,
Order,
Order,
Order);
PuzzleState.bIsSolved = SafeRuntimeState.bIsSolved;
PuzzleState.Source = EHyperTwistStateSource::Runtime; PuzzleState.Source = EHyperTwistStateSource::Runtime;
return PuzzleState; return PuzzleState;
} }
@ -600,6 +762,47 @@ UHyperTwistVirtual3333ProjectionLibrary::GetAvailableRotationAxesForSliceAxis(
return HyperTwistVirtual3333ProjectionLibraryInternal::GetAxesExcluding(SliceAxis); return HyperTwistVirtual3333ProjectionLibraryInternal::GetAxesExcluding(SliceAxis);
} }
TArray<int32> UHyperTwistVirtual3333ProjectionLibrary::GetSliceCoordinatesForOrder(
const int32 Order
)
{
TArray<int32> Coordinates;
if (!IsSupportedOrder(Order))
{
return Coordinates;
}
Coordinates.Reserve(Order);
for (int32 LayerIndex = 0; LayerIndex < Order; ++LayerIndex)
{
Coordinates.Add(
HyperTwistVirtual3333ProjectionLibraryInternal::CoordinateFromDigit(
LayerIndex,
Order));
}
return Coordinates;
}
int32 UHyperTwistVirtual3333ProjectionLibrary::GetSliceCoordinateForLayer(
const int32 Order,
const int32 OneBasedLayer
)
{
if (!IsSupportedOrder(Order))
{
return 0;
}
const int32 SafeLayer = FMath::Clamp(OneBasedLayer, 1, Order);
return HyperTwistVirtual3333ProjectionLibraryInternal::CoordinateFromDigit(
SafeLayer - 1,
Order);
}
bool UHyperTwistVirtual3333ProjectionLibrary::IsSupportedOrder(const int32 Order)
{
return Order >= HyperTwistVirtual3333ProjectionLibraryInternal::MinimumOrder
&& Order <= HyperTwistVirtual3333ProjectionLibraryInternal::MaximumOrder;
}
bool UHyperTwistVirtual3333ProjectionLibrary::IsRotationAxisAvailable( bool UHyperTwistVirtual3333ProjectionLibrary::IsRotationAxisAvailable(
const EHyperTwistVirtual3333Axis SliceAxis, const EHyperTwistVirtual3333Axis SliceAxis,
const EHyperTwistVirtual3333Axis RotationAxis const EHyperTwistVirtual3333Axis RotationAxis
@ -620,19 +823,22 @@ FHyperTwistVirtual3333SliceTurnResult UHyperTwistVirtual3333ProjectionLibrary::A
if (!State.IsStructurallyValid()) if (!State.IsStructurallyValid())
{ {
Result.Warnings.Add(TEXT("invalid-virtual-3x3x3x3-state")); Result.Warnings.Add(TEXT("invalid-virtual-nxnxnxn-state"));
return Result; return Result;
} }
if (!IsSupportedDefinition(State.Definition)) if (!IsSupportedDefinition(State.Definition))
{ {
Result.Warnings.Add(TEXT("unsupported-virtual-3x3x3x3-definition")); Result.Warnings.Add(TEXT("unsupported-virtual-nxnxnxn-definition"));
return Result; return Result;
} }
if (!Request.IsStructurallyValid()) const int32 Order = State.GetOrder();
const int32 PieceCount = State.GetPieceCount();
if (!Request.IsStructurallyValid()
|| !IsCoordinateValueValid(Request.SliceCoordinate, Order))
{ {
Result.Warnings.Add(TEXT("invalid-virtual-3x3x3x3-turn-request")); Result.Warnings.Add(TEXT("invalid-virtual-nxnxnxn-turn-request"));
return Result; return Result;
} }
@ -644,7 +850,8 @@ FHyperTwistVirtual3333SliceTurnResult UHyperTwistVirtual3333ProjectionLibrary::A
Result.State.PieceOrientations = State.PieceOrientations; Result.State.PieceOrientations = State.PieceOrientations;
for (int32 PositionIndex = 0; PositionIndex < PieceCount; ++PositionIndex) for (int32 PositionIndex = 0; PositionIndex < PieceCount; ++PositionIndex)
{ {
const FHyperTwistVirtual3333GridCoordinate Coordinate = DecodeCoordinate(PositionIndex); const FHyperTwistVirtual3333GridCoordinate Coordinate =
DecodeCoordinate(PositionIndex, Order);
if (GetCoordinateValue(Coordinate, Request.SliceAxis) != Request.SliceCoordinate) if (GetCoordinateValue(Coordinate, Request.SliceAxis) != Request.SliceCoordinate)
{ {
continue; continue;
@ -653,7 +860,7 @@ FHyperTwistVirtual3333SliceTurnResult UHyperTwistVirtual3333ProjectionLibrary::A
const int32 PieceId = State.PositionToPiece[PositionIndex]; const int32 PieceId = State.PositionToPiece[PositionIndex];
const FHyperTwistVirtual3333GridCoordinate RotatedCoordinate = const FHyperTwistVirtual3333GridCoordinate RotatedCoordinate =
RotateCoordinateInPlane(Coordinate, PlaneAxisA, PlaneAxisB, Request.Direction); RotateCoordinateInPlane(Coordinate, PlaneAxisA, PlaneAxisB, Request.Direction);
const int32 NewPositionIndex = EncodeCoordinate(RotatedCoordinate); const int32 NewPositionIndex = EncodeCoordinate(RotatedCoordinate, Order);
Result.State.PositionToPiece[NewPositionIndex] = PieceId; Result.State.PositionToPiece[NewPositionIndex] = PieceId;
Result.State.PieceOrientations[PieceId] = RotateOrientationInPlane( Result.State.PieceOrientations[PieceId] = RotateOrientationInPlane(
State.PieceOrientations[PieceId], State.PieceOrientations[PieceId],
@ -669,12 +876,89 @@ FHyperTwistVirtual3333SliceTurnResult UHyperTwistVirtual3333ProjectionLibrary::A
Result.bExactStateUpdate = Result.State.IsStructurallyValid(); Result.bExactStateUpdate = Result.State.IsStructurallyValid();
if (!Result.bExactStateUpdate) if (!Result.bExactStateUpdate)
{ {
Result.Warnings.Add(TEXT("virtual-3x3x3x3-state-became-invalid")); Result.Warnings.Add(TEXT("virtual-nxnxnxn-state-became-invalid"));
} }
return Result; return Result;
} }
FHyperTwistVirtual3333ScrambleResult
UHyperTwistVirtual3333ProjectionLibrary::GenerateScramble(
const int32 Order,
const int32 MoveCount,
const int32 RandomSeed
)
{
FHyperTwistVirtual3333ScrambleResult Result;
if (!IsSupportedOrder(Order) || MoveCount < 1 || MoveCount > 500)
{
Result.Warnings.Add(TEXT("virtual-nxnxnxn-scramble-request-invalid"));
return Result;
}
const TArray<int32> SliceCoordinates = GetSliceCoordinatesForOrder(Order);
if (SliceCoordinates.Num() != Order)
{
Result.Warnings.Add(TEXT("virtual-nxnxnxn-scramble-layers-unavailable"));
return Result;
}
FRandomStream Random(RandomSeed);
Result.State = BuildSolvedState(Order);
Result.AppliedMoves.Reserve(MoveCount);
FHyperTwistVirtual3333SliceTurnRequest PreviousRequest;
bool bHasPreviousRequest = false;
for (int32 MoveIndex = 0; MoveIndex < MoveCount; ++MoveIndex)
{
FHyperTwistVirtual3333SliceTurnRequest Request;
bool bDistinctFromPrevious = false;
for (int32 Attempt = 0; Attempt < 16 && !bDistinctFromPrevious; ++Attempt)
{
Request.SliceAxis = static_cast<EHyperTwistVirtual3333Axis>(
Random.RandRange(0, 3));
Request.SliceCoordinate =
SliceCoordinates[Random.RandRange(0, SliceCoordinates.Num() - 1)];
const TArray<EHyperTwistVirtual3333Axis> RotationAxes =
GetAvailableRotationAxesForSliceAxis(Request.SliceAxis);
Request.RotationAxis =
RotationAxes[Random.RandRange(0, RotationAxes.Num() - 1)];
Request.Direction = Random.RandRange(0, 1) == 0
? EHyperTwistVirtual3333TurnDirection::Clockwise
: EHyperTwistVirtual3333TurnDirection::CounterClockwise;
bDistinctFromPrevious = !bHasPreviousRequest
|| Request.SliceAxis != PreviousRequest.SliceAxis
|| Request.SliceCoordinate != PreviousRequest.SliceCoordinate
|| Request.RotationAxis != PreviousRequest.RotationAxis;
}
const FHyperTwistVirtual3333SliceTurnResult TurnResult =
ApplySliceTurn(Result.State, Request);
if (!TurnResult.bApplied || !TurnResult.bExactStateUpdate)
{
Result.Warnings = TurnResult.Warnings;
Result.Warnings.Add(TEXT("virtual-nxnxnxn-scramble-turn-failed"));
Result.State = FHyperTwistVirtual3333RuntimeState();
Result.AppliedMoves.Reset();
return Result;
}
Result.State = TurnResult.State;
Result.AppliedMoves.Add(TurnResult.AppliedNotation);
PreviousRequest = Request;
bHasPreviousRequest = true;
}
Result.FinalSelection = PreviousRequest;
Result.bExactStateUpdate = Result.State.IsStructurallyValid();
Result.bGenerated = Result.bExactStateUpdate
&& Result.AppliedMoves.Num() == MoveCount;
if (!Result.bGenerated)
{
Result.Warnings.Add(TEXT("virtual-nxnxnxn-scramble-state-invalid"));
}
return Result;
}
FHyperTwistVirtual3333ProjectionBuildResult FHyperTwistVirtual3333ProjectionBuildResult
UHyperTwistVirtual3333ProjectionLibrary::BuildVisibleProjection( UHyperTwistVirtual3333ProjectionLibrary::BuildVisibleProjection(
const FHyperTwistVirtual3333RuntimeState& State, const FHyperTwistVirtual3333RuntimeState& State,
@ -687,46 +971,58 @@ UHyperTwistVirtual3333ProjectionLibrary::BuildVisibleProjection(
FHyperTwistVirtual3333ProjectionBuildResult Result; FHyperTwistVirtual3333ProjectionBuildResult Result;
Result.Projection.Definition = State.Definition.IsStructurallyValid() Result.Projection.Definition = State.Definition.IsStructurallyValid()
? State.Definition ? State.Definition
: MakePuzzleDefinition(); : MakePuzzleDefinition(3);
Result.Projection.VisibleSliceAxis = VisibleSliceAxis; Result.Projection.VisibleSliceAxis = VisibleSliceAxis;
Result.Projection.VisibleSliceCoordinate = VisibleSliceCoordinate; Result.Projection.VisibleSliceCoordinate = VisibleSliceCoordinate;
Result.Projection.DisplayAxes = GetAxesExcluding(VisibleSliceAxis); Result.Projection.DisplayAxes = GetAxesExcluding(VisibleSliceAxis);
if (!State.IsStructurallyValid()) if (!State.IsStructurallyValid())
{ {
Result.Warnings.Add(TEXT("invalid-virtual-3x3x3x3-state")); Result.Warnings.Add(TEXT("invalid-virtual-nxnxnxn-state"));
return Result; return Result;
} }
if (!IsSupportedDefinition(State.Definition)) if (!IsSupportedDefinition(State.Definition))
{ {
Result.Warnings.Add(TEXT("unsupported-virtual-3x3x3x3-definition")); Result.Warnings.Add(TEXT("unsupported-virtual-nxnxnxn-definition"));
return Result; return Result;
} }
if (VisibleSliceCoordinate < -1 || VisibleSliceCoordinate > 1) const int32 Order = State.GetOrder();
const int32 PieceCount = State.GetPieceCount();
const int32 VisiblePieceCount = GetVisiblePieceCount(Order);
Result.Projection.ProjectionProfile = FString::Printf(
TEXT("hypercube-%dx%dx%dx%d-visible-slice-projection-v1"),
Order,
Order,
Order,
Order);
if (!IsCoordinateValueValid(VisibleSliceCoordinate, Order))
{ {
Result.Warnings.Add(TEXT("invalid-visible-slice-coordinate")); Result.Warnings.Add(TEXT("invalid-visible-slice-coordinate"));
return Result; return Result;
} }
Result.Projection.Tesseracts.Reserve(27); Result.Projection.Tesseracts.Reserve(VisiblePieceCount);
for (int32 PositionIndex = 0; PositionIndex < PieceCount; ++PositionIndex) for (int32 PositionIndex = 0; PositionIndex < PieceCount; ++PositionIndex)
{ {
const FHyperTwistVirtual3333GridCoordinate Coordinate = DecodeCoordinate(PositionIndex); const FHyperTwistVirtual3333GridCoordinate Coordinate =
DecodeCoordinate(PositionIndex, Order);
if (GetCoordinateValue(Coordinate, VisibleSliceAxis) != VisibleSliceCoordinate) if (GetCoordinateValue(Coordinate, VisibleSliceAxis) != VisibleSliceCoordinate)
{ {
continue; continue;
} }
const int32 PieceId = State.PositionToPiece[PositionIndex]; const int32 PieceId = State.PositionToPiece[PositionIndex];
const FHyperTwistVirtual3333GridCoordinate PieceHomeCoordinate = DecodeCoordinate(PieceId); const FHyperTwistVirtual3333GridCoordinate PieceHomeCoordinate =
DecodeCoordinate(PieceId, Order);
const FHyperTwistVirtual3333PieceOrientation& Orientation = const FHyperTwistVirtual3333PieceOrientation& Orientation =
State.PieceOrientations[PieceId]; State.PieceOrientations[PieceId];
FHyperTwistVirtual3333ProjectedTesseract Tesseract; FHyperTwistVirtual3333ProjectedTesseract Tesseract;
Tesseract.PositionIndex = PositionIndex; Tesseract.PositionIndex = PositionIndex;
Tesseract.PieceId = PieceId; Tesseract.PieceId = PieceId;
Tesseract.PuzzleOrder = Order;
Tesseract.LocalGridCoordinate = FIntVector( Tesseract.LocalGridCoordinate = FIntVector(
GetCoordinateValue(Coordinate, Result.Projection.DisplayAxes[0]), GetCoordinateValue(Coordinate, Result.Projection.DisplayAxes[0]),
GetCoordinateValue(Coordinate, Result.Projection.DisplayAxes[1]), GetCoordinateValue(Coordinate, Result.Projection.DisplayAxes[1]),
@ -739,7 +1035,7 @@ UHyperTwistVirtual3333ProjectionLibrary::BuildVisibleProjection(
for (const EHyperTwistVirtual3333Axis DisplayAxis : Result.Projection.DisplayAxes) for (const EHyperTwistVirtual3333Axis DisplayAxis : Result.Projection.DisplayAxes)
{ {
Tesseract.VisibleCells.Add( Tesseract.VisibleCells.Add(
BuildProjectedCell(PieceHomeCoordinate, Orientation, DisplayAxis) BuildProjectedCell(PieceHomeCoordinate, Orientation, DisplayAxis, Order)
); );
} }
@ -750,7 +1046,7 @@ UHyperTwistVirtual3333ProjectionLibrary::BuildVisibleProjection(
Result.bExactProjection = Result.bProjected; Result.bExactProjection = Result.bProjected;
if (!Result.bProjected) if (!Result.bProjected)
{ {
Result.Warnings.Add(TEXT("virtual-3x3x3x3-projection-invalid")); Result.Warnings.Add(TEXT("virtual-nxnxnxn-projection-invalid"));
} }
return Result; return Result;

View file

@ -6,6 +6,7 @@
#include "GameFramework/PlayerController.h" #include "GameFramework/PlayerController.h"
#include "GameFramework/SpringArmComponent.h" #include "GameFramework/SpringArmComponent.h"
#include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionActor.h" #include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionActor.h"
#include "HyperTwistUX/HyperTwistPlayerControllerBase.h"
#include "InputCoreTypes.h" #include "InputCoreTypes.h"
AHyperTwistVirtual3333ProjectionOrbitPawn::AHyperTwistVirtual3333ProjectionOrbitPawn() AHyperTwistVirtual3333ProjectionOrbitPawn::AHyperTwistVirtual3333ProjectionOrbitPawn()
@ -62,6 +63,20 @@ void AHyperTwistVirtual3333ProjectionOrbitPawn::Tick(const float DeltaSeconds)
{ {
return; return;
} }
const AHyperTwistPlayerControllerBase* HyperTwistController =
Cast<AHyperTwistPlayerControllerBase>(PlayerController);
const FHyperTwistPlayerPreferences* Preferences = HyperTwistController != nullptr
? &HyperTwistController->GetPlayerPreferences()
: nullptr;
const float ZoomMultiplier = Preferences != nullptr
? Preferences->ZoomSensitivity
: 1.0f;
const float OrbitMultiplier = Preferences != nullptr
? Preferences->OrbitSensitivity
: 1.0f;
const float VerticalDirection = Preferences != nullptr && Preferences->bInvertOrbitY
? -1.0f
: 1.0f;
if (SpringArm != nullptr) if (SpringArm != nullptr)
{ {
@ -69,7 +84,7 @@ void AHyperTwistVirtual3333ProjectionOrbitPawn::Tick(const float DeltaSeconds)
if (!FMath::IsNearlyZero(MouseWheelDelta)) if (!FMath::IsNearlyZero(MouseWheelDelta))
{ {
SpringArm->TargetArmLength = FMath::Clamp( SpringArm->TargetArmLength = FMath::Clamp(
SpringArm->TargetArmLength - (MouseWheelDelta * ZoomStep), SpringArm->TargetArmLength - (MouseWheelDelta * ZoomStep * ZoomMultiplier),
MinimumArmLength, MinimumArmLength,
MaximumArmLength MaximumArmLength
); );
@ -86,9 +101,10 @@ void AHyperTwistVirtual3333ProjectionOrbitPawn::Tick(const float DeltaSeconds)
PlayerController->GetInputMouseDelta(MouseDeltaX, MouseDeltaY); PlayerController->GetInputMouseDelta(MouseDeltaX, MouseDeltaY);
if (!FMath::IsNearlyZero(MouseDeltaX) || !FMath::IsNearlyZero(MouseDeltaY)) if (!FMath::IsNearlyZero(MouseDeltaX) || !FMath::IsNearlyZero(MouseDeltaY))
{ {
CurrentYawDegrees += MouseDeltaX * OrbitYawDegreesPerPixel; CurrentYawDegrees += MouseDeltaX * OrbitYawDegreesPerPixel * OrbitMultiplier;
CurrentPitchDegrees = FMath::Clamp( CurrentPitchDegrees = FMath::Clamp(
CurrentPitchDegrees - (MouseDeltaY * OrbitPitchDegreesPerPixel), CurrentPitchDegrees
- (MouseDeltaY * OrbitPitchDegreesPerPixel * OrbitMultiplier * VerticalDirection),
MinimumPitchDegrees, MinimumPitchDegrees,
MaximumPitchDegrees MaximumPitchDegrees
); );

View file

@ -2,6 +2,7 @@
#include "EngineUtils.h" #include "EngineUtils.h"
#include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionActor.h" #include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionActor.h"
#include "HyperTwistUX/HyperTwistFourDimensionalHUDWidget.h"
#include "InputCoreTypes.h" #include "InputCoreTypes.h"
AHyperTwistVirtual3333ProjectionPlayerController::AHyperTwistVirtual3333ProjectionPlayerController() AHyperTwistVirtual3333ProjectionPlayerController::AHyperTwistVirtual3333ProjectionPlayerController()
@ -15,6 +16,148 @@ void AHyperTwistVirtual3333ProjectionPlayerController::BeginPlay()
{ {
Super::BeginPlay(); Super::BeginPlay();
ApplyInputMode(); ApplyInputMode();
ShowPuzzleHud();
}
FString AHyperTwistVirtual3333ProjectionPlayerController::GetPauseMenuTitle() const
{
const AHyperTwistVirtual3333ProjectionActor* ProjectionActor =
ResolveProjectionActor();
const int32 Order = ProjectionActor != nullptr
? ProjectionActor->PuzzleOrder
: 3;
return FString::Printf(
TEXT("4D Cube %dx%dx%dx%d"),
Order,
Order,
Order,
Order);
}
FString AHyperTwistVirtual3333ProjectionPlayerController::GetPauseMenuSubtitle() const
{
if (const AHyperTwistVirtual3333ProjectionActor* ProjectionActor =
ResolveProjectionActor())
{
return FString::Printf(
TEXT("%s | %s"),
*ProjectionActor->GetSelectionLabel(),
ProjectionActor->CurrentState.bIsSolved
? TEXT("solved")
: *ProjectionActor->LastAppliedNotation);
}
return TEXT("Exact-state visible-slice projection");
}
bool AHyperTwistVirtual3333ProjectionPlayerController::ShowPuzzleHud()
{
if (!bShowPuzzleHud)
{
return false;
}
if (ActivePuzzleHudWidget == nullptr)
{
TSubclassOf<UHyperTwistFourDimensionalHUDWidget> ResolvedClass =
PuzzleHudWidgetClass;
if (*ResolvedClass == nullptr)
{
ResolvedClass = UHyperTwistFourDimensionalHUDWidget::StaticClass();
}
ActivePuzzleHudWidget =
CreateWidget<UHyperTwistFourDimensionalHUDWidget>(
this,
ResolvedClass);
if (ActivePuzzleHudWidget == nullptr
|| !ActivePuzzleHudWidget->PrepareHudSurface())
{
ActivePuzzleHudWidget = nullptr;
return false;
}
ActivePuzzleHudWidget->OnCycleAxisRequested.AddDynamic(
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleHudCycleAxis);
ActivePuzzleHudWidget->OnPreviousLayerRequested.AddDynamic(
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleHudPreviousLayer);
ActivePuzzleHudWidget->OnNextLayerRequested.AddDynamic(
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleHudNextLayer);
ActivePuzzleHudWidget->OnPreviousPlaneRequested.AddDynamic(
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleHudPreviousPlane);
ActivePuzzleHudWidget->OnNextPlaneRequested.AddDynamic(
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleHudNextPlane);
ActivePuzzleHudWidget->OnCounterClockwiseRequested.AddDynamic(
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleHudCounterClockwise);
ActivePuzzleHudWidget->OnClockwiseRequested.AddDynamic(
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleHudClockwise);
ActivePuzzleHudWidget->OnToggleShellRequested.AddDynamic(
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleHudToggleShell);
ActivePuzzleHudWidget->OnScrambleRequested.AddDynamic(
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleHudScramble);
ActivePuzzleHudWidget->OnResetRequested.AddDynamic(
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleHudReset);
ActivePuzzleHudWidget->OnSaveRequested.AddDynamic(
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleHudSave);
ActivePuzzleHudWidget->OnLoadRequested.AddDynamic(
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleHudLoad);
ActivePuzzleHudWidget->AddToViewport(PuzzleHudZOrder);
}
else if (!ActivePuzzleHudWidget->IsInViewport())
{
ActivePuzzleHudWidget->AddToViewport(PuzzleHudZOrder);
}
RefreshPuzzleHud();
return true;
}
void AHyperTwistVirtual3333ProjectionPlayerController::RefreshPuzzleHud()
{
if (ActivePuzzleHudWidget == nullptr)
{
return;
}
const AHyperTwistVirtual3333ProjectionActor* ProjectionActor =
ResolveProjectionActor();
if (ProjectionActor == nullptr)
{
ActivePuzzleHudWidget->ConfigureSurface(
TEXT("4D Cube"),
TEXT("WAITING FOR RUNTIME"),
TEXT("Projection actor is not ready."),
TEXT("waiting"),
TEXT("not loaded"),
false);
return;
}
const int32 Order = ProjectionActor->PuzzleOrder;
const int32 PieceCount = Order * Order * Order * Order;
const int32 VisiblePieceCount = Order * Order * Order;
ActivePuzzleHudWidget->ConfigureSurface(
FString::Printf(
TEXT("4D Cube %dx%dx%dx%d"),
Order,
Order,
Order,
Order),
FString::Printf(
TEXT("%s | %d exact pieces | %d visible"),
ProjectionActor->CurrentState.bIsSolved ? TEXT("SOLVED") : TEXT("ACTIVE"),
PieceCount,
VisiblePieceCount),
ProjectionActor->GetSelectionLabel(),
ProjectionActor->LastAppliedNotation,
ProjectionActor->LastPersistenceStatus,
false);
} }
void AHyperTwistVirtual3333ProjectionPlayerController::SetupInputComponent() void AHyperTwistVirtual3333ProjectionPlayerController::SetupInputComponent()
@ -68,6 +211,36 @@ void AHyperTwistVirtual3333ProjectionPlayerController::SetupInputComponent()
this, this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleSliceCoordinatePositive &AHyperTwistVirtual3333ProjectionPlayerController::HandleSliceCoordinatePositive
); );
InputComponent->BindKey(
EKeys::Four,
IE_Pressed,
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleSliceLayerFour
);
InputComponent->BindKey(
EKeys::Five,
IE_Pressed,
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleSliceLayerFive
);
InputComponent->BindKey(
EKeys::Six,
IE_Pressed,
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleSliceLayerSix
);
InputComponent->BindKey(
EKeys::LeftBracket,
IE_Pressed,
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandlePreviousSliceLayer
);
InputComponent->BindKey(
EKeys::RightBracket,
IE_Pressed,
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleNextSliceLayer
);
InputComponent->BindKey( InputComponent->BindKey(
EKeys::Q, EKeys::Q,
@ -101,12 +274,30 @@ void AHyperTwistVirtual3333ProjectionPlayerController::SetupInputComponent()
this, this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleResetShortcut &AHyperTwistVirtual3333ProjectionPlayerController::HandleResetShortcut
); );
InputComponent->BindKey(
EKeys::G,
IE_Pressed,
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleScrambleShortcut
);
InputComponent->BindKey( InputComponent->BindKey(
EKeys::T, EKeys::T,
IE_Pressed, IE_Pressed,
this, this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleToggleShellShortcut &AHyperTwistVirtual3333ProjectionPlayerController::HandleToggleShellShortcut
); );
InputComponent->BindKey(
EKeys::S,
IE_Pressed,
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleSaveShortcut
);
InputComponent->BindKey(
EKeys::L,
IE_Pressed,
this,
&AHyperTwistVirtual3333ProjectionPlayerController::HandleLoadShortcut
);
} }
void AHyperTwistVirtual3333ProjectionPlayerController::ResetProjectionToSolved() void AHyperTwistVirtual3333ProjectionPlayerController::ResetProjectionToSolved()
@ -115,13 +306,16 @@ void AHyperTwistVirtual3333ProjectionPlayerController::ResetProjectionToSolved()
{ {
ProjectionActor->ResetToSolvedState(); ProjectionActor->ResetToSolvedState();
} }
RefreshPuzzleHud();
} }
bool AHyperTwistVirtual3333ProjectionPlayerController::ApplySelectionClockwise() bool AHyperTwistVirtual3333ProjectionPlayerController::ApplySelectionClockwise()
{ {
if (AHyperTwistVirtual3333ProjectionActor* ProjectionActor = ResolveProjectionActor()) if (AHyperTwistVirtual3333ProjectionActor* ProjectionActor = ResolveProjectionActor())
{ {
return ProjectionActor->ApplyCurrentSelectionClockwise(); const bool bApplied = ProjectionActor->ApplyCurrentSelectionClockwise();
RefreshPuzzleHud();
return bApplied;
} }
return false; return false;
@ -131,7 +325,9 @@ bool AHyperTwistVirtual3333ProjectionPlayerController::ApplySelectionCounterCloc
{ {
if (AHyperTwistVirtual3333ProjectionActor* ProjectionActor = ResolveProjectionActor()) if (AHyperTwistVirtual3333ProjectionActor* ProjectionActor = ResolveProjectionActor())
{ {
return ProjectionActor->ApplyCurrentSelectionCounterClockwise(); const bool bApplied = ProjectionActor->ApplyCurrentSelectionCounterClockwise();
RefreshPuzzleHud();
return bApplied;
} }
return false; return false;
@ -143,7 +339,9 @@ bool AHyperTwistVirtual3333ProjectionPlayerController::SetSliceAxis(
{ {
if (AHyperTwistVirtual3333ProjectionActor* ProjectionActor = ResolveProjectionActor()) if (AHyperTwistVirtual3333ProjectionActor* ProjectionActor = ResolveProjectionActor())
{ {
return ProjectionActor->SetSliceAxis(SliceAxis); const bool bSet = ProjectionActor->SetSliceAxis(SliceAxis);
RefreshPuzzleHud();
return bSet;
} }
return false; return false;
@ -155,19 +353,49 @@ bool AHyperTwistVirtual3333ProjectionPlayerController::SetSliceCoordinate(
{ {
if (AHyperTwistVirtual3333ProjectionActor* ProjectionActor = ResolveProjectionActor()) if (AHyperTwistVirtual3333ProjectionActor* ProjectionActor = ResolveProjectionActor())
{ {
return ProjectionActor->SetSliceCoordinate(SliceCoordinate); const bool bSet = ProjectionActor->SetSliceCoordinate(SliceCoordinate);
RefreshPuzzleHud();
return bSet;
} }
return false; return false;
} }
bool AHyperTwistVirtual3333ProjectionPlayerController::SetSliceLayer(
const int32 OneBasedLayer
)
{
if (AHyperTwistVirtual3333ProjectionActor* ProjectionActor = ResolveProjectionActor())
{
const bool bSet = ProjectionActor->SetSliceLayer(OneBasedLayer);
RefreshPuzzleHud();
return bSet;
}
return false;
}
bool AHyperTwistVirtual3333ProjectionPlayerController::CycleSliceLayer(
const int32 DirectionStep
)
{
if (AHyperTwistVirtual3333ProjectionActor* ProjectionActor = ResolveProjectionActor())
{
const bool bCycled = ProjectionActor->CycleSliceCoordinate(DirectionStep);
RefreshPuzzleHud();
return bCycled;
}
return false;
}
bool AHyperTwistVirtual3333ProjectionPlayerController::CycleRotationAxis( bool AHyperTwistVirtual3333ProjectionPlayerController::CycleRotationAxis(
const int32 DirectionStep const int32 DirectionStep
) )
{ {
if (AHyperTwistVirtual3333ProjectionActor* ProjectionActor = ResolveProjectionActor()) if (AHyperTwistVirtual3333ProjectionActor* ProjectionActor = ResolveProjectionActor())
{ {
return ProjectionActor->CycleRotationAxis(DirectionStep); const bool bCycled = ProjectionActor->CycleRotationAxis(DirectionStep);
RefreshPuzzleHud();
return bCycled;
} }
return false; return false;
@ -179,18 +407,14 @@ void AHyperTwistVirtual3333ProjectionPlayerController::ToggleCellShellRendering(
{ {
ProjectionActor->ToggleCellShellRendering(); ProjectionActor->ToggleCellShellRendering();
} }
RefreshPuzzleHud();
} }
void AHyperTwistVirtual3333ProjectionPlayerController::ApplyInputMode() void AHyperTwistVirtual3333ProjectionPlayerController::ApplyInputMode()
{ {
bShowMouseCursor = true;
if (bUseGameAndUiInputMode) if (bUseGameAndUiInputMode)
{ {
FInputModeGameAndUI InputMode; ApplyGameAndUiInputMode();
InputMode.SetHideCursorDuringCapture(false);
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
SetInputMode(InputMode);
} }
} }
@ -232,17 +456,42 @@ void AHyperTwistVirtual3333ProjectionPlayerController::HandleSliceAxisW()
void AHyperTwistVirtual3333ProjectionPlayerController::HandleSliceCoordinateNegative() void AHyperTwistVirtual3333ProjectionPlayerController::HandleSliceCoordinateNegative()
{ {
SetSliceCoordinate(-1); SetSliceLayer(1);
} }
void AHyperTwistVirtual3333ProjectionPlayerController::HandleSliceCoordinateMiddle() void AHyperTwistVirtual3333ProjectionPlayerController::HandleSliceCoordinateMiddle()
{ {
SetSliceCoordinate(0); SetSliceLayer(2);
} }
void AHyperTwistVirtual3333ProjectionPlayerController::HandleSliceCoordinatePositive() void AHyperTwistVirtual3333ProjectionPlayerController::HandleSliceCoordinatePositive()
{ {
SetSliceCoordinate(1); SetSliceLayer(3);
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandleSliceLayerFour()
{
SetSliceLayer(4);
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandleSliceLayerFive()
{
SetSliceLayer(5);
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandleSliceLayerSix()
{
SetSliceLayer(6);
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandlePreviousSliceLayer()
{
CycleSliceLayer(-1);
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandleNextSliceLayer()
{
CycleSliceLayer(1);
} }
void AHyperTwistVirtual3333ProjectionPlayerController::HandleCycleRotationAxisBackward() void AHyperTwistVirtual3333ProjectionPlayerController::HandleCycleRotationAxisBackward()
@ -270,7 +519,103 @@ void AHyperTwistVirtual3333ProjectionPlayerController::HandleResetShortcut()
ResetProjectionToSolved(); ResetProjectionToSolved();
} }
void AHyperTwistVirtual3333ProjectionPlayerController::HandleScrambleShortcut()
{
if (AHyperTwistVirtual3333ProjectionActor* ProjectionActor =
ResolveProjectionActor())
{
ProjectionActor->GenerateScramble(
FMath::Clamp(ScrambleMoveCount, 1, 500),
NextScrambleSeed++);
}
RefreshPuzzleHud();
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandleToggleShellShortcut() void AHyperTwistVirtual3333ProjectionPlayerController::HandleToggleShellShortcut()
{ {
ToggleCellShellRendering(); ToggleCellShellRendering();
} }
void AHyperTwistVirtual3333ProjectionPlayerController::HandleSaveShortcut()
{
if (AHyperTwistVirtual3333ProjectionActor* ProjectionActor = ResolveProjectionActor())
{
ProjectionActor->SaveRuntimeState();
}
RefreshPuzzleHud();
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandleLoadShortcut()
{
if (AHyperTwistVirtual3333ProjectionActor* ProjectionActor = ResolveProjectionActor())
{
ProjectionActor->LoadRuntimeState();
}
RefreshPuzzleHud();
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandleHudCycleAxis()
{
if (const AHyperTwistVirtual3333ProjectionActor* ProjectionActor =
ResolveProjectionActor())
{
const int32 NextAxis =
(static_cast<int32>(ProjectionActor->CurrentSliceAxis) + 1) % 4;
SetSliceAxis(static_cast<EHyperTwistVirtual3333Axis>(NextAxis));
}
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandleHudPreviousLayer()
{
CycleSliceLayer(-1);
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandleHudNextLayer()
{
CycleSliceLayer(1);
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandleHudPreviousPlane()
{
CycleRotationAxis(-1);
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandleHudNextPlane()
{
CycleRotationAxis(1);
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandleHudCounterClockwise()
{
ApplySelectionCounterClockwise();
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandleHudClockwise()
{
ApplySelectionClockwise();
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandleHudToggleShell()
{
ToggleCellShellRendering();
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandleHudScramble()
{
HandleScrambleShortcut();
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandleHudReset()
{
ResetProjectionToSolved();
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandleHudSave()
{
HandleSaveShortcut();
}
void AHyperTwistVirtual3333ProjectionPlayerController::HandleHudLoad()
{
HandleLoadShortcut();
}

View file

@ -59,10 +59,7 @@ void AHyperTwistCoachDashboardPlayerController::ApplyDashboardInputMode()
if (bUseGameAndUiInputMode) if (bUseGameAndUiInputMode)
{ {
FInputModeGameAndUI InputMode; ApplyGameAndUiInputMode();
InputMode.SetHideCursorDuringCapture(false);
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
SetInputMode(InputMode);
} }
} }

View file

@ -5,7 +5,8 @@
namespace HyperTwistFirstRunLaunchLibraryInternal namespace HyperTwistFirstRunLaunchLibraryInternal
{ {
const TCHAR* SurfaceId = TEXT("first-run/native-launch-and-settings-surface"); const TCHAR* SurfaceId = TEXT("first-run/native-launch-and-settings-surface");
const TCHAR* DefaultRouteId = TEXT("coach-dashboard"); const TCHAR* DefaultRouteId = TEXT("classic-cube-training");
const TCHAR* CoachDashboardRouteId = TEXT("coach-dashboard");
const TCHAR* StartupFallbackRouteId = TEXT("classic-cube-training"); const TCHAR* StartupFallbackRouteId = TEXT("classic-cube-training");
const TCHAR* PackagedStartupRouteArgumentName = TEXT("HyperTwistStartupRoute"); const TCHAR* PackagedStartupRouteArgumentName = TEXT("HyperTwistStartupRoute");
const TCHAR* FirstRunGameModeClassPath = const TCHAR* FirstRunGameModeClassPath =
@ -18,12 +19,32 @@ namespace HyperTwistFirstRunLaunchLibraryInternal
TEXT("/Script/UnrealHyperTwist.HyperTwistClassicCubeFollowAlongGameMode"); TEXT("/Script/UnrealHyperTwist.HyperTwistClassicCubeFollowAlongGameMode");
const TCHAR* HigherDimensionalTrainingGameModeClassPath = const TCHAR* HigherDimensionalTrainingGameModeClassPath =
TEXT("/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode"); TEXT("/Script/UnrealHyperTwist.HyperTwistHigherDimensionalTrainingGameMode");
const TCHAR* MelindaProjectionGameModeClassPath =
TEXT("/Script/UnrealHyperTwist.HyperTwistMelindaProjectionGameMode");
const TCHAR* Virtual3333ProjectionGameModeClassPath =
TEXT("/Script/UnrealHyperTwist.HyperTwistVirtual3333ProjectionGameMode");
const TCHAR* Virtual4444ProjectionGameModeClassPath =
TEXT("/Script/UnrealHyperTwist.HyperTwistVirtual4444ProjectionGameMode");
const TCHAR* Virtual5555ProjectionGameModeClassPath =
TEXT("/Script/UnrealHyperTwist.HyperTwistVirtual5555ProjectionGameMode");
const TCHAR* Virtual6666ProjectionGameModeClassPath =
TEXT("/Script/UnrealHyperTwist.HyperTwistVirtual6666ProjectionGameMode");
const TCHAR* XrTrainingGameModeClassPath = const TCHAR* XrTrainingGameModeClassPath =
TEXT("/Script/UnrealHyperTwist.HyperTwistXrTrainingGameMode"); TEXT("/Script/UnrealHyperTwist.HyperTwistXrTrainingGameMode");
const TCHAR* ClassicCubeMapPath = const TCHAR* ClassicCubeMapPath =
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining"); TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining");
const TCHAR* FollowAlongMapPath = const TCHAR* FollowAlongMapPath =
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining"); TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining");
const TCHAR* MelindaProjectionMapPath =
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_2x2x2x2Training");
const TCHAR* Virtual3333ProjectionMapPath =
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_3x3x3x3Training");
const TCHAR* Virtual4444ProjectionMapPath =
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_4x4x4x4Training");
const TCHAR* Virtual5555ProjectionMapPath =
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_5x5x5x5Training");
const TCHAR* Virtual6666ProjectionMapPath =
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_6x6x6x6Training");
const TCHAR* Magic120CellMapPath = const TCHAR* Magic120CellMapPath =
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining"); TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining");
const TCHAR* MagicCube5DMapPath = const TCHAR* MagicCube5DMapPath =
@ -54,13 +75,16 @@ namespace HyperTwistFirstRunLaunchLibraryInternal
const FString& GameModeClassPath, const FString& GameModeClassPath,
const FString& PrimaryInputMode, const FString& PrimaryInputMode,
const FString& RuntimeProofStatus, const FString& RuntimeProofStatus,
const FString& PlayerSection,
const FString& AvailabilityLabel,
const bool bDefaultSafeChoice, const bool bDefaultSafeChoice,
const bool bOpensDashboard, const bool bOpensDashboard,
const bool bLaunchesDedicatedMap, const bool bLaunchesDedicatedMap,
const bool bRequiresEntitlement, const bool bRequiresEntitlement,
const bool bRequiresLiveHeadset, const bool bRequiresLiveHeadset,
const bool bRequiresLiveControllerObservation, const bool bRequiresLiveControllerObservation,
const bool bHasWebSimulatorEquivalent const bool bHasWebSimulatorEquivalent,
const bool bPlayerFacing = true
) )
{ {
FHyperTwistFirstRunLaunchRoute Route; FHyperTwistFirstRunLaunchRoute Route;
@ -72,6 +96,8 @@ namespace HyperTwistFirstRunLaunchLibraryInternal
Route.LaunchUrl = BuildLaunchUrl(MapAssetPath, GameModeClassPath); Route.LaunchUrl = BuildLaunchUrl(MapAssetPath, GameModeClassPath);
Route.PrimaryInputMode = PrimaryInputMode; Route.PrimaryInputMode = PrimaryInputMode;
Route.RuntimeProofStatus = RuntimeProofStatus; Route.RuntimeProofStatus = RuntimeProofStatus;
Route.PlayerSection = PlayerSection;
Route.AvailabilityLabel = AvailabilityLabel;
Route.bDefaultSafeChoice = bDefaultSafeChoice; Route.bDefaultSafeChoice = bDefaultSafeChoice;
Route.bOpensDashboard = bOpensDashboard; Route.bOpensDashboard = bOpensDashboard;
Route.bLaunchesDedicatedMap = bLaunchesDedicatedMap; Route.bLaunchesDedicatedMap = bLaunchesDedicatedMap;
@ -79,8 +105,59 @@ namespace HyperTwistFirstRunLaunchLibraryInternal
Route.bRequiresLiveHeadset = bRequiresLiveHeadset; Route.bRequiresLiveHeadset = bRequiresLiveHeadset;
Route.bRequiresLiveControllerObservation = bRequiresLiveControllerObservation; Route.bRequiresLiveControllerObservation = bRequiresLiveControllerObservation;
Route.bHasWebSimulatorEquivalent = bHasWebSimulatorEquivalent; Route.bHasWebSimulatorEquivalent = bHasWebSimulatorEquivalent;
Route.bPlayerFacing = bPlayerFacing;
return Route; return Route;
} }
FHyperTwistPuzzleCatalogEntry MakeCatalogEntry(
const FString& PuzzleId,
const FString& Title,
const FString& Dimensionality,
const FString& Description,
const EHyperTwistPuzzleAvailability Availability,
const FString& AvailabilityLabel,
const FString& LaunchRouteId,
const TArray<FString>& ExperienceTags
)
{
FHyperTwistPuzzleCatalogEntry Entry;
Entry.PuzzleId = PuzzleId;
Entry.Title = Title;
Entry.Dimensionality = Dimensionality;
Entry.Description = Description;
Entry.Availability = Availability;
Entry.AvailabilityLabel = AvailabilityLabel;
Entry.LaunchRouteId = LaunchRouteId;
Entry.ExperienceTags = ExperienceTags;
return Entry;
}
FHyperTwistIntegratedCapability MakeIntegratedCapability(
const FString& CapabilityId,
const FString& Title,
const FString& Summary,
const FString& CanonicalRepository,
const FString& LicenseId,
const EHyperTwistIntegrationLicenseLane LicenseLane,
const EHyperTwistIntegrationSurface Surface,
const FString& PrimaryExperienceRoute,
const FString& RuntimeOwnerId,
const FString& ImplementationPosture
)
{
FHyperTwistIntegratedCapability Capability;
Capability.CapabilityId = CapabilityId;
Capability.Title = Title;
Capability.Summary = Summary;
Capability.CanonicalRepository = CanonicalRepository;
Capability.LicenseId = LicenseId;
Capability.LicenseLane = LicenseLane;
Capability.Surface = Surface;
Capability.PrimaryExperienceRoute = PrimaryExperienceRoute;
Capability.RuntimeOwnerId = RuntimeOwnerId;
Capability.ImplementationPosture = ImplementationPosture;
return Capability;
}
} }
bool FHyperTwistFirstRunLaunchRoute::IsStructurallyValid() const bool FHyperTwistFirstRunLaunchRoute::IsStructurallyValid() const
@ -90,6 +167,8 @@ bool FHyperTwistFirstRunLaunchRoute::IsStructurallyValid() const
&& !Description.IsEmpty() && !Description.IsEmpty()
&& !PrimaryInputMode.IsEmpty() && !PrimaryInputMode.IsEmpty()
&& !RuntimeProofStatus.IsEmpty() && !RuntimeProofStatus.IsEmpty()
&& !PlayerSection.IsEmpty()
&& !AvailabilityLabel.IsEmpty()
&& (!bOpensDashboard || !GameModeClassPath.IsEmpty()) && (!bOpensDashboard || !GameModeClassPath.IsEmpty())
&& (!bLaunchesDedicatedMap || (!MapAssetPath.IsEmpty() && !LaunchUrl.IsEmpty())) && (!bLaunchesDedicatedMap || (!MapAssetPath.IsEmpty() && !LaunchUrl.IsEmpty()))
&& (!bRequiresLiveControllerObservation || bRequiresLiveHeadset); && (!bRequiresLiveControllerObservation || bRequiresLiveHeadset);
@ -138,13 +217,14 @@ FString UHyperTwistFirstRunLaunchLibrary::GetStartupDiagnosticsLogFileName()
TArray<FString> UHyperTwistFirstRunLaunchLibrary::BuildFirstRunGuidanceLines() TArray<FString> UHyperTwistFirstRunLaunchLibrary::BuildFirstRunGuidanceLines()
{ {
return { return {
TEXT("Desktop keyboard and mouse is the primary day-one development and training path; it is available without a headset."), TEXT("Start with Classic Cube, learn through Follow-Along, or move into 4D, the 120-cell, and 5D at your own pace."),
TEXT("Coach Dashboard opens local settings, control status, training diagnostics, browser shell status, and operator surfaces."), TEXT("Keyboard and mouse is the complete primary desktop path; a headset is never required for the puzzle library."),
TEXT("Classic cube, follow-along, Magic120Cell, and MagicCube5D launch into dedicated first-party Unreal maps."), TEXT("Open Settings at any time to choose controls, audio, graphics, accessibility, speech, AI providers, and account options."),
TEXT("XR launches through the OpenXR training game-mode override and remains gated until a live headset session plus controller input are observed."), TEXT("Press Esc inside a puzzle to pause, review controls, change settings, or return safely to this menu."),
TEXT("The web simulator is a lightweight preview and account surface; the downloadable Unreal build is the authoritative high-fidelity simulator."), TEXT("XR is an optional validation route; full readiness requires a live headset session with observed OpenXR controller input."),
TEXT("If first launch stalls, inspect Saved/Logs/HyperTwistRuntime-latest.log and Saved/Logs/HyperTwistFirstRunLaunch-latest.log."), TEXT("The browser experience is a lightweight preview and account companion; the downloadable Unreal build is the authoritative full simulator."),
TEXT("If the launch menu cannot be shown, HyperTwist automatically falls back into Classic Cube Free Play instead of remaining on a blank boot map.") TEXT("Runtime diagnostics are written to Saved/Logs/HyperTwistRuntime-latest.log and startup recovery details to Saved/Logs/HyperTwistFirstRunLaunch-latest.log."),
TEXT("If the launch menu cannot render safely, HyperTwist automatically falls back into Classic Cube Free Play instead of leaving a blank scene.")
}; };
} }
@ -155,20 +235,23 @@ UHyperTwistFirstRunLaunchLibrary::BuildFirstRunLaunchRoutes()
return { return {
MakeRoute( MakeRoute(
DefaultRouteId, CoachDashboardRouteId,
TEXT("Open Coach Dashboard and Settings"), TEXT("Open Coach Dashboard and Settings"),
TEXT("Start with the operator dashboard, control/status surfaces, browser shell bridge, and local training settings."), TEXT("Start with the operator dashboard, control/status surfaces, browser shell bridge, and local training settings."),
FString(), FString(),
CoachDashboardGameModeClassPath, CoachDashboardGameModeClassPath,
TEXT("keyboard-mouse-ui"), TEXT("keyboard-mouse-ui"),
TEXT("ready-local-dashboard"), TEXT("ready-local-dashboard"),
true, TEXT("advanced"),
TEXT("Advanced"),
false,
true, true,
false, false,
false, false,
false, false,
false, false,
true true,
false
), ),
MakeRoute( MakeRoute(
TEXT("classic-cube-training"), TEXT("classic-cube-training"),
@ -178,7 +261,9 @@ UHyperTwistFirstRunLaunchLibrary::BuildFirstRunLaunchRoutes()
ClassicCubeGameModeClassPath, ClassicCubeGameModeClassPath,
TEXT("keyboard-mouse-gameplay"), TEXT("keyboard-mouse-gameplay"),
TEXT("ready-dedicated-map"), TEXT("ready-dedicated-map"),
false, TEXT("play"),
TEXT("Playable"),
true,
false, false,
true, true,
false, false,
@ -194,6 +279,98 @@ UHyperTwistFirstRunLaunchLibrary::BuildFirstRunLaunchRoutes()
ClassicCubeFollowAlongGameModeClassPath, ClassicCubeFollowAlongGameModeClassPath,
TEXT("keyboard-mouse-guided-gameplay"), TEXT("keyboard-mouse-guided-gameplay"),
TEXT("ready-dedicated-map"), TEXT("ready-dedicated-map"),
TEXT("learn"),
TEXT("Guided"),
false,
false,
true,
false,
false,
false,
false
),
MakeRoute(
TEXT("magic-cube-4d-2x2x2x2"),
TEXT("4D Cube 2x2x2x2"),
TEXT("Enter a compact four-dimensional cube through a cell-first projection built for learning, inspection, and exact turns."),
MelindaProjectionMapPath,
MelindaProjectionGameModeClassPath,
TEXT("keyboard-mouse-4d-cell-projection"),
TEXT("ready-first-party-runtime"),
TEXT("play"),
TEXT("Playable"),
false,
false,
true,
false,
false,
false,
false
),
MakeRoute(
TEXT("magic-cube-4d-3x3x3x3"),
TEXT("4D Cube 3x3x3x3"),
TEXT("Explore a full 3x3x3x3 visible-slice runtime with selectable axes, layers, projection shells, and exact reversible turns."),
Virtual3333ProjectionMapPath,
Virtual3333ProjectionGameModeClassPath,
TEXT("keyboard-mouse-4d-visible-slice"),
TEXT("ready-first-party-runtime"),
TEXT("play"),
TEXT("Playable"),
false,
false,
true,
false,
false,
false,
false
),
MakeRoute(
TEXT("magic-cube-4d-4x4x4x4"),
TEXT("4D Cube 4x4x4x4"),
TEXT("Take the generalized exact-state 4D runtime into 256-piece territory with four selectable layers on every slice axis."),
Virtual4444ProjectionMapPath,
Virtual4444ProjectionGameModeClassPath,
TEXT("keyboard-mouse-4d-visible-slice"),
TEXT("ready-first-party-runtime"),
TEXT("play"),
TEXT("Playable"),
false,
false,
true,
false,
false,
false,
false
),
MakeRoute(
TEXT("magic-cube-4d-5x5x5x5"),
TEXT("4D Cube 5x5x5x5"),
TEXT("Explore a 625-piece four-dimensional cube with five exact layers per axis and a focused 125-piece visible slice."),
Virtual5555ProjectionMapPath,
Virtual5555ProjectionGameModeClassPath,
TEXT("keyboard-mouse-4d-visible-slice"),
TEXT("ready-first-party-runtime"),
TEXT("play"),
TEXT("Playable"),
false,
false,
true,
false,
false,
false,
false
),
MakeRoute(
TEXT("magic-cube-4d-6x6x6x6"),
TEXT("4D Cube 6x6x6x6"),
TEXT("Work at the largest retained Hyperspeedcube order in this launch set: 1,296 exact pieces and six selectable layers per axis."),
Virtual6666ProjectionMapPath,
Virtual6666ProjectionGameModeClassPath,
TEXT("keyboard-mouse-4d-visible-slice"),
TEXT("ready-first-party-runtime"),
TEXT("play"),
TEXT("Playable"),
false, false,
false, false,
true, true,
@ -210,6 +387,8 @@ UHyperTwistFirstRunLaunchLibrary::BuildFirstRunLaunchRoutes()
HigherDimensionalTrainingGameModeClassPath, HigherDimensionalTrainingGameModeClassPath,
TEXT("keyboard-mouse-higher-dimensional-training"), TEXT("keyboard-mouse-higher-dimensional-training"),
TEXT("ready-dedicated-family-map"), TEXT("ready-dedicated-family-map"),
TEXT("play"),
TEXT("Playable"),
false, false,
false, false,
true, true,
@ -226,6 +405,8 @@ UHyperTwistFirstRunLaunchLibrary::BuildFirstRunLaunchRoutes()
HigherDimensionalTrainingGameModeClassPath, HigherDimensionalTrainingGameModeClassPath,
TEXT("keyboard-mouse-higher-dimensional-training"), TEXT("keyboard-mouse-higher-dimensional-training"),
TEXT("ready-dedicated-family-map"), TEXT("ready-dedicated-family-map"),
TEXT("play"),
TEXT("Playable"),
false, false,
false, false,
true, true,
@ -242,17 +423,551 @@ UHyperTwistFirstRunLaunchLibrary::BuildFirstRunLaunchRoutes()
XrTrainingGameModeClassPath, XrTrainingGameModeClassPath,
TEXT("openxr-headset-and-controllers"), TEXT("openxr-headset-and-controllers"),
TEXT("pending-live-headset-controller-observation"), TEXT("pending-live-headset-controller-observation"),
TEXT("advanced"),
TEXT("Optional hardware check"),
false, false,
false, false,
true, true,
false, false,
true, true,
true, true,
false,
false false
) )
}; };
} }
TArray<FHyperTwistPuzzleCatalogEntry>
UHyperTwistFirstRunLaunchLibrary::BuildPlayerPuzzleCatalog()
{
using namespace HyperTwistFirstRunLaunchLibraryInternal;
return {
MakeCatalogEntry(
TEXT("cube/3x3x3"),
TEXT("Classic Cube"),
TEXT("3D"),
TEXT("A responsive 3x3x3 playground with physical picking, professional keyboard turns, timing, hints, replay, and guided practice."),
EHyperTwistPuzzleAvailability::Playable,
TEXT("Playable now"),
TEXT("classic-cube-training"),
{TEXT("free play"), TEXT("timed solves"), TEXT("guided learning")}
),
MakeCatalogEntry(
TEXT("hypercube/2x2x2x2"),
TEXT("4D Cube 2x2x2x2"),
TEXT("4D"),
TEXT("A compact tesseract puzzle presented through eight readable cells so four-dimensional turns remain learnable."),
EHyperTwistPuzzleAvailability::Playable,
TEXT("Playable now"),
TEXT("magic-cube-4d-2x2x2x2"),
{TEXT("cell-first projection"), TEXT("exact turns"), TEXT("teaching view")}
),
MakeCatalogEntry(
TEXT("hypercube/3x3x3x3"),
TEXT("4D Cube 3x3x3x3"),
TEXT("4D"),
TEXT("An 81-piece four-dimensional cube with visible-slice projection, axis selection, layer control, and optional shell context."),
EHyperTwistPuzzleAvailability::Playable,
TEXT("Playable now"),
TEXT("magic-cube-4d-3x3x3x3"),
{TEXT("visible slices"), TEXT("81 tesseracts"), TEXT("keyboard control")}
),
MakeCatalogEntry(
TEXT("hypercube/4x4x4x4"),
TEXT("4D Cube 4x4x4x4"),
TEXT("4D"),
TEXT("A 256-piece four-dimensional cube powered by the generalized exact-state visible-slice runtime, with four layers on every axis."),
EHyperTwistPuzzleAvailability::Playable,
TEXT("Playable now"),
TEXT("magic-cube-4d-4x4x4x4"),
{TEXT("visible slices"), TEXT("256 pieces"), TEXT("four layers")}
),
MakeCatalogEntry(
TEXT("hypercube/5x5x5x5"),
TEXT("4D Cube 5x5x5x5"),
TEXT("4D"),
TEXT("A 625-piece four-dimensional cube with five exact layers per axis and a focused 125-piece projection slice."),
EHyperTwistPuzzleAvailability::Playable,
TEXT("Playable now"),
TEXT("magic-cube-4d-5x5x5x5"),
{TEXT("visible slices"), TEXT("625 pieces"), TEXT("five layers")}
),
MakeCatalogEntry(
TEXT("hypercube/6x6x6x6"),
TEXT("4D Cube 6x6x6x6"),
TEXT("4D"),
TEXT("A 1,296-piece four-dimensional cube with six exact layers per axis and a focused 216-piece projection slice."),
EHyperTwistPuzzleAvailability::Playable,
TEXT("Playable now"),
TEXT("magic-cube-4d-6x6x6x6"),
{TEXT("visible slices"), TEXT("1,296 pieces"), TEXT("six layers")}
),
MakeCatalogEntry(
TEXT("polychoron/magic120cell"),
TEXT("Magic 120-cell"),
TEXT("4D polychoron"),
TEXT("Navigate all 120 dodecahedral cells through a family-owned projection, focus layers, persistence, and dedicated training scene."),
EHyperTwistPuzzleAvailability::Playable,
TEXT("Playable now"),
TEXT("magic-120-cell-training"),
{TEXT("120 cells"), TEXT("focus layers"), TEXT("save and resume")}
),
MakeCatalogEntry(
TEXT("hypercube/magiccube5d/order3"),
TEXT("5D Cube"),
TEXT("5D"),
TEXT("A five-dimensional order-3 runtime with projection layers, exact state ownership, and persistent sessions."),
EHyperTwistPuzzleAvailability::Playable,
TEXT("Playable now"),
TEXT("magic-cube-5d-training"),
{TEXT("five dimensions"), TEXT("projection layers"), TEXT("save and resume")}
),
MakeCatalogEntry(
TEXT("tiling/magictile"),
TEXT("MagicTile Lab"),
TEXT("Spherical and hyperbolic"),
TEXT("Experiment with non-Euclidean tiling concepts through the embedded browser preview while its native renderer remains intentionally gated."),
EHyperTwistPuzzleAvailability::BrowserPreview,
TEXT("Browser preview"),
FString(),
{TEXT("non-Euclidean"), TEXT("embedded preview"), TEXT("state bridge")}
)
};
}
TArray<FHyperTwistIntegratedCapability>
UHyperTwistFirstRunLaunchLibrary::BuildIntegratedCapabilityCatalog()
{
using enum EHyperTwistIntegrationLicenseLane;
using enum EHyperTwistIntegrationSurface;
using namespace HyperTwistFirstRunLaunchLibraryInternal;
return {
MakeIntegratedCapability(
TEXT("timer/local-solve-lifecycle"),
TEXT("Solve timer and session statistics"),
TEXT("Inspection, solve timing, splits, rolling averages, personal bests, local persistence, and timer-scoped replay."),
TEXT("Aarav2709/KubeTimr"),
TEXT("MIT"),
Permissive,
NativePlayerService,
TEXT("classic-cube-training"),
TEXT("UHyperTwistTrainingSubsystem.LiveTimer"),
TEXT("permissive-first-party-owner")
),
MakeIntegratedCapability(
TEXT("learning/advanced-five-style"),
TEXT("Advanced 5-style drills"),
TEXT("Focused edge-cycle and blindfold micro-drills with reveal, self-grading, timing, and review scheduling."),
TEXT("abunickabhi/5style-Trainer"),
TEXT("MIT"),
Permissive,
NativePlayerService,
TEXT("menu:learn"),
TEXT("UHyperTwistTrainingCatalogLibrary.5style-4mover"),
TEXT("permissive-first-party-owner")
),
MakeIntegratedCapability(
TEXT("browser/solve-analytics"),
TEXT("Browser solve analytics"),
TEXT("Solve-trend and progress visualization in the bounded browser companion runtime."),
TEXT("apache/echarts"),
TEXT("Apache-2.0"),
Permissive,
BrowserSupport,
TEXT("web:browser-access"),
TEXT("UHyperTwistTrainingAnalyticsLibrary"),
TEXT("permissive-browser-support")
),
MakeIntegratedCapability(
TEXT("browser/model-preview"),
TEXT("Standards-aware model preview"),
TEXT("A bounded glTF preview and quality-assurance surface for browser companion assets."),
TEXT("google/model-viewer"),
TEXT("Apache-2.0"),
Permissive,
BrowserSupport,
TEXT("web:browser-access"),
TEXT("UHyperTwistTrainingViewerLibrary"),
TEXT("permissive-browser-support")
),
MakeIntegratedCapability(
TEXT("puzzle/generalized-four-dimensional-cubes"),
TEXT("Generalized four-dimensional cubes"),
TEXT("Exact order-3 through order-6 state, notation, projection, replay verification, statistics, and puzzle-definition contracts."),
TEXT("HactarCE/Hyperspeedcube"),
TEXT("MIT OR Apache-2.0"),
Permissive,
NativePlayable,
TEXT("magic-cube-4d-3x3x3x3"),
TEXT("UHyperTwistVirtual3333ProjectionLibrary"),
TEXT("permissive-first-party-owner")
),
MakeIntegratedCapability(
TEXT("learning/higher-dimensional-knowledge"),
TEXT("Higher-dimensional learning library"),
TEXT("Curriculum, notation, puzzle taxonomy, progression stages, software references, and community context."),
TEXT("Hypercubers/hypercubing.xyz"),
TEXT("MIT"),
Permissive,
NativePlayerService,
TEXT("menu:learn"),
TEXT("UHyperTwistTrainingKnowledgeLibrary"),
TEXT("permissive-first-party-owner")
),
MakeIntegratedCapability(
TEXT("recognition/classic-cube-observation"),
TEXT("Cube recognition and calibration"),
TEXT("Face observation, webcam-shell calibration, multilingual guidance, presentation, and bounded font readiness."),
TEXT("kkoomen/qbr"),
TEXT("MIT"),
Permissive,
NativePlayerService,
TEXT("menu:settings"),
TEXT("UHyperTwistTrainingSubsystem.RecognitionSession"),
TEXT("permissive-first-party-owner")
),
MakeIntegratedCapability(
TEXT("learning/persisted-coaching"),
TEXT("Persisted and weighted coaching"),
TEXT("Weighted and recency-aware review, persisted training sessions, method exploration, and blindfold-oriented coaching."),
TEXT("Lykos/cube_trainer"),
TEXT("MIT"),
Permissive,
NativePlayerService,
TEXT("menu:learn"),
TEXT("UHyperTwistTrainingRepositoryLibrary"),
TEXT("permissive-first-party-owner")
),
MakeIntegratedCapability(
TEXT("coach/embodied-companion"),
TEXT("Narrated coach presentation"),
TEXT("Voice, subtitle, avatar-embed, and provider-boundary contracts beneath HyperTwist's fixed in-game coach."),
TEXT("met4citizen/TalkingHead"),
TEXT("MIT"),
Permissive,
NativePlayerService,
TEXT("menu:settings"),
TEXT("UHyperTwistTrainingCompanionLibrary"),
TEXT("permissive-first-party-owner")
),
MakeIntegratedCapability(
TEXT("browser/spatial-runtime"),
TEXT("Browser spatial runtime"),
TEXT("The browser companion's bounded three-dimensional rendering substrate."),
TEXT("mrdoob/three.js"),
TEXT("MIT"),
Permissive,
BrowserSupport,
TEXT("web:browser-access"),
TEXT("UHyperTwistTrainingSpatialLibrary.Three"),
TEXT("permissive-browser-support")
),
MakeIntegratedCapability(
TEXT("browser/spatial-composition"),
TEXT("Browser scene composition"),
TEXT("Declarative browser-side scene composition above the bounded spatial runtime."),
TEXT("pmndrs/react-three-fiber"),
TEXT("MIT"),
Permissive,
BrowserSupport,
TEXT("web:browser-access"),
TEXT("UHyperTwistTrainingSpatialLibrary.ReactThreeFiber"),
TEXT("permissive-browser-support")
),
MakeIntegratedCapability(
TEXT("browser/xr-support"),
TEXT("Optional browser XR support"),
TEXT("Browser-side XR session contracts kept separate from the authoritative Unreal OpenXR desktop path."),
TEXT("pmndrs/xr"),
TEXT("MIT"),
Permissive,
BrowserSupport,
TEXT("web:browser-access"),
TEXT("UHyperTwistTrainingSpatialLibrary.Xr"),
TEXT("permissive-browser-support")
),
MakeIntegratedCapability(
TEXT("puzzle/magic-120-cell"),
TEXT("Magic 120-cell"),
TEXT("A family-owned 7,560-facelet permutation state, legal turns, projection, focus, persistence, and dedicated scene."),
TEXT("roice3/Magic120Cell"),
TEXT("MIT"),
Permissive,
NativePlayable,
TEXT("magic-120-cell-training"),
TEXT("UHyperTwistMagic120CellRuntimeLibrary"),
TEXT("permissive-first-party-owner")
),
MakeIntegratedCapability(
TEXT("puzzle/magic-cube-5d"),
TEXT("Five-dimensional cube"),
TEXT("An exact order-3 five-dimensional state with 243 cubies, 810 facelets, legal turns, projection, and persistence."),
TEXT("roice3/MagicCube5D"),
TEXT("MIT"),
Permissive,
NativePlayable,
TEXT("magic-cube-5d-training"),
TEXT("UHyperTwistMagicCube5DRuntimeLibrary"),
TEXT("permissive-first-party-owner")
),
MakeIntegratedCapability(
TEXT("puzzle/non-euclidean-tiling-preview"),
TEXT("MagicTile browser lab"),
TEXT("Topology, macro remapping, state bridge, and browser preview while native renderer widening remains gated."),
TEXT("roice3/MagicTile"),
TEXT("MIT"),
Permissive,
BrowserSupport,
TEXT("web:browser-access"),
TEXT("UHyperTwistTrainingTilingLibrary"),
TEXT("permissive-browser-support")
),
MakeIntegratedCapability(
TEXT("learning/algorithm-foundation"),
TEXT("Algorithm training foundation"),
TEXT("Organized algorithm sets, prompts, scrambles, reveal flow, self-grading, and training-catalog composition."),
TEXT("tao-yu/Alg-Trainer"),
TEXT("MIT"),
Permissive,
NativePlayerService,
TEXT("menu:learn"),
TEXT("UHyperTwistTrainingCatalogLibrary.oll-t"),
TEXT("permissive-first-party-owner")
),
MakeIntegratedCapability(
TEXT("learning/cross-ladder"),
TEXT("Cross planning ladder"),
TEXT("Four-move cross-planning drills with setup, hidden answer, timing, self-grading, and review continuity."),
TEXT("newyork-anthonyng/rubiks-cross-trainer"),
TEXT("MIT"),
Permissive,
NativePlayerService,
TEXT("menu:learn"),
TEXT("UHyperTwistTrainingCatalogLibrary.cross-4move"),
TEXT("permissive-first-party-owner")
),
MakeIntegratedCapability(
TEXT("solver/classic-cube-reconstruction"),
TEXT("Classic cube solve guidance"),
TEXT("Committed-face reconstruction, correction flow, solve explanation, and bounded browser/webcam support."),
TEXT("vivaansinghvi07/rubix-cube-solver"),
TEXT("MIT"),
Permissive,
NativePlayerService,
TEXT("classic-cube-training"),
TEXT("UHyperTwistTrainingSubsystem.SolveExplanation"),
TEXT("permissive-first-party-owner")
),
MakeIntegratedCapability(
TEXT("learning/roux-method"),
TEXT("Roux method studio"),
TEXT("First-party Roux stages, blockbuilding, CMLL study, and review flow produced through the accepted clean-room chain."),
TEXT("onionhoney/roux-trainers"),
TEXT("GPL-3.0"),
RestrictiveCleanRoom,
NativePlayerService,
TEXT("menu:learn"),
TEXT("UHyperTwistTrainingCatalogLibrary.roux-cmll"),
TEXT("restrictive-clean-room-first-party")
),
MakeIntegratedCapability(
TEXT("algorithm/parser-and-interchange"),
TEXT("Algorithm parser and interchange"),
TEXT("First-party parser, AST, traversal, validation, keyboard mapping, serialization, and sharing semantics."),
TEXT("cubing/alg.js"),
TEXT("GPL-3.0-or-later"),
RestrictiveCleanRoom,
NativePlayerService,
TEXT("classic-cube-training"),
TEXT("UHyperTwistAlgorithmLibrary"),
TEXT("restrictive-clean-room-first-party")
),
MakeIntegratedCapability(
TEXT("replay/player-and-timeline"),
TEXT("Replay player and timeline"),
TEXT("First-party player shell, scrubber, cursor timeline, adapter boundary, and local visualization fallback."),
TEXT("cubing/twisty.js"),
TEXT("GPL-3.0-or-later"),
RestrictiveCleanRoom,
NativePlayerService,
TEXT("classic-cube-training"),
TEXT("UHyperTwistViewerLibrary"),
TEXT("restrictive-clean-room-first-party")
),
MakeIntegratedCapability(
TEXT("puzzle/four-dimensional-order-2"),
TEXT("Four-dimensional 2x2x2x2"),
TEXT("First-party legality, random-state generation, move algebra, exact turns, scramble packets, and teaching projection."),
TEXT("HactarCE/2x2x2x2-Scrambler"),
TEXT("GPL-3.0"),
RestrictiveCleanRoom,
NativePlayable,
TEXT("magic-cube-4d-2x2x2x2"),
TEXT("UHyperTwistCoreLibrary.Melinda"),
TEXT("restrictive-clean-room-first-party")
),
MakeIntegratedCapability(
TEXT("session/integrated-practice"),
TEXT("Integrated practice sessions"),
TEXT("First-party training/timer coupling, session analytics, smart-device lifecycle, publication gating, and local challenges."),
TEXT("kash/cubedesk"),
TEXT("GPL-3.0 / proprietary-conflict"),
RestrictiveCleanRoom,
NativePlayerService,
TEXT("classic-cube-training"),
TEXT("UHyperTwistTrainingSubsystem.CubeDeskBounds"),
TEXT("restrictive-clean-room-first-party")
),
MakeIntegratedCapability(
TEXT("classic/semantic-adapter"),
TEXT("Classic cubing semantics"),
TEXT("A bounded semantic, geometry, search, viewer-adapter, and device-boundary layer beneath classic play."),
TEXT("cubing/cubing.js"),
TEXT("MPL-2.0 OR GPL-3.0-or-later"),
BoundarySensitive,
NativePlayerService,
TEXT("classic-cube-training"),
TEXT("UHyperTwistTrainingClassicCubingLibrary"),
TEXT("boundary-sensitive-first-party-adapter")
),
MakeIntegratedCapability(
TEXT("puzzle/legacy-four-dimensional-context"),
TEXT("Legacy four-dimensional continuity"),
TEXT("Attributed history, macro, puzzle-description, and interaction contracts beneath the modern exact 4D runtimes."),
TEXT("cutelyaware/magiccube4d"),
TEXT("Custom permissive attribution license"),
BoundarySensitive,
NativePlayable,
TEXT("magic-cube-4d-3x3x3x3"),
TEXT("UHyperTwistTrainingLegacy4DLibrary"),
TEXT("boundary-sensitive-attributed-first-party")
),
MakeIntegratedCapability(
TEXT("browser/shared-asset-qa"),
TEXT("Browser asset quality fixtures"),
TEXT("An allowlisted mixed-provenance fixture and environment pack used only for bounded browser quality checks."),
TEXT("google/model-viewer/packages/shared-assets"),
TEXT("Mixed per-asset terms"),
BoundarySensitive,
BrowserSupport,
TEXT("web:browser-access"),
TEXT("UHyperTwistTrainingSharedAssetLibrary"),
TEXT("boundary-sensitive-allowlisted-support")
),
MakeIntegratedCapability(
TEXT("operator/feature-control-plane"),
TEXT("Feature and rollout control plane"),
TEXT("Local feature governance, rollout, replay diagnostics, scheduling, and compliance boundaries; telemetry is not silently enabled."),
TEXT("PostHog/posthog"),
TEXT("MIT with enterprise boundaries"),
BoundarySensitive,
OperatorSupport,
TEXT("menu:advanced"),
TEXT("UHyperTwistTrainingControlPlaneLibrary"),
TEXT("boundary-sensitive-first-party-control-plane")
),
MakeIntegratedCapability(
TEXT("operator/capture-history"),
TEXT("Consent-bound capture history"),
TEXT("Local event capture, permission, persistence, vault lifecycle, and timeline-review contracts without background capture by default."),
TEXT("screenpipe/screenpipe"),
TEXT("MIT with enterprise boundaries"),
BoundarySensitive,
OperatorSupport,
TEXT("menu:advanced"),
TEXT("UHyperTwistTrainingCaptureHistoryLibrary"),
TEXT("boundary-sensitive-first-party-contract")
),
MakeIntegratedCapability(
TEXT("operator/replay-media-export"),
TEXT("Replay media export planning"),
TEXT("First-party replay plan, playback, render-orchestration, parser, explainer, and compliance contracts; fresh shipping work prefers Unreal-native export."),
TEXT("remotion-dev/remotion"),
TEXT("Remotion License"),
BoundarySensitive,
OperatorSupport,
TEXT("menu:advanced"),
TEXT("UHyperTwistTrainingMediaExportLibrary"),
TEXT("boundary-sensitive-retained-output-restrictive-future")
)
};
}
bool UHyperTwistFirstRunLaunchLibrary::TryFindIntegratedCapability(
const FString& CanonicalRepository,
FHyperTwistIntegratedCapability& OutCapability
)
{
for (const FHyperTwistIntegratedCapability& Capability
: BuildIntegratedCapabilityCatalog())
{
if (Capability.CanonicalRepository.Equals(
CanonicalRepository,
ESearchCase::IgnoreCase))
{
OutCapability = Capability;
return true;
}
}
OutCapability = FHyperTwistIntegratedCapability();
return false;
}
FString UHyperTwistFirstRunLaunchLibrary::GetIntegrationLicenseLaneLabel(
const EHyperTwistIntegrationLicenseLane LicenseLane
)
{
switch (LicenseLane)
{
case EHyperTwistIntegrationLicenseLane::Permissive:
return TEXT("Permissive");
case EHyperTwistIntegrationLicenseLane::RestrictiveCleanRoom:
return TEXT("Restrictive clean-room");
case EHyperTwistIntegrationLicenseLane::BoundarySensitive:
return TEXT("Boundary-sensitive");
default:
return TEXT("Unknown");
}
}
FString UHyperTwistFirstRunLaunchLibrary::GetIntegrationSurfaceLabel(
const EHyperTwistIntegrationSurface Surface
)
{
switch (Surface)
{
case EHyperTwistIntegrationSurface::NativePlayable:
return TEXT("Playable puzzle engines");
case EHyperTwistIntegrationSurface::NativePlayerService:
return TEXT("Learning and player services");
case EHyperTwistIntegrationSurface::BrowserSupport:
return TEXT("Browser companion support");
case EHyperTwistIntegrationSurface::OperatorSupport:
return TEXT("Operator and release support");
default:
return TEXT("Other support");
}
}
bool UHyperTwistFirstRunLaunchLibrary::TryFindPuzzleCatalogEntry(
const FString& PuzzleId,
FHyperTwistPuzzleCatalogEntry& OutEntry
)
{
for (const FHyperTwistPuzzleCatalogEntry& Entry : BuildPlayerPuzzleCatalog())
{
if (Entry.PuzzleId.Equals(PuzzleId, ESearchCase::IgnoreCase))
{
OutEntry = Entry;
return true;
}
}
OutEntry = FHyperTwistPuzzleCatalogEntry();
return false;
}
bool UHyperTwistFirstRunLaunchLibrary::TryFindFirstRunLaunchRoute( bool UHyperTwistFirstRunLaunchLibrary::TryFindFirstRunLaunchRoute(
const FString& RouteId, const FString& RouteId,
FHyperTwistFirstRunLaunchRoute& OutRoute FHyperTwistFirstRunLaunchRoute& OutRoute

View file

@ -22,6 +22,26 @@ AHyperTwistFirstRunLaunchPlayerController::AHyperTwistFirstRunLaunchPlayerContro
bEnableMouseOverEvents = true; bEnableMouseOverEvents = true;
} }
void AHyperTwistFirstRunLaunchPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
if (InputComponent == nullptr)
{
return;
}
InputComponent->BindKey(
EKeys::Escape,
IE_Pressed,
this,
&AHyperTwistFirstRunLaunchPlayerController::ReturnFromAdvancedDashboard);
InputComponent->BindKey(
EKeys::F10,
IE_Pressed,
this,
&AHyperTwistFirstRunLaunchPlayerController::ReturnFromAdvancedDashboard);
}
void AHyperTwistFirstRunLaunchPlayerController::BeginPlay() void AHyperTwistFirstRunLaunchPlayerController::BeginPlay()
{ {
Super::BeginPlay(); Super::BeginPlay();
@ -293,6 +313,25 @@ void AHyperTwistFirstRunLaunchPlayerController::HandleFirstRunRouteRequested(
} }
} }
void AHyperTwistFirstRunLaunchPlayerController::ReturnFromAdvancedDashboard()
{
if (ActiveDashboardActor == nullptr || !ActiveDashboardActor->HasActiveDashboard())
{
return;
}
ActiveDashboardActor->RemoveDashboard();
if (UHyperTwistFirstRunLaunchWidget* LaunchWidget = ShowFirstRunLaunchMenu())
{
LaunchWidget->ShowPage(EHyperTwistMainMenuPage::Advanced);
}
ApplyFirstRunInputMode();
AppendStartupDiagnosticsLine(
TEXT("ReturnFromAdvancedDashboard"),
TEXT("Closed the engineering dashboard and restored the main menu."));
PersistStartupDiagnostics(TEXT("dashboard-returned-to-main-menu"));
}
AHyperTwistCoachDashboardActor* AHyperTwistCoachDashboardActor*
AHyperTwistFirstRunLaunchPlayerController::EnsureCoachDashboard() AHyperTwistFirstRunLaunchPlayerController::EnsureCoachDashboard()
{ {

View file

@ -15,6 +15,8 @@
#include "HyperTwistDiagnostics/HyperTwistRuntimeDiagnostics.h" #include "HyperTwistDiagnostics/HyperTwistRuntimeDiagnostics.h"
#include "HyperTwistTraining/HyperTwistCoachDashboardActor.h" #include "HyperTwistTraining/HyperTwistCoachDashboardActor.h"
#include "HyperTwistTraining/HyperTwistHigherDimensionalTrainingShellActor.h" #include "HyperTwistTraining/HyperTwistHigherDimensionalTrainingShellActor.h"
#include "HyperTwistUX/HyperTwistHigherDimensionalHUDWidget.h"
#include "HyperTwistUX/HyperTwistPlayerControllerBase.h"
#include "InputCoreTypes.h" #include "InputCoreTypes.h"
#include "Misc/FileHelper.h" #include "Misc/FileHelper.h"
#include "Misc/Paths.h" #include "Misc/Paths.h"
@ -32,6 +34,61 @@ namespace HyperTwistHigherDimensionalTrainingGameModeInternal
? TEXT("higher-dimensional-runtime-ready-magic120cell") ? TEXT("higher-dimensional-runtime-ready-magic120cell")
: TEXT("higher-dimensional-runtime-ready-magiccube5d"); : TEXT("higher-dimensional-runtime-ready-magiccube5d");
} }
int32 WrapSelection(const int32 Value, const int32 Count)
{
return Count > 0 ? ((Value % Count) + Count) % Count : 0;
}
FString GetMagic120CellAxisFamily(const int32 StickerIndex)
{
if (StickerIndex >= 1 && StickerIndex <= 12)
{
return TEXT("face axis");
}
if (StickerIndex >= 13 && StickerIndex <= 42)
{
return TEXT("edge axis");
}
if (StickerIndex >= 43 && StickerIndex <= 62)
{
return TEXT("vertex axis");
}
return TEXT("invalid axis");
}
void ResolveMagicCube5DPlane(
const int32 FaceAxisIndex,
const int32 PlaneIndex,
EHyperTwistMagicCube5DAxis& OutAxisA,
EHyperTwistMagicCube5DAxis& OutAxisB
)
{
int32 OrthogonalAxes[4] = {0, 1, 2, 3};
int32 WriteIndex = 0;
for (int32 AxisIndex = 0; AxisIndex < 5; ++AxisIndex)
{
if (AxisIndex != FaceAxisIndex && WriteIndex < UE_ARRAY_COUNT(OrthogonalAxes))
{
OrthogonalAxes[WriteIndex++] = AxisIndex;
}
}
constexpr int32 PlanePairs[6][2] =
{
{0, 1},
{0, 2},
{0, 3},
{1, 2},
{1, 3},
{2, 3}
};
const int32 SafePlaneIndex = WrapSelection(PlaneIndex, UE_ARRAY_COUNT(PlanePairs));
OutAxisA = static_cast<EHyperTwistMagicCube5DAxis>(
OrthogonalAxes[PlanePairs[SafePlaneIndex][0]]);
OutAxisB = static_cast<EHyperTwistMagicCube5DAxis>(
OrthogonalAxes[PlanePairs[SafePlaneIndex][1]]);
}
} }
AHyperTwistHigherDimensionalOrbitPawn::AHyperTwistHigherDimensionalOrbitPawn() AHyperTwistHigherDimensionalOrbitPawn::AHyperTwistHigherDimensionalOrbitPawn()
@ -71,6 +128,20 @@ void AHyperTwistHigherDimensionalOrbitPawn::Tick(const float DeltaSeconds)
{ {
return; return;
} }
const AHyperTwistPlayerControllerBase* HyperTwistController =
Cast<AHyperTwistPlayerControllerBase>(PlayerController);
const FHyperTwistPlayerPreferences* Preferences = HyperTwistController != nullptr
? &HyperTwistController->GetPlayerPreferences()
: nullptr;
const float ZoomMultiplier = Preferences != nullptr
? Preferences->ZoomSensitivity
: 1.0f;
const float OrbitMultiplier = Preferences != nullptr
? Preferences->OrbitSensitivity
: 1.0f;
const float VerticalDirection = Preferences != nullptr && Preferences->bInvertOrbitY
? -1.0f
: 1.0f;
if (SpringArm != nullptr) if (SpringArm != nullptr)
{ {
@ -78,7 +149,7 @@ void AHyperTwistHigherDimensionalOrbitPawn::Tick(const float DeltaSeconds)
if (!FMath::IsNearlyZero(MouseWheelDelta)) if (!FMath::IsNearlyZero(MouseWheelDelta))
{ {
SpringArm->TargetArmLength = FMath::Clamp( SpringArm->TargetArmLength = FMath::Clamp(
SpringArm->TargetArmLength - (MouseWheelDelta * ZoomStep), SpringArm->TargetArmLength - (MouseWheelDelta * ZoomStep * ZoomMultiplier),
MinimumArmLength, MinimumArmLength,
MaximumArmLength); MaximumArmLength);
} }
@ -97,9 +168,10 @@ void AHyperTwistHigherDimensionalOrbitPawn::Tick(const float DeltaSeconds)
return; return;
} }
CurrentYawDegrees += MouseDeltaX * OrbitDegreesPerPixel; CurrentYawDegrees += MouseDeltaX * OrbitDegreesPerPixel * OrbitMultiplier;
CurrentPitchDegrees = FMath::Clamp( CurrentPitchDegrees = FMath::Clamp(
CurrentPitchDegrees - (MouseDeltaY * OrbitDegreesPerPixel), CurrentPitchDegrees
- (MouseDeltaY * OrbitDegreesPerPixel * OrbitMultiplier * VerticalDirection),
-78.0f, -78.0f,
55.0f); 55.0f);
ApplyOrbitTransform(); ApplyOrbitTransform();
@ -166,6 +238,13 @@ void AHyperTwistHigherDimensionalTrainingHUD::DrawHUD()
{ {
return; return;
} }
const AHyperTwistHigherDimensionalTrainingPlayerController* Controller =
Cast<AHyperTwistHigherDimensionalTrainingPlayerController>(
GetOwningPlayerController());
if (Controller != nullptr && Controller->IsPuzzleHudReady())
{
return;
}
AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell = ResolveTrainingShell(); AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell = ResolveTrainingShell();
if (TrainingShell == nullptr) if (TrainingShell == nullptr)
@ -291,6 +370,34 @@ AHyperTwistHigherDimensionalTrainingPlayerController::
bEnableMouseOverEvents = true; bEnableMouseOverEvents = true;
} }
FString AHyperTwistHigherDimensionalTrainingPlayerController::GetPauseMenuTitle() const
{
if (GetWorld() != nullptr)
{
TActorIterator<AHyperTwistHigherDimensionalTrainingShellActor> ActorIt(
GetWorld());
if (ActorIt)
{
return ActorIt->Title;
}
}
return TEXT("Higher-Dimensional Training");
}
FString AHyperTwistHigherDimensionalTrainingPlayerController::GetPauseMenuSubtitle() const
{
if (GetWorld() != nullptr)
{
TActorIterator<AHyperTwistHigherDimensionalTrainingShellActor> ActorIt(
GetWorld());
if (ActorIt)
{
return ActorIt->GetRuntimeStatusSummary();
}
}
return TEXT("Family-owned runtime, projection, and persistence");
}
void AHyperTwistHigherDimensionalTrainingPlayerController::BeginPlay() void AHyperTwistHigherDimensionalTrainingPlayerController::BeginPlay()
{ {
Super::BeginPlay(); Super::BeginPlay();
@ -300,53 +407,89 @@ void AHyperTwistHigherDimensionalTrainingPlayerController::BeginPlay()
InputMode.SetHideCursorDuringCapture(false); InputMode.SetHideCursorDuringCapture(false);
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock); InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
SetInputMode(InputMode); SetInputMode(InputMode);
EnsurePuzzleHud();
} }
void AHyperTwistHigherDimensionalTrainingPlayerController::PlayerTick(const float DeltaTime) void AHyperTwistHigherDimensionalTrainingPlayerController::PlayerTick(const float DeltaTime)
{ {
Super::PlayerTick(DeltaTime); Super::PlayerTick(DeltaTime);
if (IsAssistantPanelOpen()
|| ActivePauseMenuWidget != nullptr
|| (GetWorld() != nullptr && GetWorld()->IsPaused()))
{
return;
}
AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell = ResolveTrainingShell(); AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell = ResolveTrainingShell();
if (TrainingShell == nullptr) if (TrainingShell == nullptr)
{ {
EnsurePuzzleHud();
return; return;
} }
if (WasInputKeyJustPressed(EKeys::SpaceBar)) if (WasInputKeyJustPressed(EKeys::SpaceBar))
{ {
TrainingShell->ToggleAutoRotateProjection(); HandleToggleAutoRotate();
}
if (WasInputKeyJustPressed(EKeys::Q))
{
TrainingShell->NudgeProjection(-12.0f);
}
if (WasInputKeyJustPressed(EKeys::E))
{
TrainingShell->NudgeProjection(12.0f);
} }
if (WasInputKeyJustPressed(EKeys::PageUp)) if (WasInputKeyJustPressed(EKeys::PageUp))
{ {
TrainingShell->CycleProjectionLayer(1); HandleNextProjectionLayer();
} }
if (WasInputKeyJustPressed(EKeys::PageDown)) if (WasInputKeyJustPressed(EKeys::PageDown))
{ {
TrainingShell->CycleProjectionLayer(-1); HandlePreviousProjectionLayer();
} }
if (WasInputKeyJustPressed(EKeys::N)) if (WasInputKeyJustPressed(EKeys::N))
{ {
TrainingShell->CreateScrambledRuntimeState(); HandleScramble();
} }
if (WasInputKeyJustPressed(EKeys::R)) if (WasInputKeyJustPressed(EKeys::R))
{ {
TrainingShell->ResetProjection(); HandleReset();
} }
if (WasInputKeyJustPressed(EKeys::S)) if (WasInputKeyJustPressed(EKeys::S))
{ {
TrainingShell->SaveRuntimeState(); HandleSave();
} }
if (WasInputKeyJustPressed(EKeys::L)) if (WasInputKeyJustPressed(EKeys::L))
{ {
TrainingShell->LoadRuntimeState(); HandleLoad();
}
if (WasInputKeyJustPressed(EKeys::Left))
{
HandlePreviousPrimarySelection();
}
if (WasInputKeyJustPressed(EKeys::Right))
{
HandleNextPrimarySelection();
}
if (WasInputKeyJustPressed(EKeys::Down))
{
HandlePreviousSecondarySelection();
}
if (WasInputKeyJustPressed(EKeys::Up))
{
HandleNextSecondarySelection();
}
if (WasInputKeyJustPressed(EKeys::Q))
{
HandlePreviousTertiarySelection();
}
if (WasInputKeyJustPressed(EKeys::E))
{
HandleNextTertiarySelection();
}
if (WasInputKeyJustPressed(EKeys::Tab))
{
HandleToggleFaceSide();
}
if (WasInputKeyJustPressed(EKeys::Comma))
{
HandleNegativeTurn();
}
if (WasInputKeyJustPressed(EKeys::Period))
{
HandlePositiveTurn();
} }
if (WasInputKeyJustPressed(EKeys::Home)) if (WasInputKeyJustPressed(EKeys::Home))
{ {
@ -355,12 +498,21 @@ void AHyperTwistHigherDimensionalTrainingPlayerController::PlayerTick(const floa
{ {
OrbitPawn->ResetOrbit(); OrbitPawn->ResetOrbit();
TrainingShell->LastRuntimeAction = TEXT("camera orbit reset"); TrainingShell->LastRuntimeAction = TEXT("camera orbit reset");
RefreshPuzzleHud();
} }
} }
if (WasInputKeyJustPressed(EKeys::D)) if (WasInputKeyJustPressed(EKeys::D))
{ {
ToggleCoachDashboardSurface(); ToggleCoachDashboardSurface();
} }
EnsurePuzzleHud();
}
bool AHyperTwistHigherDimensionalTrainingPlayerController::IsPuzzleHudReady() const
{
return ActivePuzzleHudWidget != nullptr
&& ActivePuzzleHudWidget->IsInViewport()
&& ActivePuzzleHudWidget->IsHudSurfaceReady();
} }
AHyperTwistHigherDimensionalTrainingShellActor* AHyperTwistHigherDimensionalTrainingShellActor*
@ -395,6 +547,446 @@ void AHyperTwistHigherDimensionalTrainingPlayerController::ToggleCoachDashboardS
EnsureCoachDashboard(); EnsureCoachDashboard();
} }
void AHyperTwistHigherDimensionalTrainingPlayerController::EnsurePuzzleHud()
{
if (!bShowPuzzleHud)
{
return;
}
if (ActivePuzzleHudWidget == nullptr)
{
TSubclassOf<UHyperTwistHigherDimensionalHUDWidget> ResolvedClass =
PuzzleHudWidgetClass;
if (*ResolvedClass == nullptr)
{
ResolvedClass = UHyperTwistHigherDimensionalHUDWidget::StaticClass();
}
ActivePuzzleHudWidget =
CreateWidget<UHyperTwistHigherDimensionalHUDWidget>(
this,
ResolvedClass);
if (ActivePuzzleHudWidget == nullptr
|| !ActivePuzzleHudWidget->PrepareHudSurface())
{
ActivePuzzleHudWidget = nullptr;
return;
}
ActivePuzzleHudWidget->OnPreviousPrimaryRequested.AddDynamic(
this,
&AHyperTwistHigherDimensionalTrainingPlayerController::
HandlePreviousPrimarySelection);
ActivePuzzleHudWidget->OnNextPrimaryRequested.AddDynamic(
this,
&AHyperTwistHigherDimensionalTrainingPlayerController::
HandleNextPrimarySelection);
ActivePuzzleHudWidget->OnPreviousSecondaryRequested.AddDynamic(
this,
&AHyperTwistHigherDimensionalTrainingPlayerController::
HandlePreviousSecondarySelection);
ActivePuzzleHudWidget->OnNextSecondaryRequested.AddDynamic(
this,
&AHyperTwistHigherDimensionalTrainingPlayerController::
HandleNextSecondarySelection);
ActivePuzzleHudWidget->OnPreviousTertiaryRequested.AddDynamic(
this,
&AHyperTwistHigherDimensionalTrainingPlayerController::
HandlePreviousTertiarySelection);
ActivePuzzleHudWidget->OnNextTertiaryRequested.AddDynamic(
this,
&AHyperTwistHigherDimensionalTrainingPlayerController::
HandleNextTertiarySelection);
ActivePuzzleHudWidget->OnToggleFaceSideRequested.AddDynamic(
this,
&AHyperTwistHigherDimensionalTrainingPlayerController::
HandleToggleFaceSide);
ActivePuzzleHudWidget->OnNegativeTurnRequested.AddDynamic(
this,
&AHyperTwistHigherDimensionalTrainingPlayerController::
HandleNegativeTurn);
ActivePuzzleHudWidget->OnPositiveTurnRequested.AddDynamic(
this,
&AHyperTwistHigherDimensionalTrainingPlayerController::
HandlePositiveTurn);
ActivePuzzleHudWidget->OnPreviousProjectionLayerRequested.AddDynamic(
this,
&AHyperTwistHigherDimensionalTrainingPlayerController::
HandlePreviousProjectionLayer);
ActivePuzzleHudWidget->OnNextProjectionLayerRequested.AddDynamic(
this,
&AHyperTwistHigherDimensionalTrainingPlayerController::
HandleNextProjectionLayer);
ActivePuzzleHudWidget->OnToggleAutoRotateRequested.AddDynamic(
this,
&AHyperTwistHigherDimensionalTrainingPlayerController::
HandleToggleAutoRotate);
ActivePuzzleHudWidget->OnScrambleRequested.AddDynamic(
this,
&AHyperTwistHigherDimensionalTrainingPlayerController::HandleScramble);
ActivePuzzleHudWidget->OnResetRequested.AddDynamic(
this,
&AHyperTwistHigherDimensionalTrainingPlayerController::HandleReset);
ActivePuzzleHudWidget->OnSaveRequested.AddDynamic(
this,
&AHyperTwistHigherDimensionalTrainingPlayerController::HandleSave);
ActivePuzzleHudWidget->OnLoadRequested.AddDynamic(
this,
&AHyperTwistHigherDimensionalTrainingPlayerController::HandleLoad);
ActivePuzzleHudWidget->OnCoachRequested.AddDynamic(
this,
&AHyperTwistHigherDimensionalTrainingPlayerController::HandleCoach);
ActivePuzzleHudWidget->OnMenuRequested.AddDynamic(
this,
&AHyperTwistHigherDimensionalTrainingPlayerController::HandleMenu);
ActivePuzzleHudWidget->AddToViewport(PuzzleHudZOrder);
}
else if (!ActivePuzzleHudWidget->IsInViewport())
{
ActivePuzzleHudWidget->AddToViewport(PuzzleHudZOrder);
}
RefreshPuzzleHud();
}
void AHyperTwistHigherDimensionalTrainingPlayerController::RefreshPuzzleHud()
{
if (ActivePuzzleHudWidget == nullptr)
{
return;
}
const AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
ResolveTrainingShell();
if (TrainingShell == nullptr)
{
ActivePuzzleHudWidget->ConfigureSurface(
UsesMagic120CellRuntime(),
bSelectedMagicCube5DPositiveFace,
false,
TEXT("Higher-Dimensional Puzzle"),
TEXT("WAITING FOR RUNTIME"),
TEXT("The family-owned simulation is starting."),
TEXT("Projection is not ready."),
TEXT("waiting"),
TEXT("not loaded"));
return;
}
ActivePuzzleHudWidget->ConfigureSurface(
UsesMagic120CellRuntime(),
bSelectedMagicCube5DPositiveFace,
TrainingShell->bAutoRotateProjection,
TrainingShell->Title,
TrainingShell->GetRuntimeStatusSummary(),
BuildMoveSelectionSummary(),
FString::Printf(
TEXT("%s | %s"),
*TrainingShell->GetProjectionLayerLabel(),
TrainingShell->bAutoRotateProjection
? TEXT("projection rotating")
: TEXT("projection paused")),
TrainingShell->LastRuntimeAction,
TrainingShell->LastPersistenceStatus);
}
FString AHyperTwistHigherDimensionalTrainingPlayerController::
BuildMoveSelectionSummary() const
{
using namespace HyperTwistHigherDimensionalTrainingGameModeInternal;
if (UsesMagic120CellRuntime())
{
const int32 TurnOrder =
UHyperTwistMagic120CellRuntimeLibrary::GetTurnOrderForSticker(
SelectedMagic120StickerIndex);
return FString::Printf(
TEXT("Cell %03d / 120 | twist %02d / 62 | %s, order %d"),
SelectedMagic120CellIndex + 1,
SelectedMagic120StickerIndex,
*GetMagic120CellAxisFamily(SelectedMagic120StickerIndex),
TurnOrder);
}
EHyperTwistMagicCube5DAxis PlaneAxisA;
EHyperTwistMagicCube5DAxis PlaneAxisB;
ResolveMagicCube5DPlane(
SelectedMagicCube5DFaceAxis,
SelectedMagicCube5DPlaneIndex,
PlaneAxisA,
PlaneAxisB);
const EHyperTwistMagicCube5DAxis FaceAxis =
static_cast<EHyperTwistMagicCube5DAxis>(
SelectedMagicCube5DFaceAxis);
return FString::Printf(
TEXT("Face %s%s | layer mask 0x%X | plane %s%s"),
bSelectedMagicCube5DPositiveFace ? TEXT("+") : TEXT("-"),
*UHyperTwistMagicCube5DRuntimeLibrary::GetAxisLabel(FaceAxis),
SelectedMagicCube5DSliceMask,
*UHyperTwistMagicCube5DRuntimeLibrary::GetAxisLabel(PlaneAxisA),
*UHyperTwistMagicCube5DRuntimeLibrary::GetAxisLabel(PlaneAxisB));
}
void AHyperTwistHigherDimensionalTrainingPlayerController::ApplySelectedTurn(
const bool bPositiveDirection
)
{
using namespace HyperTwistHigherDimensionalTrainingGameModeInternal;
AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
ResolveTrainingShell();
if (TrainingShell == nullptr)
{
return;
}
if (UsesMagic120CellRuntime())
{
FHyperTwistMagic120CellTurnRequest Request;
Request.CellIndex = SelectedMagic120CellIndex;
Request.StickerIndex = SelectedMagic120StickerIndex;
Request.bInverse = !bPositiveDirection;
TrainingShell->ApplyMagic120CellTurn(Request);
}
else
{
EHyperTwistMagicCube5DAxis PlaneAxisA;
EHyperTwistMagicCube5DAxis PlaneAxisB;
ResolveMagicCube5DPlane(
SelectedMagicCube5DFaceAxis,
SelectedMagicCube5DPlaneIndex,
PlaneAxisA,
PlaneAxisB);
FHyperTwistMagicCube5DTurnRequest Request;
Request.FaceAxis = static_cast<EHyperTwistMagicCube5DAxis>(
SelectedMagicCube5DFaceAxis);
Request.bPositiveFace = bSelectedMagicCube5DPositiveFace;
Request.SliceMask = SelectedMagicCube5DSliceMask;
Request.RotationAxisA = PlaneAxisA;
Request.RotationAxisB = PlaneAxisB;
Request.Direction = bPositiveDirection
? EHyperTwistMagicCube5DTurnDirection::PositiveQuarterTurn
: EHyperTwistMagicCube5DTurnDirection::NegativeQuarterTurn;
TrainingShell->ApplyMagicCube5DTurn(Request);
}
RefreshPuzzleHud();
}
bool AHyperTwistHigherDimensionalTrainingPlayerController::
UsesMagic120CellRuntime() const
{
if (CachedTrainingShell.IsValid())
{
return CachedTrainingShell->FamilyKey.Equals(
HyperTwistHigherDimensionalTrainingGameModeInternal::
Magic120CellFamilyKey,
ESearchCase::IgnoreCase);
}
return GetWorld() != nullptr
&& GetWorld()->GetMapName().Contains(
TEXT("Magic120Cell"),
ESearchCase::IgnoreCase);
}
void AHyperTwistHigherDimensionalTrainingPlayerController::
HandlePreviousPrimarySelection()
{
using namespace HyperTwistHigherDimensionalTrainingGameModeInternal;
if (UsesMagic120CellRuntime())
{
SelectedMagic120CellIndex = WrapSelection(
SelectedMagic120CellIndex - 1,
120);
}
else
{
SelectedMagicCube5DFaceAxis = WrapSelection(
SelectedMagicCube5DFaceAxis - 1,
5);
SelectedMagicCube5DPlaneIndex = 0;
}
RefreshPuzzleHud();
}
void AHyperTwistHigherDimensionalTrainingPlayerController::
HandleNextPrimarySelection()
{
using namespace HyperTwistHigherDimensionalTrainingGameModeInternal;
if (UsesMagic120CellRuntime())
{
SelectedMagic120CellIndex = WrapSelection(
SelectedMagic120CellIndex + 1,
120);
}
else
{
SelectedMagicCube5DFaceAxis = WrapSelection(
SelectedMagicCube5DFaceAxis + 1,
5);
SelectedMagicCube5DPlaneIndex = 0;
}
RefreshPuzzleHud();
}
void AHyperTwistHigherDimensionalTrainingPlayerController::
HandlePreviousSecondarySelection()
{
using namespace HyperTwistHigherDimensionalTrainingGameModeInternal;
if (UsesMagic120CellRuntime())
{
SelectedMagic120StickerIndex =
WrapSelection(SelectedMagic120StickerIndex - 2, 62) + 1;
}
else
{
SelectedMagicCube5DSliceMask =
WrapSelection(SelectedMagicCube5DSliceMask - 2, 7) + 1;
}
RefreshPuzzleHud();
}
void AHyperTwistHigherDimensionalTrainingPlayerController::
HandleNextSecondarySelection()
{
using namespace HyperTwistHigherDimensionalTrainingGameModeInternal;
if (UsesMagic120CellRuntime())
{
SelectedMagic120StickerIndex =
WrapSelection(SelectedMagic120StickerIndex, 62) + 1;
}
else
{
SelectedMagicCube5DSliceMask =
WrapSelection(SelectedMagicCube5DSliceMask, 7) + 1;
}
RefreshPuzzleHud();
}
void AHyperTwistHigherDimensionalTrainingPlayerController::
HandlePreviousTertiarySelection()
{
using namespace HyperTwistHigherDimensionalTrainingGameModeInternal;
if (!UsesMagic120CellRuntime())
{
SelectedMagicCube5DPlaneIndex = WrapSelection(
SelectedMagicCube5DPlaneIndex - 1,
6);
RefreshPuzzleHud();
}
}
void AHyperTwistHigherDimensionalTrainingPlayerController::
HandleNextTertiarySelection()
{
using namespace HyperTwistHigherDimensionalTrainingGameModeInternal;
if (!UsesMagic120CellRuntime())
{
SelectedMagicCube5DPlaneIndex = WrapSelection(
SelectedMagicCube5DPlaneIndex + 1,
6);
RefreshPuzzleHud();
}
}
void AHyperTwistHigherDimensionalTrainingPlayerController::
HandleToggleFaceSide()
{
if (!UsesMagic120CellRuntime())
{
bSelectedMagicCube5DPositiveFace =
!bSelectedMagicCube5DPositiveFace;
RefreshPuzzleHud();
}
}
void AHyperTwistHigherDimensionalTrainingPlayerController::HandleNegativeTurn()
{
ApplySelectedTurn(false);
}
void AHyperTwistHigherDimensionalTrainingPlayerController::HandlePositiveTurn()
{
ApplySelectedTurn(true);
}
void AHyperTwistHigherDimensionalTrainingPlayerController::
HandlePreviousProjectionLayer()
{
if (AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
ResolveTrainingShell())
{
TrainingShell->CycleProjectionLayer(-1);
RefreshPuzzleHud();
}
}
void AHyperTwistHigherDimensionalTrainingPlayerController::
HandleNextProjectionLayer()
{
if (AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
ResolveTrainingShell())
{
TrainingShell->CycleProjectionLayer(1);
RefreshPuzzleHud();
}
}
void AHyperTwistHigherDimensionalTrainingPlayerController::
HandleToggleAutoRotate()
{
if (AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
ResolveTrainingShell())
{
TrainingShell->ToggleAutoRotateProjection();
RefreshPuzzleHud();
}
}
void AHyperTwistHigherDimensionalTrainingPlayerController::HandleScramble()
{
if (AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
ResolveTrainingShell())
{
TrainingShell->CreateScrambledRuntimeState();
RefreshPuzzleHud();
}
}
void AHyperTwistHigherDimensionalTrainingPlayerController::HandleReset()
{
if (AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
ResolveTrainingShell())
{
TrainingShell->ResetProjection();
RefreshPuzzleHud();
}
}
void AHyperTwistHigherDimensionalTrainingPlayerController::HandleSave()
{
if (AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
ResolveTrainingShell())
{
TrainingShell->SaveRuntimeState();
RefreshPuzzleHud();
}
}
void AHyperTwistHigherDimensionalTrainingPlayerController::HandleLoad()
{
if (AHyperTwistHigherDimensionalTrainingShellActor* TrainingShell =
ResolveTrainingShell())
{
TrainingShell->LoadRuntimeState();
RefreshPuzzleHud();
}
}
void AHyperTwistHigherDimensionalTrainingPlayerController::HandleCoach()
{
ToggleAssistantPanel();
}
void AHyperTwistHigherDimensionalTrainingPlayerController::HandleMenu()
{
ShowPauseMenu();
}
AHyperTwistHigherDimensionalTrainingGameMode::AHyperTwistHigherDimensionalTrainingGameMode() AHyperTwistHigherDimensionalTrainingGameMode::AHyperTwistHigherDimensionalTrainingGameMode()
{ {
DefaultPawnClass = AHyperTwistHigherDimensionalOrbitPawn::StaticClass(); DefaultPawnClass = AHyperTwistHigherDimensionalOrbitPawn::StaticClass();

View file

@ -238,8 +238,8 @@ void AHyperTwistHigherDimensionalTrainingShellActor::ConfigureForFamily(
if (bUseMagic120Cell) if (bUseMagic120Cell)
{ {
TrainingShellId = TEXT("phase6c/magic120cell/dedicated-training-shell"); TrainingShellId = TEXT("phase6c/magic120cell/dedicated-training-shell");
Title = TEXT("Magic120Cell // 120-cell projection lab"); Title = TEXT("Magic120Cell // exact 120-cell simulator");
Summary = TEXT("Interactive first-party 4D projection surface with exactly 120 cell centers, layer focus, runtime-state seeding, and local persistence."); Summary = TEXT("Family-owned 4D simulation with 120 cells, all 7,560 facelets, legal face/edge/vertex twists, exact scrambles, layer focus, and local persistence.");
MapAssetPath = TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining"); MapAssetPath = TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining");
ActivationProfileId = TEXT("magic120cell-cleanroom-runtime-activation"); ActivationProfileId = TEXT("magic120cell-cleanroom-runtime-activation");
HostSurfaceId = TEXT("phase6c/magic120cell/runtime-host-surface"); HostSurfaceId = TEXT("phase6c/magic120cell/runtime-host-surface");
@ -249,15 +249,20 @@ void AHyperTwistHigherDimensionalTrainingShellActor::ConfigureForFamily(
InteractiveSceneSurfaceId = TEXT("phase6c/magic120cell/interactive-scene-surface"); InteractiveSceneSurfaceId = TEXT("phase6c/magic120cell/interactive-scene-surface");
SceneContextId = TEXT("phase6c/magic120cell/interactive-scene-context"); SceneContextId = TEXT("phase6c/magic120cell/interactive-scene-context");
PuzzleId = TEXT("polychoron/magic120cell"); PuzzleId = TEXT("polychoron/magic120cell");
RuntimeModeId = TEXT("magic120cell-full-color-runtime-v1"); RuntimeModeId = TEXT("magic120cell-exact-7560-facelet-runtime-v1");
ProjectionProfileId = TEXT("magic120cell-4d-projection-distance-v1"); ProjectionProfileId = TEXT("magic120cell-4d-projection-distance-v1");
PrimaryPersistenceBoundaryId = TEXT("magic120cell-persistence-boundary"); PrimaryPersistenceBoundaryId = TEXT("magic120cell-persistence-boundary");
if (!Magic120CellRuntimeState.IsStructurallyValid())
{
Magic120CellRuntimeState =
UHyperTwistMagic120CellRuntimeLibrary::BuildSolvedState();
}
} }
else else
{ {
TrainingShellId = TEXT("phase6c/magiccube5d/dedicated-training-shell"); TrainingShellId = TEXT("phase6c/magiccube5d/dedicated-training-shell");
Title = TEXT("MagicCube5D // order-3 projection lab"); Title = TEXT("MagicCube5D // exact order-3 simulator");
Summary = TEXT("Interactive first-party 5D projection surface with all 242 non-central order-3 cells, depth-layer focus, runtime-state seeding, and local persistence."); Summary = TEXT("Family-owned 5D simulation with 243 cubies, all 810 boundary facelets, legal signed-face plane twists, exact scrambles, depth-layer focus, and local persistence.");
MapAssetPath = TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining"); MapAssetPath = TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining");
ActivationProfileId = TEXT("magiccube5d-cleanroom-runtime-activation"); ActivationProfileId = TEXT("magiccube5d-cleanroom-runtime-activation");
HostSurfaceId = TEXT("phase6c/magiccube5d/runtime-host-surface"); HostSurfaceId = TEXT("phase6c/magiccube5d/runtime-host-surface");
@ -267,9 +272,14 @@ void AHyperTwistHigherDimensionalTrainingShellActor::ConfigureForFamily(
InteractiveSceneSurfaceId = TEXT("phase6c/magiccube5d/interactive-scene-surface"); InteractiveSceneSurfaceId = TEXT("phase6c/magiccube5d/interactive-scene-surface");
SceneContextId = TEXT("phase6c/magiccube5d/interactive-scene-context"); SceneContextId = TEXT("phase6c/magiccube5d/interactive-scene-context");
PuzzleId = TEXT("hypercube/magiccube5d/order3"); PuzzleId = TEXT("hypercube/magiccube5d/order3");
RuntimeModeId = TEXT("magiccube5d-order3-runtime-v1"); RuntimeModeId = TEXT("magiccube5d-order3-exact-piece-orientation-runtime-v1");
ProjectionProfileId = TEXT("magiccube5d-5d-projection-distance-v1"); ProjectionProfileId = TEXT("magiccube5d-5d-projection-distance-v1");
PrimaryPersistenceBoundaryId = TEXT("magiccube5d-persistence-boundary"); PrimaryPersistenceBoundaryId = TEXT("magiccube5d-persistence-boundary");
if (!MagicCube5DRuntimeState.IsStructurallyValid())
{
MagicCube5DRuntimeState =
UHyperTwistMagicCube5DRuntimeLibrary::BuildSolvedState(3);
}
} }
AuthoringManifestRelativePath = AuthoringManifestRelativePath =
@ -325,6 +335,18 @@ void AHyperTwistHigherDimensionalTrainingShellActor::ResetProjection()
ProjectionAngleDegrees = 0.0f; ProjectionAngleDegrees = 0.0f;
VisibleProjectionLayer = -1; VisibleProjectionLayer = -1;
RuntimeStateSeed = 0; RuntimeStateSeed = 0;
if (UsesMagic120CellPreview())
{
Magic120CellRuntimeState =
UHyperTwistMagic120CellRuntimeLibrary::BuildSolvedState();
RebuildSourcePoints();
}
else
{
MagicCube5DRuntimeState =
UHyperTwistMagicCube5DRuntimeLibrary::BuildSolvedState(3);
RebuildSourcePoints();
}
bAutoRotateProjection = true; bAutoRotateProjection = true;
LastRuntimeAction = TEXT("projection and state reset to solved defaults"); LastRuntimeAction = TEXT("projection and state reset to solved defaults");
UpdatePreviewTransforms(); UpdatePreviewTransforms();
@ -333,11 +355,124 @@ void AHyperTwistHigherDimensionalTrainingShellActor::ResetProjection()
void AHyperTwistHigherDimensionalTrainingShellActor::CreateScrambledRuntimeState() void AHyperTwistHigherDimensionalTrainingShellActor::CreateScrambledRuntimeState()
{ {
RuntimeStateSeed = RuntimeStateSeed >= MAX_int32 - 1 ? 1 : RuntimeStateSeed + 1; RuntimeStateSeed = RuntimeStateSeed >= MAX_int32 - 1 ? 1 : RuntimeStateSeed + 1;
ProjectionAngleDegrees = FMath::Fmod( if (UsesMagic120CellPreview())
ProjectionAngleDegrees + 37.0f + static_cast<float>(RuntimeStateSeed % 29), {
360.0f); const FHyperTwistMagic120CellScrambleResult Scramble =
LastRuntimeAction = FString::Printf(TEXT("runtime state seed %d applied"), RuntimeStateSeed); UHyperTwistMagic120CellRuntimeLibrary::GenerateScramble(
100,
120000 + RuntimeStateSeed);
if (!Scramble.bGenerated || !Scramble.bExactStateUpdate)
{
LastRuntimeAction =
TEXT("120-cell scramble rejected: exact-state generation failed");
return;
}
Magic120CellRuntimeState = Scramble.State;
ProjectionAngleDegrees = FMath::Fmod(
ProjectionAngleDegrees + 37.0f,
360.0f);
LastRuntimeAction = FString::Printf(
TEXT("exact 120-cell scramble %d applied (%d legal moves)"),
RuntimeStateSeed,
Scramble.Moves.Num());
RebuildSourcePoints();
UpdatePreviewTransforms();
return;
}
else
{
const FHyperTwistMagicCube5DScrambleResult Scramble =
UHyperTwistMagicCube5DRuntimeLibrary::GenerateScramble(
3,
60,
51000 + RuntimeStateSeed);
if (!Scramble.bGenerated || !Scramble.bExactStateUpdate)
{
LastRuntimeAction = TEXT("5D scramble rejected: exact-state generation failed");
return;
}
MagicCube5DRuntimeState = Scramble.State;
ProjectionAngleDegrees = FMath::Fmod(
ProjectionAngleDegrees + 37.0f,
360.0f);
LastRuntimeAction = FString::Printf(
TEXT("exact 5D scramble %d applied (%d legal moves)"),
RuntimeStateSeed,
Scramble.Moves.Num());
RebuildSourcePoints();
UpdatePreviewTransforms();
return;
}
}
bool AHyperTwistHigherDimensionalTrainingShellActor::ApplyMagicCube5DTurn(
const FHyperTwistMagicCube5DTurnRequest& Request)
{
if (UsesMagic120CellPreview())
{
LastRuntimeAction = TEXT("5D turn rejected on the 120-cell family route");
return false;
}
if (!MagicCube5DRuntimeState.IsStructurallyValid())
{
MagicCube5DRuntimeState =
UHyperTwistMagicCube5DRuntimeLibrary::BuildSolvedState(3);
}
const FHyperTwistMagicCube5DTurnResult Turn =
UHyperTwistMagicCube5DRuntimeLibrary::ApplyTurn(
MagicCube5DRuntimeState,
Request);
if (!Turn.bApplied || !Turn.bExactStateUpdate)
{
LastRuntimeAction = TEXT("5D turn rejected: exact-state update failed");
return false;
}
MagicCube5DRuntimeState = Turn.State;
LastRuntimeAction = FString::Printf(
TEXT("5D move %s applied%s"),
*Turn.AppliedNotation,
MagicCube5DRuntimeState.bIsSolved ? TEXT(" // solved") : TEXT(""));
RebuildSourcePoints();
UpdatePreviewTransforms(); UpdatePreviewTransforms();
return true;
}
bool AHyperTwistHigherDimensionalTrainingShellActor::ApplyMagic120CellTurn(
const FHyperTwistMagic120CellTurnRequest& Request)
{
if (!UsesMagic120CellPreview())
{
LastRuntimeAction = TEXT("120-cell turn rejected on the 5D family route");
return false;
}
if (!Magic120CellRuntimeState.IsStructurallyValid())
{
Magic120CellRuntimeState =
UHyperTwistMagic120CellRuntimeLibrary::BuildSolvedState();
}
const FHyperTwistMagic120CellTurnResult Turn =
UHyperTwistMagic120CellRuntimeLibrary::ApplyTurn(
Magic120CellRuntimeState,
Request);
if (!Turn.bApplied || !Turn.bExactStateUpdate)
{
LastRuntimeAction = TEXT("120-cell turn rejected: exact-state update failed");
return false;
}
Magic120CellRuntimeState = Turn.State;
LastRuntimeAction = FString::Printf(
TEXT("120-cell move %s applied%s"),
*Turn.AppliedNotation,
Magic120CellRuntimeState.bIsSolved ? TEXT(" // solved") : TEXT(""));
RebuildSourcePoints();
UpdatePreviewTransforms();
return true;
} }
bool AHyperTwistHigherDimensionalTrainingShellActor::SaveRuntimeState() bool AHyperTwistHigherDimensionalTrainingShellActor::SaveRuntimeState()
@ -350,13 +485,37 @@ bool AHyperTwistHigherDimensionalTrainingShellActor::SaveRuntimeState()
} }
const TSharedRef<FJsonObject> StateObject = MakeShared<FJsonObject>(); const TSharedRef<FJsonObject> StateObject = MakeShared<FJsonObject>();
StateObject->SetStringField(TEXT("schemaVersion"), TEXT("hypertwist/higher-dimensional-runtime-state/v1")); StateObject->SetStringField(TEXT("schemaVersion"), TEXT("hypertwist/higher-dimensional-runtime-state/v2"));
StateObject->SetStringField(TEXT("familyKey"), GetNormalizedFamilyKey()); StateObject->SetStringField(TEXT("familyKey"), GetNormalizedFamilyKey());
StateObject->SetStringField(TEXT("puzzleId"), PuzzleId); StateObject->SetStringField(TEXT("puzzleId"), PuzzleId);
StateObject->SetNumberField(TEXT("projectionAngleDegrees"), ProjectionAngleDegrees); StateObject->SetNumberField(TEXT("projectionAngleDegrees"), ProjectionAngleDegrees);
StateObject->SetNumberField(TEXT("visibleProjectionLayer"), VisibleProjectionLayer); StateObject->SetNumberField(TEXT("visibleProjectionLayer"), VisibleProjectionLayer);
StateObject->SetNumberField(TEXT("runtimeStateSeed"), RuntimeStateSeed); StateObject->SetNumberField(TEXT("runtimeStateSeed"), RuntimeStateSeed);
StateObject->SetBoolField(TEXT("autoRotateProjection"), bAutoRotateProjection); StateObject->SetBoolField(TEXT("autoRotateProjection"), bAutoRotateProjection);
if (UsesMagic120CellPreview())
{
if (!Magic120CellRuntimeState.IsStructurallyValid())
{
LastPersistenceStatus = TEXT("save failed: invalid exact 120-cell runtime state");
return false;
}
StateObject->SetStringField(
TEXT("familyRuntimeStateJson"),
UHyperTwistMagic120CellRuntimeLibrary::SerializeRuntimeStateToJson(
Magic120CellRuntimeState));
}
else
{
if (!MagicCube5DRuntimeState.IsStructurallyValid())
{
LastPersistenceStatus = TEXT("save failed: invalid exact 5D runtime state");
return false;
}
StateObject->SetStringField(
TEXT("familyRuntimeStateJson"),
UHyperTwistMagicCube5DRuntimeLibrary::SerializeRuntimeStateToJson(
MagicCube5DRuntimeState));
}
StateObject->SetStringField(TEXT("savedAtUtc"), FDateTime::UtcNow().ToIso8601()); StateObject->SetStringField(TEXT("savedAtUtc"), FDateTime::UtcNow().ToIso8601());
FString SerializedState; FString SerializedState;
@ -412,6 +571,42 @@ bool AHyperTwistHigherDimensionalTrainingShellActor::LoadRuntimeState()
return false; return false;
} }
FHyperTwistMagicCube5DRuntimeState StoredMagicCube5DState;
FHyperTwistMagic120CellRuntimeState StoredMagic120CellState;
FString FamilyRuntimeStateJson;
if (!StateObject->TryGetStringField(
TEXT("familyRuntimeStateJson"),
FamilyRuntimeStateJson))
{
LastPersistenceStatus = TEXT("load rejected: exact family state missing");
return false;
}
if (UsesMagic120CellPreview())
{
if (!UHyperTwistMagic120CellRuntimeLibrary::
TryDeserializeRuntimeStateFromJson(
FamilyRuntimeStateJson,
StoredMagic120CellState))
{
LastPersistenceStatus =
TEXT("load rejected: exact 120-cell family state invalid");
return false;
}
}
else
{
if (!UHyperTwistMagicCube5DRuntimeLibrary::
TryDeserializeRuntimeStateFromJson(
FamilyRuntimeStateJson,
StoredMagicCube5DState)
|| StoredMagicCube5DState.Order != 3)
{
LastPersistenceStatus =
TEXT("load rejected: exact 5D family state missing or invalid");
return false;
}
}
ProjectionAngleDegrees = FMath::Fmod(static_cast<float>(StoredProjectionAngle) + 360.0f, 360.0f); ProjectionAngleDegrees = FMath::Fmod(static_cast<float>(StoredProjectionAngle) + 360.0f, 360.0f);
VisibleProjectionLayer = FMath::Clamp( VisibleProjectionLayer = FMath::Clamp(
static_cast<int32>(StoredVisibleLayer), static_cast<int32>(StoredVisibleLayer),
@ -419,6 +614,16 @@ bool AHyperTwistHigherDimensionalTrainingShellActor::LoadRuntimeState()
GetProjectionLayerCount() - 1); GetProjectionLayerCount() - 1);
RuntimeStateSeed = FMath::Max(static_cast<int32>(StoredRuntimeStateSeed), 0); RuntimeStateSeed = FMath::Max(static_cast<int32>(StoredRuntimeStateSeed), 0);
bAutoRotateProjection = bStoredAutoRotate; bAutoRotateProjection = bStoredAutoRotate;
if (UsesMagic120CellPreview())
{
Magic120CellRuntimeState = MoveTemp(StoredMagic120CellState);
RebuildSourcePoints();
}
else
{
MagicCube5DRuntimeState = MoveTemp(StoredMagicCube5DState);
RebuildSourcePoints();
}
LastPersistenceStatus = TEXT("loaded local runtime state"); LastPersistenceStatus = TEXT("loaded local runtime state");
LastRuntimeAction = LastPersistenceStatus; LastRuntimeAction = LastPersistenceStatus;
UpdatePreviewTransforms(); UpdatePreviewTransforms();
@ -471,12 +676,25 @@ FString AHyperTwistHigherDimensionalTrainingShellActor::GetProjectionLayerLabel(
FString AHyperTwistHigherDimensionalTrainingShellActor::GetRuntimeStatusSummary() const FString AHyperTwistHigherDimensionalTrainingShellActor::GetRuntimeStatusSummary() const
{ {
const FString StateSummary = UsesMagic120CellPreview()
&& Magic120CellRuntimeState.IsStructurallyValid()
? FString::Printf(
TEXT("%s | %d exact moves"),
Magic120CellRuntimeState.bIsSolved ? TEXT("solved") : TEXT("mixed"),
Magic120CellRuntimeState.AppliedMoveCount)
: (!UsesMagic120CellPreview()
&& MagicCube5DRuntimeState.IsStructurallyValid()
? FString::Printf(
TEXT("%s | %d exact moves"),
MagicCube5DRuntimeState.bIsSolved ? TEXT("solved") : TEXT("mixed"),
MagicCube5DRuntimeState.AppliedMoveCount)
: TEXT("state unavailable"));
return FString::Printf( return FString::Printf(
TEXT("%d / %d elements | %s | state seed %d | %s"), TEXT("%d / %d elements | %s | %s | %s"),
GetRenderableElementCount(), GetRenderableElementCount(),
GetCanonicalElementCount(), GetCanonicalElementCount(),
*GetProjectionLayerLabel(), *GetProjectionLayerLabel(),
RuntimeStateSeed, *StateSummary,
bAutoRotateProjection ? TEXT("auto-rotating") : TEXT("rotation paused")); bAutoRotateProjection ? TEXT("auto-rotating") : TEXT("rotation paused"));
} }
@ -578,6 +796,18 @@ void AHyperTwistHigherDimensionalTrainingShellActor::RebuildSourcePoints()
void AHyperTwistHigherDimensionalTrainingShellActor::BuildMagic120CellSourcePoints() void AHyperTwistHigherDimensionalTrainingShellActor::BuildMagic120CellSourcePoints()
{ {
using namespace HyperTwistHigherDimensionalTrainingShellActorInternal; using namespace HyperTwistHigherDimensionalTrainingShellActorInternal;
if (!Magic120CellRuntimeState.IsStructurallyValid())
{
Magic120CellRuntimeState =
UHyperTwistMagic120CellRuntimeLibrary::BuildSolvedState();
}
const FHyperTwistMagic120CellProjectionBuildResult Projection =
UHyperTwistMagic120CellRuntimeLibrary::BuildProjection(
Magic120CellRuntimeState);
if (!Projection.bProjected || !Projection.bExactProjection)
{
return;
}
for (int32 Axis = 0; Axis < 4; ++Axis) for (int32 Axis = 0; Axis < 4; ++Axis)
{ {
@ -678,43 +908,48 @@ void AHyperTwistHigherDimensionalTrainingShellActor::BuildMagic120CellSourcePoin
SourcePoints4D.Num() == Magic120CellElementCount, SourcePoints4D.Num() == Magic120CellElementCount,
TEXT("Magic120Cell projection source must contain exactly 120 cell centers; observed %d."), TEXT("Magic120Cell projection source must contain exactly 120 cell centers; observed %d."),
SourcePoints4D.Num()); SourcePoints4D.Num());
if (SourceColorIndices.Num() == Projection.Projection.Cells.Num())
{
for (int32 CellIndex = 0;
CellIndex < SourceColorIndices.Num();
++CellIndex)
{
SourceColorIndices[CellIndex] =
Projection.Projection.Cells[CellIndex].RepresentativeColorIndex
% PreviewColorLayerCount;
}
}
} }
void AHyperTwistHigherDimensionalTrainingShellActor::BuildMagicCube5DSourcePoints() void AHyperTwistHigherDimensionalTrainingShellActor::BuildMagicCube5DSourcePoints()
{ {
using namespace HyperTwistHigherDimensionalTrainingShellActorInternal; using namespace HyperTwistHigherDimensionalTrainingShellActorInternal;
if (!MagicCube5DRuntimeState.IsStructurallyValid())
for (int32 X = -1; X <= 1; ++X)
{ {
for (int32 Y = -1; Y <= 1; ++Y) MagicCube5DRuntimeState =
{ UHyperTwistMagicCube5DRuntimeLibrary::BuildSolvedState(3);
for (int32 Z = -1; Z <= 1; ++Z) }
{ const FHyperTwistMagicCube5DProjectionBuildResult Projection =
for (int32 W = -1; W <= 1; ++W) UHyperTwistMagicCube5DRuntimeLibrary::BuildProjection(
{ MagicCube5DRuntimeState);
for (int32 V = -1; V <= 1; ++V) if (!Projection.bProjected || !Projection.bExactProjection)
{ {
if (X == 0 && Y == 0 && Z == 0 && W == 0 && V == 0) return;
{ }
continue;
}
const int32 ColorIndex = FMath::Abs( for (const FHyperTwistMagicCube5DProjectedCubie& Cubie :
(X * 31) + (Y * 17) + (Z * 13) + (W * 7) + (V * 3)) Projection.Projection.Cubies)
% PreviewColorLayerCount; {
AddSourcePoint( const TArray<int32>& Components = Cubie.Position.Components;
FVector4( AddSourcePoint(
static_cast<float>(X), FVector4(
static_cast<float>(Y), static_cast<float>(Components[0]),
static_cast<float>(Z), static_cast<float>(Components[1]),
static_cast<float>(W)), static_cast<float>(Components[2]),
static_cast<float>(V), static_cast<float>(Components[3])),
V + 1, static_cast<float>(Components[4]),
ColorIndex); Components[4] + 1,
} Cubie.RepresentativeColorIndex % PreviewColorLayerCount);
}
}
}
} }
ensureMsgf( ensureMsgf(
@ -744,9 +979,7 @@ void AHyperTwistHigherDimensionalTrainingShellActor::UpdatePreviewTransforms()
const FVector ProjectedPoint = bMagic120Cell const FVector ProjectedPoint = bMagic120Cell
? ProjectMagic120CellPoint(SourcePoints4D[Index]) ? ProjectMagic120CellPoint(SourcePoints4D[Index])
: ProjectMagicCube5DPoint(SourcePoints4D[Index], SourceFifthCoordinates[Index]); : ProjectMagicCube5DPoint(SourcePoints4D[Index], SourceFifthCoordinates[Index]);
const int32 ColorIndex = FMath::Abs( const int32 ColorIndex = SourceColorIndices[Index];
SourceColorIndices[Index] + RuntimeStateSeed + (Index * FMath::Max(RuntimeStateSeed, 1)))
% HyperTwistHigherDimensionalTrainingShellActorInternal::PreviewColorLayerCount;
const float ElementScale = bMagic120Cell ? 0.145f : 0.092f; const float ElementScale = bMagic120Cell ? 0.145f : 0.092f;
AddPreviewInstance(ProjectedPoint, ElementScale, ColorIndex); AddPreviewInstance(ProjectedPoint, ElementScale, ColorIndex);
} }
@ -929,8 +1162,8 @@ FText AHyperTwistHigherDimensionalTrainingShellActor::BuildLabelText() const
const FString EffectiveTitle = !Title.IsEmpty() const FString EffectiveTitle = !Title.IsEmpty()
? Title ? Title
: (UsesMagic120CellPreview() : (UsesMagic120CellPreview()
? TEXT("Magic120Cell // 120-cell projection lab") ? TEXT("Magic120Cell // exact 120-cell simulator")
: TEXT("MagicCube5D // order-3 projection lab")); : TEXT("MagicCube5D // exact order-3 simulator"));
return FText::FromString(FString::Printf( return FText::FromString(FString::Printf(
TEXT("%s\n%d canonical elements // %s"), TEXT("%s\n%d canonical elements // %s"),

View file

@ -720,14 +720,30 @@ FHyperTwistRetainedHyperPuzzleCatalog UHyperTwistTrainingCatalogLibrary::MakeRet
{TEXT("hypercube"), TEXT("virtual"), TEXT("compact"), TEXT("retained/hyperspeedcube")}, {TEXT("hypercube"), TEXT("virtual"), TEXT("compact"), TEXT("retained/hyperspeedcube")},
{TEXT("2x2x2x2")} {TEXT("2x2x2x2")}
), ),
HyperTwistTrainingCatalogLibraryInternal::MakeRetainedHyperCatalogEntry( HyperTwistTrainingCatalogLibraryInternal::MakeRetainedHyperCatalogEntry(
TEXT("hypercube/4x4x4x4"), TEXT("hypercube/4x4x4x4"),
TEXT("4x4x4x4"), TEXT("4x4x4x4"),
{4, 4, 4, 4}, {4, 4, 4, 4},
FString(), FString(),
{TEXT("hypercube"), TEXT("virtual"), TEXT("extended"), TEXT("retained/hyperspeedcube")}, {TEXT("hypercube"), TEXT("virtual"), TEXT("extended"), TEXT("retained/hyperspeedcube")},
{TEXT("4x4x4x4")} {TEXT("4x4x4x4")}
), ),
HyperTwistTrainingCatalogLibraryInternal::MakeRetainedHyperCatalogEntry(
TEXT("hypercube/5x5x5x5"),
TEXT("5x5x5x5"),
{5, 5, 5, 5},
FString(),
{TEXT("hypercube"), TEXT("virtual"), TEXT("extended"), TEXT("retained/hyperspeedcube")},
{TEXT("5x5x5x5")}
),
HyperTwistTrainingCatalogLibraryInternal::MakeRetainedHyperCatalogEntry(
TEXT("hypercube/6x6x6x6"),
TEXT("6x6x6x6"),
{6, 6, 6, 6},
FString(),
{TEXT("hypercube"), TEXT("virtual"), TEXT("extended"), TEXT("retained/hyperspeedcube")},
{TEXT("6x6x6x6")}
),
HyperTwistTrainingCatalogLibraryInternal::MakeRetainedHyperCatalogEntry( HyperTwistTrainingCatalogLibraryInternal::MakeRetainedHyperCatalogEntry(
TEXT("hypercube/3x3x3x3x3"), TEXT("hypercube/3x3x3x3x3"),
TEXT("3x3x3x3x3"), TEXT("3x3x3x3x3"),

View file

@ -264,15 +264,15 @@ namespace HyperTwistTrainingPanelWidgetInternal
return TEXT("Classic cube action shortcuts: unavailable."); return TEXT("Classic cube action shortcuts: unavailable.");
} }
return FString::Printf( return FString::Printf(
TEXT("Classic cube action shortcuts: R scramble %s | H hint %s | Enter submit %s | F mode %s | V hold-to-talk %s | C cycle voice %s."), TEXT("Classic cube action shortcuts: new puzzle %s | hint %s | submit %s | guided mode %s | global coach dictation %s | optional Classic voice commands %s."),
ClassicCubeDefaults->bBindFreshAttemptShortcut ? TEXT("ready") : TEXT("disabled"), ClassicCubeDefaults->bBindFreshAttemptShortcut ? TEXT("ready") : TEXT("disabled"),
ClassicCubeDefaults->bBindHintShortcut ? TEXT("ready") : TEXT("disabled"), ClassicCubeDefaults->bBindHintShortcut ? TEXT("ready") : TEXT("disabled"),
ClassicCubeDefaults->bBindSubmitSolveShortcut ? TEXT("ready") : TEXT("disabled"), ClassicCubeDefaults->bBindSubmitSolveShortcut ? TEXT("ready") : TEXT("disabled"),
ClassicCubeDefaults->bBindModeToggleShortcut ? TEXT("ready") : TEXT("disabled"), ClassicCubeDefaults->bBindModeToggleShortcut ? TEXT("ready") : TEXT("disabled"),
ClassicCubeDefaults->bBindVoiceHoldShortcut ? TEXT("ready") : TEXT("disabled"), ClassicCubeDefaults->GlobalDictationCapture != nullptr ? TEXT("ready") : TEXT("disabled"),
ClassicCubeDefaults->bBindVoiceCycleShortcut ? TEXT("ready") : TEXT("disabled") ClassicCubeDefaults->bBindVoiceHoldShortcut ? TEXT("enabled") : TEXT("off by default")
); );
} }
bool TryFindViewerEditorToolById( bool TryFindViewerEditorToolById(
@ -1450,13 +1450,12 @@ UHyperTwistTrainingPanelWidget::GetDisplayedControlInputReadinessInspectSurface(
&& ClassicCubeDefaults->bShowMouseCursor; && ClassicCubeDefaults->bShowMouseCursor;
Surface.bClassicCubeTouchInputReady = Surface.bClassicCubeTouchInputReady =
ClassicCubeDefaults != nullptr && ClassicCubeDefaults->bEnableTouchTurnInput; ClassicCubeDefaults != nullptr && ClassicCubeDefaults->bEnableTouchTurnInput;
Surface.bClassicCubeShortcutReady = ClassicCubeDefaults != nullptr Surface.bClassicCubeShortcutReady = ClassicCubeDefaults != nullptr
&& ClassicCubeDefaults->bBindFreshAttemptShortcut && ClassicCubeDefaults->bBindFreshAttemptShortcut
&& ClassicCubeDefaults->bBindHintShortcut && ClassicCubeDefaults->bBindHintShortcut
&& ClassicCubeDefaults->bBindSubmitSolveShortcut && ClassicCubeDefaults->bBindSubmitSolveShortcut
&& ClassicCubeDefaults->bBindModeToggleShortcut && ClassicCubeDefaults->bBindModeToggleShortcut
&& ClassicCubeDefaults->bBindVoiceHoldShortcut && ClassicCubeDefaults->GlobalDictationCapture != nullptr;
&& ClassicCubeDefaults->bBindVoiceCycleShortcut;
const AHyperTwistVirtual3333ProjectionPlayerController* HigherDimensionalDefaults = const AHyperTwistVirtual3333ProjectionPlayerController* HigherDimensionalDefaults =
GetDefault<AHyperTwistVirtual3333ProjectionPlayerController>(); GetDefault<AHyperTwistVirtual3333ProjectionPlayerController>();

View file

@ -14,12 +14,47 @@
#include "HyperTwistTraining/HyperTwistTrainingPublicationLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingPublicationLibrary.h"
#include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h"
#include "HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h"
#include "HyperTwistUX/HyperTwistPlayerSettings.h"
#include "JsonObjectConverter.h" #include "JsonObjectConverter.h"
namespace HyperTwistTrainingSubsystemInternal namespace HyperTwistTrainingSubsystemInternal
{ {
const TCHAR* ActiveCoachSessionQueueId = TEXT("coach_queue_active"); const TCHAR* ActiveCoachSessionQueueId = TEXT("coach_queue_active");
void ApplyPlayerSpeechPreferences(
FHyperTwistSpeechSessionConfig& SessionConfig,
const bool bUseMockClient
)
{
if (bUseMockClient)
{
return;
}
const FHyperTwistPlayerPreferences Preferences =
UHyperTwistPlayerSettingsLibrary::LoadPreferences();
SessionConfig.LanguageMode = Preferences.SpeechLanguage;
SessionConfig.ProviderProfileId = TEXT("speech-provider/player-settings/v1");
SessionConfig.ProviderProfileDefinition.ProviderProfileId =
SessionConfig.ProviderProfileId;
SessionConfig.ProviderProfileDefinition.DisplayLabel =
Preferences.SpeechProviderId;
SessionConfig.ProviderProfileDefinition.EndpointBaseUrl =
Preferences.SpeechEndpoint;
SessionConfig.ProviderProfileDefinition.SupportedModelIds.Reset();
if (!Preferences.SpeechModel.IsEmpty())
{
SessionConfig.ProviderProfileDefinition.SupportedModelIds.Add(
Preferences.SpeechModel);
}
SessionConfig.ProviderProfileDefinition.bEnabled =
Preferences.bSpeechInputEnabled;
if (!Preferences.SpeechRecognitionContext.IsEmpty())
{
SessionConfig.OrchestrationProfile.PrefixHint =
Preferences.SpeechRecognitionContext;
}
}
struct FResolvedCoachCaseBudget struct FResolvedCoachCaseBudget
{ {
int32 RequestedCaseCount = 0; int32 RequestedCaseCount = 0;
@ -14474,14 +14509,6 @@ bool UHyperTwistTrainingSubsystem::OpenActiveCompanionSpeechSession(FString& Out
{ {
OutError.Reset(); OutError.Reset();
if (!HasActiveRun())
{
OutError = TEXT("no-active-training-run");
ActiveCompanionSpeechSessionState.LastError = OutError;
RefreshCompanionSpeechServiceHealth();
return false;
}
IHyperTwistSpeechClient* SpeechClient = HyperTwistTrainingSubsystemInternal::ResolveSpeechClient( IHyperTwistSpeechClient* SpeechClient = HyperTwistTrainingSubsystemInternal::ResolveSpeechClient(
ResolveCompanionSpeechClientObject() ResolveCompanionSpeechClientObject()
); );
@ -14612,12 +14639,6 @@ FHyperTwistSpeechTranscriptResult UHyperTwistTrainingSubsystem::SubmitActiveComp
{ {
FHyperTwistSpeechTranscriptResult Result; FHyperTwistSpeechTranscriptResult Result;
if (!HasActiveRun())
{
Result.Warnings.Add(TEXT("no-active-training-run"));
return Result;
}
FString OpenError; FString OpenError;
if (!OpenActiveCompanionSpeechSession(OpenError)) if (!OpenActiveCompanionSpeechSession(OpenError))
{ {
@ -15529,53 +15550,21 @@ FHyperTwistVisionSessionConfig UHyperTwistTrainingSubsystem::BuildActiveRecognit
FHyperTwistSpeechSessionConfig UHyperTwistTrainingSubsystem::BuildActiveCompanionSpeechSessionConfig() const FHyperTwistSpeechSessionConfig UHyperTwistTrainingSubsystem::BuildActiveCompanionSpeechSessionConfig() const
{ {
FHyperTwistSpeechSessionConfig SessionConfig = ActiveCompanionSpeechSessionState.SessionConfig; FHyperTwistSpeechSessionConfig SessionConfig = ActiveCompanionSpeechSessionState.SessionConfig;
const bool bUseMockClient = CompanionSpeechClientKind.Equals(TEXT("mock"), ESearchCase::IgnoreCase); const bool bUseMockClient =
if (!HasActiveRun()) ResolveCompanionSpeechClientKind().Equals(TEXT("mock"), ESearchCase::IgnoreCase);
{
if (SessionConfig.NativeCaptureRouteWorkflowProfileId.IsEmpty())
{
SessionConfig.NativeCaptureRouteWorkflowProfileId =
TEXT("speech-native-capture-route-workflow-v1");
}
if (!SessionConfig.NativeCaptureRouteWorkflowProfileDefinition.IsStructurallyValid())
{
SessionConfig.NativeCaptureRouteWorkflowProfileDefinition =
HyperTwistTrainingSubsystemInternal::BuildDefaultSpeechNativeCaptureRouteWorkflowProfile(
SessionConfig.NativeCaptureRouteWorkflowProfileId
);
}
if (SessionConfig.NativeCaptureRouteShellProfileId.IsEmpty())
{
SessionConfig.NativeCaptureRouteShellProfileId =
TEXT("speech-native-capture-route-shell-v1");
}
if (!SessionConfig.NativeCaptureRouteShellProfileDefinition.IsStructurallyValid())
{
SessionConfig.NativeCaptureRouteShellProfileDefinition =
HyperTwistTrainingSubsystemInternal::BuildDefaultSpeechNativeCaptureRouteShellProfile(
SessionConfig.NativeCaptureRouteShellProfileId
);
}
HyperTwistTrainingSubsystemInternal::ApplySpeechPayloadCustodyDefaults(SessionConfig);
HyperTwistTrainingSubsystemInternal::ApplySpeechExternalDictationDefaults(SessionConfig);
HyperTwistTrainingSubsystemInternal::ApplySpeechProviderProfileDefaults(
SessionConfig,
bUseMockClient
);
HyperTwistTrainingSubsystemInternal::ApplySpeechProviderRoutingDefaults(
SessionConfig,
bUseMockClient
);
HyperTwistTrainingSubsystemInternal::ApplySpeechUsageCostDefaults(SessionConfig);
return SessionConfig;
}
const FHyperTwistTrainingCompanionReferenceBundle CompanionBundle = const FHyperTwistTrainingCompanionReferenceBundle CompanionBundle =
UHyperTwistTrainingRuntimeLibrary::GetBundledEmbodiedCompanionReferenceBundle(); UHyperTwistTrainingRuntimeLibrary::GetBundledEmbodiedCompanionReferenceBundle();
SessionConfig.SessionId = !ActiveCompanionSpeechSessionState.ActiveSessionId.IsEmpty() SessionConfig.SessionId = !ActiveCompanionSpeechSessionState.ActiveSessionId.IsEmpty()
? ActiveCompanionSpeechSessionState.ActiveSessionId ? ActiveCompanionSpeechSessionState.ActiveSessionId
: HyperTwistTrainingSubsystemInternal::BuildSpeechSessionId(ActiveRunState.Session, 1); : HasActiveRun()
? HyperTwistTrainingSubsystemInternal::BuildSpeechSessionId(
ActiveRunState.Session,
1)
: FString::Printf(
TEXT("speech-global-%s"),
*FGuid::NewGuid().ToString(EGuidFormats::Digits));
SessionConfig.ListeningContractId = !CompanionBundle.PrimaryListeningContractId.IsEmpty() SessionConfig.ListeningContractId = !CompanionBundle.PrimaryListeningContractId.IsEmpty()
? CompanionBundle.PrimaryListeningContractId ? CompanionBundle.PrimaryListeningContractId
: TEXT("listening-threshold-lifecycle"); : TEXT("listening-threshold-lifecycle");
@ -15790,6 +15779,9 @@ FHyperTwistSpeechSessionConfig UHyperTwistTrainingSubsystem::BuildActiveCompanio
bUseMockClient bUseMockClient
); );
HyperTwistTrainingSubsystemInternal::ApplySpeechUsageCostDefaults(SessionConfig); HyperTwistTrainingSubsystemInternal::ApplySpeechUsageCostDefaults(SessionConfig);
HyperTwistTrainingSubsystemInternal::ApplyPlayerSpeechPreferences(
SessionConfig,
bUseMockClient);
return SessionConfig; return SessionConfig;
} }
@ -15841,6 +15833,18 @@ UObject* UHyperTwistTrainingSubsystem::ResolveCompanionSpeechClientObject()
ActiveCompanionSpeechClientObject = HttpSpeechClient; ActiveCompanionSpeechClientObject = HttpSpeechClient;
} }
} }
if (UHyperTwistHttpSpeechClient* HttpSpeechClient =
Cast<UHyperTwistHttpSpeechClient>(ActiveCompanionSpeechClientObject))
{
const FHyperTwistPlayerPreferences Preferences =
UHyperTwistPlayerSettingsLibrary::LoadPreferences();
HttpSpeechClient->ProviderLabel = Preferences.SpeechProviderId;
HttpSpeechClient->ServiceBaseUrl = Preferences.SpeechEndpoint;
HttpSpeechClient->AuthorizationToken.Reset();
UHyperTwistPlayerSettingsLibrary::LoadProviderCredential(
TEXT("speech-api-key"),
HttpSpeechClient->AuthorizationToken);
}
return ActiveCompanionSpeechClientObject; return ActiveCompanionSpeechClientObject;
} }

View file

@ -0,0 +1,453 @@
#include "HyperTwistUX/HyperTwistDictationCaptureComponent.h"
#include "HAL/PlatformFileManager.h"
#include "Interfaces/VoiceCapture.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "VoiceModule.h"
#include "HyperTwistRecognition/HyperTwistSpeechLibrary.h"
#include "HyperTwistUX/HyperTwistPlayerSettings.h"
namespace HyperTwistDictationCaptureComponentInternal
{
constexpr int32 BytesPerPcm16Sample = 2;
void AppendLittleEndian16(TArray<uint8>& Bytes, const uint16 Value)
{
Bytes.Add(static_cast<uint8>(Value & 0xff));
Bytes.Add(static_cast<uint8>((Value >> 8) & 0xff));
}
void AppendLittleEndian32(TArray<uint8>& Bytes, const uint32 Value)
{
Bytes.Add(static_cast<uint8>(Value & 0xff));
Bytes.Add(static_cast<uint8>((Value >> 8) & 0xff));
Bytes.Add(static_cast<uint8>((Value >> 16) & 0xff));
Bytes.Add(static_cast<uint8>((Value >> 24) & 0xff));
}
void AppendWaveHeader(
TArray<uint8>& Bytes,
const int32 PcmByteCount,
const int32 SampleRateHz,
const int32 ChannelCount
)
{
const uint16 BitsPerSample = 16;
const uint16 BlockAlign =
static_cast<uint16>(ChannelCount * (BitsPerSample / 8));
const uint32 BytesPerSecond =
static_cast<uint32>(SampleRateHz) * BlockAlign;
const ANSICHAR Riff[] = "RIFF";
Bytes.Append(reinterpret_cast<const uint8*>(Riff), 4);
AppendLittleEndian32(Bytes, 36U + static_cast<uint32>(PcmByteCount));
const ANSICHAR Wave[] = "WAVE";
Bytes.Append(reinterpret_cast<const uint8*>(Wave), 4);
const ANSICHAR Format[] = "fmt ";
Bytes.Append(reinterpret_cast<const uint8*>(Format), 4);
AppendLittleEndian32(Bytes, 16);
AppendLittleEndian16(Bytes, 1);
AppendLittleEndian16(Bytes, static_cast<uint16>(ChannelCount));
AppendLittleEndian32(Bytes, static_cast<uint32>(SampleRateHz));
AppendLittleEndian32(Bytes, BytesPerSecond);
AppendLittleEndian16(Bytes, BlockAlign);
AppendLittleEndian16(Bytes, BitsPerSample);
const ANSICHAR Data[] = "data";
Bytes.Append(reinterpret_cast<const uint8*>(Data), 4);
AppendLittleEndian32(Bytes, static_cast<uint32>(PcmByteCount));
}
bool IsCloudSpeechProvider(const FString& ProviderId)
{
return ProviderId == TEXT("openai")
|| ProviderId == TEXT("groq")
|| ProviderId == TEXT("deepgram")
|| ProviderId == TEXT("elevenlabs");
}
}
UHyperTwistDictationCaptureComponent::UHyperTwistDictationCaptureComponent()
{
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bStartWithTickEnabled = false;
}
void UHyperTwistDictationCaptureComponent::EndPlay(
const EEndPlayReason::Type EndPlayReason
)
{
CancelCapture();
Super::EndPlay(EndPlayReason);
}
void UHyperTwistDictationCaptureComponent::TickComponent(
const float DeltaTime,
const ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction
)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (!bCaptureActive)
{
return;
}
FString FailureReason;
if (!DrainCaptureBuffer(FailureReason))
{
ResetCaptureState(true);
SetStatus(FailureReason, true);
return;
}
if (FPlatformTime::Seconds() - CaptureStartedAtSeconds
>= FMath::Max(static_cast<double>(MaximumCaptureSeconds), 1.0))
{
FString Transcript;
EndCapture(Transcript, FailureReason);
if (!FailureReason.IsEmpty())
{
SetStatus(FailureReason, true);
}
}
}
bool UHyperTwistDictationCaptureComponent::BeginCapture(FString& OutFailureReason)
{
OutFailureReason.Reset();
if (bCaptureActive)
{
return true;
}
const FHyperTwistPlayerPreferences Preferences =
UHyperTwistPlayerSettingsLibrary::LoadPreferences();
if (!Preferences.bSpeechInputEnabled)
{
OutFailureReason = TEXT("Enable speech-to-text in Settings > Voice & AI.");
SetStatus(OutFailureReason, true);
return false;
}
if (HyperTwistDictationCaptureComponentInternal::IsCloudSpeechProvider(
Preferences.SpeechProviderId)
&& !Preferences.bAllowCloudProviders)
{
OutFailureReason =
TEXT("Enable cloud providers before using the selected transcription service.");
SetStatus(OutFailureReason, true);
return false;
}
if (!UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
Preferences.SpeechEndpoint,
Preferences.bAllowCloudProviders,
OutFailureReason))
{
SetStatus(OutFailureReason, true);
return false;
}
if (HyperTwistDictationCaptureComponentInternal::IsCloudSpeechProvider(
Preferences.SpeechProviderId)
&& !UHyperTwistPlayerSettingsLibrary::HasProviderCredential(
TEXT("speech-api-key")))
{
OutFailureReason =
TEXT("Save a protected transcription API key before starting dictation.");
SetStatus(OutFailureReason, true);
return false;
}
if (!UHyperTwistSpeechLibrary::StartDictationSession(
this,
OutFailureReason,
true,
AudioDuckMultiplier))
{
SetStatus(
OutFailureReason.IsEmpty()
? TEXT("The transcription service is not ready.")
: OutFailureReason,
true);
return false;
}
if (!FVoiceModule::Get().DoesPlatformSupportVoiceCapture())
{
OutFailureReason = TEXT("Microphone capture is unsupported on this platform.");
ResetCaptureState(true);
SetStatus(OutFailureReason, true);
return false;
}
const FString DeviceName = Preferences.SpeechMicrophoneId.Equals(
TEXT("system-default"),
ESearchCase::IgnoreCase)
? FString()
: Preferences.SpeechMicrophoneId;
ActiveVoiceCapture = FVoiceModule::Get().CreateVoiceCapture(
DeviceName,
CaptureSampleRateHz,
CaptureChannelCount);
if (!ActiveVoiceCapture.IsValid() || !ActiveVoiceCapture->Start())
{
OutFailureReason =
TEXT("HyperTwist could not open the selected microphone. Check Windows privacy settings or choose another device.");
ResetCaptureState(true);
SetStatus(OutFailureReason, true);
return false;
}
CapturedPcmBytes.Reset();
ActiveUtteranceId = FGuid::NewGuid().ToString(EGuidFormats::Digits);
ActiveCapturePath.Reset();
CaptureStartedAtSeconds = FPlatformTime::Seconds();
bCaptureActive = true;
SetComponentTickEnabled(true);
SetStatus(TEXT("Listening... release the shortcut or select Stop dictation."));
return true;
}
bool UHyperTwistDictationCaptureComponent::EndCapture(
FString& OutTranscript,
FString& OutFailureReason
)
{
OutTranscript.Reset();
OutFailureReason.Reset();
if (!bCaptureActive)
{
OutFailureReason = TEXT("Dictation is not currently recording.");
return false;
}
DrainCaptureBuffer(OutFailureReason);
if (ActiveVoiceCapture.IsValid())
{
ActiveVoiceCapture->Stop();
}
DrainCaptureBuffer(OutFailureReason);
const FHyperTwistTrainingCompanionSpeechSessionState SessionState =
UHyperTwistSpeechLibrary::GetActiveDictationSessionState(this);
if (CapturedPcmBytes.IsEmpty())
{
OutFailureReason =
TEXT("No microphone audio was captured. Check the selected device and Windows microphone permission.");
ResetCaptureState(true);
SetStatus(OutFailureReason, true);
return false;
}
ActiveCapturePath = BuildTransientCapturePath();
if (!WriteTransientWaveFile(ActiveCapturePath, CapturedPcmBytes))
{
OutFailureReason = TEXT("HyperTwist could not prepare the temporary dictation audio.");
ResetCaptureState(true);
SetStatus(OutFailureReason, true);
return false;
}
FHyperTwistSpeechUtteranceEnvelope Utterance;
Utterance.SessionId = SessionState.ActiveSessionId;
Utterance.UtteranceId = ActiveUtteranceId;
Utterance.CapturedAtUtc = FDateTime::UtcNow().ToIso8601();
Utterance.AudioRef = ActiveCapturePath;
Utterance.SourceTimestampMs = FMath::RoundToInt(
static_cast<float>(
(FPlatformTime::Seconds() - CaptureStartedAtSeconds) * 1000.0));
Utterance.SampleCount =
CapturedPcmBytes.Num()
/ HyperTwistDictationCaptureComponentInternal::BytesPerPcm16Sample;
Utterance.SampleRateHz = CaptureSampleRateHz;
Utterance.ChannelCount = CaptureChannelCount;
Utterance.SpeechStartMs = 0;
Utterance.SpeechEndMs = FMath::Max(Utterance.SourceTimestampMs, 1);
Utterance.SilenceGapMs = 0;
Utterance.EnergyThreshold = 0.0f;
const FHyperTwistSpeechTranscriptResult Result =
UHyperTwistSpeechLibrary::SubmitDictationUtterance(this, Utterance);
FString CloseError;
UHyperTwistSpeechLibrary::EndDictationSession(this, CloseError, true);
OutTranscript = Result.TranscriptText.TrimStartAndEnd();
ResetCaptureState(false);
if (OutTranscript.IsEmpty())
{
OutFailureReason = Result.Warnings.IsEmpty()
? TEXT("The transcription service returned no text.")
: FString::Printf(
TEXT("Transcription failed: %s"),
*FString::Join(Result.Warnings, TEXT(", ")));
SetStatus(OutFailureReason, true);
return false;
}
if (!CloseError.IsEmpty())
{
SetStatus(
FString::Printf(
TEXT("Dictation complete; audio cleanup reported: %s"),
*CloseError),
true);
}
else
{
SetStatus(TEXT("Transcript ready."));
}
OnTranscriptReady.Broadcast(OutTranscript);
return true;
}
void UHyperTwistDictationCaptureComponent::CancelCapture()
{
if (!bCaptureActive && !ActiveVoiceCapture.IsValid())
{
return;
}
ResetCaptureState(true);
SetStatus(TEXT("Dictation cancelled."));
}
bool UHyperTwistDictationCaptureComponent::IsCapturing() const
{
return bCaptureActive;
}
FString UHyperTwistDictationCaptureComponent::GetStatus() const
{
return CurrentStatus;
}
bool UHyperTwistDictationCaptureComponent::DrainCaptureBuffer(
FString& OutFailureReason
)
{
OutFailureReason.Reset();
if (!ActiveVoiceCapture.IsValid())
{
OutFailureReason = TEXT("The microphone capture route is unavailable.");
return false;
}
uint32 AvailableBytes = 0;
const EVoiceCaptureState::Type CaptureState =
ActiveVoiceCapture->GetCaptureState(AvailableBytes);
if (CaptureState == EVoiceCaptureState::NoData
|| CaptureState == EVoiceCaptureState::Stopping
|| CaptureState == EVoiceCaptureState::NotCapturing)
{
return true;
}
if (CaptureState != EVoiceCaptureState::Ok
&& CaptureState != EVoiceCaptureState::BufferTooSmall)
{
OutFailureReason = FString::Printf(
TEXT("Microphone capture failed (%s)."),
EVoiceCaptureState::ToString(CaptureState));
return false;
}
if (AvailableBytes == 0)
{
return true;
}
const int64 MaximumByteCount =
static_cast<int64>(CaptureSampleRateHz)
* FMath::Max(CaptureChannelCount, 1)
* HyperTwistDictationCaptureComponentInternal::BytesPerPcm16Sample
* FMath::Max(FMath::CeilToInt(MaximumCaptureSeconds), 1);
if (static_cast<int64>(CapturedPcmBytes.Num()) + AvailableBytes
> MaximumByteCount)
{
OutFailureReason = TEXT("Dictation reached its safe recording limit.");
return false;
}
const int32 BufferOffset = CapturedPcmBytes.Num();
CapturedPcmBytes.AddUninitialized(static_cast<int32>(AvailableBytes));
uint32 ReadBytes = 0;
const EVoiceCaptureState::Type ReadState = ActiveVoiceCapture->GetVoiceData(
CapturedPcmBytes.GetData() + BufferOffset,
AvailableBytes,
ReadBytes);
if (ReadState != EVoiceCaptureState::Ok
&& ReadState != EVoiceCaptureState::NoData)
{
CapturedPcmBytes.SetNum(BufferOffset);
OutFailureReason = FString::Printf(
TEXT("Microphone read failed (%s)."),
EVoiceCaptureState::ToString(ReadState));
return false;
}
CapturedPcmBytes.SetNum(BufferOffset + static_cast<int32>(ReadBytes));
return true;
}
bool UHyperTwistDictationCaptureComponent::WriteTransientWaveFile(
const FString& FilePath,
const TArray<uint8>& PcmBytes
) const
{
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
if (!PlatformFile.CreateDirectoryTree(*FPaths::GetPath(FilePath)))
{
return false;
}
TArray<uint8> WaveBytes;
WaveBytes.Reserve(44 + PcmBytes.Num());
HyperTwistDictationCaptureComponentInternal::AppendWaveHeader(
WaveBytes,
PcmBytes.Num(),
CaptureSampleRateHz,
CaptureChannelCount);
WaveBytes.Append(PcmBytes);
return FFileHelper::SaveArrayToFile(WaveBytes, *FilePath);
}
void UHyperTwistDictationCaptureComponent::ResetCaptureState(
const bool bCloseDictationSession
)
{
if (ActiveVoiceCapture.IsValid())
{
ActiveVoiceCapture->Stop();
ActiveVoiceCapture.Reset();
}
if (bCloseDictationSession)
{
FString CloseError;
UHyperTwistSpeechLibrary::EndDictationSession(this, CloseError, true);
}
if (!ActiveCapturePath.IsEmpty())
{
FPlatformFileManager::Get().GetPlatformFile().DeleteFile(*ActiveCapturePath);
}
SetComponentTickEnabled(false);
CapturedPcmBytes.Reset();
ActiveUtteranceId.Reset();
ActiveCapturePath.Reset();
CaptureStartedAtSeconds = 0.0;
bCaptureActive = false;
}
void UHyperTwistDictationCaptureComponent::SetStatus(
const FString& NewStatus,
const bool bError
)
{
CurrentStatus = NewStatus;
OnStateChanged.Broadcast(CurrentStatus, bError);
}
FString UHyperTwistDictationCaptureComponent::BuildTransientCapturePath() const
{
return FPaths::Combine(
FPaths::ProjectSavedDir(),
TEXT("VoiceCaptures"),
TEXT("Transient"),
FString::Printf(TEXT("%s.wav"), *ActiveUtteranceId));
}

View file

@ -0,0 +1,445 @@
#include "HyperTwistUX/HyperTwistFourDimensionalHUDWidget.h"
#include "Blueprint/WidgetTree.h"
#include "Components/Border.h"
#include "Components/Button.h"
#include "Components/ButtonSlot.h"
#include "Components/HorizontalBox.h"
#include "Components/HorizontalBoxSlot.h"
#include "Components/Overlay.h"
#include "Components/OverlaySlot.h"
#include "Components/SizeBox.h"
#include "Components/TextBlock.h"
#include "Components/VerticalBox.h"
#include "Components/VerticalBoxSlot.h"
namespace HyperTwistFourDimensionalHudWidgetInternal
{
const FLinearColor PanelColor(0.012f, 0.026f, 0.043f, 0.94f);
const FLinearColor CardColor(0.035f, 0.070f, 0.095f, 0.98f);
const FLinearColor AccentColor(0.10f, 0.92f, 0.80f, 1.0f);
const FLinearColor WarmColor(1.0f, 0.63f, 0.24f, 1.0f);
const FLinearColor TextColor(0.93f, 0.97f, 1.0f, 1.0f);
const FLinearColor MutedColor(0.62f, 0.72f, 0.80f, 1.0f);
}
UHyperTwistFourDimensionalHUDWidget::UHyperTwistFourDimensionalHUDWidget(
const FObjectInitializer& ObjectInitializer
)
: Super(ObjectInitializer)
{
SetIsFocusable(false);
}
TSharedRef<SWidget> UHyperTwistFourDimensionalHUDWidget::RebuildWidget()
{
Initialize();
EnsureWidgetTreeBuilt();
return Super::RebuildWidget();
}
bool UHyperTwistFourDimensionalHUDWidget::PrepareHudSurface()
{
EnsureWidgetTreeBuilt();
return IsHudSurfaceReady();
}
bool UHyperTwistFourDimensionalHUDWidget::IsHudSurfaceReady() const
{
return WidgetTree != nullptr
&& WidgetTree->RootWidget != nullptr
&& TitleText != nullptr
&& StateText != nullptr
&& SelectionText != nullptr
&& PersistenceText != nullptr
&& SelectionActionRow != nullptr
&& TurnActionRow != nullptr
&& ScrambleActionRow != nullptr;
}
void UHyperTwistFourDimensionalHUDWidget::ConfigureSurface(
const FString& Title,
const FString& StateSummary,
const FString& SelectionSummary,
const FString& LastAction,
const FString& PersistenceSummary,
const bool bCellFirstMode
)
{
EnsureWidgetTreeBuilt();
if (TitleText != nullptr)
{
TitleText->SetText(FText::FromString(Title));
}
if (StateText != nullptr)
{
StateText->SetText(FText::FromString(StateSummary));
StateText->SetColorAndOpacity(FSlateColor(
StateSummary.StartsWith(TEXT("SOLVED"))
? HyperTwistFourDimensionalHudWidgetInternal::AccentColor
: HyperTwistFourDimensionalHudWidgetInternal::TextColor));
}
if (SelectionText != nullptr)
{
SelectionText->SetText(FText::FromString(SelectionSummary));
}
if (LastActionText != nullptr)
{
LastActionText->SetText(FText::FromString(
LastAction.IsEmpty()
? TEXT("Last action: ready")
: FString::Printf(TEXT("Last action: %s"), *LastAction)));
}
if (PersistenceText != nullptr)
{
PersistenceText->SetText(FText::FromString(
FString::Printf(TEXT("Session: %s"), *PersistenceSummary)));
}
if (GuidanceText != nullptr)
{
GuidanceText->SetText(FText::FromString(
bCellFirstMode
? TEXT("Left click turns a cell clockwise; right click turns it counter-clockwise. Shift + right drag or middle drag orbits. Wheel zooms.")
: TEXT("X/Y/Z/W selects an axis; 1-6 or [ ] selects a layer; Q/E selects a plane; comma/period turns. Shift + right drag or middle drag orbits.")));
}
if (SelectionActionRow != nullptr)
{
SelectionActionRow->SetVisibility(
bCellFirstMode ? ESlateVisibility::Collapsed : ESlateVisibility::Visible);
}
if (TurnActionRow != nullptr)
{
TurnActionRow->SetVisibility(
bCellFirstMode ? ESlateVisibility::Collapsed : ESlateVisibility::Visible);
}
if (ScrambleActionRow != nullptr)
{
ScrambleActionRow->SetVisibility(ESlateVisibility::Visible);
}
if (ShellButton != nullptr)
{
ShellButton->SetVisibility(
bCellFirstMode ? ESlateVisibility::Collapsed : ESlateVisibility::Visible);
}
}
void UHyperTwistFourDimensionalHUDWidget::EnsureWidgetTreeBuilt()
{
using namespace HyperTwistFourDimensionalHudWidgetInternal;
if (WidgetTree == nullptr || WidgetTree->RootWidget != nullptr)
{
return;
}
UOverlay* Root = WidgetTree->ConstructWidget<UOverlay>(
UOverlay::StaticClass(),
TEXT("FourDimensionalHudRoot"));
Root->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
WidgetTree->RootWidget = Root;
USizeBox* PanelSize = WidgetTree->ConstructWidget<USizeBox>(
USizeBox::StaticClass(),
TEXT("FourDimensionalHudPanelSize"));
PanelSize->SetWidthOverride(520.0f);
if (UOverlaySlot* PanelSlot = Root->AddChildToOverlay(PanelSize))
{
PanelSlot->SetHorizontalAlignment(HAlign_Left);
PanelSlot->SetVerticalAlignment(VAlign_Top);
PanelSlot->SetPadding(FMargin(20.0f));
}
UBorder* Panel = WidgetTree->ConstructWidget<UBorder>(
UBorder::StaticClass(),
TEXT("FourDimensionalHudPanel"));
Panel->SetPadding(FMargin(20.0f));
Panel->SetBrushColor(PanelColor);
PanelSize->AddChild(Panel);
UVerticalBox* Content = WidgetTree->ConstructWidget<UVerticalBox>(
UVerticalBox::StaticClass(),
TEXT("FourDimensionalHudContent"));
Panel->AddChild(Content);
UTextBlock* Eyebrow = MakeText(
TEXT("FOUR-DIMENSIONAL PUZZLE LAB"),
11,
AccentColor);
Content->AddChildToVerticalBox(Eyebrow);
TitleText = MakeText(
TEXT("4D Cube"),
27,
TextColor,
TEXT("FourDimensionalHudTitle"));
Content->AddChildToVerticalBox(TitleText);
StateText = MakeText(
TEXT("SOLVED"),
14,
AccentColor,
TEXT("FourDimensionalHudState"));
if (UVerticalBoxSlot* StateSlot = Content->AddChildToVerticalBox(StateText))
{
StateSlot->SetPadding(FMargin(0.0f, 8.0f, 0.0f, 0.0f));
}
SelectionText = MakeText(
TEXT("Projection ready"),
13,
TextColor,
TEXT("FourDimensionalHudSelection"));
Content->AddChildToVerticalBox(SelectionText);
LastActionText = MakeText(
TEXT("Last action: ready"),
12,
MutedColor,
TEXT("FourDimensionalHudLastAction"));
Content->AddChildToVerticalBox(LastActionText);
PersistenceText = MakeText(
TEXT("Session: not loaded"),
12,
WarmColor,
TEXT("FourDimensionalHudPersistence"));
Content->AddChildToVerticalBox(PersistenceText);
SelectionActionRow = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("FourDimensionalHudSelectionActions"));
if (UVerticalBoxSlot* RowSlot =
Content->AddChildToVerticalBox(SelectionActionRow))
{
RowSlot->SetPadding(FMargin(0.0f, 14.0f, 0.0f, 4.0f));
}
UButton* AxisButton = AddButton(
SelectionActionRow,
TEXT("Cycle axis"),
TEXT("FourDimensionalHudAxisButton"));
UButton* PreviousLayerButton = AddButton(
SelectionActionRow,
TEXT("- Layer"),
TEXT("FourDimensionalHudPreviousLayerButton"));
UButton* NextLayerButton = AddButton(
SelectionActionRow,
TEXT("+ Layer"),
TEXT("FourDimensionalHudNextLayerButton"));
AxisButton->OnClicked.AddDynamic(
this,
&UHyperTwistFourDimensionalHUDWidget::HandleCycleAxis);
PreviousLayerButton->OnClicked.AddDynamic(
this,
&UHyperTwistFourDimensionalHUDWidget::HandlePreviousLayer);
NextLayerButton->OnClicked.AddDynamic(
this,
&UHyperTwistFourDimensionalHUDWidget::HandleNextLayer);
TurnActionRow = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("FourDimensionalHudTurnActions"));
if (UVerticalBoxSlot* RowSlot = Content->AddChildToVerticalBox(TurnActionRow))
{
RowSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 4.0f));
}
UButton* PreviousPlaneButton = AddButton(
TurnActionRow,
TEXT("- Plane"),
TEXT("FourDimensionalHudPreviousPlaneButton"));
UButton* CounterClockwiseButton = AddButton(
TurnActionRow,
TEXT("Turn CCW"),
TEXT("FourDimensionalHudCounterClockwiseButton"));
UButton* ClockwiseButton = AddButton(
TurnActionRow,
TEXT("Turn CW"),
TEXT("FourDimensionalHudClockwiseButton"),
true);
UButton* NextPlaneButton = AddButton(
TurnActionRow,
TEXT("+ Plane"),
TEXT("FourDimensionalHudNextPlaneButton"));
PreviousPlaneButton->OnClicked.AddDynamic(
this,
&UHyperTwistFourDimensionalHUDWidget::HandlePreviousPlane);
CounterClockwiseButton->OnClicked.AddDynamic(
this,
&UHyperTwistFourDimensionalHUDWidget::HandleCounterClockwise);
ClockwiseButton->OnClicked.AddDynamic(
this,
&UHyperTwistFourDimensionalHUDWidget::HandleClockwise);
NextPlaneButton->OnClicked.AddDynamic(
this,
&UHyperTwistFourDimensionalHUDWidget::HandleNextPlane);
ScrambleActionRow = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("FourDimensionalHudScrambleActions"));
if (UVerticalBoxSlot* RowSlot = Content->AddChildToVerticalBox(ScrambleActionRow))
{
RowSlot->SetPadding(FMargin(0.0f, 14.0f, 0.0f, 4.0f));
}
UButton* ScrambleButton = AddButton(
ScrambleActionRow,
TEXT("Scramble"),
TEXT("FourDimensionalHudScrambleButton"),
true);
ScrambleButton->OnClicked.AddDynamic(
this,
&UHyperTwistFourDimensionalHUDWidget::HandleScramble);
UHorizontalBox* SessionActions = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("FourDimensionalHudSessionActions"));
if (UVerticalBoxSlot* RowSlot = Content->AddChildToVerticalBox(SessionActions))
{
RowSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 8.0f));
}
UButton* ResetButton = AddButton(
SessionActions,
TEXT("Reset"),
TEXT("FourDimensionalHudResetButton"));
ShellButton = AddButton(
SessionActions,
TEXT("Cell shell"),
TEXT("FourDimensionalHudShellButton"));
UButton* SaveButton = AddButton(
SessionActions,
TEXT("Save"),
TEXT("FourDimensionalHudSaveButton"));
UButton* LoadButton = AddButton(
SessionActions,
TEXT("Load"),
TEXT("FourDimensionalHudLoadButton"));
ResetButton->OnClicked.AddDynamic(
this,
&UHyperTwistFourDimensionalHUDWidget::HandleReset);
ShellButton->OnClicked.AddDynamic(
this,
&UHyperTwistFourDimensionalHUDWidget::HandleToggleShell);
SaveButton->OnClicked.AddDynamic(
this,
&UHyperTwistFourDimensionalHUDWidget::HandleSave);
LoadButton->OnClicked.AddDynamic(
this,
&UHyperTwistFourDimensionalHUDWidget::HandleLoad);
GuidanceText = MakeText(
TEXT("Select a projection control or use the keyboard."),
11,
MutedColor,
TEXT("FourDimensionalHudGuidance"));
Content->AddChildToVerticalBox(GuidanceText);
UTextBlock* GlobalHelp = MakeText(
TEXT("Esc menu | F3 coach | F10 library | S save | L load"),
10,
AccentColor);
if (UVerticalBoxSlot* HelpSlot = Content->AddChildToVerticalBox(GlobalHelp))
{
HelpSlot->SetPadding(FMargin(0.0f, 8.0f, 0.0f, 0.0f));
}
}
UTextBlock* UHyperTwistFourDimensionalHUDWidget::MakeText(
const FString& Text,
const int32 FontSize,
const FLinearColor& Color,
const FName& Name
)
{
UTextBlock* TextBlock = WidgetTree->ConstructWidget<UTextBlock>(
UTextBlock::StaticClass(),
Name);
TextBlock->SetText(FText::FromString(Text));
TextBlock->SetAutoWrapText(true);
TextBlock->SetColorAndOpacity(FSlateColor(Color));
FSlateFontInfo Font = TextBlock->GetFont();
Font.Size = FontSize;
TextBlock->SetFont(Font);
return TextBlock;
}
UButton* UHyperTwistFourDimensionalHUDWidget::AddButton(
UHorizontalBox* Parent,
const FString& Label,
const FName& Name,
const bool bPrimary
)
{
using namespace HyperTwistFourDimensionalHudWidgetInternal;
UButton* Button = WidgetTree->ConstructWidget<UButton>(
UButton::StaticClass(),
Name);
Button->SetBackgroundColor(bPrimary ? AccentColor : CardColor);
UTextBlock* LabelText = MakeText(
Label,
11,
bPrimary ? FLinearColor(0.01f, 0.04f, 0.05f, 1.0f) : TextColor);
LabelText->SetJustification(ETextJustify::Center);
Button->AddChild(LabelText);
if (UButtonSlot* ContentSlot = Cast<UButtonSlot>(LabelText->Slot))
{
ContentSlot->SetPadding(FMargin(10.0f, 7.0f));
ContentSlot->SetHorizontalAlignment(HAlign_Center);
}
if (UHorizontalBoxSlot* ButtonSlot = Parent->AddChildToHorizontalBox(Button))
{
ButtonSlot->SetPadding(FMargin(2.0f));
ButtonSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
}
return Button;
}
void UHyperTwistFourDimensionalHUDWidget::HandleCycleAxis()
{
OnCycleAxisRequested.Broadcast();
}
void UHyperTwistFourDimensionalHUDWidget::HandlePreviousLayer()
{
OnPreviousLayerRequested.Broadcast();
}
void UHyperTwistFourDimensionalHUDWidget::HandleNextLayer()
{
OnNextLayerRequested.Broadcast();
}
void UHyperTwistFourDimensionalHUDWidget::HandlePreviousPlane()
{
OnPreviousPlaneRequested.Broadcast();
}
void UHyperTwistFourDimensionalHUDWidget::HandleNextPlane()
{
OnNextPlaneRequested.Broadcast();
}
void UHyperTwistFourDimensionalHUDWidget::HandleCounterClockwise()
{
OnCounterClockwiseRequested.Broadcast();
}
void UHyperTwistFourDimensionalHUDWidget::HandleClockwise()
{
OnClockwiseRequested.Broadcast();
}
void UHyperTwistFourDimensionalHUDWidget::HandleToggleShell()
{
OnToggleShellRequested.Broadcast();
}
void UHyperTwistFourDimensionalHUDWidget::HandleScramble()
{
OnScrambleRequested.Broadcast();
}
void UHyperTwistFourDimensionalHUDWidget::HandleReset()
{
OnResetRequested.Broadcast();
}
void UHyperTwistFourDimensionalHUDWidget::HandleSave()
{
OnSaveRequested.Broadcast();
}
void UHyperTwistFourDimensionalHUDWidget::HandleLoad()
{
OnLoadRequested.Broadcast();
}

View file

@ -0,0 +1,594 @@
#include "HyperTwistUX/HyperTwistHigherDimensionalHUDWidget.h"
#include "Blueprint/WidgetTree.h"
#include "Components/Border.h"
#include "Components/Button.h"
#include "Components/ButtonSlot.h"
#include "Components/HorizontalBox.h"
#include "Components/HorizontalBoxSlot.h"
#include "Components/Overlay.h"
#include "Components/OverlaySlot.h"
#include "Components/SizeBox.h"
#include "Components/TextBlock.h"
#include "Components/VerticalBox.h"
#include "Components/VerticalBoxSlot.h"
namespace HyperTwistHigherDimensionalHudWidgetInternal
{
const FLinearColor PanelColor(0.010f, 0.023f, 0.039f, 0.965f);
const FLinearColor CardColor(0.030f, 0.064f, 0.090f, 0.985f);
const FLinearColor CyanAccent(0.12f, 0.95f, 1.0f, 1.0f);
const FLinearColor OrangeAccent(1.0f, 0.60f, 0.22f, 1.0f);
const FLinearColor TextColor(0.93f, 0.97f, 1.0f, 1.0f);
const FLinearColor MutedColor(0.62f, 0.72f, 0.80f, 1.0f);
const FLinearColor WarmColor(1.0f, 0.73f, 0.37f, 1.0f);
}
UHyperTwistHigherDimensionalHUDWidget::
UHyperTwistHigherDimensionalHUDWidget(
const FObjectInitializer& ObjectInitializer
)
: Super(ObjectInitializer)
{
SetIsFocusable(false);
}
TSharedRef<SWidget> UHyperTwistHigherDimensionalHUDWidget::RebuildWidget()
{
Initialize();
EnsureWidgetTreeBuilt();
return Super::RebuildWidget();
}
bool UHyperTwistHigherDimensionalHUDWidget::PrepareHudSurface()
{
EnsureWidgetTreeBuilt();
return IsHudSurfaceReady();
}
bool UHyperTwistHigherDimensionalHUDWidget::IsHudSurfaceReady() const
{
return WidgetTree != nullptr
&& WidgetTree->RootWidget != nullptr
&& TitleText != nullptr
&& StateText != nullptr
&& SelectionText != nullptr
&& ProjectionText != nullptr
&& PositiveTurnButton != nullptr
&& NegativeTurnButton != nullptr;
}
void UHyperTwistHigherDimensionalHUDWidget::ConfigureSurface(
const bool bMagic120Cell,
const bool bPositive5DFace,
const bool bAutoRotating,
const FString& Title,
const FString& StateSummary,
const FString& SelectionSummary,
const FString& ProjectionSummary,
const FString& LastAction,
const FString& PersistenceSummary
)
{
using namespace HyperTwistHigherDimensionalHudWidgetInternal;
EnsureWidgetTreeBuilt();
const FLinearColor Accent = bMagic120Cell ? CyanAccent : OrangeAccent;
if (EyebrowText != nullptr)
{
EyebrowText->SetText(FText::FromString(
bMagic120Cell
? TEXT("EXACT 120-CELL LAB")
: TEXT("EXACT FIVE-DIMENSIONAL LAB")));
EyebrowText->SetColorAndOpacity(FSlateColor(Accent));
}
if (TitleText != nullptr)
{
TitleText->SetText(FText::FromString(Title));
}
if (StateText != nullptr)
{
StateText->SetText(FText::FromString(StateSummary));
StateText->SetColorAndOpacity(FSlateColor(
StateSummary.Contains(TEXT("solved"), ESearchCase::IgnoreCase)
? Accent
: TextColor));
}
if (SelectionText != nullptr)
{
SelectionText->SetText(FText::FromString(SelectionSummary));
}
if (ProjectionText != nullptr)
{
ProjectionText->SetText(FText::FromString(ProjectionSummary));
}
if (LastActionText != nullptr)
{
LastActionText->SetText(FText::FromString(
LastAction.IsEmpty()
? TEXT("Last action: ready")
: FString::Printf(TEXT("Last action: %s"), *LastAction)));
}
if (PersistenceText != nullptr)
{
PersistenceText->SetText(FText::FromString(
FString::Printf(TEXT("Session: %s"), *PersistenceSummary)));
}
SetButtonLabel(
PreviousPrimaryButton,
bMagic120Cell ? TEXT("Cell -") : TEXT("Face -"));
SetButtonLabel(
NextPrimaryButton,
bMagic120Cell ? TEXT("Cell +") : TEXT("Face +"));
SetButtonLabel(
PreviousSecondaryButton,
bMagic120Cell ? TEXT("Twist -") : TEXT("Layer -"));
SetButtonLabel(
NextSecondaryButton,
bMagic120Cell ? TEXT("Twist +") : TEXT("Layer +"));
SetButtonLabel(
NegativeTurnButton,
bMagic120Cell ? TEXT("Inverse") : TEXT("-90 degrees"));
SetButtonLabel(
PositiveTurnButton,
bMagic120Cell ? TEXT("Forward") : TEXT("+90 degrees"));
SetButtonLabel(
FaceSideButton,
bPositive5DFace ? TEXT("Face side: +") : TEXT("Face side: -"));
SetButtonLabel(
AutoRotateButton,
bAutoRotating ? TEXT("Pause rotation") : TEXT("Resume rotation"));
if (TertiaryActionRow != nullptr)
{
TertiaryActionRow->SetVisibility(
bMagic120Cell
? ESlateVisibility::Collapsed
: ESlateVisibility::Visible);
}
if (FaceSideButton != nullptr)
{
FaceSideButton->SetVisibility(
bMagic120Cell
? ESlateVisibility::Collapsed
: ESlateVisibility::Visible);
}
if (GuidanceText != nullptr)
{
GuidanceText->SetText(FText::FromString(
bMagic120Cell
? TEXT("Choose one of 120 cells and one of its 62 legal face, edge, or vertex axes, then apply the exact forward or inverse permutation.")
: TEXT("Choose a signed face, any non-empty layer mask, and an orthogonal rotation plane, then apply an exact quarter turn.")));
}
}
void UHyperTwistHigherDimensionalHUDWidget::EnsureWidgetTreeBuilt()
{
using namespace HyperTwistHigherDimensionalHudWidgetInternal;
if (WidgetTree == nullptr || WidgetTree->RootWidget != nullptr)
{
return;
}
UOverlay* Root = WidgetTree->ConstructWidget<UOverlay>(
UOverlay::StaticClass(),
TEXT("HigherDimensionalHudRoot"));
Root->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
WidgetTree->RootWidget = Root;
USizeBox* PanelSize = WidgetTree->ConstructWidget<USizeBox>(
USizeBox::StaticClass(),
TEXT("HigherDimensionalHudPanelSize"));
PanelSize->SetWidthOverride(570.0f);
if (UOverlaySlot* PanelSlot = Root->AddChildToOverlay(PanelSize))
{
PanelSlot->SetHorizontalAlignment(HAlign_Left);
PanelSlot->SetVerticalAlignment(VAlign_Top);
PanelSlot->SetPadding(FMargin(20.0f));
}
UBorder* Panel = WidgetTree->ConstructWidget<UBorder>(
UBorder::StaticClass(),
TEXT("HigherDimensionalHudPanel"));
Panel->SetPadding(FMargin(20.0f));
Panel->SetBrushColor(PanelColor);
PanelSize->AddChild(Panel);
UVerticalBox* Content = WidgetTree->ConstructWidget<UVerticalBox>(
UVerticalBox::StaticClass(),
TEXT("HigherDimensionalHudContent"));
Panel->AddChild(Content);
EyebrowText = MakeText(
TEXT("HIGHER-DIMENSIONAL PUZZLE LAB"),
11,
CyanAccent,
TEXT("HigherDimensionalHudEyebrow"));
Content->AddChildToVerticalBox(EyebrowText);
TitleText = MakeText(
TEXT("Higher-Dimensional Puzzle"),
27,
TextColor,
TEXT("HigherDimensionalHudTitle"));
Content->AddChildToVerticalBox(TitleText);
StateText = MakeText(
TEXT("Exact state ready"),
14,
CyanAccent,
TEXT("HigherDimensionalHudState"));
if (UVerticalBoxSlot* StateSlot = Content->AddChildToVerticalBox(StateText))
{
StateSlot->SetPadding(FMargin(0.0f, 8.0f, 0.0f, 0.0f));
}
SelectionText = MakeText(
TEXT("Selection ready"),
13,
TextColor,
TEXT("HigherDimensionalHudSelection"));
Content->AddChildToVerticalBox(SelectionText);
ProjectionText = MakeText(
TEXT("Projection ready"),
12,
MutedColor,
TEXT("HigherDimensionalHudProjection"));
Content->AddChildToVerticalBox(ProjectionText);
LastActionText = MakeText(
TEXT("Last action: ready"),
12,
MutedColor,
TEXT("HigherDimensionalHudLastAction"));
Content->AddChildToVerticalBox(LastActionText);
PersistenceText = MakeText(
TEXT("Session: not loaded"),
12,
WarmColor,
TEXT("HigherDimensionalHudPersistence"));
Content->AddChildToVerticalBox(PersistenceText);
UHorizontalBox* PrimaryRow = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("HigherDimensionalHudPrimaryActions"));
if (UVerticalBoxSlot* RowSlot = Content->AddChildToVerticalBox(PrimaryRow))
{
RowSlot->SetPadding(FMargin(0.0f, 14.0f, 0.0f, 4.0f));
}
PreviousPrimaryButton = AddButton(
PrimaryRow,
TEXT("Primary -"),
TEXT("HigherDimensionalHudPreviousPrimary"));
NextPrimaryButton = AddButton(
PrimaryRow,
TEXT("Primary +"),
TEXT("HigherDimensionalHudNextPrimary"));
PreviousSecondaryButton = AddButton(
PrimaryRow,
TEXT("Secondary -"),
TEXT("HigherDimensionalHudPreviousSecondary"));
NextSecondaryButton = AddButton(
PrimaryRow,
TEXT("Secondary +"),
TEXT("HigherDimensionalHudNextSecondary"));
PreviousPrimaryButton->OnClicked.AddDynamic(
this,
&UHyperTwistHigherDimensionalHUDWidget::HandlePreviousPrimary);
NextPrimaryButton->OnClicked.AddDynamic(
this,
&UHyperTwistHigherDimensionalHUDWidget::HandleNextPrimary);
PreviousSecondaryButton->OnClicked.AddDynamic(
this,
&UHyperTwistHigherDimensionalHUDWidget::HandlePreviousSecondary);
NextSecondaryButton->OnClicked.AddDynamic(
this,
&UHyperTwistHigherDimensionalHUDWidget::HandleNextSecondary);
TertiaryActionRow = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("HigherDimensionalHudTertiaryActions"));
if (UVerticalBoxSlot* RowSlot =
Content->AddChildToVerticalBox(TertiaryActionRow))
{
RowSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 4.0f));
}
PreviousTertiaryButton = AddButton(
TertiaryActionRow,
TEXT("Plane -"),
TEXT("HigherDimensionalHudPreviousTertiary"));
NextTertiaryButton = AddButton(
TertiaryActionRow,
TEXT("Plane +"),
TEXT("HigherDimensionalHudNextTertiary"));
FaceSideButton = AddButton(
TertiaryActionRow,
TEXT("Face side: +"),
TEXT("HigherDimensionalHudFaceSide"));
PreviousTertiaryButton->OnClicked.AddDynamic(
this,
&UHyperTwistHigherDimensionalHUDWidget::HandlePreviousTertiary);
NextTertiaryButton->OnClicked.AddDynamic(
this,
&UHyperTwistHigherDimensionalHUDWidget::HandleNextTertiary);
FaceSideButton->OnClicked.AddDynamic(
this,
&UHyperTwistHigherDimensionalHUDWidget::HandleToggleFaceSide);
UHorizontalBox* TurnRow = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("HigherDimensionalHudTurnActions"));
if (UVerticalBoxSlot* RowSlot = Content->AddChildToVerticalBox(TurnRow))
{
RowSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 4.0f));
}
NegativeTurnButton = AddButton(
TurnRow,
TEXT("Inverse"),
TEXT("HigherDimensionalHudNegativeTurn"));
PositiveTurnButton = AddButton(
TurnRow,
TEXT("Forward"),
TEXT("HigherDimensionalHudPositiveTurn"),
true);
NegativeTurnButton->OnClicked.AddDynamic(
this,
&UHyperTwistHigherDimensionalHUDWidget::HandleNegativeTurn);
PositiveTurnButton->OnClicked.AddDynamic(
this,
&UHyperTwistHigherDimensionalHUDWidget::HandlePositiveTurn);
UHorizontalBox* ProjectionRow = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("HigherDimensionalHudProjectionActions"));
if (UVerticalBoxSlot* RowSlot = Content->AddChildToVerticalBox(ProjectionRow))
{
RowSlot->SetPadding(FMargin(0.0f, 8.0f, 0.0f, 4.0f));
}
UButton* PreviousLayerButton = AddButton(
ProjectionRow,
TEXT("View layer -"),
TEXT("HigherDimensionalHudPreviousProjectionLayer"));
UButton* NextLayerButton = AddButton(
ProjectionRow,
TEXT("View layer +"),
TEXT("HigherDimensionalHudNextProjectionLayer"));
AutoRotateButton = AddButton(
ProjectionRow,
TEXT("Pause rotation"),
TEXT("HigherDimensionalHudAutoRotate"));
PreviousLayerButton->OnClicked.AddDynamic(
this,
&UHyperTwistHigherDimensionalHUDWidget::HandlePreviousProjectionLayer);
NextLayerButton->OnClicked.AddDynamic(
this,
&UHyperTwistHigherDimensionalHUDWidget::HandleNextProjectionLayer);
AutoRotateButton->OnClicked.AddDynamic(
this,
&UHyperTwistHigherDimensionalHUDWidget::HandleToggleAutoRotate);
UHorizontalBox* SessionRow = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("HigherDimensionalHudSessionActions"));
if (UVerticalBoxSlot* RowSlot = Content->AddChildToVerticalBox(SessionRow))
{
RowSlot->SetPadding(FMargin(0.0f, 8.0f, 0.0f, 4.0f));
}
UButton* ScrambleButton = AddButton(
SessionRow,
TEXT("Scramble"),
TEXT("HigherDimensionalHudScramble"),
true);
UButton* ResetButton = AddButton(
SessionRow,
TEXT("Reset"),
TEXT("HigherDimensionalHudReset"));
UButton* SaveButton = AddButton(
SessionRow,
TEXT("Save"),
TEXT("HigherDimensionalHudSave"));
UButton* LoadButton = AddButton(
SessionRow,
TEXT("Load"),
TEXT("HigherDimensionalHudLoad"));
ScrambleButton->OnClicked.AddDynamic(
this,
&UHyperTwistHigherDimensionalHUDWidget::HandleScramble);
ResetButton->OnClicked.AddDynamic(
this,
&UHyperTwistHigherDimensionalHUDWidget::HandleReset);
SaveButton->OnClicked.AddDynamic(
this,
&UHyperTwistHigherDimensionalHUDWidget::HandleSave);
LoadButton->OnClicked.AddDynamic(
this,
&UHyperTwistHigherDimensionalHUDWidget::HandleLoad);
UHorizontalBox* UtilityRow = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("HigherDimensionalHudUtilityActions"));
if (UVerticalBoxSlot* RowSlot = Content->AddChildToVerticalBox(UtilityRow))
{
RowSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 8.0f));
}
UButton* CoachButton = AddButton(
UtilityRow,
TEXT("Open coach"),
TEXT("HigherDimensionalHudCoach"));
UButton* MenuButton = AddButton(
UtilityRow,
TEXT("Menu and settings"),
TEXT("HigherDimensionalHudMenu"));
CoachButton->OnClicked.AddDynamic(
this,
&UHyperTwistHigherDimensionalHUDWidget::HandleCoach);
MenuButton->OnClicked.AddDynamic(
this,
&UHyperTwistHigherDimensionalHUDWidget::HandleMenu);
GuidanceText = MakeText(
TEXT("Select a legal move and apply it."),
11,
MutedColor,
TEXT("HigherDimensionalHudGuidance"));
Content->AddChildToVerticalBox(GuidanceText);
UTextBlock* HelpText = MakeText(
TEXT("Arrows select | , / . turn | Wheel zoom | Shift+RMB or MMB orbit | Esc menu | F3 coach"),
10,
CyanAccent);
if (UVerticalBoxSlot* HelpSlot = Content->AddChildToVerticalBox(HelpText))
{
HelpSlot->SetPadding(FMargin(0.0f, 8.0f, 0.0f, 0.0f));
}
}
UTextBlock* UHyperTwistHigherDimensionalHUDWidget::MakeText(
const FString& Text,
const int32 FontSize,
const FLinearColor& Color,
const FName& Name
)
{
UTextBlock* TextBlock = WidgetTree->ConstructWidget<UTextBlock>(
UTextBlock::StaticClass(),
Name);
TextBlock->SetText(FText::FromString(Text));
TextBlock->SetAutoWrapText(true);
TextBlock->SetColorAndOpacity(FSlateColor(Color));
FSlateFontInfo Font = TextBlock->GetFont();
Font.Size = FontSize;
TextBlock->SetFont(Font);
return TextBlock;
}
UButton* UHyperTwistHigherDimensionalHUDWidget::AddButton(
UHorizontalBox* Parent,
const FString& Label,
const FName& Name,
const bool bPrimary
)
{
using namespace HyperTwistHigherDimensionalHudWidgetInternal;
UButton* Button = WidgetTree->ConstructWidget<UButton>(
UButton::StaticClass(),
Name);
Button->SetBackgroundColor(bPrimary ? CyanAccent : CardColor);
UTextBlock* LabelText = MakeText(
Label,
11,
bPrimary ? FLinearColor(0.01f, 0.04f, 0.05f, 1.0f) : TextColor);
LabelText->SetJustification(ETextJustify::Center);
Button->AddChild(LabelText);
if (UButtonSlot* ContentSlot = Cast<UButtonSlot>(LabelText->Slot))
{
ContentSlot->SetPadding(FMargin(9.0f, 7.0f));
ContentSlot->SetHorizontalAlignment(HAlign_Center);
}
if (UHorizontalBoxSlot* ButtonSlot = Parent->AddChildToHorizontalBox(Button))
{
ButtonSlot->SetPadding(FMargin(2.0f));
ButtonSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
}
return Button;
}
void UHyperTwistHigherDimensionalHUDWidget::SetButtonLabel(
UButton* Button,
const FString& Label
)
{
if (Button != nullptr)
{
if (UTextBlock* LabelText = Cast<UTextBlock>(Button->GetContent()))
{
LabelText->SetText(FText::FromString(Label));
}
}
}
void UHyperTwistHigherDimensionalHUDWidget::HandlePreviousPrimary()
{
OnPreviousPrimaryRequested.Broadcast();
}
void UHyperTwistHigherDimensionalHUDWidget::HandleNextPrimary()
{
OnNextPrimaryRequested.Broadcast();
}
void UHyperTwistHigherDimensionalHUDWidget::HandlePreviousSecondary()
{
OnPreviousSecondaryRequested.Broadcast();
}
void UHyperTwistHigherDimensionalHUDWidget::HandleNextSecondary()
{
OnNextSecondaryRequested.Broadcast();
}
void UHyperTwistHigherDimensionalHUDWidget::HandlePreviousTertiary()
{
OnPreviousTertiaryRequested.Broadcast();
}
void UHyperTwistHigherDimensionalHUDWidget::HandleNextTertiary()
{
OnNextTertiaryRequested.Broadcast();
}
void UHyperTwistHigherDimensionalHUDWidget::HandleToggleFaceSide()
{
OnToggleFaceSideRequested.Broadcast();
}
void UHyperTwistHigherDimensionalHUDWidget::HandleNegativeTurn()
{
OnNegativeTurnRequested.Broadcast();
}
void UHyperTwistHigherDimensionalHUDWidget::HandlePositiveTurn()
{
OnPositiveTurnRequested.Broadcast();
}
void UHyperTwistHigherDimensionalHUDWidget::HandlePreviousProjectionLayer()
{
OnPreviousProjectionLayerRequested.Broadcast();
}
void UHyperTwistHigherDimensionalHUDWidget::HandleNextProjectionLayer()
{
OnNextProjectionLayerRequested.Broadcast();
}
void UHyperTwistHigherDimensionalHUDWidget::HandleToggleAutoRotate()
{
OnToggleAutoRotateRequested.Broadcast();
}
void UHyperTwistHigherDimensionalHUDWidget::HandleScramble()
{
OnScrambleRequested.Broadcast();
}
void UHyperTwistHigherDimensionalHUDWidget::HandleReset()
{
OnResetRequested.Broadcast();
}
void UHyperTwistHigherDimensionalHUDWidget::HandleSave()
{
OnSaveRequested.Broadcast();
}
void UHyperTwistHigherDimensionalHUDWidget::HandleLoad()
{
OnLoadRequested.Broadcast();
}
void UHyperTwistHigherDimensionalHUDWidget::HandleCoach()
{
OnCoachRequested.Broadcast();
}
void UHyperTwistHigherDimensionalHUDWidget::HandleMenu()
{
OnMenuRequested.Broadcast();
}

View file

@ -0,0 +1,337 @@
#include "HyperTwistUX/HyperTwistPauseMenuWidget.h"
#include "Blueprint/WidgetTree.h"
#include "Components/Border.h"
#include "Components/Button.h"
#include "Components/ButtonSlot.h"
#include "Components/HorizontalBox.h"
#include "Components/HorizontalBoxSlot.h"
#include "Components/Overlay.h"
#include "Components/OverlaySlot.h"
#include "Components/SizeBox.h"
#include "Components/Spacer.h"
#include "Components/TextBlock.h"
#include "Components/VerticalBox.h"
#include "Components/VerticalBoxSlot.h"
#include "Components/WidgetSwitcher.h"
#include "HyperTwistUX/HyperTwistSettingsPanelWidget.h"
#include "Input/Reply.h"
#include "InputCoreTypes.h"
namespace HyperTwistPauseMenuWidgetInternal
{
const FLinearColor BackdropColor(0.004f, 0.009f, 0.018f, 0.88f);
const FLinearColor PanelColor(0.018f, 0.030f, 0.049f, 0.99f);
const FLinearColor CardColor(0.045f, 0.075f, 0.108f, 1.0f);
const FLinearColor AccentColor(0.10f, 0.92f, 0.80f, 1.0f);
const FLinearColor WarmColor(1.0f, 0.61f, 0.22f, 1.0f);
const FLinearColor PrimaryText(0.94f, 0.98f, 1.0f, 1.0f);
const FLinearColor MutedText(0.62f, 0.72f, 0.80f, 1.0f);
UTextBlock* AddText(
UWidgetTree* WidgetTree,
UVerticalBox* Parent,
const FString& Text,
const int32 Size,
const FLinearColor& Color,
const FName& Name = NAME_None
)
{
UTextBlock* Block = WidgetTree->ConstructWidget<UTextBlock>(
UTextBlock::StaticClass(),
Name);
Block->SetText(FText::FromString(Text));
Block->SetAutoWrapText(true);
Block->SetColorAndOpacity(FSlateColor(Color));
FSlateFontInfo Font = Block->GetFont();
Font.Size = Size;
Block->SetFont(Font);
if (UVerticalBoxSlot* Slot = Parent->AddChildToVerticalBox(Block))
{
Slot->SetPadding(FMargin(0.0f, 3.0f));
}
return Block;
}
UButton* AddButton(
UWidgetTree* WidgetTree,
UVerticalBox* Parent,
const FString& Label,
const FName& Name,
const bool bPrimary = false,
const bool bDanger = false
)
{
UButton* Button = WidgetTree->ConstructWidget<UButton>(
UButton::StaticClass(),
Name);
Button->SetBackgroundColor(
bPrimary
? AccentColor
: bDanger
? FLinearColor(0.30f, 0.065f, 0.070f, 1.0f)
: CardColor);
UTextBlock* Text = WidgetTree->ConstructWidget<UTextBlock>(
UTextBlock::StaticClass());
Text->SetText(FText::FromString(Label));
Text->SetJustification(ETextJustify::Center);
Text->SetColorAndOpacity(FSlateColor(
bPrimary ? FLinearColor(0.01f, 0.04f, 0.05f, 1.0f) : PrimaryText));
FSlateFontInfo Font = Text->GetFont();
Font.Size = 16;
Text->SetFont(Font);
Button->AddChild(Text);
if (UButtonSlot* ButtonSlot = Cast<UButtonSlot>(Text->Slot))
{
ButtonSlot->SetPadding(FMargin(22.0f, 12.0f));
ButtonSlot->SetHorizontalAlignment(HAlign_Center);
}
if (UVerticalBoxSlot* Slot = Parent->AddChildToVerticalBox(Button))
{
Slot->SetPadding(FMargin(0.0f, 5.0f));
Slot->SetHorizontalAlignment(HAlign_Fill);
}
return Button;
}
}
UHyperTwistPauseMenuWidget::UHyperTwistPauseMenuWidget(
const FObjectInitializer& ObjectInitializer
)
: Super(ObjectInitializer)
{
SetIsFocusable(true);
}
TSharedRef<SWidget> UHyperTwistPauseMenuWidget::RebuildWidget()
{
Initialize();
EnsureWidgetTreeBuilt();
return Super::RebuildWidget();
}
FReply UHyperTwistPauseMenuWidget::NativeOnKeyDown(
const FGeometry& InGeometry,
const FKeyEvent& InKeyEvent
)
{
static_cast<void>(InGeometry);
if (InKeyEvent.GetKey() == EKeys::Escape)
{
if (PageSwitcher != nullptr && PageSwitcher->GetActiveWidgetIndex() != 0)
{
PageSwitcher->SetActiveWidgetIndex(0);
}
else
{
OnResumeRequested.Broadcast();
}
return FReply::Handled();
}
return Super::NativeOnKeyDown(InGeometry, InKeyEvent);
}
bool UHyperTwistPauseMenuWidget::PreparePauseSurface()
{
EnsureWidgetTreeBuilt();
SetPuzzleContext(PuzzleTitle, PuzzleSubtitle);
return IsPauseSurfaceReady();
}
bool UHyperTwistPauseMenuWidget::IsPauseSurfaceReady() const
{
return WidgetTree != nullptr
&& WidgetTree->RootWidget != nullptr
&& PageSwitcher != nullptr
&& PageSwitcher->GetNumWidgets() == 2
&& SettingsPanel != nullptr
&& SettingsPanel->IsSettingsSurfaceReady();
}
void UHyperTwistPauseMenuWidget::SetPuzzleContext(
const FString& Title,
const FString& Subtitle
)
{
PuzzleTitle = Title.IsEmpty() ? TEXT("HyperTwist") : Title;
PuzzleSubtitle = Subtitle.IsEmpty() ? TEXT("Puzzle paused") : Subtitle;
if (PuzzleTitleText != nullptr)
{
PuzzleTitleText->SetText(FText::FromString(PuzzleTitle));
}
if (PuzzleSubtitleText != nullptr)
{
PuzzleSubtitleText->SetText(FText::FromString(PuzzleSubtitle));
}
}
void UHyperTwistPauseMenuWidget::EnsureWidgetTreeBuilt()
{
using namespace HyperTwistPauseMenuWidgetInternal;
if (WidgetTree == nullptr || WidgetTree->RootWidget != nullptr)
{
return;
}
UOverlay* Root = WidgetTree->ConstructWidget<UOverlay>(
UOverlay::StaticClass(),
TEXT("HyperTwistPauseRoot"));
WidgetTree->RootWidget = Root;
UBorder* Backdrop = WidgetTree->ConstructWidget<UBorder>(
UBorder::StaticClass(),
TEXT("HyperTwistPauseBackdrop"));
Backdrop->SetBrushColor(BackdropColor);
Root->AddChildToOverlay(Backdrop);
PageSwitcher = WidgetTree->ConstructWidget<UWidgetSwitcher>(
UWidgetSwitcher::StaticClass(),
TEXT("HyperTwistPausePageSwitcher"));
if (UOverlaySlot* SwitcherSlot = Root->AddChildToOverlay(PageSwitcher))
{
SwitcherSlot->SetHorizontalAlignment(HAlign_Fill);
SwitcherSlot->SetVerticalAlignment(VAlign_Fill);
SwitcherSlot->SetPadding(FMargin(34.0f));
}
UHorizontalBox* PausePage = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("HyperTwistPauseMainPage"));
PageSwitcher->AddChild(PausePage);
USpacer* LeftSpacer = WidgetTree->ConstructWidget<USpacer>(
USpacer::StaticClass());
if (UHorizontalBoxSlot* SpacerSlot = PausePage->AddChildToHorizontalBox(LeftSpacer))
{
SpacerSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
}
USizeBox* PanelSize = WidgetTree->ConstructWidget<USizeBox>(
USizeBox::StaticClass(),
TEXT("HyperTwistPausePanelSize"));
PanelSize->SetWidthOverride(560.0f);
if (UHorizontalBoxSlot* PanelSlot = PausePage->AddChildToHorizontalBox(PanelSize))
{
PanelSlot->SetVerticalAlignment(VAlign_Center);
}
UBorder* Panel = WidgetTree->ConstructWidget<UBorder>(
UBorder::StaticClass(),
TEXT("HyperTwistPausePanel"));
Panel->SetPadding(FMargin(34.0f));
Panel->SetBrushColor(PanelColor);
PanelSize->AddChild(Panel);
UVerticalBox* Content = WidgetTree->ConstructWidget<UVerticalBox>(
UVerticalBox::StaticClass(),
TEXT("HyperTwistPauseContent"));
Panel->AddChild(Content);
AddText(WidgetTree, Content, TEXT("PAUSED"), 13, AccentColor);
PuzzleTitleText = AddText(
WidgetTree,
Content,
PuzzleTitle,
32,
PrimaryText,
TEXT("HyperTwistPausePuzzleTitle"));
PuzzleSubtitleText = AddText(
WidgetTree,
Content,
PuzzleSubtitle,
14,
MutedText,
TEXT("HyperTwistPausePuzzleSubtitle"));
AddText(
WidgetTree,
Content,
TEXT("Your state remains exactly where you left it."),
13,
MutedText);
USpacer* MenuSpacer = WidgetTree->ConstructWidget<USpacer>(
USpacer::StaticClass());
MenuSpacer->SetSize(FVector2D(1.0f, 20.0f));
Content->AddChildToVerticalBox(MenuSpacer);
UButton* ResumeButton = AddButton(
WidgetTree,
Content,
TEXT("Resume"),
TEXT("HyperTwistPauseResumeButton"),
true);
UButton* SettingsButton = AddButton(
WidgetTree,
Content,
TEXT("Settings & Controls"),
TEXT("HyperTwistPauseSettingsButton"));
UButton* MainMenuButton = AddButton(
WidgetTree,
Content,
TEXT("Return to Puzzle Library"),
TEXT("HyperTwistPauseMainMenuButton"));
UButton* QuitButton = AddButton(
WidgetTree,
Content,
TEXT("Exit HyperTwist"),
TEXT("HyperTwistPauseQuitButton"),
false,
true);
ResumeButton->OnClicked.AddDynamic(this, &UHyperTwistPauseMenuWidget::HandleResumeClicked);
SettingsButton->OnClicked.AddDynamic(this, &UHyperTwistPauseMenuWidget::HandleSettingsClicked);
MainMenuButton->OnClicked.AddDynamic(this, &UHyperTwistPauseMenuWidget::HandleMainMenuClicked);
QuitButton->OnClicked.AddDynamic(this, &UHyperTwistPauseMenuWidget::HandleQuitClicked);
AddText(
WidgetTree,
Content,
TEXT("Esc resume | F10 puzzle library"),
12,
WarmColor);
USpacer* RightSpacer = WidgetTree->ConstructWidget<USpacer>(
USpacer::StaticClass());
if (UHorizontalBoxSlot* SpacerSlot = PausePage->AddChildToHorizontalBox(RightSpacer))
{
SpacerSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
}
SettingsPanel = WidgetTree->ConstructWidget<UHyperTwistSettingsPanelWidget>(
UHyperTwistSettingsPanelWidget::StaticClass(),
TEXT("HyperTwistPauseSettingsPanel"));
SettingsPanel->bShowCloseButton = true;
SettingsPanel->OnCloseRequested.AddDynamic(
this,
&UHyperTwistPauseMenuWidget::HandleSettingsClosed);
SettingsPanel->PrepareSettingsSurface();
PageSwitcher->AddChild(SettingsPanel);
PageSwitcher->SetActiveWidgetIndex(0);
}
void UHyperTwistPauseMenuWidget::HandleResumeClicked()
{
OnResumeRequested.Broadcast();
}
void UHyperTwistPauseMenuWidget::HandleSettingsClicked()
{
if (SettingsPanel != nullptr)
{
SettingsPanel->ReloadPreferences();
}
PageSwitcher->SetActiveWidgetIndex(1);
}
void UHyperTwistPauseMenuWidget::HandleSettingsClosed()
{
PageSwitcher->SetActiveWidgetIndex(0);
SetKeyboardFocus();
}
void UHyperTwistPauseMenuWidget::HandleMainMenuClicked()
{
OnMainMenuRequested.Broadcast();
}
void UHyperTwistPauseMenuWidget::HandleQuitClicked()
{
OnQuitRequested.Broadcast();
}

View file

@ -0,0 +1,935 @@
#include "HyperTwistUX/HyperTwistPlayerControllerBase.h"
#include "Components/AudioComponent.h"
#include "Blueprint/UserWidget.h"
#include "GameFramework/InputSettings.h"
#include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
#include "HyperTwistRecognition/HyperTwistSpeechLibrary.h"
#include "HyperTwistUX/HyperTwistCoachAssistantWidget.h"
#include "HyperTwistUX/HyperTwistDictationCaptureComponent.h"
#include "HyperTwistUX/HyperTwistPauseMenuWidget.h"
#include "HttpModule.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetSystemLibrary.h"
#include "JsonObjectConverter.h"
#include "Misc/ScopeExit.h"
#include "Sound/SoundWaveProcedural.h"
#include "TimerManager.h"
namespace HyperTwistPlayerControllerBaseInternal
{
uint16 ReadLittleEndianUint16(const TArray<uint8>& Bytes, const int32 Offset)
{
if (Offset < 0 || Offset + 1 >= Bytes.Num())
{
return 0;
}
return static_cast<uint16>(Bytes[Offset])
| (static_cast<uint16>(Bytes[Offset + 1]) << 8);
}
uint32 ReadLittleEndianUint32(const TArray<uint8>& Bytes, const int32 Offset)
{
if (Offset < 0 || Offset + 3 >= Bytes.Num())
{
return 0;
}
return static_cast<uint32>(Bytes[Offset])
| (static_cast<uint32>(Bytes[Offset + 1]) << 8)
| (static_cast<uint32>(Bytes[Offset + 2]) << 16)
| (static_cast<uint32>(Bytes[Offset + 3]) << 24);
}
bool TryReadPcm16WaveFormat(
const TArray<uint8>& Bytes,
int32& OutSampleRateHz,
int32& OutChannelCount
)
{
OutSampleRateHz = 0;
OutChannelCount = 0;
if (Bytes.Num() < 44
|| Bytes[0] != 'R'
|| Bytes[1] != 'I'
|| Bytes[2] != 'F'
|| Bytes[3] != 'F'
|| Bytes[8] != 'W'
|| Bytes[9] != 'A'
|| Bytes[10] != 'V'
|| Bytes[11] != 'E')
{
return false;
}
bool bFoundFormat = false;
bool bFoundAudio = false;
int32 Offset = 12;
while (Offset + 8 <= Bytes.Num())
{
const uint32 ChunkSize = ReadLittleEndianUint32(Bytes, Offset + 4);
const int64 ChunkDataOffset = static_cast<int64>(Offset) + 8;
const int64 ChunkEndOffset = ChunkDataOffset + ChunkSize;
if (ChunkEndOffset > Bytes.Num())
{
return false;
}
if (Bytes[Offset] == 'f'
&& Bytes[Offset + 1] == 'm'
&& Bytes[Offset + 2] == 't'
&& Bytes[Offset + 3] == ' ')
{
if (ChunkSize < 16)
{
return false;
}
const uint16 AudioFormat =
ReadLittleEndianUint16(Bytes, static_cast<int32>(ChunkDataOffset));
const uint16 ChannelCount =
ReadLittleEndianUint16(Bytes, static_cast<int32>(ChunkDataOffset) + 2);
const uint32 SampleRate =
ReadLittleEndianUint32(Bytes, static_cast<int32>(ChunkDataOffset) + 4);
const uint16 BitsPerSample =
ReadLittleEndianUint16(Bytes, static_cast<int32>(ChunkDataOffset) + 14);
if (AudioFormat != 1
|| BitsPerSample != 16
|| ChannelCount == 0
|| ChannelCount > 8
|| SampleRate < 8000
|| SampleRate > 192000)
{
return false;
}
OutChannelCount = ChannelCount;
OutSampleRateHz = SampleRate;
bFoundFormat = true;
}
else if (Bytes[Offset] == 'd'
&& Bytes[Offset + 1] == 'a'
&& Bytes[Offset + 2] == 't'
&& Bytes[Offset + 3] == 'a')
{
bFoundAudio = ChunkSize > 0;
}
Offset = static_cast<int32>(ChunkEndOffset + (ChunkSize & 1u));
}
return bFoundFormat && bFoundAudio;
}
FString BuildVoiceNarrationUrl(const FString& Endpoint)
{
FString Result = Endpoint.TrimStartAndEnd();
if (Result.EndsWith(TEXT("/voice/narrate"), ESearchCase::IgnoreCase))
{
return Result;
}
while (Result.EndsWith(TEXT("/")))
{
Result.LeftChopInline(1, EAllowShrinking::No);
}
return Result.IsEmpty()
? FString()
: Result + TEXT("/voice/narrate");
}
}
AHyperTwistPlayerControllerBase::AHyperTwistPlayerControllerBase()
{
bShowMouseCursor = true;
bEnableClickEvents = true;
bEnableMouseOverEvents = true;
GlobalDictationCapture = CreateDefaultSubobject<UHyperTwistDictationCaptureComponent>(
TEXT("GlobalDictationCapture"));
}
void AHyperTwistPlayerControllerBase::BeginPlay()
{
Super::BeginPlay();
if (GlobalDictationCapture != nullptr)
{
GlobalDictationCapture->OnStateChanged.AddDynamic(
this,
&AHyperTwistPlayerControllerBase::HandleGlobalDictationStateChanged);
GlobalDictationCapture->OnTranscriptReady.AddDynamic(
this,
&AHyperTwistPlayerControllerBase::HandleGlobalDictationTranscriptReady);
}
ReloadAndApplyPlayerPreferences(false);
ApplyGameAndUiInputMode();
if (bEnableGlobalAssistantPanel
&& PlayerPreferences.bAssistantPanelEnabled
&& PlayerPreferences.bAssistantPanelOpenByDefault
&& GetWorld() != nullptr)
{
TWeakObjectPtr<AHyperTwistPlayerControllerBase> WeakThis(this);
GetWorld()->GetTimerManager().SetTimerForNextTick(
[WeakThis]()
{
if (WeakThis.IsValid())
{
WeakThis->ShowAssistantPanel();
}
});
}
}
void AHyperTwistPlayerControllerBase::EndPlay(
const EEndPlayReason::Type EndPlayReason
)
{
StopCoachNarration();
CancelGlobalDictation();
Super::EndPlay(EndPlayReason);
}
void AHyperTwistPlayerControllerBase::SetupInputComponent()
{
Super::SetupInputComponent();
if (InputComponent == nullptr)
{
return;
}
PlayerPreferences = UHyperTwistPlayerSettingsLibrary::LoadPreferences();
const FKey ConfiguredPauseKey =
UHyperTwistPlayerSettingsLibrary::ResolveKeyBinding(
PlayerPreferences,
TEXT("navigation.pause"));
FInputKeyBinding& EscapeBinding = InputComponent->BindKey(
EKeys::Escape,
IE_Pressed,
this,
&AHyperTwistPlayerControllerBase::HandlePauseShortcut);
EscapeBinding.bExecuteWhenPaused = true;
if (ConfiguredPauseKey.IsValid() && ConfiguredPauseKey != EKeys::Escape)
{
FInputKeyBinding& PauseBinding = InputComponent->BindKey(
ConfiguredPauseKey,
IE_Pressed,
this,
&AHyperTwistPlayerControllerBase::HandlePauseShortcut);
PauseBinding.bExecuteWhenPaused = true;
}
const FKey MainMenuKey =
UHyperTwistPlayerSettingsLibrary::ResolveKeyBinding(
PlayerPreferences,
TEXT("navigation.main-menu"));
if (MainMenuKey.IsValid())
{
FInputKeyBinding& MainMenuBinding = InputComponent->BindKey(
MainMenuKey,
IE_Pressed,
this,
&AHyperTwistPlayerControllerBase::HandleMainMenuShortcut);
MainMenuBinding.bExecuteWhenPaused = true;
}
const FKey AssistantKey =
UHyperTwistPlayerSettingsLibrary::ResolveKeyBinding(
PlayerPreferences,
TEXT("assistant.toggle"));
if (AssistantKey.IsValid())
{
InputComponent->BindKey(
AssistantKey,
IE_Pressed,
this,
&AHyperTwistPlayerControllerBase::HandleAssistantShortcut);
}
if (PlayerPreferences.bSpeechInputEnabled)
{
const FKey DictationKey =
UHyperTwistPlayerSettingsLibrary::ResolveKeyBinding(
PlayerPreferences,
TEXT("speech.push-to-talk"));
if (DictationKey.IsValid())
{
InputComponent->BindKey(
DictationKey,
IE_Pressed,
this,
&AHyperTwistPlayerControllerBase::HandleGlobalDictationPressed);
InputComponent->BindKey(
DictationKey,
IE_Released,
this,
&AHyperTwistPlayerControllerBase::HandleGlobalDictationReleased);
}
}
}
bool AHyperTwistPlayerControllerBase::ShowPauseMenu()
{
if (!bEnableGlobalPauseMenu)
{
return false;
}
PrepareForModalUi();
HideAssistantPanel();
if (ActivePauseMenuWidget != nullptr)
{
if (!ActivePauseMenuWidget->IsInViewport())
{
ActivePauseMenuWidget->AddToViewport(PauseMenuZOrder);
}
return true;
}
TSubclassOf<UHyperTwistPauseMenuWidget> ResolvedClass = PauseMenuWidgetClass;
if (*ResolvedClass == nullptr)
{
ResolvedClass = UHyperTwistPauseMenuWidget::StaticClass();
}
ActivePauseMenuWidget = CreateWidget<UHyperTwistPauseMenuWidget>(
this,
ResolvedClass);
if (ActivePauseMenuWidget == nullptr
|| !ActivePauseMenuWidget->PreparePauseSurface())
{
ActivePauseMenuWidget = nullptr;
return false;
}
ActivePauseMenuWidget->SetPuzzleContext(
GetPauseMenuTitle(),
GetPauseMenuSubtitle());
ActivePauseMenuWidget->OnResumeRequested.AddDynamic(
this,
&AHyperTwistPlayerControllerBase::HandlePauseResumeRequested);
ActivePauseMenuWidget->OnMainMenuRequested.AddDynamic(
this,
&AHyperTwistPlayerControllerBase::HandlePauseMainMenuRequested);
ActivePauseMenuWidget->OnQuitRequested.AddDynamic(
this,
&AHyperTwistPlayerControllerBase::HandlePauseQuitRequested);
ActivePauseMenuWidget->AddToViewport(PauseMenuZOrder);
SetPause(true);
bShowMouseCursor = true;
FInputModeUIOnly InputMode;
InputMode.SetWidgetToFocus(ActivePauseMenuWidget->TakeWidget());
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
SetInputMode(InputMode);
ActivePauseMenuWidget->SetKeyboardFocus();
return true;
}
void AHyperTwistPlayerControllerBase::HidePauseMenu()
{
if (ActivePauseMenuWidget != nullptr)
{
ActivePauseMenuWidget->RemoveFromParent();
ActivePauseMenuWidget = nullptr;
}
SetPause(false);
ReloadAndApplyPlayerPreferences(false);
ApplyGameAndUiInputMode();
}
void AHyperTwistPlayerControllerBase::ReturnToMainMenu()
{
PrepareForModalUi();
SetPause(false);
HideAssistantPanel();
if (ActivePauseMenuWidget != nullptr)
{
ActivePauseMenuWidget->RemoveFromParent();
ActivePauseMenuWidget = nullptr;
}
const FString Options = FString::Printf(
TEXT("game=%s"),
UHyperTwistPlayerSettingsLibrary::GetMainMenuGameModeClassPath());
UGameplayStatics::OpenLevel(
this,
FName(UHyperTwistPlayerSettingsLibrary::GetMainMenuMapAssetPath()),
true,
Options);
}
void AHyperTwistPlayerControllerBase::QuitToDesktop()
{
PrepareForModalUi();
HideAssistantPanel();
UKismetSystemLibrary::QuitGame(
this,
this,
EQuitPreference::Quit,
false);
}
bool AHyperTwistPlayerControllerBase::ShowAssistantPanel()
{
ReloadAndApplyPlayerPreferences(false);
if (!bEnableGlobalAssistantPanel
|| !PlayerPreferences.bAssistantPanelEnabled
|| ActivePauseMenuWidget != nullptr
|| (GetWorld() != nullptr && GetWorld()->IsPaused()))
{
return false;
}
PrepareForModalUi();
if (ActiveAssistantPanelWidget != nullptr)
{
if (!ActiveAssistantPanelWidget->IsInViewport())
{
ActiveAssistantPanelWidget->AddToViewport(AssistantPanelZOrder);
}
return true;
}
TSubclassOf<UHyperTwistCoachAssistantWidget> ResolvedClass =
AssistantPanelWidgetClass;
if (*ResolvedClass == nullptr)
{
ResolvedClass = UHyperTwistCoachAssistantWidget::StaticClass();
}
ActiveAssistantPanelWidget = CreateWidget<UHyperTwistCoachAssistantWidget>(
this,
ResolvedClass);
if (ActiveAssistantPanelWidget == nullptr
|| !ActiveAssistantPanelWidget->PrepareAssistantSurface())
{
ActiveAssistantPanelWidget = nullptr;
return false;
}
ActiveAssistantPanelWidget->SetPuzzleContext(
GetPauseMenuTitle(),
GetPauseMenuSubtitle());
ActiveAssistantPanelWidget->OnCloseRequested.AddDynamic(
this,
&AHyperTwistPlayerControllerBase::HandleAssistantCloseRequested);
ActiveAssistantPanelWidget->AddToViewport(AssistantPanelZOrder);
bShowMouseCursor = true;
FInputModeGameAndUI InputMode;
InputMode.SetWidgetToFocus(ActiveAssistantPanelWidget->TakeWidget());
InputMode.SetHideCursorDuringCapture(false);
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
SetInputMode(InputMode);
ActiveAssistantPanelWidget->SetKeyboardFocus();
return true;
}
void AHyperTwistPlayerControllerBase::HideAssistantPanel()
{
CancelGlobalDictation();
StopCoachNarration();
if (ActiveAssistantPanelWidget != nullptr)
{
ActiveAssistantPanelWidget->RemoveFromParent();
ActiveAssistantPanelWidget = nullptr;
}
if (ActivePauseMenuWidget == nullptr
&& (GetWorld() == nullptr || !GetWorld()->IsPaused()))
{
ApplyGameAndUiInputMode();
}
}
void AHyperTwistPlayerControllerBase::ToggleAssistantPanel()
{
if (ActiveAssistantPanelWidget != nullptr)
{
HideAssistantPanel();
}
else
{
ShowAssistantPanel();
}
}
bool AHyperTwistPlayerControllerBase::IsAssistantPanelOpen() const
{
return ActiveAssistantPanelWidget != nullptr
&& ActiveAssistantPanelWidget->IsInViewport();
}
bool AHyperTwistPlayerControllerBase::BeginGlobalDictation()
{
ReloadAndApplyPlayerPreferences(false);
if (GlobalDictationCapture == nullptr
|| !PlayerPreferences.bSpeechInputEnabled
|| !PlayerPreferences.bAssistantPanelEnabled
|| !ShowAssistantPanel())
{
return false;
}
StopCoachNarration();
FString FailureReason;
const bool bStarted = GlobalDictationCapture->BeginCapture(FailureReason);
bToggleDictationCaptureActive = bStarted
&& PlayerPreferences.SpeechActivationMode == TEXT("toggle");
return bStarted;
}
bool AHyperTwistPlayerControllerBase::EndGlobalDictation()
{
if (GlobalDictationCapture == nullptr
|| !GlobalDictationCapture->IsCapturing())
{
bToggleDictationCaptureActive = false;
return false;
}
FString Transcript;
FString FailureReason;
const bool bCompleted =
GlobalDictationCapture->EndCapture(Transcript, FailureReason);
bToggleDictationCaptureActive = false;
return bCompleted;
}
void AHyperTwistPlayerControllerBase::CancelGlobalDictation()
{
if (GlobalDictationCapture != nullptr
&& GlobalDictationCapture->IsCapturing())
{
GlobalDictationCapture->CancelCapture();
}
bToggleDictationCaptureActive = false;
}
void AHyperTwistPlayerControllerBase::ToggleGlobalDictation()
{
if (IsGlobalDictationActive())
{
EndGlobalDictation();
}
else
{
BeginGlobalDictation();
}
}
bool AHyperTwistPlayerControllerBase::IsGlobalDictationActive() const
{
return GlobalDictationCapture != nullptr
&& GlobalDictationCapture->IsCapturing();
}
bool AHyperTwistPlayerControllerBase::NarrateCoachResponse(
const FString& ResponseText
)
{
ReloadAndApplyPlayerPreferences(false);
const FString SafeResponse = ResponseText.TrimStartAndEnd();
if (!PlayerPreferences.bCoachNarrationEnabled || SafeResponse.IsEmpty())
{
return false;
}
FString FailureReason;
if (!UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
PlayerPreferences.VoiceEndpoint,
PlayerPreferences.bAllowCloudProviders,
FailureReason))
{
ReportCoachNarrationStatus(
TEXT("Text response ready; narration is blocked by the provider privacy policy."),
true);
return false;
}
if (PlayerPreferences.VoiceModel.IsEmpty()
|| PlayerPreferences.VoiceId.IsEmpty())
{
ReportCoachNarrationStatus(
TEXT("Text response ready; choose a voice model and voice in Settings."),
true);
return false;
}
FString Credential;
const bool bHasCredential =
UHyperTwistPlayerSettingsLibrary::LoadProviderCredential(
TEXT("voice-api-key"),
Credential);
const bool bCredentialRequired =
PlayerPreferences.VoiceProviderId == TEXT("openai")
|| PlayerPreferences.VoiceProviderId == TEXT("elevenlabs");
ON_SCOPE_EXIT
{
if (!Credential.IsEmpty())
{
Credential.Reset(Credential.Len());
}
};
if (bCredentialRequired && (!bHasCredential || Credential.IsEmpty()))
{
ReportCoachNarrationStatus(
TEXT("Text response ready; save a protected voice API key to enable narration."),
true);
return false;
}
FHyperTwistNarrationSynthesisRequest Request =
UHyperTwistContractLibrary::MakeSampleNarrationSynthesisRequest();
Request.RequestId =
FGuid::NewGuid().ToString(EGuidFormats::DigitsWithHyphensLower);
const FString NarrationText = SafeResponse.Left(6000);
Request.ScriptText = NarrationText;
Request.SubtitleSeedText = NarrationText;
Request.VoiceProfileId = PlayerPreferences.VoiceId;
Request.LengthScale = 1.0f / FMath::Max(PlayerPreferences.VoiceSpeed, 0.1f);
Request.OrchestrationProfile.ModelBindingId = PlayerPreferences.VoiceModel;
FString RequestBody;
if (!FJsonObjectConverter::UStructToJsonObjectString(
FHyperTwistNarrationSynthesisRequest::StaticStruct(),
&Request,
RequestBody,
0,
0))
{
ReportCoachNarrationStatus(
TEXT("Text response ready; narration request preparation failed."),
true);
return false;
}
const FString RequestUrl =
HyperTwistPlayerControllerBaseInternal::BuildVoiceNarrationUrl(
PlayerPreferences.VoiceEndpoint);
if (RequestUrl.IsEmpty())
{
ReportCoachNarrationStatus(
TEXT("Text response ready; configure a narration endpoint."),
true);
return false;
}
StopCoachNarration();
ActiveCoachNarrationRequest = FHttpModule::Get().CreateRequest();
ActiveCoachNarrationRequest->SetURL(RequestUrl);
ActiveCoachNarrationRequest->SetVerb(TEXT("POST"));
ActiveCoachNarrationRequest->SetHeader(TEXT("Accept"), TEXT("audio/wav"));
ActiveCoachNarrationRequest->SetHeader(
TEXT("Content-Type"),
TEXT("application/json"));
if (!Credential.IsEmpty())
{
ActiveCoachNarrationRequest->SetHeader(
TEXT("Authorization"),
FString::Printf(TEXT("Bearer %s"), *Credential));
}
ActiveCoachNarrationRequest->SetContentAsString(RequestBody);
ActiveCoachNarrationRequest->SetTimeout(
PlayerPreferences.CoachRequestTimeoutSeconds);
TWeakObjectPtr<AHyperTwistPlayerControllerBase> WeakThis(this);
ActiveCoachNarrationRequest->OnProcessRequestComplete().BindLambda(
[WeakThis](
FHttpRequestPtr CompletedRequest,
FHttpResponsePtr Response,
const bool bSucceeded)
{
AHyperTwistPlayerControllerBase* Controller = WeakThis.Get();
if (Controller == nullptr
|| Controller->ActiveCoachNarrationRequest.Get()
!= CompletedRequest.Get())
{
return;
}
Controller->HandleCoachNarrationResponse(
bSucceeded && Response.IsValid(),
Response.IsValid() ? Response->GetResponseCode() : 0,
Response.IsValid() ? Response->GetContent() : TArray<uint8>());
});
ReportCoachNarrationStatus(TEXT("Preparing spoken response..."), false);
const bool bStarted = ActiveCoachNarrationRequest->ProcessRequest();
if (!bStarted)
{
ActiveCoachNarrationRequest->OnProcessRequestComplete().Unbind();
ActiveCoachNarrationRequest.Reset();
ReportCoachNarrationStatus(
TEXT("Text response ready; narration service could not start."),
true);
}
return bStarted;
}
void AHyperTwistPlayerControllerBase::StopCoachNarration()
{
if (ActiveCoachNarrationRequest.IsValid())
{
ActiveCoachNarrationRequest->OnProcessRequestComplete().Unbind();
ActiveCoachNarrationRequest->CancelRequest();
ActiveCoachNarrationRequest.Reset();
}
ReleaseCoachNarrationAudio(true);
}
void AHyperTwistPlayerControllerBase::HandleCoachNarrationResponse(
const bool bTransportSucceeded,
const int32 StatusCode,
const TArray<uint8>& AudioBytes
)
{
if (ActiveCoachNarrationRequest.IsValid())
{
ActiveCoachNarrationRequest->OnProcessRequestComplete().Unbind();
ActiveCoachNarrationRequest.Reset();
}
if (!bTransportSucceeded || StatusCode < 200 || StatusCode >= 300)
{
ReportCoachNarrationStatus(
StatusCode > 0
? FString::Printf(
TEXT("Text response ready; narration service returned HTTP %d."),
StatusCode)
: TEXT("Text response ready; narration service is unavailable."),
true);
return;
}
if (!TryPlayCoachNarration(AudioBytes))
{
ReportCoachNarrationStatus(
TEXT("Text response ready; narration returned unsupported audio."),
true);
return;
}
ReportCoachNarrationStatus(TEXT("Playing spoken response..."), false);
}
bool AHyperTwistPlayerControllerBase::TryPlayCoachNarration(
const TArray<uint8>& AudioBytes
)
{
int32 SampleRateHz = 0;
int32 ChannelCount = 0;
if (GetWorld() == nullptr
|| !HyperTwistPlayerControllerBaseInternal::TryReadPcm16WaveFormat(
AudioBytes,
SampleRateHz,
ChannelCount))
{
return false;
}
const TArray<uint8> PcmBytes =
UHyperTwistSpeechLibrary::ExtractPcm16PayloadFromAudioBytes(
AudioBytes,
TEXT("wav-pcm16-mono"));
if (PcmBytes.IsEmpty())
{
return false;
}
ReleaseCoachNarrationAudio(true);
ActiveCoachNarrationSoundWave =
NewObject<USoundWaveProcedural>(this);
if (ActiveCoachNarrationSoundWave == nullptr)
{
return false;
}
ActiveCoachNarrationSoundWave->SetSampleRate(SampleRateHz);
ActiveCoachNarrationSoundWave->NumChannels = ChannelCount;
ActiveCoachNarrationSoundWave->SampleByteSize = 2;
ActiveCoachNarrationSoundWave->Duration =
static_cast<float>(PcmBytes.Num())
/ static_cast<float>(SampleRateHz * ChannelCount * 2);
ActiveCoachNarrationSoundWave->QueueAudio(
PcmBytes.GetData(),
PcmBytes.Num());
ActiveCoachNarrationAudioComponent = UGameplayStatics::SpawnSound2D(
GetWorld(),
ActiveCoachNarrationSoundWave);
if (ActiveCoachNarrationAudioComponent == nullptr)
{
ActiveCoachNarrationSoundWave = nullptr;
return false;
}
UHyperTwistSpeechLibrary::RegisterCategorizedAudioComponent(
ActiveCoachNarrationAudioComponent,
EHyperTwistManagedAudioCategory::Voice);
ActiveCoachNarrationAudioComponent->OnAudioFinished.AddDynamic(
this,
&AHyperTwistPlayerControllerBase::HandleCoachNarrationFinished);
return true;
}
void AHyperTwistPlayerControllerBase::ReleaseCoachNarrationAudio(
const bool bStopPlayback
)
{
if (ActiveCoachNarrationAudioComponent != nullptr)
{
ActiveCoachNarrationAudioComponent->OnAudioFinished.RemoveDynamic(
this,
&AHyperTwistPlayerControllerBase::HandleCoachNarrationFinished);
UHyperTwistSpeechLibrary::UnregisterManagedAudioComponent(
ActiveCoachNarrationAudioComponent);
if (bStopPlayback)
{
ActiveCoachNarrationAudioComponent->Stop();
}
ActiveCoachNarrationAudioComponent = nullptr;
}
ActiveCoachNarrationSoundWave = nullptr;
}
void AHyperTwistPlayerControllerBase::ReportCoachNarrationStatus(
const FString& Status,
const bool bError
)
{
if (ActiveAssistantPanelWidget != nullptr)
{
ActiveAssistantPanelWidget->SetNarrationStatus(Status, bError);
}
}
void AHyperTwistPlayerControllerBase::HandleCoachNarrationFinished()
{
ReleaseCoachNarrationAudio(false);
ReportCoachNarrationStatus(TEXT("Spoken response complete."), false);
}
const FHyperTwistPlayerPreferences&
AHyperTwistPlayerControllerBase::GetPlayerPreferences() const
{
return PlayerPreferences;
}
void AHyperTwistPlayerControllerBase::ReloadAndApplyPlayerPreferences(
const bool bApplyGraphicsSettings
)
{
PlayerPreferences = UHyperTwistPlayerSettingsLibrary::LoadPreferences();
UHyperTwistPlayerSettingsLibrary::ApplyRuntimePreferences(
PlayerPreferences,
bApplyGraphicsSettings);
}
FString AHyperTwistPlayerControllerBase::GetPauseMenuTitle() const
{
return TEXT("HyperTwist");
}
FString AHyperTwistPlayerControllerBase::GetPauseMenuSubtitle() const
{
const UWorld* World = GetWorld();
return World != nullptr
? FString::Printf(TEXT("%s is paused"), *World->GetMapName())
: TEXT("Puzzle paused");
}
void AHyperTwistPlayerControllerBase::PrepareForModalUi()
{
CancelGlobalDictation();
}
void AHyperTwistPlayerControllerBase::ApplyGameAndUiInputMode()
{
bShowMouseCursor = true;
bEnableClickEvents = true;
bEnableMouseOverEvents = true;
FInputModeGameAndUI InputMode;
InputMode.SetHideCursorDuringCapture(false);
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
SetInputMode(InputMode);
}
void AHyperTwistPlayerControllerBase::HandlePauseShortcut()
{
if (!bEnableGlobalPauseMenu)
{
return;
}
if (ActivePauseMenuWidget != nullptr)
{
HidePauseMenu();
}
else
{
ShowPauseMenu();
}
}
void AHyperTwistPlayerControllerBase::HandleMainMenuShortcut()
{
ReturnToMainMenu();
}
void AHyperTwistPlayerControllerBase::HandleAssistantShortcut()
{
ToggleAssistantPanel();
}
void AHyperTwistPlayerControllerBase::HandleAssistantCloseRequested()
{
HideAssistantPanel();
}
void AHyperTwistPlayerControllerBase::HandleGlobalDictationPressed()
{
if (PlayerPreferences.SpeechActivationMode == TEXT("toggle"))
{
ToggleGlobalDictation();
bToggleDictationCaptureActive = IsGlobalDictationActive();
return;
}
BeginGlobalDictation();
}
void AHyperTwistPlayerControllerBase::HandleGlobalDictationReleased()
{
if (PlayerPreferences.SpeechActivationMode != TEXT("toggle"))
{
EndGlobalDictation();
}
}
void AHyperTwistPlayerControllerBase::HandleGlobalDictationStateChanged(
const FString& Status,
const bool bError
)
{
if (ActiveAssistantPanelWidget != nullptr)
{
ActiveAssistantPanelWidget->SetDictationStatus(
Status,
IsGlobalDictationActive(),
bError);
}
}
void AHyperTwistPlayerControllerBase::HandleGlobalDictationTranscriptReady(
const FString& Transcript
)
{
if (Transcript.IsEmpty())
{
return;
}
if (ActiveAssistantPanelWidget == nullptr && !ShowAssistantPanel())
{
return;
}
ReloadAndApplyPlayerPreferences(false);
ActiveAssistantPanelWidget->SubmitDictatedPrompt(
Transcript,
PlayerPreferences.SpeechDictationDestination == TEXT("coach-send"));
}
void AHyperTwistPlayerControllerBase::HandlePauseResumeRequested()
{
HidePauseMenu();
}
void AHyperTwistPlayerControllerBase::HandlePauseMainMenuRequested()
{
ReturnToMainMenu();
}
void AHyperTwistPlayerControllerBase::HandlePauseQuitRequested()
{
QuitToDesktop();
}

View file

@ -3,6 +3,7 @@
#include "EngineUtils.h" #include "EngineUtils.h"
#include "GameFramework/PlayerController.h" #include "GameFramework/PlayerController.h"
#include "GenericPlatform/GenericPlatformMisc.h" #include "GenericPlatform/GenericPlatformMisc.h"
#include "HyperTwistUX/HyperTwistPlayerControllerBase.h"
#include "HyperTwistXR/HyperTwistXrPackageValidationLibrary.h" #include "HyperTwistXR/HyperTwistXrPackageValidationLibrary.h"
#include "HyperTwistXR/HyperTwistXrTrainingPawn.h" #include "HyperTwistXR/HyperTwistXrTrainingPawn.h"
#include "Misc/CommandLine.h" #include "Misc/CommandLine.h"
@ -11,7 +12,7 @@
AHyperTwistXrTrainingGameMode::AHyperTwistXrTrainingGameMode() AHyperTwistXrTrainingGameMode::AHyperTwistXrTrainingGameMode()
{ {
PrimaryActorTick.bCanEverTick = true; PrimaryActorTick.bCanEverTick = true;
PlayerControllerClass = APlayerController::StaticClass(); PlayerControllerClass = AHyperTwistPlayerControllerBase::StaticClass();
DefaultPawnClass = AHyperTwistXrTrainingPawn::StaticClass(); DefaultPawnClass = AHyperTwistXrTrainingPawn::StaticClass();
ConfiguredValidationObservationSeconds = DefaultValidationObservationSeconds; ConfiguredValidationObservationSeconds = DefaultValidationObservationSeconds;
} }

View file

@ -8,6 +8,14 @@
class UAudioComponent; class UAudioComponent;
UENUM(BlueprintType)
enum class EHyperTwistManagedAudioCategory : uint8
{
Music,
Effects,
Voice
};
UCLASS() UCLASS()
class UNREALHYPERTWIST_API UHyperTwistSpeechLibrary : public UBlueprintFunctionLibrary class UNREALHYPERTWIST_API UHyperTwistSpeechLibrary : public UBlueprintFunctionLibrary
{ {
@ -46,9 +54,22 @@ public:
bool bTreatAsSpeechAudio = false bool bTreatAsSpeechAudio = false
); );
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech|Audio")
static void RegisterCategorizedAudioComponent(
UAudioComponent* AudioComponent,
EHyperTwistManagedAudioCategory Category
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech|Audio") UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech|Audio")
static void UnregisterManagedAudioComponent(UAudioComponent* AudioComponent); static void UnregisterManagedAudioComponent(UAudioComponent* AudioComponent);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech|Audio")
static int32 ApplyManagedAudioCategoryVolumes(
float MusicVolume,
float EffectsVolume,
float VoiceVolume
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech|Audio") UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech|Audio")
static int32 DuckManagedAudioComponents(float DuckVolumeMultiplier = 0.15f); static int32 DuckManagedAudioComponents(float DuckVolumeMultiplier = 0.15f);

View file

@ -37,7 +37,7 @@ public:
const FString& InReplayLine, const FString& InReplayLine,
const FString& InLeaderboardLine, const FString& InLeaderboardLine,
const FString& InModeLine, const FString& InModeLine,
const FString& InVoiceLine const FString& InTrainingLine
); );
UFUNCTION(BlueprintPure, Category = "HyperTwist|ClassicCube|HUD") UFUNCTION(BlueprintPure, Category = "HyperTwist|ClassicCube|HUD")
@ -49,9 +49,6 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|ClassicCube|HUD") UFUNCTION(BlueprintCallable, Category = "HyperTwist|ClassicCube|HUD")
void SetModeToggleButtonLabel(const FString& InLabel); void SetModeToggleButtonLabel(const FString& InLabel);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|ClassicCube|HUD")
void SetVoiceCycleButtonLabel(const FString& InLabel);
protected: protected:
virtual TSharedRef<SWidget> RebuildWidget() override; virtual TSharedRef<SWidget> RebuildWidget() override;
@ -85,15 +82,6 @@ private:
UFUNCTION() UFUNCTION()
void HandleModeToggleClicked(); void HandleModeToggleClicked();
UFUNCTION()
void HandleVoicePressed();
UFUNCTION()
void HandleVoiceReleased();
UFUNCTION()
void HandleVoiceCycleClicked();
UPROPERTY(Transient) UPROPERTY(Transient)
TObjectPtr<UTextBlock> StatusTextBlock = nullptr; TObjectPtr<UTextBlock> StatusTextBlock = nullptr;
@ -128,7 +116,7 @@ private:
TObjectPtr<UTextBlock> ModeTextBlock = nullptr; TObjectPtr<UTextBlock> ModeTextBlock = nullptr;
UPROPERTY(Transient) UPROPERTY(Transient)
TObjectPtr<UTextBlock> VoiceTextBlock = nullptr; TObjectPtr<UTextBlock> TrainingTextBlock = nullptr;
UPROPERTY(Transient) UPROPERTY(Transient)
TObjectPtr<UButton> NewScrambleButton = nullptr; TObjectPtr<UButton> NewScrambleButton = nullptr;
@ -154,17 +142,5 @@ private:
UPROPERTY(Transient) UPROPERTY(Transient)
TObjectPtr<UTextBlock> ModeToggleButtonLabel = nullptr; TObjectPtr<UTextBlock> ModeToggleButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> VoiceHoldButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> VoiceHoldButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> VoiceCycleButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> VoiceCycleButtonLabel = nullptr;
bool bNewScrambleButtonHovered = false; bool bNewScrambleButtonHovered = false;
}; };

View file

@ -1,13 +1,14 @@
#pragma once #pragma once
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "GameFramework/PlayerController.h" #include "HyperTwistUX/HyperTwistPlayerControllerBase.h"
#include "HyperTwistClassicCubePlayerController.generated.h" #include "HyperTwistClassicCubePlayerController.generated.h"
class AHyperTwistClassicCubeActor; class AHyperTwistClassicCubeActor;
UCLASS(BlueprintType, Blueprintable) UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API AHyperTwistClassicCubePlayerController : public APlayerController class UNREALHYPERTWIST_API AHyperTwistClassicCubePlayerController
: public AHyperTwistPlayerControllerBase
{ {
GENERATED_BODY() GENERATED_BODY()
@ -16,6 +17,7 @@ public:
virtual void BeginPlay() override; virtual void BeginPlay() override;
virtual void SetupInputComponent() override; virtual void SetupInputComponent() override;
virtual bool InputKey(const FInputKeyEventArgs& Params) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Input") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Input")
bool bUseGameAndUiInputMode = true; bool bUseGameAndUiInputMode = true;
@ -39,10 +41,10 @@ public:
bool bBindModeToggleShortcut = true; bool bBindModeToggleShortcut = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Input") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Input")
bool bBindVoiceHoldShortcut = true; bool bBindVoiceHoldShortcut = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Input") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|ClassicCube|Input")
bool bBindVoiceCycleShortcut = true; bool bBindVoiceCycleShortcut = false;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|ClassicCube|Input") UFUNCTION(BlueprintCallable, Category = "HyperTwist|ClassicCube|Input")
bool TryProcessCubeClickFromCursor(bool bCounterClockwise = false); bool TryProcessCubeClickFromCursor(bool bCounterClockwise = false);
@ -54,6 +56,9 @@ public:
void RequestFreshAttempt(); void RequestFreshAttempt();
protected: protected:
virtual FString GetPauseMenuTitle() const override;
virtual FString GetPauseMenuSubtitle() const override;
virtual void PrepareForModalUi() override;
void ApplyClassicCubeInputMode(); void ApplyClassicCubeInputMode();
AHyperTwistClassicCubeActor* ResolveClassicCubeActor() const; AHyperTwistClassicCubeActor* ResolveClassicCubeActor() const;
void HandlePrimaryClick(); void HandlePrimaryClick();
@ -66,4 +71,7 @@ protected:
void HandleVoiceReleased(); void HandleVoiceReleased();
void HandleVoiceCycleShortcut(); void HandleVoiceCycleShortcut();
void HandleTouchPressed(ETouchIndex::Type FingerIndex, FVector Location); void HandleTouchPressed(ETouchIndex::Type FingerIndex, FVector Location);
bool TryApplyKeyboardPuzzleMove(const FInputKeyEventArgs& Params);
bool bToggleVoiceCaptureActive = false;
}; };

View file

@ -0,0 +1,48 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/SaveGame.h"
#include "HyperTwistCore/HyperTwistCoreLibrary.h"
#include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionLibrary.h"
#include "HyperTwistFourDimensionalSaveGame.generated.h"
UCLASS()
class UNREALHYPERTWIST_API UHyperTwistFourDimensionalSaveGame : public USaveGame
{
GENERATED_BODY()
public:
UPROPERTY()
int32 SchemaVersion = 1;
UPROPERTY()
int32 PuzzleOrder = 0;
UPROPERTY()
bool bUsesCellFirstState = false;
UPROPERTY()
FHyperTwistPuzzleState CellFirstState;
UPROPERTY()
FHyperTwistVirtual3333RuntimeState VisibleSliceState;
UPROPERTY()
EHyperTwistVirtual3333Axis SliceAxis = EHyperTwistVirtual3333Axis::W;
UPROPERTY()
int32 SliceCoordinate = 1;
UPROPERTY()
EHyperTwistVirtual3333Axis RotationAxis = EHyperTwistVirtual3333Axis::X;
UPROPERTY()
bool bRenderCellShells = false;
UPROPERTY()
FString LastAppliedNotation;
UPROPERTY()
FString SavedAtUtc;
};

View file

@ -0,0 +1,208 @@
#pragma once
#include "CoreMinimal.h"
#include "HyperTwistCore/HyperTwistCoreLibrary.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "HyperTwistMagic120CellRuntimeLibrary.generated.h"
USTRUCT(BlueprintType)
struct FHyperTwistMagic120CellRuntimeState
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
FString StateProfile = TEXT("magic120cell-7560-facelet-permutation-v1");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
FHyperTwistPuzzleDefinitionRef Definition;
/** Destination slot to solved-cell color index for all 120 x 63 stickers. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
TArray<int32> StickerColorIndices;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
int32 AppliedMoveCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
bool bIsSolved = true;
bool IsStructurallyValid() const;
};
USTRUCT(BlueprintType)
struct FHyperTwistMagic120CellTurnRequest
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
int32 CellIndex = 0;
/** 1-12 face axes, 13-42 edge axes, and 43-62 vertex axes. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
int32 StickerIndex = 1;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
bool bInverse = false;
bool IsStructurallyValid() const;
};
USTRUCT(BlueprintType)
struct FHyperTwistMagic120CellTurnResult
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
FHyperTwistMagic120CellRuntimeState State;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
FString AppliedNotation;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
TArray<FString> Warnings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
int32 MovedStickerCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
bool bApplied = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
bool bExactStateUpdate = false;
};
USTRUCT(BlueprintType)
struct FHyperTwistMagic120CellScrambleResult
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
FHyperTwistMagic120CellRuntimeState State;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
TArray<FHyperTwistMagic120CellTurnRequest> Moves;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
TArray<FString> AppliedNotation;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
TArray<FString> Warnings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
bool bGenerated = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
bool bExactStateUpdate = false;
};
USTRUCT(BlueprintType)
struct FHyperTwistMagic120CellProjectedCell
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
int32 CellIndex = INDEX_NONE;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
TArray<int32> StickerColorIndices;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
int32 RepresentativeColorIndex = INDEX_NONE;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
int32 DisplacedStickerCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
bool bCellSolved = true;
bool IsStructurallyValid() const;
};
USTRUCT(BlueprintType)
struct FHyperTwistMagic120CellProjection
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
FString ProjectionProfile = TEXT("magic120cell-120-cell-7560-facelet-projection-v1");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
TArray<FHyperTwistMagic120CellProjectedCell> Cells;
bool IsStructurallyValid() const;
};
USTRUCT(BlueprintType)
struct FHyperTwistMagic120CellProjectionBuildResult
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
FHyperTwistMagic120CellProjection Projection;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
TArray<FString> Warnings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
bool bProjected = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Magic120Cell")
bool bExactProjection = false;
};
/**
* Exact runtime behavior is derived from the MIT-licensed roice3/Magic120Cell
* move engine. Copyright (c) 2016 Roice Nelson.
*/
UCLASS()
class UNREALHYPERTWIST_API UHyperTwistMagic120CellRuntimeLibrary
: public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Magic120Cell")
static FHyperTwistPuzzleDefinitionRef MakePuzzleDefinition();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Magic120Cell")
static FHyperTwistMagic120CellRuntimeState BuildSolvedState();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Magic120Cell")
static FHyperTwistPuzzleState BuildPuzzleStateEnvelope(
const FHyperTwistMagic120CellRuntimeState& RuntimeState
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Magic120Cell")
static FHyperTwistMagic120CellTurnResult ApplyTurn(
const FHyperTwistMagic120CellRuntimeState& State,
const FHyperTwistMagic120CellTurnRequest& Request
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Magic120Cell")
static FHyperTwistMagic120CellScrambleResult GenerateScramble(
int32 MoveCount = 100,
int32 RandomSeed = 2027
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Magic120Cell")
static FHyperTwistMagic120CellProjectionBuildResult BuildProjection(
const FHyperTwistMagic120CellRuntimeState& State
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Magic120Cell")
static int32 GetTurnOrderForSticker(int32 StickerIndex);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Magic120Cell")
static FString SerializeRuntimeStateToJson(
const FHyperTwistMagic120CellRuntimeState& RuntimeState
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Magic120Cell")
static bool TryDeserializeRuntimeStateFromJson(
const FString& Json,
FHyperTwistMagic120CellRuntimeState& OutRuntimeState
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Magic120Cell")
static bool IsPermutationTableReady(FString& OutError);
};

View file

@ -0,0 +1,300 @@
#pragma once
#include "CoreMinimal.h"
#include "HyperTwistCore/HyperTwistCoreLibrary.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "HyperTwistMagicCube5DRuntimeLibrary.generated.h"
UENUM(BlueprintType)
enum class EHyperTwistMagicCube5DAxis : uint8
{
X UMETA(DisplayName = "X"),
Y UMETA(DisplayName = "Y"),
Z UMETA(DisplayName = "Z"),
W UMETA(DisplayName = "W"),
V UMETA(DisplayName = "V")
};
UENUM(BlueprintType)
enum class EHyperTwistMagicCube5DTurnDirection : uint8
{
PositiveQuarterTurn UMETA(DisplayName = "Positive Quarter Turn"),
NegativeQuarterTurn UMETA(DisplayName = "Negative Quarter Turn")
};
USTRUCT(BlueprintType)
struct FHyperTwistMagicCube5DSignedAxis
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
EHyperTwistMagicCube5DAxis Axis = EHyperTwistMagicCube5DAxis::X;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
bool bPositiveDirection = true;
};
USTRUCT(BlueprintType)
struct FHyperTwistMagicCube5DPieceOrientation
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
TArray<FHyperTwistMagicCube5DSignedAxis> Basis;
bool IsStructurallyValid() const;
};
USTRUCT(BlueprintType)
struct FHyperTwistMagicCube5DGridCoordinate
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
TArray<int32> Components;
bool IsValidForOrder(int32 Order) const;
};
USTRUCT(BlueprintType)
struct FHyperTwistMagicCube5DRuntimeState
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
FString StateProfile = TEXT("magiccube5d-order-3-piece-orientation-v1");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
FHyperTwistPuzzleDefinitionRef Definition;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
int32 Order = 3;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
TArray<int32> PositionToPiece;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
TArray<FHyperTwistMagicCube5DPieceOrientation> PieceOrientations;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
int32 AppliedMoveCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
bool bIsSolved = true;
bool IsStructurallyValid() const;
int32 GetPieceCount() const;
};
USTRUCT(BlueprintType)
struct FHyperTwistMagicCube5DTurnRequest
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
EHyperTwistMagicCube5DAxis FaceAxis = EHyperTwistMagicCube5DAxis::V;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
bool bPositiveFace = true;
/** Bit zero is the outer face layer; subsequent bits select inward layers. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
int32 SliceMask = 1;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
EHyperTwistMagicCube5DAxis RotationAxisA = EHyperTwistMagicCube5DAxis::X;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
EHyperTwistMagicCube5DAxis RotationAxisB = EHyperTwistMagicCube5DAxis::Y;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
EHyperTwistMagicCube5DTurnDirection Direction =
EHyperTwistMagicCube5DTurnDirection::PositiveQuarterTurn;
bool IsValidForOrder(int32 Order) const;
};
USTRUCT(BlueprintType)
struct FHyperTwistMagicCube5DTurnResult
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
FHyperTwistMagicCube5DRuntimeState State;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
FString AppliedNotation;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
TArray<FString> Warnings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
bool bApplied = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
bool bExactStateUpdate = false;
};
USTRUCT(BlueprintType)
struct FHyperTwistMagicCube5DScrambleResult
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
FHyperTwistMagicCube5DRuntimeState State;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
TArray<FHyperTwistMagicCube5DTurnRequest> Moves;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
TArray<FString> AppliedNotation;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
TArray<FString> Warnings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
bool bGenerated = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
bool bExactStateUpdate = false;
};
USTRUCT(BlueprintType)
struct FHyperTwistMagicCube5DProjectedCubie
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
int32 PositionIndex = INDEX_NONE;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
int32 PieceId = INDEX_NONE;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
FHyperTwistMagicCube5DGridCoordinate Position;
/** The first outward-facing sticker color, or INDEX_NONE for an internal cubie. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
int32 RepresentativeColorIndex = INDEX_NONE;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
bool bPieceInSolvedPosition = false;
bool IsStructurallyValid(int32 Order) const;
};
USTRUCT(BlueprintType)
struct FHyperTwistMagicCube5DProjectedFacelet
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
int32 PositionIndex = INDEX_NONE;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
int32 PieceId = INDEX_NONE;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
EHyperTwistMagicCube5DAxis WorldAxis = EHyperTwistMagicCube5DAxis::X;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
bool bPositiveWorldSide = true;
/** Solved colors are X-/X+/Y-/Y+/Z-/Z+/W-/W+/V-/V+. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
int32 ColorIndex = INDEX_NONE;
bool IsStructurallyValid(int32 PieceCount) const;
};
USTRUCT(BlueprintType)
struct FHyperTwistMagicCube5DProjection
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
FString ProjectionProfile = TEXT("magiccube5d-boundary-facelet-projection-v1");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
int32 Order = 3;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
TArray<FHyperTwistMagicCube5DProjectedCubie> Cubies;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
TArray<FHyperTwistMagicCube5DProjectedFacelet> Facelets;
bool IsStructurallyValid() const;
};
USTRUCT(BlueprintType)
struct FHyperTwistMagicCube5DProjectionBuildResult
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
FHyperTwistMagicCube5DProjection Projection;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
TArray<FString> Warnings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
bool bProjected = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|MagicCube5D")
bool bExactProjection = false;
};
UCLASS()
class UNREALHYPERTWIST_API UHyperTwistMagicCube5DRuntimeLibrary
: public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|MagicCube5D")
static bool IsSupportedOrder(int32 Order);
UFUNCTION(BlueprintPure, Category = "HyperTwist|MagicCube5D")
static FHyperTwistPuzzleDefinitionRef MakePuzzleDefinition(int32 Order = 3);
UFUNCTION(BlueprintPure, Category = "HyperTwist|MagicCube5D")
static FHyperTwistMagicCube5DRuntimeState BuildSolvedState(int32 Order = 3);
UFUNCTION(BlueprintPure, Category = "HyperTwist|MagicCube5D")
static FHyperTwistPuzzleState BuildPuzzleStateEnvelope(
const FHyperTwistMagicCube5DRuntimeState& RuntimeState
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|MagicCube5D")
static FHyperTwistMagicCube5DTurnResult ApplyTurn(
const FHyperTwistMagicCube5DRuntimeState& State,
const FHyperTwistMagicCube5DTurnRequest& Request
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|MagicCube5D")
static FHyperTwistMagicCube5DScrambleResult GenerateScramble(
int32 Order = 3,
int32 MoveCount = 60,
int32 RandomSeed = 2027
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|MagicCube5D")
static FHyperTwistMagicCube5DProjectionBuildResult BuildProjection(
const FHyperTwistMagicCube5DRuntimeState& State
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|MagicCube5D")
static FString SerializeRuntimeStateToJson(
const FHyperTwistMagicCube5DRuntimeState& RuntimeState
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|MagicCube5D")
static bool TryDeserializeRuntimeStateFromJson(
const FString& Json,
FHyperTwistMagicCube5DRuntimeState& OutRuntimeState
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|MagicCube5D")
static FString GetAxisLabel(EHyperTwistMagicCube5DAxis Axis);
};

View file

@ -56,6 +56,9 @@ public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda")
TArray<FString> LastWarnings; TArray<FString> LastWarnings;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda|Persistence")
FString LastPersistenceStatus = TEXT("not loaded");
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda") UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda")
void ResetToSolvedState(); void ResetToSolvedState();
@ -75,12 +78,27 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda") UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda")
bool RefreshProjection(); bool RefreshProjection();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda|Persistence")
bool SaveRuntimeState();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda|Persistence")
bool LoadRuntimeState();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Melinda|Persistence")
static FString GetRuntimeSaveSlotName();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Melinda") UFUNCTION(BlueprintPure, Category = "HyperTwist|Melinda")
bool HasValidProjection() const bool HasValidProjection() const
{ {
return CurrentProjection.IsStructurallyValid(); return CurrentProjection.IsStructurallyValid();
} }
UFUNCTION(BlueprintPure, Category = "HyperTwist|Melinda")
int32 GetRenderableCubieViewCount() const
{
return SpawnedCubieMeshes.Num();
}
protected: protected:
struct FDisplayedCubieMetadata struct FDisplayedCubieMetadata
{ {

View file

@ -36,6 +36,9 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
int32 StartupRandomSeed = 2026; int32 StartupRandomSeed = 2026;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Persistence")
bool bLoadSavedStateOnBeginPlay = true;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda")
TObjectPtr<AHyperTwistMelindaProjectionActor> ActiveProjectionActor = nullptr; TObjectPtr<AHyperTwistMelindaProjectionActor> ActiveProjectionActor = nullptr;

View file

@ -1,14 +1,15 @@
#pragma once #pragma once
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "GameFramework/PlayerController.h" #include "HyperTwistUX/HyperTwistPlayerControllerBase.h"
#include "HyperTwistMelindaProjectionPlayerController.generated.h" #include "HyperTwistMelindaProjectionPlayerController.generated.h"
class AHyperTwistMelindaProjectionActor; class AHyperTwistMelindaProjectionActor;
class UHyperTwistFourDimensionalHUDWidget;
UCLASS(BlueprintType, Blueprintable) UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API AHyperTwistMelindaProjectionPlayerController class UNREALHYPERTWIST_API AHyperTwistMelindaProjectionPlayerController
: public APlayerController : public AHyperTwistPlayerControllerBase
{ {
GENERATED_BODY() GENERATED_BODY()
@ -36,6 +37,18 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Input") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Input")
int32 NextRandomSeed = 2027; int32 NextRandomSeed = 2027;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|HUD")
bool bShowPuzzleHud = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|HUD")
int32 PuzzleHudZOrder = 120;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|HUD")
TSubclassOf<UHyperTwistFourDimensionalHUDWidget> PuzzleHudWidgetClass;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda|HUD")
TObjectPtr<UHyperTwistFourDimensionalHUDWidget> ActivePuzzleHudWidget = nullptr;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda|Input") UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda|Input")
bool TryProcessProjectionClickFromCursor(bool bCounterClockwise = false); bool TryProcessProjectionClickFromCursor(bool bCounterClockwise = false);
@ -52,11 +65,29 @@ public:
bool GenerateProjectionRandomState(); bool GenerateProjectionRandomState();
protected: protected:
virtual FString GetPauseMenuTitle() const override;
virtual FString GetPauseMenuSubtitle() const override;
void ApplyInputMode(); void ApplyInputMode();
bool ShowPuzzleHud();
void RefreshPuzzleHud();
AHyperTwistMelindaProjectionActor* ResolveProjectionActor() const; AHyperTwistMelindaProjectionActor* ResolveProjectionActor() const;
void HandlePrimaryClick(); void HandlePrimaryClick();
void HandleSecondaryClick(); void HandleSecondaryClick();
void HandleResetShortcut(); void HandleResetShortcut();
void HandleRandomizeShortcut(); void HandleRandomizeShortcut();
void HandleSaveShortcut();
void HandleLoadShortcut();
void HandleTouchPressed(ETouchIndex::Type FingerIndex, FVector Location); void HandleTouchPressed(ETouchIndex::Type FingerIndex, FVector Location);
UFUNCTION()
void HandleHudScramble();
UFUNCTION()
void HandleHudReset();
UFUNCTION()
void HandleHudSave();
UFUNCTION()
void HandleHudLoad();
}; };

View file

@ -20,6 +20,9 @@ public:
virtual void OnConstruction(const FTransform& Transform) override; virtual void OnConstruction(const FTransform& Transform) override;
virtual void BeginPlay() override; virtual void BeginPlay() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333")
int32 PuzzleOrder = 3;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333|Rendering") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333|Rendering")
float TesseractSize = 42.0f; float TesseractSize = 42.0f;
@ -68,9 +71,21 @@ public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Virtual3333") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Virtual3333")
TArray<FString> LastWarnings; TArray<FString> LastWarnings;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Virtual3333")
TArray<FString> LastScramble;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Virtual3333|Persistence")
FString LastPersistenceStatus = TEXT("not loaded");
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333") UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333")
void ResetToSolvedState(); void ResetToSolvedState();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333")
bool GenerateScramble(int32 MoveCount = 40, int32 RandomSeed = 2027);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333")
bool SetPuzzleOrder(int32 NewOrder);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333") UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333")
bool ApplyTurnRequest(const FHyperTwistVirtual3333SliceTurnRequest& Request); bool ApplyTurnRequest(const FHyperTwistVirtual3333SliceTurnRequest& Request);
@ -86,6 +101,12 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333") UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333")
bool SetSliceCoordinate(int32 SliceCoordinate); bool SetSliceCoordinate(int32 SliceCoordinate);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333")
bool SetSliceLayer(int32 OneBasedLayer);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333")
bool CycleSliceCoordinate(int32 DirectionStep = 1);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333") UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333")
bool SetRotationAxis(EHyperTwistVirtual3333Axis RotationAxis); bool SetRotationAxis(EHyperTwistVirtual3333Axis RotationAxis);
@ -98,12 +119,27 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333") UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333")
bool RefreshProjection(); bool RefreshProjection();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333|Persistence")
bool SaveRuntimeState();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333|Persistence")
bool LoadRuntimeState();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Virtual3333|Persistence")
FString GetRuntimeSaveSlotName() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Virtual3333") UFUNCTION(BlueprintPure, Category = "HyperTwist|Virtual3333")
bool HasValidProjection() const bool HasValidProjection() const
{ {
return CurrentProjection.IsStructurallyValid(); return CurrentProjection.IsStructurallyValid();
} }
UFUNCTION(BlueprintPure, Category = "HyperTwist|Virtual3333")
int32 GetRenderableTesseractCount() const
{
return SpawnedTesseractMeshes.Num();
}
UFUNCTION(BlueprintPure, Category = "HyperTwist|Virtual3333") UFUNCTION(BlueprintPure, Category = "HyperTwist|Virtual3333")
FString GetSelectionLabel() const; FString GetSelectionLabel() const;

View file

@ -22,6 +22,9 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333")
bool bAutoSpawnProjectionActor = true; bool bAutoSpawnProjectionActor = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333")
int32 PuzzleOrder = 3;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333")
TSubclassOf<AHyperTwistVirtual3333ProjectionActor> ProjectionActorClass; TSubclassOf<AHyperTwistVirtual3333ProjectionActor> ProjectionActorClass;
@ -40,6 +43,9 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333")
EHyperTwistVirtual3333Axis StartupRotationAxis = EHyperTwistVirtual3333Axis::X; EHyperTwistVirtual3333Axis StartupRotationAxis = EHyperTwistVirtual3333Axis::X;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333|Persistence")
bool bLoadSavedStateOnBeginPlay = true;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Virtual3333") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Virtual3333")
TObjectPtr<AHyperTwistVirtual3333ProjectionActor> ActiveProjectionActor = nullptr; TObjectPtr<AHyperTwistVirtual3333ProjectionActor> ActiveProjectionActor = nullptr;
@ -49,3 +55,33 @@ public:
protected: protected:
AHyperTwistVirtual3333ProjectionActor* ResolveOrSpawnProjectionActor(); AHyperTwistVirtual3333ProjectionActor* ResolveOrSpawnProjectionActor();
}; };
UCLASS()
class UNREALHYPERTWIST_API AHyperTwistVirtual4444ProjectionGameMode
: public AHyperTwistVirtual3333ProjectionGameMode
{
GENERATED_BODY()
public:
AHyperTwistVirtual4444ProjectionGameMode();
};
UCLASS()
class UNREALHYPERTWIST_API AHyperTwistVirtual5555ProjectionGameMode
: public AHyperTwistVirtual3333ProjectionGameMode
{
GENERATED_BODY()
public:
AHyperTwistVirtual5555ProjectionGameMode();
};
UCLASS()
class UNREALHYPERTWIST_API AHyperTwistVirtual6666ProjectionGameMode
: public AHyperTwistVirtual3333ProjectionGameMode
{
GENERATED_BODY()
public:
AHyperTwistVirtual6666ProjectionGameMode();
};

View file

@ -54,6 +54,8 @@ struct FHyperTwistVirtual3333PieceOrientation
} }
bool bSeenAxes[4] = {false, false, false, false}; bool bSeenAxes[4] = {false, false, false, false};
int32 NegativeDirectionCount = 0;
int32 PermutationInversionCount = 0;
for (const FHyperTwistVirtual3333SignedAxis& SignedAxis : Basis) for (const FHyperTwistVirtual3333SignedAxis& SignedAxis : Basis)
{ {
const int32 AxisIndex = static_cast<int32>(SignedAxis.Axis); const int32 AxisIndex = static_cast<int32>(SignedAxis.Axis);
@ -66,9 +68,23 @@ struct FHyperTwistVirtual3333PieceOrientation
} }
bSeenAxes[AxisIndex] = true; bSeenAxes[AxisIndex] = true;
NegativeDirectionCount += SignedAxis.bPositiveDirection ? 0 : 1;
} }
return true; for (int32 LeftIndex = 0; LeftIndex < Basis.Num(); ++LeftIndex)
{
for (int32 RightIndex = LeftIndex + 1; RightIndex < Basis.Num(); ++RightIndex)
{
if (static_cast<int32>(Basis[LeftIndex].Axis)
> static_cast<int32>(Basis[RightIndex].Axis))
{
++PermutationInversionCount;
}
}
}
// Legal 4D quarter turns are proper rotations, never reflections.
return (NegativeDirectionCount + PermutationInversionCount) % 2 == 0;
} }
}; };
@ -91,11 +107,13 @@ struct FHyperTwistVirtual3333GridCoordinate
bool IsStructurallyValid() const bool IsStructurallyValid() const
{ {
return X >= -1 && X <= 1 return FMath::Abs(X) <= 8
&& Y >= -1 && Y <= 1 && FMath::Abs(Y) <= 8
&& Z >= -1 && Z <= 1 && FMath::Abs(Z) <= 8
&& W >= -1 && W <= 1; && FMath::Abs(W) <= 8;
} }
bool IsValidForOrder(int32 Order) const;
}; };
USTRUCT(BlueprintType) USTRUCT(BlueprintType)
@ -119,6 +137,8 @@ struct FHyperTwistVirtual3333RuntimeState
bool bIsSolved = true; bool bIsSolved = true;
bool IsStructurallyValid() const; bool IsStructurallyValid() const;
int32 GetOrder() const;
int32 GetPieceCount() const;
}; };
USTRUCT(BlueprintType) USTRUCT(BlueprintType)
@ -140,9 +160,7 @@ struct FHyperTwistVirtual3333SliceTurnRequest
bool IsStructurallyValid() const bool IsStructurallyValid() const
{ {
return SliceCoordinate >= -1 return FMath::Abs(SliceCoordinate) <= 8 && RotationAxis != SliceAxis;
&& SliceCoordinate <= 1
&& RotationAxis != SliceAxis;
} }
}; };
@ -167,6 +185,30 @@ struct FHyperTwistVirtual3333SliceTurnResult
bool bExactStateUpdate = false; bool bExactStateUpdate = false;
}; };
USTRUCT(BlueprintType)
struct FHyperTwistVirtual3333ScrambleResult
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333")
FHyperTwistVirtual3333RuntimeState State;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333")
TArray<FString> AppliedMoves;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333")
FHyperTwistVirtual3333SliceTurnRequest FinalSelection;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333")
TArray<FString> Warnings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333")
bool bGenerated = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333")
bool bExactStateUpdate = false;
};
USTRUCT(BlueprintType) USTRUCT(BlueprintType)
struct FHyperTwistVirtual3333ProjectedCell struct FHyperTwistVirtual3333ProjectedCell
{ {
@ -213,6 +255,9 @@ struct FHyperTwistVirtual3333ProjectedTesseract
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333")
bool bPieceInSolvedPosition = false; bool bPieceInSolvedPosition = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333")
int32 PuzzleOrder = 3;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333")
TArray<FHyperTwistVirtual3333ProjectedCell> VisibleCells; TArray<FHyperTwistVirtual3333ProjectedCell> VisibleCells;
@ -271,10 +316,10 @@ class UNREALHYPERTWIST_API UHyperTwistVirtual3333ProjectionLibrary
public: public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Virtual3333") UFUNCTION(BlueprintPure, Category = "HyperTwist|Virtual3333")
static FHyperTwistPuzzleDefinitionRef MakePuzzleDefinition(); static FHyperTwistPuzzleDefinitionRef MakePuzzleDefinition(int32 Order = 3);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Virtual3333") UFUNCTION(BlueprintPure, Category = "HyperTwist|Virtual3333")
static FHyperTwistVirtual3333RuntimeState BuildSolvedState(); static FHyperTwistVirtual3333RuntimeState BuildSolvedState(int32 Order = 3);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Virtual3333") UFUNCTION(BlueprintPure, Category = "HyperTwist|Virtual3333")
static FHyperTwistPuzzleState BuildPuzzleStateEnvelope( static FHyperTwistPuzzleState BuildPuzzleStateEnvelope(
@ -294,6 +339,15 @@ public:
EHyperTwistVirtual3333Axis SliceAxis EHyperTwistVirtual3333Axis SliceAxis
); );
UFUNCTION(BlueprintPure, Category = "HyperTwist|Virtual3333")
static TArray<int32> GetSliceCoordinatesForOrder(int32 Order);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Virtual3333")
static int32 GetSliceCoordinateForLayer(int32 Order, int32 OneBasedLayer);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Virtual3333")
static bool IsSupportedOrder(int32 Order);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Virtual3333") UFUNCTION(BlueprintPure, Category = "HyperTwist|Virtual3333")
static bool IsRotationAxisAvailable( static bool IsRotationAxisAvailable(
EHyperTwistVirtual3333Axis SliceAxis, EHyperTwistVirtual3333Axis SliceAxis,
@ -306,6 +360,13 @@ public:
const FHyperTwistVirtual3333SliceTurnRequest& Request const FHyperTwistVirtual3333SliceTurnRequest& Request
); );
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333")
static FHyperTwistVirtual3333ScrambleResult GenerateScramble(
int32 Order,
int32 MoveCount = 40,
int32 RandomSeed = 2027
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333") UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333")
static FHyperTwistVirtual3333ProjectionBuildResult BuildVisibleProjection( static FHyperTwistVirtual3333ProjectionBuildResult BuildVisibleProjection(
const FHyperTwistVirtual3333RuntimeState& State, const FHyperTwistVirtual3333RuntimeState& State,

View file

@ -1,15 +1,16 @@
#pragma once #pragma once
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "GameFramework/PlayerController.h" #include "HyperTwistUX/HyperTwistPlayerControllerBase.h"
#include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionLibrary.h" #include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionLibrary.h"
#include "HyperTwistVirtual3333ProjectionPlayerController.generated.h" #include "HyperTwistVirtual3333ProjectionPlayerController.generated.h"
class AHyperTwistVirtual3333ProjectionActor; class AHyperTwistVirtual3333ProjectionActor;
class UHyperTwistFourDimensionalHUDWidget;
UCLASS(BlueprintType, Blueprintable) UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API AHyperTwistVirtual3333ProjectionPlayerController class UNREALHYPERTWIST_API AHyperTwistVirtual3333ProjectionPlayerController
: public APlayerController : public AHyperTwistPlayerControllerBase
{ {
GENERATED_BODY() GENERATED_BODY()
@ -22,6 +23,24 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333|Input") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333|Input")
bool bUseGameAndUiInputMode = true; bool bUseGameAndUiInputMode = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333|Input")
int32 ScrambleMoveCount = 40;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333|Input")
int32 NextScrambleSeed = 2027;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333|HUD")
bool bShowPuzzleHud = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333|HUD")
int32 PuzzleHudZOrder = 120;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Virtual3333|HUD")
TSubclassOf<UHyperTwistFourDimensionalHUDWidget> PuzzleHudWidgetClass;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Virtual3333|HUD")
TObjectPtr<UHyperTwistFourDimensionalHUDWidget> ActivePuzzleHudWidget = nullptr;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333|Input") UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333|Input")
void ResetProjectionToSolved(); void ResetProjectionToSolved();
@ -37,6 +56,12 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333|Input") UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333|Input")
bool SetSliceCoordinate(int32 SliceCoordinate); bool SetSliceCoordinate(int32 SliceCoordinate);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333|Input")
bool SetSliceLayer(int32 OneBasedLayer);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333|Input")
bool CycleSliceLayer(int32 DirectionStep = 1);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333|Input") UFUNCTION(BlueprintCallable, Category = "HyperTwist|Virtual3333|Input")
bool CycleRotationAxis(int32 DirectionStep = 1); bool CycleRotationAxis(int32 DirectionStep = 1);
@ -44,7 +69,11 @@ public:
void ToggleCellShellRendering(); void ToggleCellShellRendering();
protected: protected:
virtual FString GetPauseMenuTitle() const override;
virtual FString GetPauseMenuSubtitle() const override;
void ApplyInputMode(); void ApplyInputMode();
bool ShowPuzzleHud();
void RefreshPuzzleHud();
AHyperTwistVirtual3333ProjectionActor* ResolveProjectionActor() const; AHyperTwistVirtual3333ProjectionActor* ResolveProjectionActor() const;
void HandleSliceAxisX(); void HandleSliceAxisX();
void HandleSliceAxisY(); void HandleSliceAxisY();
@ -53,10 +82,54 @@ protected:
void HandleSliceCoordinateNegative(); void HandleSliceCoordinateNegative();
void HandleSliceCoordinateMiddle(); void HandleSliceCoordinateMiddle();
void HandleSliceCoordinatePositive(); void HandleSliceCoordinatePositive();
void HandleSliceLayerFour();
void HandleSliceLayerFive();
void HandleSliceLayerSix();
void HandlePreviousSliceLayer();
void HandleNextSliceLayer();
void HandleCycleRotationAxisBackward(); void HandleCycleRotationAxisBackward();
void HandleCycleRotationAxisForward(); void HandleCycleRotationAxisForward();
void HandleApplyCounterClockwise(); void HandleApplyCounterClockwise();
void HandleApplyClockwise(); void HandleApplyClockwise();
void HandleResetShortcut(); void HandleResetShortcut();
void HandleScrambleShortcut();
void HandleToggleShellShortcut(); void HandleToggleShellShortcut();
void HandleSaveShortcut();
void HandleLoadShortcut();
UFUNCTION()
void HandleHudCycleAxis();
UFUNCTION()
void HandleHudPreviousLayer();
UFUNCTION()
void HandleHudNextLayer();
UFUNCTION()
void HandleHudPreviousPlane();
UFUNCTION()
void HandleHudNextPlane();
UFUNCTION()
void HandleHudCounterClockwise();
UFUNCTION()
void HandleHudClockwise();
UFUNCTION()
void HandleHudToggleShell();
UFUNCTION()
void HandleHudScramble();
UFUNCTION()
void HandleHudReset();
UFUNCTION()
void HandleHudSave();
UFUNCTION()
void HandleHudLoad();
}; };

View file

@ -1,14 +1,15 @@
#pragma once #pragma once
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "HyperTwistTraining/HyperTwistTrainingTypes.h" #include "HyperTwistTraining/HyperTwistTrainingTypes.h"
#include "HyperTwistUX/HyperTwistPlayerControllerBase.h"
#include "HyperTwistCoachDashboardPlayerController.generated.h" #include "HyperTwistCoachDashboardPlayerController.generated.h"
class AHyperTwistCoachDashboardActor; class AHyperTwistCoachDashboardActor;
UCLASS(BlueprintType, Blueprintable) UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API AHyperTwistCoachDashboardPlayerController : public APlayerController class UNREALHYPERTWIST_API AHyperTwistCoachDashboardPlayerController
: public AHyperTwistPlayerControllerBase
{ {
GENERATED_BODY() GENERATED_BODY()

View file

@ -12,6 +12,84 @@ enum class EHyperTwistPackagedStartupRouteParseResult : uint8
Invalid Invalid
}; };
UENUM(BlueprintType)
enum class EHyperTwistPuzzleAvailability : uint8
{
Playable,
BrowserPreview,
LearningLibrary,
Planned
};
UENUM(BlueprintType)
enum class EHyperTwistIntegrationLicenseLane : uint8
{
Permissive,
RestrictiveCleanRoom,
BoundarySensitive
};
UENUM(BlueprintType)
enum class EHyperTwistIntegrationSurface : uint8
{
NativePlayable,
NativePlayerService,
BrowserSupport,
OperatorSupport
};
USTRUCT(BlueprintType)
struct UNREALHYPERTWIST_API FHyperTwistIntegratedCapability
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Integration")
FString CapabilityId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Integration")
FString Title;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Integration")
FString Summary;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Integration")
FString CanonicalRepository;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Integration")
FString LicenseId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Integration")
EHyperTwistIntegrationLicenseLane LicenseLane =
EHyperTwistIntegrationLicenseLane::Permissive;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Integration")
EHyperTwistIntegrationSurface Surface =
EHyperTwistIntegrationSurface::NativePlayerService;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Integration")
FString PrimaryExperienceRoute;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Integration")
FString RuntimeOwnerId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Integration")
FString ImplementationPosture;
bool IsStructurallyValid() const
{
return !CapabilityId.IsEmpty()
&& !Title.IsEmpty()
&& !Summary.IsEmpty()
&& !CanonicalRepository.IsEmpty()
&& !LicenseId.IsEmpty()
&& !PrimaryExperienceRoute.IsEmpty()
&& !RuntimeOwnerId.IsEmpty()
&& !ImplementationPosture.IsEmpty()
&& (LicenseLane != EHyperTwistIntegrationLicenseLane::RestrictiveCleanRoom
|| ImplementationPosture.Contains(TEXT("clean-room")));
}
};
USTRUCT(BlueprintType) USTRUCT(BlueprintType)
struct UNREALHYPERTWIST_API FHyperTwistFirstRunLaunchRoute struct UNREALHYPERTWIST_API FHyperTwistFirstRunLaunchRoute
{ {
@ -41,6 +119,12 @@ struct UNREALHYPERTWIST_API FHyperTwistFirstRunLaunchRoute
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun")
FString RuntimeProofStatus; FString RuntimeProofStatus;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun")
FString PlayerSection;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun")
FString AvailabilityLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun")
bool bDefaultSafeChoice = false; bool bDefaultSafeChoice = false;
@ -62,9 +146,53 @@ struct UNREALHYPERTWIST_API FHyperTwistFirstRunLaunchRoute
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun")
bool bHasWebSimulatorEquivalent = false; bool bHasWebSimulatorEquivalent = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun")
bool bPlayerFacing = true;
bool IsStructurallyValid() const; bool IsStructurallyValid() const;
}; };
USTRUCT(BlueprintType)
struct UNREALHYPERTWIST_API FHyperTwistPuzzleCatalogEntry
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Catalog")
FString PuzzleId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Catalog")
FString Title;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Catalog")
FString Dimensionality;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Catalog")
FString Description;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Catalog")
EHyperTwistPuzzleAvailability Availability = EHyperTwistPuzzleAvailability::Planned;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Catalog")
FString AvailabilityLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Catalog")
FString LaunchRouteId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Catalog")
TArray<FString> ExperienceTags;
bool IsStructurallyValid() const
{
return !PuzzleId.IsEmpty()
&& !Title.IsEmpty()
&& !Dimensionality.IsEmpty()
&& !Description.IsEmpty()
&& !AvailabilityLabel.IsEmpty()
&& (Availability != EHyperTwistPuzzleAvailability::Playable
|| !LaunchRouteId.IsEmpty());
}
};
UCLASS() UCLASS()
class UNREALHYPERTWIST_API UHyperTwistFirstRunLaunchLibrary class UNREALHYPERTWIST_API UHyperTwistFirstRunLaunchLibrary
: public UBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
@ -102,6 +230,34 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|FirstRun") UFUNCTION(BlueprintPure, Category = "HyperTwist|FirstRun")
static TArray<FHyperTwistFirstRunLaunchRoute> BuildFirstRunLaunchRoutes(); static TArray<FHyperTwistFirstRunLaunchRoute> BuildFirstRunLaunchRoutes();
UFUNCTION(BlueprintPure, Category = "HyperTwist|FirstRun")
static TArray<FHyperTwistPuzzleCatalogEntry> BuildPlayerPuzzleCatalog();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Integration")
static TArray<FHyperTwistIntegratedCapability> BuildIntegratedCapabilityCatalog();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Integration")
static bool TryFindIntegratedCapability(
const FString& CanonicalRepository,
FHyperTwistIntegratedCapability& OutCapability
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Integration")
static FString GetIntegrationLicenseLaneLabel(
EHyperTwistIntegrationLicenseLane LicenseLane
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Integration")
static FString GetIntegrationSurfaceLabel(
EHyperTwistIntegrationSurface Surface
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|FirstRun")
static bool TryFindPuzzleCatalogEntry(
const FString& PuzzleId,
FHyperTwistPuzzleCatalogEntry& OutEntry
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|FirstRun") UFUNCTION(BlueprintPure, Category = "HyperTwist|FirstRun")
static bool TryFindFirstRunLaunchRoute( static bool TryFindFirstRunLaunchRoute(
const FString& RouteId, const FString& RouteId,

View file

@ -19,6 +19,7 @@ public:
AHyperTwistFirstRunLaunchPlayerController(); AHyperTwistFirstRunLaunchPlayerController();
virtual void BeginPlay() override; virtual void BeginPlay() override;
virtual void SetupInputComponent() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun")
bool bShowFirstRunLaunchOnBeginPlay = true; bool bShowFirstRunLaunchOnBeginPlay = true;
@ -68,6 +69,9 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|FirstRun") UFUNCTION(BlueprintCallable, Category = "HyperTwist|FirstRun")
void HandleFirstRunRouteRequested(const FString& RouteId); void HandleFirstRunRouteRequested(const FString& RouteId);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|FirstRun")
void ReturnFromAdvancedDashboard();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|FirstRun|Dashboard") UFUNCTION(BlueprintCallable, Category = "HyperTwist|FirstRun|Dashboard")
AHyperTwistCoachDashboardActor* EnsureCoachDashboard(); AHyperTwistCoachDashboardActor* EnsureCoachDashboard();

View file

@ -6,10 +6,16 @@
#include "HyperTwistFirstRunLaunchWidget.generated.h" #include "HyperTwistFirstRunLaunchWidget.generated.h"
class FJsonObject;
class IHttpRequest;
class UButton; class UButton;
class UEditableTextBox;
class UHyperTwistSettingsPanelWidget;
class UHyperTwistTrainingSubsystem;
class UTextBlock; class UTextBlock;
class UUniformGridPanel;
class UVerticalBox; class UVerticalBox;
class UWidget; class UWidgetSwitcher;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(
FHyperTwistFirstRunRouteRequestedSignature, FHyperTwistFirstRunRouteRequestedSignature,
@ -17,6 +23,62 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(
RouteId RouteId
); );
UENUM(BlueprintType)
enum class EHyperTwistMainMenuPage : uint8
{
Home,
Puzzles,
Learn,
Settings,
Account,
Advanced,
About
};
UCLASS()
class UNREALHYPERTWIST_API UHyperTwistPuzzleCardWidget : public UUserWidget
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|MainMenu")
void ConfigureCatalogEntry(const FHyperTwistPuzzleCatalogEntry& InEntry);
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|MainMenu")
FHyperTwistFirstRunRouteRequestedSignature OnLaunchRequested;
protected:
virtual TSharedRef<SWidget> RebuildWidget() override;
private:
void EnsureWidgetTreeBuilt();
void RefreshFromCatalogEntry();
UFUNCTION()
void HandleLaunchClicked();
FHyperTwistPuzzleCatalogEntry Entry;
FString ActionId;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> AvailabilityText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TitleText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> DescriptionText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TagsText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> LaunchButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> LaunchButtonText = nullptr;
};
UCLASS(BlueprintType, Blueprintable) UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API UHyperTwistFirstRunLaunchWidget class UNREALHYPERTWIST_API UHyperTwistFirstRunLaunchWidget
: public UUserWidget : public UUserWidget
@ -26,18 +88,28 @@ class UNREALHYPERTWIST_API UHyperTwistFirstRunLaunchWidget
public: public:
virtual void NativeOnInitialized() override; virtual void NativeOnInitialized() override;
virtual void NativeConstruct() override; virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun")
FLinearColor BackgroundColor = FLinearColor(0.010f, 0.014f, 0.024f, 0.96f); FLinearColor BackgroundColor = FLinearColor(0.007f, 0.012f, 0.021f, 1.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun")
FLinearColor PanelColor = FLinearColor(0.018f, 0.030f, 0.047f, 0.985f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun")
FLinearColor CardColor = FLinearColor(0.034f, 0.054f, 0.078f, 0.98f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun")
FLinearColor AccentColor = FLinearColor(0.0f, 0.88f, 0.78f, 1.0f); FLinearColor AccentColor = FLinearColor(0.0f, 0.88f, 0.78f, 1.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun")
FLinearColor TextColor = FLinearColor(0.88f, 0.94f, 0.96f, 1.0f); FLinearColor WarmAccentColor = FLinearColor(1.0f, 0.62f, 0.22f, 1.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun")
FLinearColor MutedTextColor = FLinearColor(0.58f, 0.70f, 0.74f, 1.0f); FLinearColor TextColor = FLinearColor(0.91f, 0.96f, 0.98f, 1.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|FirstRun")
FLinearColor MutedTextColor = FLinearColor(0.58f, 0.70f, 0.76f, 1.0f);
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|FirstRun") UPROPERTY(BlueprintAssignable, Category = "HyperTwist|FirstRun")
FHyperTwistFirstRunRouteRequestedSignature OnFirstRunRouteRequested; FHyperTwistFirstRunRouteRequestedSignature OnFirstRunRouteRequested;
@ -48,8 +120,15 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|FirstRun") UFUNCTION(BlueprintPure, Category = "HyperTwist|FirstRun")
bool IsFirstRunLaunchSurfaceReady() const; bool IsFirstRunLaunchSurfaceReady() const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|MainMenu")
void ShowPage(EHyperTwistMainMenuPage Page);
protected: protected:
virtual TSharedRef<SWidget> RebuildWidget() override; virtual TSharedRef<SWidget> RebuildWidget() override;
virtual FReply NativeOnKeyDown(
const FGeometry& InGeometry,
const FKeyEvent& InKeyEvent
) override;
UFUNCTION() UFUNCTION()
void OpenCoachDashboard(); void OpenCoachDashboard();
@ -70,17 +149,185 @@ protected:
void OpenXrTrainingValidation(); void OpenXrTrainingValidation();
private: private:
UHyperTwistTrainingSubsystem* ResolveTrainingSubsystem() const;
void StartLearningProgram(const FString& ProgramTitle, const FString& DeckId);
void SubmitLearningStudioAttempt(bool bRemembered);
void RefreshLearningStudioSurface(const FString& StatusMessage = FString(), bool bError = false);
void RequestRoute(const FString& RouteId); void RequestRoute(const FString& RouteId);
void BuildNavigation(UVerticalBox* NavigationColumn);
void BuildHomePage(UVerticalBox* Page);
void BuildPuzzlePage(UVerticalBox* Page);
void BuildLearnPage(UVerticalBox* Page);
void BuildSettingsPage(UVerticalBox* Page);
void BuildAccountPage(UVerticalBox* Page);
void BuildAdvancedPage(UVerticalBox* Page);
void BuildAboutPage(UVerticalBox* Page);
void RefreshAccountSurface(const FString& StatusMessage = FString(), bool bError = false);
void ProcessDesktopLinkResponse(bool bConnectedSuccessfully, int32 StatusCode, const FString& Body);
void OpenExternalUrl(const FString& Url) const;
UTextBlock* AddTextLine( UTextBlock* AddTextLine(
UVerticalBox* Parent, UVerticalBox* Parent,
const FString& Text, const FString& Text,
int32 FontSize, int32 FontSize,
const FLinearColor& Color const FLinearColor& Color,
const FName& WidgetName = NAME_None
) const; ) const;
UButton* AddRouteButton( UButton* AddNavigationButton(
UVerticalBox* Parent, UVerticalBox* Parent,
const FHyperTwistFirstRunLaunchRoute& Route, const FString& Label,
const FName ButtonName const FName& ButtonName
) const;
UButton* AddActionButton(
UVerticalBox* Parent,
const FString& Label,
const FString& SupportingText,
const FName& ButtonName,
bool bPrimary = false
) const;
UVerticalBox* AddPage(
const FName& PageName,
const FString& Eyebrow,
const FString& Title,
const FString& Description
);
void AddFeatureCard(
UVerticalBox* Parent,
const FString& Eyebrow,
const FString& Title,
const FString& Description,
const FLinearColor& Accent
) const; ) const;
void AddSpacer(UVerticalBox* Parent, float Height) const; void AddSpacer(UVerticalBox* Parent, float Height) const;
UFUNCTION()
void ShowHomePage();
UFUNCTION()
void ShowPuzzlesPage();
UFUNCTION()
void ShowLearnPage();
UFUNCTION()
void ShowSettingsPage();
UFUNCTION()
void ShowAccountPage();
UFUNCTION()
void ShowAdvancedPage();
UFUNCTION()
void ShowAboutPage();
UFUNCTION()
void HandleCatalogLaunchRequested(const FString& RouteId);
UFUNCTION()
void HandleStartOllLearningProgram();
UFUNCTION()
void HandleStartCrossLearningProgram();
UFUNCTION()
void HandleStartFiveStyleLearningProgram();
UFUNCTION()
void HandleStartRouxLearningProgram();
UFUNCTION()
void HandleStartBlindfoldLearningProgram();
UFUNCTION()
void HandleRevealLearningAnswer();
UFUNCTION()
void HandleLearningRemembered();
UFUNCTION()
void HandleLearningReviewAgain();
UFUNCTION()
void HandleOpenAccountWebsite();
UFUNCTION()
void HandleOpenBrowserExperience();
UFUNCTION()
void HandleOpenHelpCenter();
UFUNCTION()
void HandleLinkAccount();
UFUNCTION()
void HandleUnlinkAccount();
UFUNCTION()
void HandleOpenDiagnosticsFolder();
UFUNCTION()
void HandleExitApplication();
UPROPERTY(Transient)
TObjectPtr<UWidgetSwitcher> PageSwitcher = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> CurrentPageTitle = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> AccountIdentityText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> AccountAccessText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> AccountStatusText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UEditableTextBox> DesktopLinkTokenInput = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> LinkAccountButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> UnlinkAccountButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> LearningProgramText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> LearningProgressText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> LearningPromptText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> LearningSetupText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> LearningAnswerText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> LearningStatusText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> LearningRevealButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> LearningRememberedButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> LearningReviewButton = nullptr;
FString ActiveLearningProgramTitle;
FString ActiveLearningDeckId;
double LearningCaseStartedAtSeconds = 0.0;
bool bLearningAnswerRevealed = false;
UPROPERTY(Transient)
TObjectPtr<UHyperTwistSettingsPanelWidget> EmbeddedSettings = nullptr;
TSharedPtr<IHttpRequest, ESPMode::ThreadSafe> ActiveAccountRequest;
EHyperTwistMainMenuPage ActivePage = EHyperTwistMainMenuPage::Home;
}; };

View file

@ -10,6 +10,7 @@
class AHyperTwistHigherDimensionalTrainingShellActor; class AHyperTwistHigherDimensionalTrainingShellActor;
class ADirectionalLight; class ADirectionalLight;
class UCameraComponent; class UCameraComponent;
class UHyperTwistHigherDimensionalHUDWidget;
class USceneComponent; class USceneComponent;
class USpringArmComponent; class USpringArmComponent;
@ -94,11 +95,95 @@ public:
virtual void BeginPlay() override; virtual void BeginPlay() override;
virtual void PlayerTick(float DeltaTime) override; virtual void PlayerTick(float DeltaTime) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|HigherDimensional|HUD")
TSubclassOf<UHyperTwistHigherDimensionalHUDWidget> PuzzleHudWidgetClass;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|HigherDimensional|HUD")
bool bShowPuzzleHud = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|HigherDimensional|HUD")
int32 PuzzleHudZOrder = 120;
UPROPERTY(Transient, BlueprintReadOnly, Category = "HyperTwist|Training|HigherDimensional|HUD")
TObjectPtr<UHyperTwistHigherDimensionalHUDWidget> ActivePuzzleHudWidget = nullptr;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|HigherDimensional|HUD")
bool IsPuzzleHudReady() const;
protected:
virtual FString GetPauseMenuTitle() const override;
virtual FString GetPauseMenuSubtitle() const override;
private: private:
TWeakObjectPtr<AHyperTwistHigherDimensionalTrainingShellActor> CachedTrainingShell; TWeakObjectPtr<AHyperTwistHigherDimensionalTrainingShellActor> CachedTrainingShell;
int32 SelectedMagic120CellIndex = 0;
int32 SelectedMagic120StickerIndex = 1;
int32 SelectedMagicCube5DFaceAxis = 4;
int32 SelectedMagicCube5DSliceMask = 1;
int32 SelectedMagicCube5DPlaneIndex = 0;
bool bSelectedMagicCube5DPositiveFace = true;
AHyperTwistHigherDimensionalTrainingShellActor* ResolveTrainingShell(); AHyperTwistHigherDimensionalTrainingShellActor* ResolveTrainingShell();
void ToggleCoachDashboardSurface(); void ToggleCoachDashboardSurface();
void EnsurePuzzleHud();
void RefreshPuzzleHud();
FString BuildMoveSelectionSummary() const;
void ApplySelectedTurn(bool bPositiveDirection);
bool UsesMagic120CellRuntime() const;
UFUNCTION()
void HandlePreviousPrimarySelection();
UFUNCTION()
void HandleNextPrimarySelection();
UFUNCTION()
void HandlePreviousSecondarySelection();
UFUNCTION()
void HandleNextSecondarySelection();
UFUNCTION()
void HandlePreviousTertiarySelection();
UFUNCTION()
void HandleNextTertiarySelection();
UFUNCTION()
void HandleToggleFaceSide();
UFUNCTION()
void HandleNegativeTurn();
UFUNCTION()
void HandlePositiveTurn();
UFUNCTION()
void HandlePreviousProjectionLayer();
UFUNCTION()
void HandleNextProjectionLayer();
UFUNCTION()
void HandleToggleAutoRotate();
UFUNCTION()
void HandleScramble();
UFUNCTION()
void HandleReset();
UFUNCTION()
void HandleSave();
UFUNCTION()
void HandleLoad();
UFUNCTION()
void HandleCoach();
UFUNCTION()
void HandleMenu();
}; };
UCLASS(BlueprintType, Blueprintable) UCLASS(BlueprintType, Blueprintable)

View file

@ -2,6 +2,8 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "GameFramework/Actor.h" #include "GameFramework/Actor.h"
#include "HyperTwistSimulation/HyperTwistMagic120CellRuntimeLibrary.h"
#include "HyperTwistSimulation/HyperTwistMagicCube5DRuntimeLibrary.h"
#include "HyperTwistHigherDimensionalTrainingShellActor.generated.h" #include "HyperTwistHigherDimensionalTrainingShellActor.generated.h"
class UInstancedStaticMeshComponent; class UInstancedStaticMeshComponent;
@ -118,6 +120,12 @@ public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Shell|State") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Shell|State")
int32 RuntimeStateSeed = 0; int32 RuntimeStateSeed = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Shell|State")
FHyperTwistMagicCube5DRuntimeState MagicCube5DRuntimeState;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Shell|State")
FHyperTwistMagic120CellRuntimeState Magic120CellRuntimeState;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Shell|State") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Shell|State")
FString LastRuntimeAction = TEXT("ready"); FString LastRuntimeAction = TEXT("ready");
@ -142,6 +150,12 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Shell|State") UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Shell|State")
void CreateScrambledRuntimeState(); void CreateScrambledRuntimeState();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Shell|State")
bool ApplyMagicCube5DTurn(const FHyperTwistMagicCube5DTurnRequest& Request);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Shell|State")
bool ApplyMagic120CellTurn(const FHyperTwistMagic120CellTurnRequest& Request);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Shell|Persistence") UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Shell|Persistence")
bool SaveRuntimeState(); bool SaveRuntimeState();

View file

@ -0,0 +1,145 @@
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "HyperTwistCoachAssistantWidget.generated.h"
class IHttpRequest;
class UButton;
class UEditableTextBox;
class UScrollBox;
class UTextBlock;
class UVerticalBox;
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FHyperTwistCoachAssistantCloseRequestedSignature);
UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API UHyperTwistCoachAssistantWidget : public UUserWidget
{
GENERATED_BODY()
public:
UHyperTwistCoachAssistantWidget(const FObjectInitializer& ObjectInitializer);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Coach")
bool PrepareAssistantSurface();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Coach")
bool IsAssistantSurfaceReady() const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Coach")
void SetPuzzleContext(const FString& Title, const FString& Subtitle);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Coach|Speech")
void SubmitDictatedPrompt(
const FString& Transcript,
bool bSubmitImmediately
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Coach|Speech")
void SetDictationStatus(
const FString& Status,
bool bRecording,
bool bError = false
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Coach|Speech")
void SetNarrationStatus(const FString& Status, bool bError = false);
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|Coach")
FHyperTwistCoachAssistantCloseRequestedSignature OnCloseRequested;
protected:
virtual TSharedRef<SWidget> RebuildWidget() override;
virtual void NativeDestruct() override;
virtual FReply NativeOnKeyDown(
const FGeometry& InGeometry,
const FKeyEvent& InKeyEvent
) override;
private:
void EnsureWidgetTreeBuilt();
void AddConversationMessage(
const FString& RoleLabel,
const FString& Message,
bool bUserMessage
);
void SubmitPrompt(const FString& Prompt);
void BeginCoachRequest(const FString& Prompt);
void HandleCoachResponse(
bool bTransportSucceeded,
int32 StatusCode,
const FString& ResponseBody
);
void NarrateResponseIfEnabled(const FString& ResponseText);
void SetStatus(const FString& Message, bool bError = false);
FString BuildBuiltInGuideResponse(const FString& Prompt) const;
FString BuildRequestUrl(const FString& Endpoint) const;
FString BuildRequestBody(
const FString& Prompt,
bool bUseResponsesApi
) const;
FString ParseResponseText(
const FString& ResponseBody,
bool bUseResponsesApi,
FString& OutFailureReason
) const;
UFUNCTION()
void HandleSendClicked();
UFUNCTION()
void HandleClearClicked();
UFUNCTION()
void HandleDictationClicked();
UFUNCTION()
void HandleCloseClicked();
UFUNCTION()
void HandleControlsPromptClicked();
UFUNCTION()
void HandlePuzzlePromptClicked();
UFUNCTION()
void HandleSettingsPromptClicked();
UFUNCTION()
void HandlePromptCommitted(
const FText& Text,
ETextCommit::Type CommitMethod
);
UPROPERTY(Transient)
TObjectPtr<UScrollBox> ConversationScroll = nullptr;
UPROPERTY(Transient)
TObjectPtr<UVerticalBox> ConversationList = nullptr;
UPROPERTY(Transient)
TObjectPtr<UEditableTextBox> PromptInput = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> ContextText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> StatusText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> DictationButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> DictationButtonLabel = nullptr;
TSharedPtr<IHttpRequest, ESPMode::ThreadSafe> ActiveRequest;
TArray<FString> ConversationRoles;
TArray<FString> ConversationMessages;
FString PuzzleTitle = TEXT("HyperTwist");
FString PuzzleSubtitle = TEXT("Active puzzle");
FString PendingUserPrompt;
bool bRequestInFlight = false;
bool bLastRequestUsedResponsesApi = false;
};

View file

@ -0,0 +1,89 @@
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "HyperTwistDictationCaptureComponent.generated.h"
class IVoiceCapture;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
FHyperTwistDictationStateChangedSignature,
const FString&,
Status,
bool,
bError
);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(
FHyperTwistDictationTranscriptReadySignature,
const FString&,
Transcript
);
UCLASS(ClassGroup = (HyperTwist), BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API UHyperTwistDictationCaptureComponent : public UActorComponent
{
GENERATED_BODY()
public:
UHyperTwistDictationCaptureComponent();
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
virtual void TickComponent(
float DeltaTime,
ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction
) override;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech")
bool BeginCapture(FString& OutFailureReason);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech")
bool EndCapture(FString& OutTranscript, FString& OutFailureReason);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech")
void CancelCapture();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Speech")
bool IsCapturing() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Speech")
FString GetStatus() const;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|Speech")
FHyperTwistDictationStateChangedSignature OnStateChanged;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|Speech")
FHyperTwistDictationTranscriptReadySignature OnTranscriptReady;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Speech")
int32 CaptureSampleRateHz = 16000;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Speech")
int32 CaptureChannelCount = 1;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Speech")
float MaximumCaptureSeconds = 60.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Speech")
float AudioDuckMultiplier = 0.15f;
private:
bool DrainCaptureBuffer(FString& OutFailureReason);
bool WriteTransientWaveFile(
const FString& FilePath,
const TArray<uint8>& PcmBytes
) const;
void ResetCaptureState(bool bCloseDictationSession);
void SetStatus(const FString& NewStatus, bool bError = false);
FString BuildTransientCapturePath() const;
TSharedPtr<IVoiceCapture> ActiveVoiceCapture;
TArray<uint8> CapturedPcmBytes;
FString ActiveUtteranceId;
FString ActiveCapturePath;
FString CurrentStatus = TEXT("Dictation ready.");
double CaptureStartedAtSeconds = 0.0;
bool bCaptureActive = false;
};

View file

@ -0,0 +1,158 @@
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "HyperTwistFourDimensionalHUDWidget.generated.h"
class UHorizontalBox;
class UButton;
class UTextBlock;
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FHyperTwistFourDimensionalHudActionSignature);
UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API UHyperTwistFourDimensionalHUDWidget : public UUserWidget
{
GENERATED_BODY()
public:
UHyperTwistFourDimensionalHUDWidget(
const FObjectInitializer& ObjectInitializer);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|4D|HUD")
bool PrepareHudSurface();
UFUNCTION(BlueprintPure, Category = "HyperTwist|4D|HUD")
bool IsHudSurfaceReady() const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|4D|HUD")
void ConfigureSurface(
const FString& Title,
const FString& StateSummary,
const FString& SelectionSummary,
const FString& LastAction,
const FString& PersistenceSummary,
bool bCellFirstMode
);
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|4D|HUD")
FHyperTwistFourDimensionalHudActionSignature OnCycleAxisRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|4D|HUD")
FHyperTwistFourDimensionalHudActionSignature OnPreviousLayerRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|4D|HUD")
FHyperTwistFourDimensionalHudActionSignature OnNextLayerRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|4D|HUD")
FHyperTwistFourDimensionalHudActionSignature OnPreviousPlaneRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|4D|HUD")
FHyperTwistFourDimensionalHudActionSignature OnNextPlaneRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|4D|HUD")
FHyperTwistFourDimensionalHudActionSignature OnCounterClockwiseRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|4D|HUD")
FHyperTwistFourDimensionalHudActionSignature OnClockwiseRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|4D|HUD")
FHyperTwistFourDimensionalHudActionSignature OnToggleShellRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|4D|HUD")
FHyperTwistFourDimensionalHudActionSignature OnScrambleRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|4D|HUD")
FHyperTwistFourDimensionalHudActionSignature OnResetRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|4D|HUD")
FHyperTwistFourDimensionalHudActionSignature OnSaveRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|4D|HUD")
FHyperTwistFourDimensionalHudActionSignature OnLoadRequested;
protected:
virtual TSharedRef<SWidget> RebuildWidget() override;
private:
void EnsureWidgetTreeBuilt();
UTextBlock* MakeText(
const FString& Text,
int32 FontSize,
const FLinearColor& Color,
const FName& Name = NAME_None
);
UButton* AddButton(
UHorizontalBox* Parent,
const FString& Label,
const FName& Name,
bool bPrimary = false
);
UFUNCTION()
void HandleCycleAxis();
UFUNCTION()
void HandlePreviousLayer();
UFUNCTION()
void HandleNextLayer();
UFUNCTION()
void HandlePreviousPlane();
UFUNCTION()
void HandleNextPlane();
UFUNCTION()
void HandleCounterClockwise();
UFUNCTION()
void HandleClockwise();
UFUNCTION()
void HandleToggleShell();
UFUNCTION()
void HandleScramble();
UFUNCTION()
void HandleReset();
UFUNCTION()
void HandleSave();
UFUNCTION()
void HandleLoad();
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TitleText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> StateText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> SelectionText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> LastActionText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> PersistenceText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> GuidanceText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UHorizontalBox> SelectionActionRow = nullptr;
UPROPERTY(Transient)
TObjectPtr<UHorizontalBox> TurnActionRow = nullptr;
UPROPERTY(Transient)
TObjectPtr<UHorizontalBox> ScrambleActionRow = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> ShellButton = nullptr;
};

View file

@ -0,0 +1,231 @@
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "HyperTwistHigherDimensionalHUDWidget.generated.h"
class UButton;
class UHorizontalBox;
class UTextBlock;
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FHyperTwistHigherDimensionalHudActionSignature);
/**
* Native, family-aware control surface for the exact Magic120Cell and
* MagicCube5D runtimes. The widget remains deliberately independent of either
* simulation engine; the owning controller translates its semantic actions.
*/
UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API UHyperTwistHigherDimensionalHUDWidget
: public UUserWidget
{
GENERATED_BODY()
public:
UHyperTwistHigherDimensionalHUDWidget(
const FObjectInitializer& ObjectInitializer);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|HigherDimensional|HUD")
bool PrepareHudSurface();
UFUNCTION(BlueprintPure, Category = "HyperTwist|HigherDimensional|HUD")
bool IsHudSurfaceReady() const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|HigherDimensional|HUD")
void ConfigureSurface(
bool bMagic120Cell,
bool bPositive5DFace,
bool bAutoRotating,
const FString& Title,
const FString& StateSummary,
const FString& SelectionSummary,
const FString& ProjectionSummary,
const FString& LastAction,
const FString& PersistenceSummary
);
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|HigherDimensional|HUD")
FHyperTwistHigherDimensionalHudActionSignature OnPreviousPrimaryRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|HigherDimensional|HUD")
FHyperTwistHigherDimensionalHudActionSignature OnNextPrimaryRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|HigherDimensional|HUD")
FHyperTwistHigherDimensionalHudActionSignature OnPreviousSecondaryRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|HigherDimensional|HUD")
FHyperTwistHigherDimensionalHudActionSignature OnNextSecondaryRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|HigherDimensional|HUD")
FHyperTwistHigherDimensionalHudActionSignature OnPreviousTertiaryRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|HigherDimensional|HUD")
FHyperTwistHigherDimensionalHudActionSignature OnNextTertiaryRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|HigherDimensional|HUD")
FHyperTwistHigherDimensionalHudActionSignature OnToggleFaceSideRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|HigherDimensional|HUD")
FHyperTwistHigherDimensionalHudActionSignature OnNegativeTurnRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|HigherDimensional|HUD")
FHyperTwistHigherDimensionalHudActionSignature OnPositiveTurnRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|HigherDimensional|HUD")
FHyperTwistHigherDimensionalHudActionSignature OnPreviousProjectionLayerRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|HigherDimensional|HUD")
FHyperTwistHigherDimensionalHudActionSignature OnNextProjectionLayerRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|HigherDimensional|HUD")
FHyperTwistHigherDimensionalHudActionSignature OnToggleAutoRotateRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|HigherDimensional|HUD")
FHyperTwistHigherDimensionalHudActionSignature OnScrambleRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|HigherDimensional|HUD")
FHyperTwistHigherDimensionalHudActionSignature OnResetRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|HigherDimensional|HUD")
FHyperTwistHigherDimensionalHudActionSignature OnSaveRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|HigherDimensional|HUD")
FHyperTwistHigherDimensionalHudActionSignature OnLoadRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|HigherDimensional|HUD")
FHyperTwistHigherDimensionalHudActionSignature OnCoachRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|HigherDimensional|HUD")
FHyperTwistHigherDimensionalHudActionSignature OnMenuRequested;
protected:
virtual TSharedRef<SWidget> RebuildWidget() override;
private:
void EnsureWidgetTreeBuilt();
UTextBlock* MakeText(
const FString& Text,
int32 FontSize,
const FLinearColor& Color,
const FName& Name = NAME_None
);
UButton* AddButton(
UHorizontalBox* Parent,
const FString& Label,
const FName& Name,
bool bPrimary = false
);
static void SetButtonLabel(UButton* Button, const FString& Label);
UFUNCTION()
void HandlePreviousPrimary();
UFUNCTION()
void HandleNextPrimary();
UFUNCTION()
void HandlePreviousSecondary();
UFUNCTION()
void HandleNextSecondary();
UFUNCTION()
void HandlePreviousTertiary();
UFUNCTION()
void HandleNextTertiary();
UFUNCTION()
void HandleToggleFaceSide();
UFUNCTION()
void HandleNegativeTurn();
UFUNCTION()
void HandlePositiveTurn();
UFUNCTION()
void HandlePreviousProjectionLayer();
UFUNCTION()
void HandleNextProjectionLayer();
UFUNCTION()
void HandleToggleAutoRotate();
UFUNCTION()
void HandleScramble();
UFUNCTION()
void HandleReset();
UFUNCTION()
void HandleSave();
UFUNCTION()
void HandleLoad();
UFUNCTION()
void HandleCoach();
UFUNCTION()
void HandleMenu();
UPROPERTY(Transient)
TObjectPtr<UTextBlock> EyebrowText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TitleText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> StateText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> SelectionText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> ProjectionText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> LastActionText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> PersistenceText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> GuidanceText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UHorizontalBox> TertiaryActionRow = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> PreviousPrimaryButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> NextPrimaryButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> PreviousSecondaryButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> NextSecondaryButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> PreviousTertiaryButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> NextTertiaryButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> FaceSideButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> NegativeTurnButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> PositiveTurnButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> AutoRotateButton = nullptr;
};

View file

@ -0,0 +1,81 @@
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "HyperTwistPauseMenuWidget.generated.h"
class UTextBlock;
class UWidgetSwitcher;
class UHyperTwistSettingsPanelWidget;
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FHyperTwistPauseResumeRequestedSignature);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FHyperTwistPauseMainMenuRequestedSignature);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FHyperTwistPauseQuitRequestedSignature);
UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API UHyperTwistPauseMenuWidget : public UUserWidget
{
GENERATED_BODY()
public:
UHyperTwistPauseMenuWidget(const FObjectInitializer& ObjectInitializer);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Pause")
bool PreparePauseSurface();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Pause")
bool IsPauseSurfaceReady() const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Pause")
void SetPuzzleContext(const FString& Title, const FString& Subtitle);
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|Pause")
FHyperTwistPauseResumeRequestedSignature OnResumeRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|Pause")
FHyperTwistPauseMainMenuRequestedSignature OnMainMenuRequested;
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|Pause")
FHyperTwistPauseQuitRequestedSignature OnQuitRequested;
protected:
virtual TSharedRef<SWidget> RebuildWidget() override;
virtual FReply NativeOnKeyDown(
const FGeometry& InGeometry,
const FKeyEvent& InKeyEvent
) override;
private:
void EnsureWidgetTreeBuilt();
UFUNCTION()
void HandleResumeClicked();
UFUNCTION()
void HandleSettingsClicked();
UFUNCTION()
void HandleSettingsClosed();
UFUNCTION()
void HandleMainMenuClicked();
UFUNCTION()
void HandleQuitClicked();
UPROPERTY(Transient)
TObjectPtr<UWidgetSwitcher> PageSwitcher = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> PuzzleTitleText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> PuzzleSubtitleText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UHyperTwistSettingsPanelWidget> SettingsPanel = nullptr;
FString PuzzleTitle = TEXT("HyperTwist");
FString PuzzleSubtitle = TEXT("Puzzle paused");
};

View file

@ -0,0 +1,171 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "HyperTwistUX/HyperTwistPlayerSettings.h"
#include "HyperTwistPlayerControllerBase.generated.h"
class UHyperTwistPauseMenuWidget;
class UHyperTwistCoachAssistantWidget;
class UHyperTwistDictationCaptureComponent;
class IHttpRequest;
class UAudioComponent;
class USoundWaveProcedural;
UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API AHyperTwistPlayerControllerBase : public APlayerController
{
GENERATED_BODY()
public:
AHyperTwistPlayerControllerBase();
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
virtual void SetupInputComponent() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Navigation")
bool bEnableGlobalPauseMenu = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Navigation")
int32 PauseMenuZOrder = 900;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Navigation")
TSubclassOf<UHyperTwistPauseMenuWidget> PauseMenuWidgetClass;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Navigation")
TObjectPtr<UHyperTwistPauseMenuWidget> ActivePauseMenuWidget = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Coach")
bool bEnableGlobalAssistantPanel = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Coach")
int32 AssistantPanelZOrder = 700;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Coach")
TSubclassOf<UHyperTwistCoachAssistantWidget> AssistantPanelWidgetClass;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Coach")
TObjectPtr<UHyperTwistCoachAssistantWidget> ActiveAssistantPanelWidget = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Speech")
TObjectPtr<UHyperTwistDictationCaptureComponent> GlobalDictationCapture = nullptr;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Navigation")
bool ShowPauseMenu();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Navigation")
void HidePauseMenu();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Navigation")
void ReturnToMainMenu();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Navigation")
void QuitToDesktop();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Coach")
bool ShowAssistantPanel();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Coach")
void HideAssistantPanel();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Coach")
void ToggleAssistantPanel();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Coach")
bool IsAssistantPanelOpen() const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech")
bool BeginGlobalDictation();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech")
bool EndGlobalDictation();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech")
void CancelGlobalDictation();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech")
void ToggleGlobalDictation();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Speech")
bool IsGlobalDictationActive() const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech|Voice")
bool NarrateCoachResponse(const FString& ResponseText);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Speech|Voice")
void StopCoachNarration();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Settings")
const FHyperTwistPlayerPreferences& GetPlayerPreferences() const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Settings")
void ReloadAndApplyPlayerPreferences(bool bApplyGraphicsSettings = false);
protected:
virtual FString GetPauseMenuTitle() const;
virtual FString GetPauseMenuSubtitle() const;
virtual void PrepareForModalUi();
void ApplyGameAndUiInputMode();
UFUNCTION()
void HandlePauseShortcut();
UFUNCTION()
void HandleMainMenuShortcut();
UFUNCTION()
void HandleAssistantShortcut();
UFUNCTION()
void HandleAssistantCloseRequested();
UFUNCTION()
void HandleGlobalDictationPressed();
UFUNCTION()
void HandleGlobalDictationReleased();
UFUNCTION()
void HandleGlobalDictationStateChanged(
const FString& Status,
bool bError
);
UFUNCTION()
void HandleGlobalDictationTranscriptReady(const FString& Transcript);
UFUNCTION()
void HandleCoachNarrationFinished();
UFUNCTION()
void HandlePauseResumeRequested();
UFUNCTION()
void HandlePauseMainMenuRequested();
UFUNCTION()
void HandlePauseQuitRequested();
FHyperTwistPlayerPreferences PlayerPreferences;
bool bToggleDictationCaptureActive = false;
private:
void HandleCoachNarrationResponse(
bool bTransportSucceeded,
int32 StatusCode,
const TArray<uint8>& AudioBytes
);
bool TryPlayCoachNarration(const TArray<uint8>& AudioBytes);
void ReleaseCoachNarrationAudio(bool bStopPlayback);
void ReportCoachNarrationStatus(const FString& Status, bool bError);
TSharedPtr<IHttpRequest, ESPMode::ThreadSafe> ActiveCoachNarrationRequest;
UPROPERTY(Transient)
TObjectPtr<UAudioComponent> ActiveCoachNarrationAudioComponent = nullptr;
UPROPERTY(Transient)
TObjectPtr<USoundWaveProcedural> ActiveCoachNarrationSoundWave = nullptr;
};

View file

@ -0,0 +1,368 @@
#pragma once
#include "CoreMinimal.h"
#include "InputCoreTypes.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "HyperTwistAlgorithm/HyperTwistAlgorithmKeyboard.h"
#include "HyperTwistPlayerSettings.generated.h"
UENUM(BlueprintType)
enum class EHyperTwistGraphicsQuality : uint8
{
Low,
Medium,
High,
Epic
};
UENUM(BlueprintType)
enum class EHyperTwistWindowMode : uint8
{
Fullscreen,
Borderless,
Windowed
};
USTRUCT(BlueprintType)
struct UNREALHYPERTWIST_API FHyperTwistKeyBindingDescriptor
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings")
FName ActionId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings")
FString Label;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings")
FString Category;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings")
FKey Key;
bool IsStructurallyValid() const
{
return !ActionId.IsNone() && !Label.IsEmpty() && Key.IsValid();
}
};
USTRUCT(BlueprintType)
struct UNREALHYPERTWIST_API FHyperTwistDesktopAccountState
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Account")
bool bLinked = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Account")
FString Email;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Account")
FString Plan = TEXT("explorer");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Account")
FString Role = TEXT("user");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Account")
bool bCanDownload = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Account")
FString AccessStatus = TEXT("unlinked");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Account")
FString LinkedAtUtc;
bool IsStructurallyValid() const
{
return !bLinked || (!Email.IsEmpty() && !Plan.IsEmpty() && !Role.IsEmpty());
}
};
USTRUCT(BlueprintType)
struct UNREALHYPERTWIST_API FHyperTwistMicrophoneDescriptor
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
FString DeviceId = TEXT("system-default");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
FString DisplayName = TEXT("System default microphone");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
bool bIsDefault = false;
bool IsStructurallyValid() const
{
return !DeviceId.IsEmpty() && !DisplayName.IsEmpty();
}
};
USTRUCT(BlueprintType)
struct UNREALHYPERTWIST_API FHyperTwistPlayerPreferences
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings")
int32 SchemaVersion = 4;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Controls")
FString KeyboardProfileId = TEXT("classic-wca-keyboard/v1");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Controls")
TMap<FName, FKey> KeyBindings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Controls")
float PointerSensitivity = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Controls")
float OrbitSensitivity = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Controls")
float ZoomSensitivity = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Controls")
bool bInvertOrbitY = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Controls")
bool bTouchInputEnabled = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Audio")
float MasterVolume = 0.85f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Audio")
float MusicVolume = 0.55f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Audio")
float EffectsVolume = 0.80f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Audio")
float VoiceVolume = 0.85f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Audio")
bool bMuted = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Graphics")
EHyperTwistGraphicsQuality GraphicsQuality = EHyperTwistGraphicsQuality::High;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Graphics")
EHyperTwistWindowMode WindowMode = EHyperTwistWindowMode::Borderless;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Graphics")
float ResolutionScalePercent = 100.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Graphics")
bool bVSyncEnabled = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Accessibility")
float UiScale = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Accessibility")
bool bReducedMotion = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Accessibility")
bool bHighContrast = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Accessibility")
bool bSubtitlesEnabled = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
bool bSpeechInputEnabled = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
bool bCoachNarrationEnabled = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
bool bAllowCloudProviders = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
FString SpeechProviderId = TEXT("local-whisper-cpp");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
FString SpeechEndpoint = TEXT("http://127.0.0.1:8766");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
FString SpeechActivationMode = TEXT("hold");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
FString SpeechDictationDestination = TEXT("coach-draft");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
FString SpeechModel = TEXT("local-whisper/base-q5_1");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
FString SpeechLanguage = TEXT("auto");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
FString SpeechMicrophoneId = TEXT("system-default");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
FString SpeechRecognitionContext = TEXT("HyperTwist, cubing, hypercube, 120-cell");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
bool bSpeechCleanupEnabled = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
bool bSpeechSoundFeedbackEnabled = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
FString VoiceProviderId = TEXT("local-piper");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
FString VoiceEndpoint = TEXT("http://127.0.0.1:8766");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
FString VoiceModel = TEXT("piper-medium");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
FString VoiceId = TEXT("en_US-lessac-medium");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Speech")
float VoiceSpeed = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|AI")
bool bCoachAiEnabled = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|AI")
bool bAssistantPanelEnabled = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|AI")
bool bAssistantPanelOpenByDefault = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|AI")
FString CoachProviderId = TEXT("local-disabled");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|AI")
FString CoachEndpoint;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|AI")
FString CoachModel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|AI")
FString CoachSystemInstructions =
TEXT("You are the HyperTwist puzzle coach. Give concise, accurate guidance without inventing puzzle state.");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|AI")
float CoachTemperature = 0.2f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|AI")
int32 CoachMaxResponseTokens = 600;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|AI")
float CoachRequestTimeoutSeconds = 30.0f;
bool IsStructurallyValid() const;
};
UCLASS()
class UNREALHYPERTWIST_API UHyperTwistPlayerSettingsLibrary
: public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Settings")
static FHyperTwistPlayerPreferences GetDefaultPreferences();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Settings")
static FHyperTwistPlayerPreferences LoadPreferences();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Settings")
static bool SavePreferences(const FHyperTwistPlayerPreferences& Preferences);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Settings")
static void ApplyRuntimePreferences(
const FHyperTwistPlayerPreferences& Preferences,
bool bApplyGraphicsSettings
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Settings|Controls")
static TArray<FHyperTwistKeyBindingDescriptor> BuildKeyBindingDescriptors(
const FHyperTwistPlayerPreferences& Preferences
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Settings|Controls")
static TArray<FString> GetSupportedKeyboardProfileIds();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Settings|Controls")
static FString GetKeyboardProfileDisplayName(const FString& ProfileId);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Settings|Controls")
static bool ApplyKeyboardProfile(
UPARAM(ref) FHyperTwistPlayerPreferences& Preferences,
const FString& ProfileId
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Settings|Controls")
static bool TrySetKeyBinding(
UPARAM(ref) FHyperTwistPlayerPreferences& Preferences,
FName ActionId,
FKey NewKey,
FString& OutFailureReason
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Settings|Controls")
static FKey ResolveKeyBinding(
const FHyperTwistPlayerPreferences& Preferences,
FName ActionId
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Settings|Controls")
static FHyperTwistAlgorithmKeyboardProfile BuildClassicKeyboardProfile(
const FHyperTwistPlayerPreferences& Preferences
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Settings|Controls")
static FName BuildClassicMoveBindingId(
const FHyperTwistAlgorithmBlockMove& Move
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Settings|Speech")
static TArray<FHyperTwistMicrophoneDescriptor> GetAvailableMicrophones();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Settings|Speech")
static bool TestMicrophoneDevice(
const FString& DeviceId,
FString& OutFailureReason
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Account")
static FHyperTwistDesktopAccountState LoadDesktopAccountState();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Account")
static bool SaveDesktopAccountState(const FHyperTwistDesktopAccountState& AccountState);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Account")
static void ClearDesktopAccountState();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Settings|Credentials")
static bool StoreProviderCredential(
const FString& CredentialSlot,
const FString& Secret,
FString& OutFailureReason
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Settings|Credentials")
static bool LoadProviderCredential(
const FString& CredentialSlot,
FString& OutSecret
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Settings|Credentials")
static bool HasProviderCredential(const FString& CredentialSlot);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Settings|Credentials")
static void DeleteProviderCredential(const FString& CredentialSlot);
static bool IsProviderEndpointAllowed(
const FString& Endpoint,
bool bAllowCloudProviders,
FString& OutFailureReason
);
static const TCHAR* GetMainMenuMapAssetPath();
static const TCHAR* GetMainMenuGameModeClassPath();
static const TCHAR* GetAccountWebsiteUrl();
static const TCHAR* GetDesktopLinkVerifyUrl();
};

View file

@ -0,0 +1,659 @@
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "HyperTwistUX/HyperTwistPlayerSettings.h"
#include "HyperTwistSettingsPanelWidget.generated.h"
class UButton;
class UCheckBox;
class UComboBoxString;
class UEditableTextBox;
class UHorizontalBox;
class USlider;
class UTextBlock;
class UUniformGridPanel;
class UVerticalBox;
class UWidgetSwitcher;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(
FHyperTwistRebindRequestedSignature,
FName,
ActionId
);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FHyperTwistSettingsCloseRequestedSignature);
UCLASS()
class UNREALHYPERTWIST_API UHyperTwistKeyBindingRowWidget : public UUserWidget
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Settings|Controls")
void ConfigureBinding(
FName InActionId,
const FString& InLabel,
const FString& InCategory,
FKey InKey
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Settings|Controls")
void UpdateBoundKey(FKey InKey, bool bAwaitingInput = false);
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|Settings|Controls")
FHyperTwistRebindRequestedSignature OnRebindRequested;
protected:
virtual TSharedRef<SWidget> RebuildWidget() override;
private:
void EnsureWidgetTreeBuilt();
UFUNCTION()
void HandleRebindClicked();
FName ActionId;
FString DisplayLabel;
FString CategoryLabel;
FKey BoundKey;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> BindingLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> KeyLabel = nullptr;
};
UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API UHyperTwistSettingsPanelWidget : public UUserWidget
{
GENERATED_BODY()
public:
UHyperTwistSettingsPanelWidget(const FObjectInitializer& ObjectInitializer);
virtual void NativeConstruct() override;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Settings")
bool PrepareSettingsSurface();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Settings")
bool IsSettingsSurfaceReady() const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Settings")
void ReloadPreferences();
UPROPERTY(BlueprintAssignable, Category = "HyperTwist|Settings")
FHyperTwistSettingsCloseRequestedSignature OnCloseRequested;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Style")
bool bShowCloseButton = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Style")
FLinearColor PanelColor = FLinearColor(0.020f, 0.031f, 0.050f, 0.985f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Style")
FLinearColor CardColor = FLinearColor(0.038f, 0.060f, 0.087f, 1.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Style")
FLinearColor AccentColor = FLinearColor(0.10f, 0.92f, 0.80f, 1.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Style")
FLinearColor WarmAccentColor = FLinearColor(1.0f, 0.63f, 0.24f, 1.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Style")
FLinearColor PrimaryTextColor = FLinearColor(0.93f, 0.97f, 1.0f, 1.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Settings|Style")
FLinearColor MutedTextColor = FLinearColor(0.62f, 0.72f, 0.80f, 1.0f);
protected:
virtual TSharedRef<SWidget> RebuildWidget() override;
virtual FReply NativeOnKeyDown(
const FGeometry& InGeometry,
const FKeyEvent& InKeyEvent
) override;
virtual FReply NativeOnPreviewMouseButtonDown(
const FGeometry& InGeometry,
const FPointerEvent& InMouseEvent
) override;
private:
void EnsureWidgetTreeBuilt();
void BuildControlsPage(UVerticalBox* Parent);
void BuildAudioPage(UVerticalBox* Parent);
void BuildGraphicsPage(UVerticalBox* Parent);
void BuildAccessibilityPage(UVerticalBox* Parent);
void BuildSpeechAndCoachPage(UVerticalBox* Parent);
void RefreshWidgetsFromPreferences();
void RefreshBindingRows();
void RefreshMicrophoneOptions();
void RefreshProviderReadiness();
bool TryCompletePendingRebind(FKey PressedKey);
void SavePreferencesAndApply(const FString& StatusMessage, bool bApplyGraphics = false);
void SetStatus(const FString& Message, bool bError = false);
UTextBlock* AddText(
UVerticalBox* Parent,
const FString& Text,
int32 FontSize,
const FLinearColor& Color,
const FName& WidgetName = NAME_None
);
void AddSectionIntro(
UVerticalBox* Parent,
const FString& Eyebrow,
const FString& Title,
const FString& Description
);
UButton* AddTabButton(
UUniformGridPanel* Parent,
const FString& Label,
int32 Column,
const FName& WidgetName
);
UButton* AddActionButton(
UHorizontalBox* Parent,
const FString& Label,
const FName& WidgetName,
bool bPrimary = false
);
USlider* AddSliderRow(
UVerticalBox* Parent,
const FString& Label,
const FString& SupportingText,
const FName& WidgetName,
TObjectPtr<UTextBlock>& OutValueLabel
);
UCheckBox* AddCheckRow(
UVerticalBox* Parent,
const FString& Label,
const FString& SupportingText,
const FName& WidgetName
);
UEditableTextBox* AddTextSetting(
UVerticalBox* Parent,
const FString& Label,
const FString& SupportingText,
const FName& WidgetName,
bool bPassword = false
);
UComboBoxString* AddComboSetting(
UVerticalBox* Parent,
const FString& Label,
const FString& SupportingText,
const FName& WidgetName,
const TArray<FString>& Options
);
UVerticalBox* AddScrollablePage(const FName& WidgetName);
UFUNCTION()
void ShowControlsPage();
UFUNCTION()
void ShowAudioPage();
UFUNCTION()
void ShowGraphicsPage();
UFUNCTION()
void ShowAccessibilityPage();
UFUNCTION()
void ShowSpeechAndCoachPage();
UFUNCTION()
void HandleCloseClicked();
UFUNCTION()
void HandleResetDefaultsClicked();
UFUNCTION()
void HandleBindingRequested(FName ActionId);
UFUNCTION()
void HandleKeyboardProfileChanged(
FString SelectedItem,
ESelectInfo::Type SelectionType
);
UFUNCTION()
void HandlePointerSensitivityChanged(float Value);
UFUNCTION()
void HandleOrbitSensitivityChanged(float Value);
UFUNCTION()
void HandleZoomSensitivityChanged(float Value);
UFUNCTION()
void HandleInvertOrbitChanged(bool bChecked);
UFUNCTION()
void HandleTouchInputChanged(bool bChecked);
UFUNCTION()
void HandleMasterVolumeChanged(float Value);
UFUNCTION()
void HandleMusicVolumeChanged(float Value);
UFUNCTION()
void HandleEffectsVolumeChanged(float Value);
UFUNCTION()
void HandleVoiceVolumeChanged(float Value);
UFUNCTION()
void HandleMutedChanged(bool bChecked);
UFUNCTION()
void HandleGraphicsQualityChanged(
FString SelectedItem,
ESelectInfo::Type SelectionType
);
UFUNCTION()
void HandleWindowModeChanged(
FString SelectedItem,
ESelectInfo::Type SelectionType
);
UFUNCTION()
void HandleResolutionScaleChanged(float Value);
UFUNCTION()
void HandleVSyncChanged(bool bChecked);
UFUNCTION()
void HandleUiScaleChanged(float Value);
UFUNCTION()
void HandleReducedMotionChanged(bool bChecked);
UFUNCTION()
void HandleHighContrastChanged(bool bChecked);
UFUNCTION()
void HandleSubtitlesChanged(bool bChecked);
UFUNCTION()
void HandleSpeechInputChanged(bool bChecked);
UFUNCTION()
void HandleCoachNarrationChanged(bool bChecked);
UFUNCTION()
void HandleCloudProvidersChanged(bool bChecked);
UFUNCTION()
void HandleAssistantPanelEnabledChanged(bool bChecked);
UFUNCTION()
void HandleAssistantPanelOpenByDefaultChanged(bool bChecked);
UFUNCTION()
void HandleCoachAiEnabledChanged(bool bChecked);
UFUNCTION()
void HandleSpeechProviderChanged(
FString SelectedItem,
ESelectInfo::Type SelectionType
);
UFUNCTION()
void HandleSpeechActivationModeChanged(
FString SelectedItem,
ESelectInfo::Type SelectionType
);
UFUNCTION()
void HandleSpeechDictationDestinationChanged(
FString SelectedItem,
ESelectInfo::Type SelectionType
);
UFUNCTION()
void HandleSpeechLanguageChanged(
FString SelectedItem,
ESelectInfo::Type SelectionType
);
UFUNCTION()
void HandleSpeechEndpointCommitted(
const FText& Text,
ETextCommit::Type CommitMethod
);
UFUNCTION()
void HandleSpeechModelCommitted(
const FText& Text,
ETextCommit::Type CommitMethod
);
UFUNCTION()
void HandleSpeechMicrophoneChanged(
FString SelectedItem,
ESelectInfo::Type SelectionType
);
UFUNCTION()
void HandleSpeechContextCommitted(
const FText& Text,
ETextCommit::Type CommitMethod
);
UFUNCTION()
void HandleSpeechCleanupChanged(bool bChecked);
UFUNCTION()
void HandleSpeechSoundFeedbackChanged(bool bChecked);
UFUNCTION()
void HandleVoiceProviderChanged(
FString SelectedItem,
ESelectInfo::Type SelectionType
);
UFUNCTION()
void HandleVoiceEndpointCommitted(
const FText& Text,
ETextCommit::Type CommitMethod
);
UFUNCTION()
void HandleVoiceModelCommitted(
const FText& Text,
ETextCommit::Type CommitMethod
);
UFUNCTION()
void HandleVoiceIdCommitted(
const FText& Text,
ETextCommit::Type CommitMethod
);
UFUNCTION()
void HandleVoiceSpeedChanged(float Value);
UFUNCTION()
void HandleCoachProviderChanged(
FString SelectedItem,
ESelectInfo::Type SelectionType
);
UFUNCTION()
void HandleCoachEndpointCommitted(
const FText& Text,
ETextCommit::Type CommitMethod
);
UFUNCTION()
void HandleCoachModelCommitted(
const FText& Text,
ETextCommit::Type CommitMethod
);
UFUNCTION()
void HandleCoachInstructionsCommitted(
const FText& Text,
ETextCommit::Type CommitMethod
);
UFUNCTION()
void HandleCoachTemperatureChanged(float Value);
UFUNCTION()
void HandleCoachMaxResponseTokensChanged(float Value);
UFUNCTION()
void HandleCoachRequestTimeoutChanged(float Value);
UFUNCTION()
void HandleSaveSpeechCredential();
UFUNCTION()
void HandleRemoveSpeechCredential();
UFUNCTION()
void HandleSaveVoiceCredential();
UFUNCTION()
void HandleRemoveVoiceCredential();
UFUNCTION()
void HandleSaveCoachCredential();
UFUNCTION()
void HandleRemoveCoachCredential();
UFUNCTION()
void HandleValidateSpeechConfiguration();
UFUNCTION()
void HandleRefreshMicrophones();
UFUNCTION()
void HandleTestMicrophone();
UFUNCTION()
void HandleValidateVoiceConfiguration();
UFUNCTION()
void HandleValidateCoachConfiguration();
FHyperTwistPlayerPreferences Preferences;
FName PendingRebindActionId;
bool bRefreshingWidgets = false;
UPROPERTY(Transient)
TObjectPtr<UWidgetSwitcher> SettingsPageSwitcher = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> StatusText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UComboBoxString> KeyboardProfileCombo = nullptr;
UPROPERTY(Transient)
TObjectPtr<USlider> PointerSensitivitySlider = nullptr;
UPROPERTY(Transient)
TObjectPtr<USlider> OrbitSensitivitySlider = nullptr;
UPROPERTY(Transient)
TObjectPtr<USlider> ZoomSensitivitySlider = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> PointerSensitivityValue = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> OrbitSensitivityValue = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> ZoomSensitivityValue = nullptr;
UPROPERTY(Transient)
TObjectPtr<UCheckBox> InvertOrbitCheck = nullptr;
UPROPERTY(Transient)
TObjectPtr<UCheckBox> TouchInputCheck = nullptr;
UPROPERTY(Transient)
TObjectPtr<USlider> MasterVolumeSlider = nullptr;
UPROPERTY(Transient)
TObjectPtr<USlider> MusicVolumeSlider = nullptr;
UPROPERTY(Transient)
TObjectPtr<USlider> EffectsVolumeSlider = nullptr;
UPROPERTY(Transient)
TObjectPtr<USlider> VoiceVolumeSlider = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> MasterVolumeValue = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> MusicVolumeValue = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> EffectsVolumeValue = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> VoiceVolumeValue = nullptr;
UPROPERTY(Transient)
TObjectPtr<UCheckBox> MutedCheck = nullptr;
UPROPERTY(Transient)
TObjectPtr<UComboBoxString> GraphicsQualityCombo = nullptr;
UPROPERTY(Transient)
TObjectPtr<UComboBoxString> WindowModeCombo = nullptr;
UPROPERTY(Transient)
TObjectPtr<USlider> ResolutionScaleSlider = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> ResolutionScaleValue = nullptr;
UPROPERTY(Transient)
TObjectPtr<UCheckBox> VSyncCheck = nullptr;
UPROPERTY(Transient)
TObjectPtr<USlider> UiScaleSlider = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> UiScaleValue = nullptr;
UPROPERTY(Transient)
TObjectPtr<UCheckBox> ReducedMotionCheck = nullptr;
UPROPERTY(Transient)
TObjectPtr<UCheckBox> HighContrastCheck = nullptr;
UPROPERTY(Transient)
TObjectPtr<UCheckBox> SubtitlesCheck = nullptr;
UPROPERTY(Transient)
TObjectPtr<UCheckBox> SpeechInputCheck = nullptr;
UPROPERTY(Transient)
TObjectPtr<UCheckBox> CoachNarrationCheck = nullptr;
UPROPERTY(Transient)
TObjectPtr<UCheckBox> CloudProvidersCheck = nullptr;
UPROPERTY(Transient)
TObjectPtr<UCheckBox> AssistantPanelEnabledCheck = nullptr;
UPROPERTY(Transient)
TObjectPtr<UCheckBox> AssistantPanelOpenByDefaultCheck = nullptr;
UPROPERTY(Transient)
TObjectPtr<UCheckBox> CoachAiEnabledCheck = nullptr;
UPROPERTY(Transient)
TObjectPtr<UComboBoxString> SpeechProviderCombo = nullptr;
UPROPERTY(Transient)
TObjectPtr<UComboBoxString> SpeechActivationModeCombo = nullptr;
UPROPERTY(Transient)
TObjectPtr<UComboBoxString> SpeechDictationDestinationCombo = nullptr;
UPROPERTY(Transient)
TObjectPtr<UComboBoxString> SpeechLanguageCombo = nullptr;
UPROPERTY(Transient)
TObjectPtr<UEditableTextBox> SpeechEndpointInput = nullptr;
UPROPERTY(Transient)
TObjectPtr<UEditableTextBox> SpeechModelInput = nullptr;
UPROPERTY(Transient)
TObjectPtr<UComboBoxString> SpeechMicrophoneCombo = nullptr;
UPROPERTY(Transient)
TObjectPtr<UEditableTextBox> SpeechContextInput = nullptr;
UPROPERTY(Transient)
TObjectPtr<UCheckBox> SpeechCleanupCheck = nullptr;
UPROPERTY(Transient)
TObjectPtr<UCheckBox> SpeechSoundFeedbackCheck = nullptr;
UPROPERTY(Transient)
TObjectPtr<UComboBoxString> VoiceProviderCombo = nullptr;
UPROPERTY(Transient)
TObjectPtr<UEditableTextBox> VoiceEndpointInput = nullptr;
UPROPERTY(Transient)
TObjectPtr<UEditableTextBox> VoiceModelInput = nullptr;
UPROPERTY(Transient)
TObjectPtr<UEditableTextBox> VoiceIdInput = nullptr;
UPROPERTY(Transient)
TObjectPtr<USlider> VoiceSpeedSlider = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> VoiceSpeedValue = nullptr;
UPROPERTY(Transient)
TObjectPtr<UComboBoxString> CoachProviderCombo = nullptr;
UPROPERTY(Transient)
TObjectPtr<UEditableTextBox> CoachEndpointInput = nullptr;
UPROPERTY(Transient)
TObjectPtr<UEditableTextBox> CoachModelInput = nullptr;
UPROPERTY(Transient)
TObjectPtr<UEditableTextBox> CoachInstructionsInput = nullptr;
UPROPERTY(Transient)
TObjectPtr<USlider> CoachTemperatureSlider = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> CoachTemperatureValue = nullptr;
UPROPERTY(Transient)
TObjectPtr<USlider> CoachMaxResponseTokensSlider = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> CoachMaxResponseTokensValue = nullptr;
UPROPERTY(Transient)
TObjectPtr<USlider> CoachRequestTimeoutSlider = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> CoachRequestTimeoutValue = nullptr;
UPROPERTY(Transient)
TObjectPtr<UEditableTextBox> SpeechCredentialInput = nullptr;
UPROPERTY(Transient)
TObjectPtr<UEditableTextBox> VoiceCredentialInput = nullptr;
UPROPERTY(Transient)
TObjectPtr<UEditableTextBox> CoachCredentialInput = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> SpeechReadinessText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> VoiceReadinessText = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> CoachReadinessText = nullptr;
TArray<FHyperTwistMicrophoneDescriptor> AvailableMicrophones;
UPROPERTY(Transient)
TArray<TObjectPtr<UHyperTwistKeyBindingRowWidget>> BindingRows;
};

View file

@ -99,7 +99,12 @@ bool FHyperTwistClassicCubePlayerControllerDefaultsTest::RunTest(const FString&
PlayerController->bBindFreshAttemptShortcut PlayerController->bBindFreshAttemptShortcut
); );
TestTrue(TEXT("The classic cube player controller must expose a solver hint shortcut."), PlayerController->bBindHintShortcut); TestTrue(TEXT("The classic cube player controller must expose a solver hint shortcut."), PlayerController->bBindHintShortcut);
TestTrue(TEXT("The classic cube player controller must expose a hold-to-talk shortcut."), PlayerController->bBindVoiceHoldShortcut); TestNotNull(
TEXT("Classic Cube must inherit the same global dictation route as every puzzle."),
PlayerController->GlobalDictationCapture.Get());
TestFalse(
TEXT("The legacy Classic-only voice shortcut must remain opt-in to avoid a duplicate global binding."),
PlayerController->bBindVoiceHoldShortcut);
return true; return true;
} }

View file

@ -5,17 +5,60 @@
#include "Blueprint/WidgetTree.h" #include "Blueprint/WidgetTree.h"
#include "Components/Button.h" #include "Components/Button.h"
#include "Containers/Set.h"
#include "HyperTwistSimulation/HyperTwistClassicCubeHUDWidget.h" #include "HyperTwistSimulation/HyperTwistClassicCubeHUDWidget.h"
#include "HyperTwistTraining/HyperTwistCoachDashboardWidget.h" #include "HyperTwistTraining/HyperTwistCoachDashboardWidget.h"
#include "HyperTwistTraining/HyperTwistFirstRunLaunchGameMode.h" #include "HyperTwistTraining/HyperTwistFirstRunLaunchGameMode.h"
#include "HyperTwistTraining/HyperTwistFirstRunLaunchLibrary.h" #include "HyperTwistTraining/HyperTwistFirstRunLaunchLibrary.h"
#include "HyperTwistTraining/HyperTwistFirstRunLaunchPlayerController.h" #include "HyperTwistTraining/HyperTwistFirstRunLaunchPlayerController.h"
#include "HyperTwistTraining/HyperTwistFirstRunLaunchWidget.h" #include "HyperTwistTraining/HyperTwistFirstRunLaunchWidget.h"
#include "HyperTwistTraining/HyperTwistTrainingCatalogLibrary.h"
#if WITH_AUTOMATION_TESTS #if WITH_AUTOMATION_TESTS
namespace HyperTwistFirstRunLaunchSurfaceTestInternal namespace HyperTwistFirstRunLaunchSurfaceTestInternal
{ {
struct FExpectedFourDimensionalRoute
{
const TCHAR* RouteId;
const TCHAR* PuzzleId;
const TCHAR* MapAssetPath;
const TCHAR* GameModeClassPath;
};
const FExpectedFourDimensionalRoute FourDimensionalRoutes[] = {
{
TEXT("magic-cube-4d-2x2x2x2"),
TEXT("hypercube/2x2x2x2"),
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_2x2x2x2Training"),
TEXT("/Script/UnrealHyperTwist.HyperTwistMelindaProjectionGameMode")
},
{
TEXT("magic-cube-4d-3x3x3x3"),
TEXT("hypercube/3x3x3x3"),
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_3x3x3x3Training"),
TEXT("/Script/UnrealHyperTwist.HyperTwistVirtual3333ProjectionGameMode")
},
{
TEXT("magic-cube-4d-4x4x4x4"),
TEXT("hypercube/4x4x4x4"),
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_4x4x4x4Training"),
TEXT("/Script/UnrealHyperTwist.HyperTwistVirtual4444ProjectionGameMode")
},
{
TEXT("magic-cube-4d-5x5x5x5"),
TEXT("hypercube/5x5x5x5"),
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_5x5x5x5Training"),
TEXT("/Script/UnrealHyperTwist.HyperTwistVirtual5555ProjectionGameMode")
},
{
TEXT("magic-cube-4d-6x6x6x6"),
TEXT("hypercube/6x6x6x6"),
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_6x6x6x6Training"),
TEXT("/Script/UnrealHyperTwist.HyperTwistVirtual6666ProjectionGameMode")
}
};
bool ContainsGuidanceSubstring(const TArray<FString>& Lines, const FString& ExpectedSubstring) bool ContainsGuidanceSubstring(const TArray<FString>& Lines, const FString& ExpectedSubstring)
{ {
for (const FString& Line : Lines) for (const FString& Line : Lines)
@ -38,9 +81,10 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST(
bool FHyperTwistFirstRunLaunchRouteContractTest::RunTest(const FString& Parameters) bool FHyperTwistFirstRunLaunchRouteContractTest::RunTest(const FString& Parameters)
{ {
using namespace HyperTwistFirstRunLaunchSurfaceTestInternal;
const TArray<FHyperTwistFirstRunLaunchRoute> Routes = const TArray<FHyperTwistFirstRunLaunchRoute> Routes =
UHyperTwistFirstRunLaunchLibrary::BuildFirstRunLaunchRoutes(); UHyperTwistFirstRunLaunchLibrary::BuildFirstRunLaunchRoutes();
TestEqual(TEXT("The first-run surface must expose all launch choices."), Routes.Num(), 6); TestEqual(TEXT("The first-run surface must expose all launch choices."), Routes.Num(), 11);
for (const FHyperTwistFirstRunLaunchRoute& Route : Routes) for (const FHyperTwistFirstRunLaunchRoute& Route : Routes)
{ {
@ -52,14 +96,15 @@ bool FHyperTwistFirstRunLaunchRouteContractTest::RunTest(const FString& Paramete
FHyperTwistFirstRunLaunchRoute Route; FHyperTwistFirstRunLaunchRoute Route;
TestTrue( TestTrue(
TEXT("The default route must open the coach dashboard/settings surface."), TEXT("The default route must enter the complete keyboard-and-mouse Classic runtime."),
UHyperTwistFirstRunLaunchLibrary::TryFindFirstRunLaunchRoute( UHyperTwistFirstRunLaunchLibrary::TryFindFirstRunLaunchRoute(
UHyperTwistFirstRunLaunchLibrary::GetDefaultRouteId(), UHyperTwistFirstRunLaunchLibrary::GetDefaultRouteId(),
Route Route
) )
&& Route.bDefaultSafeChoice && Route.bDefaultSafeChoice
&& Route.bOpensDashboard && !Route.bOpensDashboard
&& !Route.bLaunchesDedicatedMap && Route.bLaunchesDedicatedMap
&& Route.RouteId == TEXT("classic-cube-training")
); );
TestEqual( TestEqual(
TEXT("The packaged startup fallback route must target classic free play."), TEXT("The packaged startup fallback route must target classic free play."),
@ -81,6 +126,31 @@ bool FHyperTwistFirstRunLaunchRouteContractTest::RunTest(const FString& Paramete
&& Route.GameModeClassPath == TEXT("/Script/UnrealHyperTwist.HyperTwistClassicCubeFollowAlongGameMode") && Route.GameModeClassPath == TEXT("/Script/UnrealHyperTwist.HyperTwistClassicCubeFollowAlongGameMode")
); );
for (const FExpectedFourDimensionalRoute& Expected : FourDimensionalRoutes)
{
const FString RouteLabel = Expected.RouteId;
TestTrue(
*FString::Printf(
TEXT("The %s route must target its dedicated map and exact runtime."),
*RouteLabel),
UHyperTwistFirstRunLaunchLibrary::TryFindFirstRunLaunchRoute(
Expected.RouteId,
Route)
&& Route.MapAssetPath == Expected.MapAssetPath
&& Route.GameModeClassPath == Expected.GameModeClassPath
&& Route.bLaunchesDedicatedMap
&& Route.RuntimeProofStatus == TEXT("ready-first-party-runtime"));
TestEqual(
*FString::Printf(
TEXT("The %s launch URL must bind its dedicated game mode."),
*RouteLabel),
Route.LaunchUrl,
FString::Printf(
TEXT("%s?game=%s"),
Expected.MapAssetPath,
Expected.GameModeClassPath));
}
TestTrue( TestTrue(
TEXT("The Magic120Cell route must target its dedicated family map and runtime game mode."), TEXT("The Magic120Cell route must target its dedicated family map and runtime game mode."),
UHyperTwistFirstRunLaunchLibrary::TryFindFirstRunLaunchRoute(TEXT("magic-120-cell-training"), Route) UHyperTwistFirstRunLaunchLibrary::TryFindFirstRunLaunchRoute(TEXT("magic-120-cell-training"), Route)
@ -111,6 +181,246 @@ bool FHyperTwistFirstRunLaunchRouteContractTest::RunTest(const FString& Paramete
return true; return true;
} }
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistFirstRunPuzzleCatalogContractTest,
"HyperTwist.FirstRun.PuzzleCatalogContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistFirstRunPuzzleCatalogContractTest::RunTest(const FString& Parameters)
{
static_cast<void>(Parameters);
using namespace HyperTwistFirstRunLaunchSurfaceTestInternal;
const TArray<FHyperTwistPuzzleCatalogEntry> Catalog =
UHyperTwistFirstRunLaunchLibrary::BuildPlayerPuzzleCatalog();
TestEqual(
TEXT("The player catalog must expose every playable family plus MagicTile preview."),
Catalog.Num(),
9);
for (const FExpectedFourDimensionalRoute& Expected : FourDimensionalRoutes)
{
FHyperTwistPuzzleCatalogEntry Entry;
TestTrue(
*FString::Printf(
TEXT("The player catalog must expose %s."),
Expected.PuzzleId),
UHyperTwistFirstRunLaunchLibrary::TryFindPuzzleCatalogEntry(
Expected.PuzzleId,
Entry));
TestEqual(
*FString::Printf(
TEXT("%s must launch through its registered route."),
Expected.PuzzleId),
Entry.LaunchRouteId,
FString(Expected.RouteId));
TestTrue(
*FString::Printf(
TEXT("%s must be player-facing and playable."),
Expected.PuzzleId),
Entry.Availability == EHyperTwistPuzzleAvailability::Playable);
}
FHyperTwistPuzzleCatalogEntry MagicTileEntry;
TestTrue(
TEXT("MagicTile must remain explicitly represented without overstating native readiness."),
UHyperTwistFirstRunLaunchLibrary::TryFindPuzzleCatalogEntry(
TEXT("tiling/magictile"),
MagicTileEntry)
&& MagicTileEntry.Availability
== EHyperTwistPuzzleAvailability::BrowserPreview
&& MagicTileEntry.LaunchRouteId.IsEmpty());
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistIntegratedCapabilityCatalogContractTest,
"HyperTwist.FirstRun.IntegratedCapabilityCatalogContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistIntegratedCapabilityCatalogContractTest::RunTest(
const FString& Parameters
)
{
static_cast<void>(Parameters);
const TArray<FHyperTwistIntegratedCapability> Catalog =
UHyperTwistFirstRunLaunchLibrary::BuildIntegratedCapabilityCatalog();
TestEqual(
TEXT("The integration catalog must retain the canonical original 29 live lanes."),
Catalog.Num(),
29);
TSet<FString> CapabilityIds;
TSet<FString> CanonicalRepositories;
int32 PermissiveCount = 0;
int32 CleanRoomCount = 0;
int32 BoundarySensitiveCount = 0;
int32 NativePlayableCount = 0;
int32 NativePlayerServiceCount = 0;
int32 BrowserSupportCount = 0;
int32 OperatorSupportCount = 0;
for (const FHyperTwistIntegratedCapability& Capability : Catalog)
{
TestTrue(
*FString::Printf(
TEXT("Capability '%s' must be structurally valid."),
*Capability.CapabilityId),
Capability.IsStructurallyValid());
TestFalse(
*FString::Printf(
TEXT("Capability id '%s' must be unique."),
*Capability.CapabilityId),
CapabilityIds.Contains(Capability.CapabilityId));
TestFalse(
*FString::Printf(
TEXT("Repository '%s' must appear exactly once."),
*Capability.CanonicalRepository),
CanonicalRepositories.Contains(Capability.CanonicalRepository));
CapabilityIds.Add(Capability.CapabilityId);
CanonicalRepositories.Add(Capability.CanonicalRepository);
switch (Capability.LicenseLane)
{
case EHyperTwistIntegrationLicenseLane::Permissive:
++PermissiveCount;
break;
case EHyperTwistIntegrationLicenseLane::RestrictiveCleanRoom:
++CleanRoomCount;
TestTrue(
*FString::Printf(
TEXT("Restrictive row '%s' must remain explicitly clean-roomed."),
*Capability.CanonicalRepository),
Capability.ImplementationPosture.Contains(TEXT("clean-room")));
break;
case EHyperTwistIntegrationLicenseLane::BoundarySensitive:
++BoundarySensitiveCount;
break;
default:
AddError(TEXT("The capability catalog contains an unknown license lane."));
break;
}
switch (Capability.Surface)
{
case EHyperTwistIntegrationSurface::NativePlayable:
++NativePlayableCount;
break;
case EHyperTwistIntegrationSurface::NativePlayerService:
++NativePlayerServiceCount;
break;
case EHyperTwistIntegrationSurface::BrowserSupport:
++BrowserSupportCount;
break;
case EHyperTwistIntegrationSurface::OperatorSupport:
++OperatorSupportCount;
break;
default:
AddError(TEXT("The capability catalog contains an unknown product surface."));
break;
}
if (!Capability.PrimaryExperienceRoute.StartsWith(TEXT("menu:"))
&& !Capability.PrimaryExperienceRoute.StartsWith(TEXT("web:")))
{
FHyperTwistFirstRunLaunchRoute Route;
TestTrue(
*FString::Printf(
TEXT("Product route '%s' for '%s' must resolve."),
*Capability.PrimaryExperienceRoute,
*Capability.CanonicalRepository),
UHyperTwistFirstRunLaunchLibrary::TryFindFirstRunLaunchRoute(
Capability.PrimaryExperienceRoute,
Route));
}
}
TestEqual(TEXT("The original 29 must retain 18 permissive lanes."), PermissiveCount, 18);
TestEqual(TEXT("The original 29 must retain five restrictive clean-room lanes."), CleanRoomCount, 5);
TestEqual(TEXT("The original 29 must retain six boundary-sensitive lanes."), BoundarySensitiveCount, 6);
TestEqual(TEXT("Five integration rows own playable puzzle engines."), NativePlayableCount, 5);
TestEqual(TEXT("Fourteen integration rows own native player services."), NativePlayerServiceCount, 14);
TestEqual(TEXT("Seven integration rows remain bounded browser support."), BrowserSupportCount, 7);
TestEqual(TEXT("Three integration rows remain operator support."), OperatorSupportCount, 3);
FHyperTwistIntegratedCapability Capability;
TestTrue(
TEXT("KubeTimr must explicitly resolve to the live timer owner and Classic route."),
UHyperTwistFirstRunLaunchLibrary::TryFindIntegratedCapability(
TEXT("Aarav2709/KubeTimr"),
Capability)
&& Capability.RuntimeOwnerId.Contains(TEXT("LiveTimer"))
&& Capability.PrimaryExperienceRoute == TEXT("classic-cube-training"));
TestTrue(
TEXT("cube_trainer must explicitly resolve to persisted coaching in Learning Studio."),
UHyperTwistFirstRunLaunchLibrary::TryFindIntegratedCapability(
TEXT("Lykos/cube_trainer"),
Capability)
&& Capability.RuntimeOwnerId.Contains(TEXT("TrainingRepository"))
&& Capability.PrimaryExperienceRoute == TEXT("menu:learn"));
TestTrue(
TEXT("Hyperspeedcube must resolve to the generalized native 4D runtime."),
UHyperTwistFirstRunLaunchLibrary::TryFindIntegratedCapability(
TEXT("HactarCE/Hyperspeedcube"),
Capability)
&& Capability.Surface == EHyperTwistIntegrationSurface::NativePlayable
&& Capability.PrimaryExperienceRoute
== TEXT("magic-cube-4d-3x3x3x3"));
TestTrue(
TEXT("MagicTile must remain browser support rather than a falsely complete native renderer."),
UHyperTwistFirstRunLaunchLibrary::TryFindIntegratedCapability(
TEXT("roice3/MagicTile"),
Capability)
&& Capability.Surface == EHyperTwistIntegrationSurface::BrowserSupport);
TestTrue(
TEXT("Remotion must preserve landed outputs while marking future widening restrictive."),
UHyperTwistFirstRunLaunchLibrary::TryFindIntegratedCapability(
TEXT("remotion-dev/remotion"),
Capability)
&& Capability.ImplementationPosture.Contains(TEXT("restrictive-future")));
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistLearningStudioDeckContractTest,
"HyperTwist.FirstRun.LearningStudioDeckContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistLearningStudioDeckContractTest::RunTest(const FString& Parameters)
{
static_cast<void>(Parameters);
const TArray<FHyperTwistContentPack> Catalog =
UHyperTwistTrainingCatalogLibrary::MakePhase3TrainingCatalog();
const TArray<FString> LearningStudioDeckIds = {
TEXT("oll-t"),
TEXT("cross-4move"),
TEXT("5style-4mover"),
TEXT("roux-trainers-algorithm-case-deck-cmll-default"),
TEXT("cube-trainer-algorithm-case-deck-commutator-uf-edge")
};
for (const FString& DeckId : LearningStudioDeckIds)
{
FHyperTwistTrainingDeck Deck;
TestTrue(
*FString::Printf(
TEXT("Learning Studio deck '%s' must resolve from the packaged catalog."),
*DeckId),
UHyperTwistTrainingCatalogLibrary::TryFindDeckInCatalog(
Catalog,
DeckId,
Deck)
&& Deck.IsStructurallyValid()
&& !Deck.Cases.IsEmpty());
}
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST( IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistPackagedStartupRouteArgumentContractTest, FHyperTwistPackagedStartupRouteArgumentContractTest,
"HyperTwist.FirstRun.PackagedStartupRouteArgumentContract", "HyperTwist.FirstRun.PackagedStartupRouteArgumentContract",
@ -400,6 +710,56 @@ bool FHyperTwistNativeWidgetRootLifecycleTest::RunTest(const FString& Parameters
TEXT("The Classic cube route button must retain its click delegate after Slate rebuild."), TEXT("The Classic cube route button must retain its click delegate after Slate rebuild."),
ClassicCubeButton != nullptr && ClassicCubeButton->OnClicked.IsBound() ClassicCubeButton != nullptr && ClassicCubeButton->OnClicked.IsBound()
); );
const TArray<FName> LearningStudioButtonNames = {
TEXT("LearningStudioOllButton"),
TEXT("LearningStudioCrossButton"),
TEXT("LearningStudioFiveStyleButton"),
TEXT("LearningStudioRouxButton"),
TEXT("LearningStudioBlindfoldButton"),
TEXT("LearningStudioRevealButton"),
TEXT("LearningStudioRememberedButton"),
TEXT("LearningStudioReviewButton")
};
for (const FName ButtonName : LearningStudioButtonNames)
{
const UButton* LearningButton = FirstRunWidget->WidgetTree != nullptr
? Cast<UButton>(FirstRunWidget->WidgetTree->FindWidget(ButtonName))
: nullptr;
TestNotNull(
*FString::Printf(
TEXT("The native Learning Studio must expose '%s'."),
*ButtonName.ToString()),
LearningButton);
TestTrue(
*FString::Printf(
TEXT("Learning Studio button '%s' must retain its action binding."),
*ButtonName.ToString()),
LearningButton != nullptr && LearningButton->OnClicked.IsBound());
}
TestNotNull(
TEXT("The native Learning Studio must expose its active exercise panel."),
FirstRunWidget->WidgetTree != nullptr
? FirstRunWidget->WidgetTree->FindWidget(
FName(TEXT("LearningStudioExercisePanel")))
: nullptr);
const UButton* AboutButton = FirstRunWidget->WidgetTree != nullptr
? Cast<UButton>(
FirstRunWidget->WidgetTree->FindWidget(
FName(TEXT("AboutNavButton"))))
: nullptr;
TestNotNull(
TEXT("The native menu must expose About & Credits."),
AboutButton);
TestTrue(
TEXT("The About & Credits navigation action must remain bound."),
AboutButton != nullptr && AboutButton->OnClicked.IsBound());
TestNotNull(
TEXT("The native menu must build the complete integration-credit page."),
FirstRunWidget->WidgetTree != nullptr
? FirstRunWidget->WidgetTree->FindWidget(
FName(TEXT("AboutPage")))
: nullptr);
} }
UHyperTwistClassicCubeHUDWidget* ClassicHudWidget = UHyperTwistClassicCubeHUDWidget* ClassicHudWidget =

View file

@ -39,7 +39,7 @@ bool FHyperTwistHyperspeedcubePhase6RACatalogLookupTest::RunTest(const FString&
UHyperTwistTrainingCatalogLibrary::MakeRetainedHyperPuzzleCatalog(); UHyperTwistTrainingCatalogLibrary::MakeRetainedHyperPuzzleCatalog();
TestTrue(TEXT("The retained hyper puzzle catalog must be structurally valid."), Catalog.IsStructurallyValid()); TestTrue(TEXT("The retained hyper puzzle catalog must be structurally valid."), Catalog.IsStructurallyValid());
TestTrue(TEXT("The retained hyper puzzle catalog must register multiple retained entries."), Catalog.Entries.Num() >= 5); TestTrue(TEXT("The retained hyper puzzle catalog must register all retained launch orders."), Catalog.Entries.Num() >= 7);
FHyperTwistRetainedHyperPuzzleCatalogEntry VirtualEntry; FHyperTwistRetainedHyperPuzzleCatalogEntry VirtualEntry;
TestTrue( TestTrue(
@ -57,6 +57,33 @@ bool FHyperTwistHyperspeedcubePhase6RACatalogLookupTest::RunTest(const FString&
4 4
); );
for (int32 Order = 4; Order <= 6; ++Order)
{
const FString Alias =
FString::Printf(TEXT("%dx%dx%dx%d"), Order, Order, Order, Order);
FHyperTwistRetainedHyperPuzzleCatalogEntry ExtendedEntry;
TestTrue(
*FString::Printf(TEXT("The catalog must resolve retained %s."), *Alias),
UHyperTwistTrainingCatalogLibrary::TryGetRetainedHyperPuzzleCatalogEntry(
Alias,
ExtendedEntry));
TestEqual(
*FString::Printf(TEXT("The retained %s entry must remain four-dimensional."), *Alias),
ExtendedEntry.Dimension,
4);
TestEqual(
*FString::Printf(TEXT("The retained %s entry must expose four size axes."), *Alias),
ExtendedEntry.SizeVector.Num(),
4);
for (const int32 AxisOrder : ExtendedEntry.SizeVector)
{
TestEqual(
*FString::Printf(TEXT("Every %s size axis must use the retained order."), *Alias),
AxisOrder,
Order);
}
}
FHyperTwistRetainedHyperPuzzleCatalogEntry PhysicalEntry; FHyperTwistRetainedHyperPuzzleCatalogEntry PhysicalEntry;
TestTrue( TestTrue(
TEXT("The catalog must resolve the physical 2x2x2x2 alias."), TEXT("The catalog must resolve the physical 2x2x2x2 alias."),

View file

@ -0,0 +1,233 @@
#include "Misc/AutomationTest.h"
#include "HyperTwistSimulation/HyperTwistMagic120CellRuntimeLibrary.h"
#if WITH_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistMagic120CellStateProjectionContractTest,
"HyperTwist.Permissive.Magic120Cell.StateProjectionContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistMagic120CellStateProjectionContractTest::RunTest(
const FString& Parameters
)
{
static_cast<void>(Parameters);
FString TableError;
TestTrue(
TEXT("Magic120Cell exact permutation table must decode and validate."),
UHyperTwistMagic120CellRuntimeLibrary::IsPermutationTableReady(TableError));
if (!TableError.IsEmpty())
{
AddError(TableError);
}
const FHyperTwistMagic120CellRuntimeState SolvedState =
UHyperTwistMagic120CellRuntimeLibrary::BuildSolvedState();
TestTrue(
TEXT("Magic120Cell solved state must be structurally valid."),
SolvedState.IsStructurallyValid());
TestTrue(TEXT("Magic120Cell fresh state must be solved."), SolvedState.bIsSolved);
TestEqual(
TEXT("Magic120Cell must own all 120 x 63 facelet slots."),
SolvedState.StickerColorIndices.Num(),
7560);
const FHyperTwistMagic120CellProjectionBuildResult Projection =
UHyperTwistMagic120CellRuntimeLibrary::BuildProjection(SolvedState);
TestTrue(
TEXT("Magic120Cell solved projection must be exact."),
Projection.bProjected && Projection.bExactProjection);
TestTrue(
TEXT("Magic120Cell solved projection must be structurally valid."),
Projection.Projection.IsStructurallyValid());
TestEqual(
TEXT("Magic120Cell projection must retain all 120 cells."),
Projection.Projection.Cells.Num(),
120);
int32 ProjectedStickerCount = 0;
for (const FHyperTwistMagic120CellProjectedCell& Cell :
Projection.Projection.Cells)
{
ProjectedStickerCount += Cell.StickerColorIndices.Num();
TestTrue(TEXT("Every solved projected cell must be solved."), Cell.bCellSolved);
}
TestEqual(
TEXT("Magic120Cell projection must retain all 7,560 facelets."),
ProjectedStickerCount,
7560);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistMagic120CellPermutationCoverageContractTest,
"HyperTwist.Permissive.Magic120Cell.PermutationCoverageContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistMagic120CellPermutationCoverageContractTest::RunTest(
const FString& Parameters
)
{
static_cast<void>(Parameters);
const FHyperTwistMagic120CellRuntimeState SolvedState =
UHyperTwistMagic120CellRuntimeLibrary::BuildSolvedState();
for (int32 CellIndex = 0; CellIndex < 120; ++CellIndex)
{
for (int32 StickerIndex = 1; StickerIndex < 63; ++StickerIndex)
{
FHyperTwistMagic120CellTurnRequest Request;
Request.CellIndex = CellIndex;
Request.StickerIndex = StickerIndex;
const FHyperTwistMagic120CellTurnResult Turn =
UHyperTwistMagic120CellRuntimeLibrary::ApplyTurn(
SolvedState,
Request);
if (!Turn.bApplied
|| !Turn.bExactStateUpdate
|| Turn.State.bIsSolved
|| (Turn.MovedStickerCount != 190
&& Turn.MovedStickerCount != 192))
{
AddError(FString::Printf(
TEXT("Exact move failed for cell %d sticker %d."),
CellIndex,
StickerIndex));
return false;
}
Request.bInverse = true;
const FHyperTwistMagic120CellTurnResult Inverse =
UHyperTwistMagic120CellRuntimeLibrary::ApplyTurn(
Turn.State,
Request);
if (!Inverse.bApplied
|| !Inverse.bExactStateUpdate
|| !Inverse.State.bIsSolved)
{
AddError(FString::Printf(
TEXT("Exact inverse failed for cell %d sticker %d."),
CellIndex,
StickerIndex));
return false;
}
}
}
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistMagic120CellTurnOrderContractTest,
"HyperTwist.Permissive.Magic120Cell.TurnOrderContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistMagic120CellTurnOrderContractTest::RunTest(
const FString& Parameters
)
{
static_cast<void>(Parameters);
const int32 RepresentativeStickers[] = {1, 13, 43};
const int32 ExpectedOrders[] = {5, 2, 3};
for (int32 RepresentativeIndex = 0;
RepresentativeIndex < UE_ARRAY_COUNT(RepresentativeStickers);
++RepresentativeIndex)
{
const int32 StickerIndex = RepresentativeStickers[RepresentativeIndex];
const int32 TurnOrder =
UHyperTwistMagic120CellRuntimeLibrary::GetTurnOrderForSticker(
StickerIndex);
TestEqual(
TEXT("Magic120Cell axis family must expose its geometric turn order."),
TurnOrder,
ExpectedOrders[RepresentativeIndex]);
FHyperTwistMagic120CellRuntimeState State =
UHyperTwistMagic120CellRuntimeLibrary::BuildSolvedState();
FHyperTwistMagic120CellTurnRequest Request;
Request.CellIndex = 37;
Request.StickerIndex = StickerIndex;
for (int32 TurnIndex = 0; TurnIndex < TurnOrder; ++TurnIndex)
{
const FHyperTwistMagic120CellTurnResult Turn =
UHyperTwistMagic120CellRuntimeLibrary::ApplyTurn(State, Request);
TestTrue(
TEXT("Magic120Cell geometric-order turn must remain exact."),
Turn.bApplied && Turn.bExactStateUpdate);
State = Turn.State;
}
TestTrue(
TEXT("Magic120Cell geometric turn order must recover identity."),
State.bIsSolved);
}
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistMagic120CellScramblePersistenceContractTest,
"HyperTwist.Permissive.Magic120Cell.ScramblePersistenceContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistMagic120CellScramblePersistenceContractTest::RunTest(
const FString& Parameters
)
{
static_cast<void>(Parameters);
const FHyperTwistMagic120CellScrambleResult Scramble =
UHyperTwistMagic120CellRuntimeLibrary::GenerateScramble(100, 120631);
const FHyperTwistMagic120CellScrambleResult Repeat =
UHyperTwistMagic120CellRuntimeLibrary::GenerateScramble(100, 120631);
TestTrue(
TEXT("Magic120Cell scramble must generate an exact state."),
Scramble.bGenerated
&& Scramble.bExactStateUpdate
&& Scramble.State.IsStructurallyValid());
TestFalse(
TEXT("Magic120Cell scramble must leave solved identity."),
Scramble.State.bIsSolved);
TestEqual(
TEXT("Magic120Cell scramble must retain every move."),
Scramble.Moves.Num(),
100);
const FString Json =
UHyperTwistMagic120CellRuntimeLibrary::SerializeRuntimeStateToJson(
Scramble.State);
TestEqual(
TEXT("Equal seeds must produce identical Magic120Cell state."),
Json,
UHyperTwistMagic120CellRuntimeLibrary::SerializeRuntimeStateToJson(
Repeat.State));
FHyperTwistMagic120CellRuntimeState Restored;
TestTrue(
TEXT("Magic120Cell exact state must round-trip through JSON."),
UHyperTwistMagic120CellRuntimeLibrary::TryDeserializeRuntimeStateFromJson(
Json,
Restored));
TestEqual(
TEXT("Restored Magic120Cell state must reserialize identically."),
UHyperTwistMagic120CellRuntimeLibrary::SerializeRuntimeStateToJson(
Restored),
Json);
const FHyperTwistMagic120CellProjectionBuildResult Projection =
UHyperTwistMagic120CellRuntimeLibrary::BuildProjection(Restored);
TestTrue(
TEXT("Scrambled Magic120Cell state must retain an exact projection."),
Projection.bProjected && Projection.bExactProjection);
FHyperTwistMagic120CellRuntimeState Invalid = Restored;
Invalid.StickerColorIndices[0] = 121;
Invalid.bIsSolved = false;
TestFalse(
TEXT("Magic120Cell state must reject an invalid color index."),
Invalid.IsStructurallyValid());
return true;
}
#endif

View file

@ -0,0 +1,228 @@
#include "Misc/AutomationTest.h"
#include "HyperTwistSimulation/HyperTwistMagicCube5DRuntimeLibrary.h"
#if WITH_AUTOMATION_TESTS
namespace HyperTwistMagicCube5DRuntimeContractTestInternal
{
const EHyperTwistMagicCube5DAxis Axes[] = {
EHyperTwistMagicCube5DAxis::X,
EHyperTwistMagicCube5DAxis::Y,
EHyperTwistMagicCube5DAxis::Z,
EHyperTwistMagicCube5DAxis::W,
EHyperTwistMagicCube5DAxis::V
};
TArray<EHyperTwistMagicCube5DAxis> GetOrthogonalAxes(
const EHyperTwistMagicCube5DAxis FaceAxis
)
{
TArray<EHyperTwistMagicCube5DAxis> Result;
for (const EHyperTwistMagicCube5DAxis Axis : Axes)
{
if (Axis != FaceAxis)
{
Result.Add(Axis);
}
}
return Result;
}
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistMagicCube5DStateProjectionContractTest,
"HyperTwist.Permissive.MagicCube5D.StateProjectionContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistMagicCube5DStateProjectionContractTest::RunTest(
const FString& Parameters
)
{
static_cast<void>(Parameters);
const FHyperTwistMagicCube5DRuntimeState State =
UHyperTwistMagicCube5DRuntimeLibrary::BuildSolvedState(3);
TestTrue(TEXT("Order-3 5D state must be structurally valid."), State.IsStructurallyValid());
TestTrue(TEXT("Fresh 5D state must be solved."), State.bIsSolved);
TestEqual(TEXT("Order-3 5D must own all 243 cubies."), State.PositionToPiece.Num(), 243);
TestEqual(
TEXT("Order-3 5D must own every cubie orientation."),
State.PieceOrientations.Num(),
243);
const FHyperTwistMagicCube5DProjectionBuildResult Projection =
UHyperTwistMagicCube5DRuntimeLibrary::BuildProjection(State);
TestTrue(
TEXT("Solved 5D projection must be exact."),
Projection.bProjected && Projection.bExactProjection);
TestTrue(
TEXT("Solved 5D projection must be structurally valid."),
Projection.Projection.IsStructurallyValid());
TestEqual(
TEXT("Order-3 5D projection must expose 242 non-central boundary cubies."),
Projection.Projection.Cubies.Num(),
242);
TestEqual(
TEXT("Order-3 5D projection must expose all 810 boundary facelets."),
Projection.Projection.Facelets.Num(),
810);
int32 FaceColorCounts[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
for (const FHyperTwistMagicCube5DProjectedFacelet& Facelet :
Projection.Projection.Facelets)
{
if (Facelet.ColorIndex >= 0 && Facelet.ColorIndex < 10)
{
++FaceColorCounts[Facelet.ColorIndex];
}
}
for (int32 ColorIndex = 0; ColorIndex < 10; ++ColorIndex)
{
TestEqual(
*FString::Printf(
TEXT("Solved 5D color %d must own one 3^4 face."),
ColorIndex),
FaceColorCounts[ColorIndex],
81);
}
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistMagicCube5DTurnIdentityContractTest,
"HyperTwist.Permissive.MagicCube5D.TurnIdentityContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistMagicCube5DTurnIdentityContractTest::RunTest(
const FString& Parameters
)
{
static_cast<void>(Parameters);
using namespace HyperTwistMagicCube5DRuntimeContractTestInternal;
const FHyperTwistMagicCube5DRuntimeState SolvedState =
UHyperTwistMagicCube5DRuntimeLibrary::BuildSolvedState(3);
for (const EHyperTwistMagicCube5DAxis FaceAxis : Axes)
{
const TArray<EHyperTwistMagicCube5DAxis> OrthogonalAxes =
GetOrthogonalAxes(FaceAxis);
for (int32 AxisAIndex = 0; AxisAIndex < OrthogonalAxes.Num(); ++AxisAIndex)
{
for (int32 AxisBIndex = AxisAIndex + 1;
AxisBIndex < OrthogonalAxes.Num();
++AxisBIndex)
{
FHyperTwistMagicCube5DTurnRequest Request;
Request.FaceAxis = FaceAxis;
Request.bPositiveFace = true;
Request.SliceMask = 1;
Request.RotationAxisA = OrthogonalAxes[AxisAIndex];
Request.RotationAxisB = OrthogonalAxes[AxisBIndex];
Request.Direction =
EHyperTwistMagicCube5DTurnDirection::PositiveQuarterTurn;
const FHyperTwistMagicCube5DTurnResult Turn =
UHyperTwistMagicCube5DRuntimeLibrary::ApplyTurn(
SolvedState,
Request);
TestTrue(
TEXT("Every signed-face orthogonal plane move must apply exactly."),
Turn.bApplied && Turn.bExactStateUpdate);
TestFalse(TEXT("A single 5D quarter turn must not be solved."), Turn.State.bIsSolved);
FHyperTwistMagicCube5DTurnRequest Inverse = Request;
Inverse.Direction =
EHyperTwistMagicCube5DTurnDirection::NegativeQuarterTurn;
const FHyperTwistMagicCube5DTurnResult Undo =
UHyperTwistMagicCube5DRuntimeLibrary::ApplyTurn(
Turn.State,
Inverse);
TestTrue(
TEXT("A 5D move followed by its inverse must remain exact."),
Undo.bApplied && Undo.bExactStateUpdate);
TestTrue(
TEXT("A 5D move followed by its inverse must recover solved identity."),
Undo.State.bIsSolved);
FHyperTwistMagicCube5DRuntimeState FourTurnState = SolvedState;
for (int32 TurnIndex = 0; TurnIndex < 4; ++TurnIndex)
{
const FHyperTwistMagicCube5DTurnResult QuarterTurn =
UHyperTwistMagicCube5DRuntimeLibrary::ApplyTurn(
FourTurnState,
Request);
TestTrue(
TEXT("Every four-turn identity step must remain exact."),
QuarterTurn.bApplied && QuarterTurn.bExactStateUpdate);
FourTurnState = QuarterTurn.State;
}
TestTrue(
TEXT("Four 5D quarter turns in one plane must recover identity."),
FourTurnState.bIsSolved);
}
}
}
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistMagicCube5DScramblePersistenceContractTest,
"HyperTwist.Permissive.MagicCube5D.ScramblePersistenceContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistMagicCube5DScramblePersistenceContractTest::RunTest(
const FString& Parameters
)
{
static_cast<void>(Parameters);
const FHyperTwistMagicCube5DScrambleResult Scramble =
UHyperTwistMagicCube5DRuntimeLibrary::GenerateScramble(3, 60, 51205);
const FHyperTwistMagicCube5DScrambleResult Repeat =
UHyperTwistMagicCube5DRuntimeLibrary::GenerateScramble(3, 60, 51205);
TestTrue(
TEXT("5D scramble must generate a valid exact state."),
Scramble.bGenerated
&& Scramble.bExactStateUpdate
&& Scramble.State.IsStructurallyValid());
TestFalse(TEXT("5D scramble must leave solved identity."), Scramble.State.bIsSolved);
TestEqual(TEXT("5D scramble must retain every move."), Scramble.Moves.Num(), 60);
TestEqual(
TEXT("Equal seeds must produce identical exact 5D state."),
UHyperTwistMagicCube5DRuntimeLibrary::SerializeRuntimeStateToJson(Scramble.State),
UHyperTwistMagicCube5DRuntimeLibrary::SerializeRuntimeStateToJson(Repeat.State));
const FHyperTwistMagicCube5DProjectionBuildResult Projection =
UHyperTwistMagicCube5DRuntimeLibrary::BuildProjection(Scramble.State);
TestTrue(
TEXT("Scrambled 5D state must retain an exact 810-facelet projection."),
Projection.bProjected
&& Projection.bExactProjection
&& Projection.Projection.Facelets.Num() == 810);
const FString Json =
UHyperTwistMagicCube5DRuntimeLibrary::SerializeRuntimeStateToJson(Scramble.State);
FHyperTwistMagicCube5DRuntimeState Restored;
TestTrue(
TEXT("Exact 5D state must round-trip through persistence JSON."),
UHyperTwistMagicCube5DRuntimeLibrary::TryDeserializeRuntimeStateFromJson(
Json,
Restored));
TestEqual(
TEXT("Restored 5D state must be byte-semantically stable when reserialized."),
UHyperTwistMagicCube5DRuntimeLibrary::SerializeRuntimeStateToJson(Restored),
Json);
FHyperTwistMagicCube5DRuntimeState Reflected =
UHyperTwistMagicCube5DRuntimeLibrary::BuildSolvedState(3);
Reflected.PieceOrientations[0].Basis[0].bPositiveDirection = false;
Reflected.bIsSolved = false;
TestFalse(
TEXT("5D state must reject an impossible reflected orientation."),
Reflected.IsStructurallyValid());
return true;
}
#endif

View file

@ -0,0 +1,646 @@
#include "Misc/AutomationTest.h"
#include "Blueprint/WidgetTree.h"
#include "Engine/GameInstance.h"
#include "Engine/Level.h"
#include "Engine/World.h"
#include "GameFramework/PlayerStart.h"
#include "GameFramework/WorldSettings.h"
#include "HyperTwistSimulation/HyperTwistMelindaProjectionGameMode.h"
#include "HyperTwistSimulation/HyperTwistClassicCubeHUDWidget.h"
#include "HyperTwistSimulation/HyperTwistMelindaProjectionPlayerController.h"
#include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionGameMode.h"
#include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionPlayerController.h"
#include "HyperTwistTraining/HyperTwistHigherDimensionalTrainingGameMode.h"
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"
#include "HyperTwistUX/HyperTwistCoachAssistantWidget.h"
#include "HyperTwistUX/HyperTwistDictationCaptureComponent.h"
#include "HyperTwistUX/HyperTwistFourDimensionalHUDWidget.h"
#include "HyperTwistUX/HyperTwistHigherDimensionalHUDWidget.h"
#include "HyperTwistUX/HyperTwistPlayerControllerBase.h"
#include "HyperTwistUX/HyperTwistPlayerSettings.h"
#include "HyperTwistUX/HyperTwistSettingsPanelWidget.h"
#include "Materials/MaterialInterface.h"
#include "Misc/PackageName.h"
#if WITH_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistPlayerPreferenceDefaultsContractTest,
"HyperTwist.PlayerExperience.PreferenceDefaults",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistPlayerPreferenceDefaultsContractTest::RunTest(
const FString& Parameters
)
{
static_cast<void>(Parameters);
const FHyperTwistPlayerPreferences Preferences =
UHyperTwistPlayerSettingsLibrary::GetDefaultPreferences();
TestTrue(
TEXT("Default player preferences must be structurally valid."),
Preferences.IsStructurallyValid());
TestEqual(TEXT("The current settings schema must remain explicit."), Preferences.SchemaVersion, 4);
TestEqual(
TEXT("Professional Classic controls must remain the default profile."),
Preferences.KeyboardProfileId,
FString(TEXT("classic-wca-keyboard/v1")));
TestFalse(
TEXT("Cloud providers must remain explicit opt-in."),
Preferences.bAllowCloudProviders);
TestFalse(
TEXT("Generated coach responses must remain explicit opt-in."),
Preferences.bCoachAiEnabled);
TestTrue(
TEXT("The private built-in assistant must remain available without AI."),
Preferences.bAssistantPanelEnabled);
TestEqual(
TEXT("Speech must default to the local Whisper lane."),
Preferences.SpeechProviderId,
FString(TEXT("local-whisper-cpp")));
TestEqual(
TEXT("Dictation must default to a reviewable coach draft."),
Preferences.SpeechDictationDestination,
FString(TEXT("coach-draft")));
TestEqual(
TEXT("Narration must default to the local Piper lane."),
Preferences.VoiceProviderId,
FString(TEXT("local-piper")));
TestEqual(
TEXT("The fixed assistant shortcut must default to F3."),
UHyperTwistPlayerSettingsLibrary::ResolveKeyBinding(
Preferences,
TEXT("assistant.toggle")),
EKeys::F3);
const TArray<FHyperTwistKeyBindingDescriptor> Bindings =
UHyperTwistPlayerSettingsLibrary::BuildKeyBindingDescriptors(Preferences);
TestTrue(
TEXT("The settings surface must expose a substantial configurable keyboard map."),
Bindings.Num() >= 24);
for (const FHyperTwistKeyBindingDescriptor& Binding : Bindings)
{
TestTrue(
*FString::Printf(
TEXT("Binding %s must remain structurally valid."),
*Binding.ActionId.ToString()),
Binding.IsStructurallyValid());
}
for (const FString& ProfileId :
UHyperTwistPlayerSettingsLibrary::GetSupportedKeyboardProfileIds())
{
if (ProfileId == TEXT("custom/v1"))
{
continue;
}
FHyperTwistPlayerPreferences ProfilePreferences = Preferences;
TestTrue(
*FString::Printf(TEXT("Profile %s must apply."), *ProfileId),
UHyperTwistPlayerSettingsLibrary::ApplyKeyboardProfile(
ProfilePreferences,
ProfileId));
TSet<FName> UsedKeys;
for (const FHyperTwistKeyBindingDescriptor& Binding :
UHyperTwistPlayerSettingsLibrary::BuildKeyBindingDescriptors(
ProfilePreferences))
{
const FName KeyName = Binding.Key.GetFName();
TestFalse(
*FString::Printf(
TEXT("Profile %s must not assign %s more than once."),
*ProfileId,
*KeyName.ToString()),
UsedKeys.Contains(KeyName));
UsedKeys.Add(KeyName);
}
}
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistProviderEndpointPolicyContractTest,
"HyperTwist.PlayerExperience.ProviderEndpointPolicy",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistProviderEndpointPolicyContractTest::RunTest(
const FString& Parameters
)
{
static_cast<void>(Parameters);
FString FailureReason;
TestTrue(
TEXT("Loopback HTTP must work without cloud consent."),
UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
TEXT("http://127.0.0.1:8766"),
false,
FailureReason));
TestTrue(TEXT("A valid loopback endpoint must not retain an error."), FailureReason.IsEmpty());
TestTrue(
TEXT("IPv6 loopback HTTPS must remain local."),
UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
TEXT("https://[::1]:8766/v1"),
false,
FailureReason));
TestFalse(
TEXT("Remote HTTPS must require explicit cloud consent."),
UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
TEXT("https://api.openai.com/v1/responses"),
false,
FailureReason));
TestTrue(
TEXT("Remote HTTPS may proceed after explicit cloud consent."),
UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
TEXT("https://api.openai.com/v1/responses"),
true,
FailureReason));
TestFalse(
TEXT("Remote plaintext HTTP must never be accepted."),
UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
TEXT("http://example.com/v1"),
true,
FailureReason));
TestFalse(
TEXT("A deceptive localhost suffix must not pass the loopback boundary."),
UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
TEXT("http://localhost.example.com/v1"),
false,
FailureReason));
TestFalse(
TEXT("Provider URLs must reject embedded credentials."),
UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
TEXT("https://user:secret@example.com/v1"),
true,
FailureReason));
TestFalse(
TEXT("Provider URLs must reject whitespace."),
UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
TEXT("https://example.com/v1 responses"),
true,
FailureReason));
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistNativeControlCenterAndAssistantContractTest,
"HyperTwist.PlayerExperience.NativeControlCenterAndAssistant",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistNativeControlCenterAndAssistantContractTest::RunTest(
const FString& Parameters
)
{
static_cast<void>(Parameters);
UHyperTwistSettingsPanelWidget* Settings =
NewObject<UHyperTwistSettingsPanelWidget>();
TestNotNull(TEXT("The native settings surface must be constructible."), Settings);
if (Settings != nullptr)
{
Settings->TakeWidget();
TestTrue(
TEXT("The complete native settings surface must materialize."),
Settings->IsSettingsSurfaceReady());
const FName RequiredSettingsControls[] = {
TEXT("MasterVolumeSlider"),
TEXT("MusicVolumeSlider"),
TEXT("EffectsVolumeSlider"),
TEXT("VoiceVolumeSlider"),
TEXT("MutedCheck"),
TEXT("SpeechInputCheck"),
TEXT("SpeechProviderCombo"),
TEXT("SpeechActivationModeCombo"),
TEXT("SpeechDictationDestinationCombo"),
TEXT("SpeechLanguageCombo"),
TEXT("SpeechModelInput"),
TEXT("SpeechMicrophoneCombo"),
TEXT("SpeechContextInput"),
TEXT("SpeechEndpointInput"),
TEXT("SpeechCredentialInput"),
TEXT("VoiceProviderCombo"),
TEXT("VoiceModelInput"),
TEXT("VoiceIdInput"),
TEXT("VoiceSpeedSlider"),
TEXT("VoiceEndpointInput"),
TEXT("VoiceCredentialInput"),
TEXT("CoachProviderCombo"),
TEXT("CoachEndpointInput"),
TEXT("CoachModelInput"),
TEXT("CoachInstructionsInput"),
TEXT("CoachMaxResponseTokensSlider"),
TEXT("CoachRequestTimeoutSlider"),
TEXT("CoachCredentialInput"),
TEXT("AssistantPanelEnabledCheck"),
TEXT("CoachAiEnabledCheck"),
TEXT("CloudProvidersCheck")
};
for (const FName ControlName : RequiredSettingsControls)
{
TestNotNull(
*FString::Printf(
TEXT("Settings must expose %s."),
*ControlName.ToString()),
Settings->WidgetTree != nullptr
? Settings->WidgetTree->FindWidget(ControlName)
: nullptr);
}
const TArray<FHyperTwistMicrophoneDescriptor> Microphones =
UHyperTwistPlayerSettingsLibrary::GetAvailableMicrophones();
TestTrue(
TEXT("The control center must always expose a system-default microphone route."),
Microphones.ContainsByPredicate(
[](const FHyperTwistMicrophoneDescriptor& Microphone)
{
return Microphone.DeviceId == TEXT("system-default")
&& Microphone.bIsDefault
&& Microphone.IsStructurallyValid();
}));
}
UHyperTwistCoachAssistantWidget* Assistant =
NewObject<UHyperTwistCoachAssistantWidget>();
TestNotNull(TEXT("The fixed in-game assistant must be constructible."), Assistant);
if (Assistant != nullptr)
{
Assistant->TakeWidget();
TestTrue(
TEXT("The fixed in-game assistant must materialize its conversation surface."),
Assistant->IsAssistantSurfaceReady());
TestNotNull(
TEXT("The assistant must expose a session-only prompt composer."),
Assistant->WidgetTree != nullptr
? Assistant->WidgetTree->FindWidget(
FName(TEXT("CoachAssistantPrompt")))
: nullptr);
TestNotNull(
TEXT("The assistant must expose global dictation without polluting puzzle HUDs."),
Assistant->WidgetTree != nullptr
? Assistant->WidgetTree->FindWidget(
FName(TEXT("CoachAssistantDictationButton")))
: nullptr);
}
const AHyperTwistPlayerControllerBase* PlayerControllerDefaults =
GetDefault<AHyperTwistPlayerControllerBase>();
TestNotNull(
TEXT("Every native puzzle controller must own the common dictation capture route."),
PlayerControllerDefaults != nullptr
? PlayerControllerDefaults->GlobalDictationCapture.Get()
: nullptr);
TestNotNull(
TEXT("Every native puzzle controller must expose non-blocking coach narration."),
AHyperTwistPlayerControllerBase::StaticClass()->FindFunctionByName(
TEXT("NarrateCoachResponse")));
TestNotNull(
TEXT("Every native puzzle controller must expose narration cancellation."),
AHyperTwistPlayerControllerBase::StaticClass()->FindFunctionByName(
TEXT("StopCoachNarration")));
UHyperTwistFourDimensionalHUDWidget* FourDimensionalHud =
NewObject<UHyperTwistFourDimensionalHUDWidget>();
TestNotNull(
TEXT("The shared 4D HUD must be constructible."),
FourDimensionalHud);
if (FourDimensionalHud != nullptr)
{
FourDimensionalHud->TakeWidget();
TestTrue(
TEXT("The shared 4D HUD must materialize its player controls."),
FourDimensionalHud->IsHudSurfaceReady());
const FName RequiredFourDimensionalControls[] = {
TEXT("FourDimensionalHudAxisButton"),
TEXT("FourDimensionalHudPreviousLayerButton"),
TEXT("FourDimensionalHudNextLayerButton"),
TEXT("FourDimensionalHudCounterClockwiseButton"),
TEXT("FourDimensionalHudClockwiseButton"),
TEXT("FourDimensionalHudScrambleButton"),
TEXT("FourDimensionalHudResetButton"),
TEXT("FourDimensionalHudSaveButton"),
TEXT("FourDimensionalHudLoadButton")
};
for (const FName ControlName : RequiredFourDimensionalControls)
{
TestNotNull(
*FString::Printf(
TEXT("The shared 4D HUD must expose %s."),
*ControlName.ToString()),
FourDimensionalHud->WidgetTree != nullptr
? FourDimensionalHud->WidgetTree->FindWidget(ControlName)
: nullptr);
}
}
UHyperTwistHigherDimensionalHUDWidget* HigherDimensionalHud =
NewObject<UHyperTwistHigherDimensionalHUDWidget>();
TestNotNull(
TEXT("The exact 120-cell and 5D HUD must be constructible."),
HigherDimensionalHud);
if (HigherDimensionalHud != nullptr)
{
HigherDimensionalHud->TakeWidget();
TestTrue(
TEXT("The exact 120-cell and 5D HUD must materialize its player controls."),
HigherDimensionalHud->IsHudSurfaceReady());
const FName RequiredHigherDimensionalControls[] = {
TEXT("HigherDimensionalHudPreviousPrimary"),
TEXT("HigherDimensionalHudNextPrimary"),
TEXT("HigherDimensionalHudPreviousSecondary"),
TEXT("HigherDimensionalHudNextSecondary"),
TEXT("HigherDimensionalHudPreviousTertiary"),
TEXT("HigherDimensionalHudNextTertiary"),
TEXT("HigherDimensionalHudFaceSide"),
TEXT("HigherDimensionalHudNegativeTurn"),
TEXT("HigherDimensionalHudPositiveTurn"),
TEXT("HigherDimensionalHudPreviousProjectionLayer"),
TEXT("HigherDimensionalHudNextProjectionLayer"),
TEXT("HigherDimensionalHudAutoRotate"),
TEXT("HigherDimensionalHudScramble"),
TEXT("HigherDimensionalHudReset"),
TEXT("HigherDimensionalHudSave"),
TEXT("HigherDimensionalHudLoad"),
TEXT("HigherDimensionalHudCoach"),
TEXT("HigherDimensionalHudMenu")
};
for (const FName ControlName : RequiredHigherDimensionalControls)
{
TestNotNull(
*FString::Printf(
TEXT("The exact 120-cell and 5D HUD must expose %s."),
*ControlName.ToString()),
HigherDimensionalHud->WidgetTree != nullptr
? HigherDimensionalHud->WidgetTree->FindWidget(ControlName)
: nullptr);
}
}
UHyperTwistClassicCubeHUDWidget* ClassicHud =
NewObject<UHyperTwistClassicCubeHUDWidget>();
TestNotNull(TEXT("The Classic HUD must remain constructible."), ClassicHud);
if (ClassicHud != nullptr)
{
ClassicHud->TakeWidget();
TestNull(
TEXT("Speech activation must not reappear as a Classic puzzle HUD button."),
ClassicHud->WidgetTree != nullptr
? ClassicHud->WidgetTree->FindWidget(FName(TEXT("VoiceHoldButton")))
: nullptr);
TestNull(
TEXT("Voice selection must remain a global setting, not a Classic HUD button."),
ClassicHud->WidgetTree != nullptr
? ClassicHud->WidgetTree->FindWidget(FName(TEXT("VoiceCycleButton")))
: nullptr);
}
const AHyperTwistPlayerControllerBase* ControllerDefaults =
GetDefault<AHyperTwistPlayerControllerBase>();
TestTrue(
TEXT("Gameplay controllers must keep the fixed assistant capability enabled."),
ControllerDefaults != nullptr
&& ControllerDefaults->bEnableGlobalAssistantPanel);
const AHyperTwistMelindaProjectionPlayerController* CellFirstController =
GetDefault<AHyperTwistMelindaProjectionPlayerController>();
const AHyperTwistVirtual3333ProjectionPlayerController* VisibleSliceController =
GetDefault<AHyperTwistVirtual3333ProjectionPlayerController>();
const AHyperTwistHigherDimensionalTrainingPlayerController*
HigherDimensionalController =
GetDefault<AHyperTwistHigherDimensionalTrainingPlayerController>();
TestTrue(
TEXT("All native higher-dimensional routes must show actionable player HUDs by default."),
CellFirstController != nullptr
&& CellFirstController->bShowPuzzleHud
&& VisibleSliceController != nullptr
&& VisibleSliceController->bShowPuzzleHud
&& HigherDimensionalController != nullptr
&& HigherDimensionalController->bShowPuzzleHud);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistGlobalAssistantSpeechSessionContractTest,
"HyperTwist.PlayerExperience.GlobalAssistantSpeechSession",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistGlobalAssistantSpeechSessionContractTest::RunTest(
const FString& Parameters
)
{
static_cast<void>(Parameters);
UGameInstance* GameInstance = NewObject<UGameInstance>();
UHyperTwistTrainingSubsystem* TrainingSubsystem =
GameInstance != nullptr
? NewObject<UHyperTwistTrainingSubsystem>(GameInstance)
: nullptr;
TestNotNull(
TEXT("The global assistant must be able to create its speech subsystem."),
TrainingSubsystem);
if (TrainingSubsystem == nullptr)
{
return false;
}
TestFalse(
TEXT("This proof must not depend on a Classic training run."),
TrainingSubsystem->HasActiveRun());
TrainingSubsystem->SetCompanionSpeechClientKindForAutomation(TEXT("mock"));
FString OpenError;
TestTrue(
TEXT("The fixed assistant must open dictation in 4D, 5D, and 120-cell routes without an active training run."),
TrainingSubsystem->OpenActiveCompanionSpeechSession(OpenError));
TestTrue(
TEXT("Standalone assistant dictation must not report an open error."),
OpenError.IsEmpty());
const FHyperTwistTrainingCompanionSpeechSessionState SessionState =
TrainingSubsystem->GetActiveCompanionSpeechSessionState();
TestTrue(
TEXT("The standalone assistant speech session must remain open."),
SessionState.bSessionOpen);
TestTrue(
TEXT("The standalone assistant speech session must own an explicit global session id."),
SessionState.ActiveSessionId.StartsWith(TEXT("speech-global-")));
TestTrue(
TEXT("The standalone assistant speech configuration must remain structurally valid."),
SessionState.SessionConfig.IsStructurallyValid());
FString CloseError;
TestTrue(
TEXT("The standalone assistant speech session must close cleanly."),
TrainingSubsystem->CloseActiveCompanionSpeechSession(CloseError));
TestTrue(
TEXT("Closing standalone assistant dictation must not report an error."),
CloseError.IsEmpty());
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistDedicatedVirtual4DMapContractTest,
"HyperTwist.PlayerExperience.DedicatedVirtual4DMaps",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistDedicatedVirtual4DMapContractTest::RunTest(
const FString& Parameters
)
{
static_cast<void>(Parameters);
struct FMapExpectation
{
int32 Order;
const TCHAR* MapAssetPath;
UClass* GameModeClass;
const TCHAR* ProjectionActorClassPath;
};
const FMapExpectation Expectations[] = {
{
2,
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_2x2x2x2Training"),
AHyperTwistMelindaProjectionGameMode::StaticClass(),
TEXT("/Script/UnrealHyperTwist.HyperTwistMelindaProjectionActor")
},
{
3,
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_3x3x3x3Training"),
AHyperTwistVirtual3333ProjectionGameMode::StaticClass(),
TEXT("/Script/UnrealHyperTwist.HyperTwistVirtual3333ProjectionActor")
},
{
4,
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_4x4x4x4Training"),
AHyperTwistVirtual4444ProjectionGameMode::StaticClass(),
TEXT("/Script/UnrealHyperTwist.HyperTwistVirtual3333ProjectionActor")
},
{
5,
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_5x5x5x5Training"),
AHyperTwistVirtual5555ProjectionGameMode::StaticClass(),
TEXT("/Script/UnrealHyperTwist.HyperTwistVirtual3333ProjectionActor")
},
{
6,
TEXT("/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_6x6x6x6Training"),
AHyperTwistVirtual6666ProjectionGameMode::StaticClass(),
TEXT("/Script/UnrealHyperTwist.HyperTwistVirtual3333ProjectionActor")
}
};
TestNotNull(
TEXT("The vertex-color projection material must be a real content asset."),
LoadObject<UMaterialInterface>(
nullptr,
TEXT("/Game/HyperTwistTraining/Materials/M_HT_ProjectionVertexColor.M_HT_ProjectionVertexColor")));
for (const FMapExpectation& Expectation : Expectations)
{
const FString MapAssetPath(Expectation.MapAssetPath);
const FString MapObjectPath = FString::Printf(
TEXT("%s.%s"),
*MapAssetPath,
*FPackageName::GetShortName(MapAssetPath));
UWorld* World = LoadObject<UWorld>(nullptr, *MapObjectPath);
TestNotNull(
*FString::Printf(
TEXT("The exact %dx%dx%dx%d route must load its dedicated map."),
Expectation.Order,
Expectation.Order,
Expectation.Order,
Expectation.Order),
World);
if (World == nullptr || World->PersistentLevel == nullptr)
{
continue;
}
const AWorldSettings* WorldSettings = World->GetWorldSettings();
TestEqual(
*FString::Printf(
TEXT("The %dD-order map must own its exact runtime game mode."),
Expectation.Order),
WorldSettings != nullptr && WorldSettings->DefaultGameMode != nullptr
? WorldSettings->DefaultGameMode->GetPathName()
: FString(),
Expectation.GameModeClass->GetPathName());
APlayerStart* DedicatedAnchor = nullptr;
int32 PlayerStartCount = 0;
for (AActor* Actor : World->PersistentLevel->Actors)
{
if (APlayerStart* PlayerStart = Cast<APlayerStart>(Actor))
{
++PlayerStartCount;
DedicatedAnchor = PlayerStart;
}
}
TestEqual(
*FString::Printf(
TEXT("The %dx route must own exactly one launch anchor."),
Expectation.Order),
PlayerStartCount,
1);
if (DedicatedAnchor != nullptr)
{
const FName ExpectedOrderTag(*FString::Printf(
TEXT("puzzle-order:%d"),
Expectation.Order));
const FName ExpectedProjectionClassTag(*FString::Printf(
TEXT("projection-class:%s"),
Expectation.ProjectionActorClassPath));
TestTrue(
TEXT("The dedicated launch anchor must retain first-party ownership."),
DedicatedAnchor->ActorHasTag(
FName(TEXT("ownership:first-party-dedicated-map"))));
TestTrue(
TEXT("The dedicated launch anchor must identify its puzzle order."),
DedicatedAnchor->ActorHasTag(ExpectedOrderTag));
TestTrue(
TEXT("The projection must remain runtime-owned by the game mode."),
DedicatedAnchor->ActorHasTag(
FName(TEXT("projection-spawn:runtime-game-mode"))));
TestTrue(
TEXT("The launch anchor must bind its exact projection actor class."),
DedicatedAnchor->ActorHasTag(ExpectedProjectionClassTag));
TestTrue(
TEXT("The dedicated route must retain its keyboard-and-mouse lane."),
DedicatedAnchor->ActorHasTag(FName(TEXT("input:keyboard-mouse"))));
}
if (Expectation.Order == 2)
{
const AHyperTwistMelindaProjectionGameMode* Defaults =
Cast<AHyperTwistMelindaProjectionGameMode>(
Expectation.GameModeClass->GetDefaultObject());
TestTrue(
TEXT("The exact 2x runtime must auto-spawn its projection actor."),
Defaults != nullptr && Defaults->bAutoSpawnProjectionActor);
}
else
{
const AHyperTwistVirtual3333ProjectionGameMode* Defaults =
Cast<AHyperTwistVirtual3333ProjectionGameMode>(
Expectation.GameModeClass->GetDefaultObject());
TestTrue(
*FString::Printf(
TEXT("The exact %dx runtime must auto-spawn its projection actor."),
Expectation.Order),
Defaults != nullptr && Defaults->bAutoSpawnProjectionActor);
TestEqual(
*FString::Printf(
TEXT("The exact %dx runtime must preserve its puzzle order."),
Expectation.Order),
Defaults != nullptr ? Defaults->PuzzleOrder : 0,
Expectation.Order);
}
}
return true;
}
#endif

View file

@ -116,8 +116,17 @@ bool FHyperTwistSpeechLibraryDictationAudioDuckingTest::RunTest(const FString& P
MusicComponent->SetVolumeMultiplier(0.8f); MusicComponent->SetVolumeMultiplier(0.8f);
SpeechComponent->SetVolumeMultiplier(1.0f); SpeechComponent->SetVolumeMultiplier(1.0f);
UHyperTwistSpeechLibrary::RegisterManagedAudioComponent(MusicComponent, false); UHyperTwistSpeechLibrary::RegisterCategorizedAudioComponent(
MusicComponent,
EHyperTwistManagedAudioCategory::Music);
UHyperTwistSpeechLibrary::RegisterManagedAudioComponent(SpeechComponent, true); UHyperTwistSpeechLibrary::RegisterManagedAudioComponent(SpeechComponent, true);
UHyperTwistSpeechLibrary::ApplyManagedAudioCategoryVolumes(0.5f, 0.75f, 0.6f);
TestTrue(
TEXT("The music category must multiply the authored component level."),
FMath::IsNearlyEqual(MusicComponent->VolumeMultiplier, 0.4f));
TestTrue(
TEXT("The voice category must multiply the authored speech level."),
FMath::IsNearlyEqual(SpeechComponent->VolumeMultiplier, 0.6f));
FString OpenError; FString OpenError;
TestTrue( TestTrue(
@ -125,15 +134,13 @@ bool FHyperTwistSpeechLibraryDictationAudioDuckingTest::RunTest(const FString& P
UHyperTwistSpeechLibrary::StartDictationSession(TrainingSubsystem, OpenError, true, 0.15f) UHyperTwistSpeechLibrary::StartDictationSession(TrainingSubsystem, OpenError, true, 0.15f)
); );
TestTrue(TEXT("Opening the dictation session must not report an error."), OpenError.IsEmpty()); TestTrue(TEXT("Opening the dictation session must not report an error."), OpenError.IsEmpty());
TestEqual( TestTrue(
TEXT("Managed non-speech audio must be ducked."), TEXT("Managed non-speech audio must be ducked."),
MusicComponent->VolumeMultiplier, FMath::IsNearlyEqual(MusicComponent->VolumeMultiplier, 0.06f)
0.15f
); );
TestEqual( TestTrue(
TEXT("Managed speech audio must retain its original volume."), TEXT("Managed speech audio must retain its original volume."),
SpeechComponent->VolumeMultiplier, FMath::IsNearlyEqual(SpeechComponent->VolumeMultiplier, 0.6f)
1.0f
); );
FString CloseError; FString CloseError;
@ -142,19 +149,18 @@ bool FHyperTwistSpeechLibraryDictationAudioDuckingTest::RunTest(const FString& P
UHyperTwistSpeechLibrary::EndDictationSession(TrainingSubsystem, CloseError, true) UHyperTwistSpeechLibrary::EndDictationSession(TrainingSubsystem, CloseError, true)
); );
TestTrue(TEXT("Closing the dictation session must not report an error."), CloseError.IsEmpty()); TestTrue(TEXT("Closing the dictation session must not report an error."), CloseError.IsEmpty());
TestEqual( TestTrue(
TEXT("Managed non-speech audio must restore after dictation ends."), TEXT("Managed non-speech audio must restore after dictation ends."),
MusicComponent->VolumeMultiplier, FMath::IsNearlyEqual(MusicComponent->VolumeMultiplier, 0.4f)
0.8f
); );
TestEqual( TestTrue(
TEXT("Managed speech audio must remain unchanged after restore."), TEXT("Managed speech audio must remain unchanged after restore."),
SpeechComponent->VolumeMultiplier, FMath::IsNearlyEqual(SpeechComponent->VolumeMultiplier, 0.6f)
1.0f
); );
UHyperTwistSpeechLibrary::UnregisterManagedAudioComponent(MusicComponent); UHyperTwistSpeechLibrary::UnregisterManagedAudioComponent(MusicComponent);
UHyperTwistSpeechLibrary::UnregisterManagedAudioComponent(SpeechComponent); UHyperTwistSpeechLibrary::UnregisterManagedAudioComponent(SpeechComponent);
UHyperTwistSpeechLibrary::ApplyManagedAudioCategoryVolumes(1.0f, 1.0f, 1.0f);
return true; return true;
} }

View file

@ -0,0 +1,427 @@
#include "Misc/AutomationTest.h"
#include "HyperTwistSimulation/HyperTwistFourDimensionalSaveGame.h"
#include "HyperTwistSimulation/HyperTwistMelindaProjectionActor.h"
#include "HyperTwistSimulation/HyperTwistMelindaProjectionGameMode.h"
#include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionActor.h"
#include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionGameMode.h"
#include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionLibrary.h"
#if WITH_AUTOMATION_TESTS
namespace HyperTwistVirtualNxNxNxNRuntimeContractTestInternal
{
const EHyperTwistVirtual3333Axis Axes[] = {
EHyperTwistVirtual3333Axis::X,
EHyperTwistVirtual3333Axis::Y,
EHyperTwistVirtual3333Axis::Z,
EHyperTwistVirtual3333Axis::W
};
FString OrderLabel(const int32 Order)
{
return FString::Printf(TEXT("%dx%dx%dx%d"), Order, Order, Order, Order);
}
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistVirtualNxNxNxNStateAndProjectionContractTest,
"HyperTwist.CleanRoom.HactarCE.VirtualNxNxNxN.StateAndProjectionContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistVirtualNxNxNxNStateAndProjectionContractTest::RunTest(
const FString& Parameters
)
{
static_cast<void>(Parameters);
using namespace HyperTwistVirtualNxNxNxNRuntimeContractTestInternal;
for (int32 Order = 3; Order <= 6; ++Order)
{
const FString Label = OrderLabel(Order);
const int32 ExpectedPieceCount = Order * Order * Order * Order;
const int32 ExpectedVisiblePieceCount = Order * Order * Order;
const FHyperTwistVirtual3333RuntimeState SolvedState =
UHyperTwistVirtual3333ProjectionLibrary::BuildSolvedState(Order);
TestTrue(
*FString::Printf(TEXT("%s solved state must be structurally valid."), *Label),
SolvedState.IsStructurallyValid());
TestTrue(
*FString::Printf(TEXT("%s solved state must report solved."), *Label),
SolvedState.bIsSolved);
TestEqual(
*FString::Printf(TEXT("%s must own every exact runtime piece."), *Label),
SolvedState.PositionToPiece.Num(),
ExpectedPieceCount);
TestEqual(
*FString::Printf(TEXT("%s must own every piece orientation."), *Label),
SolvedState.PieceOrientations.Num(),
ExpectedPieceCount);
const TArray<int32> SliceCoordinates =
UHyperTwistVirtual3333ProjectionLibrary::GetSliceCoordinatesForOrder(Order);
TestEqual(
*FString::Printf(TEXT("%s must expose every layer."), *Label),
SliceCoordinates.Num(),
Order);
for (const EHyperTwistVirtual3333Axis VisibleAxis : Axes)
{
for (const int32 SliceCoordinate : SliceCoordinates)
{
const FHyperTwistVirtual3333ProjectionBuildResult ProjectionResult =
UHyperTwistVirtual3333ProjectionLibrary::BuildVisibleProjection(
SolvedState,
VisibleAxis,
SliceCoordinate);
TestTrue(
*FString::Printf(
TEXT("%s axis %d slice %d must project exactly."),
*Label,
static_cast<int32>(VisibleAxis),
SliceCoordinate),
ProjectionResult.bProjected && ProjectionResult.bExactProjection);
TestTrue(
*FString::Printf(
TEXT("%s axis %d slice %d projection must be valid."),
*Label,
static_cast<int32>(VisibleAxis),
SliceCoordinate),
ProjectionResult.Projection.IsStructurallyValid());
TestEqual(
*FString::Printf(
TEXT("%s axis %d slice %d must expose N^3 tesseracts."),
*Label,
static_cast<int32>(VisibleAxis),
SliceCoordinate),
ProjectionResult.Projection.Tesseracts.Num(),
ExpectedVisiblePieceCount);
int32 StickerCount = 0;
for (const FHyperTwistVirtual3333ProjectedTesseract& Tesseract :
ProjectionResult.Projection.Tesseracts)
{
for (const FHyperTwistVirtual3333ProjectedCell& Cell :
Tesseract.VisibleCells)
{
StickerCount += Cell.bHasSticker ? 1 : 0;
}
}
TestEqual(
*FString::Printf(
TEXT("%s solved projection must expose three N^2 positive stickers."),
*Label),
StickerCount,
3 * Order * Order);
}
}
}
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistVirtualNxNxNxNTurnIdentityContractTest,
"HyperTwist.CleanRoom.HactarCE.VirtualNxNxNxN.TurnIdentityContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistVirtualNxNxNxNTurnIdentityContractTest::RunTest(
const FString& Parameters
)
{
static_cast<void>(Parameters);
using namespace HyperTwistVirtualNxNxNxNRuntimeContractTestInternal;
for (int32 Order = 3; Order <= 6; ++Order)
{
const FString Label = OrderLabel(Order);
const FHyperTwistVirtual3333RuntimeState SolvedState =
UHyperTwistVirtual3333ProjectionLibrary::BuildSolvedState(Order);
const FString SerializedSolved =
UHyperTwistVirtual3333ProjectionLibrary::SerializeRuntimeStateToJson(SolvedState);
const TArray<int32> SliceCoordinates =
UHyperTwistVirtual3333ProjectionLibrary::GetSliceCoordinatesForOrder(Order);
for (const EHyperTwistVirtual3333Axis SliceAxis : Axes)
{
const TArray<EHyperTwistVirtual3333Axis> RotationAxes =
UHyperTwistVirtual3333ProjectionLibrary::
GetAvailableRotationAxesForSliceAxis(SliceAxis);
TestEqual(
*FString::Printf(TEXT("%s must expose three rotation axes."), *Label),
RotationAxes.Num(),
3);
for (const int32 SliceCoordinate : SliceCoordinates)
{
for (const EHyperTwistVirtual3333Axis RotationAxis : RotationAxes)
{
FHyperTwistVirtual3333SliceTurnRequest Request;
Request.SliceAxis = SliceAxis;
Request.SliceCoordinate = SliceCoordinate;
Request.RotationAxis = RotationAxis;
Request.Direction =
EHyperTwistVirtual3333TurnDirection::Clockwise;
const FHyperTwistVirtual3333SliceTurnResult TurnResult =
UHyperTwistVirtual3333ProjectionLibrary::ApplySliceTurn(
SolvedState,
Request);
TestTrue(
*FString::Printf(
TEXT("%s axis %d slice %d rotation %d must turn exactly."),
*Label,
static_cast<int32>(SliceAxis),
SliceCoordinate,
static_cast<int32>(RotationAxis)),
TurnResult.bApplied && TurnResult.bExactStateUpdate);
TestFalse(
*FString::Printf(
TEXT("%s quarter turn must leave solved state."),
*Label),
TurnResult.State.bIsSolved);
FHyperTwistVirtual3333SliceTurnRequest InverseRequest = Request;
InverseRequest.Direction =
EHyperTwistVirtual3333TurnDirection::CounterClockwise;
const FHyperTwistVirtual3333SliceTurnResult InverseResult =
UHyperTwistVirtual3333ProjectionLibrary::ApplySliceTurn(
TurnResult.State,
InverseRequest);
TestTrue(
*FString::Printf(TEXT("%s inverse turn must be exact."), *Label),
InverseResult.bApplied && InverseResult.bExactStateUpdate);
TestEqual(
*FString::Printf(
TEXT("%s turn plus inverse must recover exact identity."),
*Label),
UHyperTwistVirtual3333ProjectionLibrary::SerializeRuntimeStateToJson(
InverseResult.State),
SerializedSolved);
FHyperTwistVirtual3333RuntimeState FourTurnState = SolvedState;
for (int32 TurnIndex = 0; TurnIndex < 4; ++TurnIndex)
{
const FHyperTwistVirtual3333SliceTurnResult FourTurnResult =
UHyperTwistVirtual3333ProjectionLibrary::ApplySliceTurn(
FourTurnState,
Request);
TestTrue(
*FString::Printf(
TEXT("%s four-turn identity step must remain exact."),
*Label),
FourTurnResult.bApplied
&& FourTurnResult.bExactStateUpdate);
FourTurnState = FourTurnResult.State;
}
TestEqual(
*FString::Printf(
TEXT("%s four quarter turns must recover exact identity."),
*Label),
UHyperTwistVirtual3333ProjectionLibrary::SerializeRuntimeStateToJson(
FourTurnState),
SerializedSolved);
}
}
}
}
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistVirtualNxNxNxNStateIntegrityContractTest,
"HyperTwist.CleanRoom.HactarCE.VirtualNxNxNxN.StateIntegrityContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistVirtualNxNxNxNStateIntegrityContractTest::RunTest(
const FString& Parameters
)
{
static_cast<void>(Parameters);
for (int32 Order = 3; Order <= 6; ++Order)
{
FHyperTwistVirtual3333RuntimeState StaleSolvedFlag =
UHyperTwistVirtual3333ProjectionLibrary::BuildSolvedState(Order);
StaleSolvedFlag.bIsSolved = false;
TestFalse(
*FString::Printf(
TEXT("%dx state must reject a stale solved flag."),
Order),
StaleSolvedFlag.IsStructurallyValid());
FHyperTwistVirtual3333RuntimeState ReflectedOrientation =
UHyperTwistVirtual3333ProjectionLibrary::BuildSolvedState(Order);
ReflectedOrientation.PieceOrientations[0].Basis[0].bPositiveDirection = false;
ReflectedOrientation.bIsSolved = false;
TestFalse(
*FString::Printf(
TEXT("%dx state must reject an impossible reflected orientation."),
Order),
ReflectedOrientation.IsStructurallyValid());
const FHyperTwistPuzzleState SafeEnvelope =
UHyperTwistVirtual3333ProjectionLibrary::BuildPuzzleStateEnvelope(
ReflectedOrientation);
TestTrue(
*FString::Printf(
TEXT("%dx invalid input must produce a valid fallback envelope."),
Order),
SafeEnvelope.IsStructurallyValid());
TestTrue(
*FString::Printf(
TEXT("%dx invalid input must fall back atomically to order 3."),
Order),
SafeEnvelope.Definition.SizeVector.Num() == 4
&& SafeEnvelope.Definition.SizeVector[0] == 3
&& SafeEnvelope.Definition.SizeVector[1] == 3
&& SafeEnvelope.Definition.SizeVector[2] == 3
&& SafeEnvelope.Definition.SizeVector[3] == 3);
}
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistVirtualNxNxNxNScrambleContractTest,
"HyperTwist.CleanRoom.HactarCE.VirtualNxNxNxN.ScrambleContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistVirtualNxNxNxNScrambleContractTest::RunTest(
const FString& Parameters
)
{
static_cast<void>(Parameters);
for (int32 Order = 3; Order <= 6; ++Order)
{
const int32 MoveCount = Order * 10;
const int32 Seed = 2400 + Order;
const FHyperTwistVirtual3333ScrambleResult First =
UHyperTwistVirtual3333ProjectionLibrary::GenerateScramble(
Order,
MoveCount,
Seed);
const FHyperTwistVirtual3333ScrambleResult Repeat =
UHyperTwistVirtual3333ProjectionLibrary::GenerateScramble(
Order,
MoveCount,
Seed);
TestTrue(
*FString::Printf(TEXT("%dx scramble must generate exactly."), Order),
First.bGenerated
&& First.bExactStateUpdate
&& First.State.IsStructurallyValid());
TestFalse(
*FString::Printf(TEXT("%dx scramble must leave solved state."), Order),
First.State.bIsSolved);
TestEqual(
*FString::Printf(TEXT("%dx scramble must retain every generated move."), Order),
First.AppliedMoves.Num(),
MoveCount);
TestEqual(
*FString::Printf(TEXT("%dx scramble must be deterministic by seed."), Order),
UHyperTwistVirtual3333ProjectionLibrary::SerializeRuntimeStateToJson(
First.State),
UHyperTwistVirtual3333ProjectionLibrary::SerializeRuntimeStateToJson(
Repeat.State));
TestTrue(
*FString::Printf(TEXT("%dx scramble selection must remain valid."), Order),
First.FinalSelection.IsStructurallyValid());
const FHyperTwistVirtual3333ProjectionBuildResult Projection =
UHyperTwistVirtual3333ProjectionLibrary::BuildVisibleProjection(
First.State,
First.FinalSelection.SliceAxis,
First.FinalSelection.SliceCoordinate);
TestTrue(
*FString::Printf(TEXT("%dx scrambled state must project exactly."), Order),
Projection.bProjected
&& Projection.bExactProjection
&& Projection.Projection.IsStructurallyValid());
}
const FHyperTwistVirtual3333ScrambleResult Invalid =
UHyperTwistVirtual3333ProjectionLibrary::GenerateScramble(6, 0, 1);
TestFalse(
TEXT("A zero-move scramble must be rejected."),
Invalid.bGenerated);
TestTrue(
TEXT("A rejected scramble must explain its failure."),
!Invalid.Warnings.IsEmpty());
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistVirtualNxNxNxNPersistenceDefaultsContractTest,
"HyperTwist.CleanRoom.HactarCE.VirtualNxNxNxN.PersistenceDefaultsContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistVirtualNxNxNxNPersistenceDefaultsContractTest::RunTest(
const FString& Parameters
)
{
static_cast<void>(Parameters);
const UHyperTwistFourDimensionalSaveGame* SaveDefaults =
GetDefault<UHyperTwistFourDimensionalSaveGame>();
TestNotNull(
TEXT("The 4D save-game schema must be constructible."),
SaveDefaults);
TestEqual(
TEXT("The 4D save-game schema must retain its current version."),
SaveDefaults != nullptr ? SaveDefaults->SchemaVersion : 0,
1);
TestEqual(
TEXT("The 2x runtime must own a stable dedicated save slot."),
AHyperTwistMelindaProjectionActor::GetRuntimeSaveSlotName(),
FString(TEXT("HyperTwist_4D_2x2x2x2_v1")));
const AHyperTwistVirtual3333ProjectionActor* VirtualActorDefaults =
GetDefault<AHyperTwistVirtual3333ProjectionActor>();
TestEqual(
TEXT("The visible-slice runtime must derive a stable order-specific slot."),
VirtualActorDefaults != nullptr
? VirtualActorDefaults->GetRuntimeSaveSlotName()
: FString(),
FString(TEXT("HyperTwist_4D_3x3x3x3_v1")));
const AHyperTwistMelindaProjectionGameMode* CellFirstGameMode =
GetDefault<AHyperTwistMelindaProjectionGameMode>();
const AHyperTwistVirtual3333ProjectionGameMode* Order3GameMode =
GetDefault<AHyperTwistVirtual3333ProjectionGameMode>();
const AHyperTwistVirtual4444ProjectionGameMode* Order4GameMode =
GetDefault<AHyperTwistVirtual4444ProjectionGameMode>();
const AHyperTwistVirtual5555ProjectionGameMode* Order5GameMode =
GetDefault<AHyperTwistVirtual5555ProjectionGameMode>();
const AHyperTwistVirtual6666ProjectionGameMode* Order6GameMode =
GetDefault<AHyperTwistVirtual6666ProjectionGameMode>();
TestTrue(
TEXT("Every 4D game mode must restore a valid local session by default."),
CellFirstGameMode != nullptr
&& CellFirstGameMode->bLoadSavedStateOnBeginPlay
&& Order3GameMode != nullptr
&& Order3GameMode->bLoadSavedStateOnBeginPlay
&& Order4GameMode != nullptr
&& Order4GameMode->bLoadSavedStateOnBeginPlay
&& Order5GameMode != nullptr
&& Order5GameMode->bLoadSavedStateOnBeginPlay
&& Order6GameMode != nullptr
&& Order6GameMode->bLoadSavedStateOnBeginPlay);
TestTrue(
TEXT("The dedicated visible-slice game modes must retain orders 3 through 6."),
Order3GameMode != nullptr
&& Order3GameMode->PuzzleOrder == 3
&& Order4GameMode != nullptr
&& Order4GameMode->PuzzleOrder == 4
&& Order5GameMode != nullptr
&& Order5GameMode->PuzzleOrder == 5
&& Order6GameMode != nullptr
&& Order6GameMode->PuzzleOrder == 6);
return true;
}
#endif

View file

@ -15,6 +15,12 @@ public class UnrealHyperTwist : ModuleRules
new string[] { "Slate", "SlateCore", "HTTP", "WebBrowser", "WebBrowserWidget", "Projects", "RenderCore" } new string[] { "Slate", "SlateCore", "HTTP", "WebBrowser", "WebBrowserWidget", "Projects", "RenderCore" }
); );
if (Target.Platform == UnrealTargetPlatform.Win64)
{
PublicSystemLibraries.Add("Crypt32.lib");
PublicSystemLibraries.Add("Dsound.lib");
}
// External donor repo include paths // External donor repo include paths
string ExternalPath = Path.Combine(ModuleDirectory, "../../../.external/"); string ExternalPath = Path.Combine(ModuleDirectory, "../../../.external/");
string ThirdPartyPath = Path.Combine(ModuleDirectory, "../../../ThirdParty/"); string ThirdPartyPath = Path.Combine(ModuleDirectory, "../../../ThirdParty/");

View file

@ -654,7 +654,9 @@ Boundary-sensitive lanes already implemented/live:
### What this means ### What this means
- HyperTwist does **not** currently have dozens of donor repos already implemented in owned Unreal surfaces. - HyperTwist has `29` original live repository capabilities with owned
first-party realization routes, but they are not `29` standalone games and
they are not all direct-linked Unreal libraries.
- HyperTwist does **not** currently show live Unreal evidence for `cstimer`. - HyperTwist does **not** currently show live Unreal evidence for `cstimer`.
- HyperTwist **does** now show live Unreal evidence for `Hyperspeedcube`, `qbr`, `MagicTile`, `MagicCube5D`, and `Magic120Cell` through later landed `Phase 6R` packets. - HyperTwist **does** now show live Unreal evidence for `Hyperspeedcube`, `qbr`, `MagicTile`, `MagicCube5D`, and `Magic120Cell` through later landed `Phase 6R` packets.
- HyperTwist also has four later landed speech rows in the broader current - HyperTwist also has four later landed speech rows in the broader current
@ -667,6 +669,30 @@ Boundary-sensitive lanes already implemented/live:
Those are not the same thing. Those are not the same thing.
### 2026-07-23 executable capability proof
The current row-by-row authority is
`docs/ops/HYPERTWIST_ORIGINAL_29_LIVE_REPOSITORY_RUNTIME_CAPABILITY_PROOF_MATRIX_2026-07-23.md`.
It binds every one of the original live `29` rows to a legal lane, first-party
owner, reachable player/browser/operator surface, focused evidence, and
expected package artifact. The compiled catalog contains exactly `18`
permissive, `5` restrictive clean-room, and `6` boundary-sensitive rows and is
rendered in the native About & Credits page.
Do not conflate that set with the separate historical `29` selected Phase 1
wiring candidates. The latter now have an independent exact `1` through `29`
browser/sidecar/dependency/reference roster contract and remain governed by
`docs/ops/HYPERTWIST_UNWIRED_REPO_INVENTORY_AND_WIRING_SCHEDULE_2026-06-10.md`.
Release 19 closes the combined package gate for both count contracts. Its fresh
Shipping package passed all `9` native route smokes, verified the staged browser
runtime after a green `1,872`-module build, reopened and hashed the `167`-entry
ZIP, hash-matched the `173`-file Downloads delivery, and produced independent
semantic first-run proof from both the validation archive and exact delivered
executable. This package evidence upgrades realization proof; it does not turn
dependency, sidecar, QA, reference, or browser-support rows into standalone
native games.
## Working definitions ## Working definitions
### Landed / verified live ### Landed / verified live

View file

@ -0,0 +1,135 @@
# HyperTwist Alpha Test Release 19 Evidence
## Authority
Release 19 is the current Windows desktop Alpha authority for the combined
player-experience repair. Release 17 remains a rollback artifact and historical
proof source; it is not the current feature-complete delivery.
Current operator delivery:
- Folder:
`C:\Users\Anthracite Ace\Downloads\HyperTwist-Alpha-Test-20260723-R19`
- ZIP:
`C:\Users\Anthracite Ace\Downloads\HyperTwist-Alpha-Test-20260723-R19.zip`
- Executable:
`C:\Users\Anthracite Ace\Downloads\HyperTwist-Alpha-Test-20260723-R19\Windows\UnrealHyperTwist.exe`
- ZIP entries: `167`
- ZIP size: `1,093,537,260` bytes
- ZIP SHA-256:
`630963f96c681fc49130cbc56c6c850008d4cddda7e83f2ee9c077c52d7709a9`
The delivered folder was compared against the validated archive source by
hashing all `173` files. The comparison found `0` mismatches. The Downloads ZIP
size and SHA-256 also match the validation-source ZIP.
## Product Scope
This package contains the repaired first-run and player-facing shell, reliable
back/pause/exit navigation, Learning Studio, Settings, Account, Advanced, and
About/Credits surfaces. The native puzzle catalog exposes:
- Classic Cube
- Follow-Along training
- 4D `2x2x2x2`
- 4D `3x3x3x3`
- 4D `4x4x4x4`
- 4D `5x5x5x5`
- 4D `6x6x6x6`
- Magic 120-cell
- order-3 5D Cube
The common native puzzle controller also owns a fixed, session-only in-game
coach. It provides built-in offline help and optional provider-backed AI,
global STT dictation, and asynchronous TTS narration. The control center keeps
AI, STT, and TTS separately configurable, protects credentials for the current
Windows user, requires consent before cloud use, and does not make any provider
or API key a prerequisite for play.
The final speech repair specifically proves that a standalone global speech
session can open outside a Classic training run. This closes the prior defect
where the visible global assistant could reject dictation on 4D, 5D, or
120-cell routes.
## Build And Automation
- UE 5.7 incremental editor build after the global-speech repair:
`Result: Succeeded`, `7` actions, `60.93` seconds.
- `HyperTwist.PlayerExperience`: `5` succeeded, `0` failed, `0` warnings.
- Added proof:
`HyperTwist.PlayerExperience.GlobalAssistantSpeechSession`.
- Shipping build, cook, archive, package smoke, and ZIP export: `passed`.
- Browser shell verification: `passed`.
- Browser production build: `1,872` modules transformed.
- Exact solver table: `676,207,080` bytes, SHA-256
`dc6de3d909a37afe2ba7f1f978543a13eae215bd20d954d66813524a8ca20034`.
The automation authority is
`player_experience_automation_index.json`. Package, staged-browser,
solver-table, bootstrap, headless-launch, and ZIP authority is carried by the
four root-level package reports in this directory.
## Route Smoke Matrix
All `9` cooked native routes passed semantic Shipping smoke validation:
| Route | Expected runtime proof |
|---|---|
| Classic | exact map and valid Classic presentation |
| Follow-Along | registered route and exact map |
| 4D order 2 | `16` exact state pieces; `64` visible piece views |
| 4D order 3 | `81` exact state pieces; `27` visible piece views |
| 4D order 4 | `256` exact state pieces; `64` visible piece views |
| 4D order 5 | `625` exact state pieces; `125` visible piece views |
| 4D order 6 | `1,296` exact state pieces; `216` visible piece views |
| Magic 120-cell | exact route and representative `120`-center scene |
| order-3 5D Cube | exact route and `242`-element scene |
Every smoke report recorded `result: passed`, process-tree cleanup, no fatal
signature, no package-owned listening endpoint, and no trace-control listener.
The per-route reports live under `smoke/`.
The visual scene claims remain deliberately narrower than the simulation-state
claims. Magic 120-cell owns an exact `7,560`-facelet permutation and `7,440`
legal base moves, but the current scene visualizes representative cell centers.
The 5D runtime owns `243` cubies and `810` facelets while the scene renders its
bounded projected element set.
## Visible Desktop Authority
Two independent interactive desktop gates passed:
1. The package archive executable rendered a semantic-ready first-run frame.
2. The exact executable copied into Downloads rendered the same semantic-ready
first-run frame.
Both reports recorded:
- `result: visual-passed`
- `captureMethod: hyper-twist-semantic-ready-frame`
- `startupDiagnosticsResult: result=launch-menu-renderable`
- `windowClassName: UnrealWindow`
- client size `1280x720`
- screenshot SHA-256
`caa45f3d3104a8e5406f493a60c8a136d852bd35e0bd1c8dfbf79738c1343892`
- no package-owned or trace-control listening endpoint
The archive proof is under `archive_visual/`; the delivered-executable proof is
under `downloads_visual/`. The separate headless launch report records
`result=startup-recovered`. Headless recovery is useful runtime evidence, but
it is not substituted for the interactive `launch-menu-renderable` proof.
## Remaining External Gates
Release 19 proves code, package, archive, route, and visible first-run
authority. It does not fabricate evidence that requires unavailable external
conditions:
- A real provider-reachability, microphone-transcription, and audible TTS
smoke still requires an explicitly configured local or cloud service and
real audio hardware.
- Live XR headset-session and controller-input observation still requires a
connected headset and controllers.
- Installer/uninstaller, code signing, clean-machine prerequisites,
entitlement, and update-channel rollout remain release-engineering gates
above this portable Alpha package.

View file

@ -0,0 +1,8 @@
{
"startedAtUtc": "2026-07-23T17:02:28.7954212Z",
"user": "DESKTOP-KS3VGHU\\Anthracite Ace",
"processSessionId": 1,
"exitCode": 0,
"error": null,
"finishedAtUtc": "2026-07-23T17:02:44.8721804Z"
}

View file

@ -0,0 +1,6 @@
format=hypertwist-runtime-diagnostics/v1
session=20260723T170231Z
process_id=4532
command_line=-notraceserver -traceautostart=0 -windowed -ResX=1280 -ResY=720 -HyperTwistCaptureWhenReady -HyperTwistDiagnosticsLog=C:\HyperTwist_worktrees\phase10validate_packaged_alpha_release19_shipping_20260723\validation\visual\first-run-interactive\HyperTwist-window-20260723T170228Z.hyperdiagnostics.log
diagnostics_path=C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release19_shipping_20260723/validation/visual/first-run-interactive/HyperTwist-window-20260723T170228Z.hyperdiagnostics.log
[2026-07-23T17:02:31Z] [Process] HyperTwist runtime module initialized.

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View file

@ -0,0 +1,20 @@
2026-07-23T17:02:29.5385700Z launch-starting
2026-07-23T17:02:29.6267182Z launch-process-started
2026-07-23T17:02:30.7832358Z window-detected
2026-07-23T17:02:35.7850582Z initial-layout-settled
2026-07-23T17:02:35.7872661Z initial-window-refreshed
2026-07-23T17:02:35.8570439Z initial-bounds-measured
2026-07-23T17:02:36.2553775Z initial-window-focused
2026-07-23T17:02:36.2576786Z capture-window-ready
2026-07-23T17:02:36.2694212Z semantic-ready-capture-waiting
2026-07-23T17:02:39.1077578Z semantic-ready-capture-detected
2026-07-23T17:02:39.3187713Z capture-bitmap-created
2026-07-23T17:02:39.3198000Z bitmap-measurement-starting
2026-07-23T17:02:39.7730835Z bitmap-measurement-complete
2026-07-23T17:02:39.8419573Z screenshot-saved
2026-07-23T17:02:44.1305421Z visual-gate-passed
2026-07-23T17:02:44.1442211Z owned-process-scan-starting
2026-07-23T17:02:44.5587369Z owned-process-scan-complete
2026-07-23T17:02:44.7443640Z owned-process-cleanup-complete
2026-07-23T17:02:44.7469352Z report-write-starting
2026-07-23T17:02:44.8236344Z report-write-complete

View file

@ -0,0 +1,94 @@
{
"reportVersion": "ht-packaged-window-visual/v1",
"generatedAtUtc": "2026-07-23T17:02:29.4696122Z",
"launchStartedAtUtc": "2026-07-23T17:02:29.5423283Z",
"executablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\Windows\\UnrealHyperTwist.exe",
"executableSha256": "886f69c6a823770b3ed68db4fe3790a488124c99c98dbd41944a6bed5d634dc6",
"launchArguments": [
"-windowed",
"-ResX=1280",
"-ResY=720",
"-HyperTwistCaptureWhenReady",
"-HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\validation\\visual\\first-run-interactive\\HyperTwist-window-20260723T170228Z.hyperdiagnostics.log\""
],
"perMonitorV2DpiAware": true,
"processId": 4532,
"windowTitle": "UnrealHyperTwist ",
"windowClassName": "UnrealWindow",
"expectedWindowClassName": "UnrealWindow",
"clientBounds": {
"x": 368,
"y": 297,
"width": 1280,
"height": 720
},
"screenshotPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\validation\\visual\\first-run-interactive\\HyperTwist-window-20260723T170228Z.png",
"progressPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\validation\\visual\\first-run-interactive\\HyperTwist-window-20260723T170228Z.progress.log",
"screenshotSha256": "caa45f3d3104a8e5406f493a60c8a136d852bd35e0bd1c8dfbf79738c1343892",
"captureMethod": "hyper-twist-semantic-ready-frame",
"expectedSemanticReadyCaptureName": "first-run-launch-ready.png",
"semanticReadyCaptureSourcePath": "C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\Screenshots\\HyperTwistDiagnostics\\first-run-launch-ready.png",
"semanticReadyCaptureSourceSha256": "b3c3b38d34319717162ad7e58dda68ec330221257c782431853822227f0799e7",
"metrics": {
"sampleStride": 6,
"sampleCount": 25680,
"nonBlackRatio": 1,
"brightRatio": 0.92250778816199375,
"averageLuminance": 57.363220428345734,
"luminanceDeviation": 37.130366769121125,
"minimumLuminance": 27.8808,
"maximumLuminance": 249.1536,
"luminanceRange": 221.27280000000002,
"quantizedColorBucketCount": 92
},
"thresholds": {
"minimumNonBlackRatio": 0.02,
"minimumBrightRatio": 0.001,
"minimumLuminanceDeviation": 2.5,
"minimumLuminanceRange": 20
},
"interaction": {
"requested": false,
"normalizedX": -1,
"normalizedY": -1,
"screenX": null,
"screenY": null,
"clientX": null,
"clientY": null,
"injectionMethod": null,
"clicked": false,
"expectedStartupDiagnosticsResult": "launch-menu-renderable"
},
"startupDiagnosticsPath": "C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\Logs\\HyperTwistFirstRunLaunch-latest.log",
"startupDiagnosticsLastWriteUtc": "2026-07-23T17:02:32.9924351Z",
"startupDiagnosticsResult": "result=launch-menu-renderable",
"runtimeDiagnosticsPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\validation\\visual\\first-run-interactive\\HyperTwist-window-20260723T170228Z.hyperdiagnostics.log",
"runtimeDiagnosticsExists": true,
"runtimeDiagnosticsLines": [
"format=hypertwist-runtime-diagnostics/v1",
"session=20260723T170231Z",
"process_id=4532",
"command_line=-notraceserver -traceautostart=0 -windowed -ResX=1280 -ResY=720 -HyperTwistCaptureWhenReady -HyperTwistDiagnosticsLog=C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\validation\\visual\\first-run-interactive\\HyperTwist-window-20260723T170228Z.hyperdiagnostics.log",
"diagnostics_path=C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release19_shipping_20260723/validation/visual/first-run-interactive/HyperTwist-window-20260723T170228Z.hyperdiagnostics.log",
"[2026-07-23T17:02:31Z] [Process] HyperTwist runtime module initialized."
],
"processCommandLines": [
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\Windows\\UnrealHyperTwist.exe\" -windowed -ResX=1280 -ResY=720 -HyperTwistCaptureWhenReady -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\validation\\visual\\first-run-interactive\\HyperTwist-window-20260723T170228Z.hyperdiagnostics.log\" ",
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\Windows\\UnrealHyperTwist\\Binaries\\Win64\\UnrealHyperTwist-Win64-Shipping.exe\" UnrealHyperTwist -notraceserver -traceautostart=0 -windowed -ResX=1280 -ResY=720 -HyperTwistCaptureWhenReady -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\validation\\visual\\first-run-interactive\\HyperTwist-window-20260723T170228Z.hyperdiagnostics.log\" ",
"C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release19_shipping_20260723/Windows/Engine/Binaries/Win64/EpicWebHelper.exe --type=gpu-process --no-sandbox --use-adapter-luid=0,66319 --use-angle=d3d11 --start-stack-profiler --user-data-dir=\"C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\webcache_6613\" --locales-dir-path=C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release19_shipping_20260723/Windows/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources/locales --log-severity=warning --resources-dir-path=C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release19_shipping_20260723/Windows/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources --user-agent-product=\"UnrealHyperTwist/++UE5+Release-5.7-CL-51494982 UnrealEngine/5.7.4-51494982+++UE5+Release-5.7 Chrome/128.0.6613.138\" --gpu-preferences=UAAAAAAAAADgABAMAAAAAAAAAAAAAAAAAABgAAEAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAA --ipc-connection-timeout=60 --field-trial-handle=3036,i,8072072964101906737,4885358424669595112,262144 --variations-seed-version --enable-logging=handle --log-file=3096 --mojo-platform-channel-handle=3032 /prefetch:2",
"C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release19_shipping_20260723/Windows/Engine/Binaries/Win64/EpicWebHelper.exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --use-angle=d3d11 --start-stack-profiler --user-data-dir=\"C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\webcache_6613\" --locales-dir-path=C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release19_shipping_20260723/Windows/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources/locales --log-severity=warning --resources-dir-path=C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release19_shipping_20260723/Windows/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources --user-agent-product=\"UnrealHyperTwist/++UE5+Release-5.7-CL-51494982 UnrealEngine/5.7.4-51494982+++UE5+Release-5.7 Chrome/128.0.6613.138\" --ipc-connection-timeout=60 --field-trial-handle=3204,i,8072072964101906737,4885358424669595112,262144 --variations-seed-version --enable-logging=handle --log-file=3160 --mojo-platform-channel-handle=3148 /prefetch:11",
"C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release19_shipping_20260723/Windows/Engine/Binaries/Win64/EpicWebHelper.exe --type=utility --utility-sub-type=storage.mojom.StorageService --lang=en-US --service-sandbox-type=service --no-sandbox --use-angle=d3d11 --user-data-dir=\"C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\webcache_6613\" --locales-dir-path=C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release19_shipping_20260723/Windows/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources/locales --log-severity=warning --resources-dir-path=C:/HyperTwist_worktrees/phase10validate_packaged_alpha_release19_shipping_20260723/Windows/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources --user-agent-product=\"UnrealHyperTwist/++UE5+Release-5.7-CL-51494982 UnrealEngine/5.7.4-51494982+++UE5+Release-5.7 Chrome/128.0.6613.138\" --ipc-connection-timeout=60 --field-trial-handle=3320,i,8072072964101906737,4885358424669595112,262144 --variations-seed-version --enable-logging=handle --log-file=3340 --mojo-platform-channel-handle=3336 /prefetch:13"
],
"ownedListeningTcpEndpoints": [
],
"traceControlListenerLines": [
],
"traceControlListeningEndpoints": [
],
"focusMethod": "topmost-attempt-1",
"result": "visual-passed",
"error": null
}

View file

@ -0,0 +1,21 @@
{
"reportVersion": "ht-bootstrap-launch-arguments/v1",
"generatedAtUtc": "2026-07-23T16:36:41.1378396Z",
"executablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\Windows\\UnrealHyperTwist.exe",
"resourceType": 10,
"resourceId": 202,
"verifyOnly": false,
"expectedArguments": [
"UnrealHyperTwist",
"-notraceserver",
"-traceautostart=0"
],
"expectedResourceValue": "UnrealHyperTwist -notraceserver -traceautostart=0",
"originalResourceValue": "UnrealHyperTwist",
"finalResourceValue": "UnrealHyperTwist -notraceserver -traceautostart=0",
"originalSha256": "b7c268fc083a565b7c3c1e0a84db773c9714e4f23f14cca3bffb14145e48f88f",
"finalSha256": "886f69c6a823770b3ed68db4fe3790a488124c99c98dbd41944a6bed5d634dc6",
"changed": true,
"result": "passed",
"error": null
}

View file

@ -0,0 +1,71 @@
{
"reportVersion": "ht-desktop-package-launch-surface/v2",
"generatedAtUtc": "2026-07-23T16:41:44.1751081Z",
"packageRoot": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723",
"executablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\Windows\\UnrealHyperTwist.exe",
"mapUrl": "",
"runtimeLogPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\validation\\logs\\desktop-runtime.log",
"runtimeLogExists": false,
"runtimeDiagnosticsPath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\validation\\logs\\desktop-runtime.hyperdiagnostics.log",
"runtimeDiagnosticsExists": true,
"runtimeDiagnosticsInitialized": true,
"detectedFatalLogLines": [
],
"smokeSeconds": 30,
"resolution": {
"width": 1600,
"height": 900
},
"keepRunning": false,
"useNullRhi": true,
"noSound": true,
"requireStartupDiagnostics": true,
"expectedStartupDiagnosticsResult": "",
"startupDiagnosticsLogPath": "C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\Logs\\HyperTwistFirstRunLaunch-latest.log",
"startupDiagnosticsLogExists": true,
"startupDiagnosticsTail": [
"session=20260723T164149Z",
"world=L_HyperTwist_ClassicTraining",
"runtime_log=C:/Users/Anthracite Ace/AppData/Local/UnrealHyperTwist/Saved/Logs/HyperTwistRuntime-latest.log",
"latest_diagnostics_log=C:/Users/Anthracite Ace/AppData/Local/UnrealHyperTwist/Saved/Logs/HyperTwistFirstRunLaunch-latest.log",
"[2026-07-23T16:41:49Z] [BeginPlay] First-run controller booted on world \u0027L_HyperTwist_ClassicTraining\u0027.",
"[2026-07-23T16:41:49Z] [ApplyFirstRunInputMode] Applied game-and-UI input mode with unlocked cursor capture.",
"[2026-07-23T16:41:49Z] [ShowFirstRunLaunchMenu] Fell back to the native first-run widget class.",
"[2026-07-23T16:41:49Z] [ShowFirstRunLaunchMenu] Added the first-run launch widget to the viewport at z-order 100.",
"[2026-07-23T16:41:49Z] [ApplyFirstRunInputMode] Applied game-and-UI input mode with unlocked cursor capture.",
"[2026-07-23T16:41:51Z] [StartupRecovery] The first-run launch widget never produced a visible non-zero layout after 20 attempts (last size 0x0).",
"[2026-07-23T16:41:51Z] [StartupRecovery] Attempting fallback route \u0027classic-cube-training\u0027.",
"[2026-07-23T16:41:51Z] [OpenFirstRunRoute] Opening route \u0027classic-cube-training\u0027 (dedicated-map).",
"[2026-07-23T16:41:51Z] [OpenMapRoute] Opening map \u0027/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining\u0027 with options \u0027game=/Script/UnrealHyperTwist.HyperTwistClassicCubeGameMode\u0027.",
"result=startup-recovered",
"last_failure=The first-run launch widget never produced a visible non-zero layout after 20 attempts (last size 0x0)."
],
"startupDiagnosticsResult": "result=startup-recovered",
"result": "passed",
"processId": 2832,
"processIds": [
2832,
21200
],
"processCommandLines": [
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\Windows\\UnrealHyperTwist.exe\" -ResX=1600 -ResY=900 -windowed -log -FORCELOGFLUSH -abslog=C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\validation\\logs\\desktop-runtime.log -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\validation\\logs\\desktop-runtime.hyperdiagnostics.log\" -NullRHI -nosound ",
"\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\Windows\\UnrealHyperTwist\\Binaries\\Win64\\UnrealHyperTwist-Win64-Shipping.exe\" UnrealHyperTwist -notraceserver -traceautostart=0 -ResX=1600 -ResY=900 -windowed -log -FORCELOGFLUSH -abslog=C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\validation\\logs\\desktop-runtime.log -HyperTwistDiagnosticsLog=\"C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\validation\\logs\\desktop-runtime.hyperdiagnostics.log\" -NullRHI -nosound "
],
"ownedListeningTcpEndpoints": [
],
"traceControlListenerLines": [
],
"traceControlListeningEndpoints": [
],
"processStopped": true,
"stoppedProcessIds": [
2832,
21200
],
"exitCode": null,
"error": null
}

View file

@ -0,0 +1,149 @@
{
"reportVersion": "ht-packaged-build-zip-export/v2",
"generatedAtUtc": "2026-07-23T16:42:30.7970930Z",
"packageRoot": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723",
"packagedExecutablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_alpha_release19_shipping_20260723\\Windows\\UnrealHyperTwist.exe",
"destinationZipPath": "C:\\HyperTwist_worktrees\\HyperTwist-Alpha-Test-20260723-R19.zip",
"result": "passed",
"entryCount": 167,
"packagedExecutableEntry": "Windows\\UnrealHyperTwist.exe",
"requiredEntries": [
{
"kind": "launcher-executable",
"relativePath": "Windows/UnrealHyperTwist.exe",
"sizeBytes": 165376,
"sha256": "886f69c6a823770b3ed68db4fe3790a488124c99c98dbd41944a6bed5d634dc6"
},
{
"kind": "game-executable",
"relativePath": "Windows/UnrealHyperTwist/Binaries/Win64/UnrealHyperTwist-Win64-Shipping.exe",
"sizeBytes": 181121024,
"sha256": "e583531cbb21d22f6c223eb12bf0cf81f82798faff9e2fbec4ebeb953bf98bf3"
},
{
"kind": "runtime-dependency-tbbmalloc",
"relativePath": "Windows/UnrealHyperTwist/Binaries/Win64/tbbmalloc.dll",
"sizeBytes": 117688,
"sha256": "f81a11f2e6e93036bab4e51a1daab9940fb8a9a5ec0f508c4cb76d398e27c100"
},
{
"kind": "cooked-pak",
"relativePath": "Windows/UnrealHyperTwist/Content/Paks/UnrealHyperTwist-Windows.pak",
"sizeBytes": 11069853,
"sha256": "605e0b2ff7dd284762f4d4281945535f2a3ff86065904a2ce4f1c2837c72c900"
},
{
"kind": "cooked-utoc",
"relativePath": "Windows/UnrealHyperTwist/Content/Paks/UnrealHyperTwist-Windows.utoc",
"sizeBytes": 216013,
"sha256": "f317d5ac0c82871400697dc29b17925a49d55c7aa2608ddc4f91cfea896ec07e"
},
{
"kind": "cooked-ucas",
"relativePath": "Windows/UnrealHyperTwist/Content/Paks/UnrealHyperTwist-Windows.ucas",
"sizeBytes": 281222144,
"sha256": "d52a40ad8633351078b351f4bc1321d12ee12b397ad585f7a8a43418f54cfe62"
},
{
"kind": "solver-table",
"relativePath": "Windows/UnrealHyperTwist/Saved/twophase-ht.tbl",
"sizeBytes": 676207080,
"sha256": "dc6de3d909a37afe2ba7f1f978543a13eae215bd20d954d66813524a8ca20034"
},
{
"kind": "browser-shell-index",
"relativePath": "Windows/Content/Browser/index.html",
"sizeBytes": 7473,
"sha256": "00a730f4d8ee099d0d0d63a171b94194f9fe9c9c92de810a411050c0b260a562"
},
{
"kind": "browser-shell-bootstrap",
"relativePath": "Windows/Content/Browser/src/browser-runtime-bootstrap.js",
"sizeBytes": 3422,
"sha256": "e35963e1888deac61578141579c64e686c15cb964b2b4d8d8c080ed35b4ed00e"
},
{
"kind": "browser-shell-fallback",
"relativePath": "Windows/Content/Browser/src/browser-spatial-runtime-fallback.js",
"sizeBytes": 30201,
"sha256": "0089004d3aa86de1edefcc7c95ea0fc80c9352e49778bc384f6d676e8a9ad1a4"
},
{
"kind": "browser-shell-runtime",
"relativePath": "Windows/Content/Browser/dist/browser-spatial-runtime.js",
"sizeBytes": 30651,
"sha256": "0feb7fc24108b4804e4b801518ce91a0a5a9eb8f806edaedb7506d8a82bb283e"
}
],
"archivedRequiredEntries": [
{
"kind": "launcher-executable",
"relativePath": "Windows/UnrealHyperTwist.exe",
"sizeBytes": 165376,
"sha256": "886f69c6a823770b3ed68db4fe3790a488124c99c98dbd41944a6bed5d634dc6"
},
{
"kind": "game-executable",
"relativePath": "Windows/UnrealHyperTwist/Binaries/Win64/UnrealHyperTwist-Win64-Shipping.exe",
"sizeBytes": 181121024,
"sha256": "e583531cbb21d22f6c223eb12bf0cf81f82798faff9e2fbec4ebeb953bf98bf3"
},
{
"kind": "runtime-dependency-tbbmalloc",
"relativePath": "Windows/UnrealHyperTwist/Binaries/Win64/tbbmalloc.dll",
"sizeBytes": 117688,
"sha256": "f81a11f2e6e93036bab4e51a1daab9940fb8a9a5ec0f508c4cb76d398e27c100"
},
{
"kind": "cooked-pak",
"relativePath": "Windows/UnrealHyperTwist/Content/Paks/UnrealHyperTwist-Windows.pak",
"sizeBytes": 11069853,
"sha256": "605e0b2ff7dd284762f4d4281945535f2a3ff86065904a2ce4f1c2837c72c900"
},
{
"kind": "cooked-utoc",
"relativePath": "Windows/UnrealHyperTwist/Content/Paks/UnrealHyperTwist-Windows.utoc",
"sizeBytes": 216013,
"sha256": "f317d5ac0c82871400697dc29b17925a49d55c7aa2608ddc4f91cfea896ec07e"
},
{
"kind": "cooked-ucas",
"relativePath": "Windows/UnrealHyperTwist/Content/Paks/UnrealHyperTwist-Windows.ucas",
"sizeBytes": 281222144,
"sha256": "d52a40ad8633351078b351f4bc1321d12ee12b397ad585f7a8a43418f54cfe62"
},
{
"kind": "solver-table",
"relativePath": "Windows/UnrealHyperTwist/Saved/twophase-ht.tbl",
"sizeBytes": 676207080,
"sha256": "dc6de3d909a37afe2ba7f1f978543a13eae215bd20d954d66813524a8ca20034"
},
{
"kind": "browser-shell-index",
"relativePath": "Windows/Content/Browser/index.html",
"sizeBytes": 7473,
"sha256": "00a730f4d8ee099d0d0d63a171b94194f9fe9c9c92de810a411050c0b260a562"
},
{
"kind": "browser-shell-bootstrap",
"relativePath": "Windows/Content/Browser/src/browser-runtime-bootstrap.js",
"sizeBytes": 3422,
"sha256": "e35963e1888deac61578141579c64e686c15cb964b2b4d8d8c080ed35b4ed00e"
},
{
"kind": "browser-shell-fallback",
"relativePath": "Windows/Content/Browser/src/browser-spatial-runtime-fallback.js",
"sizeBytes": 30201,
"sha256": "0089004d3aa86de1edefcc7c95ea0fc80c9352e49778bc384f6d676e8a9ad1a4"
},
{
"kind": "browser-shell-runtime",
"relativePath": "Windows/Content/Browser/dist/browser-spatial-runtime.js",
"sizeBytes": 30651,
"sha256": "0feb7fc24108b4804e4b801518ce91a0a5a9eb8f806edaedb7506d8a82bb283e"
}
],
"zipSizeBytes": 1093537260,
"zipSha256": "630963f96c681fc49130cbc56c6c850008d4cddda7e83f2ee9c077c52d7709a9",
"error": null
}

View file

@ -0,0 +1,8 @@
{
"startedAtUtc": "2026-07-23T17:05:58.3722812Z",
"user": "DESKTOP-KS3VGHU\\Anthracite Ace",
"processSessionId": 1,
"exitCode": 0,
"error": null,
"finishedAtUtc": "2026-07-23T17:06:16.5278987Z"
}

View file

@ -0,0 +1,6 @@
format=hypertwist-runtime-diagnostics/v1
session=20260723T170602Z
process_id=6072
command_line=-notraceserver -traceautostart=0 -windowed -ResX=1280 -ResY=720 -HyperTwistCaptureWhenReady -HyperTwistDiagnosticsLog="C:\Users\Anthracite Ace\Downloads\HyperTwist-Alpha-Test-20260723-R19\validation\visual\downloads-first-run-interactive\HyperTwist-window-20260723T170558Z.hyperdiagnostics.log"
diagnostics_path=C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723-R19/validation/visual/downloads-first-run-interactive/HyperTwist-window-20260723T170558Z.hyperdiagnostics.log
[2026-07-23T17:06:02Z] [Process] HyperTwist runtime module initialized.

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View file

@ -0,0 +1,20 @@
2026-07-23T17:05:59.8464744Z launch-starting
2026-07-23T17:05:59.9462741Z launch-process-started
2026-07-23T17:06:01.8621577Z window-detected
2026-07-23T17:06:06.8665874Z initial-layout-settled
2026-07-23T17:06:06.8910426Z initial-window-refreshed
2026-07-23T17:06:07.1125047Z initial-bounds-measured
2026-07-23T17:06:07.6207108Z initial-window-focused
2026-07-23T17:06:07.6218960Z capture-window-ready
2026-07-23T17:06:07.6350098Z semantic-ready-capture-waiting
2026-07-23T17:06:09.5241510Z semantic-ready-capture-detected
2026-07-23T17:06:09.7736401Z capture-bitmap-created
2026-07-23T17:06:09.7746576Z bitmap-measurement-starting
2026-07-23T17:06:10.3549959Z bitmap-measurement-complete
2026-07-23T17:06:10.4351766Z screenshot-saved
2026-07-23T17:06:15.7318347Z visual-gate-passed
2026-07-23T17:06:15.7515834Z owned-process-scan-starting
2026-07-23T17:06:16.2630629Z owned-process-scan-complete
2026-07-23T17:06:16.4219555Z owned-process-cleanup-complete
2026-07-23T17:06:16.4230820Z report-write-starting
2026-07-23T17:06:16.5087831Z report-write-complete

View file

@ -0,0 +1,94 @@
{
"reportVersion": "ht-packaged-window-visual/v1",
"generatedAtUtc": "2026-07-23T17:05:59.7460003Z",
"launchStartedAtUtc": "2026-07-23T17:05:59.8512744Z",
"executablePath": "C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723-R19\\Windows\\UnrealHyperTwist.exe",
"executableSha256": "886f69c6a823770b3ed68db4fe3790a488124c99c98dbd41944a6bed5d634dc6",
"launchArguments": [
"-windowed",
"-ResX=1280",
"-ResY=720",
"-HyperTwistCaptureWhenReady",
"-HyperTwistDiagnosticsLog=\"C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723-R19\\validation\\visual\\downloads-first-run-interactive\\HyperTwist-window-20260723T170558Z.hyperdiagnostics.log\""
],
"perMonitorV2DpiAware": true,
"processId": 6072,
"windowTitle": "UnrealHyperTwist ",
"windowClassName": "UnrealWindow",
"expectedWindowClassName": "UnrealWindow",
"clientBounds": {
"x": 368,
"y": 297,
"width": 1280,
"height": 720
},
"screenshotPath": "C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723-R19\\validation\\visual\\downloads-first-run-interactive\\HyperTwist-window-20260723T170558Z.png",
"progressPath": "C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723-R19\\validation\\visual\\downloads-first-run-interactive\\HyperTwist-window-20260723T170558Z.progress.log",
"screenshotSha256": "caa45f3d3104a8e5406f493a60c8a136d852bd35e0bd1c8dfbf79738c1343892",
"captureMethod": "hyper-twist-semantic-ready-frame",
"expectedSemanticReadyCaptureName": "first-run-launch-ready.png",
"semanticReadyCaptureSourcePath": "C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\Screenshots\\HyperTwistDiagnostics\\first-run-launch-ready.png",
"semanticReadyCaptureSourceSha256": "b3c3b38d34319717162ad7e58dda68ec330221257c782431853822227f0799e7",
"metrics": {
"sampleStride": 6,
"sampleCount": 25680,
"nonBlackRatio": 1,
"brightRatio": 0.92250778816199375,
"averageLuminance": 57.363220428345734,
"luminanceDeviation": 37.130366769121125,
"minimumLuminance": 27.8808,
"maximumLuminance": 249.1536,
"luminanceRange": 221.27280000000002,
"quantizedColorBucketCount": 92
},
"thresholds": {
"minimumNonBlackRatio": 0.02,
"minimumBrightRatio": 0.001,
"minimumLuminanceDeviation": 2.5,
"minimumLuminanceRange": 20
},
"interaction": {
"requested": false,
"normalizedX": -1,
"normalizedY": -1,
"screenX": null,
"screenY": null,
"clientX": null,
"clientY": null,
"injectionMethod": null,
"clicked": false,
"expectedStartupDiagnosticsResult": "launch-menu-renderable"
},
"startupDiagnosticsPath": "C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\Logs\\HyperTwistFirstRunLaunch-latest.log",
"startupDiagnosticsLastWriteUtc": "2026-07-23T17:06:03.5694606Z",
"startupDiagnosticsResult": "result=launch-menu-renderable",
"runtimeDiagnosticsPath": "C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723-R19\\validation\\visual\\downloads-first-run-interactive\\HyperTwist-window-20260723T170558Z.hyperdiagnostics.log",
"runtimeDiagnosticsExists": true,
"runtimeDiagnosticsLines": [
"format=hypertwist-runtime-diagnostics/v1",
"session=20260723T170602Z",
"process_id=6072",
"command_line=-notraceserver -traceautostart=0 -windowed -ResX=1280 -ResY=720 -HyperTwistCaptureWhenReady -HyperTwistDiagnosticsLog=\"C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723-R19\\validation\\visual\\downloads-first-run-interactive\\HyperTwist-window-20260723T170558Z.hyperdiagnostics.log\"",
"diagnostics_path=C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723-R19/validation/visual/downloads-first-run-interactive/HyperTwist-window-20260723T170558Z.hyperdiagnostics.log",
"[2026-07-23T17:06:02Z] [Process] HyperTwist runtime module initialized."
],
"processCommandLines": [
"\"C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723-R19\\Windows\\UnrealHyperTwist.exe\" -windowed -ResX=1280 -ResY=720 -HyperTwistCaptureWhenReady -HyperTwistDiagnosticsLog=\"C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723-R19\\validation\\visual\\downloads-first-run-interactive\\HyperTwist-window-20260723T170558Z.hyperdiagnostics.log\" ",
"\"C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723-R19\\Windows\\UnrealHyperTwist\\Binaries\\Win64\\UnrealHyperTwist-Win64-Shipping.exe\" UnrealHyperTwist -notraceserver -traceautostart=0 -windowed -ResX=1280 -ResY=720 -HyperTwistCaptureWhenReady -HyperTwistDiagnosticsLog=\"C:\\Users\\Anthracite Ace\\Downloads\\HyperTwist-Alpha-Test-20260723-R19\\validation\\visual\\downloads-first-run-interactive\\HyperTwist-window-20260723T170558Z.hyperdiagnostics.log\" ",
"\"C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723-R19/Windows/Engine/Binaries/Win64/EpicWebHelper.exe\" --type=gpu-process --no-sandbox --use-adapter-luid=0,66319 --use-angle=d3d11 --start-stack-profiler --user-data-dir=\"C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\webcache_6613\" --locales-dir-path=\"C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723-R19/Windows/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources/locales\" --log-severity=warning --resources-dir-path=\"C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723-R19/Windows/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources\" --user-agent-product=\"UnrealHyperTwist/++UE5+Release-5.7-CL-51494982 UnrealEngine/5.7.4-51494982+++UE5+Release-5.7 Chrome/128.0.6613.138\" --gpu-preferences=UAAAAAAAAADgABAMAAAAAAAAAAAAAAAAAABgAAEAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAA --ipc-connection-timeout=60 --field-trial-handle=3080,i,12991372510356583921,16920954278907190757,262144 --variations-seed-version --enable-logging=handle --log-file=3180 --mojo-platform-channel-handle=3044 /prefetch:2",
"\"C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723-R19/Windows/Engine/Binaries/Win64/EpicWebHelper.exe\" --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --use-angle=d3d11 --start-stack-profiler --user-data-dir=\"C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\webcache_6613\" --locales-dir-path=\"C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723-R19/Windows/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources/locales\" --log-severity=warning --resources-dir-path=\"C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723-R19/Windows/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources\" --user-agent-product=\"UnrealHyperTwist/++UE5+Release-5.7-CL-51494982 UnrealEngine/5.7.4-51494982+++UE5+Release-5.7 Chrome/128.0.6613.138\" --ipc-connection-timeout=60 --field-trial-handle=3188,i,12991372510356583921,16920954278907190757,262144 --variations-seed-version --enable-logging=handle --log-file=3232 --mojo-platform-channel-handle=3224 /prefetch:11",
"\"C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723-R19/Windows/Engine/Binaries/Win64/EpicWebHelper.exe\" --type=utility --utility-sub-type=storage.mojom.StorageService --lang=en-US --service-sandbox-type=service --no-sandbox --use-angle=d3d11 --user-data-dir=\"C:\\Users\\Anthracite Ace\\AppData\\Local\\UnrealHyperTwist\\Saved\\webcache_6613\" --locales-dir-path=\"C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723-R19/Windows/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources/locales\" --log-severity=warning --resources-dir-path=\"C:/Users/Anthracite Ace/Downloads/HyperTwist-Alpha-Test-20260723-R19/Windows/Engine/Binaries/ThirdParty/CEF3/Win64/128.4.13+ge76af7e+chromium-128.0.6613.138/Resources\" --user-agent-product=\"UnrealHyperTwist/++UE5+Release-5.7-CL-51494982 UnrealEngine/5.7.4-51494982+++UE5+Release-5.7 Chrome/128.0.6613.138\" --ipc-connection-timeout=60 --field-trial-handle=3300,i,12991372510356583921,16920954278907190757,262144 --variations-seed-version --enable-logging=handle --log-file=3352 --mojo-platform-channel-handle=3348 /prefetch:13"
],
"ownedListeningTcpEndpoints": [
],
"traceControlListenerLines": [
],
"traceControlListeningEndpoints": [
],
"focusMethod": "topmost-attempt-1",
"result": "visual-passed",
"error": null
}

Some files were not shown because too many files have changed in this diff Show more