632 lines
19 KiB
TypeScript
632 lines
19 KiB
TypeScript
// Content/Debug/Components/AC_DebugHUD.ts
|
|
|
|
import type {AC_Movement} from "../../Movement/Components/AC_Movement.js";
|
|
import type {Float, Integer} from "../../types.js";
|
|
import type {S_DebugPage} from "../Structs/S_DebugPage.js";
|
|
import {AddToArray, CreateWidget, GetFromArray, IsValid, Print, SetArrayElem} from "../../functions.js";
|
|
import {E_DebugPageID} from "../Enums/E_DebugPageID.js";
|
|
import {E_DebugUpdateFunction} from "../Enums/E_DebugUpdateFunction.js";
|
|
import {WBP_DebugHUD} from "../UI/WBP_DebugHUD.js";
|
|
import type {S_DebugSettings} from "../Structs/S_DebugSettings.js";
|
|
import {E_DebugMode} from "../Enums/E_DebugMode.js";
|
|
import {ESlateVisibility} from "../../enums.js";
|
|
|
|
/**
|
|
* Debug HUD Controller Component
|
|
* Manages debug information display system for deterministic movement
|
|
* Provides real-time performance monitoring and parameter visualization
|
|
* Part of Stage 2: Debug HUD system implementation
|
|
*/
|
|
export class AC_DebugHUD {
|
|
// Category: "DebugConfig"
|
|
// Instance Editable: true
|
|
/**
|
|
* Debug system configuration settings
|
|
* Controls visibility, update frequency, and current page
|
|
* Instance editable for designer customization
|
|
*/
|
|
public DebugSettings: S_DebugSettings = {
|
|
CurrentMode: E_DebugMode.Visible,
|
|
CurrentPageIndex: 0,
|
|
ShowVisualDebug: false,
|
|
UpdateFrequency: 0.0,
|
|
}
|
|
|
|
// Category: "DebugState"
|
|
/**
|
|
* System initialization state flag
|
|
* Set to true after successful InitializeDebugHUD call
|
|
*/
|
|
private IsInitialized: boolean = false;
|
|
|
|
// Category: "DebugState"
|
|
/**
|
|
* Timestamp of last HUD update (seconds)
|
|
* Used for update frequency control
|
|
*/
|
|
private LastUpdateTime: Float = 0;
|
|
|
|
// Category: "DebugState"
|
|
/**
|
|
* Current frame counter for performance tracking
|
|
* Incremented each update cycle
|
|
*/
|
|
private FrameCounter: Float = 0;
|
|
|
|
// Category: "DebugState"
|
|
/**
|
|
* Current frames per second calculation
|
|
* Calculated as 1.0 / DeltaTime
|
|
*/
|
|
private FPS: Float = 0;
|
|
|
|
// Category: "DebugState"
|
|
/**
|
|
* Reference to movement component being debugged
|
|
* Set during initialization, used for accessing movement data
|
|
*/
|
|
private MovementComponent: AC_Movement | null = null;
|
|
|
|
// Category: "Widget Control"
|
|
/**
|
|
* Debug HUD widget instance
|
|
* Created during initialization, manages UI display
|
|
*/
|
|
private DebugWidget: WBP_DebugHUD | null = null;
|
|
|
|
// Category: "Page System"
|
|
/**
|
|
* Array of registered debug pages
|
|
* Contains all available pages with their data and update functions
|
|
*/
|
|
private DebugPages: S_DebugPage[] = [];
|
|
|
|
// Category: "HUD Control"
|
|
// Pure: true
|
|
/**
|
|
* Check if debug HUD should be visible
|
|
* @returns True if system is initialized and mode is visible
|
|
* @pure Function has no side effects
|
|
*/
|
|
private ShouldShowDebugHUD() {
|
|
return this.IsInitialized && this.DebugSettings.CurrentMode === E_DebugMode.Visible;
|
|
}
|
|
|
|
// Category: "HUD Control"
|
|
// Pure: true
|
|
/**
|
|
* Check if debug HUD should update this frame
|
|
* @param CurrentTime - Current game time in seconds
|
|
* @returns True if enough time has passed since last update
|
|
* @pure Function has no side effects
|
|
* @example
|
|
* // Update every frame (UpdateFrequency = 0)
|
|
* ShouldUpdateDebugHUD(gameTime) // returns true
|
|
* // Update at 30Hz (UpdateFrequency = 30)
|
|
* ShouldUpdateDebugHUD(gameTime) // returns true every 1/30 seconds
|
|
*/
|
|
private ShouldUpdateDebugHUD(CurrentTime: Float): boolean {
|
|
if (this.DebugSettings.UpdateFrequency <= 0) {
|
|
return true; // Update every frame
|
|
} else {
|
|
return (CurrentTime - this.LastUpdateTime) >= (1.0 / this.DebugSettings.UpdateFrequency);
|
|
}
|
|
}
|
|
|
|
// Category: "Page Management"
|
|
/**
|
|
* Register or update a debug page in the system
|
|
* @param PageData - Page configuration and content data
|
|
* @private Internal page management method
|
|
* @example
|
|
* // Register movement constants page
|
|
* RegisterDebugPage({
|
|
* PageID: E_DebugPageID.MovementInfo,
|
|
* Title: "Movement Constants",
|
|
* Content: "",
|
|
* IsVisible: true,
|
|
* UpdateFunction: E_DebugUpdateFunction.UpdateMovementPage
|
|
* })
|
|
*/
|
|
private RegisterDebugPage(PageData: S_DebugPage): void {
|
|
let existingIndex: Integer = -1;
|
|
|
|
this.DebugPages.forEach((page, index) => {
|
|
if (page.PageID === PageData.PageID) {
|
|
this.DebugPages[index] = PageData;
|
|
return;
|
|
}
|
|
})
|
|
|
|
if (existingIndex >= 0) {
|
|
SetArrayElem(this.DebugPages, existingIndex, PageData);
|
|
} else{
|
|
AddToArray(this.DebugPages, PageData);
|
|
}
|
|
}
|
|
|
|
// Category: "Page Management"
|
|
// Pure: true
|
|
/**
|
|
* Get all currently visible debug pages
|
|
* @returns Array of visible pages only
|
|
* @pure Function has no side effects
|
|
*/
|
|
private GetVisiblePages(): S_DebugPage[] {
|
|
const filteredPages: S_DebugPage[] = [];
|
|
|
|
this.DebugPages.forEach((page) => {
|
|
if (page.IsVisible) {
|
|
AddToArray(filteredPages, page);
|
|
}
|
|
})
|
|
|
|
return filteredPages;
|
|
}
|
|
|
|
// Category: "Page Management"
|
|
/**
|
|
* Get currently selected debug page
|
|
* @returns Object with page data and validity flag
|
|
* @pure Function has no side effects
|
|
*/
|
|
private GetCurrentPage(): {Page: S_DebugPage | null, IsFound: boolean} {
|
|
const length = this.GetVisiblePages().length;
|
|
|
|
if (!((length === 0) || (this.DebugSettings.CurrentPageIndex >= length))) {
|
|
return {Page: GetFromArray(this.DebugPages, this.DebugSettings.CurrentPageIndex), IsFound: true};
|
|
} else {
|
|
return {Page: null, IsFound: false};
|
|
}
|
|
}
|
|
|
|
// Category: "Navigation"
|
|
/**
|
|
* Toggle debug HUD visibility between visible and hidden
|
|
* @public User input handler for F1 key or similar
|
|
*/
|
|
public ToggleDebugHUD() {
|
|
this.DebugSettings.CurrentMode = this.DebugSettings.CurrentMode === E_DebugMode.Visible ? E_DebugMode.Hidden : E_DebugMode.Visible;
|
|
}
|
|
|
|
// Category: "Navigation"
|
|
/**
|
|
* Navigate to previous debug page
|
|
* Wraps around to last page if at beginning
|
|
* @public User input handler for PageUp key
|
|
*/
|
|
public PreviousPage() {
|
|
const length = this.GetVisiblePages().length;
|
|
|
|
if (length > 1) {
|
|
const currentPage = this.DebugSettings.CurrentPageIndex;
|
|
|
|
this.DebugSettings.CurrentPageIndex = currentPage - 1 < 0 ? length - 1 : currentPage - 1;
|
|
}
|
|
}
|
|
|
|
// Category: "Navigation"
|
|
/**
|
|
* Navigate to next debug page
|
|
* Wraps around to first page if at end
|
|
* @public User input handler for PageDown key
|
|
*/
|
|
public NextPage() {
|
|
const length = this.GetVisiblePages().length;
|
|
|
|
if (length > 1) {
|
|
this.DebugSettings.CurrentPageIndex = (this.DebugSettings.CurrentPageIndex + 1) % length;
|
|
}
|
|
}
|
|
|
|
// Category: "Visual Debug"
|
|
/**
|
|
* Toggle visual debug rendering (collision shapes, rays, etc.)
|
|
* @public User input handler for visual debug toggle
|
|
*/
|
|
public ToggleVisualDebug() {
|
|
this.DebugSettings.ShowVisualDebug = !this.DebugSettings.ShowVisualDebug;
|
|
}
|
|
|
|
// Category: "Page Updates"
|
|
/**
|
|
* Update content of currently selected page
|
|
* Calls appropriate update function based on page type
|
|
* @private Internal update system method
|
|
*/
|
|
private UpdateCurrentPage() {
|
|
const {Page, IsFound}: {Page: S_DebugPage, IsFound: boolean} = this.GetCurrentPage();
|
|
let CurrentPage = Page;
|
|
|
|
if (IsFound) {
|
|
switch (CurrentPage.UpdateFunction) {
|
|
case E_DebugUpdateFunction.UpdateMovementPage:
|
|
CurrentPage = this.UpdateMovementPage(CurrentPage);
|
|
break;
|
|
case E_DebugUpdateFunction.UpdateSurfacePage:
|
|
CurrentPage = this.UpdateSurfacePage(CurrentPage);
|
|
break;
|
|
case E_DebugUpdateFunction.UpdatePerformancePage:
|
|
CurrentPage = this.UpdatePerformancePage(CurrentPage);
|
|
break;
|
|
}
|
|
|
|
SetArrayElem(this.DebugPages, this.DebugSettings.CurrentPageIndex, CurrentPage);
|
|
this.UpdateWidgetPage();
|
|
}
|
|
}
|
|
|
|
// Category: "Page Updates"
|
|
/**
|
|
* Update movement constants page content
|
|
* @param Page - Page structure to update
|
|
* @returns Updated page with current movement data
|
|
* @private Page-specific update method
|
|
*/
|
|
private UpdateMovementPage(Page: S_DebugPage): S_DebugPage {
|
|
if (IsValid(this.MovementComponent)) {
|
|
return {
|
|
PageID: Page.PageID,
|
|
Title: Page.Title,
|
|
Content: `Max Speed: ${this.MovementComponent.MovementConstants.MaxSpeed}\n` +
|
|
`Acceleration: ${this.MovementComponent.MovementConstants.Acceleration}\n` +
|
|
`Friction: ${this.MovementComponent.MovementConstants.Friction}\n` +
|
|
`Gravity: ${this.MovementComponent.MovementConstants.Gravity}`,
|
|
IsVisible: Page.IsVisible,
|
|
UpdateFunction: Page.UpdateFunction
|
|
}
|
|
} else {
|
|
return {
|
|
PageID: Page.PageID,
|
|
Title: Page.Title,
|
|
Content: 'Movement Component Not Found',
|
|
IsVisible: Page.IsVisible,
|
|
UpdateFunction: Page.UpdateFunction
|
|
}
|
|
}
|
|
}
|
|
|
|
// Category: "Page Updates"
|
|
/**
|
|
* Update surface classification page content
|
|
* @param Page - Page structure to update
|
|
* @returns Updated page with current surface angle thresholds
|
|
* @private Page-specific update method
|
|
*/
|
|
private UpdateSurfacePage(Page: S_DebugPage): S_DebugPage {
|
|
if (IsValid(this.MovementComponent)) {
|
|
return {
|
|
PageID: Page.PageID,
|
|
Title: Page.Title,
|
|
Content: `Walkable: ≤${this.MovementComponent.AngleThresholdsDegrees.Walkable}°\n` +
|
|
`Steep Slope: ${this.MovementComponent.AngleThresholdsDegrees.Walkable}°-${this.MovementComponent.AngleThresholdsDegrees.SteepSlope}°\n` +
|
|
`Wall: ${this.MovementComponent.AngleThresholdsDegrees.SteepSlope}°-${this.MovementComponent.AngleThresholdsDegrees.Wall}°\n` +
|
|
`Ceiling: >${this.MovementComponent.AngleThresholdsDegrees.Wall}°`,
|
|
IsVisible: Page.IsVisible,
|
|
UpdateFunction: Page.UpdateFunction
|
|
}
|
|
} else {
|
|
return {
|
|
PageID: Page.PageID,
|
|
Title: Page.Title,
|
|
Content: 'Movement Component Not Found',
|
|
IsVisible: Page.IsVisible,
|
|
UpdateFunction: Page.UpdateFunction
|
|
}
|
|
}
|
|
}
|
|
|
|
// Category: "Page Updates"
|
|
/**
|
|
* Update performance metrics page content
|
|
* @param Page - Page structure to update
|
|
* @returns Updated page with current performance data
|
|
* @private Page-specific update method
|
|
*/
|
|
private UpdatePerformancePage(Page: S_DebugPage): S_DebugPage {
|
|
if (IsValid(this.MovementComponent)) {
|
|
return {
|
|
PageID: Page.PageID,
|
|
Title: Page.Title,
|
|
Content: `Frame: ${this.FrameCounter}\n` +
|
|
`FPS: ${this.FPS}\n` +
|
|
`Update Rate: ${this.DebugSettings.UpdateFrequency <= 0 ? 'Every Frame' : (this.DebugSettings.UpdateFrequency + ' Hz')}\n` +
|
|
`ActivePages: ${this.GetVisiblePages().length}`,
|
|
IsVisible: Page.IsVisible,
|
|
UpdateFunction: Page.UpdateFunction
|
|
}
|
|
} else {
|
|
return {
|
|
PageID: Page.PageID,
|
|
Title: Page.Title,
|
|
Content: 'Movement Component Not Found',
|
|
IsVisible: Page.IsVisible,
|
|
UpdateFunction: Page.UpdateFunction
|
|
}
|
|
}
|
|
}
|
|
|
|
// Category: "Widget Control"
|
|
/**
|
|
* Create debug widget instance and add to viewport
|
|
* @private Internal widget management method
|
|
*/
|
|
private CreateDebugWidget() {
|
|
this.DebugWidget = CreateWidget<WBP_DebugHUD>(new WBP_DebugHUD);
|
|
this.DebugWidget.MovementComponent = this.MovementComponent;
|
|
|
|
if (IsValid(this.DebugWidget)) {
|
|
this.DebugWidget.AddToViewport();
|
|
} else {
|
|
Print('Failed to create debug widget');
|
|
}
|
|
}
|
|
|
|
// Category: "Widget Control"
|
|
/**
|
|
* Update widget visibility based on current debug mode
|
|
* @private Internal widget management method
|
|
*/
|
|
private UpdateWidgetVisibility() {
|
|
if (IsValid(this.DebugWidget)) {
|
|
this.DebugWidget.SetVisibility(this.ShouldShowDebugHUD() ? ESlateVisibility.Visible : ESlateVisibility.Hidden);
|
|
}
|
|
}
|
|
|
|
// Category: "Widget Communication"
|
|
/**
|
|
* Send current page data to debug widget for display
|
|
* @private Internal widget communication method
|
|
*/
|
|
private UpdateWidgetPage() {
|
|
const {Page, IsFound} = this.GetCurrentPage();
|
|
|
|
if (IsFound) {
|
|
this.DebugWidget.SetHeaderText(Page.Title);
|
|
this.DebugWidget.SetContentText(Page.Content);
|
|
this.DebugWidget.SetNavigationText(`Page ${this.DebugSettings.CurrentPageIndex + 1}/${this.GetVisiblePages().length} | PageUp/PageDown - Navigate`);
|
|
}
|
|
}
|
|
|
|
// Category: "HUD Rendering"
|
|
/**
|
|
* Main update loop for debug HUD system
|
|
* Called every frame from game loop
|
|
* @param CurrentTime - Current game time in seconds
|
|
* @param DeltaTime - Time since last frame in seconds
|
|
* @public Called by BP_MainCharacter or game framework
|
|
*/
|
|
public UpdateHUD(CurrentTime: Float, DeltaTime: Float) {
|
|
if (this.IsInitialized && this.ShouldUpdateDebugHUD(CurrentTime)) {
|
|
this.FrameCounter++;
|
|
this.FPS = DeltaTime > 0.0 ? 1.0 / DeltaTime : 0.0;
|
|
this.LastUpdateTime = CurrentTime;
|
|
|
|
if (this.ShouldShowDebugHUD()) {
|
|
this.UpdateCurrentPage();
|
|
this.UpdateWidgetPage();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Category: "System Setup"
|
|
/**
|
|
* Register default debug pages (Movement, Surface, Performance)
|
|
* @private Internal system setup method
|
|
*/
|
|
private RegisterDefaultPages() {
|
|
this.RegisterDebugPage({
|
|
PageID: E_DebugPageID.MovementInfo,
|
|
Title: "Movement Constants",
|
|
Content: '',
|
|
IsVisible: true,
|
|
UpdateFunction: E_DebugUpdateFunction.UpdateMovementPage
|
|
});
|
|
|
|
this.RegisterDebugPage({
|
|
PageID: E_DebugPageID.SurfaceInfo,
|
|
Title: "Surface Classification",
|
|
Content: '',
|
|
IsVisible: true,
|
|
UpdateFunction: E_DebugUpdateFunction.UpdateSurfacePage
|
|
});
|
|
|
|
this.RegisterDebugPage({
|
|
PageID: E_DebugPageID.PerformanceInfo,
|
|
Title: "Performance Metrics",
|
|
Content: '',
|
|
IsVisible: true,
|
|
UpdateFunction: E_DebugUpdateFunction.UpdatePerformancePage
|
|
});
|
|
}
|
|
|
|
// Category: "Testing"
|
|
/**
|
|
* Test basic debug system functionality
|
|
* @returns True if all basic tests pass
|
|
* @private Internal testing method
|
|
*/
|
|
private TestDebugSystem() {
|
|
Print('=== DEBUG SYSTEM TESTS ===');
|
|
|
|
if (this.IsInitialized) {
|
|
Print('✅ System initialized');
|
|
|
|
if (this.DebugPages.length === 3) {
|
|
Print('✅ All pages registered');
|
|
|
|
if (IsValid(this.MovementComponent)) {
|
|
Print('✅ Movement component valid');
|
|
|
|
if (IsValid(this.DebugWidget)) {
|
|
Print('✅ Debug widget created');
|
|
Print('✅ ALL TESTS PASSED');
|
|
return true;
|
|
} else {
|
|
Print('❌ Debug widget not created');
|
|
return false;
|
|
}
|
|
} else {
|
|
Print('❌ Movement component not valid');
|
|
return false;
|
|
}
|
|
} else {
|
|
Print(`❌ Expected 3 pages, got ${this.DebugPages.length}`);
|
|
return false;
|
|
}
|
|
} else {
|
|
Print('❌ System not initialized');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Category: "Testing"
|
|
/**
|
|
* Test page content generation for all registered pages
|
|
* @returns True if all pages generate non-empty content
|
|
* @private Internal testing method
|
|
*/
|
|
private TestPageContentGeneration() {
|
|
let allTestsPassed = true;
|
|
|
|
this.DebugPages.forEach((page, index) => {
|
|
let updatedPage: S_DebugPage = page;
|
|
|
|
switch (updatedPage.UpdateFunction) {
|
|
case E_DebugUpdateFunction.UpdateMovementPage:
|
|
updatedPage = this.UpdateMovementPage(updatedPage);
|
|
break;
|
|
case E_DebugUpdateFunction.UpdateSurfacePage:
|
|
updatedPage = this.UpdateSurfacePage(updatedPage);
|
|
break;
|
|
case E_DebugUpdateFunction.UpdatePerformancePage:
|
|
updatedPage = this.UpdatePerformancePage(updatedPage);
|
|
break;
|
|
}
|
|
|
|
if (updatedPage.Content.length > 0) {
|
|
Print(`✅ Page ${index + 1} content generated`);
|
|
} else {
|
|
Print(`❌ Page ${index + 1} content empty`);
|
|
allTestsPassed = false;
|
|
return;
|
|
}
|
|
})
|
|
|
|
return allTestsPassed;
|
|
}
|
|
|
|
// Category: "Testing"
|
|
// Pure: true
|
|
/**
|
|
* Validate current navigation state
|
|
* @returns True if current page index is valid
|
|
* @pure Function has no side effects
|
|
*/
|
|
private IsValidNavigationState() {
|
|
if (this.DebugSettings.CurrentPageIndex >= 0) {
|
|
const visiblePagesLength = this.GetVisiblePages().length;
|
|
|
|
return !((visiblePagesLength > 0) && (this.DebugSettings.CurrentPageIndex >= visiblePagesLength));
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Category: "Testing"
|
|
/**
|
|
* Test page navigation functionality
|
|
* @param StartCurrentPage - Initial page index to restore after test
|
|
* @returns True if navigation works correctly
|
|
* @private Internal testing method
|
|
*/
|
|
private TestNavigation(StartCurrentPage: Integer) {
|
|
if (this.IsValidNavigationState()) {
|
|
if (this.IsValidNavigationState()) {
|
|
this.NextPage();
|
|
|
|
if (this.IsValidNavigationState()) {
|
|
if (this.IsValidNavigationState()) {
|
|
this.PreviousPage();
|
|
|
|
if (this.IsValidNavigationState()) {
|
|
this.DebugSettings.CurrentPageIndex = StartCurrentPage;
|
|
|
|
if (this.IsValidNavigationState()) {
|
|
return true;
|
|
} else {
|
|
Print('❌ Navigation: Failed to restore valid state');
|
|
return false;
|
|
}
|
|
} else {
|
|
Print('❌ Navigation: PrevPage failed — State became invalid after PreviousPage');
|
|
return false;
|
|
}
|
|
} else {
|
|
Print('❌ Navigation: PreviousPage failed — Invalid state before PreviousPage');
|
|
return false;
|
|
}
|
|
} else {
|
|
Print('❌ Navigation: NextPage failed — State became invalid after NextPage');
|
|
return false;
|
|
}
|
|
} else {
|
|
Print('❌ Navigation: NextPage failed — Invalid state before NextPage');
|
|
return false;
|
|
}
|
|
} else {
|
|
Print('❌ Navigation: Invalid initial state');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Category: "Testing"
|
|
/**
|
|
* Run comprehensive test suite for debug system
|
|
* Tests initialization, page content generation, and navigation
|
|
* @private Called during initialization for validation
|
|
*/
|
|
private RunAllTests() {
|
|
Print('=== COMPREHENSIVE DEBUG SYSTEM TESTING ===');
|
|
|
|
if (this.TestDebugSystem()) {
|
|
if (this.TestPageContentGeneration()) {
|
|
if (this.TestNavigation(this.DebugSettings.CurrentPageIndex)) {
|
|
Print('✅ ALL TESTS PASSED');
|
|
} else {
|
|
Print('❌ Navigation Tests: Failed');
|
|
}
|
|
} else {
|
|
Print('❌ Page Content Generation Tests: Failed');
|
|
}
|
|
} else {
|
|
Print('❌ System Tests: Failed');
|
|
}
|
|
}
|
|
|
|
// Category: "System Setup"
|
|
/**
|
|
* Initialize debug HUD system with movement component reference
|
|
* Sets up pages, creates widget, runs tests, and starts display
|
|
* @param MovementComponentRef - Reference to movement component to debug
|
|
* @public Called by BP_MainCharacter during initialization
|
|
* @example
|
|
* // Initialize debug HUD in main character
|
|
* this.DebugHUDComponent.InitializeDebugHUD(this.MovementComponent);
|
|
*/
|
|
public InitializeDebugHUD(MovementComponentRef: AC_Movement) {
|
|
this.MovementComponent = MovementComponentRef;
|
|
this.IsInitialized = true;
|
|
this.FrameCounter = 0;
|
|
this.LastUpdateTime = 0;
|
|
this.FPS = 0;
|
|
this.RegisterDefaultPages();
|
|
this.CreateDebugWidget();
|
|
this.RunAllTests();
|
|
this.DebugSettings.CurrentPageIndex = 0;
|
|
this.UpdateWidgetVisibility();
|
|
Print('=== DEBUG HUD INITIALIZED ===');
|
|
this.UpdateCurrentPage();
|
|
}
|
|
}
|