feat: ship HyperTwist release 19 player experience
This commit is contained in:
parent
49422acf8f
commit
02ad8fa781
129 changed files with 54134 additions and 772 deletions
16
Content/Browser/package-lock.json
generated
16
Content/Browser/package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,10 +59,7 @@ void AHyperTwistCoachDashboardPlayerController::ApplyDashboardInputMode()
|
||||||
|
|
||||||
if (bUseGameAndUiInputMode)
|
if (bUseGameAndUiInputMode)
|
||||||
{
|
{
|
||||||
FInputModeGameAndUI InputMode;
|
ApplyGameAndUiInputMode();
|
||||||
InputMode.SetHideCursorDuringCapture(false);
|
|
||||||
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
|
|
||||||
SetInputMode(InputMode);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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>();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -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");
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -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();
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
|
@ -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."),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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/");
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
135
docs/generated/alpha_test_20260723_release19/README.md
Normal file
135
docs/generated/alpha_test_20260723_release19/README.md
Normal 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.
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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 |
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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 |
|
|
@ -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
|
||||||
|
|
@ -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
Loading…
Add table
Reference in a new issue