Add centralized input control management and mission brief improvements
All checks were successful
Build / build (push) Successful in 1m31s

- Create InputControlManager singleton for centralized ship controls and pointer selection management
  - Last-wins behavior for state changes
  - Mutually exclusive ship controls and VR pointer selection
  - Observable events for state changes with requester tracking
  - Enables debugging and prevents conflicts between UI components

- Refactor Ship class to use InputControlManager
  - Remove disableControls() and enableControls() methods
  - Register input systems with InputControlManager on initialization
  - Simplify control state management throughout ship lifecycle

- Update StatusScreen to use InputControlManager
  - Remove manual pointer selection enable/disable methods
  - Delegate control management to InputControlManager
  - Automatic laser pointer enabling when screen shows

- Update Level1 mission brief to use InputControlManager
  - Consistent control management for mission brief display
  - Proper pointer selection during mission brief interaction

- Fix controller input trigger blocking bug
  - Triggers now properly blocked when controls disabled
  - Prevents shooting when status screen or mission brief is visible
  - Only X-button (status screen toggle) allowed when disabled

- Add START MISSION button to mission brief
  - Replace "Pull trigger to start" text with clickable button
  - Green styled button matching StatusScreen design
  - Works with VR laser pointer interaction
  - Trigger pull still works as fallback

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-11-21 17:25:11 -06:00
parent eccf101b73
commit 1422c5b926
10 changed files with 293 additions and 99 deletions

13
package-lock.json generated
View File

@ -18,6 +18,7 @@
"@babylonjs/procedural-textures": "8.36.1", "@babylonjs/procedural-textures": "8.36.1",
"@babylonjs/serializers": "8.36.1", "@babylonjs/serializers": "8.36.1",
"@newrelic/browser-agent": "^1.302.0", "@newrelic/browser-agent": "^1.302.0",
"loglevel": "^1.9.2",
"openai": "4.52.3", "openai": "4.52.3",
"svelte-spa-router": "^4.0.1" "svelte-spa-router": "^4.0.1"
}, },
@ -1431,6 +1432,18 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}, },
"node_modules/loglevel": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
"integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
"engines": {
"node": ">= 0.6.0"
},
"funding": {
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/loglevel"
}
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",

View File

@ -26,6 +26,7 @@
"@babylonjs/serializers": "8.36.1", "@babylonjs/serializers": "8.36.1",
"@newrelic/browser-agent": "^1.302.0", "@newrelic/browser-agent": "^1.302.0",
"openai": "4.52.3", "openai": "4.52.3",
"loglevel": "^1.9.2",
"svelte-spa-router": "^4.0.1" "svelte-spa-router": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -18,6 +18,7 @@ import {PhysicsRecorder} from "../replay/recording/physicsRecorder";
import {getAnalytics} from "../analytics"; import {getAnalytics} from "../analytics";
import {MissionBrief} from "../ui/hud/missionBrief"; import {MissionBrief} from "../ui/hud/missionBrief";
import {LevelRegistry, LevelDirectoryEntry} from "./storage/levelRegistry"; import {LevelRegistry, LevelDirectoryEntry} from "./storage/levelRegistry";
import { InputControlManager } from "../ship/input/inputControlManager";
export class Level1 implements Level { export class Level1 implements Level {
private _ship: Ship; private _ship: Ship;
@ -162,12 +163,13 @@ export class Level1 implements Level {
// Disable ship controls while mission brief is showing // Disable ship controls while mission brief is showing
debugLog('[Level1] Disabling ship controls for mission brief'); debugLog('[Level1] Disabling ship controls for mission brief');
this._ship.disableControls(); const inputManager = InputControlManager.getInstance();
inputManager.disableShipControls("MissionBrief");
// Show mission brief with trigger observable // Show mission brief with trigger observable
this._missionBrief.show(this._levelConfig, directoryEntry, this._ship.onMissionBriefTriggerObservable, () => { this._missionBrief.show(this._levelConfig, directoryEntry, this._ship.onMissionBriefTriggerObservable, () => {
debugLog('[Level1] Mission brief dismissed - enabling controls and starting game'); debugLog('[Level1] Mission brief dismissed - enabling controls and starting game');
this._ship.enableControls(); inputManager.enableShipControls("MissionBrief");
this.startGameplay(); this.startGameplay();
}); });
} }
@ -343,10 +345,10 @@ export class Level1 implements Level {
// Load background music before marking as ready // Load background music before marking as ready
if (this._audioEngine) { if (this._audioEngine) {
setLoadingMessage("Loading background music..."); setLoadingMessage("Loading background music...");
/*this._backgroundMusic = await this._audioEngine.createSoundAsync("background", "/song1.mp3", { this._backgroundMusic = await this._audioEngine.createSoundAsync("background", "/song1.mp3", {
loop: true, loop: true,
volume: 0.5 volume: 0.5
});*/ });
debugLog('Background music loaded successfully'); debugLog('Background music loaded successfully');
} }

