diff --git a/INPUT_API.md b/INPUT_API.md new file mode 100644 index 0000000..ee251a1 --- /dev/null +++ b/INPUT_API.md @@ -0,0 +1,546 @@ +# WillowVox Input API Documentation + +The WillowVox engine provides a Unity-like input system that abstracts platform-specific input into a consistent API. All input is handled through three main classes: + +## Core Classes + +### 1. **InputManager** - Main Input API +The primary interface for all input queries. Works across all platforms (PC, mobile, consoles). + +### 2. **GamepadInput** - Gamepad-Specific Features +Advanced gamepad features like individual stick values, triggers, and vibration. + +### 3. **TouchInput** - Touch-Specific Features +Multi-touch support for mobile (Android, iOS) and Wii U GamePad. + +--- + +## InputManager - Core Input API + +### Button Input + +```cpp +#include + +using namespace WillowVox; + +void Update(const InputState& input) +{ + // Check if jump button is held down + if (InputManager::GetButton(InputAction::Jump)) + { + player->Jump(); + } + + // Check if action button was pressed THIS FRAME + if (InputManager::GetButtonDown(InputAction::Action1)) + { + player->Attack(); + } + + // Check if button was released THIS FRAME + if (InputManager::GetButtonUp(InputAction::Action2)) + { + player->StopAiming(); + } +} +``` + +### Axis Input (Movement & Look) + +```cpp +// Get individual axes by name +float horizontal = InputManager::GetAxis("MoveX"); // -1.0 to 1.0 +float vertical = InputManager::GetAxis("MoveY"); // -1.0 to 1.0 +float lookX = InputManager::GetAxis("LookX"); +float lookY = InputManager::GetAxis("LookY"); + +// Or get as vectors (recommended) +glm::vec2 movement = InputManager::GetMovementVector(); // (x, y) +glm::vec2 look = InputManager::GetLookVector(); // (x, y) + +// Apply to player +player->Move(movement.x, movement.y); +camera->Rotate(look.x, look.y); +``` + +**Supported Axis Names:** +- `"MoveX"` or `"Horizontal"` - Left/Right movement +- `"MoveY"` or `"Vertical"` - Forward/Backward movement +- `"LookX"` or `"Mouse X"` - Horizontal look/camera +- `"LookY"` or `"Mouse Y"` - Vertical look/camera + +### Pointer/Mouse Input + +```cpp +// Get pointer position (mouse or touch) +glm::vec2 pointerPos = InputManager::GetPointerPosition(); + +// Get pointer movement delta (useful for camera) +glm::vec2 pointerDelta = InputManager::GetPointerDelta(); +camera->Rotate(pointerDelta.x * sensitivity, pointerDelta.y * sensitivity); + +// Check if pointer/mouse button is down +if (InputManager::GetPointerDown()) +{ + // Click or touch is active +} + +// Get scroll wheel delta +float scroll = InputManager::GetScrollDelta(); +camera->Zoom(scroll); +``` + +### Device Detection + +```cpp +// Check what device is being used +InputDeviceType device = InputManager::GetDeviceType(); + +// Check for specific device types +if (InputManager::IsGamepad()) +{ + // Show gamepad button prompts + UI::ShowPrompt("Press A to continue"); +} +else if (InputManager::IsTouchscreen()) +{ + // Show touch controls + UI::ShowVirtualButtons(); +} + +// Check if specific device +if (InputManager::IsDeviceType(InputDeviceType::XboxController)) +{ + // Xbox-specific UI +} +``` + +### Convenience Methods + +```cpp +// Check if player is trying to move +if (InputManager::IsMoving()) +{ + player->PlayWalkAnimation(); +} + +// Check if player is looking around +if (InputManager::IsLooking()) +{ + camera->UpdateRotation(); +} +``` + +--- + +## GamepadInput - Gamepad Features + +For more advanced gamepad control, use the `GamepadInput` class: + +### Individual Stick Values + +```cpp +#include + +// Get left stick (movement) +float leftX = GamepadInput::GetLeftStickX(); +float leftY = GamepadInput::GetLeftStickY(); +glm::vec2 leftStick = GamepadInput::GetLeftStick(); + +// Get right stick (camera/look) +float rightX = GamepadInput::GetRightStickX(); +float rightY = GamepadInput::GetRightStickY(); +glm::vec2 rightStick = GamepadInput::GetRightStick(); +``` + +### Trigger Values + +```cpp +// Get analog trigger values (0.0 to 1.0) +float leftTrigger = GamepadInput::GetTriggerLeft(); +float rightTrigger = GamepadInput::GetTriggerRight(); + +// Use for vehicle acceleration +vehicle->Accelerate(rightTrigger); +vehicle->Brake(leftTrigger); + +// Check if trigger exceeds threshold +if (GamepadInput::GetTriggerRightDown(0.5f)) +{ + // Trigger pressed at least 50% +} +``` + +### D-Pad Input + +```cpp +// Get D-Pad as vector +glm::vec2 dpad = GamepadInput::GetDPad(); + +// Or check individual directions +if (GamepadInput::GetDPadUp()) { menu->MoveUp(); } +if (GamepadInput::GetDPadDown()) { menu->MoveDown(); } +if (GamepadInput::GetDPadLeft()) { menu->MoveLeft(); } +if (GamepadInput::GetDPadRight()) { menu->MoveRight(); } +``` + +### Face Buttons (Position-Based) + +```cpp +// These map to physical button locations, not labels +bool south = GamepadInput::GetButtonSouth(); // A / Cross (bottom) +bool east = GamepadInput::GetButtonEast(); // B / Circle (right) +bool west = GamepadInput::GetButtonWest(); // X / Square (left) +bool north = GamepadInput::GetButtonNorth(); // Y / Triangle (top) + +// Useful for QTE sequences or when you need exact button positions +if (GamepadInput::GetButtonSouth()) +{ + qte->PressCorrectButton(); +} +``` + +### Shoulder Buttons + +```cpp +bool lb = GamepadInput::GetShoulderLeft(); // LB / L1 +bool rb = GamepadInput::GetShoulderRight(); // RB / R1 +``` + +### Connection Status + +```cpp +// Check if gamepad is connected +if (GamepadInput::IsConnected(0)) +{ + // Player 1 gamepad connected +} + +// Get gamepad type +InputDeviceType type = GamepadInput::GetGamepadType(); +if (type == InputDeviceType::XboxController) +{ + UI::ShowXboxButtons(); +} +else if (type == InputDeviceType::PSController) +{ + UI::ShowPlayStationButtons(); +} +``` + +### Vibration/Rumble + +```cpp +// Set vibration (platform-dependent) +GamepadInput::SetVibration(0.5f, 0.8f, 1.0f); // (low motor, high motor, duration) + +// Stop vibration +GamepadInput::StopVibration(); +``` + +--- + +## TouchInput - Mobile & Touch Features + +For touchscreen platforms (Android, iOS, Wii U GamePad): + +### Basic Touch Input + +```cpp +#include + +// Check if touchscreen is supported +if (TouchInput::IsSupported()) +{ + // Get number of active touches + int touchCount = TouchInput::GetTouchCount(); + + // Check if any finger is touching + if (TouchInput::IsTouching()) + { + // Handle touch + } +} +``` + +### Primary Touch + +```cpp +// Get the first/primary touch +Touch touch = TouchInput::GetPrimaryTouch(); + +if (touch.isActive) +{ + glm::vec2 position = touch.position; + glm::vec2 delta = touch.deltaPosition; + + // Check touch phase + if (touch.phase == Touch::Phase::Began) + { + // Touch just started + } + else if (touch.phase == Touch::Phase::Moved) + { + // Touch is moving + camera->Pan(delta.x, delta.y); + } +} +``` + +### Multi-Touch Support + +```cpp +// Get all active touches +std::vector touches = TouchInput::GetTouches(); + +for (const Touch& touch : touches) +{ + // Process each touch point + UI::DrawTouchIndicator(touch.position); +} + +// Or access by index +Touch touch0 = TouchInput::GetTouch(0); // First touch +Touch touch1 = TouchInput::GetTouch(1); // Second touch + +// Pinch-to-zoom example +if (TouchInput::GetTouchCount() == 2) +{ + Touch t0 = TouchInput::GetTouch(0); + Touch t1 = TouchInput::GetTouch(1); + + float distance = glm::distance(t0.position, t1.position); + camera->Zoom(distance); +} +``` + +### Touch Phase Detection + +```cpp +// Check for specific touch events +if (TouchInput::TouchBegan()) +{ + // New touch detected this frame +} + +if (TouchInput::TouchMoved()) +{ + // Active touch moved this frame +} + +if (TouchInput::TouchEnded()) +{ + // Touch was released this frame +} +``` + +### Platform Features + +```cpp +// Check capabilities +bool multiTouch = TouchInput::SupportsMultiTouch(); +int maxTouches = TouchInput::GetMaxTouchCount(); + +// Wii U GamePad specific +if (TouchInput::IsWiiUGamePad()) +{ + // Use GamePad-specific features +} +``` + +--- + +## Platform-Specific Notes + +### Desktop (Windows/Linux/macOS) +- **Movement**: WASD or arrow keys → `MoveX/MoveY` +- **Look**: Mouse movement → `LookX/LookY` +- **Actions**: Mouse buttons, Space, etc. → `Action1/2/3, Jump` +- **Gamepad**: XInput controllers supported + +### Mobile (Android/iOS) +- **Movement**: Virtual joystick (left side) → `MoveX/MoveY` +- **Look**: Touch drag (right side) → `LookX/LookY` +- **Actions**: Virtual buttons → `Action1/2, Jump` +- **Multi-touch**: Supported via `TouchInput` + +### PlayStation (PS3/PS4) +- **Movement**: Left analog stick → `MoveX/MoveY` +- **Look**: Right analog stick → `LookX/LookY` +- **Actions**: DualShock buttons → `Action1/2/3/4` +- **Triggers**: L2/R2 → `Crouch/Sprint` + +### Xbox Series X/S +- **Movement**: Left analog stick → `MoveX/MoveY` +- **Look**: Right analog stick → `LookX/LookY` +- **Actions**: ABXY buttons → `Action1/2/3/4` +- **Triggers**: LT/RT → `Crouch/Sprint` + +### Nintendo Switch +- **Movement**: Left analog stick → `MoveX/MoveY` +- **Look**: Right analog stick → `LookX/LookY` +- **Actions**: ABXY buttons → `Action1/2/3/4` +- **Triggers**: ZL/ZR → `Crouch/Sprint` + +### Nintendo Wii +- **Movement**: Nunchuk joystick or D-pad → `MoveX/MoveY` +- **Look**: Wiimote IR pointer → `LookX/LookY` +- **Actions**: A/B/1/2 buttons → `Action1/2/3/4` + +### Nintendo GameCube +- **Movement**: Control stick → `MoveX/MoveY` +- **Look**: C-stick → `LookX/LookY` +- **Actions**: ABXY buttons → `Action1/2/3/4` +- **Triggers**: L/R analog triggers supported + +### Nintendo Wii U +- **Movement**: Left analog stick → `MoveX/MoveY` +- **Look**: Right analog stick → `LookX/LookY` +- **Actions**: ABXY buttons → `Action1/2/3/4` +- **Touchscreen**: GamePad touchscreen via `TouchInput` + +--- + +## Complete Usage Example + +```cpp +#include + +using namespace WillowVox; + +class MyGame : public App +{ +public: + void Start() override + { + Logger::Log("Game Started!"); + } + + void Update(const InputState& input) override + { + // === Movement === + glm::vec2 movement = InputManager::GetMovementVector(); + player->Move(movement * speed * App::m_deltaTime); + + // === Camera/Look === + glm::vec2 look = InputManager::GetLookVector(); + camera->Rotate(look * sensitivity); + + // === Actions === + if (InputManager::GetButtonDown(InputAction::Jump)) + { + player->Jump(); + } + + if (InputManager::GetButton(InputAction::Action1)) + { + player->PrimaryAction(); + } + + if (InputManager::GetButtonDown(InputAction::MenuOpen)) + { + menu->Toggle(); + } + + // === Platform-Specific === + if (InputManager::IsGamepad()) + { + // Advanced gamepad features + float rightTrigger = GamepadInput::GetTriggerRight(); + if (rightTrigger > 0.5f) + { + player->Sprint(); + } + } + else if (InputManager::IsTouchscreen()) + { + // Touch-specific features + int touchCount = TouchInput::GetTouchCount(); + if (touchCount == 2) + { + // Pinch to zoom + Touch t0 = TouchInput::GetTouch(0); + Touch t1 = TouchInput::GetTouch(1); + float dist = glm::distance(t0.position, t1.position); + camera->Zoom(dist); + } + } + } + + void Render() override + { + // Your rendering code + } + +private: + Player* player; + Camera* camera; + Menu* menu; + float speed = 5.0f; + float sensitivity = 2.0f; +}; +``` + +--- + +## InputAction Reference + +All abstract actions available in `InputAction` enum: + +| Action | Description | Common Mapping | +|--------|-------------|----------------| +| `MoveForward` | Move forward | W, Up, D-pad Up, Left Stick Up | +| `MoveBackward` | Move backward | S, Down, D-pad Down, Left Stick Down | +| `MoveLeft` | Move left | A, Left, D-pad Left, Left Stick Left | +| `MoveRight` | Move right | D, Right, D-pad Right, Left Stick Right | +| `Action1` | Primary action | Mouse Left, A/Cross, Touch Button | +| `Action2` | Secondary action | Mouse Right, B/Circle | +| `Action3` | Tertiary action | X/Square | +| `Action4` | Quaternary action | Y/Triangle | +| `Jump` | Jump | Space, A/Cross | +| `Crouch` | Crouch | Ctrl, L2/LT, Z | +| `Sprint` | Sprint | Shift, R2/RT | +| `MenuOpen` | Open menu | Esc, Start, Plus | +| `MenuBack` | Back/Cancel | Esc, B/Circle, Minus | +| `CycleLeft` | Cycle items left | Q, LB/L1 | +| `CycleRight` | Cycle items right | E, RB/R1 | + +--- + +## Best Practices + +1. **Always use InputManager for basic input** - It works across all platforms +2. **Use GamepadInput only when you need advanced gamepad features** (triggers, vibration) +3. **Use TouchInput only for multi-touch or touch-specific features** +4. **Detect device type to show appropriate UI prompts** (keyboard vs gamepad vs touch) +5. **Test on all target platforms** - Input behavior may vary slightly +6. **Use GetButtonDown for single-press actions** (menus, attacks) +7. **Use GetButton for continuous actions** (movement, aiming) +8. **Normalize and multiply by deltaTime for frame-independent movement** + +--- + +## Migration from Old Input System + +If you're using the old `Input` class (keyboard/mouse only): + +```cpp +// OLD (Desktop only) +if (Input::GetKey(Key::W)) + player->Move(0, 1); + +// NEW (All platforms) +glm::vec2 movement = InputManager::GetMovementVector(); +player->Move(movement.x, movement.y); +``` + +```cpp +// OLD (Desktop only) +glm::vec2 mouseDelta = Input::GetMouseDelta(); + +// NEW (All platforms) +glm::vec2 lookDelta = InputManager::GetLookVector(); +``` + +The new API is **backward compatible** - you can still use the old `Input` class for desktop-specific features, but `InputManager` is recommended for cross-platform projects. diff --git a/include/wv/core.h b/include/wv/core.h index 58a606c..16af670 100644 --- a/include/wv/core.h +++ b/include/wv/core.h @@ -12,6 +12,9 @@ #include #include +#include +#include +#include #include #include diff --git a/include/wv/input/GamepadInput.h b/include/wv/input/GamepadInput.h new file mode 100644 index 0000000..0f3e77a --- /dev/null +++ b/include/wv/input/GamepadInput.h @@ -0,0 +1,177 @@ +#pragma once + +#include +#include + +namespace WillowVox +{ + /** + * Gamepad-Specific Input API + * + * Provides access to gamepad-specific features like: + * - Individual stick values + * - Trigger values + * - Vibration/haptics (future) + * - Multiple gamepad support (future) + * + * Usage: + * float leftX = GamepadInput::GetLeftStickX(); + * float rightTrigger = GamepadInput::GetTriggerRight(); + * bool connected = GamepadInput::IsConnected(0); + */ + class GamepadInput + { + public: + /** + * Initialize gamepad input with current state + * Called automatically by InputManager + */ + static void SetInputState(const InputState* state); + + // ===== Stick Input ===== + + /** + * Get left stick X axis (-1.0 to 1.0) + * @return Left stick horizontal value + */ + static float GetLeftStickX(); + + /** + * Get left stick Y axis (-1.0 to 1.0) + * @return Left stick vertical value + */ + static float GetLeftStickY(); + + /** + * Get left stick as a vector + * @return glm::vec2 (x, y) of left stick + */ + static glm::vec2 GetLeftStick(); + + /** + * Get right stick X axis (-1.0 to 1.0) + * @return Right stick horizontal value + */ + static float GetRightStickX(); + + /** + * Get right stick Y axis (-1.0 to 1.0) + * @return Right stick vertical value + */ + static float GetRightStickY(); + + /** + * Get right stick as a vector + * @return glm::vec2 (x, y) of right stick + */ + static glm::vec2 GetRightStick(); + + // ===== Trigger Input ===== + + /** + * Get left trigger value (0.0 to 1.0) + * Simulated on platforms without analog triggers + * @return Left trigger pressure + */ + static float GetTriggerLeft(); + + /** + * Get right trigger value (0.0 to 1.0) + * Simulated on platforms without analog triggers + * @return Right trigger pressure + */ + static float GetTriggerRight(); + + /** + * Check if left trigger is pressed past threshold + * @param threshold Threshold value (default 0.5) + * @return True if trigger exceeds threshold + */ + static bool GetTriggerLeftDown(float threshold = 0.5f); + + /** + * Check if right trigger is pressed past threshold + * @param threshold Threshold value (default 0.5) + * @return True if trigger exceeds threshold + */ + static bool GetTriggerRightDown(float threshold = 0.5f); + + // ===== D-Pad Input ===== + + /** + * Get D-Pad as a vector + * @return glm::vec2 (-1/0/1 for each axis) + */ + static glm::vec2 GetDPad(); + + /** + * Check if D-Pad direction is pressed + */ + static bool GetDPadUp(); + static bool GetDPadDown(); + static bool GetDPadLeft(); + static bool GetDPadRight(); + + // ===== Face Buttons ===== + + /** + * Get face button state (A/B/X/Y or Cross/Circle/Square/Triangle) + * These map to the physical location, not semantic meaning + */ + static bool GetButtonSouth(); // A / Cross + static bool GetButtonEast(); // B / Circle + static bool GetButtonWest(); // X / Square + static bool GetButtonNorth(); // Y / Triangle + + // ===== Shoulder Buttons ===== + + static bool GetShoulderLeft(); // LB / L1 + static bool GetShoulderRight(); // RB / R1 + + // ===== Special Buttons ===== + + static bool GetButtonStart(); // Start / Options / Plus + static bool GetButtonSelect(); // Back / Share / Minus + + // ===== Connection Status ===== + + /** + * Check if a gamepad is connected + * @param index Gamepad index (0-3), default 0 + * @return True if gamepad is connected + */ + static bool IsConnected(int index = 0); + + /** + * Get the gamepad type + * @return InputDeviceType of the connected gamepad + */ + static InputDeviceType GetGamepadType(); + + // ===== Vibration (Platform-Specific) ===== + + /** + * Set vibration/rumble on the gamepad + * Not all platforms support this + * @param leftMotor Low frequency motor (0.0-1.0) + * @param rightMotor High frequency motor (0.0-1.0) + * @param duration Duration in seconds (0 = continuous) + */ + static void SetVibration(float leftMotor, float rightMotor, float duration = 0.0f); + + /** + * Stop all vibration + */ + static void StopVibration(); + + private: + static const InputState* s_currentState; + static class IPlatform* s_platform; + + // Trigger values (simulated on platforms without analog triggers) + static float s_leftTrigger; + static float s_rightTrigger; + + friend class InputManager; // Allow InputManager to set platform pointer + }; +} diff --git a/include/wv/input/InputManager.h b/include/wv/input/InputManager.h new file mode 100644 index 0000000..15e3fe8 --- /dev/null +++ b/include/wv/input/InputManager.h @@ -0,0 +1,156 @@ +#pragma once + +#include +#include + +namespace WillowVox +{ + /** + * Centralized Input Manager (Unity-like API) + * + * Provides a static interface for querying input state across all platforms. + * Automatically adapts to the current platform's input device. + * + * Usage: + * if (InputManager::GetButton(InputAction::Jump)) + * if (InputManager::GetButtonDown(InputAction::Action1)) + * float moveX = InputManager::GetAxis("MoveX"); + * float lookY = InputManager::GetAxis("LookY"); + */ + class InputManager + { + public: + /** + * Initialize the input manager with the current input state + * Called automatically by the engine each frame + */ + static void SetInputState(const InputState* state); + + // ===== Button Input ===== + + /** + * Check if a button is currently held down + * @param action The abstract input action to check + * @return True if the button is held down + */ + static bool GetButton(InputAction action); + + /** + * Check if a button was pressed this frame + * @param action The abstract input action to check + * @return True if the button was pressed this frame + */ + static bool GetButtonDown(InputAction action); + + /** + * Check if a button was released this frame + * @param action The abstract input action to check + * @return True if the button was released this frame + */ + static bool GetButtonUp(InputAction action); + + // ===== Axis Input ===== + + /** + * Get the value of a virtual axis (-1.0 to 1.0) + * @param axisName Name of the axis ("MoveX", "MoveY", "LookX", "LookY") + * @return The axis value normalized to -1.0 to 1.0 + */ + static float GetAxis(const char* axisName); + + /** + * Get raw axis value without smoothing + * Same as GetAxis() for now, but provided for Unity compatibility + */ + static float GetAxisRaw(const char* axisName); + + // ===== Movement ===== + + /** + * Get movement vector (X, Y) from analog stick or keyboard + * @return glm::vec2 where x = left/right, y = forward/backward + */ + static glm::vec2 GetMovementVector(); + + /** + * Get look vector (X, Y) from analog stick or mouse + * @return glm::vec2 where x = horizontal, y = vertical + */ + static glm::vec2 GetLookVector(); + + // ===== Pointer/Touch ===== + + /** + * Get pointer position (mouse or primary touch) + * @return glm::vec2 screen position + */ + static glm::vec2 GetPointerPosition(); + + /** + * Get pointer delta (mouse movement or touch drag) + * @return glm::vec2 delta since last frame + */ + static glm::vec2 GetPointerDelta(); + + /** + * Check if pointer is down (mouse button or touch) + * @return True if pointer is currently down + */ + static bool GetPointerDown(); + + // ===== Device Info ===== + + /** + * Get the current input device type + * @return InputDeviceType enum value + */ + static InputDeviceType GetDeviceType(); + + /** + * Check if a specific device type is active + * @param deviceType Device type to check + * @return True if the device type matches + */ + static bool IsDeviceType(InputDeviceType deviceType); + + /** + * Check if the current device is a gamepad + * @return True if using any gamepad type + */ + static bool IsGamepad(); + + /** + * Check if the current device has a touchscreen + * @return True if using touchscreen + */ + static bool IsTouchscreen(); + + // ===== Scroll ===== + + /** + * Get scroll wheel delta + * @return Scroll delta this frame + */ + static float GetScrollDelta(); + + // ===== Convenience Methods ===== + + /** + * Check if any movement input is active + * @return True if player is trying to move + */ + static bool IsMoving(); + + /** + * Check if any look input is active + * @return True if player is trying to look around + */ + static bool IsLooking(); + + private: + static const InputState* s_currentState; + + // Helper to get axis by name + static float GetAxisInternal(const char* axisName); + }; +} diff --git a/include/wv/input/TouchInput.h b/include/wv/input/TouchInput.h new file mode 100644 index 0000000..87c524e --- /dev/null +++ b/include/wv/input/TouchInput.h @@ -0,0 +1,169 @@ +#pragma once + +#include +#include +#include + +namespace WillowVox +{ + /** + * Touch Point Information + * + * Represents a single touch point on the screen + */ + struct Touch + { + int fingerId; // Unique identifier for this touch + glm::vec2 position; // Current position (screen coordinates) + glm::vec2 deltaPosition; // Movement since last frame + float pressure; // Touch pressure (0.0-1.0), if supported + bool isActive; // Is this touch currently active + + // Touch phase + enum class Phase + { + Began, // Touch just started + Moved, // Touch moved + Stationary, // Touch is down but hasn't moved + Ended, // Touch just ended + Canceled // Touch was canceled + }; + Phase phase; + + Touch() : fingerId(-1), position(0.0f), deltaPosition(0.0f), + pressure(1.0f), isActive(false), phase(Phase::Ended) {} + }; + + /** + * Touch-Specific Input API + * + * Provides access to touchscreen features for mobile and Wii U GamePad: + * - Multi-touch support + * - Touch position and delta + * - Touch phase tracking + * - Gesture recognition (future) + * + * Usage: + * int touchCount = TouchInput::GetTouchCount(); + * Touch touch = TouchInput::GetTouch(0); + * if (touch.phase == Touch::Phase::Began) { ... } + */ + class TouchInput + { + public: + /** + * Initialize touch input with current state + * Called automatically by InputManager + */ + static void SetInputState(const InputState* state); + + // ===== Touch Count ===== + + /** + * Get the number of active touches + * @return Number of fingers currently touching the screen + */ + static int GetTouchCount(); + + /** + * Check if any touch is active + * @return True if at least one finger is touching + */ + static bool IsTouching(); + + // ===== Touch Access ===== + + /** + * Get touch information by index + * @param index Touch index (0 to GetTouchCount()-1) + * @return Touch information, or invalid touch if index out of range + */ + static Touch GetTouch(int index); + + /** + * Get the primary touch (first touch point) + * @return Primary touch information + */ + static Touch GetPrimaryTouch(); + + /** + * Get all active touches + * @return Vector of all active touch points + */ + static std::vector GetTouches(); + + // ===== Touch Position ===== + + /** + * Get primary touch position + * @return Screen position of first touch + */ + static glm::vec2 GetTouchPosition(); + + /** + * Get primary touch delta + * @return Movement delta of first touch + */ + static glm::vec2 GetTouchDelta(); + + // ===== Touch Phase Queries ===== + + /** + * Check if any touch just began this frame + * @return True if new touch detected + */ + static bool TouchBegan(); + + /** + * Check if any touch moved this frame + * @return True if active touch moved + */ + static bool TouchMoved(); + + /** + * Check if any touch ended this frame + * @return True if touch was released + */ + static bool TouchEnded(); + + // ===== Platform Features ===== + + /** + * Check if the current platform supports touch input + * @return True if touchscreen is available + */ + static bool IsSupported(); + + /** + * Check if the platform supports multi-touch + * @return True if multiple simultaneous touches are supported + */ + static bool SupportsMultiTouch(); + + /** + * Get maximum number of simultaneous touches supported + * @return Max touch points (typically 5-10) + */ + static int GetMaxTouchCount(); + + // ===== Wii U GamePad Specific ===== + + /** + * Check if touch input is from Wii U GamePad + * @return True if device is Wii U GamePad + */ + static bool IsWiiUGamePad(); + + private: + static const InputState* s_currentState; + static std::vector s_touches; + static std::vector s_prevTouches; + static constexpr int MAX_TOUCHES = 10; + + // Update touch list from input state + static void UpdateTouches(); + + // Helper to find touch in previous frame + static Touch* FindPreviousTouch(int fingerId); + }; +} diff --git a/include/wv/platform/IPlatform.h b/include/wv/platform/IPlatform.h index 768f411..90ed3ae 100644 --- a/include/wv/platform/IPlatform.h +++ b/include/wv/platform/IPlatform.h @@ -49,6 +49,11 @@ namespace WillowVox // Special platform features virtual bool HasFeature(const char* featureName) const = 0; + // Vibration/Rumble (controllers, gamepads) + // Not all platforms support this, default implementation is no-op + virtual void SetVibration(int playerIndex, float lowFrequency, float highFrequency) {} + virtual void StopVibration(int playerIndex) {} + // Optional: Some platforms may need special per-frame updates virtual void Update(float deltaTime) {} }; diff --git a/include/wv/platform/InputState.h b/include/wv/platform/InputState.h index f3e7c51..3e31865 100644 --- a/include/wv/platform/InputState.h +++ b/include/wv/platform/InputState.h @@ -62,6 +62,8 @@ namespace WillowVox Touchscreen, Gamepad, WiiRemote, + GameCubeController, + WiiUGamePad, PSController, XboxController, SwitchController diff --git a/src/app/App.cpp b/src/app/App.cpp index 444862e..453b8a1 100644 --- a/src/app/App.cpp +++ b/src/app/App.cpp @@ -3,6 +3,9 @@ #include #include #include +#include +#include +#include #include namespace WillowVox @@ -70,6 +73,12 @@ namespace WillowVox // Poll input m_platform->PollInput(m_inputState); + // Update centralized input managers + InputManager::SetInputState(&m_inputState); + GamepadInput::SetInputState(&m_inputState); + GamepadInput::s_platform = m_platform; // Set platform for vibration + TouchInput::SetInputState(&m_inputState); + // Begin frame m_graphicsContext->BeginFrame(); diff --git a/src/input/GamepadInput.cpp b/src/input/GamepadInput.cpp new file mode 100644 index 0000000..e6ee1c9 --- /dev/null +++ b/src/input/GamepadInput.cpp @@ -0,0 +1,209 @@ +#include +#include +#include + +namespace WillowVox +{ + const InputState* GamepadInput::s_currentState = nullptr; + IPlatform* GamepadInput::s_platform = nullptr; + float GamepadInput::s_leftTrigger = 0.0f; + float GamepadInput::s_rightTrigger = 0.0f; + + void GamepadInput::SetInputState(const InputState* state) + { + s_currentState = state; + + // Update simulated trigger values based on button state + if (s_currentState) + { + s_leftTrigger = s_currentState->IsActionHeld(InputAction::Crouch) ? 1.0f : 0.0f; + s_rightTrigger = s_currentState->IsActionHeld(InputAction::Sprint) ? 1.0f : 0.0f; + } + } + + float GamepadInput::GetLeftStickX() + { + if (!s_currentState) return 0.0f; + return s_currentState->moveAxisX; + } + + float GamepadInput::GetLeftStickY() + { + if (!s_currentState) return 0.0f; + return s_currentState->moveAxisY; + } + + glm::vec2 GamepadInput::GetLeftStick() + { + if (!s_currentState) return glm::vec2(0.0f); + return glm::vec2(s_currentState->moveAxisX, s_currentState->moveAxisY); + } + + float GamepadInput::GetRightStickX() + { + if (!s_currentState) return 0.0f; + // Look axis from platforms is typically scaled up (e.g., 100x for sensitivity) + // Normalize it back to -1..1 range + float value = s_currentState->lookAxisX; + return glm::clamp(value / 100.0f, -1.0f, 1.0f); + } + + float GamepadInput::GetRightStickY() + { + if (!s_currentState) return 0.0f; + float value = s_currentState->lookAxisY; + return glm::clamp(value / 100.0f, -1.0f, 1.0f); + } + + glm::vec2 GamepadInput::GetRightStick() + { + return glm::vec2(GetRightStickX(), GetRightStickY()); + } + + float GamepadInput::GetTriggerLeft() + { + return s_leftTrigger; + } + + float GamepadInput::GetTriggerRight() + { + return s_rightTrigger; + } + + bool GamepadInput::GetTriggerLeftDown(float threshold) + { + return s_leftTrigger > threshold; + } + + bool GamepadInput::GetTriggerRightDown(float threshold) + { + return s_rightTrigger > threshold; + } + + glm::vec2 GamepadInput::GetDPad() + { + if (!s_currentState) return glm::vec2(0.0f); + + float x = 0.0f; + float y = 0.0f; + + if (s_currentState->IsActionHeld(InputAction::MoveLeft)) x = -1.0f; + if (s_currentState->IsActionHeld(InputAction::MoveRight)) x = 1.0f; + if (s_currentState->IsActionHeld(InputAction::MoveForward)) y = 1.0f; + if (s_currentState->IsActionHeld(InputAction::MoveBackward)) y = -1.0f; + + return glm::vec2(x, y); + } + + bool GamepadInput::GetDPadUp() + { + if (!s_currentState) return false; + return s_currentState->IsActionHeld(InputAction::MoveForward); + } + + bool GamepadInput::GetDPadDown() + { + if (!s_currentState) return false; + return s_currentState->IsActionHeld(InputAction::MoveBackward); + } + + bool GamepadInput::GetDPadLeft() + { + if (!s_currentState) return false; + return s_currentState->IsActionHeld(InputAction::MoveLeft); + } + + bool GamepadInput::GetDPadRight() + { + if (!s_currentState) return false; + return s_currentState->IsActionHeld(InputAction::MoveRight); + } + + bool GamepadInput::GetButtonSouth() + { + if (!s_currentState) return false; + return s_currentState->IsActionHeld(InputAction::Action1); + } + + bool GamepadInput::GetButtonEast() + { + if (!s_currentState) return false; + return s_currentState->IsActionHeld(InputAction::Action2); + } + + bool GamepadInput::GetButtonWest() + { + if (!s_currentState) return false; + return s_currentState->IsActionHeld(InputAction::Action3); + } + + bool GamepadInput::GetButtonNorth() + { + if (!s_currentState) return false; + return s_currentState->IsActionHeld(InputAction::Action4); + } + + bool GamepadInput::GetShoulderLeft() + { + if (!s_currentState) return false; + return s_currentState->IsActionHeld(InputAction::CycleLeft); + } + + bool GamepadInput::GetShoulderRight() + { + if (!s_currentState) return false; + return s_currentState->IsActionHeld(InputAction::CycleRight); + } + + bool GamepadInput::GetButtonStart() + { + if (!s_currentState) return false; + return s_currentState->IsActionHeld(InputAction::MenuOpen); + } + + bool GamepadInput::GetButtonSelect() + { + if (!s_currentState) return false; + return s_currentState->IsActionHeld(InputAction::MenuBack); + } + + bool GamepadInput::IsConnected(int index) + { + if (index != 0) return false; // Only support player 0 for now + if (!s_currentState) return false; + + // Consider connected if device type is a gamepad + InputDeviceType device = s_currentState->deviceType; + return device == InputDeviceType::Gamepad || + device == InputDeviceType::XboxController || + device == InputDeviceType::PSController || + device == InputDeviceType::SwitchController || + device == InputDeviceType::WiiRemote || + device == InputDeviceType::GameCubeController || + device == InputDeviceType::WiiUGamePad; + } + + InputDeviceType GamepadInput::GetGamepadType() + { + if (!s_currentState) return InputDeviceType::Unknown; + return s_currentState->deviceType; + } + + void GamepadInput::SetVibration(float leftMotor, float rightMotor, float duration) + { + if (!s_platform) + return; + + // Call platform's vibration implementation + // Duration is not yet implemented (would need a timer system) + s_platform->SetVibration(0, leftMotor, rightMotor); + } + + void GamepadInput::StopVibration() + { + if (!s_platform) + return; + + s_platform->StopVibration(0); + } +} diff --git a/src/input/InputManager.cpp b/src/input/InputManager.cpp new file mode 100644 index 0000000..ddb45ba --- /dev/null +++ b/src/input/InputManager.cpp @@ -0,0 +1,141 @@ +#include +#include +#include + +namespace WillowVox +{ + const InputState* InputManager::s_currentState = nullptr; + + void InputManager::SetInputState(const InputState* state) + { + s_currentState = state; + } + + bool InputManager::GetButton(InputAction action) + { + if (!s_currentState) return false; + return s_currentState->IsActionHeld(action); + } + + bool InputManager::GetButtonDown(InputAction action) + { + if (!s_currentState) return false; + return s_currentState->IsActionPressed(action); + } + + bool InputManager::GetButtonUp(InputAction action) + { + if (!s_currentState) return false; + return s_currentState->IsActionReleased(action); + } + + float InputManager::GetAxis(const char* axisName) + { + return GetAxisInternal(axisName); + } + + float InputManager::GetAxisRaw(const char* axisName) + { + // For now, same as GetAxis - could add smoothing to GetAxis() later + return GetAxisInternal(axisName); + } + + float InputManager::GetAxisInternal(const char* axisName) + { + if (!s_currentState) return 0.0f; + + // Map axis names to input state values + if (strcmp(axisName, "MoveX") == 0 || strcmp(axisName, "Horizontal") == 0) + return s_currentState->moveAxisX; + else if (strcmp(axisName, "MoveY") == 0 || strcmp(axisName, "Vertical") == 0) + return s_currentState->moveAxisY; + else if (strcmp(axisName, "LookX") == 0 || strcmp(axisName, "Mouse X") == 0) + return s_currentState->lookAxisX; + else if (strcmp(axisName, "LookY") == 0 || strcmp(axisName, "Mouse Y") == 0) + return s_currentState->lookAxisY; + + return 0.0f; + } + + glm::vec2 InputManager::GetMovementVector() + { + if (!s_currentState) return glm::vec2(0.0f); + return glm::vec2(s_currentState->moveAxisX, s_currentState->moveAxisY); + } + + glm::vec2 InputManager::GetLookVector() + { + if (!s_currentState) return glm::vec2(0.0f); + return glm::vec2(s_currentState->lookAxisX, s_currentState->lookAxisY); + } + + glm::vec2 InputManager::GetPointerPosition() + { + if (!s_currentState) return glm::vec2(0.0f); + return glm::vec2(s_currentState->pointerX, s_currentState->pointerY); + } + + glm::vec2 InputManager::GetPointerDelta() + { + if (!s_currentState) return glm::vec2(0.0f); + return glm::vec2(s_currentState->pointerDeltaX, s_currentState->pointerDeltaY); + } + + bool InputManager::GetPointerDown() + { + if (!s_currentState) return false; + return s_currentState->pointerDown; + } + + InputDeviceType InputManager::GetDeviceType() + { + if (!s_currentState) return InputDeviceType::Unknown; + return s_currentState->deviceType; + } + + bool InputManager::IsDeviceType(InputDeviceType deviceType) + { + return GetDeviceType() == deviceType; + } + + bool InputManager::IsGamepad() + { + InputDeviceType device = GetDeviceType(); + return device == InputDeviceType::Gamepad || + device == InputDeviceType::XboxController || + device == InputDeviceType::PSController || + device == InputDeviceType::SwitchController || + device == InputDeviceType::WiiRemote; + } + + bool InputManager::IsTouchscreen() + { + return GetDeviceType() == InputDeviceType::Touchscreen; + } + + float InputManager::GetScrollDelta() + { + if (!s_currentState) return 0.0f; + return s_currentState->scrollDelta; + } + + bool InputManager::IsMoving() + { + if (!s_currentState) return false; + float threshold = 0.1f; + return std::abs(s_currentState->moveAxisX) > threshold || + std::abs(s_currentState->moveAxisY) > threshold || + GetButton(InputAction::MoveForward) || + GetButton(InputAction::MoveBackward) || + GetButton(InputAction::MoveLeft) || + GetButton(InputAction::MoveRight); + } + + bool InputManager::IsLooking() + { + if (!s_currentState) return false; + float threshold = 0.1f; + return std::abs(s_currentState->lookAxisX) > threshold || + std::abs(s_currentState->lookAxisY) > threshold; + } +} diff --git a/src/input/TouchInput.cpp b/src/input/TouchInput.cpp new file mode 100644 index 0000000..2da543f --- /dev/null +++ b/src/input/TouchInput.cpp @@ -0,0 +1,186 @@ +#include + +namespace WillowVox +{ + const InputState* TouchInput::s_currentState = nullptr; + std::vector TouchInput::s_touches; + std::vector TouchInput::s_prevTouches; + + void TouchInput::SetInputState(const InputState* state) + { + s_currentState = state; + + // Store previous frame's touches + s_prevTouches = s_touches; + + UpdateTouches(); + } + + void TouchInput::UpdateTouches() + { + s_touches.clear(); + + if (!s_currentState) return; + if (!IsSupported()) return; + + // Create touch from pointer state + if (s_currentState->pointerDown) + { + Touch touch; + touch.fingerId = 0; + touch.position = glm::vec2(s_currentState->pointerX, s_currentState->pointerY); + touch.deltaPosition = glm::vec2(s_currentState->pointerDeltaX, s_currentState->pointerDeltaY); + touch.pressure = 1.0f; + touch.isActive = true; + + // Determine phase based on previous frame + Touch* prevTouch = FindPreviousTouch(touch.fingerId); + + if (!prevTouch) + { + // New touch - just began + touch.phase = Touch::Phase::Began; + } + else + { + // Existing touch - check if moved + float deltaX = touch.position.x - prevTouch->position.x; + float deltaY = touch.position.y - prevTouch->position.y; + float moveDist = std::sqrt(deltaX * deltaX + deltaY * deltaY); + + if (moveDist > 0.01f) + touch.phase = Touch::Phase::Moved; + else + touch.phase = Touch::Phase::Stationary; + } + + s_touches.push_back(touch); + } + } + + Touch* TouchInput::FindPreviousTouch(int fingerId) + { + for (auto& touch : s_prevTouches) + { + if (touch.fingerId == fingerId && touch.isActive) + return &touch; + } + return nullptr; + } + + int TouchInput::GetTouchCount() + { + return static_cast(s_touches.size()); + } + + bool TouchInput::IsTouching() + { + return GetTouchCount() > 0; + } + + Touch TouchInput::GetTouch(int index) + { + if (index < 0 || index >= static_cast(s_touches.size())) + return Touch(); // Return invalid touch + + return s_touches[index]; + } + + Touch TouchInput::GetPrimaryTouch() + { + return GetTouch(0); + } + + std::vector TouchInput::GetTouches() + { + return s_touches; + } + + glm::vec2 TouchInput::GetTouchPosition() + { + if (!s_currentState) return glm::vec2(0.0f); + return glm::vec2(s_currentState->pointerX, s_currentState->pointerY); + } + + glm::vec2 TouchInput::GetTouchDelta() + { + if (!s_currentState) return glm::vec2(0.0f); + return glm::vec2(s_currentState->pointerDeltaX, s_currentState->pointerDeltaY); + } + + bool TouchInput::TouchBegan() + { + if (s_touches.empty()) return false; + + for (const auto& touch : s_touches) + { + if (touch.phase == Touch::Phase::Began) + return true; + } + return false; + } + + bool TouchInput::TouchMoved() + { + if (s_touches.empty()) return false; + + for (const auto& touch : s_touches) + { + if (touch.phase == Touch::Phase::Moved) + return true; + } + return false; + } + + bool TouchInput::TouchEnded() + { + // Check if any touch from previous frame is no longer active + for (const auto& prevTouch : s_prevTouches) + { + if (!prevTouch.isActive) + continue; + + // Check if this touch still exists in current frame + bool stillActive = false; + for (const auto& currentTouch : s_touches) + { + if (currentTouch.fingerId == prevTouch.fingerId) + { + stillActive = true; + break; + } + } + + if (!stillActive) + return true; // Touch ended this frame + } + + return false; + } + + bool TouchInput::IsSupported() + { + if (!s_currentState) return false; + + return s_currentState->deviceType == InputDeviceType::Touchscreen || + s_currentState->deviceType == InputDeviceType::WiiUGamePad; + } + + bool TouchInput::SupportsMultiTouch() + { + // Most modern touchscreens support multi-touch + return IsSupported(); + } + + int TouchInput::GetMaxTouchCount() + { + if (!IsSupported()) return 0; + return MAX_TOUCHES; + } + + bool TouchInput::IsWiiUGamePad() + { + if (!s_currentState) return false; + return s_currentState->deviceType == InputDeviceType::WiiUGamePad; + } +} diff --git a/src/platform/Desktop/DesktopPlatform.cpp b/src/platform/Desktop/DesktopPlatform.cpp index bb9e1f6..470b420 100644 --- a/src/platform/Desktop/DesktopPlatform.cpp +++ b/src/platform/Desktop/DesktopPlatform.cpp @@ -260,11 +260,14 @@ namespace WillowVox { // Platform-specific user data paths #if defined(PLATFORM_WINDOWS) - static const char* path = "./userdata"; // TODO: Use AppData + // Windows: %APPDATA%\WillowVox + static const char* path = "%APPDATA%\\WillowVox"; #elif defined(PLATFORM_MACOS) - static const char* path = "./userdata"; // TODO: Use ~/Library/Application Support + // macOS: ~/Library/Application Support/WillowVox + static const char* path = "~/Library/Application Support/WillowVox"; #else - static const char* path = "./userdata"; // TODO: Use ~/.local/share + // Linux: ~/.local/share/WillowVox + static const char* path = "~/.local/share/WillowVox"; #endif return path; } @@ -293,4 +296,25 @@ namespace WillowVox if (strcmp(featureName, "filesystem") == 0) return true; return false; } + + void DesktopPlatform::SetVibration(int playerIndex, float lowFrequency, float highFrequency) + { +#if defined(PLATFORM_WINDOWS) + // Windows: Use XInput for gamepad vibration + // Note: This requires linking against Xinput.lib + // For now, GLFW doesn't expose vibration API, so we'd need platform-specific code + // This is a placeholder - actual implementation would use XInput directly + Logger::EngineLog("Vibration requested (Windows XInput not yet implemented)"); +#else + // Linux/macOS: GLFW doesn't support vibration + // Would need to use platform-specific APIs (evdev on Linux, IOKit on macOS) + Logger::EngineLog("Vibration not supported on this platform"); +#endif + } + + void DesktopPlatform::StopVibration(int playerIndex) + { + // Stop vibration by setting both motors to 0 + SetVibration(playerIndex, 0.0f, 0.0f); + } } diff --git a/src/platform/Desktop/DesktopPlatform.h b/src/platform/Desktop/DesktopPlatform.h index b2c81fc..de968cb 100644 --- a/src/platform/Desktop/DesktopPlatform.h +++ b/src/platform/Desktop/DesktopPlatform.h @@ -48,6 +48,9 @@ namespace WillowVox bool HasFeature(const char* featureName) const override; + void SetVibration(int playerIndex, float lowFrequency, float highFrequency) override; + void StopVibration(int playerIndex) override; + private: std::unique_ptr m_graphicsContext; diff --git a/src/platform/Nintendo/GameCubeGraphicsContext.cpp b/src/platform/Nintendo/GameCubeGraphicsContext.cpp new file mode 100644 index 0000000..83edd8b --- /dev/null +++ b/src/platform/Nintendo/GameCubeGraphicsContext.cpp @@ -0,0 +1,225 @@ +#include "GameCubeGraphicsContext.h" +#include + +#ifdef PLATFORM_GAMECUBE + +// libogc includes +#include +#include +#include +#include +#include +#include + +#define DEFAULT_FIFO_SIZE (256 * 1024) + +namespace WillowVox +{ + GameCubeGraphicsContext::GameCubeGraphicsContext() + { + m_startTime = static_cast(SYS_Time()) / TB_TIMER_CLOCK; + } + + GameCubeGraphicsContext::~GameCubeGraphicsContext() + { + Shutdown(); + } + + bool GameCubeGraphicsContext::Initialize(int width, int height, const char* title) + { + Logger::EngineLog("Initializing GameCube Graphics (GX)"); + + // Initialize VIDEO subsystem + VIDEO_Init(); + + // Get preferred video mode + GXRModeObj* rmode = VIDEO_GetPreferredMode(NULL); + m_renderMode = rmode; + + m_width = rmode->fbWidth; + m_height = rmode->efbHeight; + + Logger::EngineLog("Video mode: %dx%d", m_width, m_height); + + // Allocate framebuffers (double buffering) + m_frameBuffer[0] = MEM_K0_TO_K1(SYS_AllocateFramebuffer(rmode)); + m_frameBuffer[1] = MEM_K0_TO_K1(SYS_AllocateFramebuffer(rmode)); + + if (!m_frameBuffer[0] || !m_frameBuffer[1]) + { + Logger::EngineError("Failed to allocate framebuffers"); + return false; + } + + // Configure VIDEO + VIDEO_Configure(rmode); + VIDEO_SetNextFramebuffer(m_frameBuffer[0]); + VIDEO_SetBlack(FALSE); + VIDEO_Flush(); + VIDEO_WaitVSync(); + if (rmode->viTVMode & VI_NON_INTERLACE) + VIDEO_WaitVSync(); + + // Allocate GX FIFO buffer + m_fifoBuffer = memalign(32, DEFAULT_FIFO_SIZE); + if (!m_fifoBuffer) + { + Logger::EngineError("Failed to allocate GX FIFO buffer"); + return false; + } + memset(m_fifoBuffer, 0, DEFAULT_FIFO_SIZE); + + // Initialize GX + GX_Init(m_fifoBuffer, DEFAULT_FIFO_SIZE); + + // Set up GX for 3D rendering + GX_SetCopyClear((GXColor){26, 26, 26, 255}, 0x00FFFFFF); + + // Set viewport + GX_SetViewport(0, 0, rmode->fbWidth, rmode->efbHeight, 0, 1); + f32 yscale = GX_GetYScaleFactor(rmode->efbHeight, rmode->xfbHeight); + u32 xfbHeight = GX_SetDispCopyYScale(yscale); + GX_SetScissor(0, 0, rmode->fbWidth, rmode->efbHeight); + GX_SetDispCopySrc(0, 0, rmode->fbWidth, rmode->efbHeight); + GX_SetDispCopyDst(rmode->fbWidth, xfbHeight); + GX_SetCopyFilter(rmode->aa, rmode->sample_pattern, GX_TRUE, rmode->vfilter); + GX_SetFieldMode(rmode->field_rendering, ((rmode->viHeight == 2 * rmode->xfbHeight) ? GX_ENABLE : GX_DISABLE)); + + // Set pixel format + if (rmode->aa) + GX_SetPixelFmt(GX_PF_RGB565_Z16, GX_ZC_LINEAR); + else + GX_SetPixelFmt(GX_PF_RGB8_Z24, GX_ZC_LINEAR); + + // Enable depth testing + GX_SetZMode(GX_TRUE, GX_LEQUAL, GX_TRUE); + + // Enable backface culling + GX_SetCullMode(GX_CULL_BACK); + + // Enable blending + GX_SetBlendMode(GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_CLEAR); + + // Set color update + GX_SetColorUpdate(GX_TRUE); + + Logger::EngineLog("GameCube Graphics initialized (GX, %dx%d)", m_width, m_height); + m_initialized = true; + + return true; + } + + void GameCubeGraphicsContext::Shutdown() + { + if (m_initialized) + { + if (m_fifoBuffer) + { + free(m_fifoBuffer); + m_fifoBuffer = nullptr; + } + + // Framebuffers are managed by libogc, no need to free + + m_initialized = false; + Logger::EngineLog("GameCube Graphics shutdown"); + } + } + + void GameCubeGraphicsContext::BeginFrame() + { + // Nothing specific needed + } + + void GameCubeGraphicsContext::EndFrame() + { + // Execute all pending GX commands + GX_DrawDone(); + } + + void GameCubeGraphicsContext::SwapBuffers() + { + // Copy EFB to XFB + GX_CopyDisp(m_frameBuffer[m_currentFB], GX_TRUE); + GX_Flush(); + + // Swap framebuffers + VIDEO_SetNextFramebuffer(m_frameBuffer[m_currentFB]); + VIDEO_Flush(); + + if (m_vsyncEnabled) + VIDEO_WaitVSync(); + + m_currentFB ^= 1; // Toggle between 0 and 1 + } + + void GameCubeGraphicsContext::Clear(float r, float g, float b, float a) + { + GXColor color = { + static_cast(r * 255), + static_cast(g * 255), + static_cast(b * 255), + static_cast(a * 255) + }; + GX_SetCopyClear(color, 0x00FFFFFF); + } + + void GameCubeGraphicsContext::ClearDepth() + { + // GX clears depth automatically with GX_CopyDisp + } + + bool GameCubeGraphicsContext::ShouldClose() const + { + return m_shouldClose; + } + + void GameCubeGraphicsContext::SetShouldClose(bool shouldClose) + { + m_shouldClose = shouldClose; + } + + void GameCubeGraphicsContext::GetFramebufferSize(int& width, int& height) const + { + width = m_width; + height = m_height; + } + + void GameCubeGraphicsContext::GetWindowSize(int& width, int& height) const + { + width = m_width; + height = m_height; + } + + void GameCubeGraphicsContext::SetVSync(bool enabled) + { + m_vsyncEnabled = enabled; + } + + bool GameCubeGraphicsContext::IsVSyncEnabled() const + { + return m_vsyncEnabled; + } + + float GameCubeGraphicsContext::GetTime() const + { + float currentTime = static_cast(SYS_Time()) / TB_TIMER_CLOCK; + return currentTime - m_startTime; + } + + void GameCubeGraphicsContext::SetBackgroundColor(float r, float g, float b, float a) + { + m_clearColor[0] = r; + m_clearColor[1] = g; + m_clearColor[2] = b; + m_clearColor[3] = a; + } + + void GameCubeGraphicsContext::SetViewport(int x, int y, int width, int height) + { + GX_SetViewport(x, y, width, height, 0, 1); + GX_SetScissor(x, y, width, height); + } +} + +#endif // PLATFORM_GAMECUBE diff --git a/src/platform/Nintendo/GameCubeGraphicsContext.h b/src/platform/Nintendo/GameCubeGraphicsContext.h index 2c016d0..63cf2f8 100644 --- a/src/platform/Nintendo/GameCubeGraphicsContext.h +++ b/src/platform/Nintendo/GameCubeGraphicsContext.h @@ -36,8 +36,16 @@ namespace WillowVox private: bool m_initialized = false; bool m_shouldClose = false; + bool m_vsyncEnabled = true; int m_width = 640; int m_height = 480; float m_clearColor[4] = {0.1f, 0.1f, 0.1f, 1.0f}; + float m_startTime = 0.0f; + + // GX-specific framebuffer (using void* to avoid including libogc headers) + void* m_fifoBuffer = nullptr; // GX FIFO buffer + void* m_frameBuffer[2] = {nullptr, nullptr}; // Double buffering + int m_currentFB = 0; + void* m_renderMode = nullptr; // GXRModeObj* }; } diff --git a/src/platform/Nintendo/GameCubePlatform.cpp b/src/platform/Nintendo/GameCubePlatform.cpp new file mode 100644 index 0000000..5924f4e --- /dev/null +++ b/src/platform/Nintendo/GameCubePlatform.cpp @@ -0,0 +1,221 @@ +#include "GameCubePlatform.h" +#include "GameCubeGraphicsContext.h" +#include + +#ifdef PLATFORM_GAMECUBE + +// libogc includes +#include +#include +#include + +namespace WillowVox +{ + GameCubePlatform::GameCubePlatform() + { + } + + GameCubePlatform::~GameCubePlatform() + { + Shutdown(); + } + + bool GameCubePlatform::Initialize() + { + Logger::EngineLog("Initializing GameCube Platform (libogc)"); + + // Initialize PAD (GameCube controller) + PAD_Init(); + + // Initialize graphics + m_graphicsContext = std::make_unique(); + + Logger::EngineLog("GameCube Platform initialized"); + return true; + } + + void GameCubePlatform::Shutdown() + { + m_graphicsContext.reset(); + Logger::EngineLog("GameCube Platform shutdown"); + } + + void GameCubePlatform::ProcessEvents() + { + // GameCube doesn't have traditional event polling + // Input is polled directly via PAD_ScanPads() + } + + void GameCubePlatform::PollInput(InputState& outInputState) + { + UpdateGamepadInput(outInputState); + outInputState.deviceType = InputDeviceType::GameCubeController; + } + + void GameCubePlatform::UpdateGamepadInput(InputState& outInputState) + { + // Store previous button state + m_prevButtons = m_buttons; + + // Scan for button presses + PAD_ScanPads(); + + // Read controller 0 state + u16 buttonsDown = PAD_ButtonsDown(0); + u16 buttonsHeld = PAD_ButtonsHeld(0); + m_buttons = buttonsHeld; + + // Read analog sticks + s8 mainStickX = PAD_StickX(0); + s8 mainStickY = PAD_StickY(0); + s8 cStickX = PAD_SubStickX(0); + s8 cStickY = PAD_SubStickY(0); + + m_mainStickX = NormalizeAxis(mainStickX); + m_mainStickY = NormalizeAxis(mainStickY); + m_cStickX = NormalizeAxis(cStickX); + m_cStickY = NormalizeAxis(cStickY); + + // Read triggers + u8 triggerL = PAD_TriggerL(0); + u8 triggerR = PAD_TriggerR(0); + + m_leftTrigger = NormalizeTrigger(triggerL); + m_rightTrigger = NormalizeTrigger(triggerR); + + // Map analog sticks + outInputState.moveAxisX = m_mainStickX; + outInputState.moveAxisY = m_mainStickY; + outInputState.lookAxisX = m_cStickX * 100.0f; + outInputState.lookAxisY = -m_cStickY * 100.0f; // Invert Y for camera + + // Button mapping (GameCube controller) + bool btnA = (m_buttons & PAD_BUTTON_A) != 0; + bool btnB = (m_buttons & PAD_BUTTON_B) != 0; + bool btnX = (m_buttons & PAD_BUTTON_X) != 0; + bool btnY = (m_buttons & PAD_BUTTON_Y) != 0; + bool btnZ = (m_buttons & PAD_TRIGGER_Z) != 0; + bool btnL = (m_buttons & PAD_TRIGGER_L) != 0; + bool btnR = (m_buttons & PAD_TRIGGER_R) != 0; + bool btnStart = (m_buttons & PAD_BUTTON_START) != 0; + bool btnDpadUp = (m_buttons & PAD_BUTTON_UP) != 0; + bool btnDpadDown = (m_buttons & PAD_BUTTON_DOWN) != 0; + bool btnDpadLeft = (m_buttons & PAD_BUTTON_LEFT) != 0; + bool btnDpadRight = (m_buttons & PAD_BUTTON_RIGHT) != 0; + + // Map to abstract actions + outInputState.actionsHeld[static_cast(InputAction::Action1)] = btnA; + outInputState.actionsHeld[static_cast(InputAction::Action2)] = btnB; + outInputState.actionsHeld[static_cast(InputAction::Action3)] = btnX; + outInputState.actionsHeld[static_cast(InputAction::Action4)] = btnY; + outInputState.actionsHeld[static_cast(InputAction::Jump)] = btnA; + outInputState.actionsHeld[static_cast(InputAction::Crouch)] = btnZ; + outInputState.actionsHeld[static_cast(InputAction::CycleLeft)] = btnL; + outInputState.actionsHeld[static_cast(InputAction::CycleRight)] = btnR; + outInputState.actionsHeld[static_cast(InputAction::MenuOpen)] = btnStart; + outInputState.actionsHeld[static_cast(InputAction::MenuBack)] = btnB; + + // Analog triggers for sprint/crouch + if (m_leftTrigger > 0.5f) + outInputState.actionsHeld[static_cast(InputAction::CycleLeft)] = true; + if (m_rightTrigger > 0.5f) + outInputState.actionsHeld[static_cast(InputAction::Sprint)] = true; + + // D-pad for movement + outInputState.actionsHeld[static_cast(InputAction::MoveForward)] = btnDpadUp; + outInputState.actionsHeld[static_cast(InputAction::MoveBackward)] = btnDpadDown; + outInputState.actionsHeld[static_cast(InputAction::MoveLeft)] = btnDpadLeft; + outInputState.actionsHeld[static_cast(InputAction::MoveRight)] = btnDpadRight; + + // Pressed this frame + outInputState.actions[static_cast(InputAction::Action1)] = (buttonsDown & PAD_BUTTON_A) != 0; + outInputState.actions[static_cast(InputAction::Action2)] = (buttonsDown & PAD_BUTTON_B) != 0; + outInputState.actions[static_cast(InputAction::MenuOpen)] = (buttonsDown & PAD_BUTTON_START) != 0; + outInputState.actions[static_cast(InputAction::Jump)] = (buttonsDown & PAD_BUTTON_A) != 0; + + // Analog to digital movement + if (std::abs(outInputState.moveAxisY) > 0.3f) + { + if (outInputState.moveAxisY > 0.3f) + outInputState.actionsHeld[static_cast(InputAction::MoveForward)] = true; + else + outInputState.actionsHeld[static_cast(InputAction::MoveBackward)] = true; + } + if (std::abs(outInputState.moveAxisX) > 0.3f) + { + if (outInputState.moveAxisX > 0.3f) + outInputState.actionsHeld[static_cast(InputAction::MoveRight)] = true; + else + outInputState.actionsHeld[static_cast(InputAction::MoveLeft)] = true; + } + } + + float GameCubePlatform::NormalizeAxis(int8_t value) const + { + // GameCube sticks are -128 to 127 + float normalized = static_cast(value) / 127.0f; + + // Apply deadzone + const float deadzone = 0.15f; + if (std::abs(normalized) < deadzone) + return 0.0f; + + // Rescale to account for deadzone + float sign = (normalized > 0.0f) ? 1.0f : -1.0f; + float absValue = std::abs(normalized); + return sign * ((absValue - deadzone) / (1.0f - deadzone)); + } + + float GameCubePlatform::NormalizeTrigger(uint8_t value) const + { + // GameCube triggers are 0-255 + float normalized = static_cast(value) / 255.0f; + + // Apply trigger deadzone + const float deadzone = 0.1f; + if (normalized < deadzone) + return 0.0f; + + // Rescale to account for deadzone + return (normalized - deadzone) / (1.0f - deadzone); + } + + void GameCubePlatform::ResetInputFrameState(InputState& inputState) + { + inputState.ResetFrameStates(); + } + + IGraphicsContext* GameCubePlatform::GetGraphicsContext() + { + return m_graphicsContext.get(); + } + + const char* GameCubePlatform::GetUserDataPath() const + { + return "sd:/WillowVox/userdata"; + } + + const char* GameCubePlatform::GetAssetsPath() const + { + return "sd:/WillowVox/assets"; + } + + const char* GameCubePlatform::GetPlatformName() const + { + return "Nintendo GameCube"; + } + + InputDeviceType GameCubePlatform::GetPrimaryInputDevice() const + { + return InputDeviceType::GameCubeController; + } + + bool GameCubePlatform::HasFeature(const char* featureName) const + { + if (strcmp(featureName, "gamepad") == 0) return true; + if (strcmp(featureName, "filesystem") == 0) return true; + return false; + } +} + +#endif // PLATFORM_GAMECUBE diff --git a/src/platform/Nintendo/GameCubePlatform.h b/src/platform/Nintendo/GameCubePlatform.h index 3e31167..cf876b6 100644 --- a/src/platform/Nintendo/GameCubePlatform.h +++ b/src/platform/Nintendo/GameCubePlatform.h @@ -44,5 +44,19 @@ namespace WillowVox private: std::unique_ptr m_graphicsContext; + + // GameCube controller input state + uint16_t m_buttons = 0; + uint16_t m_prevButtons = 0; + float m_mainStickX = 0.0f; + float m_mainStickY = 0.0f; + float m_cStickX = 0.0f; + float m_cStickY = 0.0f; + float m_leftTrigger = 0.0f; + float m_rightTrigger = 0.0f; + + void UpdateGamepadInput(InputState& outInputState); + float NormalizeAxis(int8_t value) const; + float NormalizeTrigger(uint8_t value) const; }; } diff --git a/src/platform/Nintendo/WiiGraphicsContext.cpp b/src/platform/Nintendo/WiiGraphicsContext.cpp new file mode 100644 index 0000000..31a5b2a --- /dev/null +++ b/src/platform/Nintendo/WiiGraphicsContext.cpp @@ -0,0 +1,225 @@ +#include "WiiGraphicsContext.h" +#include + +#ifdef PLATFORM_WII + +// libogc includes +#include +#include +#include +#include +#include +#include + +#define DEFAULT_FIFO_SIZE (256 * 1024) + +namespace WillowVox +{ + WiiGraphicsContext::WiiGraphicsContext() + { + m_startTime = static_cast(SYS_Time()) / TB_TIMER_CLOCK; + } + + WiiGraphicsContext::~WiiGraphicsContext() + { + Shutdown(); + } + + bool WiiGraphicsContext::Initialize(int width, int height, const char* title) + { + Logger::EngineLog("Initializing Wii Graphics (GX)"); + + // Initialize VIDEO subsystem + VIDEO_Init(); + + // Get preferred video mode + GXRModeObj* rmode = VIDEO_GetPreferredMode(NULL); + m_renderMode = rmode; + + m_width = rmode->fbWidth; + m_height = rmode->efbHeight; + + Logger::EngineLog("Video mode: %dx%d", m_width, m_height); + + // Allocate framebuffers (double buffering) + m_frameBuffer[0] = MEM_K0_TO_K1(SYS_AllocateFramebuffer(rmode)); + m_frameBuffer[1] = MEM_K0_TO_K1(SYS_AllocateFramebuffer(rmode)); + + if (!m_frameBuffer[0] || !m_frameBuffer[1]) + { + Logger::EngineError("Failed to allocate framebuffers"); + return false; + } + + // Configure VIDEO + VIDEO_Configure(rmode); + VIDEO_SetNextFramebuffer(m_frameBuffer[0]); + VIDEO_SetBlack(FALSE); + VIDEO_Flush(); + VIDEO_WaitVSync(); + if (rmode->viTVMode & VI_NON_INTERLACE) + VIDEO_WaitVSync(); + + // Allocate GX FIFO buffer + m_fifoBuffer = memalign(32, DEFAULT_FIFO_SIZE); + if (!m_fifoBuffer) + { + Logger::EngineError("Failed to allocate GX FIFO buffer"); + return false; + } + memset(m_fifoBuffer, 0, DEFAULT_FIFO_SIZE); + + // Initialize GX + GX_Init(m_fifoBuffer, DEFAULT_FIFO_SIZE); + + // Set up GX for 3D rendering + GX_SetCopyClear((GXColor){26, 26, 26, 255}, 0x00FFFFFF); + + // Set viewport + GX_SetViewport(0, 0, rmode->fbWidth, rmode->efbHeight, 0, 1); + f32 yscale = GX_GetYScaleFactor(rmode->efbHeight, rmode->xfbHeight); + u32 xfbHeight = GX_SetDispCopyYScale(yscale); + GX_SetScissor(0, 0, rmode->fbWidth, rmode->efbHeight); + GX_SetDispCopySrc(0, 0, rmode->fbWidth, rmode->efbHeight); + GX_SetDispCopyDst(rmode->fbWidth, xfbHeight); + GX_SetCopyFilter(rmode->aa, rmode->sample_pattern, GX_TRUE, rmode->vfilter); + GX_SetFieldMode(rmode->field_rendering, ((rmode->viHeight == 2 * rmode->xfbHeight) ? GX_ENABLE : GX_DISABLE)); + + // Set pixel format + if (rmode->aa) + GX_SetPixelFmt(GX_PF_RGB565_Z16, GX_ZC_LINEAR); + else + GX_SetPixelFmt(GX_PF_RGB8_Z24, GX_ZC_LINEAR); + + // Enable depth testing + GX_SetZMode(GX_TRUE, GX_LEQUAL, GX_TRUE); + + // Enable backface culling + GX_SetCullMode(GX_CULL_BACK); + + // Enable blending + GX_SetBlendMode(GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_CLEAR); + + // Set color update + GX_SetColorUpdate(GX_TRUE); + + Logger::EngineLog("Wii Graphics initialized (GX, %dx%d)", m_width, m_height); + m_initialized = true; + + return true; + } + + void WiiGraphicsContext::Shutdown() + { + if (m_initialized) + { + if (m_fifoBuffer) + { + free(m_fifoBuffer); + m_fifoBuffer = nullptr; + } + + // Framebuffers are managed by libogc, no need to free + + m_initialized = false; + Logger::EngineLog("Wii Graphics shutdown"); + } + } + + void WiiGraphicsContext::BeginFrame() + { + // Nothing specific needed + } + + void WiiGraphicsContext::EndFrame() + { + // Execute all pending GX commands + GX_DrawDone(); + } + + void WiiGraphicsContext::SwapBuffers() + { + // Copy EFB to XFB + GX_CopyDisp(m_frameBuffer[m_currentFB], GX_TRUE); + GX_Flush(); + + // Swap framebuffers + VIDEO_SetNextFramebuffer(m_frameBuffer[m_currentFB]); + VIDEO_Flush(); + + if (m_vsyncEnabled) + VIDEO_WaitVSync(); + + m_currentFB ^= 1; // Toggle between 0 and 1 + } + + void WiiGraphicsContext::Clear(float r, float g, float b, float a) + { + GXColor color = { + static_cast(r * 255), + static_cast(g * 255), + static_cast(b * 255), + static_cast(a * 255) + }; + GX_SetCopyClear(color, 0x00FFFFFF); + } + + void WiiGraphicsContext::ClearDepth() + { + // GX clears depth automatically with GX_CopyDisp + } + + bool WiiGraphicsContext::ShouldClose() const + { + return m_shouldClose; + } + + void WiiGraphicsContext::SetShouldClose(bool shouldClose) + { + m_shouldClose = shouldClose; + } + + void WiiGraphicsContext::GetFramebufferSize(int& width, int& height) const + { + width = m_width; + height = m_height; + } + + void WiiGraphicsContext::GetWindowSize(int& width, int& height) const + { + width = m_width; + height = m_height; + } + + void WiiGraphicsContext::SetVSync(bool enabled) + { + m_vsyncEnabled = enabled; + } + + bool WiiGraphicsContext::IsVSyncEnabled() const + { + return m_vsyncEnabled; + } + + float WiiGraphicsContext::GetTime() const + { + float currentTime = static_cast(SYS_Time()) / TB_TIMER_CLOCK; + return currentTime - m_startTime; + } + + void WiiGraphicsContext::SetBackgroundColor(float r, float g, float b, float a) + { + m_clearColor[0] = r; + m_clearColor[1] = g; + m_clearColor[2] = b; + m_clearColor[3] = a; + } + + void WiiGraphicsContext::SetViewport(int x, int y, int width, int height) + { + GX_SetViewport(x, y, width, height, 0, 1); + GX_SetScissor(x, y, width, height); + } +} + +#endif // PLATFORM_WII diff --git a/src/platform/Nintendo/WiiGraphicsContext.h b/src/platform/Nintendo/WiiGraphicsContext.h index d6075ec..ba20f15 100644 --- a/src/platform/Nintendo/WiiGraphicsContext.h +++ b/src/platform/Nintendo/WiiGraphicsContext.h @@ -36,8 +36,16 @@ namespace WillowVox private: bool m_initialized = false; bool m_shouldClose = false; + bool m_vsyncEnabled = true; int m_width = 640; int m_height = 480; float m_clearColor[4] = {0.1f, 0.1f, 0.1f, 1.0f}; + float m_startTime = 0.0f; + + // GX-specific framebuffer (using void* to avoid including libogc headers) + void* m_fifoBuffer = nullptr; // GX FIFO buffer + void* m_frameBuffer[2] = {nullptr, nullptr}; // Double buffering + int m_currentFB = 0; + void* m_renderMode = nullptr; // GXRModeObj* }; } diff --git a/src/platform/Nintendo/WiiPlatform.cpp b/src/platform/Nintendo/WiiPlatform.cpp new file mode 100644 index 0000000..306d573 --- /dev/null +++ b/src/platform/Nintendo/WiiPlatform.cpp @@ -0,0 +1,223 @@ +#include "WiiPlatform.h" +#include "WiiGraphicsContext.h" +#include + +#ifdef PLATFORM_WII + +// libogc includes +#include +#include +#include + +namespace WillowVox +{ + WiiPlatform::WiiPlatform() + { + } + + WiiPlatform::~WiiPlatform() + { + Shutdown(); + } + + bool WiiPlatform::Initialize() + { + Logger::EngineLog("Initializing Wii Platform (libogc)"); + + // Initialize WPAD (Wiimote) + WPAD_Init(); + WPAD_SetDataFormat(WPAD_CHAN_0, WPAD_FMT_BTNS_ACC_IR); + WPAD_SetVRes(WPAD_CHAN_0, 640, 480); + + // Initialize graphics + m_graphicsContext = std::make_unique(); + + Logger::EngineLog("Wii Platform initialized"); + return true; + } + + void WiiPlatform::Shutdown() + { + WPAD_Shutdown(); + m_graphicsContext.reset(); + Logger::EngineLog("Wii Platform shutdown"); + } + + void WiiPlatform::ProcessEvents() + { + // Wii doesn't have traditional event polling + // Input is polled directly via WPAD_ScanPads() + } + + void WiiPlatform::PollInput(InputState& outInputState) + { + UpdateWiimoteInput(outInputState); + outInputState.deviceType = InputDeviceType::WiiRemote; + } + + void WiiPlatform::UpdateWiimoteInput(InputState& outInputState) + { + // Store previous button state + m_prevButtons = m_buttons; + + // Scan for button presses + WPAD_ScanPads(); + + // Read Wiimote data + u32 pressed = WPAD_ButtonsDown(WPAD_CHAN_0); + u32 held = WPAD_ButtonsHeld(WPAD_CHAN_0); + m_buttons = held; + + // Get expansion data + WPADData* data = WPAD_Data(WPAD_CHAN_0); + + if (data && data->exp.type == WPAD_EXP_NUNCHUK) + { + // Nunchuk is connected - use for movement + joystick_t nunchuk = data->exp.nunchuk.js; + m_nunchukX = NormalizeAxis(nunchuk.pos.x, nunchuk.center.x); + m_nunchukY = NormalizeAxis(nunchuk.pos.y, nunchuk.center.y); + + // Nunchuk buttons + if (data->exp.nunchuk.btns & NUNCHUK_BUTTON_Z) + m_buttons |= WPAD_BUTTON_HOME; // Map Z to crouch + if (data->exp.nunchuk.btns & NUNCHUK_BUTTON_C) + m_buttons |= WPAD_BUTTON_1; // Map C to sprint + } + else + { + // No Nunchuk - reset analog values + m_nunchukX = 0.0f; + m_nunchukY = 0.0f; + } + + // Get IR pointer data + if (data && data->ir.valid) + { + m_irValid = true; + m_irX = static_cast(data->ir.x) / 640.0f; + m_irY = static_cast(data->ir.y) / 480.0f; + } + else + { + m_irValid = false; + } + + // Map analog sticks + outInputState.moveAxisX = m_nunchukX; + outInputState.moveAxisY = m_nunchukY; + + // IR pointer for look (if valid) + if (m_irValid) + { + // Center IR to -1..1 range + outInputState.lookAxisX = (m_irX - 0.5f) * 200.0f; + outInputState.lookAxisY = (m_irY - 0.5f) * 200.0f; + } + + // Button mapping (Wiimote) + bool btnA = (m_buttons & WPAD_BUTTON_A) != 0; + bool btnB = (m_buttons & WPAD_BUTTON_B) != 0; + bool btn1 = (m_buttons & WPAD_BUTTON_1) != 0; + bool btn2 = (m_buttons & WPAD_BUTTON_2) != 0; + bool btnHome = (m_buttons & WPAD_BUTTON_HOME) != 0; + bool btnPlus = (m_buttons & WPAD_BUTTON_PLUS) != 0; + bool btnMinus = (m_buttons & WPAD_BUTTON_MINUS) != 0; + bool btnUp = (m_buttons & WPAD_BUTTON_UP) != 0; + bool btnDown = (m_buttons & WPAD_BUTTON_DOWN) != 0; + bool btnLeft = (m_buttons & WPAD_BUTTON_LEFT) != 0; + bool btnRight = (m_buttons & WPAD_BUTTON_RIGHT) != 0; + + // Map to abstract actions + outInputState.actionsHeld[static_cast(InputAction::Action1)] = btnA; + outInputState.actionsHeld[static_cast(InputAction::Action2)] = btnB; + outInputState.actionsHeld[static_cast(InputAction::Action3)] = btn1; + outInputState.actionsHeld[static_cast(InputAction::Action4)] = btn2; + outInputState.actionsHeld[static_cast(InputAction::Jump)] = btnA; + outInputState.actionsHeld[static_cast(InputAction::MenuOpen)] = btnHome; + outInputState.actionsHeld[static_cast(InputAction::MenuBack)] = btnB; + outInputState.actionsHeld[static_cast(InputAction::CycleLeft)] = btnMinus; + outInputState.actionsHeld[static_cast(InputAction::CycleRight)] = btnPlus; + + // D-pad for movement + outInputState.actionsHeld[static_cast(InputAction::MoveForward)] = btnUp; + outInputState.actionsHeld[static_cast(InputAction::MoveBackward)] = btnDown; + outInputState.actionsHeld[static_cast(InputAction::MoveLeft)] = btnLeft; + outInputState.actionsHeld[static_cast(InputAction::MoveRight)] = btnRight; + + // Pressed this frame + outInputState.actions[static_cast(InputAction::Action1)] = (pressed & WPAD_BUTTON_A) != 0; + outInputState.actions[static_cast(InputAction::Action2)] = (pressed & WPAD_BUTTON_B) != 0; + outInputState.actions[static_cast(InputAction::MenuOpen)] = (pressed & WPAD_BUTTON_HOME) != 0; + outInputState.actions[static_cast(InputAction::Jump)] = (pressed & WPAD_BUTTON_A) != 0; + + // Analog to digital movement (Nunchuk) + if (std::abs(outInputState.moveAxisY) > 0.3f) + { + if (outInputState.moveAxisY > 0.3f) + outInputState.actionsHeld[static_cast(InputAction::MoveForward)] = true; + else + outInputState.actionsHeld[static_cast(InputAction::MoveBackward)] = true; + } + if (std::abs(outInputState.moveAxisX) > 0.3f) + { + if (outInputState.moveAxisX > 0.3f) + outInputState.actionsHeld[static_cast(InputAction::MoveRight)] = true; + else + outInputState.actionsHeld[static_cast(InputAction::MoveLeft)] = true; + } + } + + float WiiPlatform::NormalizeAxis(uint8_t value, uint8_t center) const + { + // Nunchuk stick is 0-255, center varies by device + float normalized = (static_cast(value) - static_cast(center)) / 128.0f; + + // Apply deadzone + const float deadzone = 0.15f; + if (std::abs(normalized) < deadzone) + return 0.0f; + + return normalized; + } + + void WiiPlatform::ResetInputFrameState(InputState& inputState) + { + inputState.ResetFrameStates(); + } + + IGraphicsContext* WiiPlatform::GetGraphicsContext() + { + return m_graphicsContext.get(); + } + + const char* WiiPlatform::GetUserDataPath() const + { + return "sd:/WillowVox/userdata"; + } + + const char* WiiPlatform::GetAssetsPath() const + { + return "sd:/WillowVox/assets"; + } + + const char* WiiPlatform::GetPlatformName() const + { + return "Nintendo Wii"; + } + + InputDeviceType WiiPlatform::GetPrimaryInputDevice() const + { + return InputDeviceType::WiiRemote; + } + + bool WiiPlatform::HasFeature(const char* featureName) const + { + if (strcmp(featureName, "gamepad") == 0) return true; + if (strcmp(featureName, "motion") == 0) return true; + if (strcmp(featureName, "filesystem") == 0) return true; + return false; + } +} + +#endif // PLATFORM_WII diff --git a/src/platform/Nintendo/WiiPlatform.h b/src/platform/Nintendo/WiiPlatform.h index c560ed8..724ff8f 100644 --- a/src/platform/Nintendo/WiiPlatform.h +++ b/src/platform/Nintendo/WiiPlatform.h @@ -43,6 +43,17 @@ namespace WillowVox private: std::unique_ptr m_graphicsContext; - // Wii-specific input handling + + // Wiimote + Nunchuk input state + uint32_t m_buttons = 0; + uint32_t m_prevButtons = 0; + float m_nunchukX = 0.0f; + float m_nunchukY = 0.0f; + float m_irX = 0.0f; + float m_irY = 0.0f; + bool m_irValid = false; + + void UpdateWiimoteInput(InputState& outInputState); + float NormalizeAxis(uint8_t value, uint8_t center = 128) const; }; } diff --git a/src/platform/Nintendo/WiiUGraphicsContext.cpp b/src/platform/Nintendo/WiiUGraphicsContext.cpp new file mode 100644 index 0000000..5c77415 --- /dev/null +++ b/src/platform/Nintendo/WiiUGraphicsContext.cpp @@ -0,0 +1,275 @@ +#include "WiiUGraphicsContext.h" +#include + +#ifdef PLATFORM_WIIU + +// wut library includes (Wii U homebrew) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WillowVox +{ + WiiUGraphicsContext::WiiUGraphicsContext() + { + m_startTime = static_cast(OSGetTime()) / static_cast(OSTimerClockSpeed); + } + + WiiUGraphicsContext::~WiiUGraphicsContext() + { + Shutdown(); + } + + bool WiiUGraphicsContext::Initialize(int width, int height, const char* title) + { + Logger::EngineLog("Initializing Wii U Graphics (GX2)"); + + // Get TV scan buffer dimensions + m_width = 1280; // Wii U TV default + m_height = 720; + + // Initialize GX2 + GX2Init(NULL); + + // Allocate color buffer + GX2ColorBuffer* colorBuffer = (GX2ColorBuffer*)memalign(0x100, sizeof(GX2ColorBuffer)); + if (!colorBuffer) + { + Logger::EngineError("Failed to allocate color buffer"); + return false; + } + memset(colorBuffer, 0, sizeof(GX2ColorBuffer)); + m_colorBuffer = colorBuffer; + + // Setup color buffer + colorBuffer->surface.dim = GX2_SURFACE_DIM_TEXTURE_2D; + colorBuffer->surface.width = m_width; + colorBuffer->surface.height = m_height; + colorBuffer->surface.depth = 1; + colorBuffer->surface.mipLevels = 1; + colorBuffer->surface.format = GX2_SURFACE_FORMAT_UNORM_R8_G8_B8_A8; + colorBuffer->surface.use = GX2_SURFACE_USE_TEXTURE_COLOR_BUFFER_TV; + colorBuffer->viewNumSlices = 1; + + // Calculate surface size + GX2CalcSurfaceSizeAndAlignment(&colorBuffer->surface); + GX2InitColorBufferRegs(colorBuffer); + + // Allocate color buffer surface + colorBuffer->surface.image = memalign(colorBuffer->surface.alignment, colorBuffer->surface.imageSize); + if (!colorBuffer->surface.image) + { + Logger::EngineError("Failed to allocate color buffer surface"); + return false; + } + + // Allocate depth buffer + GX2DepthBuffer* depthBuffer = (GX2DepthBuffer*)memalign(0x100, sizeof(GX2DepthBuffer)); + if (!depthBuffer) + { + Logger::EngineError("Failed to allocate depth buffer"); + return false; + } + memset(depthBuffer, 0, sizeof(GX2DepthBuffer)); + m_depthBuffer = depthBuffer; + + // Setup depth buffer + depthBuffer->surface.dim = GX2_SURFACE_DIM_TEXTURE_2D; + depthBuffer->surface.width = m_width; + depthBuffer->surface.height = m_height; + depthBuffer->surface.depth = 1; + depthBuffer->surface.mipLevels = 1; + depthBuffer->surface.format = GX2_SURFACE_FORMAT_FLOAT_D24_S8; + depthBuffer->surface.use = GX2_SURFACE_USE_DEPTH_BUFFER; + depthBuffer->viewNumSlices = 1; + + // Calculate depth surface size + GX2CalcSurfaceSizeAndAlignment(&depthBuffer->surface); + GX2InitDepthBufferRegs(depthBuffer); + + // Allocate depth buffer surface + depthBuffer->surface.image = memalign(depthBuffer->surface.alignment, depthBuffer->surface.imageSize); + if (!depthBuffer->surface.image) + { + Logger::EngineError("Failed to allocate depth buffer surface"); + return false; + } + + // Allocate context state + GX2ContextState* contextState = (GX2ContextState*)memalign(GX2_CONTEXT_STATE_ALIGNMENT, sizeof(GX2ContextState)); + if (!contextState) + { + Logger::EngineError("Failed to allocate context state"); + return false; + } + GX2SetupContextStateEx(contextState, GX2_TRUE); + m_contextState = contextState; + + // Set context state + GX2SetContextState(contextState); + + // Set color and depth buffers + GX2SetColorBuffer(colorBuffer, GX2_RENDER_TARGET_0); + GX2SetDepthBuffer(depthBuffer); + + // Set viewport + GX2SetViewport(0, 0, m_width, m_height, 0.0f, 1.0f); + GX2SetScissor(0, 0, m_width, m_height); + + // Enable depth test + GX2SetDepthOnlyControl(GX2_ENABLE, GX2_ENABLE, GX2_COMPARE_FUNC_LEQUAL); + + // Enable backface culling + GX2SetCullOnlyControl(GX2_FRONT_FACE_CCW, GX2_ENABLE, GX2_DISABLE); + + // Enable alpha blending + GX2SetColorControl(GX2_LOGIC_OP_COPY, GX2_ENABLE, GX2_DISABLE, GX2_ENABLE); + GX2SetBlendControl(GX2_RENDER_TARGET_0, GX2_BLEND_MODE_SRC_ALPHA, GX2_BLEND_MODE_INV_SRC_ALPHA, + GX2_BLEND_COMBINE_MODE_ADD, GX2_ENABLE, + GX2_BLEND_MODE_SRC_ALPHA, GX2_BLEND_MODE_INV_SRC_ALPHA, + GX2_BLEND_COMBINE_MODE_ADD); + + Logger::EngineLog("Wii U Graphics initialized (GX2, %dx%d)", m_width, m_height); + m_initialized = true; + + return true; + } + + void WiiUGraphicsContext::Shutdown() + { + if (m_initialized) + { + if (m_colorBuffer) + { + GX2ColorBuffer* cb = (GX2ColorBuffer*)m_colorBuffer; + if (cb->surface.image) + free(cb->surface.image); + free(cb); + m_colorBuffer = nullptr; + } + + if (m_depthBuffer) + { + GX2DepthBuffer* db = (GX2DepthBuffer*)m_depthBuffer; + if (db->surface.image) + free(db->surface.image); + free(db); + m_depthBuffer = nullptr; + } + + if (m_contextState) + { + free(m_contextState); + m_contextState = nullptr; + } + + GX2Shutdown(); + m_initialized = false; + Logger::EngineLog("Wii U Graphics shutdown"); + } + } + + void WiiUGraphicsContext::BeginFrame() + { + // Nothing specific needed + } + + void WiiUGraphicsContext::EndFrame() + { + // Flush GPU + GX2Flush(); + } + + void WiiUGraphicsContext::SwapBuffers() + { + // Copy color buffer to TV scan buffer + GX2CopyColorBufferToScanBuffer((GX2ColorBuffer*)m_colorBuffer, GX2_SCAN_TARGET_TV); + + // Swap scan buffers + GX2SwapScanBuffers(); + + if (m_vsyncEnabled) + { + GX2WaitForVsync(); + } + + // Flush and wait + GX2Flush(); + GX2DrawDone(); + } + + void WiiUGraphicsContext::Clear(float r, float g, float b, float a) + { + GX2ClearColor((GX2ColorBuffer*)m_colorBuffer, r, g, b, a); + GX2ClearDepthStencilEx((GX2DepthBuffer*)m_depthBuffer, 1.0f, 0, GX2_CLEAR_FLAGS_BOTH); + } + + void WiiUGraphicsContext::ClearDepth() + { + GX2ClearDepthStencilEx((GX2DepthBuffer*)m_depthBuffer, 1.0f, 0, GX2_CLEAR_FLAGS_DEPTH); + } + + bool WiiUGraphicsContext::ShouldClose() const + { + return m_shouldClose; + } + + void WiiUGraphicsContext::SetShouldClose(bool shouldClose) + { + m_shouldClose = shouldClose; + } + + void WiiUGraphicsContext::GetFramebufferSize(int& width, int& height) const + { + width = m_width; + height = m_height; + } + + void WiiUGraphicsContext::GetWindowSize(int& width, int& height) const + { + width = m_width; + height = m_height; + } + + void WiiUGraphicsContext::SetVSync(bool enabled) + { + m_vsyncEnabled = enabled; + } + + bool WiiUGraphicsContext::IsVSyncEnabled() const + { + return m_vsyncEnabled; + } + + float WiiUGraphicsContext::GetTime() const + { + float currentTime = static_cast(OSGetTime()) / static_cast(OSTimerClockSpeed); + return currentTime - m_startTime; + } + + void WiiUGraphicsContext::SetBackgroundColor(float r, float g, float b, float a) + { + m_clearColor[0] = r; + m_clearColor[1] = g; + m_clearColor[2] = b; + m_clearColor[3] = a; + } + + void WiiUGraphicsContext::SetViewport(int x, int y, int width, int height) + { + GX2SetViewport(x, y, width, height, 0.0f, 1.0f); + GX2SetScissor(x, y, width, height); + } +} + +#endif // PLATFORM_WIIU diff --git a/src/platform/Nintendo/WiiUGraphicsContext.h b/src/platform/Nintendo/WiiUGraphicsContext.h index 6f33f30..370422a 100644 --- a/src/platform/Nintendo/WiiUGraphicsContext.h +++ b/src/platform/Nintendo/WiiUGraphicsContext.h @@ -36,8 +36,16 @@ namespace WillowVox private: bool m_initialized = false; bool m_shouldClose = false; + bool m_vsyncEnabled = true; int m_width = 1920; int m_height = 1080; float m_clearColor[4] = {0.1f, 0.1f, 0.1f, 1.0f}; + float m_startTime = 0.0f; + + // GX2-specific (using void* to avoid including wut headers) + void* m_colorBuffer = nullptr; // GX2ColorBuffer* + void* m_depthBuffer = nullptr; // GX2DepthBuffer* + void* m_contextState = nullptr; // GX2ContextState* + int m_currentTV = 0; // TV scan buffer index }; } diff --git a/src/platform/Nintendo/WiiUPlatform.cpp b/src/platform/Nintendo/WiiUPlatform.cpp new file mode 100644 index 0000000..f6a6726 --- /dev/null +++ b/src/platform/Nintendo/WiiUPlatform.cpp @@ -0,0 +1,211 @@ +#include "WiiUPlatform.h" +#include "WiiUGraphicsContext.h" +#include + +#ifdef PLATFORM_WIIU + +// wut library includes (Wii U homebrew) +#include +#include +#include +#include + +namespace WillowVox +{ + WiiUPlatform::WiiUPlatform() + { + } + + WiiUPlatform::~WiiUPlatform() + { + Shutdown(); + } + + bool WiiUPlatform::Initialize() + { + Logger::EngineLog("Initializing Wii U Platform (wut)"); + + // Initialize VPAD (GamePad) + VPADInit(); + + // Initialize KPAD (Pro Controller / Wiimote) + KPADInit(); + + // Initialize graphics + m_graphicsContext = std::make_unique(); + + Logger::EngineLog("Wii U Platform initialized"); + return true; + } + + void WiiUPlatform::Shutdown() + { + KPADShutdown(); + VPADShutdown(); + m_graphicsContext.reset(); + Logger::EngineLog("Wii U Platform shutdown"); + } + + void WiiUPlatform::ProcessEvents() + { + // Wii U doesn't have traditional event polling + // Input is polled directly via VPADRead() + } + + void WiiUPlatform::PollInput(InputState& outInputState) + { + UpdateGamepadInput(outInputState); + outInputState.deviceType = InputDeviceType::WiiUGamePad; + } + + void WiiUPlatform::UpdateGamepadInput(InputState& outInputState) + { + // Store previous button state + m_prevButtons = m_buttons; + + // Read GamePad data (VPAD) + VPADStatus vpadStatus; + VPADReadError error; + VPADRead(VPAD_CHAN_0, &vpadStatus, 1, &error); + + if (error == VPAD_READ_SUCCESS) + { + // Store button state + m_buttons = vpadStatus.hold; + + // Read analog sticks + m_lstickX = NormalizeAxis(vpadStatus.leftStick.x, -1.0f, 1.0f); + m_lstickY = NormalizeAxis(vpadStatus.leftStick.y, -1.0f, 1.0f); + m_rstickX = NormalizeAxis(vpadStatus.rightStick.x, -1.0f, 1.0f); + m_rstickY = NormalizeAxis(vpadStatus.rightStick.y, -1.0f, 1.0f); + } + else + { + // No input, reset to defaults + m_buttons = 0; + m_lstickX = 0.0f; + m_lstickY = 0.0f; + m_rstickX = 0.0f; + m_rstickY = 0.0f; + } + + // Map analog sticks + outInputState.moveAxisX = m_lstickX; + outInputState.moveAxisY = m_lstickY; + outInputState.lookAxisX = m_rstickX * 100.0f; + outInputState.lookAxisY = -m_rstickY * 100.0f; // Invert Y for camera + + // Button mapping (Wii U GamePad - similar to Pro Controller) + bool btnA = (m_buttons & VPAD_BUTTON_A) != 0; + bool btnB = (m_buttons & VPAD_BUTTON_B) != 0; + bool btnX = (m_buttons & VPAD_BUTTON_X) != 0; + bool btnY = (m_buttons & VPAD_BUTTON_Y) != 0; + bool btnL = (m_buttons & VPAD_BUTTON_L) != 0; + bool btnR = (m_buttons & VPAD_BUTTON_R) != 0; + bool btnZL = (m_buttons & VPAD_BUTTON_ZL) != 0; + bool btnZR = (m_buttons & VPAD_BUTTON_ZR) != 0; + bool btnPlus = (m_buttons & VPAD_BUTTON_PLUS) != 0; + bool btnMinus = (m_buttons & VPAD_BUTTON_MINUS) != 0; + bool btnHome = (m_buttons & VPAD_BUTTON_HOME) != 0; + bool btnDpadUp = (m_buttons & VPAD_BUTTON_UP) != 0; + bool btnDpadDown = (m_buttons & VPAD_BUTTON_DOWN) != 0; + bool btnDpadLeft = (m_buttons & VPAD_BUTTON_LEFT) != 0; + bool btnDpadRight = (m_buttons & VPAD_BUTTON_RIGHT) != 0; + + // Map to abstract actions + outInputState.actionsHeld[static_cast(InputAction::Action1)] = btnA; + outInputState.actionsHeld[static_cast(InputAction::Action2)] = btnB; + outInputState.actionsHeld[static_cast(InputAction::Action3)] = btnX; + outInputState.actionsHeld[static_cast(InputAction::Action4)] = btnY; + outInputState.actionsHeld[static_cast(InputAction::Jump)] = btnA; + outInputState.actionsHeld[static_cast(InputAction::Crouch)] = btnZL; + outInputState.actionsHeld[static_cast(InputAction::Sprint)] = btnZR; + outInputState.actionsHeld[static_cast(InputAction::CycleLeft)] = btnL; + outInputState.actionsHeld[static_cast(InputAction::CycleRight)] = btnR; + outInputState.actionsHeld[static_cast(InputAction::MenuOpen)] = btnPlus || btnHome; + outInputState.actionsHeld[static_cast(InputAction::MenuBack)] = btnB || btnMinus; + + // D-pad for movement + outInputState.actionsHeld[static_cast(InputAction::MoveForward)] = btnDpadUp; + outInputState.actionsHeld[static_cast(InputAction::MoveBackward)] = btnDpadDown; + outInputState.actionsHeld[static_cast(InputAction::MoveLeft)] = btnDpadLeft; + outInputState.actionsHeld[static_cast(InputAction::MoveRight)] = btnDpadRight; + + // Pressed this frame + uint32_t pressed = m_buttons & ~m_prevButtons; + outInputState.actions[static_cast(InputAction::Action1)] = (pressed & VPAD_BUTTON_A) != 0; + outInputState.actions[static_cast(InputAction::Action2)] = (pressed & VPAD_BUTTON_B) != 0; + outInputState.actions[static_cast(InputAction::MenuOpen)] = (pressed & VPAD_BUTTON_PLUS) != 0; + outInputState.actions[static_cast(InputAction::Jump)] = (pressed & VPAD_BUTTON_A) != 0; + + // Analog to digital movement + if (std::abs(outInputState.moveAxisY) > 0.3f) + { + if (outInputState.moveAxisY > 0.3f) + outInputState.actionsHeld[static_cast(InputAction::MoveForward)] = true; + else + outInputState.actionsHeld[static_cast(InputAction::MoveBackward)] = true; + } + if (std::abs(outInputState.moveAxisX) > 0.3f) + { + if (outInputState.moveAxisX > 0.3f) + outInputState.actionsHeld[static_cast(InputAction::MoveRight)] = true; + else + outInputState.actionsHeld[static_cast(InputAction::MoveLeft)] = true; + } + } + + float WiiUPlatform::NormalizeAxis(float value, float min, float max) const + { + // VPAD sticks are already -1.0 to 1.0 + // Apply deadzone + const float deadzone = 0.15f; + if (std::abs(value) < deadzone) + return 0.0f; + + // Rescale to account for deadzone + float sign = (value > 0.0f) ? 1.0f : -1.0f; + float absValue = std::abs(value); + return sign * ((absValue - deadzone) / (1.0f - deadzone)); + } + + void WiiUPlatform::ResetInputFrameState(InputState& inputState) + { + inputState.ResetFrameStates(); + } + + IGraphicsContext* WiiUPlatform::GetGraphicsContext() + { + return m_graphicsContext.get(); + } + + const char* WiiUPlatform::GetUserDataPath() const + { + return "fs:/vol/external01/WillowVox/userdata"; + } + + const char* WiiUPlatform::GetAssetsPath() const + { + return "fs:/vol/content/WillowVox/assets"; + } + + const char* WiiUPlatform::GetPlatformName() const + { + return "Nintendo Wii U"; + } + + InputDeviceType WiiUPlatform::GetPrimaryInputDevice() const + { + return InputDeviceType::WiiUGamePad; + } + + bool WiiUPlatform::HasFeature(const char* featureName) const + { + if (strcmp(featureName, "gamepad") == 0) return true; + if (strcmp(featureName, "touchscreen") == 0) return true; // GamePad has touchscreen + if (strcmp(featureName, "filesystem") == 0) return true; + return false; + } +} + +#endif // PLATFORM_WIIU diff --git a/src/platform/Nintendo/WiiUPlatform.h b/src/platform/Nintendo/WiiUPlatform.h index a54a0e9..c5a4aaa 100644 --- a/src/platform/Nintendo/WiiUPlatform.h +++ b/src/platform/Nintendo/WiiUPlatform.h @@ -36,5 +36,16 @@ namespace WillowVox private: std::unique_ptr m_graphicsContext; + + // Wii U GamePad/Pro Controller input state + uint32_t m_buttons = 0; + uint32_t m_prevButtons = 0; + float m_lstickX = 0.0f; + float m_lstickY = 0.0f; + float m_rstickX = 0.0f; + float m_rstickY = 0.0f; + + void UpdateGamepadInput(InputState& outInputState); + float NormalizeAxis(float value, float min, float max) const; }; } diff --git a/src/platform/PS4/PS4GraphicsContext.cpp b/src/platform/PS4/PS4GraphicsContext.cpp new file mode 100644 index 0000000..b583bde --- /dev/null +++ b/src/platform/PS4/PS4GraphicsContext.cpp @@ -0,0 +1,255 @@ +#include "PS4GraphicsContext.h" +#include + +#ifdef PLATFORM_PS4 + +// OpenOrbis includes +#include +#include +#include +#include +#include + +namespace WillowVox +{ + PS4GraphicsContext::PS4GraphicsContext() + { + m_startTime = sceKernelGetProcessTime() / 1000000000.0f; + } + + PS4GraphicsContext::~PS4GraphicsContext() + { + Shutdown(); + } + + bool PS4GraphicsContext::Initialize(int width, int height, const char* title) + { + m_width = width; + m_height = height; + + Logger::EngineLog("Initializing PS4 Graphics (OpenGL via piglet)"); + + // Initialize video out + int ret = sceVideoOutOpen(ORBIS_USER_SERVICE_USER_ID_SYSTEM, ORBIS_VIDEO_OUT_BUS_TYPE_MAIN, 0, NULL); + if (ret < 0) + { + Logger::EngineError("Failed to open video out: 0x%08X", ret); + return false; + } + m_videoHandle = ret; + + // Initialize piglet (OpenGL wrapper for PS4) + ret = scePigletSetConfigurationVSH(SCE_PIGLET_CONFIGURATION_VSH_API_VERSION, + SCE_PIGLET_API_VERSION_GLES20); + if (ret != 0) + { + Logger::EngineError("Failed to set piglet configuration: 0x%08X", ret); + return false; + } + + // Get EGL display + EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (display == EGL_NO_DISPLAY) + { + Logger::EngineError("eglGetDisplay failed"); + return false; + } + m_eglDisplay = display; + + // Initialize EGL + if (!eglInitialize(display, nullptr, nullptr)) + { + Logger::EngineError("eglInitialize failed"); + return false; + } + + // Choose EGL config + const EGLint attribs[] = { + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_BLUE_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_RED_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_DEPTH_SIZE, 24, + EGL_STENCIL_SIZE, 8, + EGL_NONE + }; + + EGLConfig config; + EGLint numConfigs; + if (!eglChooseConfig(display, attribs, &config, 1, &numConfigs)) + { + Logger::EngineError("eglChooseConfig failed"); + return false; + } + + // Create EGL window surface + EGLSurface surface = eglCreateWindowSurface(display, config, + (EGLNativeWindowType)m_videoHandle, nullptr); + if (surface == EGL_NO_SURFACE) + { + Logger::EngineError("eglCreateWindowSurface failed"); + return false; + } + m_eglSurface = surface; + + // Create EGL context + const EGLint contextAttribs[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE + }; + + EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs); + if (context == EGL_NO_CONTEXT) + { + Logger::EngineError("eglCreateContext failed"); + return false; + } + m_eglContext = context; + + // Make context current + if (!eglMakeCurrent(display, surface, surface, context)) + { + Logger::EngineError("eglMakeCurrent failed"); + return false; + } + + // Get actual surface size + eglQuerySurface(display, surface, EGL_WIDTH, &m_width); + eglQuerySurface(display, surface, EGL_HEIGHT, &m_height); + + // Enable OpenGL ES features + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LEQUAL); + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + Logger::EngineLog("PS4 Graphics initialized (OpenGL ES 2.0, %dx%d)", m_width, m_height); + m_initialized = true; + + return true; + } + + void PS4GraphicsContext::Shutdown() + { + if (m_initialized) + { + if (m_eglDisplay != EGL_NO_DISPLAY) + { + eglMakeCurrent((EGLDisplay)m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + + if (m_eglContext != EGL_NO_CONTEXT) + { + eglDestroyContext((EGLDisplay)m_eglDisplay, (EGLContext)m_eglContext); + m_eglContext = EGL_NO_CONTEXT; + } + + if (m_eglSurface != EGL_NO_SURFACE) + { + eglDestroySurface((EGLDisplay)m_eglDisplay, (EGLSurface)m_eglSurface); + m_eglSurface = EGL_NO_SURFACE; + } + + eglTerminate((EGLDisplay)m_eglDisplay); + m_eglDisplay = EGL_NO_DISPLAY; + } + + if (m_videoHandle >= 0) + { + sceVideoOutClose(m_videoHandle); + m_videoHandle = -1; + } + + m_initialized = false; + } + } + + void PS4GraphicsContext::BeginFrame() + { + // Nothing specific needed + } + + void PS4GraphicsContext::EndFrame() + { + // Nothing specific needed + } + + void PS4GraphicsContext::SwapBuffers() + { + if (m_eglDisplay && m_eglSurface) + { + eglSwapBuffers((EGLDisplay)m_eglDisplay, (EGLSurface)m_eglSurface); + } + } + + void PS4GraphicsContext::Clear(float r, float g, float b, float a) + { + glClearColor(r, g, b, a); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + } + + void PS4GraphicsContext::ClearDepth() + { + glClear(GL_DEPTH_BUFFER_BIT); + } + + bool PS4GraphicsContext::ShouldClose() const + { + return m_shouldClose; + } + + void PS4GraphicsContext::SetShouldClose(bool shouldClose) + { + m_shouldClose = shouldClose; + } + + void PS4GraphicsContext::GetFramebufferSize(int& width, int& height) const + { + width = m_width; + height = m_height; + } + + void PS4GraphicsContext::GetWindowSize(int& width, int& height) const + { + width = m_width; + height = m_height; + } + + void PS4GraphicsContext::SetVSync(bool enabled) + { + m_vsyncEnabled = enabled; + if (m_eglDisplay) + { + eglSwapInterval((EGLDisplay)m_eglDisplay, enabled ? 1 : 0); + } + } + + bool PS4GraphicsContext::IsVSyncEnabled() const + { + return m_vsyncEnabled; + } + + float PS4GraphicsContext::GetTime() const + { + float currentTime = sceKernelGetProcessTime() / 1000000000.0f; + return currentTime - m_startTime; + } + + void PS4GraphicsContext::SetBackgroundColor(float r, float g, float b, float a) + { + m_clearColor[0] = r; + m_clearColor[1] = g; + m_clearColor[2] = b; + m_clearColor[3] = a; + } + + void PS4GraphicsContext::SetViewport(int x, int y, int width, int height) + { + glViewport(x, y, width, height); + } +} + +#endif // PLATFORM_PS4 diff --git a/src/platform/PS4/PS4GraphicsContext.h b/src/platform/PS4/PS4GraphicsContext.h index 36c3d72..e1fb384 100644 --- a/src/platform/PS4/PS4GraphicsContext.h +++ b/src/platform/PS4/PS4GraphicsContext.h @@ -5,10 +5,10 @@ namespace WillowVox { /** - * PS4 Graphics Context (GNM) + * PS4 Graphics Context (OpenGL ES via piglet) * - * PS4 uses GNM (low-level graphics API) via OpenOrbis SDK. - * This is a template implementation. + * PS4 uses OpenGL ES 2.0 via piglet library (OpenOrbis SDK). + * This is easier than GNM and provides good compatibility. */ class PS4GraphicsContext : public IGraphicsContext { @@ -36,8 +36,16 @@ namespace WillowVox private: bool m_initialized = false; bool m_shouldClose = false; + bool m_vsyncEnabled = true; int m_width = 1920; int m_height = 1080; float m_clearColor[4] = {0.1f, 0.1f, 0.1f, 1.0f}; + float m_startTime = 0.0f; + + // PS4-specific handles (using void* to avoid including headers) + int m_videoHandle = -1; + void* m_eglDisplay = nullptr; + void* m_eglSurface = nullptr; + void* m_eglContext = nullptr; }; } diff --git a/src/platform/PS4/PS4Platform.cpp b/src/platform/PS4/PS4Platform.cpp new file mode 100644 index 0000000..d63ecf1 --- /dev/null +++ b/src/platform/PS4/PS4Platform.cpp @@ -0,0 +1,254 @@ +#include "PS4Platform.h" +#include "PS4GraphicsContext.h" +#include + +#ifdef PLATFORM_PS4 + +// OpenOrbis includes +#include +#include +#include + +namespace WillowVox +{ + PS4Platform::PS4Platform() + { + } + + PS4Platform::~PS4Platform() + { + Shutdown(); + } + + bool PS4Platform::Initialize() + { + Logger::EngineLog("Initializing PS4 Platform (OpenOrbis)"); + + // Initialize user service + int ret = sceUserServiceInitialize(NULL); + if (ret != 0) + { + Logger::EngineError("Failed to initialize user service: 0x%08X", ret); + return false; + } + + // Get initial user ID + ret = sceUserServiceGetInitialUser(&m_userId); + if (ret != 0) + { + Logger::EngineError("Failed to get initial user: 0x%08X", ret); + return false; + } + + // Initialize pad library + ret = scePadInit(); + if (ret != 0) + { + Logger::EngineError("Failed to initialize pad: 0x%08X", ret); + return false; + } + + // Open pad for user + m_padHandle = scePadOpen(m_userId, 0, 0, NULL); + if (m_padHandle < 0) + { + Logger::EngineError("Failed to open pad: 0x%08X", m_padHandle); + return false; + } + + // Initialize graphics + m_graphicsContext = std::make_unique(); + + Logger::EngineLog("PS4 Platform initialized (User ID: %d, Pad: %d)", m_userId, m_padHandle); + return true; + } + + void PS4Platform::Shutdown() + { + if (m_padHandle >= 0) + { + scePadClose(m_padHandle); + m_padHandle = -1; + } + + m_graphicsContext.reset(); + + Logger::EngineLog("PS4 Platform shutdown"); + } + + void PS4Platform::ProcessEvents() + { + // PS4 doesn't have traditional event polling like desktop + // Events are handled through system callbacks + } + + void PS4Platform::PollInput(InputState& outInputState) + { + UpdateGamepadInput(outInputState); + outInputState.deviceType = InputDeviceType::PSController; + } + + void PS4Platform::UpdateGamepadInput(InputState& outInputState) + { + if (m_padHandle < 0) + return; + + // Store previous button state + m_prevButtons = m_buttons; + + // Read pad data + ScePadData padData; + int ret = scePadReadState(m_padHandle, &padData); + + if (ret == 0 && padData.connected) + { + // Get button states + m_buttons = padData.buttons; + + // Get analog stick values + m_lstickX = padData.leftStick.x; + m_lstickY = padData.leftStick.y; + m_rstickX = padData.rightStick.x; + m_rstickY = padData.rightStick.y; + + // Trigger values + m_l2Value = padData.analogButtons.l2; + m_r2Value = padData.analogButtons.r2; + } + else + { + // Controller disconnected, reset to defaults + m_buttons = 0; + m_lstickX = 128; + m_lstickY = 128; + m_rstickX = 128; + m_rstickY = 128; + m_l2Value = 0; + m_r2Value = 0; + } + + // Analog sticks + outInputState.moveAxisX = NormalizeAxis(m_lstickX); + outInputState.moveAxisY = -NormalizeAxis(m_lstickY); + outInputState.lookAxisX = NormalizeAxis(m_rstickX) * 100.0f; + outInputState.lookAxisY = -NormalizeAxis(m_rstickY) * 100.0f; + + // Button mapping (DualShock 4) + bool btnCross = (m_buttons & SCE_PAD_BUTTON_CROSS) != 0; + bool btnCircle = (m_buttons & SCE_PAD_BUTTON_CIRCLE) != 0; + bool btnSquare = (m_buttons & SCE_PAD_BUTTON_SQUARE) != 0; + bool btnTriangle = (m_buttons & SCE_PAD_BUTTON_TRIANGLE) != 0; + bool btnL1 = (m_buttons & SCE_PAD_BUTTON_L1) != 0; + bool btnR1 = (m_buttons & SCE_PAD_BUTTON_R1) != 0; + bool btnL2 = m_l2Value > 64; // Digital threshold for analog trigger + bool btnR2 = m_r2Value > 64; + bool btnOptions = (m_buttons & SCE_PAD_BUTTON_OPTIONS) != 0; + bool btnTouchPad = (m_buttons & SCE_PAD_BUTTON_TOUCH_PAD) != 0; + + // Map to abstract actions + outInputState.actionsHeld[static_cast(InputAction::Action1)] = btnCross; + outInputState.actionsHeld[static_cast(InputAction::Action2)] = btnCircle; + outInputState.actionsHeld[static_cast(InputAction::Action3)] = btnSquare; + outInputState.actionsHeld[static_cast(InputAction::Action4)] = btnTriangle; + outInputState.actionsHeld[static_cast(InputAction::Jump)] = btnCross; + outInputState.actionsHeld[static_cast(InputAction::Crouch)] = btnL2; + outInputState.actionsHeld[static_cast(InputAction::Sprint)] = btnR2; + outInputState.actionsHeld[static_cast(InputAction::CycleLeft)] = btnL1; + outInputState.actionsHeld[static_cast(InputAction::CycleRight)] = btnR1; + outInputState.actionsHeld[static_cast(InputAction::MenuOpen)] = btnOptions; + outInputState.actionsHeld[static_cast(InputAction::MenuBack)] = btnCircle; + + // Pressed this frame + uint32_t pressed = m_buttons & ~m_prevButtons; + outInputState.actions[static_cast(InputAction::Action1)] = (pressed & SCE_PAD_BUTTON_CROSS) != 0; + outInputState.actions[static_cast(InputAction::Action2)] = (pressed & SCE_PAD_BUTTON_CIRCLE) != 0; + outInputState.actions[static_cast(InputAction::MenuOpen)] = (pressed & SCE_PAD_BUTTON_OPTIONS) != 0; + + // Analog to digital movement + if (std::abs(outInputState.moveAxisY) > 0.3f) + { + if (outInputState.moveAxisY > 0.3f) + outInputState.actionsHeld[static_cast(InputAction::MoveForward)] = true; + else + outInputState.actionsHeld[static_cast(InputAction::MoveBackward)] = true; + } + if (std::abs(outInputState.moveAxisX) > 0.3f) + { + if (outInputState.moveAxisX > 0.3f) + outInputState.actionsHeld[static_cast(InputAction::MoveRight)] = true; + else + outInputState.actionsHeld[static_cast(InputAction::MoveLeft)] = true; + } + } + + float PS4Platform::NormalizeAxis(uint8_t value) const + { + // PS4 pad axes are 0-255, center at 128 + float normalized = (static_cast(value) - 128.0f) / 128.0f; + + // Apply deadzone + const float deadzone = 0.15f; + if (std::abs(normalized) < deadzone) + return 0.0f; + + return normalized; + } + + void PS4Platform::ResetInputFrameState(InputState& inputState) + { + inputState.ResetFrameStates(); + } + + IGraphicsContext* PS4Platform::GetGraphicsContext() + { + return m_graphicsContext.get(); + } + + const char* PS4Platform::GetUserDataPath() const + { + return "/data/WillowVox/userdata"; + } + + const char* PS4Platform::GetAssetsPath() const + { + return "/app0/assets"; + } + + const char* PS4Platform::GetPlatformName() const + { + return "PlayStation 4"; + } + + InputDeviceType PS4Platform::GetPrimaryInputDevice() const + { + return InputDeviceType::PSController; + } + + bool PS4Platform::HasFeature(const char* featureName) const + { + if (strcmp(featureName, "gamepad") == 0) return true; + if (strcmp(featureName, "filesystem") == 0) return true; + return false; + } + + void PS4Platform::SetVibration(int playerIndex, float lowFrequency, float highFrequency) + { + if (m_padHandle < 0) + return; + + // PS4 uses scePadSetVibration + // Values are 0-255 + ScePadVibrationParam vibParam; + vibParam.largeMotor = static_cast(lowFrequency * 255.0f); + vibParam.smallMotor = static_cast(highFrequency * 255.0f); + + scePadSetVibration(m_padHandle, &vibParam); + } + + void PS4Platform::StopVibration(int playerIndex) + { + SetVibration(playerIndex, 0.0f, 0.0f); + } +} + +#endif // PLATFORM_PS4 diff --git a/src/platform/PS4/PS4Platform.h b/src/platform/PS4/PS4Platform.h index a0a9127..c63f0b6 100644 --- a/src/platform/PS4/PS4Platform.h +++ b/src/platform/PS4/PS4Platform.h @@ -10,11 +10,18 @@ namespace WillowVox /** * PlayStation 4 Platform (OpenOrbis Homebrew) * - * Controller: DualShock 4 (same mapping as PS3) - * Graphics: GNM (low-level) or Gnm wrapper - * Input: libSceUserService + libScePad + * Controller: DualShock 4 + * - Cross (X): Action1 / Jump + * - Circle: Action2 / MenuBack + * - Square: Action3 + * - Triangle: Action4 + * - L1/R1: Cycle items + * - L2/R2: Crouch/Sprint + * - Options: MenuOpen + * - TouchPad: Special action * - * See PS3Platform for controller mapping (similar button layout) + * Graphics: OpenGL via piglet (easier than GNM) + * Input: libSceUserService + libScePad (OpenOrbis) */ class PS4Platform : public IPlatform { @@ -34,8 +41,27 @@ namespace WillowVox InputDeviceType GetPrimaryInputDevice() const override; bool HasFeature(const char* featureName) const override; + void SetVibration(int playerIndex, float lowFrequency, float highFrequency) override; + void StopVibration(int playerIndex) override; + private: std::unique_ptr m_graphicsContext; - // PS4-specific pad handling (similar to PS3) + + // PS4-specific handles + int m_userId = 0; + int m_padHandle = -1; + + // Pad state + uint32_t m_buttons = 0; + uint32_t m_prevButtons = 0; + uint8_t m_lstickX = 128; + uint8_t m_lstickY = 128; + uint8_t m_rstickX = 128; + uint8_t m_rstickY = 128; + uint8_t m_l2Value = 0; + uint8_t m_r2Value = 0; + + void UpdateGamepadInput(InputState& outInputState); + float NormalizeAxis(uint8_t value) const; }; } diff --git a/src/platform/Xbox/XboxSeriesGraphicsContext.cpp b/src/platform/Xbox/XboxSeriesGraphicsContext.cpp new file mode 100644 index 0000000..8f01163 --- /dev/null +++ b/src/platform/Xbox/XboxSeriesGraphicsContext.cpp @@ -0,0 +1,242 @@ +#include "XboxSeriesGraphicsContext.h" +#include + +#ifdef PLATFORM_XBOX + +// UWP includes +#include +#include + +// ANGLE includes (OpenGL ES via DirectX) +#include +#include +#include +#include + +using namespace Microsoft::WRL; + +namespace WillowVox +{ + XboxSeriesGraphicsContext::XboxSeriesGraphicsContext() + { + m_startTime = static_cast(GetTickCount64()) / 1000.0f; + } + + XboxSeriesGraphicsContext::~XboxSeriesGraphicsContext() + { + Shutdown(); + } + + bool XboxSeriesGraphicsContext::Initialize(int width, int height, const char* title) + { + m_width = width; + m_height = height; + + Logger::EngineLog("Initializing Xbox Graphics (OpenGL ES via ANGLE)"); + + // Get EGL display (ANGLE will translate to DirectX) + EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (display == EGL_NO_DISPLAY) + { + Logger::EngineError("eglGetDisplay failed"); + return false; + } + m_eglDisplay = display; + + // Initialize EGL + EGLint major, minor; + if (!eglInitialize(display, &major, &minor)) + { + Logger::EngineError("eglInitialize failed"); + return false; + } + Logger::EngineLog("EGL Version: %d.%d", major, minor); + + // Choose EGL config for OpenGL ES 3.0 + const EGLint attribs[] = { + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_DEPTH_SIZE, 24, + EGL_STENCIL_SIZE, 8, + EGL_NONE + }; + + EGLConfig config; + EGLint numConfigs; + if (!eglChooseConfig(display, attribs, &config, 1, &numConfigs)) + { + Logger::EngineError("eglChooseConfig failed"); + return false; + } + + // In a full UWP implementation, you would get the CoreWindow here + // For now, we create a window surface with default parameters + EGLint surfaceAttribs[] = { + EGL_NONE + }; + + EGLSurface surface = eglCreateWindowSurface(display, config, nullptr, surfaceAttribs); + if (surface == EGL_NO_SURFACE) + { + Logger::EngineError("eglCreateWindowSurface failed: 0x%X", eglGetError()); + return false; + } + m_eglSurface = surface; + + // Create OpenGL ES 3.0 context + const EGLint contextAttribs[] = { + EGL_CONTEXT_CLIENT_VERSION, 3, + EGL_NONE + }; + + EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs); + if (context == EGL_NO_CONTEXT) + { + Logger::EngineError("eglCreateContext failed"); + return false; + } + m_eglContext = context; + + // Make context current + if (!eglMakeCurrent(display, surface, surface, context)) + { + Logger::EngineError("eglMakeCurrent failed"); + return false; + } + + // Query surface size + eglQuerySurface(display, surface, EGL_WIDTH, &m_width); + eglQuerySurface(display, surface, EGL_HEIGHT, &m_height); + + // Enable OpenGL ES features + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LEQUAL); + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + Logger::EngineLog("Xbox Graphics initialized (OpenGL ES 3.0, %dx%d)", m_width, m_height); + m_initialized = true; + + return true; + } + + void XboxSeriesGraphicsContext::Shutdown() + { + if (m_initialized) + { + if (m_eglDisplay != EGL_NO_DISPLAY) + { + eglMakeCurrent((EGLDisplay)m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + + if (m_eglContext != EGL_NO_CONTEXT) + { + eglDestroyContext((EGLDisplay)m_eglDisplay, (EGLContext)m_eglContext); + m_eglContext = EGL_NO_CONTEXT; + } + + if (m_eglSurface != EGL_NO_SURFACE) + { + eglDestroySurface((EGLDisplay)m_eglDisplay, (EGLSurface)m_eglSurface); + m_eglSurface = EGL_NO_SURFACE; + } + + eglTerminate((EGLDisplay)m_eglDisplay); + m_eglDisplay = EGL_NO_DISPLAY; + } + + m_initialized = false; + } + } + + void XboxSeriesGraphicsContext::BeginFrame() + { + // Nothing specific needed + } + + void XboxSeriesGraphicsContext::EndFrame() + { + // Nothing specific needed + } + + void XboxSeriesGraphicsContext::SwapBuffers() + { + if (m_eglDisplay && m_eglSurface) + { + eglSwapBuffers((EGLDisplay)m_eglDisplay, (EGLSurface)m_eglSurface); + } + } + + void XboxSeriesGraphicsContext::Clear(float r, float g, float b, float a) + { + glClearColor(r, g, b, a); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + } + + void XboxSeriesGraphicsContext::ClearDepth() + { + glClear(GL_DEPTH_BUFFER_BIT); + } + + bool XboxSeriesGraphicsContext::ShouldClose() const + { + return m_shouldClose; + } + + void XboxSeriesGraphicsContext::SetShouldClose(bool shouldClose) + { + m_shouldClose = shouldClose; + } + + void XboxSeriesGraphicsContext::GetFramebufferSize(int& width, int& height) const + { + width = m_width; + height = m_height; + } + + void XboxSeriesGraphicsContext::GetWindowSize(int& width, int& height) const + { + width = m_width; + height = m_height; + } + + void XboxSeriesGraphicsContext::SetVSync(bool enabled) + { + m_vsyncEnabled = enabled; + if (m_eglDisplay) + { + eglSwapInterval((EGLDisplay)m_eglDisplay, enabled ? 1 : 0); + } + } + + bool XboxSeriesGraphicsContext::IsVSyncEnabled() const + { + return m_vsyncEnabled; + } + + float XboxSeriesGraphicsContext::GetTime() const + { + float currentTime = static_cast(GetTickCount64()) / 1000.0f; + return currentTime - m_startTime; + } + + void XboxSeriesGraphicsContext::SetBackgroundColor(float r, float g, float b, float a) + { + m_clearColor[0] = r; + m_clearColor[1] = g; + m_clearColor[2] = b; + m_clearColor[3] = a; + } + + void XboxSeriesGraphicsContext::SetViewport(int x, int y, int width, int height) + { + glViewport(x, y, width, height); + } +} + +#endif // PLATFORM_XBOX diff --git a/src/platform/Xbox/XboxSeriesGraphicsContext.h b/src/platform/Xbox/XboxSeriesGraphicsContext.h index 4f03ad9..4f8c45a 100644 --- a/src/platform/Xbox/XboxSeriesGraphicsContext.h +++ b/src/platform/Xbox/XboxSeriesGraphicsContext.h @@ -5,10 +5,10 @@ namespace WillowVox { /** - * Xbox Series X/S Graphics Context (DirectX 12) + * Xbox Series X/S Graphics Context (UWP with OpenGL via ANGLE) * - * Xbox uses DirectX 12 via the GDK (Game Development Kit). - * This is a template implementation. + * Uses ANGLE (Almost Native Graphics Layer Evolution) to translate + * OpenGL ES calls to DirectX on Xbox via UWP. */ class XboxSeriesGraphicsContext : public IGraphicsContext { @@ -36,8 +36,16 @@ namespace WillowVox private: bool m_initialized = false; bool m_shouldClose = false; + bool m_vsyncEnabled = true; int m_width = 1920; int m_height = 1080; float m_clearColor[4] = {0.1f, 0.1f, 0.1f, 1.0f}; + float m_startTime = 0.0f; + + // UWP/ANGLE handles (using void* to avoid including headers) + void* m_coreWindow = nullptr; // Windows::UI::Core::CoreWindow^ + void* m_eglDisplay = nullptr; // EGLDisplay + void* m_eglSurface = nullptr; // EGLSurface + void* m_eglContext = nullptr; // EGLContext }; } diff --git a/src/platform/Xbox/XboxSeriesPlatform.cpp b/src/platform/Xbox/XboxSeriesPlatform.cpp new file mode 100644 index 0000000..29ce962 --- /dev/null +++ b/src/platform/Xbox/XboxSeriesPlatform.cpp @@ -0,0 +1,249 @@ +#include "XboxSeriesPlatform.h" +#include "XboxSeriesGraphicsContext.h" +#include + +#ifdef PLATFORM_XBOX + +// Windows/Xbox includes +#include +#include +#include + +// Link XInput library +#pragma comment(lib, "xinput.lib") + +namespace WillowVox +{ + XboxSeriesPlatform::XboxSeriesPlatform() + { + } + + XboxSeriesPlatform::~XboxSeriesPlatform() + { + Shutdown(); + } + + bool XboxSeriesPlatform::Initialize() + { + Logger::EngineLog("Initializing Xbox Series Platform (UWP)"); + + // Initialize graphics context + m_graphicsContext = std::make_unique(); + + Logger::EngineLog("Xbox Series Platform initialized"); + return true; + } + + void XboxSeriesPlatform::Shutdown() + { + m_graphicsContext.reset(); + Logger::EngineLog("Xbox Series Platform shutdown"); + } + + void XboxSeriesPlatform::ProcessEvents() + { + // UWP event processing would be handled by CoreWindow dispatcher + // This is called from the main game loop + } + + void XboxSeriesPlatform::PollInput(InputState& outInputState) + { + UpdateGamepadInput(outInputState); + outInputState.deviceType = InputDeviceType::XboxController; + } + + void XboxSeriesPlatform::UpdateGamepadInput(InputState& outInputState) + { + // Store previous button state + m_prevButtons = m_buttons; + + // Read XInput state (controller 0) + XINPUT_STATE state; + ZeroMemory(&state, sizeof(XINPUT_STATE)); + + DWORD result = XInputGetState(0, &state); + + if (result == ERROR_SUCCESS) + { + // Controller is connected + XINPUT_GAMEPAD& pad = state.Gamepad; + + // Store button state + m_buttons = pad.wButtons; + + // Store analog sticks + m_lstickX = NormalizeAxis(pad.sThumbLX); + m_lstickY = NormalizeAxis(pad.sThumbLY); + m_rstickX = NormalizeAxis(pad.sThumbRX); + m_rstickY = NormalizeAxis(pad.sThumbRY); + + // Store triggers + m_leftTrigger = NormalizeTrigger(pad.bLeftTrigger); + m_rightTrigger = NormalizeTrigger(pad.bRightTrigger); + } + else + { + // Controller is not connected, reset to defaults + m_buttons = 0; + m_lstickX = 0.0f; + m_lstickY = 0.0f; + m_rstickX = 0.0f; + m_rstickY = 0.0f; + m_leftTrigger = 0.0f; + m_rightTrigger = 0.0f; + } + + // Map analog sticks + outInputState.moveAxisX = m_lstickX; + outInputState.moveAxisY = m_lstickY; + outInputState.lookAxisX = m_rstickX * 100.0f; + outInputState.lookAxisY = -m_rstickY * 100.0f; // Invert Y for camera + + // Button mapping (Xbox controller) + bool btnA = (m_buttons & XINPUT_GAMEPAD_A) != 0; + bool btnB = (m_buttons & XINPUT_GAMEPAD_B) != 0; + bool btnX = (m_buttons & XINPUT_GAMEPAD_X) != 0; + bool btnY = (m_buttons & XINPUT_GAMEPAD_Y) != 0; + bool btnLB = (m_buttons & XINPUT_GAMEPAD_LEFT_SHOULDER) != 0; + bool btnRB = (m_buttons & XINPUT_GAMEPAD_RIGHT_SHOULDER) != 0; + bool btnLT = m_leftTrigger > 0.5f; // Digital threshold for analog trigger + bool btnRT = m_rightTrigger > 0.5f; + bool btnMenu = (m_buttons & XINPUT_GAMEPAD_START) != 0; + bool btnView = (m_buttons & XINPUT_GAMEPAD_BACK) != 0; + bool btnDpadUp = (m_buttons & XINPUT_GAMEPAD_DPAD_UP) != 0; + bool btnDpadDown = (m_buttons & XINPUT_GAMEPAD_DPAD_DOWN) != 0; + bool btnDpadLeft = (m_buttons & XINPUT_GAMEPAD_DPAD_LEFT) != 0; + bool btnDpadRight = (m_buttons & XINPUT_GAMEPAD_DPAD_RIGHT) != 0; + + // Map to abstract actions + outInputState.actionsHeld[static_cast(InputAction::Action1)] = btnA; + outInputState.actionsHeld[static_cast(InputAction::Action2)] = btnB; + outInputState.actionsHeld[static_cast(InputAction::Action3)] = btnX; + outInputState.actionsHeld[static_cast(InputAction::Action4)] = btnY; + outInputState.actionsHeld[static_cast(InputAction::Jump)] = btnA; + outInputState.actionsHeld[static_cast(InputAction::Crouch)] = btnLT; + outInputState.actionsHeld[static_cast(InputAction::Sprint)] = btnRT; + outInputState.actionsHeld[static_cast(InputAction::CycleLeft)] = btnLB; + outInputState.actionsHeld[static_cast(InputAction::CycleRight)] = btnRB; + outInputState.actionsHeld[static_cast(InputAction::MenuOpen)] = btnMenu; + outInputState.actionsHeld[static_cast(InputAction::MenuBack)] = btnB || btnView; + + // D-pad for movement + outInputState.actionsHeld[static_cast(InputAction::MoveForward)] = btnDpadUp; + outInputState.actionsHeld[static_cast(InputAction::MoveBackward)] = btnDpadDown; + outInputState.actionsHeld[static_cast(InputAction::MoveLeft)] = btnDpadLeft; + outInputState.actionsHeld[static_cast(InputAction::MoveRight)] = btnDpadRight; + + // Pressed this frame + uint16_t pressed = m_buttons & ~m_prevButtons; + outInputState.actions[static_cast(InputAction::Action1)] = (pressed & XINPUT_GAMEPAD_A) != 0; + outInputState.actions[static_cast(InputAction::Action2)] = (pressed & XINPUT_GAMEPAD_B) != 0; + outInputState.actions[static_cast(InputAction::MenuOpen)] = (pressed & XINPUT_GAMEPAD_START) != 0; + outInputState.actions[static_cast(InputAction::Jump)] = (pressed & XINPUT_GAMEPAD_A) != 0; + + // Analog to digital movement + if (std::abs(outInputState.moveAxisY) > 0.3f) + { + if (outInputState.moveAxisY > 0.3f) + outInputState.actionsHeld[static_cast(InputAction::MoveForward)] = true; + else + outInputState.actionsHeld[static_cast(InputAction::MoveBackward)] = true; + } + if (std::abs(outInputState.moveAxisX) > 0.3f) + { + if (outInputState.moveAxisX > 0.3f) + outInputState.actionsHeld[static_cast(InputAction::MoveRight)] = true; + else + outInputState.actionsHeld[static_cast(InputAction::MoveLeft)] = true; + } + } + + float XboxSeriesPlatform::NormalizeAxis(int16_t value) const + { + // XInput thumbsticks are -32768 to 32767 + float normalized = static_cast(value) / 32767.0f; + + // Apply deadzone (XInput default is ~7849 / 32767 = 0.24) + const float deadzone = 0.24f; + if (std::abs(normalized) < deadzone) + return 0.0f; + + // Rescale to account for deadzone + float sign = (normalized > 0.0f) ? 1.0f : -1.0f; + float absValue = std::abs(normalized); + return sign * ((absValue - deadzone) / (1.0f - deadzone)); + } + + float XboxSeriesPlatform::NormalizeTrigger(uint8_t value) const + { + // XInput triggers are 0-255 + float normalized = static_cast(value) / 255.0f; + + // Apply trigger deadzone (XINPUT_GAMEPAD_TRIGGER_THRESHOLD = 30) + const float deadzone = 30.0f / 255.0f; + if (normalized < deadzone) + return 0.0f; + + // Rescale to account for deadzone + return (normalized - deadzone) / (1.0f - deadzone); + } + + void XboxSeriesPlatform::ResetInputFrameState(InputState& inputState) + { + inputState.ResetFrameStates(); + } + + IGraphicsContext* XboxSeriesPlatform::GetGraphicsContext() + { + return m_graphicsContext.get(); + } + + const char* XboxSeriesPlatform::GetUserDataPath() const + { + // UWP apps have local application data folder + return "LocalState/WillowVox"; + } + + const char* XboxSeriesPlatform::GetAssetsPath() const + { + // UWP assets are in the app package + return "Assets"; + } + + const char* XboxSeriesPlatform::GetPlatformName() const + { + return "Xbox Series X|S"; + } + + InputDeviceType XboxSeriesPlatform::GetPrimaryInputDevice() const + { + return InputDeviceType::XboxController; + } + + bool XboxSeriesPlatform::HasFeature(const char* featureName) const + { + if (strcmp(featureName, "gamepad") == 0) return true; + if (strcmp(featureName, "filesystem") == 0) return true; + return false; + } + + void XboxSeriesPlatform::SetVibration(int playerIndex, float lowFrequency, float highFrequency) + { + if (playerIndex < 0 || playerIndex >= XUSER_MAX_COUNT) + return; + + // XInput vibration values are 0-65535 + XINPUT_VIBRATION vibration; + vibration.wLeftMotorSpeed = static_cast(lowFrequency * 65535.0f); + vibration.wRightMotorSpeed = static_cast(highFrequency * 65535.0f); + + XInputSetState(playerIndex, &vibration); + } + + void XboxSeriesPlatform::StopVibration(int playerIndex) + { + SetVibration(playerIndex, 0.0f, 0.0f); + } +} + +#endif // PLATFORM_XBOX diff --git a/src/platform/Xbox/XboxSeriesPlatform.h b/src/platform/Xbox/XboxSeriesPlatform.h index 6bccf6d..3cae122 100644 --- a/src/platform/Xbox/XboxSeriesPlatform.h +++ b/src/platform/Xbox/XboxSeriesPlatform.h @@ -8,7 +8,7 @@ namespace WillowVox class XboxSeriesGraphicsContext; /** - * Xbox Series X/S Platform (Dev Mode / GDK) + * Xbox Series X/S Platform (UWP with XInput) * * Controller: Xbox Series Controller (standard Xbox layout) * - Left Stick: Movement @@ -22,8 +22,8 @@ namespace WillowVox * - Menu (≡): MenuOpen * - View (::): MenuBack * - * Graphics: DirectX 12 (via GDK) - * Input: GameInput API or XInput + * Graphics: OpenGL ES via ANGLE (translates to DirectX) + * Input: XInput API */ class XboxSeriesPlatform : public IPlatform { @@ -43,7 +43,24 @@ namespace WillowVox InputDeviceType GetPrimaryInputDevice() const override; bool HasFeature(const char* featureName) const override; + void SetVibration(int playerIndex, float lowFrequency, float highFrequency) override; + void StopVibration(int playerIndex) override; + private: std::unique_ptr m_graphicsContext; + + // XInput gamepad state + uint16_t m_buttons = 0; + uint16_t m_prevButtons = 0; + float m_lstickX = 0.0f; + float m_lstickY = 0.0f; + float m_rstickX = 0.0f; + float m_rstickY = 0.0f; + float m_leftTrigger = 0.0f; + float m_rightTrigger = 0.0f; + + void UpdateGamepadInput(InputState& outInputState); + float NormalizeAxis(int16_t value) const; + float NormalizeTrigger(uint8_t value) const; }; } diff --git a/src/platform/iOS/iOSGraphicsContext.cpp b/src/platform/iOS/iOSGraphicsContext.cpp new file mode 100644 index 0000000..9376eb8 --- /dev/null +++ b/src/platform/iOS/iOSGraphicsContext.cpp @@ -0,0 +1,243 @@ +#include "iOSGraphicsContext.h" +#include + +#ifdef PLATFORM_IOS + +// iOS SDK includes +#import +#import +#import +#import +#import +#include + +namespace WillowVox +{ + iOSGraphicsContext::iOSGraphicsContext() + { + // Get start time + mach_timebase_info_data_t timebase; + mach_timebase_info(&timebase); + uint64_t time = mach_absolute_time(); + m_startTime = (double)time * (double)timebase.numer / (double)timebase.denom / 1e9; + } + + iOSGraphicsContext::~iOSGraphicsContext() + { + Shutdown(); + } + + bool iOSGraphicsContext::Initialize(int width, int height, const char* title) + { + m_width = width; + m_height = height; + + Logger::EngineLog("Initializing iOS Graphics (OpenGL ES 3.0)"); + + // Create EAGLContext with OpenGL ES 3.0 + EAGLContext* context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; + if (!context) + { + Logger::EngineError("Failed to create EAGLContext with ES 3.0, trying ES 2.0"); + context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; + if (!context) + { + Logger::EngineError("Failed to create EAGLContext"); + return false; + } + } + m_eaglContext = (__bridge_retained void*)context; + + // Make context current + if (![EAGLContext setCurrentContext:context]) + { + Logger::EngineError("Failed to set current EAGLContext"); + return false; + } + + // Get the main UIWindow's layer (this is a simplification - in real app, you'd get this from your ViewController) + UIWindow* window = [[UIApplication sharedApplication] keyWindow]; + if (!window) + { + Logger::EngineError("Failed to get UIWindow"); + return false; + } + + CAEAGLLayer* eaglLayer = (CAEAGLLayer*)window.layer; + eaglLayer.opaque = YES; + eaglLayer.drawableProperties = @{ + kEAGLDrawablePropertyRetainedBacking: @NO, + kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8 + }; + m_eaglLayer = (__bridge void*)eaglLayer; + + // Create framebuffer + glGenFramebuffers(1, &m_framebuffer); + glBindFramebuffer(GL_FRAMEBUFFER, m_framebuffer); + + // Create color renderbuffer + glGenRenderbuffers(1, &m_colorRenderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, m_colorRenderbuffer); + + // Allocate storage for the renderbuffer from the layer + [context renderbufferStorage:GL_RENDERBUFFER fromDrawable:eaglLayer]; + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_colorRenderbuffer); + + // Get actual renderbuffer size + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &m_width); + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &m_height); + + // Create depth renderbuffer + glGenRenderbuffers(1, &m_depthRenderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, m_depthRenderbuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, m_width, m_height); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_depthRenderbuffer); + + // Check framebuffer status + GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) + { + Logger::EngineError("Framebuffer is not complete: 0x%X", status); + return false; + } + + // Enable OpenGL ES features + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LEQUAL); + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + Logger::EngineLog("iOS Graphics initialized (OpenGL ES, %dx%d)", m_width, m_height); + m_initialized = true; + + return true; + } + + void iOSGraphicsContext::Shutdown() + { + if (m_initialized) + { + // Delete framebuffer objects + if (m_framebuffer) + { + glDeleteFramebuffers(1, &m_framebuffer); + m_framebuffer = 0; + } + if (m_colorRenderbuffer) + { + glDeleteRenderbuffers(1, &m_colorRenderbuffer); + m_colorRenderbuffer = 0; + } + if (m_depthRenderbuffer) + { + glDeleteRenderbuffers(1, &m_depthRenderbuffer); + m_depthRenderbuffer = 0; + } + + // Release EAGL context + if (m_eaglContext) + { + EAGLContext* context = (__bridge_transfer EAGLContext*)m_eaglContext; + if ([EAGLContext currentContext] == context) + { + [EAGLContext setCurrentContext:nil]; + } + context = nil; + m_eaglContext = nullptr; + } + + m_initialized = false; + } + } + + void iOSGraphicsContext::BeginFrame() + { + // Bind framebuffer for rendering + glBindFramebuffer(GL_FRAMEBUFFER, m_framebuffer); + glBindRenderbuffer(GL_RENDERBUFFER, m_colorRenderbuffer); + } + + void iOSGraphicsContext::EndFrame() + { + // Nothing specific needed + } + + void iOSGraphicsContext::SwapBuffers() + { + // Present the color renderbuffer + glBindRenderbuffer(GL_RENDERBUFFER, m_colorRenderbuffer); + EAGLContext* context = (__bridge EAGLContext*)m_eaglContext; + [context presentRenderbuffer:GL_RENDERBUFFER]; + } + + void iOSGraphicsContext::Clear(float r, float g, float b, float a) + { + glClearColor(r, g, b, a); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + } + + void iOSGraphicsContext::ClearDepth() + { + glClear(GL_DEPTH_BUFFER_BIT); + } + + bool iOSGraphicsContext::ShouldClose() const + { + return m_shouldClose; + } + + void iOSGraphicsContext::SetShouldClose(bool shouldClose) + { + m_shouldClose = shouldClose; + } + + void iOSGraphicsContext::GetFramebufferSize(int& width, int& height) const + { + width = m_width; + height = m_height; + } + + void iOSGraphicsContext::GetWindowSize(int& width, int& height) const + { + width = m_width; + height = m_height; + } + + void iOSGraphicsContext::SetVSync(bool enabled) + { + m_vsyncEnabled = enabled; + // VSync on iOS is typically controlled at the CADisplayLink level + // This would require integration with the view controller + } + + bool iOSGraphicsContext::IsVSyncEnabled() const + { + return m_vsyncEnabled; + } + + float iOSGraphicsContext::GetTime() const + { + mach_timebase_info_data_t timebase; + mach_timebase_info(&timebase); + uint64_t time = mach_absolute_time(); + double currentTime = (double)time * (double)timebase.numer / (double)timebase.denom / 1e9; + return static_cast(currentTime - m_startTime); + } + + void iOSGraphicsContext::SetBackgroundColor(float r, float g, float b, float a) + { + m_clearColor[0] = r; + m_clearColor[1] = g; + m_clearColor[2] = b; + m_clearColor[3] = a; + } + + void iOSGraphicsContext::SetViewport(int x, int y, int width, int height) + { + glViewport(x, y, width, height); + } +} + +#endif // PLATFORM_IOS diff --git a/src/platform/iOS/iOSGraphicsContext.h b/src/platform/iOS/iOSGraphicsContext.h index 9f380ed..31035c2 100644 --- a/src/platform/iOS/iOSGraphicsContext.h +++ b/src/platform/iOS/iOSGraphicsContext.h @@ -5,10 +5,10 @@ namespace WillowVox { /** - * iOS Graphics Context (Metal or OpenGL ES) + * iOS Graphics Context (OpenGL ES) * - * iOS can use Metal (preferred) or OpenGL ES for compatibility. - * This is a template implementation. + * Uses OpenGL ES 3.0 with EAGLContext and CAEAGLLayer. + * Provides compatibility for cross-platform OpenGL code. */ class iOSGraphicsContext : public IGraphicsContext { @@ -36,8 +36,17 @@ namespace WillowVox private: bool m_initialized = false; bool m_shouldClose = false; + bool m_vsyncEnabled = true; int m_width = 0; int m_height = 0; float m_clearColor[4] = {0.1f, 0.1f, 0.1f, 1.0f}; + float m_startTime = 0.0f; + + // iOS-specific OpenGL ES handles (using void* to avoid including Objective-C headers) + void* m_eaglContext = nullptr; // EAGLContext* + void* m_eaglLayer = nullptr; // CAEAGLLayer* + unsigned int m_colorRenderbuffer = 0; + unsigned int m_depthRenderbuffer = 0; + unsigned int m_framebuffer = 0; }; } diff --git a/src/platform/iOS/iOSPlatform.cpp b/src/platform/iOS/iOSPlatform.cpp new file mode 100644 index 0000000..2e22808 --- /dev/null +++ b/src/platform/iOS/iOSPlatform.cpp @@ -0,0 +1,325 @@ +#include "iOSPlatform.h" +#include "iOSGraphicsContext.h" +#include + +#ifdef PLATFORM_IOS + +// iOS SDK includes +#include +#include +#include + +namespace WillowVox +{ + iOSPlatform::iOSPlatform() + { + } + + iOSPlatform::~iOSPlatform() + { + Shutdown(); + } + + bool iOSPlatform::Initialize() + { + Logger::EngineLog("Initializing iOS Platform"); + + // Graphics context will handle UIKit/OpenGL ES setup + m_graphicsContext = std::make_unique(); + + // Get screen dimensions from UIScreen + UIScreen* screen = [UIScreen mainScreen]; + CGRect bounds = [screen bounds]; + CGFloat scale = [screen scale]; + m_screenWidth = static_cast(bounds.size.width * scale); + m_screenHeight = static_cast(bounds.size.height * scale); + Logger::EngineLog("Screen size: %dx%d (scale: %.1f)", m_screenWidth, m_screenHeight, scale); + + // Setup virtual touch controls (similar to Android) + SetupVirtualControls(); + + Logger::EngineLog("iOS Platform initialized"); + return true; + } + + void iOSPlatform::Shutdown() + { + m_graphicsContext.reset(); + Logger::EngineLog("iOS Platform shutdown"); + } + + void iOSPlatform::ProcessEvents() + { + // iOS event processing handled by UIKit run loop + // This is called from the main game loop + } + + void iOSPlatform::PollInput(InputState& outInputState) + { + ProcessTouchInput(outInputState); + + // Copy button states + std::memcpy(m_buttonPrevStates, m_buttonStates, sizeof(m_buttonStates)); + + outInputState.deviceType = InputDeviceType::Touchscreen; + } + + void iOSPlatform::SetupVirtualControls() + { + // Virtual joystick (left side, bottom) + m_virtualControls.push_back({0.15f, 0.85f, 0.12f, InputAction::Count, true}); + + // Action buttons (right side, bottom) + m_virtualControls.push_back({0.85f, 0.85f, 0.06f, InputAction::Action1, false}); + m_virtualControls.push_back({0.75f, 0.80f, 0.06f, InputAction::Action2, false}); + m_virtualControls.push_back({0.85f, 0.75f, 0.06f, InputAction::Jump, false}); + + // Menu button (top left) + m_virtualControls.push_back({0.10f, 0.10f, 0.05f, InputAction::MenuOpen, false}); + } + + void iOSPlatform::ProcessTouchInput(InputState& outInputState) + { + // Reset per-frame state + m_lookDeltaX = 0.0f; + m_lookDeltaY = 0.0f; + m_joystickX = 0.0f; + m_joystickY = 0.0f; + + // Reset button states + for (int i = 0; i < static_cast(InputAction::Count); ++i) + { + m_buttonStates[i] = false; + } + + // Process each active touch point + // Note: Touch points are populated by UIKit touch event handlers + // This would be integrated with UIViewController's touchesBegan/Moved/Ended methods + for (int i = 0; i < MAX_TOUCH_POINTS; ++i) + { + TouchPoint& touch = m_touchPoints[i]; + if (!touch.active) + continue; + + // Check virtual controls + bool handled = false; + + // Virtual joystick (left side) + if (touch.startX < 0.5f && touch.id == m_joystickTouchId) + { + // Calculate joystick displacement from start position + float dx = touch.x - touch.startX; + float dy = touch.y - touch.startY; + + // Normalize joystick input (clamp to radius) + float dist = std::sqrt(dx * dx + dy * dy); + float maxDist = 0.12f; // Same as virtual control radius + + if (dist > maxDist) + { + dx = (dx / dist) * maxDist; + dy = (dy / dist) * maxDist; + } + + m_joystickX = dx / maxDist; + m_joystickY = dy / maxDist; + + handled = true; + } + else if (touch.startX < 0.5f && m_joystickTouchId == -1) + { + // New touch on left side - assign to joystick + m_joystickTouchId = touch.id; + handled = true; + } + + // Look control (right side) + if (touch.startX >= 0.5f && touch.id == m_lookTouchId) + { + // Calculate look delta + static float prevLookX = touch.startX; + static float prevLookY = touch.startY; + + m_lookDeltaX = (touch.x - prevLookX) * m_screenWidth; + m_lookDeltaY = (touch.y - prevLookY) * m_screenHeight; + + prevLookX = touch.x; + prevLookY = touch.y; + + handled = true; + } + else if (touch.startX >= 0.5f && m_lookTouchId == -1) + { + // Check if touching a button first + bool isTouchingButton = false; + for (const auto& control : m_virtualControls) + { + if (!control.isJoystick && + IsInsideCircle(touch.x, touch.y, control.x, control.y, control.radius)) + { + m_buttonStates[static_cast(control.action)] = true; + isTouchingButton = true; + handled = true; + break; + } + } + + // If not touching a button, use for look control + if (!isTouchingButton) + { + m_lookTouchId = touch.id; + handled = true; + } + } + + // Check button touches + if (!handled) + { + for (const auto& control : m_virtualControls) + { + if (!control.isJoystick && + IsInsideCircle(touch.x, touch.y, control.x, control.y, control.radius)) + { + m_buttonStates[static_cast(control.action)] = true; + break; + } + } + } + } + + // Check for released touch IDs + bool joystickActive = false; + bool lookActive = false; + for (int i = 0; i < MAX_TOUCH_POINTS; ++i) + { + if (m_touchPoints[i].active) + { + if (m_touchPoints[i].id == m_joystickTouchId) + joystickActive = true; + if (m_touchPoints[i].id == m_lookTouchId) + lookActive = true; + } + } + + if (!joystickActive) + m_joystickTouchId = -1; + if (!lookActive) + m_lookTouchId = -1; + + // Map to InputState + outInputState.moveAxisX = m_joystickX; + outInputState.moveAxisY = -m_joystickY; // Invert Y + + outInputState.lookAxisX = m_lookDeltaX; + outInputState.lookAxisY = m_lookDeltaY; + + outInputState.pointerDeltaX = m_lookDeltaX; + outInputState.pointerDeltaY = m_lookDeltaY; + + // Map analog to digital actions + if (std::abs(m_joystickX) > 0.5f || std::abs(m_joystickY) > 0.5f) + { + if (m_joystickY < -0.5f) + outInputState.actionsHeld[static_cast(InputAction::MoveForward)] = true; + if (m_joystickY > 0.5f) + outInputState.actionsHeld[static_cast(InputAction::MoveBackward)] = true; + if (m_joystickX < -0.5f) + outInputState.actionsHeld[static_cast(InputAction::MoveLeft)] = true; + if (m_joystickX > 0.5f) + outInputState.actionsHeld[static_cast(InputAction::MoveRight)] = true; + } + + // Copy button states + for (int i = 0; i < static_cast(InputAction::Count); ++i) + { + outInputState.actionsHeld[i] = m_buttonStates[i]; + outInputState.actions[i] = m_buttonStates[i] && !m_buttonPrevStates[i]; + outInputState.actionsReleased[i] = !m_buttonStates[i] && m_buttonPrevStates[i]; + } + } + + void iOSPlatform::ResetInputFrameState(InputState& inputState) + { + inputState.ResetFrameStates(); + } + + IGraphicsContext* iOSPlatform::GetGraphicsContext() + { + return m_graphicsContext.get(); + } + + const char* iOSPlatform::GetUserDataPath() const + { + // iOS uses app-specific directories + return "~/Documents/WillowVox"; + } + + const char* iOSPlatform::GetAssetsPath() const + { + // iOS bundles assets in the app bundle + return ""; // Use NSBundle for assets + } + + const char* iOSPlatform::GetPlatformName() const + { + return "iOS"; + } + + InputDeviceType iOSPlatform::GetPrimaryInputDevice() const + { + return InputDeviceType::Touchscreen; + } + + bool iOSPlatform::HasFeature(const char* featureName) const + { + if (strcmp(featureName, "touchscreen") == 0) return true; + if (strcmp(featureName, "filesystem") == 0) return true; + return false; + } + + TouchPoint* iOSPlatform::GetTouchPoint(int32_t id) + { + for (int i = 0; i < MAX_TOUCH_POINTS; ++i) + { + if (m_touchPoints[i].active && m_touchPoints[i].id == id) + return &m_touchPoints[i]; + } + return nullptr; + } + + TouchPoint* iOSPlatform::AllocateTouchPoint(int32_t id) + { + for (int i = 0; i < MAX_TOUCH_POINTS; ++i) + { + if (!m_touchPoints[i].active) + { + m_touchPoints[i].active = true; + m_touchPoints[i].id = id; + return &m_touchPoints[i]; + } + } + return nullptr; + } + + void iOSPlatform::ReleaseTouchPoint(int32_t id) + { + for (int i = 0; i < MAX_TOUCH_POINTS; ++i) + { + if (m_touchPoints[i].active && m_touchPoints[i].id == id) + { + m_touchPoints[i].active = false; + m_touchPoints[i].id = -1; + return; + } + } + } + + bool iOSPlatform::IsInsideCircle(float x, float y, float cx, float cy, float radius) const + { + float dx = x - cx; + float dy = y - cy; + return (dx * dx + dy * dy) <= (radius * radius); + } +} + +#endif // PLATFORM_IOS diff --git a/src/platform/iOS/iOSPlatform.h b/src/platform/iOS/iOSPlatform.h index a8d68ca..52ebf83 100644 --- a/src/platform/iOS/iOSPlatform.h +++ b/src/platform/iOS/iOSPlatform.h @@ -2,6 +2,7 @@ #include #include +#include namespace WillowVox { @@ -38,8 +39,51 @@ namespace WillowVox bool HasFeature(const char* featureName) const override; private: + struct TouchPoint + { + int32_t id; + float x, y; + float startX, startY; + bool active; + }; + std::unique_ptr m_graphicsContext; - // Touch input tracking (similar to Android) - // Implementation details omitted for brevity + + // Touch tracking + static constexpr int MAX_TOUCH_POINTS = 10; + TouchPoint m_touchPoints[MAX_TOUCH_POINTS] = {}; + + // Virtual control regions (normalized 0-1) + struct VirtualControl + { + float x, y, radius; + InputAction action; + bool isJoystick; + }; + + std::vector m_virtualControls; + + // Input state + float m_joystickX = 0.0f; + float m_joystickY = 0.0f; + int m_joystickTouchId = -1; + + float m_lookDeltaX = 0.0f; + float m_lookDeltaY = 0.0f; + int m_lookTouchId = -1; + + bool m_buttonStates[static_cast(InputAction::Count)] = {}; + bool m_buttonPrevStates[static_cast(InputAction::Count)] = {}; + + int m_screenWidth = 1920; + int m_screenHeight = 1080; + + // Helper methods + void SetupVirtualControls(); + void ProcessTouchInput(InputState& outInputState); + TouchPoint* GetTouchPoint(int32_t id); + TouchPoint* AllocateTouchPoint(int32_t id); + void ReleaseTouchPoint(int32_t id); + bool IsInsideCircle(float x, float y, float cx, float cy, float radius) const; }; }