View File

@ -42,6 +42,7 @@ import App from './components/layouts/App.svelte';
import { BrowserAgent } from '@newrelic/browser-agent/loaders/browser-agent' import { BrowserAgent } from '@newrelic/browser-agent/loaders/browser-agent'
import { AnalyticsService } from './analytics/analyticsService'; import { AnalyticsService } from './analytics/analyticsService';
import { NewRelicAdapter } from './analytics/adapters/newRelicAdapter'; import { NewRelicAdapter } from './analytics/adapters/newRelicAdapter';
import { InputControlManager } from './ship/input/inputControlManager';
// Populate using values from NerdGraph // Populate using values from NerdGraph
const options = { const options = {
@ -524,16 +525,19 @@ export class Main {
debugLog(WebXRFeaturesManager.GetAvailableFeatures()); debugLog(WebXRFeaturesManager.GetAvailableFeatures());
debugLog("WebXR initialized successfully"); debugLog("WebXR initialized successfully");
// Store pointer selection feature reference and detach it initially // Register pointer selection feature with InputControlManager
if (DefaultScene.XR) { if (DefaultScene.XR) {
const pointerFeature = DefaultScene.XR.baseExperience.featuresManager.getEnabledFeature( const pointerFeature = DefaultScene.XR.baseExperience.featuresManager.getEnabledFeature(
"xr-controller-pointer-selection" "xr-controller-pointer-selection"
); );
if (pointerFeature) { if (pointerFeature) {
// Store for backward compatibility (can be removed later if not needed)
(DefaultScene.XR as any).pointerSelectionFeature = pointerFeature; (DefaultScene.XR as any).pointerSelectionFeature = pointerFeature;
// Detach immediately to prevent interaction during gameplay
pointerFeature.detach(); // Register with InputControlManager
debugLog("Pointer selection feature stored and detached"); const inputManager = InputControlManager.getInstance();
inputManager.registerPointerFeature(pointerFeature);
debugLog("Pointer selection feature registered with InputControlManager");
} }
// Hide Discord widget when entering VR, show when exiting // Hide Discord widget when entering VR, show when exiting

View File

@ -274,11 +274,15 @@ export class ControllerInput {
return; return;
} }
if (!this._enabled && controllerEvent.type === "button" && if (!this._enabled && controllerEvent.type === "button") {
!(controllerEvent.component.id === "x-button" && controllerEvent.hand === "left") && // Only allow X-button on left controller (for status screen toggle)
controllerEvent.component.type !== "trigger") { if (controllerEvent.component.id === "x-button" && controllerEvent.hand === "left") {
// Allow this through
} else {
// Block all other buttons including triggers
return; return;
} }
}
if (controllerEvent.type === "thumbstick") { if (controllerEvent.type === "thumbstick") {
// Store raw stick values (mapping will be applied in getInputState()) // Store raw stick values (mapping will be applied in getInputState())
@ -295,6 +299,9 @@ export class ControllerInput {
if (controllerEvent.type === "button") { if (controllerEvent.type === "button") {
if (controllerEvent.component.type === "trigger") { if (controllerEvent.component.type === "trigger") {
if (!this._enabled) {
return;
}
if (controllerEvent.value > 0.9 && !this._shooting) { if (controllerEvent.value > 0.9 && !this._shooting) {
this._shooting = true; this._shooting = true;
this._onShootObservable.notifyObservers(); this._onShootObservable.notifyObservers();

View File

@ -0,0 +1,208 @@
import { Observable } from "@babylonjs/core";
import { KeyboardInput } from "./keyboardInput";
import { ControllerInput } from "./controllerInput";
import debugLog from "../../core/debug";
/**
* State change event emitted when ship controls or pointer selection state changes
*/
export interface InputControlStateChange {
shipControlsEnabled: boolean;
pointerSelectionEnabled: boolean;
requester: string; // e.g., "StatusScreen", "MissionBrief", "Level1"
timestamp: number;
}
/**
* Centralized manager for ship controls and pointer selection
* Ensures ship controls and pointer selection are mutually exclusive
* Emits events when state changes for debugging and analytics
*
* Design principles:
* - Last-wins behavior: Most recent state change takes precedence
* - Mutually exclusive: Ship controls and pointer selection are inverses
* - Event-driven: Emits observables when state changes
* - Centralized: Single source of truth via singleton pattern
*/
export class InputControlManager {
private static _instance: InputControlManager | null = null;
private _shipControlsEnabled: boolean = true;
private _pointerSelectionEnabled: boolean = false;
// Observable for state changes
private _onStateChangedObservable: Observable<InputControlStateChange> = new Observable();
// References to systems we control
private _keyboardInput: KeyboardInput | null = null;
private _controllerInput: ControllerInput | null = null;
private _xrPointerFeature: any = null;
/**
* Private constructor for singleton pattern
*/
private constructor() {
debugLog('[InputControlManager] Instance created');
}
/**
* Get singleton instance
*/
public static getInstance(): InputControlManager {
if (!InputControlManager._instance) {
InputControlManager._instance = new InputControlManager();
}
return InputControlManager._instance;
}
/**
* Register input systems (called by Ship during initialization)
*/
public registerInputSystems(keyboard: KeyboardInput | null, controller: ControllerInput | null): void {
debugLog('[InputControlManager] Registering input systems', { keyboard: !!keyboard, controller: !!controller });
this._keyboardInput = keyboard;
this._controllerInput = controller;
}
/**
* Register XR pointer feature (called by main.ts during XR setup)
*/
public registerPointerFeature(pointerFeature: any): void {
debugLog('[InputControlManager] Registering XR pointer feature');
this._xrPointerFeature = pointerFeature;
// Apply current state to the newly registered pointer feature
this.updatePointerFeature();
}
/**
* Enable ship controls, disable pointer selection
*/
public enableShipControls(requester: string): void {
debugLog(`[InputControlManager] Enabling ship controls (requester: ${requester})`);
// Update state
this._shipControlsEnabled = true;
this._pointerSelectionEnabled = false;
// Apply to input systems
if (this._keyboardInput) {
this._keyboardInput.setEnabled(true);
}
if (this._controllerInput) {
this._controllerInput.setEnabled(true);
}
// Disable pointer selection
this.updatePointerFeature();
// Emit state change event
this.emitStateChange(requester);
}
/**
* Disable ship controls, enable pointer selection
*/
public disableShipControls(requester: string): void {
debugLog(`[InputControlManager] Disabling ship controls (requester: ${requester})`);
// Update state
this._shipControlsEnabled = false;
this._pointerSelectionEnabled = true;
// Apply to input systems
if (this._keyboardInput) {
this._keyboardInput.setEnabled(false);
}
if (this._controllerInput) {
this._controllerInput.setEnabled(false);
}
// Enable pointer selection
this.updatePointerFeature();
// Emit state change event
this.emitStateChange(requester);
}
/**
* Update XR pointer feature state based on current settings
*/
private updatePointerFeature(): void {
if (!this._xrPointerFeature) {
return;
}
try {
if (this._pointerSelectionEnabled) {
// Enable pointer selection (attach feature)
this._xrPointerFeature.attach();
debugLog('[InputControlManager] Pointer selection enabled');
} else {
// Disable pointer selection (detach feature)
this._xrPointerFeature.detach();
debugLog('[InputControlManager] Pointer selection disabled');
}
} catch (error) {
console.warn('[InputControlManager] Failed to update pointer feature:', error);
}
}
/**
* Emit state change event
*/
private emitStateChange(requester: string): void {
const stateChange: InputControlStateChange = {
shipControlsEnabled: this._shipControlsEnabled,
pointerSelectionEnabled: this._pointerSelectionEnabled,
requester: requester,
timestamp: Date.now()
};
this._onStateChangedObservable.notifyObservers(stateChange);
debugLog('[InputControlManager] State changed:', stateChange);
}
/**
* Get current ship controls enabled state
*/
public get shipControlsEnabled(): boolean {
return this._shipControlsEnabled;
}
/**
* Get current pointer selection enabled state
*/
public get pointerSelectionEnabled(): boolean {
return this._pointerSelectionEnabled;
}
/**
* Get observable for state changes
*/
public get onStateChanged(): Observable<InputControlStateChange> {
return this._onStateChangedObservable;
}
/**
* Cleanup (for testing or hot reload)
*/
public dispose(): void {
debugLog('[InputControlManager] Disposing');
this._onStateChangedObservable.clear();
this._keyboardInput = null;
this._controllerInput = null;
this._xrPointerFeature = null;
}
/**
* Reset singleton instance (for testing)
*/
public static reset(): void {
if (InputControlManager._instance) {
InputControlManager._instance.dispose();
InputControlManager._instance = null;
}
}
}

View File

@ -29,6 +29,7 @@ import { WeaponSystem } from "./weaponSystem";
import { StatusScreen } from "../ui/hud/statusScreen"; import { StatusScreen } from "../ui/hud/statusScreen";
import { GameStats } from "../game/gameStats"; import { GameStats } from "../game/gameStats";
import { getAnalytics } from "../analytics"; import { getAnalytics } from "../analytics";
import { InputControlManager } from "./input/inputControlManager";
export class Ship { export class Ship {
private _ship: TransformNode; private _ship: TransformNode;
@ -124,6 +125,7 @@ export class Ship {
this._ship = new TransformNode("shipBase", DefaultScene.MainScene); this._ship = new TransformNode("shipBase", DefaultScene.MainScene);
const data = await loadAsset("ship.glb"); const data = await loadAsset("ship.glb");
this._ship = data.container.transformNodes[0]; this._ship = data.container.transformNodes[0];
// this._ship.id = "Ship"; // Set ID so mission brief can find it
this._ship.position.y = 5; this._ship.position.y = 5;
// Create physics if enabled // Create physics if enabled
@ -221,6 +223,10 @@ export class Ship {
this._controllerInput = new ControllerInput(); this._controllerInput = new ControllerInput();
// Register input systems with InputControlManager
const inputManager = InputControlManager.getInstance();
inputManager.registerInputSystems(this._keyboardInput, this._controllerInput);
// Wire up shooting events // Wire up shooting events
this._keyboardInput.onShootObservable.add(() => { this._keyboardInput.onShootObservable.add(() => {
this.handleShoot(); this.handleShoot();
@ -234,15 +240,12 @@ export class Ship {
this._controllerInput.onStatusScreenToggleObservable.add(() => { this._controllerInput.onStatusScreenToggleObservable.add(() => {
if (this._statusScreen) { if (this._statusScreen) {
if (this._statusScreen.isVisible) { if (this._statusScreen.isVisible) {
// Hide status screen and re-enable controls // Hide status screen - InputControlManager will handle control re-enabling
this._statusScreen.hide(); this._statusScreen.hide();
this._keyboardInput?.setEnabled(true);
this._controllerInput?.setEnabled(true);
} else { } else {
// Show status screen (manual pause, not game end) and disable controls // Show status screen (manual pause, not game end)
// InputControlManager will handle control disabling
this._statusScreen.show(false); this._statusScreen.show(false);
this._keyboardInput?.setEnabled(false);
this._controllerInput?.setEnabled(false);
} }
} }
}); });
@ -392,10 +395,9 @@ export class Ship {
* Handle resume button click from status screen * Handle resume button click from status screen
*/ */
private handleResume(): void { private handleResume(): void {
debugLog('Resume button clicked - hiding status screen and re-enabling controls'); debugLog('Resume button clicked - hiding status screen');
// InputControlManager will handle re-enabling controls when status screen hides
this._statusScreen.hide(); this._statusScreen.hide();
this._keyboardInput?.setEnabled(true);
this._controllerInput?.setEnabled(true);
} }
/** /**
@ -439,8 +441,7 @@ export class Ship {
if (!this._isInLandingZone && hull < 0.01) { if (!this._isInLandingZone && hull < 0.01) {
debugLog('Game end condition met: Hull critical outside landing zone'); debugLog('Game end condition met: Hull critical outside landing zone');
this._statusScreen.show(true, false); // Game ended, not victory this._statusScreen.show(true, false); // Game ended, not victory
this._keyboardInput?.setEnabled(false); // InputControlManager will handle disabling controls when status screen shows
this._controllerInput?.setEnabled(false);
this._statusScreenAutoShown = true; this._statusScreenAutoShown = true;
return; return;
} }
@ -449,8 +450,7 @@ export class Ship {
if (!this._isInLandingZone && fuel < 0.01 && totalVelocity < 5) { if (!this._isInLandingZone && fuel < 0.01 && totalVelocity < 5) {
debugLog('Game end condition met: Stranded (no fuel, low velocity)'); debugLog('Game end condition met: Stranded (no fuel, low velocity)');
this._statusScreen.show(true, false); // Game ended, not victory this._statusScreen.show(true, false); // Game ended, not victory
this._keyboardInput?.setEnabled(false); // InputControlManager will handle disabling controls when status screen shows
this._controllerInput?.setEnabled(false);
this._statusScreenAutoShown = true; this._statusScreenAutoShown = true;
return; return;
} }
@ -459,8 +459,7 @@ export class Ship {
if (asteroidsRemaining <= 0 && this._isInLandingZone) { if (asteroidsRemaining <= 0 && this._isInLandingZone) {
debugLog('Game end condition met: Victory (all asteroids destroyed)'); debugLog('Game end condition met: Victory (all asteroids destroyed)');
this._statusScreen.show(true, true); // Game ended, VICTORY! this._statusScreen.show(true, true); // Game ended, VICTORY!
this._keyboardInput?.setEnabled(false); // InputControlManager will handle disabling controls when status screen shows
this._controllerInput?.setEnabled(false);
this._statusScreenAutoShown = true; this._statusScreenAutoShown = true;
return; return;
} }
@ -628,33 +627,6 @@ export class Ship {
} }
} }
/**
* Disable ship controls (for mission brief, etc.)
*/
public disableControls(): void {
debugLog('[Ship] Disabling controls');
this._controlsEnabled = false;
if (this._controllerInput) {
this._controllerInput.setEnabled(false);
}
if (this._keyboardInput) {
this._keyboardInput.setEnabled(false);
}
}
/**
* Enable ship controls
*/
public enableControls(): void {
debugLog('[Ship] Enabling controls');
this._controlsEnabled = true;
if (this._controllerInput) {
this._controllerInput.setEnabled(true);
}
if (this._keyboardInput) {
this._keyboardInput.setEnabled(true);
}
}
/** /**
* Dispose of ship resources * Dispose of ship resources

View File

@ -1,5 +1,6 @@
import { import {
AdvancedDynamicTexture, AdvancedDynamicTexture,
Button,
Control, Control,
Rectangle, Rectangle,
StackPanel, StackPanel,
@ -199,16 +200,30 @@ export class MissionBrief {
spacer3.thickness = 0; spacer3.thickness = 0;
contentPanel.addControl(spacer3); contentPanel.addControl(spacer3);
const startText = new TextBlock("startTExt"); // START button
startText.text = 'Pull trigger to start'; const startButton = Button.CreateSimpleButton("startButton", "START MISSION");
startText.color = "#00aa00"; startButton.width = "400px";
startText.fontSize = 48; startButton.height = "60px";
startText.textWrapping = true; startButton.color = "white";
startText.height = "80px"; startButton.background = "#00ff88";
startText.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER; startButton.cornerRadius = 10;
startText.textVerticalAlignment = Control.VERTICAL_ALIGNMENT_TOP; startButton.thickness = 0;
startText.paddingLeft = "20px"; startButton.fontSize = "36px";
contentPanel.addControl(startText); startButton.fontWeight = "bold";
startButton.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
startButton.onPointerClickObservable.add(() => {
debugLog('[MissionBrief] START button clicked - dismissing mission brief');
this.hide();
if (this._onStartCallback) {
this._onStartCallback();
}
// Remove trigger observer when button is clicked
if (this._triggerObserver) {
triggerObservable.remove(this._triggerObserver);
this._triggerObserver = null;
}
});
contentPanel.addControl(startButton);
// Show the container // Show the container
this._container.isVisible = true; this._container.isVisible = true;

View File

@ -19,6 +19,7 @@ import { DefaultScene } from "../../core/defaultScene";
import { ProgressionManager } from "../../game/progression"; import { ProgressionManager } from "../../game/progression";
import { AuthService } from "../../services/authService"; import { AuthService } from "../../services/authService";
import { FacebookShare, ShareData } from "../../services/facebookShare"; import { FacebookShare, ShareData } from "../../services/facebookShare";
import { InputControlManager } from "../../ship/input/inputControlManager";
/** /**
* Status screen that displays game statistics * Status screen that displays game statistics
@ -300,37 +301,6 @@ export class StatusScreen {
} }
} }
/**
* Enable VR controller picking for button interaction
*/
private enablePointerSelection(): void {
// Get the stored pointer selection feature
const pointerFeature = (DefaultScene.XR as any)?.pointerSelectionFeature;
if (pointerFeature && DefaultScene.XR?.baseExperience?.state === 2) { // WebXRState.IN_XR = 2
try {
// Attach the feature to enable pointer interaction
pointerFeature.attach();
} catch (error) {
console.warn('Failed to attach pointer selection:', error);
}
}
}
/**
* Disable VR controller picking
*/
private disablePointerSelection(): void {
// Get the stored pointer selection feature
const pointerFeature = (DefaultScene.XR as any)?.pointerSelectionFeature;
if (pointerFeature) {
try {
// Detach the feature to disable pointer interaction
pointerFeature.detach();
} catch (error) {
console.warn('Failed to detach pointer selection:', error);
}
}
}
/** /**
* Set the current level name for progression tracking * Set the current level name for progression tracking
@ -394,8 +364,9 @@ export class StatusScreen {
} }
} }
// Enable pointer selection for button interaction // Disable ship controls and enable pointer selection via InputControlManager
this.enablePointerSelection(); const inputManager = InputControlManager.getInstance();
inputManager.disableShipControls("StatusScreen");
// Update statistics before showing // Update statistics before showing
this.updateStatistics(); this.updateStatistics();
@ -426,8 +397,9 @@ export class StatusScreen {
return; return;
} }
// Disable pointer selection when hiding // Re-enable ship controls and disable pointer selection via InputControlManager
this.disablePointerSelection(); const inputManager = InputControlManager.getInstance();
inputManager.enableShipControls("StatusScreen");
this._screenMesh.setEnabled(false); this._screenMesh.setEnabled(false);
this._isVisible = false; this._isVisible = false;

BIN
themes/default/base2.blend1 Normal file

Binary file not shown.