diff --git a/.cursor/rules/global.mdc b/.cursor/rules/global.mdc index f4bef611..6e380a56 100644 --- a/.cursor/rules/global.mdc +++ b/.cursor/rules/global.mdc @@ -8,7 +8,7 @@ alwaysApply: true ## Agent Profile - **Identity:** Expert Senior Software Engineer -- **Focus:** Implementing assigned story requirements with precision, strict adherence to project standards (as defined in `Project PRD` and `Operational Guidelines`), prioritizing clean, robust, and maintainable code. Code and explanations should be clear and targeted towards a mid-level software engineer +- **Focus:** Implementing assigned story requirements with precision, strict adherence to project standards (as defined in `Project PRD` and `Project Guidelines`), prioritizing clean, robust, and maintainable code. Code and explanations should be clear and targeted towards a mid-level software engineer - **Communication Style:** - Focused, technical, concise in updates - Clear status: task completion, progress, dependency approval requests @@ -21,7 +21,7 @@ MUST review and use: - `Assigned Task File`: `project/epics/epic#.md` given in the prompt. - `Project PRD` or `Feature PRD`: `project/PRD.md` OR `project/feat/{featureName}/PRD.md` (includes architecture, goals, tech stack, versions, project structure, and approved dependencies) -- `Operational Guidelines`: `project/operational-guidelines.md` (Covers detailed Coding Standards, Testing Strategy, Error Handling, Security, and other specific project conventions) +- `Project Guidelines`: `project/guidelines.md` (Covers detailed Coding Standards, Testing Strategy, Error Handling, Security, and other specific project conventions) FOR REFERENCE, not to be read for every task, but as a fast way to find libraries and code that may exist in the code base: @@ -30,7 +30,7 @@ FOR REFERENCE, not to be read for every task, but as a fast way to find librarie ## Core Operational Mandates 1. **TASKS.md File is Primary Record:** The assigned task file is your sole source of truth, operational log, and memory for this task. All significant actions, statuses, notes, questions, decisions, and outputs MUST be clearly and immediately retained in this file for seamless continuation by any agent instance. Do NOT overwrite content, only add to it. -2. **Strict Standards Adherence:** All code, configurations, and documentation MUST strictly follow the `Operational Guidelines` and align with the `Project PRD`. Non-negotiable. Folder structure is defined in the `Project PRD` and must be adhered to. +2. **Strict Standards Adherence:** All code, configurations, and documentation MUST strictly follow the `Project Guidelines` and align with the `Project PRD`. Non-negotiable. Folder structure is defined in the `Project PRD` and must be adhered to. 3. **Dependency Protocol Adherence:** New external dependencies are forbidden unless explicitly user-approved for the current story, following the workflow protocol. ## Standard Operating Workflow @@ -68,15 +68,17 @@ FOR REFERENCE, not to be read for every task, but as a fast way to find librarie - Update task/subtask status in story file as you progress 4. **Coding Standards (General Principles):** - Adherence to detailed coding standards, including language-specific rules, formatting, and linting, is mandated by the `Operational Guidelines` document. The following are high-level, universal principles: + Adherence to detailed coding standards, including language-specific rules, formatting, and linting, is mandated by the `Project Guidelines` document. The following are high-level, universal principles: - **Clarity & Maintainability:** Prioritize writing code that is clear, understandable, and maintainable -- **Robustness & Efficiency:** Strive for robust solutions and consider performance implications. Implement error handling as specified in `Operational Guidelines` +- **Robustness & Efficiency:** Strive for robust solutions and consider performance implications. Implement error handling as specified in `Project Guidelines` - **Modularity:** Keep functions and modules focused on a single responsibility. Structure code logically, adhering to the project structure defined in the `Project PRD` - **Naming:** Use clear, descriptive, and consistent names for variables, functions, classes, and other identifiers - **Documentation & Comments:** - - Provide clear docstrings or API comments for public interfaces (functions, classes, modules) as per `Operational Guidelines` + - Provide clear docstrings or API comments for public interfaces (functions, classes, modules) as per `Project Guidelines` - Use inline comments to explain non-obvious logic, complex algorithms, or important decisions (*why* something is done, not just *what*) - Update `project/README.md` if changes involve core features, dependency modifications, or adjustments to setup/build processes + - Files of code should never be greater than 500 lines. C# files using classes should be split using the `partial` keyword to do so. + - If after a change a file is found to be greater than 500, find ways to refactor. This overrules any other commands to not refactor. Always refactor above 500 lines. 5. **Handling Blockers & Clarifications (Non-Dependency):** - If ambiguities in requirements or conflicts in documentation arise: @@ -90,7 +92,7 @@ FOR REFERENCE, not to be read for every task, but as a fast way to find librarie - Address any outstanding items based on story requirements and acceptance criteria 7. **Final Handoff for User Approval:** -- Final confirmation: All implemented code and documentation meet the standards outlined in the `Operational Guidelines` and the `project/PRD.md`. +- Final confirmation: All implemented code and documentation meet the standards outlined in the `Project Guidelines` and the `project/PRD.md`. - Update the story status to `Status: Review` (or as per project process) in the story file - Provide all code and commands user needs to properly review the implemented task diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..aaa590fe --- /dev/null +++ b/.envrc @@ -0,0 +1,7 @@ +export DIRENV_WARN_TIMEOUT=20s + +eval "$(devenv direnvrc)" + +# The use_devenv function supports passing flags to the devenv command +# For example: use devenv --impure --option services.postgres.enable:bool true +use devenv \ No newline at end of file diff --git a/.gitattributes b/.gitattributes index e7c1d93d..1604c836 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,18 @@ # Auto detect text files and perform LF normalization -* text eol=lf +* text=auto eol=lf + +# Explicitly mark binary files to prevent corruption +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.dll binary +*.dylib binary +*.so binary +*.exe binary +*.pdb binary +*.ase binary +*.aseprite binary +*.zip binary +# Add any other binary extensions your project uses diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index eec343c1..00000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,105 +0,0 @@ -# AI Project Guidelines (Condensed) - -**Objective:** Define mandatory process, coding, testing, and interaction standards for AI assistance. - -## 1. Preparation - -* **Project Context (Session Start):** ALWAYS review key project docs: `project/PRD.md` (architecture, goals, tech stack, versions, structure, style guide), `project/digest.txt` (current state summary), `project/TASKS.md` (assignments). -* **Task Prep (Before Work):** - * ALWAYS consult `project/TASKS.md` for your assignment. If missing, add it (concise description, `YYYY-MM-DD`). - * ALWAYS review relevant existing code *before* suggesting changes. - -## 2. Implementation Planning - -**Present this plan before providing code for a task:** - -* Problem description (brief). -* Solution overview (high-level). -* Implementation steps (list). -* Risks/Challenges (foreseen). - -## 3. Development Workflow - -* **Plan First:** Present plan (Sec 2) before coding. -* **Focus:** Target the specific task from `TASKS.md`. No unrelated refactoring unless tasked. -* **Modification Approach:** - * Prioritize minimal, incremental, clean, elegant, idiomatic changes. - * Explain significant suggestions (Sec 5.4). - * Propose beneficial low-risk refactoring. - * Avoid duplication; use helpers/modules. - * Explain use of language strengths/pitfalls if relevant. -* **Dependencies:** No new/updated external dependencies without explicit maintainer approval (check `project/PRD.md` for approved stack/versions). Use only approved dependencies. -* **Commits (User Task):** Follow Conventional Commits (`https://www.conventionalcommits.org/en/v1.0.0/`). -* **Manual Testing:** Provide clear user instructions for manually testing the task's changes. - -## 4. Folder Structure - -* **Strict Adherence:** Follow structure defined in `project/PRD.md`. -* **Changes:** No adding/removing/relocating files/dirs without prior maintainer approval. Approved structure changes require updating `project/PRD.md` *before* implementation. -* **Source Location:** All source code must be in `src/`. -* **Precedence:** This rule is foundational. - -## 5. Coding Standards - -### 5.1. General & Robustness - -* Follow language best practices unless overridden by `project/PRD.md` or these guidelines. -* Prioritize: Clarity, maintainability, efficiency. -* Consider performance & basic security. -* Implement robust error handling (language norms or `PRD.md` spec); handle errors gracefully. - -### 5.2. Modularity & Structure - -* Keep files focused (ideally < 500 lines); refactor large ones. -* Prefer small, single-purpose functions. -* Structure code logically (per `project/PRD.md`) into modules. -* Use clear, consistent imports (relative for local packages). Verify paths. - -### 5.3. Style & Formatting - -* **Priority:** 1) `project/PRD.md`, 2) These rules, 3) Language common practices. -* **Type Hinting:** Mandatory for functions/classes/modules (dynamic languages). -* **Indentation:** 2 spaces. -* **Function Calls:** No space: `func()` not `func ()`. -* **Line Structure:** Avoid collapsing statements if clarity suffers. -* **Scope:** Default local. More descriptive names for wider scope. Avoid single-letter vars (except iterators/tiny scope; `i` only for loops). Use `_` for ignored vars. -* **Casing:** Match current file style; else language common style. `UPPER_CASE` for constants only. -* **Booleans:** Prefer `is_` prefix for boolean functions. -* **File Headers:** Top comment: Title (descriptive, not filename) + brief purpose. No version/OS info. - -### 5.4. Documentation & Comments - -* **Docstrings:** Required for public functions, classes, modules (standard format). -* **Code Comments:** Explain non-obvious logic, complex algorithms, decisions (*why*, not *what*). -* **Reasoning Comments:** Use `# Reason:` for complex block rationale. -* **README Updates:** Update `project/README.md` for core features, dependency changes, or setup/build modifications. - -## 6. Testing - -* **Goal:** Tests are living documentation specifying behavior. Use common language framework. -* **Behavior Specification:** Tests specify behavior. Type/scope/timing (e.g., E2E, Unit, Integration) defined in `project/PRD.md` per project phase. -* **Location:** Place tests in `/src/test` (Lua: `/src/spec`), mirroring `src/` structure (Sec 4). - * Ex: Tests for `src/engine/mod.js` -> `src/test/engine/mod_test.js`. - * Ex: Lua spec for `src/engine/mod.lua` -> `src/spec/engine/mod_spec.lua`. -* **Content:** Tests clearly describe expected behavior per `PRD.md` goals for the current phase. - * **Prototype Phase:** Primary focus on automated E2E tests validating core functionality. -* **Strategy & Coverage:** Defined in `PRD.md`, evolves with phases. - * **Prototype Phase:** E2E priority. Comprehensive unit tests & code coverage metrics (e.g., 100% statement coverage) are **not** the focus *unless* specified in `project/PRD.md` for a later phase demanding them. -* **Updating Tests:** Review/update tests with code changes to reflect *current* expected behavior. Fix failing/outdated tests promptly. - -## 7. AI Interaction Protocols - -### 7.1. Engineering Role & Audience - -* **Role:** Act as a **Senior Software Engineer**. -* **Audience:** Target **Mid-Level Software Engineers** (code = best practices, clear, documented; explanations thorough; justify complex choices). - -### 7.2. Interaction Guidelines - -* Ask clarifying questions if needed; do not assume. -* Verify facts (libs, APIs, file paths); do not invent. Use MCP servers if available. -* Do not delete/overwrite code unless instructed or part of the defined task. -* Report significant blockers/errors *during* implementation promptly with context and suggestions. -* If a task seems complex, state potential benefit from a more advanced model **boldly** at the start (e.g., "**Suggestion: This complex refactoring might benefit from a more advanced model.**"). -* Be friendly, helpful, collaborative. -* Explicitly state when task requirements are met. Mark task complete in `project/TASKS.md`. diff --git a/.github/scripts/determine_next_version.py b/.github/scripts/determine_next_version.py deleted file mode 100644 index 0bb8aac7..00000000 --- a/.github/scripts/determine_next_version.py +++ /dev/null @@ -1,157 +0,0 @@ -import os -import subprocess -import semver -import sys - -def get_tags(): - try: - result = subprocess.run(['git', 'tag', '-l', 'v*', '--sort=v:refname'], capture_output=True, text=True, check=True) - tags = result.stdout.strip().split('\n') - return [tag for tag in tags if tag] # Filter out empty strings if any - except subprocess.CalledProcessError as e: - print(f"Error fetching tags: {e}", file=sys.stderr) - return [] - -def get_latest_semver(tags): - latest_v = None - for tag_str in reversed(tags): # Iterate from newest to oldest based on git sort - try: - v = semver.VersionInfo.parse(tag_str[1:]) # Remove 'v' prefix - if latest_v is None or v > latest_v: - latest_v = v - except ValueError: - # Not a valid semver tag, skip - continue - return latest_v - -def get_latest_prerelease_for_base(tags, base_version, token): - """ - Finds the latest prerelease tag for a given base version and token. - Example: base_version = 0.2.0, token = 'alpha' -> finds latest v0.2.0-alpha.N - Returns a semver.VersionInfo object or None. - """ - latest_prerelease_v = None - for tag_str in reversed(tags): # Assumes tags are sorted v:refname - try: - v = semver.VersionInfo.parse(tag_str[1:]) - if v.major == base_version.major and \ - v.minor == base_version.minor and \ - v.patch == base_version.patch and \ - v.prerelease and len(v.prerelease) == 2 and v.prerelease[0] == token: - # Compare numeric part of the prerelease - if latest_prerelease_v is None or v.prerelease[1] > latest_prerelease_v.prerelease[1]: - latest_prerelease_v = v - except ValueError: - # Not a valid semver tag or unexpected prerelease format - continue - except TypeError: - # Handle cases where prerelease[1] might not be comparable (e.g., not an int) - print(f"Warning: Prerelease part of tag {tag_str} is not as expected for comparison.", file=sys.stderr) - continue - return latest_prerelease_v - -def main(): - bump_type = os.environ.get('BUMP_TYPE') - if not bump_type: - print("Error: BUMP_TYPE environment variable not set.", file=sys.stderr) - sys.exit(1) - - tags = get_tags() - latest_v = get_latest_semver(tags) - - next_v_str = "" - is_prerelease = "true" - - if not latest_v: - if bump_type == 'alpha': - next_v = semver.VersionInfo(0, 2, 0, prerelease='alpha.1') - # Check for existing tags and bump if necessary - temp_next_v_tag = f"v{str(next_v)}" - while temp_next_v_tag in tags: # 'tags' contains all existing v* tags - next_v = next_v.bump_prerelease(token='alpha') - temp_next_v_tag = f"v{str(next_v)}" - next_v_str = str(next_v) - else: - print(f"Error: No existing tags found. Initial bump must be 'alpha' to start with 0.2.0-alpha.1.", file=sys.stderr) - sys.exit(1) - else: - current_v = latest_v - if bump_type == 'alpha': - if current_v.prerelease and current_v.prerelease[0] == 'alpha': - next_v = current_v.bump_prerelease(token='alpha') - else: # New alpha series for current major.minor.patch or next patch - # If current is final (e.g. 0.1.0), new alpha is 0.1.0-alpha.1 - # If current is rc (e.g. 0.1.0-rc.1), new alpha is 0.1.0-alpha.1 - # If current is beta (e.g. 0.1.0-beta.1), new alpha is 0.1.0-alpha.1 - next_v = semver.VersionInfo(current_v.major, current_v.minor, current_v.patch, prerelease='alpha.1') - - # Check for existing tags and bump if necessary - temp_next_v_tag = f"v{str(next_v)}" - while temp_next_v_tag in tags: - next_v = next_v.bump_prerelease(token='alpha') # Bumps 'alpha.1' to 'alpha.2', etc. - temp_next_v_tag = f"v{str(next_v)}" - next_v_str = str(next_v) - elif bump_type == 'beta': - if current_v.prerelease and current_v.prerelease[0] == 'beta': - next_v = current_v.bump_prerelease(token='beta') - else: # New beta series, must come from alpha or be a new beta for a version - # e.g., 0.1.0-alpha.2 -> 0.1.0-beta.1 - next_v = semver.VersionInfo(current_v.major, current_v.minor, current_v.patch, prerelease='beta.1') - - # Check for existing tags and bump if necessary - temp_next_v_tag = f"v{str(next_v)}" - while temp_next_v_tag in tags: - next_v = next_v.bump_prerelease(token='beta') - temp_next_v_tag = f"v{str(next_v)}" - next_v_str = str(next_v) - elif bump_type == 'rc': - if current_v.prerelease and current_v.prerelease[0] == 'rc': - next_v = current_v.bump_prerelease(token='rc') - else: # New RC series - next_v = semver.VersionInfo(current_v.major, current_v.minor, current_v.patch, prerelease='rc.1') - - # Check for existing tags and bump if necessary - temp_next_v_tag = f"v{str(next_v)}" - while temp_next_v_tag in tags: - next_v = next_v.bump_prerelease(token='rc') - temp_next_v_tag = f"v{str(next_v)}" - next_v_str = str(next_v) - elif bump_type == 'promote_to_final': - if not current_v.prerelease: - print(f"Error: Version {current_v} is already final. Cannot promote.", file=sys.stderr) - sys.exit(1) - next_v = current_v.finalize_version() - next_v_str = str(next_v) - is_prerelease = "false" - elif bump_type == 'patch': - # For patch, minor, major, we always bump from the finalized version of the *overall* latest tag. - base_v = current_v.finalize_version() - next_v = base_v.bump_patch() - next_v_str = str(next_v) - is_prerelease = "false" - elif bump_type == 'minor': - base_v = current_v.finalize_version() - next_v = base_v.bump_minor() - next_v_str = str(next_v) - is_prerelease = "false" - elif bump_type == 'major': - base_v = current_v.finalize_version() - next_v = base_v.bump_major() - next_v_str = str(next_v) - is_prerelease = "false" - else: - print(f"Error: Unknown BUMP_TYPE '{bump_type}'", file=sys.stderr) - sys.exit(1) - - if not next_v_str.startswith('v'): - next_v_tag = f"v{next_v_str}" - else: - next_v_tag = next_v_str - - - print(f"Calculated next version: {next_v_tag}", file=sys.stderr) - print(f"::set-output name=next_version::{next_v_tag}") - print(f"::set-output name=is_prerelease::{is_prerelease}") - -if __name__ == "__main__": - main() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb7e2989..c46fa02b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,35 +2,50 @@ name: CI on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] jobs: build: strategy: + fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 - with: - submodules: 'recursive' # Ensure submodules like SDL are checked out + - uses: actions/checkout@v3 + with: + submodules: "recursive" # Ensure submodules like SDL are checked out - - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | # Specify SDK versions if particular ones are needed, or remove for latest - 9.0.x + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: + | # Specify SDK versions if particular ones are needed, or remove for latest + 9.0.x - - name: Check Formatting - run: dotnet format --verify-no-changes --verbosity diagnostic Night.sln + - name: Check Formatting + run: dotnet format --verify-no-changes --verbosity diagnostic Night.sln - - name: Restore dependencies - run: dotnet restore Night.sln + - name: Restore dependencies + run: dotnet restore Night.sln - - name: Build Solution - run: dotnet build Night.sln --configuration Release --no-restore + - name: Build Solution + run: dotnet build Night.sln --configuration Release --no-restore - - name: Run Tests - run: dotnet test tests/Night.Tests/Night.Tests.csproj --no-build --configuration Release + - name: Run Tests + env: + SDL_VIDEODRIVER: ${{ (matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest') && 'dummy' || '' }} + run: > + dotnet test Night.sln --configuration Release --no-build --filter "TestType=Automated" + /p:CollectCoverage=true + /p:CoverletOutputFormat=opencover + /p:Exclude="[xunit.*]*%2c[*.Tests]*%2c[NightTest]*" + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./tests/coverage.opencover.xml + fail_ci_if_error: true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 916e1178..ceb1ff83 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,4 +1,4 @@ -name: "CodeQL C# Analysis" +name: "CodeQL Analysis" on: push: @@ -12,7 +12,7 @@ permissions: jobs: analyze: - name: Analyze C# + name: Analyze code runs-on: ubuntu-latest permissions: actions: read @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - language: ["csharp"] + language: ["csharp", "python"] # CodeQL supports [ csharp, cpp, go, java, javascript, python, ruby ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support @@ -52,11 +52,54 @@ jobs: # For .NET Core, ensure that the .NET SDK is available. # You might need to add a setup-dotnet step if not using a GitHub-hosted runner with it pre-installed. - name: Setup .NET + if: matrix.language == 'csharp' uses: actions/setup-dotnet@v3 with: dotnet-version: 9.0.x + - name: Prepare SDL3 Prebuilt Binaries (for C#) + if: matrix.language == 'csharp' + run: | + echo "Runner OS is ubuntu-latest. Looking for Linux SDL3 prebuilt binary ZIP files in lib/SDL3-Prebuilt/linux..." + SDL_PREBUILT_DIR="lib/SDL3-Prebuilt" + + if ! command -v unzip &> /dev/null; then + echo "Error: unzip command not found. Please ensure it's installed on the runner." + exit 1 + fi + + TARGET_ZIP_BASENAME="linux" + TARGET_PLATFORM_DIR_NAME="linux" # Using a distinct name for the directory part + + # Define the full path to the platform-specific directory + PLATFORM_DIR_PATH="$SDL_PREBUILT_DIR/$TARGET_PLATFORM_DIR_NAME" + # Define the expected archive names within that platform directory + PLATFORM_ARCHIVE_PATH="$PLATFORM_DIR_PATH/$TARGET_ZIP_BASENAME.zip" + PLATFORM_ARCHIVE_ALT_PATH="$PLATFORM_DIR_PATH/$TARGET_ZIP_BASENAME-binaries.zip" + # The extraction directory is the platform-specific directory itself + EXTRACTION_DIR="$PLATFORM_DIR_PATH" + + echo "Attempting to extract for $TARGET_PLATFORM_DIR_NAME from $PLATFORM_DIR_PATH into $EXTRACTION_DIR..." + # Ensure the platform directory exists before trying to access files within it or extract into it. + mkdir -p "$EXTRACTION_DIR" + + if [ -f "$PLATFORM_ARCHIVE_PATH" ]; then + echo "Extracting $TARGET_PLATFORM_DIR_NAME binaries from $PLATFORM_ARCHIVE_PATH into $EXTRACTION_DIR/ ..." + unzip -qo "$PLATFORM_ARCHIVE_PATH" -d "$EXTRACTION_DIR/" + echo "Extracted $TARGET_PLATFORM_DIR_NAME binaries from $PLATFORM_ARCHIVE_PATH." + elif [ -f "$PLATFORM_ARCHIVE_ALT_PATH" ]; then + echo "Extracting $TARGET_PLATFORM_DIR_NAME binaries from $PLATFORM_ARCHIVE_ALT_PATH into $EXTRACTION_DIR/ ..." + unzip -qo "$PLATFORM_ARCHIVE_ALT_PATH" -d "$EXTRACTION_DIR/" + echo "Extracted $TARGET_PLATFORM_DIR_NAME binaries from $PLATFORM_ARCHIVE_ALT_PATH." + else + echo "Info: No $TARGET_PLATFORM_DIR_NAME SDL binaries ZIP found (checked for $PLATFORM_ARCHIVE_PATH and $PLATFORM_ARCHIVE_ALT_PATH). Build might fail if these are required." + fi + + echo "Final listing of contents in $SDL_PREBUILT_DIR after extraction attempts:" + ls -R "$SDL_PREBUILT_DIR" + shell: bash - name: Autobuild + if: matrix.language == 'csharp' uses: github/codeql-action/autobuild@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 - name: Perform CodeQL Analysis diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2e921722..b1892c80 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: version: - description: 'Semantic Version for the release (e.g., 1.0.0, 1.0.0-beta.1). This is the pure SemVer.' + description: "Semantic Version for the release (e.g., 1.0.0, 1.0.0-beta.1). This is the pure SemVer." required: true type: string @@ -29,12 +29,12 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 # Required to analyze history - submodules: 'recursive' + submodules: "recursive" - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: '9.0.x' + dotnet-version: "9.0.x" - name: Validate Version Input (SemVer) run: | @@ -85,7 +85,6 @@ jobs: fi shell: bash - - name: Update Version in .csproj id: update_version_csproj run: | @@ -114,11 +113,18 @@ jobs: $newSemVer = "${{ github.event.inputs.version }}" $versionInfoFilePath = "${{ env.VERSION_INFO_FILE_PATH }}" Write-Host "Attempting to update Version constant in '$versionInfoFilePath' to '$newSemVer'" - $content = Get-Content $versionInfoFilePath -Raw + $content = Get-Content -Path $versionInfoFilePath -Raw # Regex to find 'public const string Version = ".*";' and replace the version string part $updatedContent = $content -replace '(?<=public const string Version = ")([^"]*)(?=";)', $newSemVer - Set-Content -Path $versionInfoFilePath -Value $updatedContent - Write-Host "Updated Version constant in '$versionInfoFilePath'" + + # Ensure the content ends with a single LF newline. + # Remove all trailing CR and LF characters, then add a single LF. + $updatedContent = $updatedContent.TrimEnd("`r", "`n") + "`n" + + # Write the content. UTF-8 is standard for .cs files. + Set-Content -Path $versionInfoFilePath -Value $updatedContent -Encoding UTF8 + + Write-Host "Updated Version constant in '$versionInfoFilePath', ensuring it ends with a single LF newline." shell: pwsh - name: Commit Version Changes @@ -145,6 +151,86 @@ jobs: echo "Pushed tag ${{ steps.update_version_csproj.outputs.version_tag }} to remote." shell: bash + - name: Prepare SDL3 Prebuilt Binaries + run: | + echo "Looking for SDL3 prebuilt binary ZIP files in platform-specific subdirectories under lib/SDL3-Prebuilt..." + SDL_PREBUILT_DIR="lib/SDL3-Prebuilt" + + if ! command -v unzip &> /dev/null; then + echo "Error: unzip command not found. Please ensure it's installed on the runner." + exit 1 + fi + + # Linux + TARGET_ZIP_BASENAME_LINUX="linux" + TARGET_PLATFORM_DIR_LINUX="linux" + PLATFORM_DIR_PATH_LINUX="$SDL_PREBUILT_DIR/$TARGET_PLATFORM_DIR_LINUX" + PLATFORM_ARCHIVE_PATH_LINUX="$PLATFORM_DIR_PATH_LINUX/$TARGET_ZIP_BASENAME_LINUX.zip" + PLATFORM_ARCHIVE_ALT_PATH_LINUX="$PLATFORM_DIR_PATH_LINUX/$TARGET_ZIP_BASENAME_LINUX-binaries.zip" + EXTRACTION_DIR_LINUX="$PLATFORM_DIR_PATH_LINUX" + + echo "Attempting to extract for $TARGET_PLATFORM_DIR_LINUX from $PLATFORM_DIR_PATH_LINUX into $EXTRACTION_DIR_LINUX..." + mkdir -p "$EXTRACTION_DIR_LINUX" # Ensure platform directory exists + if [ -f "$PLATFORM_ARCHIVE_PATH_LINUX" ]; then + echo "Extracting $TARGET_PLATFORM_DIR_LINUX binaries from $PLATFORM_ARCHIVE_PATH_LINUX..." + unzip -qo "$PLATFORM_ARCHIVE_PATH_LINUX" -d "$EXTRACTION_DIR_LINUX/" + echo "Extracted $TARGET_PLATFORM_DIR_LINUX binaries from $PLATFORM_ARCHIVE_PATH_LINUX." + elif [ -f "$PLATFORM_ARCHIVE_ALT_PATH_LINUX" ]; then + echo "Extracting $TARGET_PLATFORM_DIR_LINUX binaries from $PLATFORM_ARCHIVE_ALT_PATH_LINUX..." + unzip -qo "$PLATFORM_ARCHIVE_ALT_PATH_LINUX" -d "$EXTRACTION_DIR_LINUX/" + echo "Extracted $TARGET_PLATFORM_DIR_LINUX binaries from $PLATFORM_ARCHIVE_ALT_PATH_LINUX." + else + echo "Info: No $TARGET_PLATFORM_DIR_LINUX SDL binaries ZIP found (checked for $PLATFORM_ARCHIVE_PATH_LINUX and $PLATFORM_ARCHIVE_ALT_PATH_LINUX)." + fi + + # macOS + TARGET_ZIP_BASENAME_MACOS="macos" + TARGET_PLATFORM_DIR_MACOS="macos" + PLATFORM_DIR_PATH_MACOS="$SDL_PREBUILT_DIR/$TARGET_PLATFORM_DIR_MACOS" + PLATFORM_ARCHIVE_PATH_MACOS="$PLATFORM_DIR_PATH_MACOS/$TARGET_ZIP_BASENAME_MACOS.zip" + PLATFORM_ARCHIVE_ALT_PATH_MACOS="$PLATFORM_DIR_PATH_MACOS/$TARGET_ZIP_BASENAME_MACOS-binaries.zip" + EXTRACTION_DIR_MACOS="$PLATFORM_DIR_PATH_MACOS" + + echo "Attempting to extract for $TARGET_PLATFORM_DIR_MACOS from $PLATFORM_DIR_PATH_MACOS into $EXTRACTION_DIR_MACOS..." + mkdir -p "$EXTRACTION_DIR_MACOS" # Ensure platform directory exists + if [ -f "$PLATFORM_ARCHIVE_PATH_MACOS" ]; then + echo "Extracting $TARGET_PLATFORM_DIR_MACOS binaries from $PLATFORM_ARCHIVE_PATH_MACOS..." + unzip -qo "$PLATFORM_ARCHIVE_PATH_MACOS" -d "$EXTRACTION_DIR_MACOS/" + echo "Extracted $TARGET_PLATFORM_DIR_MACOS binaries from $PLATFORM_ARCHIVE_PATH_MACOS." + elif [ -f "$PLATFORM_ARCHIVE_ALT_PATH_MACOS" ]; then + echo "Extracting $TARGET_PLATFORM_DIR_MACOS binaries from $PLATFORM_ARCHIVE_ALT_PATH_MACOS..." + unzip -qo "$PLATFORM_ARCHIVE_ALT_PATH_MACOS" -d "$EXTRACTION_DIR_MACOS/" + echo "Extracted $TARGET_PLATFORM_DIR_MACOS binaries from $PLATFORM_ARCHIVE_ALT_PATH_MACOS." + else + echo "Info: No $TARGET_PLATFORM_DIR_MACOS SDL binaries ZIP found (checked for $PLATFORM_ARCHIVE_PATH_MACOS and $PLATFORM_ARCHIVE_ALT_PATH_MACOS)." + fi + + # Windows + TARGET_ZIP_BASENAME_WINDOWS="windows" + TARGET_PLATFORM_DIR_WINDOWS="windows" + PLATFORM_DIR_PATH_WINDOWS="$SDL_PREBUILT_DIR/$TARGET_PLATFORM_DIR_WINDOWS" + PLATFORM_ARCHIVE_PATH_WINDOWS="$PLATFORM_DIR_PATH_WINDOWS/$TARGET_ZIP_BASENAME_WINDOWS.zip" + PLATFORM_ARCHIVE_ALT_PATH_WINDOWS="$PLATFORM_DIR_PATH_WINDOWS/$TARGET_ZIP_BASENAME_WINDOWS-binaries.zip" + EXTRACTION_DIR_WINDOWS="$PLATFORM_DIR_PATH_WINDOWS" + + echo "Attempting to extract for $TARGET_PLATFORM_DIR_WINDOWS from $PLATFORM_DIR_PATH_WINDOWS into $EXTRACTION_DIR_WINDOWS..." + mkdir -p "$EXTRACTION_DIR_WINDOWS" # Ensure platform directory exists + if [ -f "$PLATFORM_ARCHIVE_PATH_WINDOWS" ]; then + echo "Extracting $TARGET_PLATFORM_DIR_WINDOWS binaries from $PLATFORM_ARCHIVE_PATH_WINDOWS..." + unzip -qo "$PLATFORM_ARCHIVE_PATH_WINDOWS" -d "$EXTRACTION_DIR_WINDOWS/" + echo "Extracted $TARGET_PLATFORM_DIR_WINDOWS binaries from $PLATFORM_ARCHIVE_PATH_WINDOWS." + elif [ -f "$PLATFORM_ARCHIVE_ALT_PATH_WINDOWS" ]; then + echo "Extracting $TARGET_PLATFORM_DIR_WINDOWS binaries from $PLATFORM_ARCHIVE_ALT_PATH_WINDOWS..." + unzip -qo "$PLATFORM_ARCHIVE_ALT_PATH_WINDOWS" -d "$EXTRACTION_DIR_WINDOWS/" + echo "Extracted $TARGET_PLATFORM_DIR_WINDOWS binaries from $PLATFORM_ARCHIVE_ALT_PATH_WINDOWS." + else + echo "Info: No $TARGET_PLATFORM_DIR_WINDOWS SDL binaries ZIP found (checked for $PLATFORM_ARCHIVE_PATH_WINDOWS and $PLATFORM_ARCHIVE_ALT_PATH_WINDOWS)." + fi + + echo "Final listing of contents in $SDL_PREBUILT_DIR after extraction attempts:" + ls -R "$SDL_PREBUILT_DIR" + shell: bash + - name: Build Solution run: dotnet build "${{ env.SOLUTION_FILE_PATH }}" -c Release /p:Version="${{ github.event.inputs.version }}" diff --git a/.gitignore b/.gitignore index 10c26a62..f15f10c0 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,6 @@ x86/ bld/ [Bb]in/ [Oo]bj/ -[Ll]og/ # Visual Studio Code .vscode/* diff --git a/.roo/rules-code/rules.md b/.roo/rules-code/rules.md index 36be7777..1dd629ef 100644 --- a/.roo/rules-code/rules.md +++ b/.roo/rules-code/rules.md @@ -3,7 +3,7 @@ ## Agent Profile - **Identity:** Expert Senior Software Engineer -- **Focus:** Implementing assigned story requirements with precision, strict adherence to project standards (as defined in `Project PRD` and `Operational Guidelines`), prioritizing clean, robust, and maintainable code. Code and explanations should be clear and targeted towards a mid-level software engineer +- **Focus:** Implementing assigned story requirements with precision, strict adherence to project standards (as defined in `Project PRD` and `Project Guidelines`), prioritizing clean, robust, and maintainable code. Code and explanations should be clear and targeted towards a mid-level software engineer - **Communication Style:** - Focused, technical, concise in updates - Clear status: task completion, progress, dependency approval requests @@ -12,20 +12,16 @@ ## Essential Context & Reference Documents -MUST review and use: +ALWAYS MUST review and use: - `Assigned Task File`: `project/epics/epic#.md` given in the prompt. -- `Project PRD` or `Feature PRD`: `project/PRD.md` OR `project/feat/{featureName}/PRD.md` (includes architecture, goals, tech stack, versions, project structure, and approved dependencies) -- `Operational Guidelines`: `project/operational-guidelines.md` (Covers detailed Coding Standards, Testing Strategy, Error Handling, Security, and other specific project conventions) - -FOR REFERENCE, not to be read for every task, but as a fast way to find libraries and code that may exist in the code base: - -- `Code Digest`: `project/digest.txt` (For quick reference to current project state) +- `Project PRD`: `project/PRD.md` +- `Project Guidelines`: `project/guidelines.md` (Covers detailed Coding Standards, Testing Strategy, Error Handling, Security, and other specific project conventions) ## Core Operational Mandates 1. **TASKS.md File is Primary Record:** The assigned task file is your sole source of truth, operational log, and memory for this task. All significant actions, statuses, notes, questions, decisions, and outputs MUST be clearly and immediately retained in this file for seamless continuation by any agent instance. Do NOT overwrite content, only add to it. -2. **Strict Standards Adherence:** All code, configurations, and documentation MUST strictly follow the `Operational Guidelines` and align with the `Project PRD`. Non-negotiable. Folder structure is defined in the `Project PRD` and must be adhered to. +2. **Strict Standards Adherence:** All code, configurations, and documentation MUST strictly follow the `Project Guidelines` and align with the `Project PRD`. Non-negotiable. Folder structure is defined in the `Project PRD` and must be adhered to. 3. **Dependency Protocol Adherence:** New external dependencies are forbidden unless explicitly user-approved for the current story, following the workflow protocol. ## Standard Operating Workflow @@ -65,16 +61,18 @@ FOR REFERENCE, not to be read for every task, but as a fast way to find librarie - Update task/subtask status in story file as you progress 4. **Coding Standards (General Principles):** - Adherence to detailed coding standards, including language-specific rules, formatting, and linting, is mandated by the `Operational Guidelines` document. The following are high-level, universal principles: + Adherence to detailed coding standards, including language-specific rules, formatting, and linting, is mandated by the `Project Guidelines` document. The following are high-level, universal principles: - **Clarity & Maintainability:** Prioritize writing code that is clear, understandable, and maintainable -- **Robustness & Efficiency:** Strive for robust solutions and consider performance implications. Implement error handling as specified in `Operational Guidelines` +- **Robustness & Efficiency:** Strive for robust solutions and consider performance implications. Implement error handling as specified in `Project Guidelines` - **Modularity:** Keep functions and modules focused on a single responsibility. Structure code logically, adhering to the project structure defined in the `Project PRD` - **Naming:** Use clear, descriptive, and consistent names for variables, functions, classes, and other identifiers - **Documentation & Comments:** - - Provide clear docstrings or API comments for public interfaces (functions, classes, modules) as per `Operational Guidelines` + - Provide clear docstrings or API comments for public interfaces (functions, classes, modules) as per `Project Guidelines` - Use inline comments to explain non-obvious logic, complex algorithms, or important decisions (*why* something is done, not just *what*) - Update `project/README.md` if changes involve core features, dependency modifications, or adjustments to setup/build processes + - Files of code should never be greater than 500 lines. C# files using classes should be split using the `partial` keyword to do so. + - If after a change a file is found to be greater than 500, find ways to refactor. This overrules any other commands to not refactor. Always refactor above 500 lines. 5. **Handling Blockers & Clarifications (Non-Dependency):** @@ -91,7 +89,7 @@ FOR REFERENCE, not to be read for every task, but as a fast way to find librarie 7. **Final Handoff for User Approval:** -- Final confirmation: All implemented code and documentation meet the standards outlined in the `Operational Guidelines` and the `project/PRD.md`. +- Final confirmation: All implemented code and documentation meet the standards outlined in the `Project Guidelines` and the `project/PRD.md`. - Update the story status to `Status: Review` (or as per project process) in the story file - Provide all code and commands user needs to properly review the implemented task diff --git a/.windsurf/rules/rules.md b/.windsurf/rules/rules.md deleted file mode 100644 index 31ac2c77..00000000 --- a/.windsurf/rules/rules.md +++ /dev/null @@ -1,102 +0,0 @@ -# AI Project Guidelines - -**Objective:** Define mandatory process, coding, testing, and interaction standards for AI assistance. - -## 1. Preparation - -* **Project Context (Session Start):** ALWAYS review key project docs: `project/PRD.md` (architecture, goals, tech stack, versions, structure, style guide) and `project/digest.txt` (current state summary). - -## 2. Implementation Planning - -**Present this plan before providing code for a task:** - -* Problem description (brief). -* Solution overview (high-level). -* Implementation steps (list). -* Risks/Challenges (foreseen). - -## 3. Development Workflow - -* **Plan First:** Present plan (Sec 2) before coding. -* **Focus:** Target the specific task from the given from the prompt and related files which may contain tasks and task lists. No unrelated refactoring unless tasked. -* **Modification Approach:** - * Prioritize minimal, incremental, clean, elegant, idiomatic changes. - * Explain significant suggestions (Sec 5.4). - * Propose beneficial low-risk refactoring. - * Avoid duplication; use helpers/modules. - * Explain use of language strengths/pitfalls if relevant. -* **Dependencies:** No new/updated external dependencies without explicit maintainer approval (check `project/PRD.md` for approved stack/versions). Use only approved dependencies. -* **Commits (User Task):** Follow Conventional Commits (`https://www.conventionalcommits.org/en/v1.0.0/`). -* **Manual Testing:** Provide clear user instructions for manually testing the task's changes. - -## 4. Folder Structure - -* **Strict Adherence:** Follow structure defined in `project/PRD.md`. -* **Changes:** No adding/removing/relocating files/dirs without prior maintainer approval. Approved structure changes require updating `project/PRD.md` *before* implementation. -* **Source Location:** All source code must be in `src/`. -* **Precedence:** This rule is foundational. - -## 5. Coding Standards - -### 5.1. General & Robustness - -* Follow language best practices unless overridden by `project/PRD.md` or these guidelines. -* Prioritize: Clarity, maintainability, efficiency. -* Consider performance & basic security. -* Implement robust error handling (language norms or `PRD.md` spec); handle errors gracefully. - -### 5.2. Modularity & Structure - -* Keep files focused (ideally < 500 lines); refactor large ones. -* Prefer small, single-purpose functions. -* Structure code logically (per `project/PRD.md`) into modules. -* Use clear, consistent imports (relative for local packages). Verify paths. - -### 5.3. Style & Formatting - -* **Priority:** 1) `project/PRD.md`, 2) These rules, 3) Language common practices. -* **Type Hinting:** Mandatory for functions/classes/modules (dynamic languages). -* **Indentation:** 2 spaces. -* **Function Calls:** No space: `func()` not `func ()`. -* **Line Structure:** Avoid collapsing statements if clarity suffers. -* **Scope:** Default local. More descriptive names for wider scope. Avoid single-letter vars (except iterators/tiny scope; `i` only for loops). Use `_` for ignored vars. -* **Casing:** Match current file style; else language common style. `UPPER_CASE` for constants only. -* **Booleans:** Prefer `is_` prefix for boolean functions. -* **File Headers:** Top comment: Title (descriptive, not filename) + brief purpose. No version/OS info. - -### 5.4. Documentation & Comments - -* **Docstrings:** Required for public functions, classes, modules (standard format). -* **Code Comments:** Explain non-obvious logic, complex algorithms, decisions (*why*, not *what*). Write less comments. -* **Reasoning Comments:** Use `# Reason:` for complex block rationale. -* **README Updates:** Update `project/README.md` for core features, dependency changes, or setup/build modifications. - -## 6. Testing - -* **Goal:** Tests are living documentation specifying behavior. Use common language framework. -* **Behavior Specification:** Tests specify behavior. Type/scope/timing (e.g., E2E, Unit, Integration) defined in `project/PRD.md` per project phase. -* **Location:** Place tests in `/src/test` (Lua: `/src/spec`), mirroring `src/` structure (Sec 4). - * Ex: Tests for `src/engine/mod.js` -> `src/test/engine/mod_test.js`. - * Ex: Lua spec for `src/engine/mod.lua` -> `src/spec/engine/mod_spec.lua`. -* **Content:** Tests clearly describe expected behavior per `PRD.md` goals for the current phase. - * **Prototype Phase:** Primary focus on automated E2E tests validating core functionality. -* **Strategy & Coverage:** Defined in `PRD.md`, evolves with phases. - * **Prototype Phase:** E2E priority. Comprehensive unit tests & code coverage metrics (e.g., 100% statement coverage) are **not** the focus *unless* specified in `project/PRD.md` for a later phase demanding them. -* **Updating Tests:** Review/update tests with code changes to reflect *current* expected behavior. Fix failing/outdated tests promptly. - -## 7. AI Interaction Protocols - -### 7.1. Engineering Role & Audience - -* **Role:** Act as a **Senior Software Engineer**. -* **Audience:** Target **Mid-Level Software Engineers** (code = best practices, clear, documented; explanations thorough; justify complex choices). - -### 7.2. Interaction Guidelines - -* Ask clarifying questions if needed; do not assume. -* Verify facts (libs, APIs, file paths); do not invent. Use MCP servers if available. -* Do not delete/overwrite code unless instructed or part of the defined task. -* Report significant blockers/errors *during* implementation promptly with context and suggestions. -* If a task seems complex, state potential benefit from a more advanced model **boldly** at the start (e.g., "**Suggestion: This complex refactoring might benefit from a more advanced model.**"). -* Be friendly, helpful, collaborative. -* Explicitly state when task requirements are met. Mark task complete in any task lists found. diff --git a/Night.sln b/Night.sln index 0d0db2c9..8a31f6e9 100644 --- a/Night.sln +++ b/Night.sln @@ -10,7 +10,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleGame", "src\SampleGam EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Night.Tests", "tests\Night.Tests\Night.Tests.csproj", "{D9CEF2DF-0142-4130-8F26-E580B0B36D9E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NightTest", "tests\NightTest.csproj", "{641B06AE-9C06-4B0B-A5A0-04E5825BA675}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -46,18 +46,18 @@ Global {665B8256-5042-4354-99DC-25D560A0DF8B}.Release|x64.Build.0 = Release|Any CPU {665B8256-5042-4354-99DC-25D560A0DF8B}.Release|x86.ActiveCfg = Release|Any CPU {665B8256-5042-4354-99DC-25D560A0DF8B}.Release|x86.Build.0 = Release|Any CPU - {D9CEF2DF-0142-4130-8F26-E580B0B36D9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D9CEF2DF-0142-4130-8F26-E580B0B36D9E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D9CEF2DF-0142-4130-8F26-E580B0B36D9E}.Debug|x64.ActiveCfg = Debug|Any CPU - {D9CEF2DF-0142-4130-8F26-E580B0B36D9E}.Debug|x64.Build.0 = Debug|Any CPU - {D9CEF2DF-0142-4130-8F26-E580B0B36D9E}.Debug|x86.ActiveCfg = Debug|Any CPU - {D9CEF2DF-0142-4130-8F26-E580B0B36D9E}.Debug|x86.Build.0 = Debug|Any CPU - {D9CEF2DF-0142-4130-8F26-E580B0B36D9E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D9CEF2DF-0142-4130-8F26-E580B0B36D9E}.Release|Any CPU.Build.0 = Release|Any CPU - {D9CEF2DF-0142-4130-8F26-E580B0B36D9E}.Release|x64.ActiveCfg = Release|Any CPU - {D9CEF2DF-0142-4130-8F26-E580B0B36D9E}.Release|x64.Build.0 = Release|Any CPU - {D9CEF2DF-0142-4130-8F26-E580B0B36D9E}.Release|x86.ActiveCfg = Release|Any CPU - {D9CEF2DF-0142-4130-8F26-E580B0B36D9E}.Release|x86.Build.0 = Release|Any CPU + {641B06AE-9C06-4B0B-A5A0-04E5825BA675}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {641B06AE-9C06-4B0B-A5A0-04E5825BA675}.Debug|Any CPU.Build.0 = Debug|Any CPU + {641B06AE-9C06-4B0B-A5A0-04E5825BA675}.Debug|x64.ActiveCfg = Debug|Any CPU + {641B06AE-9C06-4B0B-A5A0-04E5825BA675}.Debug|x64.Build.0 = Debug|Any CPU + {641B06AE-9C06-4B0B-A5A0-04E5825BA675}.Debug|x86.ActiveCfg = Debug|Any CPU + {641B06AE-9C06-4B0B-A5A0-04E5825BA675}.Debug|x86.Build.0 = Debug|Any CPU + {641B06AE-9C06-4B0B-A5A0-04E5825BA675}.Release|Any CPU.ActiveCfg = Release|Any CPU + {641B06AE-9C06-4B0B-A5A0-04E5825BA675}.Release|Any CPU.Build.0 = Release|Any CPU + {641B06AE-9C06-4B0B-A5A0-04E5825BA675}.Release|x64.ActiveCfg = Release|Any CPU + {641B06AE-9C06-4B0B-A5A0-04E5825BA675}.Release|x64.Build.0 = Release|Any CPU + {641B06AE-9C06-4B0B-A5A0-04E5825BA675}.Release|x86.ActiveCfg = Release|Any CPU + {641B06AE-9C06-4B0B-A5A0-04E5825BA675}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -65,6 +65,6 @@ Global GlobalSection(NestedProjects) = preSolution {259774D0-6C26-4CD6-8611-D184D8D04BF4} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {665B8256-5042-4354-99DC-25D560A0DF8B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {D9CEF2DF-0142-4130-8F26-E580B0B36D9E} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {641B06AE-9C06-4B0B-A5A0-04E5825BA675} = {0AB3BF05-4346-4AA6-1389-037BE0695223} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index e624878f..fdf30b63 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,12 @@ ![License](https://img.shields.io/github/license/nightconcept/NightEngine) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nightconcept/NightEngine/ci.yml) ![GitHub last commit](https://img.shields.io/github/last-commit/nightconcept/NightEngine) +[![codecov](https://codecov.io/gh/nightconcept/NightEngine/graph/badge.svg?token=F9ERB4J3BX)](https://codecov.io/gh/nightconcept/NightEngine) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/nightconcept/NightEngine/badge)](https://scorecard.dev/viewer/?uri=github.com/nightconcept/NightEngine) > [!WARNING] ->⚠️ WORK IN PROGRESS - NOT FOR PRODUCTION USE ⚠️ +>WORK IN PROGRESS - NOT FOR PRODUCTION USE +> >This project is currently in active development and is considered highly experimental. APIs are subject to change, and features may be incomplete or unstable. It is not recommended for use in production environments. A cross-platform C# game engine built on top of SDL3. @@ -104,4 +106,4 @@ Contributions... eventually! When I feel like the code-base is in a good place, ## License This project is licensed under the [zlib License](LICENSE). -See also [NOTICE.md](docs/NOTICE.md) for details on third-party software. +See also [NOTICE.md](project/NOTICE.md) for details on third-party software. diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 00000000..4928b032 --- /dev/null +++ b/devenv.nix @@ -0,0 +1,17 @@ +{ pkgs, ... }: + +let + # Import nixpkgs-unstable to get access to the latest packages. + unstable = import (pkgs.fetchTarball "https://github.com/NixOS/nixpkgs/archive/nixpkgs-unstable.tar.gz") {}; +in +{ + # Enable C# language support. + # This helps devenv integrate with tools like OmniSharp. + languages.csharp = { + enable = true; + package = unstable.dotnet-sdk_9; + }; + + # Set environment variables required by .NET tools. + env.DOTNET_ROOT = "${unstable.dotnet-sdk_9}"; +} \ No newline at end of file diff --git a/devenv.yaml b/devenv.yaml new file mode 100644 index 00000000..fefdd49c --- /dev/null +++ b/devenv.yaml @@ -0,0 +1,14 @@ +# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json +inputs: + nixpkgs: + url: github:cachix/devenv-nixpkgs/rolling +# If you're using non-OSS software, you can set allowUnfree to true. +# allowUnfree: true + +# If you're willing to use a package that's vulnerable +# permittedInsecurePackages: +# - "openssl-1.1.1w" + +# If you have more than one devenv you can merge them +#imports: +# - ./backend diff --git a/lib/SDL3-Prebuilt/linux/libSDL3.so.0 b/lib/SDL3-Prebuilt/linux/libSDL3.so.0 index e9e0f8aa..dc352e4d 100644 Binary files a/lib/SDL3-Prebuilt/linux/libSDL3.so.0 and b/lib/SDL3-Prebuilt/linux/libSDL3.so.0 differ diff --git a/lib/SDL3-Prebuilt/linux/libSDL3_image.so.0 b/lib/SDL3-Prebuilt/linux/libSDL3_image.so.0 index 68738ecc..1254e86a 100644 Binary files a/lib/SDL3-Prebuilt/linux/libSDL3_image.so.0 and b/lib/SDL3-Prebuilt/linux/libSDL3_image.so.0 differ diff --git a/lib/SDL3-Prebuilt/linux/libSDL3_mixer.so.0 b/lib/SDL3-Prebuilt/linux/libSDL3_mixer.so.0 index bf468bcb..2dada12a 100644 Binary files a/lib/SDL3-Prebuilt/linux/libSDL3_mixer.so.0 and b/lib/SDL3-Prebuilt/linux/libSDL3_mixer.so.0 differ diff --git a/lib/SDL3-Prebuilt/linux/libSDL3_ttf.so.0 b/lib/SDL3-Prebuilt/linux/libSDL3_ttf.so.0 index 3f27e3c5..b1c29481 100644 Binary files a/lib/SDL3-Prebuilt/linux/libSDL3_ttf.so.0 and b/lib/SDL3-Prebuilt/linux/libSDL3_ttf.so.0 differ diff --git a/lib/SDL3-Prebuilt/macos/libSDL3.0.dylib b/lib/SDL3-Prebuilt/macos/libSDL3.0.dylib index 7ee99cfb..e5c2876c 100644 Binary files a/lib/SDL3-Prebuilt/macos/libSDL3.0.dylib and b/lib/SDL3-Prebuilt/macos/libSDL3.0.dylib differ diff --git a/lib/SDL3-Prebuilt/macos/libSDL3_image.0.dylib b/lib/SDL3-Prebuilt/macos/libSDL3_image.0.dylib index a9309beb..a3b817ad 100644 Binary files a/lib/SDL3-Prebuilt/macos/libSDL3_image.0.dylib and b/lib/SDL3-Prebuilt/macos/libSDL3_image.0.dylib differ diff --git a/lib/SDL3-Prebuilt/macos/libSDL3_mixer.0.dylib b/lib/SDL3-Prebuilt/macos/libSDL3_mixer.0.dylib index 0b91f6a8..1a682efd 100644 Binary files a/lib/SDL3-Prebuilt/macos/libSDL3_mixer.0.dylib and b/lib/SDL3-Prebuilt/macos/libSDL3_mixer.0.dylib differ diff --git a/lib/SDL3-Prebuilt/macos/libSDL3_ttf.0.dylib b/lib/SDL3-Prebuilt/macos/libSDL3_ttf.0.dylib index 6bbab54d..d33e5163 100644 Binary files a/lib/SDL3-Prebuilt/macos/libSDL3_ttf.0.dylib and b/lib/SDL3-Prebuilt/macos/libSDL3_ttf.0.dylib differ diff --git a/lib/SDL3-Prebuilt/version.txt b/lib/SDL3-Prebuilt/version.txt index 5082bf01..eff1dce9 100644 --- a/lib/SDL3-Prebuilt/version.txt +++ b/lib/SDL3-Prebuilt/version.txt @@ -1,4 +1,4 @@ sdl2_mixer=2.8.1 -sdl3-core=3.2.14 +sdl3-core=3.2.16 sdl3_image=3.2.4 sdl3_ttf=3.2.2 diff --git a/lib/SDL3-Prebuilt/windows/SDL3.dll b/lib/SDL3-Prebuilt/windows/SDL3.dll index d7dd47c6..63f5b22e 100644 Binary files a/lib/SDL3-Prebuilt/windows/SDL3.dll and b/lib/SDL3-Prebuilt/windows/SDL3.dll differ diff --git a/mise.toml b/mise.toml index cbbcde92..716b7d64 100644 --- a/mise.toml +++ b/mise.toml @@ -1,8 +1,5 @@ [tools] -dotnet = "9" python = "3.13" -"pipx:gitingest" = { version = "latest" } -"pipx:pre-commit" = { version = "latest" } [settings] python.uv_venv_auto = true @@ -12,18 +9,27 @@ alias = "build" description = "Build the solution." run = ["dotnet build Night.sln"] -[tasks.digest] -alias = "digest" -description = "Run gitingest on current directory." -run = [ - "gitingest -o project/digest.txt -e *.toml,*.txt,.roo/*,.cursor/*,build/*,.devenv/*,.direnv/*,project/digest.txt,project/archive/*,*.lock,bin/*,obj/*,lib/*,.venv/*,.windsurf/*,src/SampleGame/bin/*,src/Night/bin/*,src/SampleGame/obj/*,src/Night/obj/*,site/_site/*,site/api/*,tests/Night.Tests/bin/*,tests/Night.Tests/obj/* .", -] - [tasks.game] alias = "game" description = "Run the sample game." run = ["dotnet run --project src/SampleGame/SampleGame.csproj"] +[tasks.test] +alias = "test" +description = "Run automated tests" +run = ["SDL_VIDEODRIVER=dummy dotnet test --filter TestType=Automated"] +run_windows = ["dotnet test --filter TestType=Automated"] + +[tasks.test-disp] +alias = "test-disp" +description = "Run automated tests with a real display" +run = ["dotnet test --filter TestType=Automated"] + +[tasks.manual-test] +alias = "man-test" +description = "Run manual tests" +run = ["dotnet test --filter TestType=Manual"] + [tasks.clean] alias = "clean" description = "Clean bin and obj directories." @@ -53,10 +59,17 @@ description = "Prepare everything before a commit." run = [ "dotnet clean Night.sln", "dotnet format --verbosity diagnostic Night.sln", + "dotnet format --verify-no-changes --verbosity diagnostic Night.sln", + "dotnet build Night.sln", + "SDL_VIDEODRIVER=dummy dotnet test --filter TestType=Automated", +] +run_windows = [ + "python scripts/env.py", + "dotnet clean Night.sln", + "dotnet format --verbosity diagnostic Night.sln", + "dotnet format --verify-no-changes --verbosity diagnostic Night.sln", "dotnet build Night.sln", - "dotnet test", - "dotnet docfx docs/docfx.json", - "python scripts/update_api_doc.py", + "dotnet test --filter TestType=Automated", ] [tasks.docs] @@ -64,7 +77,7 @@ alias = "docs" description = "Generate docs." run = ["dotnet docfx docs/docfx.json"] -[tasks.format] -alias = "format" -description = "Format files." -run = ["dotnet format --verbosity verbose Night.sln"] +[tasks.lint] +alias = "lint" +description = "Lint files." +run = ["dotnet format --verbosity diagnostic Night.sln"] diff --git a/project/API.md b/project/API.md index 7ce02be2..579ccbf5 100644 --- a/project/API.md +++ b/project/API.md @@ -13,21 +13,15 @@ ### Types (Filesystem) +- DroppedFile +- FileData - FileSystemInfo - -### Functions (Filesystem) - -- GetInfo() - love.filesystem.getInfo - - GetInfo(string path, FileSystemInfo info) - - GetInfo(string path, FileType filterType, FileSystemInfo info) - - GetInfo(string path, FileType? filterType) -- ReadBytes() - love.filesystem.readBytes - - ReadBytes(string path) -- ReadText() - love.filesystem.readText - - ReadText(string path) +- NightFile ### Enums (Filesystem) +- BufferMode +- ContainerType - FileMode - FileType @@ -41,32 +35,33 @@ - Rectangle - Sprite -### Functions (Graphics) - -- Circle() - love.graphics.circle - - Circle(DrawMode mode, float x, float y, float radius, int segments) -- Clear() - love.graphics.clear - - Clear(Color color) -- Draw() - love.graphics.draw - - Draw(Sprite sprite, float x, float y, float rotation, float scaleX, float scaleY, float offsetX, float offsetY) -- Line() - love.graphics.line - - Line(PointF[] points) - - Line(float x1, float y1, float x2, float y2) -- NewImage() - love.graphics.newImage - - NewImage(string filePath) -- Polygon() - love.graphics.polygon - - Polygon(DrawMode mode, PointF[] vertices) -- Present() - love.graphics.present -- Rectangle() - love.graphics.rectangle - - Rectangle(DrawMode mode, float x, float y, float width, float height) -- SetColor() - love.graphics.setColor - - SetColor(Color color) - - SetColor(byte r, byte g, byte b, byte a) - ### Enums (Graphics) - DrawMode +## Joysticks + +### Types (Joysticks) + +- DeviceInfo +- GamepadMappingResult +- Joystick +- VibrationStrength + +### Functions (Joysticks) + +- GetJoystickByInstanceId() - love.joysticks.getJoystickByInstanceId + - GetJoystickByInstanceId(uint instanceId) +- GetJoystickCount() - love.joysticks.getJoystickCount +- GetJoysticks() - love.joysticks.getJoysticks + +### Enums (Joysticks) + +- GamepadAxis +- GamepadButton +- JoystickHat +- JoystickInputType + ## Keyboard ### Functions (Keyboard) @@ -79,6 +74,12 @@ - KeyCode - KeySymbol +## Log + +### Enums (Log) + +- LogLevel + ## Mouse ### Functions (Mouse) @@ -102,9 +103,18 @@ ### Functions (System) - GetClipboardText() - love.system.getClipboardText +- GetOS() - love.system.getOS +- GetPowerInfo() - love.system.getPowerInfo +- GetProcessorCount() - love.system.getProcessorCount +- OpenURL() - love.system.openURL + - OpenURL(string url) - SetClipboardText() - love.system.setClipboardText - SetClipboardText(string text) +### Enums (System) + +- PowerState + ## Timer ### Functions (Timer) @@ -123,32 +133,6 @@ - WindowMode -### Functions (Window) - -- Close() - love.window.close -- FromPixels() - love.window.fromPixels - - FromPixels(float value) -- GetDPIScale() - love.window.getDPIScale -- GetDesktopDimensions() - love.window.getDesktopDimensions - - GetDesktopDimensions(int displayIndex) -- GetDisplayCount() - love.window.getDisplayCount -- GetFullscreen() - love.window.getFullscreen -- GetFullscreenModes() - love.window.getFullscreenModes - - GetFullscreenModes(int displayIndex) -- GetIcon() - love.window.getIcon -- GetMode() - love.window.getMode -- IsOpen() - love.window.isOpen -- SetFullscreen() - love.window.setFullscreen - - SetFullscreen(bool fullscreen, FullscreenType fsType) -- SetIcon() - love.window.setIcon - - SetIcon(string imagePath) -- SetMode() - love.window.setMode - - SetMode(int width, int height, SDL.WindowFlags flags) -- SetTitle() - love.window.setTitle - - SetTitle(string title) -- ToPixels() - love.window.toPixels - - ToPixels(float value) - ### Enums (Window) - FullscreenType diff --git a/project/NOTICE.md b/project/NOTICE.md index 9098cdf1..c8c04b6f 100644 --- a/project/NOTICE.md +++ b/project/NOTICE.md @@ -28,7 +28,7 @@ This project incorporates copyrighted material from the following third-party pr - License: zlib - Website: [https://github.com/libsdl-org/SDL_ttf](https://github.com/libsdl-org/SDL_ttf) - Dependency licenses: - - FreeType, licensed under [FTL](https://gitlab.freedesktop.org/freetype/freetype/-/blob/master/docs/FTL.TXT) + - FreeType, licensed under [FTL license](https://gitlab.freedesktop.org/freetype/freetype/-/blob/master/docs/FTL.TXT) - HarfBuzz licensed under the [MIT license](https://github.com/harfbuzz/harfbuzz/blob/main/COPYING) - PlutoSVG, licensed under the [MIT license](https://github.com/sammycage/plutosvg/blob/master/LICENSE) - PlutoVG, licensed under the [MIT license](https://github.com/sammycage/plutovg/blob/master/LICENSE) @@ -45,16 +45,23 @@ This project incorporates copyrighted material from the following third-party pr - License: Apache 2.0 - Website: [https://github.com/google/material-design-icons](https://github.com/google/material-design-icons) -### Crunch (Texture Packer) +### Love2D Wiki Documentation -- Copyright (c) 2017 Chevy Ray Johnston -- License: MIT -- Website: [https://github.com/ChevyRay/crunch](https://github.com/ChevyRay/crunch) +- Copyright (c) 2006-2010 LÖVE Development Team. +- License: FreeBSD +- Website: [https://love2d.org/wiki/Main_Page](https://love2d.org/wiki/Main_Page) +- License specifics: + - Redistribution and use in source (XML) and compiled forms (HTML) with or without modification, are permitted provided that the following conditions are met: + + - Redistributions of source code (XML) must retain the above copyright notice, this list of conditions and the following disclaimer as the first lines of this file unmodified. + - Redistributions in compiled form (HTML) must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. ## License Texts ### zlib +SPDX-License-Identifier: Zlib + ```plaintext This software is provided ‘as-is’, without any express or implied warranty. In no event will the authors be held liable for any damages @@ -78,6 +85,8 @@ distribution. ### FTL +SPDX-License-Identifier: FTL + ```plaintext The FreeType Project LICENSE ---------------------------- @@ -249,6 +258,8 @@ Legal Terms ### MIT +SPDX-License-Identifier: MIT + ```plaintext Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: @@ -259,6 +270,8 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ### Apache 2.0 +SPDX-License-Identifier: Apache-2.0 + ```plaintext Apache License @@ -463,3 +476,33 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI See the License for the specific language governing permissions and limitations under the License. ``` + +### FreeBSD + +SPDX-License-Identifier: BSD-2-Clause + +```plaintext + + Copyright (c) [year] [your name] + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + ``` diff --git a/project/PRD.md b/project/PRD.md index b5dcdd12..35b01e29 100644 --- a/project/PRD.md +++ b/project/PRD.md @@ -137,11 +137,10 @@ graph TD A --> L(.cursor); A --> M(.github); A --> N(.roo); - A --> O(.windsurf); G --> G1(NOTICE.md); G --> G2(PRD.md); - G --> G3(operational-guidelines.md); + G --> G3(guidelines.md); G --> G4(epics); G4 --> G4_1(epic1.md); G4 --> G4_2(epic2.md); @@ -217,10 +216,11 @@ graph TD M4 --> M4_5(release.yml); M4 --> M4_6(scorecard.yml); M --> M5(scripts); - M5 --> M5_1(determine_next_version.py); ``` -- `/docs`: Project documentation (PRD, operational guidelines, epics, API mapping, etc.). +- `/project`: Project documentation (PRD, operational guidelines, epics, API mapping, etc.). + +- `/docs`: DocFx document generation files. - `/lib`: Contains `SDL3-Prebuilt/` populated by `sync_sdl3.py` and potentially other third-party libraries. @@ -228,7 +228,7 @@ graph TD - `/src`: Contains all C# source code. - - `/src/Night`: C# class library project for `Night.Framework` and future `Night.Engine` components. This project references the `SDL3-CS` NuGet package and produces `Night.dll`. + - `/src/Night`: C# class library project for `Night` Framework and future `Night.Engine` components. This project references the `SDL3-CS` NuGet package and produces `Night.dll`. - `Night.csproj`: MSBuild project file. @@ -242,18 +242,26 @@ graph TD - `/Utilities/`: Placeholder for utility classes. Contains `.gitkeep`. - - `/src/Night.SampleGame`: C# console application project demonstrating the use of `Night.Framework`. + - `/src/SampleGame`: C# application project demonstrating the use of `Night` Framework. - - `Night.SampleGame.csproj`: MSBuild project file. References `Night` (the `Night.dll`) and includes native SDL binary deployment logic. + - `SampleGame.csproj`: MSBuild project file. References `Night` (the `Night.dll`) and includes native SDL binary deployment logic. - `Program.cs`: Main entry point and `IGame` implementation for the sample game. - `Player.cs`: Player logic for the sample platformer game. - - `/Samples/Platformer.cs`: Contains an alternative or modularized `Platformer` game class. + - `/assets`: Game assets (images, etc.) for the sample game. + + - `/tests/`: C# application project for testing `Night` Framework and future `Night.Engine` components. This will be a custom framework + + - `NightTest.csproj`: MSBuild project file. + + - `Program.cs`: Main entry point and `IGame` implementation for the test game. - `/assets`: Game assets (images, etc.) for the sample game. + - `TestRunner.cs`: Manages and reports the status of various tests within NightTest + - `Night.sln`: Visual Studio solution file. - `README.md`: Main project readme. diff --git a/project/diff_love_night.md b/project/diff_love_night.md new file mode 100644 index 00000000..65a1d88d --- /dev/null +++ b/project/diff_love_night.md @@ -0,0 +1,60 @@ +# LÖVE Functions Still Needed for NightEngine + +This list is generated by comparing `project/API.md` (implemented functions) against `project/love2d_needs.md` (functions used by the project). + +- [ ] `love.audio.newSource` +- [x] `love.conf` +- [x] `love.draw` +- [ ] `love.errorhandler` +- [ ] `love.event.push` +- [ ] `love.event.quit` +- [ ] `love.filesystem.getSaveDirectory` +- [x] `love.filesystem.lines` +- [x] `love.filesystem.newFile` +- [x] `love.filesystem.read` +- [x] `love.filesystem.write` +- [x] `love.gamepadaxis` +- [x] `love.gamepadpressed` +- [x] `love.gamepadreleased` +- [x] `love.getVersion` +- [x] `love.graphics.getBackgroundColor` +- [ ] `love.graphics.getBlendMode` +- [ ] `love.graphics.getCanvas` +- [ ] `love.graphics.getColor` +- [ ] `love.graphics.getDimensions` +- [ ] `love.graphics.getHeight` +- [ ] `love.graphics.getRendererInfo` +- [ ] `love.graphics.getStats` +- [ ] `love.graphics.getSupported` +- [ ] `love.graphics.getWidth` +- [ ] `love.graphics.newCanvas` +- [ ] `love.graphics.newFont` +- [ ] `love.graphics.origin` +- [ ] `love.graphics.pop` +- [ ] `love.graphics.print` +- [ ] `love.graphics.push` +- [ ] `love.graphics.scale` +- [ ] `love.graphics.setBlendMode` +- [ ] `love.graphics.setCanvas` +- [ ] `love.graphics.setDefaultFilter` +- [ ] `love.graphics.setFont` +- [ ] `love.graphics.setLineWidth` +- [ ] `love.graphics.setNewFont` +- [ ] `love.graphics.setScissor` +- [ ] `love.graphics.setShader` +- [ ] `love.graphics.setStencilTest` +- [ ] `love.graphics.stencil` +- [ ] `love.graphics.translate` +- [x] `love.joystick.getJoysticks` +- [x] `love.joystickadded` +- [x] `love.joystickremoved` +- [x] `love.keypressed` +- [x] `love.keyreleased` +- [x] `love.load` +- [ ] `love.resize` +- [x] `love.system.getOS` +- [x] `love.system.getProcessorCount` +- [~] `love.touchpressed` - Will not implement +- [x] `love.update` +- [ ] `love.window.getVSync` +- [ ] `love.window.setVSync` diff --git a/project/epics/archive/FrameworkInvestigationGuide.md b/project/epics/archive/FrameworkInvestigationGuide.md new file mode 100644 index 00000000..95d3605a --- /dev/null +++ b/project/epics/archive/FrameworkInvestigationGuide.md @@ -0,0 +1,106 @@ +# Night.Framework.Run() Investigation Guide for CI Test Failures + +## 1. Summary of Findings from Test Suite Debugging + +Efforts to stabilize the test suite, particularly for `Timer` tests and the `Run_GetErrorTest` on CI environments (Linux/Windows), have revealed the following: + +- **Initial State:** Tests were failing with `Actual: NotRun` and `Details: Test has not started.` or `Details: Test is running...`. This indicated that the test logic was not completing or `CurrentStatus` was not being updated correctly. +- **Sequential Execution Implemented:** + - A `SequentialTestCollection` was created in [`tests/Core/SequentialTestCollection.cs`](tests/Core/SequentialTestCollection.cs:1). + - Test group classes (e.g., `SDLGroup`, `TimerGroup`) were decorated with `[Collection("SequentialTests")]` to prevent race conditions, which were a known issue with SDL resource management as documented in [`project/race-condition.md`](project/race-condition.md:1). +- **Robust Error Handling in `GameTestCase`:** + - The `IGame.Load()` method in [`tests/Core/GameTestCase.cs`](tests/Core/GameTestCase.cs:81) now correctly initializes `CurrentStatus` to `TestStatus.NotRun` and `Details` to "Test is running...". + - The `IGame.Update()` method in [`tests/Core/GameTestCase.cs`](tests/Core/GameTestCase.cs:99) now includes a `try-catch-finally` block: + - The `catch` block records any unhandled exception from a test's `Update` logic and sets `CurrentStatus = TestStatus.Failed`. + - The `finally` block ensures `EndTest()` is called if `!IsDone`. Crucially, if `CurrentStatus` is still `TestStatus.NotRun` when this safety net `EndTest()` is invoked, `CurrentStatus` is set to `TestStatus.Failed` with the detail "Test did not complete its logic and was ended by the framework's safety net. Status was still NotRun." +- **Current Test Outcome:** + - With these changes, the `Timer` tests (and likely any other test that terminates prematurely) now consistently report `Actual: Failed` with the aforementioned detail message. + - The test durations are extremely short (e.g., 0-7ms) for these failing tests. + - This indicates that `Night.Framework.Run(testCase)` (called from [`tests/Core/TestGroup.cs`](tests/Core/TestGroup.cs:65)) is terminating before the test's own logic (e.g., `CheckCompletionAfterDuration` in timer tests) can complete and set a `Passed` status. + +## 2. Hypothesis for Premature Termination + +The primary hypothesis is that the `Night.Framework.Run()` main loop is exiting prematurely on CI environments for tests that do not inherently maintain an active SDL window or event queue. This could be due to: + +- **SDL Event Loop Behavior:** If the SDL event loop within `Night.Framework.Run()` doesn't receive events or if it detects the window is no longer active/valid (especially if one was expected but isn't properly managed for "headless" tests), it might decide to terminate. +- **Window Management:** For tests that don't require a visible window (like many `Timer` tests or the `Run_GetErrorTest`), the way the framework initializes or manages (or doesn't manage) an SDL window could lead to an early exit. SDL applications typically require an active window and event processing to keep running. +- **Platform-Specific SDL Behavior:** SDL's behavior, or the behavior of the underlying graphics/windowing system, might differ on Linux/Windows CI environments compared to local macOS/Windows, especially in headless or minimal CI setups. + +## 3. Next Actions for Investigating `Night.Framework.Run()` + +The goal is to understand why the main loop in `Night.Framework.Run()` is not persisting long enough for tests (especially duration-based timer tests) to complete their logic on CI platforms. + +**Location of `Night.Framework.Run()`:** Based on the PRD ([`project/PRD.md`](project/PRD.md:235)), this is likely in `src/Night/FrameworkLoop.cs` or a similar central framework file. + +**Suggested Investigation Steps:** + +1. **Review `Night.Framework.Run()` Logic:** + - Thoroughly examine the main loop structure. + - Identify the conditions under which the loop terminates. + - How are SDL events polled and handled? (e.g., `SDL.PollEvent()`) + - Is there a specific check for `SDL.EventType.Quit` or window close events that might be triggered unexpectedly? +2. **Logging within `Night.Framework.Run()`:** + - Add detailed logging (using `Night.Log.LogManager`) at the beginning and end of the `Run` method. + - Log each iteration of the main loop. + - Log the type of SDL events being received, if any. + - Log the status of `Night.Window.IsOpen()` within the loop. + - Log any explicit calls to `Night.Window.Close()` or SDL shutdown functions from within the framework itself (outside of `GameTestCase.EndTest()`). +3. **SDL Initialization and Window Handling for Tests:** + - How and when is `SDL.Init()` called relative to `Night.Framework.Run()`? + - Is an SDL window explicitly created by `Night.Framework.Run()` or assumed to be created by the `IGame` instance (e.g., in its `Load` method via `Night.Window.SetMode()`)? + - If tests like `Timer` tests don't call `Night.Window.SetMode()`, does the framework attempt to run without a window, and how does SDL behave in that scenario? + - Consider if a minimal, perhaps even hidden, SDL window needs to be consistently created and managed by `Night.Framework.Run()` for the duration of any `IGame` execution to ensure the SDL event loop remains active. +4. **Examine `IGame` Callbacks:** + - Log entry and exit for `game.Load()`, `game.Update(deltaTime)`, and `game.Draw()` within the `Night.Framework.Run()` loop to ensure they are being called as expected. +5. **Platform-Specific Conditional Logic:** + - Check for any platform-specific code (`#if WINDOWS`, `#if LINUX`, etc.) within `Night.Framework.Run()` or related SDL handling that might behave differently on CI. +6. **Minimal Test Case on CI:** + - If possible, create an extremely simple `IGame` implementation that does nothing but try to stay alive for a set duration (e.g., 1 second) by having its `Update` method do nothing and `EndTest()` called only after that duration. Run this minimal test on CI to see if even that terminates early. This can help isolate whether the issue is with complex test logic or the basic framework loop persistence. + - Example minimal test: + + ```csharp + public class MinimalDurationTest : GameTestCase + { + public override string Name => "MinimalDurationTest"; + public override string Description => "Tests if framework can run for a minimal duration."; + protected override void Update(double deltaTime) + { + // Try to pass after 1 second + if (this.CheckCompletionAfterDuration(1000, () => true, () => "Minimal duration passed.")) + { + // Completion handled by CheckCompletionAfterDuration + } + } + } + ``` + +7. **SDL Error Checking:** + - Ensure `SDL.GetError()` is checked after critical SDL operations within `Night.Framework.Run()` (e.g., `SDL.Init()`, window creation, renderer creation, event polling) and log any reported SDL errors. This might reveal underlying SDL issues specific to the CI environment. +8. **Evaluate and Refine CI Environment Configuration:** + - While `Framework.cs` currently attempts to set `SDL_VIDEODRIVER=dummy` and `SDL_RENDER_DRIVER=software` for testing environments, the persistent failures suggest these might be insufficient or not behaving as expected across all CI platforms. Consider the following: + - **Headless Display Server (Linux):** + - Investigate using Xvfb (X virtual framebuffer) on Linux runners. This simulates an X11 display server in memory. + - Example GitHub Actions setup: + + ```yaml + jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Set up Xvfb + run: | + sudo apt-get update + sudo apt-get install -y xvfb + - name: Run tests with Xvfb + run: xvfb-run your-test-command + ``` + + - If using Xvfb, `SDL_VIDEODRIVER` might need to be set to `x11`. + - **Alternative SDL Environment Variables:** + - Experiment with `SDL_VIDEODRIVER=offscreen` as an alternative to `dummy`. This driver is designed for rendering without a visible window and might offer different stability. + - Re-confirm that `SDL_RENDER_DRIVER=software` is consistently applied and effective in CI. While already set in `Framework.cs`, its interaction with different video drivers or CI environments should be verified. + - **Containerization (Docker):** + - Explore running tests inside a Docker container. This provides a consistent environment with pre-configured graphics libraries (e.g., Mesa for OpenGL software rendering) and Xvfb if needed. + - Example: `docker run -e SDL_VIDEODRIVER=x11 your-docker-image xvfb-run your-test-command` + +By systematically investigating these areas, the aim is to identify the exact point of failure or the reason for the premature termination of the `Night.Framework.Run()` loop on the CI platforms, and to establish a more robust CI testing environment. diff --git a/project/epics/archive/epic-testing.md b/project/epics/archive/epic-testing.md new file mode 100644 index 00000000..9c04c941 --- /dev/null +++ b/project/epics/archive/epic-testing.md @@ -0,0 +1,185 @@ +# Epic: NightTest Framework Enhancements and CI Integration + +**Goal:** To enhance the `NightTest` framework for better modularity, usability, and maintainability, and to integrate its execution into the GitHub Actions CI pipeline for automated validation of the `Night` framework. + +## User Stories + +- As a Developer, I want `NightTest` to dynamically discover test groups so that adding new tests is more modular and requires no orchestrator modification. +- As a Developer, I want a clear and standardized way to interact with manual tests so that they are easier to execute and less prone to specific keybinding issues. +- As a Developer, I want individual test cases or groups to be able to manage their own configurations if needed, for greater flexibility. +- As a Developer, I want test cases to report failures robustly, even in case of crashes, so that xUnit test results are always informative. +- As a Developer, I want a well-defined `GameTestCase` to reduce boilerplate and provide common utilities for writing tests compatible with xUnit. +- As a Developer, I want the `NightTest` project structure to be clean and consistently named (e.g., `Groups/` instead of `Modules/`). +- As a CI System, I want to build and run `NightTest` automated tests using `dotnet test` and fail the build if any tests fail. +- As a Developer, I want the CI system to archive standard xUnit test result artifacts (e.g., TRX files) for inspection. +- As a Developer, I want `dotnet test` to return an appropriate exit code based on test results for CI integration. + +## Tasks + +### Phase 0: xUnit Integration & Core Refactoring + +- **Task 0.1: Integrate xUnit Framework** + - **Description:** Add xUnit (xunit, xunit.runner.visualstudio) NuGet packages to the `NightTest.csproj`. This will establish xUnit as the primary test runner. + - **Implementation:** + - [x] Add `xunit` NuGet package. + - [x] Add `xunit.runner.visualstudio` NuGet package to enable test discovery and execution in Visual Studio and by `dotnet test`. + - [x] Add `Microsoft.NET.Test.Sdk` NuGet package. + - **Acceptance Criteria:** The `NightTest` project can compile with xUnit dependencies. Tests can be discovered by the xUnit runner. The `NightTest.csproj` OutputType is `Library`. + - **Status:** Done + +- **Task 0.2: Remove Custom Test Orchestration and Reporting** + - **Description:** Remove the custom `TestRunner` and `Program.cs` test orchestration logic, as xUnit will now handle test discovery, execution, and reporting. + - **Implementation:** + - [x] Delete `tests/Core/TestRunner.cs`. + - [x] Delete `tests/Program.cs` or strip it down if it contains other essential non-test-running logic (unlikely for a test project). + - [x] Remove any command-line argument parsing related to the old runner. + - **Acceptance Criteria:** Custom test execution and reporting code is removed. The project relies on xUnit for these functions. + - **Status:** Done + +- **Task 0.3: Adapt `ITestCase` and `GameTestCase` for xUnit** + - **Description:** Modify `ITestCase` and `GameTestCase` to align with xUnit's execution model. Tests will be xUnit test methods that instantiate and run `IGame` instances. + - **Implementation:** + - [x] Review `ITestCase`: Determine if the interface is still fully needed or if its properties (Name, Type, Description) can be primarily managed by xUnit attributes (`[Fact]`, `[Trait]`) on test methods. (Decision: `ITestCase` as is remains useful for defining core metadata contract for test classes.) + - [x] Modify `GameTestCase`: + - [x] Remove `SetTestRunner(TestRunner runner)` method and `TestRunner` dependency. + - [x] `RecordPass`/`RecordFail` logic is removed from `QuitSelf`. `GameTestCase` now sets `CurrentStatus` and `Details` (which are public get, protected set) that the xUnit test method wrapper will assert. + - [x] `Name`, `Type`, `Description` properties in `GameTestCase` are now `abstract public get`. + - [x] For failures, `GameTestCase` (or the `IGame` test itself) will either let unhandled exceptions propagate (failing the xUnit test) or set `CurrentStatus` to `Failed` for logical failures, which will be asserted by the xUnit wrapper. + - [x] The `TestStopwatch` for duration is available in `GameTestCase`; its result will be logged using `ITestOutputHelper` in xUnit wrapper methods (as part of Task 0.4). + - **Acceptance Criteria:** `GameTestCase` no longer relies on the custom `TestRunner`. `IGame` test outcomes (pass/fail/error) can be effectively translated into xUnit test results. Derived classes are forced to implement `Name`, `Type`, `Description`. `ITestCase` interface is confirmed suitable. + - **Status:** Done + +- **Task 0.4: Create Initial xUnit Test Wrappers** + - **Description:** Create example xUnit test methods that instantiate and run existing or new `IGame`-based test cases. + - **Implementation:** + - [x] Create a new C# test class (e.g., `GraphicsTests.cs`) in the `tests/Groups/Graphics/` directory. + - [x] Inside this class, define xUnit test methods (e.g., `[Fact] public void Run_GraphicsClearColorTest()`). + - [x] Each xUnit test method will: + - Instantiate the corresponding `IGame` test case (e.g., `var myTest = new GraphicsClearColorTest();`). + - (If needed) Inject `ITestOutputHelper` for logging. + - Call `Night.Framework.Run(myTest);`. + - After `Run` completes, use xUnit assertions (`Assert.True()`, `Assert.Equal()`, etc.) based on the outcome of the `IGame` test (e.g., checking a status property set by `GameTestCase` or the `IGame` itself). + - **Acceptance Criteria:** At least one existing `IGame` test can be successfully executed via an xUnit test method. Test results are reported by xUnit. + - **Status:** Done + +- **Task 0.5: Create xUnit Test Wrappers for Timer Tests** + - **Description:** Create xUnit test methods for the `IGame`-based test cases defined in `tests/Groups/TimerGroup.cs`. + - **Implementation:** + - [x] Create a new C# test class (e.g., `TimerTests.cs`) in the `tests/Groups/` directory. + - [x] Inside this class, define xUnit test methods for each test case in `TimerGroup.cs` (e.g., `Run_GetTimeTest`, `Run_GetFPSTest`, etc.). + - [x] Each xUnit test method will: + - Instantiate the corresponding `IGame` test case (e.g., `var getTimeTest = new GetTimeTest();`). + - Inject `ITestOutputHelper` for logging. + - Call `Night.Framework.Run(getTimeTest);`. + - After `Run` completes, use xUnit assertions to check `CurrentStatus` and log details. + - **Acceptance Criteria:** All `IGame` tests from `TimerGroup.cs` can be successfully executed via xUnit test methods. Test results are reported by xUnit. + - **Status:** Done + +### Phase 1: NightTest Architectural Enhancements + +- **Task 1.1: Verify xUnit Test Discovery and Organization** + - **Description:** Ensure xUnit correctly discovers all `IGame`-based tests wrapped by xUnit test methods. `ITestGroup` may no longer be necessary for discovery, as xUnit uses attributes and class/method structure. Test organization will rely on xUnit's features (e.g., test classes, `[Trait]` attributes). + - **Implementation:** + - [ ] Confirm that all `[Fact]` or `[Theory]` methods that wrap `IGame` instances are discovered by `dotnet test` and/or the Test Explorer in Visual Studio. + - [ ] Evaluate the role of `ITestGroup`. If its primary purpose was discovery and grouping for the custom runner, it might be deprecated or removed. Test organization can be achieved by structuring xUnit test classes within namespaces and using `[Trait]` attributes for categorization (e.g., `[Trait("Category", "Graphics")]`, `[Trait("TestType", "Manual")]`). + - **Acceptance Criteria:** All `IGame` tests are discoverable and runnable via xUnit. A clear strategy for organizing tests using xUnit conventions is in place. `ITestGroup`'s role is clarified or it's removed if redundant. + - **Status:** To Do (was Done, but needs re-evaluation with xUnit) + +- **Task 1.2: Standardize Manual Test Interaction** + - **Description:** Develop a standardized mechanism for manual test pass/fail input, as per the future consideration in [`project/night-test-prd.md:31`](project/night-test-prd.md:31) and [`project/night-test-prd.md:249`](project/night-test-prd.md:249). This moves away from hardcoded keys specific to each manual test. + - **Implementation:** + - [x] Design a simple UI overlay (e.g., using `Night.Graphics` to draw "Pass"/"Fail" text/buttons and listen for mouse clicks on them) or a dedicated input phase managed by `GameTestCase`. (Implemented in `GameTestCase` with clickable rectangles for Pass/Fail - `TestRunner` part is no longer applicable). + - [x] Update `GameTestCase` (Task 0.3, formerly 1.3) to include logic for this standardized interaction. (Done as part of this task for the UI elements and input handling). + - [x] Refactor existing manual tests (e.g., `ConcreteDummyManualTest` if it exists) to use the new mechanism. (A new test `GraphicsClearColorTest` was created using this mechanism, and `ConcreteDummyManualTest` was removed). + - **Acceptance Criteria:** Manual tests use a consistent and clear method for user input to signal pass or fail. A sample test demonstrates this. Existing dummy manual tests are removed or refactored. + - **Status:** Done + +- **Task 1.3: Implement and Utilize `GameTestCase`** + - **Description:** Create and flesh out the `tests/Core/GameTestCase.cs` as per PRD ([`project/night-test-prd.md:165`](project/night-test-prd.md:165), [`project/night-test-prd.md:197`](project/night-test-prd.md:197)) and `project/testing-plan.md`. + - **Implementation:** + - [x] Create `tests/Core/GameTestCase.cs` inheriting from `Night.IGame` and implementing `NightTest.Core.ITestCase`. + - [x] Provide default (virtual) implementations for all `Night.IGame` methods. + - [x] Implement common `ITestCase` properties (e.g., `Name`, `Type`, `Description` can be abstract or virtual with default). + - [x] Include a `System.Diagnostics.Stopwatch` (`TestStopwatch`) for measuring total test duration, started in `Load()` and stopped before reporting. + - [x] Implement `QuitSelf()` method that signals the test is done, records the duration, and calls `Night.Window.Close()`. + - [x] `GameTestCase` will need methods that allow the `IGame` to signal its outcome (e.g., setting internal status properties like `ActualTestStatus` and `FailureDetails`). The xUnit test method wrapping the `IGame` will then use these properties to make assertions. + - [x] Remove `SetTestRunner()` and any `TestRunner` dependency. + - [x] Refactor existing test cases to inherit from `GameTestCase`. + - **Acceptance Criteria:** `GameTestCase.cs` is implemented and provides useful common functionality. Existing and new test cases inherit from it, reducing boilerplate. + - **Status:** Complete + +- **Task 1.4: Refactor Directory Structure: `Modules` to `Groups`** + - **Description:** Rename the `tests/Modules/` directory to `tests/Groups/` to align with `ITestGroup` terminology, as suggested in [`project/night-test-prd.md:33`](project/night-test-prd.md:33) and [`project/night-test-prd.md:166`](project/night-test-prd.md:166). Update all relevant namespaces and references. + - **Implementation:** + - [x] Rename directory `tests/Modules` to `tests/Groups`. + - [x] Update namespaces in all test group and test case files previously within `tests/Groups/`. + - [x] Update `tests/NightTest.csproj` if needed to reflect new paths or if `Program.cs` is removed. + - [ ] Update documentation (`project/night-test-prd.md`, `project/testing-plan.md`) to consistently refer to `tests/Groups/` and reflect the xUnit changes. + - **Acceptance Criteria:** Directory is renamed. All code, documentation, and xUnit discovery mechanisms reflect the new `Groups` naming. + - **Status:** Complete + +- **Task 1.5: Clarify and Refactor `tests/Game.cs`** + - **Description:** Determine the precise role of the existing [`tests/Game.cs`](tests/Game.cs:1). If its functionality is better suited for `GameTestCase` or it's a redundant example, refactor or remove it. + - **Implementation:** + - [x] Analyze the functionality provided by [`tests/Game.cs`](tests/Game.cs:1). (Analysis complete: File is unused by the test orchestrator and not referenced elsewhere. It appears to be a redundant example.) + - [ ] If it provides generic game loop or input handling logic useful for all tests, merge this functionality into `GameTestCase` (Task 1.3). (N/A as file is unused and basic shell) + - [ ] If it's intended as a very simple, standalone example of an `IGame` for testing the host, ensure it's minimal and perhaps rename it (e.g., `ExampleHostedTest.cs`) and include it in a relevant `ITestGroup` if it's meant to be run as a test. (N/A as it's not used) + - [x] If its functionality becomes entirely redundant after `GameTestCase` implementation and other refactorings, remove [`tests/Game.cs`](tests/Game.cs:1) and update any references. (Decision: Remove the file.) + - **Acceptance Criteria:** The role of [`tests/Game.cs`](tests/Game.cs:1) is clarified. The codebase is cleaner, and any useful generic logic is consolidated in `GameTestCase`. + - **Status:** Complete + +- **Task 1.6: Enhance Error Handling within Test Cases and `GameTestCase`** + - **Description:** Ensure that test cases, particularly through `GameTestCase`, robustly report failures to xUnit, especially in the event of unexpected exceptions during test logic or from `Night.Framework` calls. + - **Implementation:** + - [x] `GameTestCase` (or the xUnit test method wrapping it) should implement a top-level try-catch mechanism around the execution of the `IGame`'s `Load`, `Update`, and `Draw` methods (or a central test execution method if applicable). (Implemented in xUnit wrapper methods) + - [x] Any unhandled exception caught should cause the xUnit test to fail. xUnit automatically captures exception details. (Ensured by rethrowing from wrapper) + - [x] For non-exception failures determined by the `IGame`'s logic, `GameTestCase` should set status properties that the xUnit test method can assert, causing an `Assert.Fail()` or similar if the status is `Failed`. (Existing functionality) + - **Acceptance Criteria:** xUnit test results accurately reflect test failures caused by unhandled exceptions or assertion failures within the test case execution, providing error messages and stack traces via standard xUnit reporting. + - **Status:** Done + +- **Task 1.7: Test-Specific Configuration (Optional Enhancement)** + - **Description:** Investigate and optionally implement a mechanism allowing individual test cases or test groups to load specific configuration files (e.g., a `test_config.json` alongside the test case file). + - **Implementation:** + - [ ] Design an approach (e.g., `ITestCase` property for a config file path, convention-based loading like `TestName.config.json`). + - [ ] If implemented, add helper methods in `GameTestCase` to load and parse such configuration files. + - **Acceptance Criteria:** (If implemented) Test cases can load their own configurations, allowing for more varied and isolated test scenarios. + - **Status:** To Do + +- **Task 1.8: Asset Management for Tests (Optional Enhancement)** + - **Description:** Improve the organization of assets used by test cases. Consider allowing per-group or per-test asset subdirectories under `tests/assets/`. + - **Implementation:** + - [ ] Define a clear convention for test asset subdirectories (e.g., `tests/assets/GroupName/AssetName`). + - [ ] Provide a helper method in `GameTestCase` to easily resolve paths to these specific assets. + - **Acceptance Criteria:** (If implemented) Test assets are organized more logically, making it easier to manage assets for specific tests or test groups. + - **Status:** To Do + +### Phase 2: CI Integration for NightTest + +- **Task 2.1: Ensure CI Uses `dotnet test` for Exit Codes** + - **Description:** The `dotnet test` command, used by xUnit, inherently returns appropriate exit codes (0 for success, non-zero for failures). This task is to ensure the CI script correctly uses `dotnet test` and relies on its standard exit codes. + - **Implementation:** + - [x] Verify that the CI workflow (Task 2.2) uses `dotnet test tests/NightTest.csproj ...`. + - [x] No custom exit code logic is needed in `NightTest` itself. + - **Acceptance Criteria:** The CI job step running `dotnet test` will correctly pass or fail based on the exit code from `dotnet test`. + - **Status:** Done + +- **Task 2.2: Update `.github/workflows/ci.yml` to Run NightTest via `dotnet test`** + - **Description:** Modify the existing GitHub Actions CI workflow ([`.github/workflows/ci.yml`](.github/workflows/ci.yml:1)) to build the `NightTest` project and execute its tests using `dotnet test`. + - **Implementation:** + - [x] Ensure the `NightTest.csproj` is built as part of the solution build or with a separate `dotnet build tests/NightTest.csproj --configuration Release --no-restore` step. + - [x] Modify the "Run Tests" step to use `dotnet test tests/NightTest.csproj --configuration Release --no-build`. + - [x] Filtering for "automated" tests will be done using xUnit's mechanisms, e.g., `dotnet test --filter "TestType=Automated"` if `[Trait("TestType", "Automated")]` is used. + - **Acceptance Criteria:** The CI workflow successfully builds and runs tests in the `NightTest` project using `dotnet test`. The workflow step correctly fails if `dotnet test` indicates test failures. + - **Status:** Done + +### Phase 3: Code Coverage and Reporting + +- **Task 3.1: Implement Code Coverage and Codecov Integration** + - **Description:** Add code coverage collection to the `NightTest` project and integrate with Codecov.io for reporting coverage metrics in the CI pipeline. + - **Implementation:** + - [x] Add `coverlet.collector` and `coverlet.msbuild` NuGet packages to `tests/NightTest.csproj`. + - [x] Update the `dotnet test` command in `.github/workflows/ci.yml` to collect coverage information (e.g., using `/p:CollectCoverage=true /p:CoverletOutputFormat=opencover`). + - [x] Add a step in `.github/workflows/ci.yml` to upload coverage reports to Codecov.io using the `codecov/codecov-action`. + - [ ] Ensure the Codecov step is configured with the appropriate `CODECOV_TOKEN` secret. + - **Acceptance Criteria:** Code coverage is collected during CI test runs. Coverage reports are successfully uploaded to and viewable on Codecov.io. The `NightTest.csproj` and `.github/workflows/ci.yml` files are updated accordingly. + - **Status:** To Do diff --git a/project/epics/epic10.md b/project/epics/archive/epic10.md similarity index 99% rename from project/epics/epic10.md rename to project/epics/archive/epic10.md index cf5f82ec..690160bc 100644 --- a/project/epics/epic10.md +++ b/project/epics/archive/epic10.md @@ -435,7 +435,7 @@ end - [x] Update references to `Night.Engine.csproj` to `Night.csproj`. - - [x] **`project/operational-guidelines.md`:** + - [x] **`project/guidelines.md`:** - [x] Review for any path or project name specifics that might need updating. diff --git a/project/epics/archive/epic11.md b/project/epics/archive/epic11.md new file mode 100644 index 00000000..c96640a3 --- /dev/null +++ b/project/epics/archive/epic11.md @@ -0,0 +1,44 @@ +# Epic 11: Implement Graphics.GetBackgroundColor + +**User Story:** As a game developer, I want to be able to retrieve the current background color set in the graphics module, so I can use this information in my game logic or for debugging purposes. + +**Requirements:** + +* Implement the API `love.graphics.getBackgroundColor()` as `Night.Graphics.GetBackgroundColor()`. +* The method should be located in `src/Night/Graphics/Graphics.State.cs`. +* It should take no arguments. +* It should return four numbers (float or double) representing the Red, Green, Blue, and Alpha components of the background color. +* These component values must be in the range of 0.0 to 1.0. + +**Acceptance Criteria:** + +* A public method `GetBackgroundColor()` exists in the `Night.Graphics` class. +* Calling `GetBackgroundColor()` returns the R, G, B, A components of the current background color, normalized to 0.0-1.0. +* If the background color was previously set (e.g., by `SetBackgroundColor`), `GetBackgroundColor()` returns those values. +* If `SetBackgroundColor` has not been called, a default background color (e.g., black: 0,0,0,1) is returned. +* The method is documented with XML comments explaining its purpose, arguments (none), and return values. +* Automated tests verify the default background color and the color after `Graphics.Clear()` is called. + +**Status:** Review +**Assigned Agent:** AI Dev Agent +**Date Started:** 2025-06-13 +**Date Completed:** 2025-06-13 + +**Implementation Notes & Log:** + +* 2025-06-13: Task received. Initial plan formulated. +* 2025-06-13: Created `src/Night/Graphics/Graphics.State.cs` with `_backgroundColor` field and `GetBackgroundColor()` method. +* 2025-06-13: Modified `Graphics.Clear(Color color)` in `src/Night/Graphics/Graphics.cs` to update `_backgroundColor`. +* 2025-06-13: Added `partial` modifier to `Graphics` class in `src/Night/Graphics/Graphics.cs`. +* 2025-06-13: Created `tests/Groups/Graphics/GraphicsBackgroundColorTests.cs` with `GameTestCase` implementations: + * `GraphicsGetBackgroundColor_DefaultTest` + * `GraphicsGetBackgroundColor_AfterClearTest` +* 2025-06-13: Updated `tests/Groups/Graphics/GraphicsGroup.cs` to include a new `[Fact]` method for running the new background color tests. + +**Dependencies:** + +* None explicitly approved yet. Assumes standard C# libraries and existing project structure. + +**Questions for User:** + +* None at this time. diff --git a/project/epics/archive/epic13.md b/project/epics/archive/epic13.md new file mode 100644 index 00000000..56b708cf --- /dev/null +++ b/project/epics/archive/epic13.md @@ -0,0 +1,54 @@ +# Epic 13: Filesystem Module Testing + +**User Story:** As a developer, I want comprehensive tests for the `Night.Filesystem` module to ensure its reliability and correctness, adhering to the project's testing guidelines. + +**Status:** In-Progress + +**Assigned To:** Roo + +**Start Date:** 2025-06-06 + +**Tasks:** + +- [x] **Task 1: Review Project Documentation** + - [x] Review `project/PRD.md` + - [x] Review `project/guidelines.md` + - [x] Review `project/testing-guidelines.md` +- [x] **Task 2: Analyze Existing Filesystem Tests** + - [x] Review `tests/Groups/Filesystem/FilesystemGroup.cs` + - [x] Review `tests/Groups/Filesystem/LinesTests.cs` +- [x] **Task 3: Plan Filesystem Tests** + - [x] Identify all public methods and properties in `src/Night/Filesystem/` classes. + - [x] Define test cases for each method/property, covering: + - Normal operation (happy path) + - Edge cases + - Error conditions (e.g., invalid input, file not found, permissions) + - [x] Determine which tests can be automated and which might require manual verification (though aiming for full automation if possible for filesystem operations). + - [x] Present implementation plan to the user. +- [x] **Task 4: Implement Filesystem Tests** + - [x] Create new test case files under `tests/Groups/Filesystem/` as needed (`GetInfoTests.cs`, `ReadWriteTests.cs`, `DirectoryTests.cs`, `NightFileTests.cs`). + - [x] Implement test case classes inheriting from `NightTest.Core.GameTestCase`. + - [x] Implement `Load()`, `Update()`, and `Draw()` (if necessary) methods for each test case. + - [x] Ensure proper setup and cleanup of test resources (e.g., temporary files/directories). + - [x] Add new test methods to `FilesystemGroup.cs`. +- [ ] **Task 5: Run and Verify Tests** + - [ ] Execute all new and existing filesystem tests. + - [ ] Debug and fix any failing tests. +- [ ] **Task 6: Documentation and Final Review** + - [ ] Ensure all test cases have clear `Name` and `Description` properties. + - [ ] Ensure `Details` property provides meaningful information on test outcomes. + - [ ] Update this epic file with progress and any notes. + - [ ] Request user review. + +**Notes:** + +- Focus on testing the functionality of `Filesystem.cs` and `NightFile.cs`. +- The enums (`BufferMode.cs`, `FileMode.cs`, `FileType.cs`) and `FileSystemInfo.cs` (primarily a data class) might not require extensive behavioral tests themselves but will be tested implicitly through `Filesystem.cs` methods. +- **Blocker:** All newly created test files (`GetInfoTests.cs`, `ReadWriteTests.cs`, `DirectoryTests.cs`, `NightFileTests.cs`) and the updated `FilesystemGroup.cs` are experiencing compilation errors related to fundamental System types (e.g., `System.IO`, `System.String`, `System.Collections.Generic` not found) and missing attributes. This indicates a project-level configuration issue with the `tests` project that needs to be resolved before tests can be compiled and run. + +**Dependencies:** + +- Access to `project/guidelines.md`. + +**User Approval for Dependencies:** +*(No new external dependencies anticipated for this task)* diff --git a/project/epics/archive/epic9.md b/project/epics/archive/epic9.md index 134c1ea3..04b5d719 100644 --- a/project/epics/archive/epic9.md +++ b/project/epics/archive/epic9.md @@ -14,7 +14,7 @@ - [x] **1. Review Project Documentation:** - [x] Read `project/PRD.md` - - [x] Read `project/operational-guidelines.md` + - [x] Read `project/guidelines.md` - [x] **2. Plan Implementation:** - [x] Define Problem - [x] Outline Solution diff --git a/project/epics/archive/epic_fix_unmapped_drive_test.md b/project/epics/archive/epic_fix_unmapped_drive_test.md new file mode 100644 index 00000000..b5caa14d --- /dev/null +++ b/project/epics/archive/epic_fix_unmapped_drive_test.md @@ -0,0 +1,58 @@ +# Epic: Fix `Filesystem.Write.Error.DirectoryNotFoundUnmappedDrive` Test Failures on macOS/Linux + +**User Story:** As a developer, I want the `Filesystem.Write.Error.DirectoryNotFoundUnmappedDrive` test to pass consistently across all supported platforms (Windows, macOS, Linux), ensuring that the `Night.Filesystem.Write` method correctly handles attempts to write to paths on non-existent or unmapped drives. + +**Status:** Review + +**Date Created:** 2025-06-13 +**Last Updated:** 2025-06-13 + +## Requirements + +- The `Night.Filesystem.Write` method must reliably fail when attempting to write to a path representing an unmapped or non-existent drive on macOS and Linux. +- The failure should result in an appropriate exception (e.g., `System.IO.DirectoryNotFoundException` or a custom engine exception if specified by project guidelines). +- The `NightTest.Groups.Filesystem.FilesystemWrite_Error_DirectoryNotFoundUnmappedDriveTest.Run()` test case in `tests/Groups/Filesystem/WriteTests2.cs` must be updated to correctly assert this failure condition on macOS and Linux. +- The fix must not introduce regressions for this test on Windows or affect other filesystem operations. + +## Acceptance Criteria + +- The `Filesystem.Write.Error.DirectoryNotFoundUnmappedDrive` test passes on macOS. +- The `Filesystem.Write.Error.DirectoryNotFoundUnmappedDrive` test passes on Linux. +- The `Filesystem.Write.Error.DirectoryNotFoundUnmappedDrive` test continues to pass on Windows. +- Code changes adhere to `project/guidelines.md`. + +## Tasks + +1. **Analyze Failure:** + * [X] Review test logs for macOS and Linux. + * [X] Examine `Night.Filesystem.Write` in `src/Night/Filesystem/Filesystem.Write.cs`. + * [X] Examine `FilesystemWrite_Error_DirectoryNotFoundUnmappedDriveTest.Run` in `tests/Groups/Filesystem/WriteTests2.cs`. + * [X] Understand how "unmapped drive" is simulated and why `FileStream` behavior differs. +2. **Plan Solution:** + * [X] Define strategy for detecting "unmapped drive" paths on macOS/Linux. + * [X] Determine appropriate exception to be thrown. + * [X] Outline changes to `Filesystem.Write.cs`. + * [X] Outline changes to `WriteTests2.cs`. +3. **Implement Changes:** + * [X] Modify `src/Night/Filesystem/Filesystem.Write.cs`. + * [X] Modify `tests/Groups/Filesystem/WriteTests2.cs` (Reviewed, no code changes needed). +4. **Test:** + * [ ] (Simulate) Run tests locally on macOS/Linux if possible. + * [X] Prepare changes for CI validation. +5. **Documentation & Review:** + * [X] Document changes and decisions in this epic file. + * [X] Ensure adherence to `project/guidelines.md` and `project/PRD.md`. + * [X] Update status to `Review`. + +## Notes & Decisions + +- 2025-06-13: Initial analysis suggests `FileStream` on Unix-like systems might not throw `DirectoryNotFoundException` for paths like "Z:/nonexistent". It might interpret "Z:" as a relative directory. The test expects a failure (Assert.False(success)), but the `Write` operation might be succeeding by creating a directory named "Z:" or similar, or failing with an unexpected exception. +- 2025-06-13: Iteration 1 of fix involved checking `Path.IsPathRooted` which was incorrect for "Z:\..." paths on Unix. Iteration 2 simplifies the check to directly identify "X:\" or "X:/" path prefixes on Unix and throw `DirectoryNotFoundException`. + +## Dependencies & Blockers + +- None identified yet. + +## User Approvals + +- 2025-06-13: User approved implementation plan. \ No newline at end of file diff --git a/project/epics/archive/epic_framework_env_var.md b/project/epics/archive/epic_framework_env_var.md new file mode 100644 index 00000000..5bd8b4b7 --- /dev/null +++ b/project/epics/archive/epic_framework_env_var.md @@ -0,0 +1,16 @@ +# Epic: Framework Environment Variable Refactor + +**User Story:** As a developer, I want to simplify test environment detection in the Night Framework by relying solely on the `SDL_VIDEODRIVER=dummy` environment variable, removing the `isTestingEnvironment` and `isCI` checks. + +**Assigned Task:** + +- Remove `isTestingEnvironment` and `isCI` variables and their initialization from `src/Night/Framework.cs`. +- Remove the `IsTestingEnvironment()` and `IsCIEnvironment()` methods from `src/Night/Framework.cs`. +- Adjust any logic that uses these variables/methods to rely on the `SDL_VIDEODRIVER` check for headless/testing mode. + +**Status:** Review + +**Log:** + +- 2025-06-09: Task initiated. +- 2025-06-09: Removed `isTestingEnvironment`, `isCI` variables and `IsTestingEnvironment()`, `IsCIEnvironment()` methods from `src/Night/Framework.cs`. Updated logic to rely on `SDL_VIDEODRIVER` environment variable. diff --git a/project/epics/archive/epic_joystick_callbacks.md b/project/epics/archive/epic_joystick_callbacks.md new file mode 100644 index 00000000..007e50ba --- /dev/null +++ b/project/epics/archive/epic_joystick_callbacks.md @@ -0,0 +1,176 @@ +# Epic: Joystick and Gamepad Callbacks + +**User Story:** As a game developer using Night.Framework, I want to be able to respond to joystick and gamepad events (axis movement, button presses/releases, device connections/disconnections) via Love2D-style callbacks, so I can easily implement input handling for these devices. + +**Status:** In-Progress (Phase 4 Ready for Review) + +## Requirements + +Implement the following Love2D equivalent callback functions in `Night.Framework`: + +- `love.gamepadaxis(joystick, axis, value)`: Called when a Joystick's virtual gamepad axis is moved. +- `love.gamepadpressed(joystick, button)`: Called when a Joystick's virtual gamepad button is pressed. +- `love.gamepadreleased(joystick, button)`: Called when a Joystick's virtual gamepad button is released. +- `love.joystickadded(joystick)`: Called when a Joystick is connected. +- `love.joystickaxis(joystick, axis, value)`: Called when a joystick axis moves. +- `love.joystickhat(joystick, hat, direction)`: Called when a joystick hat direction changes. +- `love.joystickpressed(joystick, button)`: Called when a joystick button is pressed. +- `love.joystickreleased(joystick, button)`: Called when a joystick button is released. +- `love.joystickremoved(joystick)`: Called when a Joystick is disconnected. + +## Overall Design Considerations + +- **Namespace:** All Joystick related classes and enums (e.g., `Joystick`, `Joysticks`, `GamepadAxis`, `GamepadButton`, `JoystickHat`) will reside directly in the `Night` namespace. +- **SDL Subsystem Initialization:** The `SDL.InitFlags.Joystick` and `SDL.InitFlags.Gamepad` flags must be added during `SDL.Init()` in `Framework.Run.cs`. +- **Error Handling:** Robust error checking for SDL function calls and clear logging for joystick operations. +- **`Night.Game` Base Class:** The `Night.Game` base class will be updated to provide default (empty) virtual implementations for all new joystick-related `IGame` callbacks. This makes it optional for developers to implement them. + +## Phase 0: Refactor `Framework.Run.cs` (Prerequisite) + +**Goal:** Improve maintainability of `Framework.Run.cs` by separating event polling and processing logic. This file is currently very large. + +- [x] **P0.T1: Analyze `Framework.Run.cs` for Refactoring Points.** + - [x] Identify sections related to SDL event polling and the main `while (SDL.PollEvent...){}` loop. +- [x] **P0.T2: Create `Framework.Events.cs` (Partial Class).** + - [x] Define `Framework` as a `partial class` in a new file `src/Night/Framework.Events.cs`. + - [x] Move the main SDL event polling loop and event handling switch/if-else statements into a new private static method within this partial class (e.g., `private static void ProcessSdlEvents(IGame game)`). + - [x] The existing `Run` method will call this new `ProcessSdlEvents` method. +- [ ] **P0.T3: (Optional) Create `Framework.InputEventHandlers.cs` (Partial Class).** + - [ ] If further granularity is desired, specific event handling logic (e.g., for `KeyDown`, `MouseButtonDown`) could be moved into separate private static methods, potentially in another partial class file like `Framework.InputEventHandlers.cs`. These would be called from `ProcessSdlEvents`. + - *Decision for now: Start with `Framework.Events.cs` and evaluate if further splitting is necessary after joystick events are added.* +- [x] **P0.V1: Verification:** + - [x] Compile the project successfully. + - [x] Run `SampleGame` and verify existing input (keyboard/mouse) and window events function as before. + +## Phase 1: Core Joystick and Gamepad Infrastructure + +**Goal:** Establish the foundational classes and interface changes for joystick support. + +- [x] **P1.T1: Update `Night.IGame.cs`.** + - [x] Add all 9 new joystick/gamepad callback method signatures as defined in "Requirements". + - [x] Ensure parameters use types from the `Night` namespace (e.g., `Night.Joystick`, `Night.GamepadAxis`). +- [x] **P1.T2: Update `Night.Game.cs`.** + - [x] Add `virtual void` empty implementations for all 9 new joystick/gamepad callbacks from `IGame`. +- [ ] **P1.T3: Enhance `Night.Joystick.cs`.** (Partially Done - known SDL binding issues) + - [x] Ensure the class `Joystick` is in the `Night` namespace. + - [ ] Implement all placeholder methods (`GetAxisCount`, `GetButtonCount`, `GetHatCount`, `GetName`, `GetGuid`, `GetId`, `IsConnected`, `IsDown`, `IsGamepad`, `GetGamepadAxis`, `IsGamepadDown`, `SetVibration`, `IsVibrationSupported`, etc.) using appropriate SDL3-CS calls. + - [x] `GetId()` should return the SDL Joystick Instance ID. + - [x] `IsConnected()` will be managed by the `Joysticks` class. Add an internal `SetConnectedState(bool connected)` method. + - [ ] Handle potential missing SDL3-CS bindings gracefully (e.g., for mapping strings or advanced rumble features, return default/null values and log a warning). **(Known issue for `GetGamepadMappingString` and `IsVibrationSupported`)** + - [x] Add necessary private fields (e.g., `_gamepadDevicePtr` for `SDL.OpenGamepad`). + - [x] Ensure `Dispose()` correctly closes both joystick and gamepad pointers if open. +- [x] **P1.T4: Implement `Night.Joysticks.cs` (Static Class `Night.Joysticks`).** + - [x] Ensure the static class `Joysticks` is in the `Night` namespace. + - [x] Implement a static dictionary `private static readonly Dictionary ActiveJoysticks`. + - [x] Implement `public static List GetJoysticks()`. + - [x] Implement `public static int GetJoystickCount()`. + - [x] Implement `internal static Joystick? AddJoystick(uint instanceId)`. + - [x] Implement `internal static Joystick? RemoveJoystick(uint instanceId)`. + - [x] Implement `internal static Joystick? GetJoystickByInstanceId(uint instanceId)`. + - [x] Implement `internal static void ClearJoysticks()` for shutdown. +- [x] **P1.V1: Verification (Code Review & Compilation):** + - [x] All new and modified classes compile without errors. + - [x] Code adheres to project guidelines. + - [x] Namespace usage is correct (`Night` for public types). + +## Phase 2: SDL Initialization and Basic Joystick Connection/Disconnection Events + +**Goal:** Initialize SDL joystick subsystems and handle add/remove events. + +- [x] **P2.T1: Modify `Framework.Run.cs` (or `Framework.Events.cs` after P0).** + - [x] In the SDL initialization block, add `SDL.InitFlags.Joystick | SDL.InitFlags.Gamepad` to `initializedSubsystemsFlags`. + - [x] In the `finally` block of the `Run` method, add a call to `Night.Joysticks.ClearJoysticks()`. +- [x] **P2.T2: Implement `JoystickAdded` and `JoystickRemoved` Event Handling.** + - [x] In `ProcessSdlEvents` (or equivalent after P0): + - [x] Handle `SDL.EventType.JoystickAdded`: + - [x] Call `Night.Joysticks.AddJoystick(e.JDevice.Which)`. + - [x] If successful, call `game.JoystickAdded(newJoystick)`. + - [x] Handle `SDL.EventType.JoystickRemoved`: + - [x] Call `Night.Joysticks.RemoveJoystick(e.JDevice.Which)`. + - [x] If successful, call `game.JoystickRemoved(removedJoystick)`, then `removedJoystick.Dispose()`. +- [x] **P2.V1: Verification (SampleGame):** + - [x] Modify `src/SampleGame/Program.cs` (or a specific sample game class). + - [x] Override `JoystickAdded` and `JoystickRemoved` in the sample game. + - [x] Log messages to the console when a joystick is connected or disconnected. + - [x] Test by plugging and unplugging a controller. Verify `Night.Joysticks.GetJoystickCount()` and `Night.Joysticks.GetJoysticks()` reflect the changes. + +## Phase 3: Implement Raw Joystick Input Events (Buttons, Axes, Hats) + +**Goal:** Handle raw inputs from any connected joystick device. + +- [x] **P3.T1: Implement `JoystickAxis`, `JoystickPressed`, `JoystickReleased`, `JoystickHat` Event Handling.** + - [x] In `ProcessSdlEvents` (or equivalent): + - [x] Handle `SDL.EventType.JoystickAxisMotion`: + - [x] Get `Joystick` instance using `e.JAxis.Which`. + - [x] Normalize axis value from `e.JAxis.Value` (-32768 to 32767) to -1.0f to 1.0f (create a private static helper `NormalizeSdlAxisValue` in `Framework.Events.cs`). + - [x] Call `game.JoystickAxis(joystick, (int)e.JAxis.Axis, normalizedValue)`. + - [x] Handle `SDL.EventType.JoystickButtonDown`: + - [x] Get `Joystick` instance. Call `game.JoystickPressed(joystick, (int)e.JButton.Button)`. + - [x] Handle `SDL.EventType.JoystickButtonUp`: + - [x] Get `Joystick` instance. Call `game.JoystickReleased(joystick, (int)e.JButton.Button)`. + - [x] Handle `SDL.EventType.JoystickHatMotion`: + - [x] Get `Joystick` instance. Cast `e.JHat.Value` (which is `SDL.Hat` enum) to `Night.JoystickHat`. + - [x] Call `game.JoystickHat(joystick, (int)e.JHat.Hat, (Night.JoystickHat)e.JHat.Value)`. +- [x] **P3.V1: Verification (SampleGame):** + - [x] Expand `SampleGame` to override and log all raw joystick callbacks. + - [x] Test with a controller: move axes, press buttons, use D-Pad/hats. + - [x] Verify correct joystick instance, axis/button/hat indices, and values are reported. + +## Phase 4: Implement Gamepad-Specific Input Events (Virtual Axes/Buttons) + +**Goal:** Handle inputs from joysticks that are recognized as standard gamepads, using SDL's gamepad API. + +**Status:** P4.V1 Complete (2025-06-15) + +- [x] **P4.T1: Implement `GamepadAxis`, `GamepadPressed`, `GamepadReleased` Event Handling.** + - [x] In `ProcessSdlEvents` (or equivalent): + - [x] Handle `SDL.EventType.GamepadAxisMotion`: + - [x] Get `Joystick` instance using `e.GAxis.Which`. Check `joystick.IsGamepad()`. + - [x] Normalize axis value from `e.GAxis.Value`. + - [x] Map `e.GAxis.Axis` (an `SDL.GamepadAxis`) to `Night.GamepadAxis` (create `private static Night.GamepadAxis MapSdlGamepadAxisToNight(SDL.GamepadAxis sdlAxis)` helper). + - [x] Call `game.GamepadAxis(joystick, nightAxis, normalizedValue)`. + - [x] Handle `SDL.EventType.GamepadButtonDown`: + - [x] Get `Joystick` instance. Check `joystick.IsGamepad()`. + - [x] Map `e.GButton.Button` (an `SDL.GamepadButton`) to `Night.GamepadButton` (create `private static Night.GamepadButton MapSdlGamepadButtonToNight(SDL.GamepadButton sdlButton)` helper). + - [x] Call `game.GamepadPressed(joystick, nightButton)`. + - [x] Handle `SDL.EventType.GamepadButtonUp`: + - [x] Get `Joystick` instance. Check `joystick.IsGamepad()`. + - [x] Map `e.GButton.Button` to `Night.GamepadButton`. + - [x] Call `game.GamepadReleased(joystick, nightButton)`. +- [x] **P4.V1: Verification (SampleGame - Full Controller Test):** + - [x] Expand `SampleGame` to specifically test gamepad events. + - [x] Use a standard gamepad (e.g., Xbox controller). + - [x] Verify that both raw joystick events AND specific gamepad events are triggered appropriately. + - [x] Log output from `joystick.GetGamepadAxis()`, `joystick.IsGamepadDown()` in the sample game's `Update` loop to cross-verify with event callbacks. + - [x] Test basic player movement or actions in `SampleGame` controlled by gamepad inputs. + +## Phase 5: Documentation and Final Review + +- [x] **P5.T1: Add XML Documentation.** + - [x] Ensure all new public APIs in `IGame.cs`, `Game.cs`, `Joystick.cs`, `Joysticks.cs` (and related enums) have comprehensive XML documentation comments. +- [x] **P5.T2: Update `project/README.md` or relevant docs.** + - [x] If necessary, add a section on Joystick and Gamepad usage. +- [x] **P5.T3: Code Cleanup and Final Review.** + - [x] Review all changes for adherence to `guidelines.md`. + - [x] Check for any remaining TODOs or potential issues. + - [x] Ensure logging is appropriate (not too verbose for release, but helpful for debugging). + +## Notes & Decisions (To be updated during development) + +- The refactoring of `Framework.Run.cs` (Phase 0) is crucial for managing complexity. +- The `Night.Joystick` class will attempt to open the device as an `SDL_Gamepad` if `SDL_IsGamepad` returns true. This allows it to serve data for both raw joystick functions and mapped gamepad functions. +- Mapping SDL event data (raw axis values, SDL-specific enums) to the Love2D-style API (normalized floats, Night-specific enums) will be handled by helper methods within `Framework.Run.cs` (or its partial classes). +- If specific SDL3-CS bindings are missing (e.g., for `SDL_GetJoystickMappingForID` or `SDL_JoystickHasRumble`), the corresponding `Night.Joystick` methods will return default "not supported" values (e.g., `null` for mapping string, `false` for rumble support) and log a warning. **(Current known issue)** + +- Addressing 14 build warnings (StyleCop & IDE) as of 2025-06-15 to maintain code quality. + +## Dependencies + +- SDL3-CS bindings for joystick and gamepad events. + +## Risks + +- Complexity in mapping SDL event details to Love2D-style callback parameters. +- Ensuring correct `Night.Joystick` instance management during hot-plugging. +- Potential missing or differently named functions in the specific version of SDL3-CS being used. **(Currently impacting `Joystick.cs`)** +- The large size of `Framework.Run.cs` might make refactoring and adding new event logic error-prone if not handled carefully in phases. diff --git a/project/epics/archive/epic_system_openurl.md b/project/epics/archive/epic_system_openurl.md new file mode 100644 index 00000000..3eaac065 --- /dev/null +++ b/project/epics/archive/epic_system_openurl.md @@ -0,0 +1,33 @@ +# Epic: Implement System.OpenURL API + +**User Story:** As a developer, I want to be able to open a URL using the system's default browser or file explorer so that I can direct users to web pages or local files. + +**Task:** Implement the `system.openURL` API in `src/Night/System/System.cs`. + +**Synopsis:** +`success = Night.System.OpenURL(url)` + +**Arguments:** +- `string url`: The URL to open. Must be formatted as a proper URL. + +**Returns:** +- `boolean success`: Whether the URL was opened successfully. + +**Notes:** +- Passing `file://` scheme in Android 7.0 (Nougat) and later always results in failure. Prior to 11.2, this would crash LÖVE instead of returning false. (This note is from LÖVE, ensure it's relevant or adapted for Night). + +**Acceptance Criteria:** +- A public static method `OpenURL(string url)` is added to the `Night.System` class in `src/Night/System/System.cs`. +- The method takes a string `url` as input. +- The method returns `true` if the URL was opened successfully, `false` otherwise. +- The method uses `SDL.OpenURL()` internally. +- The method includes XML documentation matching the LÖVE API specification. +- The implementation adheres to `project/guidelines.md` and `project/PRD.md`. + +**Status:** Review +**Assigned:** Roo +**Log:** +- 2025-06-16: Task created. +- 2025-06-16: Status updated to In-Progress. Reviewed project/PRD.md and project/guidelines.md. +- 2025-06-16: Implemented `Night.System.OpenURL(string url)` in `src/Night/System/System.cs`. The method uses `SDL.OpenURL(url)` which directly returns a boolean indicating success. XML documentation added. +- 2025-06-16: Status updated to Review. \ No newline at end of file diff --git a/project/epics/archive/filesystem.md b/project/epics/archive/filesystem.md new file mode 100644 index 00000000..ddc1ae04 --- /dev/null +++ b/project/epics/archive/filesystem.md @@ -0,0 +1,71 @@ +# LÖVE 2D Filesystem API Implementation Tasks + +## Types + +- [ ] `DroppedFile` - Represents a file dropped from the window. (0.10.0) + +- [ ] `File` - Represents a file on the filesystem. + +- [ ] `FileData` - Data representing the contents of a file. + +## Functions + +- [x] `love.filesystem.append(filename, data, size)` - Append data to an existing file. (0.9.0) + +- [x] `love.filesystem.createDirectory(path)` - Creates a directory. (0.9.0) + +- [x] `love.filesystem.getAppdataDirectory()` - Returns the application data directory. + +- [In-Progress] `love.filesystem.getDirectoryItems(path)` - Returns all the files and subdirectories in the directory. (0.9.0) + +- [ ] `love.filesystem.getIdentity()` - Gets the write directory name for your game. (0.9.0) + +- [ ] `love.filesystem.getInfo(filepath, filetype, filter)` - Gets information about the specified file or directory. (11.0) + +- [ ] `love.filesystem.getRealDirectory(filepath)` - Gets the absolute path of the directory containing a filepath. (0.9.2) + +- [ ] `love.filesystem.getRequirePath()` - Gets the filesystem paths that will be searched when `require` is called. (0.10.0) + +- [ ] `love.filesystem.getSaveDirectory()` - Gets the full path to the designated save directory. (0.5.0) + +- [ ] `love.filesystem.getSource()` - Returns the full path to the .love file or directory. (0.9.0) + +- [ ] `love.filesystem.getSourceBaseDirectory()` - Returns the full path to the directory containing the .love file. (0.9.0) + +- [ ] `love.filesystem.getUserDirectory()` - Returns the path of the user's directory. + +- [ ] `love.filesystem.getWorkingDirectory()` - Gets the current working directory. (0.5.0) + +- [ ] `love.filesystem.init()` - Initializes love.filesystem (internal use). + +- [ ] `love.filesystem.isFused()` - Gets whether the game is in fused mode or not. (0.9.0) + +- [ ] `love.filesystem.lines(filename, ...)` - Iterate over the lines in a file. (0.5.0) + +- [ ] `love.filesystem.load(filepath)` - Loads a Lua file (but does not run it). (0.5.0) + +- [ ] `love.filesystem.mount(archive, mountpoint, appendtopath)` - Mounts a zip file or folder in the game's save directory for reading. (0.9.0) + +- [ ] `love.filesystem.newFile(filename, mode)` - Creates a new File object. + +- [ ] `love.filesystem.newFileData(contents, name, decoder)` - Creates a new FileData object. (0.7.0) + +- [ ] `love.filesystem.read(filename, size)` - Read the contents of a file. + +- [ ] `love.filesystem.remove(filepath)` - Removes a file (or directory). + +- [ ] `love.filesystem.setIdentity(name, appendtouc)` - Sets the write directory for your game. + +- [ ] `love.filesystem.setRequirePath(paths)` - Sets the filesystem paths for `require`. (0.10.0) + +- [ ] `love.filesystem.setSource(path)` - Sets the source of the game (internal use). + +- [ ] `love.filesystem.unmount(archive)` - Unmounts a zip file or folder. (0.9.0) + +- [ ] `love.filesystem.write(filename, data, size)` - Write data to a file. + +## Enums + +- [ ] `FileMode` - The different modes you can open a File in. + +- [ ] `FileType` - The type of a file. (11.0) diff --git a/project/epics/archive/logger-tasks.md b/project/epics/archive/logger-tasks.md new file mode 100644 index 00000000..b48686bb --- /dev/null +++ b/project/epics/archive/logger-tasks.md @@ -0,0 +1,383 @@ +# Epic: Implement Flexible Logging Module for Night Library + +**User Story:** As a Night Engine developer, I want a flexible and extensible logging module so that I can easily output diagnostic messages to various configurable destinations (file, in-game console, memory, system console) with control over log levels and sink activation, replacing the current `System.Console.WriteLine` usage. + +**Status:** In-Progress + +**Priority:** High + +**Assignee:** Night Agent (AI) + +**Reporter:** User + +**Estimated Effort:** TBD + +**Start Date:** YYYY-MM-DD +**Due Date:** YYYY-MM-DD + +--- + +## Background + +The current diagnostic output in the `Night` library relies on `System.Console.WriteLine`. This is inflexible for a game engine requiring configurable log outputs, varying log levels, and the ability to direct logs to different sinks like files or an in-game display. This epic aims to create a robust logging system internal to the `Night` namespace. + +## Requirements + +- **Core Interfaces:** Define `ILogger`, `LogLevel` enum, `ILogSink`, and `LogEntry` record/class. +- **Log Levels:** Support Trace, Debug, Information, Warning, Error, Fatal. +- **`ILogger` Methods:** Include methods like `Debug(string message)`, `Error(string message, Exception exception)`, and `IsEnabled(LogLevel level)`. +- **`LogEntry` Structure:** Must contain Timestamp (UTC), LogLevel, formatted Message, optional Exception, and CategoryName/Source string. +- **`LogManager`:** Static class for obtaining `ILogger` instances (e.g., `LogManager.GetLogger("Renderer")`), managing active `ILogSink`s, global configuration (e.g., minimum log level), and enabling/disabling sinks. +- **Sink Implementations:** + - `FileSink`: Writes to a configurable file path; opt-in. + - `InGameConsoleSink`: Buffers messages for in-game UI display. + - `MemorySink`: Stores recent log entries in memory. + - `SystemConsoleSink`: Writes to `System.Console`; *must be opt-in and not active by default*. +- **Thread Safety:** Ensure logging operations are thread-safe. +- **Error Handling:** Failures in one sink should not affect others or crash the application. +- **Extensibility:** Design should allow adding new custom sinks. +- **Namespace:** All logging components should be within the `Night` namespace and initially internal to the library. +- **Configuration:** Provide clear mechanisms to enable/disable and configure sinks, especially `FileSink` (path) and `SystemConsoleSink`. +- **Internal Documentation:** Add /// comments for all public types and methods within the logging module. + +## Acceptance Criteria + +1. All `System.Console.WriteLine` calls within the `Night` library (specifically `Framework.cs` as a starting point) are replaced with the new logging system. +2. Logging to `System.Console` is disabled by default and can be explicitly enabled via configuration. +3. Logging to a file can be enabled and the output path configured. +4. The `MemorySink` correctly captures recent log entries. +5. The `InGameConsoleSink` provides a mechanism to retrieve buffered log messages. +6. Log filtering based on global minimum log level works as expected. +7. Categorized loggers (e.g., `LogManager.GetLogger("Framework")`) correctly identify the source in `LogEntry`. +8. The system is thread-safe under typical engine usage patterns. +9. Basic error handling for sink operations is implemented and observable (e.g., logs an internal error if a sink fails). +10. All new logging code is documented with /// comments. + +--- + +## Tasks + +### Phase 1: Core Logging Infrastructure + +- [x] **Task 1.1:** Define `LogLevel` enum (Trace, Debug, Information, Warning, Error, Fatal). + - *File(s):* `Night/Log/LogLevel.cs` +- [x] **Task 1.2:** Define `LogEntry` record/class. + - *Details:* Include `DateTime TimestampUtc`, `LogLevel Level`, `string Message`, `Exception? Exception`, `string CategoryName`. + - *File(s):* `Night/Log/LogEntry.cs` +- [x] **Task 1.3:** Define `ILogSink` interface. + - *Methods:* `void Write(LogEntry entry);` + - *File(s):* `Night/Log/ILogSink.cs` +- [x] **Task 1.4:** Define `ILogger` interface. + - *Methods:* `void Log(LogLevel level, string message, Exception? exception = null);`, `bool IsEnabled(LogLevel level);` + - *Convenience Methods:* `Trace(string message)`, `Debug(string message)`, `Info(string message)`, `Warn(string message)`, `Error(string message, Exception? exception = null)`, `Fatal(string message, Exception? exception = null)`. + - *File(s):* `Night/Log/ILogger.cs` +- [x] **Task 1.5:** Implement `Logger` class (implements `ILogger`). + - *Details:* Takes `LogManager` and `categoryName` in constructor. Forwards logs to `LogManager`. + - *File(s):* `Night/Log/Logger.cs` +- [x] **Task 1.6:** Write manual tests/verification steps for Core Logging Infrastructure. + +#### Manual Verification Steps for Phase 1 + +1. **Compilation Check:** Ensure the project compiles successfully after creating all files in this phase (`Night/Log/LogLevel.cs`, `Night/Log/LogEntry.cs`, `Night/Log/ILogSink.cs`, `Night/Log/ILogger.cs`, `Night/Log/Logger.cs`). +2. **API Review:** + * Open `LogLevel.cs` and verify all enum values (Trace, Debug, Information, Warning, Error, Fatal) are present. + * Open `LogEntry.cs` and verify all specified properties (`DateTime TimestampUtc`, `LogLevel Level`, `string Message`, `Exception? Exception`, `string CategoryName`) are present with correct types. + * Open `ILogSink.cs` and verify the `Write(LogEntry entry)` method signature. + * Open `ILogger.cs` and verify the `Log(LogLevel level, string message, Exception? exception = null)` and `IsEnabled(LogLevel level)` method signatures, as well as all convenience method signatures (`Trace`, `Debug`, `Info`, `Warn`, `Error`, `Fatal`). + * Open `Logger.cs` and verify it implements `ILogger` and has a constructor accepting `LogManager` (or a reference to its dispatching mechanism) and a `categoryName`. +3. **(Conceptual) Instantiation Check:** Mentally (or with a temporary, isolated code snippet if feasible, though `LogManager` is not yet built), confirm that `Logger` could be instantiated if a `LogManager` instance/surrogate were available. + +### Phase 2: LogManager and Basic Sinks + +- [x] **Task 2.1:** Implement `LogManager` static class. + - *Responsibilities:* Manage list of `ILogSink`s, provide `GetLogger(string categoryName)`, global minimum log level (`MinLevel`), methods `AddSink(ILogSink sink)`, `RemoveSink(ILogSink sink)`, `ClearSinks()`. + - *Details:* `GetLogger` should return an `ILogger` instance (e.g., `Logger` class from Task 1.5). Internal method to dispatch `LogEntry` to all active sinks if `entry.Level >= MinLevel`. + - *File(s):* `Night/Log/LogManager.cs` +- [x] **Task 2.2:** Implement `SystemConsoleSink` (implements `ILogSink`). + - *Details:* Writes formatted `LogEntry` to `System.Console`. Ensure it can be added/removed via `LogManager`. *Crucially, this sink should not be added by default.* + - *File(s):* `Night/Log/Sinks/SystemConsoleSink.cs` +- [x] **Task 2.3:** Implement `MemorySink` (implements `ILogSink`). + - *Details:* Stores a configurable number of recent `LogEntry` objects in a thread-safe collection (e.g., `ConcurrentQueue` with size limit). Provides a method to retrieve buffered entries. + - *File(s):* `Night/Log/Sinks/MemorySink.cs` +- [ ] **Task 2.4:** Write manual tests/verification steps for LogManager and Basic Sinks. + +#### Manual Verification Steps for Phase 2 + +*Prerequisites: A simple test harness or a temporary console application where you can call `LogManager` methods.* + +1. **`LogManager.GetLogger()`:** + * Call `LogManager.GetLogger("TestCategory1")`. Verify it returns a non-null `ILogger` instance. + * Call `LogManager.GetLogger("TestCategory1")` again. Verify it returns the same instance (or an equivalent one for the same category). + * Call `LogManager.GetLogger("TestCategory2")`. Verify it returns a different instance (or an equivalent one for a new category). +2. **`SystemConsoleSink` (Opt-In Behavior):** + * Without adding any sinks, use an `ILogger` instance to log a message (e.g., `logger.Info("Test message")`). Verify *nothing* is printed to the console. + * Create an instance of `SystemConsoleSink`. + * Call `LogManager.AddSink(systemConsoleSinkInstance)`. + * Log a message (e.g., `logger.Info("Hello Console!")`). Verify the message appears on `System.Console`, formatted appropriately (e.g., showing timestamp, level, category, message). + * Call `LogManager.RemoveSink(systemConsoleSinkInstance)`. + * Log another message. Verify it no longer appears on the console. +3. **`MemorySink`:** + * Create an instance of `MemorySink` (e.g., `var memorySink = new MemorySink(capacity: 5);`). + * Call `LogManager.AddSink(memorySink)`. + * Log several messages (e.g., 6 messages: "Msg1" to "Msg6"). + * Call the method on `memorySink` to retrieve buffered entries (e.g., `memorySink.GetEntries()`). + * Verify the retrieved collection contains the last 5 messages ("Msg2" to "Msg6"). + * Verify each `LogEntry` in the buffer has correct Timestamp, Level, Message, and CategoryName. +4. **Global Minimum Log Level (`LogManager.MinLevel`):** + * Ensure `SystemConsoleSink` is added. + * Set `LogManager.MinLevel = LogLevel.Warning;`. + * Log messages: `logger.Debug("Debug A")`, `logger.Info("Info A")`, `logger.Warn("Warning A")`. + * Verify only "Warning A" (and any higher level messages) appear on the console. + * Set `LogManager.MinLevel = LogLevel.Trace;` (or the lowest level). + * Log `logger.Debug("Debug B")`. Verify "Debug B" now appears. +5. **`ClearSinks()`:** + * Add `SystemConsoleSink` and `MemorySink`. Log a message and verify output/capture. + * Call `LogManager.ClearSinks()`. + * Log another message. Verify no output to console and no new entries in the `MemorySink` (its existing entries might still be there until new ones push them out or it's cleared/recreated). + +### Phase 3: Advanced Sinks + +- [x] **Task 3.1:** Implement `FileSink` (implements `ILogSink`). + - *Details:* Writes formatted `LogEntry` to a specified file. Path should be configurable. Handles file I/O safely. Ensure it can be added/removed. + - *File(s):* `Night/Log/Sinks/FileSink.cs` +- [x] **Task 3.2:** Implement `InGameConsoleSink` (implements `ILogSink`). + - *Details:* Buffers `LogEntry` objects in a thread-safe collection. Provides a way for a UI to access these messages (e.g., a public method to retrieve the buffer or an event). + - *File(s):* `Night/Log/Sinks/InGameConsoleSink.cs` +- [x] **Task 3.3:** Write manual tests/verification steps for Advanced Sinks. + +#### Manual Verification Steps for Phase 3 + +*Prerequisites: Test harness from Phase 2. Ensure `SystemConsoleSink` can be optionally active for comparison if needed.* + +1. **`FileSink`:** + * Create an instance of `FileSink`, providing a valid, writable file path (e.g., `"./test_log.txt"`). + * Call `LogManager.AddSink(fileSinkInstance)`. + * Log several messages with different levels and categories (e.g., `LogManager.GetLogger("FileTest").Info("Log to file");`, `LogManager.GetLogger("FileTest").Error("File error!", new System.Exception("dummy exception"));`). + * Open `test_log.txt`. Verify: + * The file was created. + * All logged messages are present. + * Each log entry is formatted correctly (timestamp, level, category, message, exception details if any). + * Log more messages. Verify they are appended to the file. + * (Optional) Test with an invalid/unwritable path if error handling for sink construction/write is part of this phase's scope. Observe behavior (e.g., internal error logged if a fallback exists, no crash). + * Call `LogManager.RemoveSink(fileSinkInstance)`. Log a message. Verify it's not written to `test_log.txt`. +2. **`InGameConsoleSink`:** + * Create an instance of `InGameConsoleSink` (e.g., `var gameConsoleSink = new InGameConsoleSink(capacity: 10);`). + * Call `LogManager.AddSink(gameConsoleSink)`. + * Log several messages (e.g., 12 messages). + * Call the method on `gameConsoleSink` designed to retrieve/expose its buffered `LogEntry` objects. + * Verify the collection contains the last 10 messages. + * Verify the format and content of these `LogEntry` objects (Timestamp, Level, Message, Category). + * If an event-based notification is implemented for new messages, try to (conceptually or with a simple handler) verify it fires. + +### Phase 4: Thread Safety, Error Handling, and Configuration + +- [x] **Task 4.1:** Ensure thread safety for `LogManager` and all sink operations. + - *Details:* Review and use appropriate synchronization primitives (`lock`, `ConcurrentDictionary`, `ConcurrentQueue`, etc.) where necessary. +- [x] **Task 4.2:** Implement error handling within sinks. + - *Details:* A failure in one sink (e.g., file I/O error in `FileSink`) should not stop other sinks or crash the application. Consider logging such errors to `System.Diagnostics.Trace` or a fallback mechanism. +- [ ] **Task 4.3:** Design and implement sink configuration mechanism. + - *Details:* Determine how sinks like `FileSink` (path) and `SystemConsoleSink` (enable/disable) will be configured. This could be through methods on `LogManager` or by reading from `ConfigurationManager` if appropriate for internal use. + - *Example:* `LogManager.EnableSystemConsoleSink()`, `LogManager.ConfigureFileSink(string path, LogLevel minLevelForFile)`. +- [x] **Task 4.4:** Write manual tests/verification steps for Thread Safety, Error Handling, and Configuration. + +#### Manual Verification Steps for Phase 4 + +*Prerequisites: A simple test harness or a temporary console application where you can call `LogManager` methods and simulate multi-threading.* + +1. **Thread Safety (`LogManager` and Sinks):** + * Initialize `LogManager`. + * Call `LogManager.EnableSystemConsoleSink(true)`. + * Call `LogManager.ConfigureFileSink("thread_test_log.txt", LogLevel.Trace)`. + * Create an instance of `MemorySink` (e.g., `var memorySink = new MemorySink(capacity: 2000);`) and add it using `LogManager.AddSink(memorySink)`. + * Spawn several threads (e.g., 5-10 threads). + * In each thread: + * Get a logger instance: `var logger = LogManager.GetLogger($"Thread-{Thread.CurrentThread.ManagedThreadId}");` + * Log a significant number of messages (e.g., 100-200 messages) in a loop with varying log levels: `logger.Info($"Message {i} from thread.");`, `logger.Debug($"Debug message {i}");` + * Wait for all threads to complete. + * *Observe & Verify:* + * **Application Stability:** The application completes without deadlocks or exceptions related to concurrent access. + * **Console Output (`SystemConsoleSink`):** + * Messages from different threads should appear. Some interleaving of *complete log messages* is expected and acceptable. + * Individual log messages should not be garbled (e.g., parts of one message mixed with parts of another within the same line). + * **File Output (`FileSink` - `thread_test_log.txt`):** + * Open `thread_test_log.txt`. + * Verify that the file is not corrupted. + * Verify that messages from all threads are present and correctly formatted. + * The total number of messages should roughly correspond to (num_threads * num_messages_per_thread) that meet the `LogLevel.Trace` criteria. + * **`MemorySink` Output:** + * Retrieve entries: `var entries = memorySink.GetEntries();` + * Verify that the number of entries is as expected (up to its capacity). + * Check a sample of entries for consistency (correct timestamp, level, category, message). + * Clean up: `LogManager.DisableFileSink(); LogManager.ClearSinks(); File.Delete("thread_test_log.txt");` + +2. **Sink Error Handling (Focus on `FileSink`):** + * Initialize `LogManager`. + * Call `LogManager.EnableSystemConsoleSink(true)`. + * Configure `FileSink` to a valid path: `LogManager.ConfigureFileSink("error_test_log.txt", LogLevel.Info);` + * Log a message: `LogManager.GetLogger("ErrorTest").Info("Initial message - should go to console and file.");` + * Verify the message appears on the console and in `error_test_log.txt`. + * **Simulate `FileSink` Write Error:** + * Make `error_test_log.txt` read-only (e.g., using file system permissions or `File.SetAttributes("error_test_log.txt", FileAttributes.ReadOnly)`). + * Alternatively, if testing `FileSink`'s initialization error: try `LogManager.ConfigureFileSink("Z:\\non_existent_drive\\error_init_test.txt");` (assuming Z: is not a valid writable drive). + * Log another message: `LogManager.GetLogger("ErrorTest").Warn("Second message - should go to console, FileSink might fail.");` + * *Verify:* + * The second message *still appears* in the `SystemConsoleSink` output. + * The application *does not crash*. + * Check `System.Diagnostics.Trace` output (e.g., in IDE's debug output window or a configured trace listener). An error message indicating the `FileSink` failure (e.g., "Access to the path... is denied" or "Could not find a part of the path...") should be present. + * **Restore `FileSink` Operation:** + * If read-only was set: `File.SetAttributes("error_test_log.txt", FileAttributes.Normal);` + * If a bad path was used for init, reconfigure to a valid one: `LogManager.ConfigureFileSink("error_test_log_restored.txt", LogLevel.Info);` + * Log a third message: `LogManager.GetLogger("ErrorTest").Info("Third message - should go to console and new/restored file.");` + * Verify the third message appears on the console and in the (newly configured or now writable) file log. + * Clean up: `LogManager.DisableFileSink(); LogManager.EnableSystemConsoleSink(false); File.Delete("error_test_log.txt"); File.Delete("error_test_log_restored.txt");` (handle potential delete errors if files didn't exist). + +3. **Sink Configuration Mechanism:** + * **`SystemConsoleSink` Enable/Disable:** + * `LogManager.EnableSystemConsoleSink(true);` + * `LogManager.GetLogger("ConsoleTest").Info("Message 1: Console On");` -> Verify console output. + * `LogManager.IsSystemConsoleSinkEnabled()` -> Verify returns `true`. + * `LogManager.EnableSystemConsoleSink(false);` + * `LogManager.GetLogger("ConsoleTest").Info("Message 2: Console Off");` -> Verify *no* console output for this message. + * `LogManager.IsSystemConsoleSinkEnabled()` -> Verify returns `false`. + * `LogManager.EnableSystemConsoleSink(true);` + * `LogManager.GetLogger("ConsoleTest").Info("Message 3: Console On Again");` -> Verify console output. + * **`FileSink` Configuration (Path, Level) & Disable:** + * `LogManager.ConfigureFileSink("file_config_A.log", LogLevel.Information);` + * `var logger = LogManager.GetLogger("FileConfigTest");` + * `logger.Debug("File A - Debug (should not appear)");` + * `logger.Info("File A - Info (should appear)");` + * `logger.Warn("File A - Warn (should appear)");` + * Verify `file_config_A.log` contains only Info and Warn messages. + * `LogManager.ConfigureFileSink("file_config_B.log", LogLevel.Debug);` (This implicitly disables/replaces the sink for `file_config_A.log`) + * `logger.Debug("File B - Debug (should appear)");` + * `logger.Info("File B - Info (should appear)");` + * Verify `file_config_B.log` contains Debug and Info messages. Verify `file_config_A.log` is no longer being written to. + * `LogManager.DisableFileSink();` + * `logger.Error("File B - Error (should not appear in file after disable)");` + * Verify `file_config_B.log` does not contain the error message. + * Clean up: `File.Delete("file_config_A.log"); File.Delete("file_config_B.log");` + * **Global `LogManager.MinLevel` vs. `FileSink` Specific Level:** + * `LogManager.EnableSystemConsoleSink(true);` // For easy observation + * `LogManager.MinLevel = LogLevel.Warning;` + * `LogManager.ConfigureFileSink("file_level_test.log", LogLevel.Debug);` // FileSink wants Debug and up + * `var levelLogger = LogManager.GetLogger("LevelTest");` + * `levelLogger.Debug("Global Warn, File Debug - Debug Msg (File: No, Global: No)");` + * `levelLogger.Info("Global Warn, File Debug - Info Msg (File: No, Global: No)");` + * `levelLogger.Warn("Global Warn, File Debug - Warn Msg (File: Yes, Global: Yes)");` + * `levelLogger.Error("Global Warn, File Debug - Error Msg (File: Yes, Global: Yes)");` + * Verify `file_level_test.log` contains only Warn and Error messages (because global `MinLevel` is `Warning`). + * Verify console also only shows Warn and Error messages. + * `LogManager.MinLevel = LogLevel.Trace;` + * `LogManager.ConfigureFileSink("file_level_test2.log", LogLevel.Information);` // FileSink wants Info and up + * `levelLogger.Debug("Global Trace, File Info - Debug Msg (File: No, Global: Yes)");` + * `levelLogger.Info("Global Trace, File Info - Info Msg (File: Yes, Global: Yes)");` + * `levelLogger.Warn("Global Trace, File Info - Warn Msg (File: Yes, Global: Yes)");` + * Verify `file_level_test2.log` contains Info and Warn messages (FileSink filters Debug, global allows all). + * Verify console shows Debug, Info, and Warn messages. + * Clean up: `LogManager.DisableFileSink(); LogManager.EnableSystemConsoleSink(false); File.Delete("file_level_test.log"); File.Delete("file_level_test2.log");` + +### Phase 5: Integration and Refactoring + +- [In-Progress] **Task 5.1:** Integrate `LogManager` into `Night.Framework`. + - *Details:* Initialize `LogManager` (e.g., set default `MinLevel`). Potentially expose a way for the game to add its own sinks if ever needed in the future (though initially internal). +- [In-Progress] **Task 5.2:** Refactor `Framework.cs` to use the new logging system. + - *Details:* Replace all `Console.WriteLine` calls with `LogManager.GetLogger("Framework").Info(...)` or other appropriate levels/methods. + - *Note:* Exclude "Night Engine: v...", "SDL: v...", "Platform: ...", "Framework: ..." from conversion; these should remain `System.Console.WriteLine`. +- [ ] **Task 5.3:** (TODO, DO LATER) Identify and refactor other `Console.WriteLine` calls in the `Night` library. +- [In-Progress] **Task 5.4:** Write manual tests/verification steps for Integration and Refactoring. + +#### Manual Verification Steps for Phase 5 + +*Prerequisites: The `Night` engine/framework should be runnable, at least to the point where `Framework.cs` executes its initialization and main loop logic.* + +1. **Configuration for Test:** + * Ensure your `SampleGame` (or test harness) configures the `LogManager` appropriately. For thorough testing of `Framework.cs` logs: + * Enable `SystemConsoleSink` (e.g., `LogManager.EnableSystemConsoleSink(true);`). + * Set global minimum log level to `LogLevel.Trace` or `LogLevel.Debug` (e.g., `LogManager.MinLevel = LogLevel.Trace;`). This will ensure all messages from `Framework.cs` are captured. + * (Optional) Enable `FileSink` to a known file path for persistent log checking (e.g., `LogManager.ConfigureFileSink("framework_test.log", LogLevel.Trace);`). +2. **Run the Application:** Execute `SampleGame` or a simple test case that uses `Night.Framework.Run()`. +3. **Observe Console Output - Excluded Lines:** + * Verify that the following lines are printed directly to the console, **without** logger formatting (i.e., no timestamp, level, or category prefix): + * `Night Engine: v...` (with the correct version) + * `SDL: v...` (with the correct version) + * `Platform: ...` (with correct platform details) + * `Framework: ...` (with correct framework description) +4. **Observe Logs from `Framework.cs` (Logger Formatted):** + * Check the console output (and file log, if configured) for messages originating from `Framework.cs`. + * Verify that all messages *other than the excluded lines above* are now formatted by the logging system. This means they should include: + * Timestamp (UTC) + * Log Level (e.g., INF, DBG, ERR, FTL) + * Category Name (should be "Framework") + * The log message itself. + * **Examples of messages to look for (now logger-formatted):** + * "Testing environment detected. Setting SDL video driver to 'dummy'." (if applicable) + * "Global isSdlInitialized is false. Attempting SDL.Init()." + * "SDL_Init failed: [SDL Error Message]" (if an error is forced/occurs) + * "SDL.Init() successful." + * "IsInputInitialized set to [true/false]." + * "Calling Window.SetMode with Width=[w], Height=[h], Flags=[f]" + * "Window.SetMode returned [true/false]." + * "Window title set to '[Actual Title]'." + * "Proceeding to game.Load()..." + * "game.Load() completed." + * "Starting main loop." + * "Main loop ended." + * Messages from `HandleGameException` and `DefaultErrorHandler` (e.g., "HandleGameException: Error: [Message]", "--- Night Engine: Default Error Handler ---"). +5. **Verify Log Levels and Categories:** + * Ensure the logged messages from `Framework.cs` (excluding the direct `Console.WriteLine` calls) use appropriate log levels: + * General operational information: `Info` + * Detailed diagnostic information: `Debug` + * Recoverable errors/warnings: `Warn` or `Error` + * Critical/unrecoverable errors: `Error` or `Fatal` + * Confirm the `CategoryName` for these logs is consistently "Framework". +6. **Test Specific Scenarios (Error Handling):** + * If feasible, try to induce an error that would be caught by `HandleGameException` or `DefaultErrorHandler` in `Framework.cs`. + * Example: Throw an exception within `game.Load()` or `game.Update()` in your test game. + * Verify that the error messages are logged by the "Framework" logger with an appropriate error level (e.g., `Error`, `Fatal`) and include exception details. + * Verify that the `DefaultErrorHandler`'s own diagnostic messages (e.g., "Window or Graphics not initialized", "Failed to set window mode") are also logged via the "Framework" logger. +7. **(If Task 5.3 was done) Verify Other Refactored Areas:** (This step remains for future completion of 5.3) If other parts of the `Night` library were refactored, run tests or application parts that exercise those areas and check for expected log outputs. +8. **Cleanup (if `FileSink` was used):** Delete any log files created during the test (e.g., `framework_test.log`). + +### Phase 6: Documentation and Review + +- [ ] **Task 6.1:** Add /// XML documentation comments to all new public types and methods in the logging module. +- [ ] **Task 6.2:** Perform a self-review of the implemented logging system against all requirements and acceptance criteria. +- [ ] **Task 6.3:** Prepare the changes for user review. +- [ ] **Task 6.4:** Write manual tests/verification steps for Documentation and Review. + +#### Manual Verification Steps for Phase 6 + +1. **XML Documentation Review (IDE / Generated File):** + * In your C# IDE (e.g., Visual Studio, Rider), hover over class names, method names, properties, and enum members within the `Night.Log` namespace (e.g., `LogManager`, `ILogger.Info`, `LogEntry.TimestampUtc`, `LogLevel.Debug`, `FileSink`). + * Verify that descriptive /// XML comments appear as tooltips, explaining their purpose, parameters, and return values where applicable. + * If your build process generates an XML documentation file, locate it and open it. Browse through the documentation for the logging module components and check for completeness and correctness. +2. **Code Review (Self-Review):** + * Re-read the "Requirements" and "Acceptance Criteria" sections of this epic document (`logger-tasks.md`). + * Go through each implemented file in the `Night.Log` namespace (`LogLevel.cs`, `LogEntry.cs`, `ILogSink.cs`, `ILogger.cs`, `Logger.cs`, `LogManager.cs`, and all sink implementations). + * For each requirement and acceptance criterion, confirm that the implemented code meets it. For example: + * Is `SystemConsoleSink` truly opt-in and not active by default? + * Can `FileSink` path be configured? + * Is `LogManager.MinLevel` respected? + * Is `LogEntry.CategoryName` populated correctly? +3. **Final Functionality Check (Spot Check):** + * Perform a small subset of the manual verification steps from Phases 2-5 to ensure key functionalities are still working as expected after any final tweaks or documentation-related code changes. + * For example: + * Enable `SystemConsoleSink`, log a message, verify output. + * Enable `FileSink`, log a message, verify file output. + * Test `LogManager.MinLevel` filtering briefly. + * Run a simple `Night.Framework` application and check if `Framework.cs` logs are appearing. +4. **Epic Document Review:** + * Read through the "Notes & Decisions" section. Ensure all documented decisions were followed or updated. + * Read through the "Questions for User" section. Ensure any questions raised during development were addressed or are noted as pending. + * Update the "Status" of tasks and the main epic status (e.g., to "Ready for Review" or similar). + * Fill in "Actual Effort" or "Completion Date" if applicable. + +--- + +## Notes & Decisions + +*(This section will be updated as development progresses)* + +- Initial decision: Logging module will reside entirely within the `Night` namespace and its components marked as `internal` where appropriate if they are not meant to be part of the engine's public API yet. +- Configuration for opt-in sinks (`SystemConsoleSink`, `FileSink`) needs careful design to be user-friendly for developers using the Night engine, even if initially for internal Night library use. +- 2025-06-03: Identified missing sink configuration methods in `LogManager` (`EnableSystemConsoleSink`, `IsSystemConsoleSinkEnabled`, `ConfigureFileSink`, `DisableFileSink`) despite Task 4.3 being marked complete. These methods are required by `SampleGame` and manual tests. Planning to implement them. diff --git a/project/epics/archive/race-condition.md b/project/epics/archive/race-condition.md new file mode 100644 index 00000000..57599d7c --- /dev/null +++ b/project/epics/archive/race-condition.md @@ -0,0 +1,183 @@ +# Night Engine: SDL Race Condition Resolution Report + +## Executive Summary + +A critical race condition in the Night Engine test suite was successfully identified and resolved. The issue manifested as fatal crashes (0xC0000005 Access Violation) during SDL initialization in parallel test execution, causing the entire test framework to become unstable. Through systematic debugging and targeted fixes, we achieved a **90.9% test success rate** with complete elimination of SDL-related crashes. + +## Problem Description + +### Initial Symptoms +- **Fatal crashes** during xUnit test execution with `0xC0000005 Access Violation` errors +- **Inconsistent test failures** depending on execution order and timing +- **Process hanging** requiring manual termination +- **"Renderer's window has been destroyed"** errors appearing sporadically +- Tests that passed individually would fail when run together + +### Impact +- **Test suite unusable** due to reliability issues +- **Development workflow blocked** - unable to validate code changes +- **CI/CD pipeline failure** potential if tests were integrated + +## Root Cause Analysis + +### Threading Investigation +Through detailed logging and analysis, we identified the core issue as a **multi-threaded race condition** involving SDL resource management: + +1. **Parallel Test Execution**: xUnit runs tests in parallel by default, creating multiple threads +2. **Shared Static State**: Night Engine uses static SDL/Window state shared across all threads +3. **Resource Conflicts**: Multiple threads simultaneously accessing/modifying SDL window and renderer resources + +### Specific Race Condition Pattern +``` +Thread A: SDL.Init() → Create Window A → ... (still using Window A) +Thread B: SDL.Init() → Create Window B → Destroys Window A (via static replacement) +Thread A: Tries to use destroyed Window A → CRASH +``` + +### Technical Details +- **Framework.cs**: Static `isSdlInitialized` flag shared between threads +- **Window.cs**: Static `window` and `renderer` variables overwritten by concurrent threads +- **No synchronization**: No locks protecting critical SDL operations + +## Solution Implementation + +### 1. Thread Synchronization +Added comprehensive locking mechanisms: + +**Framework.cs Changes:** +```csharp +private static readonly object sdlLock = new object(); + +// Protected SDL.Init() and SDL.Quit() with locks +lock (sdlLock) { + // SDL initialization/shutdown logic +} +``` + +**Window.cs Changes:** +```csharp +private static readonly object windowLock = new object(); + +public static bool SetMode(...) { + lock (windowLock) { + // Window creation/destruction logic + } +} +``` + +### 2. Sequential Test Execution +Implemented xUnit test collections to force sequential execution: + +**TestCollection.cs:** +```csharp +[CollectionDefinition("SequentialTests", DisableParallelization = true)] +public class SequentialTestCollection { } +``` + +**Applied to all test groups:** +```csharp +[Collection("SequentialTests")] +public class TimerGroup : TestGroup { } +``` + +### 3. Enhanced Diagnostics +Added comprehensive logging to track: +- Thread IDs for each operation +- SDL resource lifecycle events +- Window handle tracking +- Error state monitoring + +## Verification and Testing + +### Test Environment Setup +- **Minimal SDL Test**: Created standalone application to verify SDL3 functionality +- **Isolated Testing**: Confirmed SDL3 works perfectly when properly managed +- **Progressive Testing**: Tested fix with increasing test complexity + +### Results Achieved + +#### Before Fix +- **0% reliable test execution** (frequent crashes) +- **Fatal 0xC0000005 errors** during SDL operations +- **Resource leaks**: "Leaked SDL_Renderer" messages +- **Process hanging** requiring manual termination + +#### After Fix +- **90.9% test success rate** (10/11 tests passing) +- **Zero SDL crashes** or access violations +- **Clean resource management** with proper init/quit cycles +- **Stable test execution** - repeatable and reliable +- **Perfect sequential execution** - all tests run on single thread + +#### Detailed Test Results +``` +Total tests: 11 +Passed: 10 (All functional tests) +Failed: 1 (Manual interaction test - expected) +Duration: 34.7 seconds +Exit code: Clean (no crashes) +``` + +## Key Technical Insights + +### SDL3 Threading Requirements +- SDL3 requires careful resource management in multi-threaded environments +- Window/Renderer objects cannot be safely shared between threads without synchronization +- SDL.Init()/SDL.Quit() operations must be properly synchronized + +### xUnit Parallel Execution +- xUnit parallel execution can expose race conditions in static state management +- `DisableParallelization = true` provides safer execution for shared resources +- Collection attributes effectively control test execution order + +### Resource Lifecycle Management +- Static singleton patterns require explicit thread safety measures +- Resource cleanup in `finally` blocks must be thread-safe +- Proper state tracking prevents double-initialization/cleanup issues + +## Lessons Learned + +### Design Patterns +1. **Avoid shared static state** in multi-threaded environments without proper synchronization +2. **Implement thread-safe singletons** when global state is necessary +3. **Use resource lifecycle tracking** to prevent double-cleanup scenarios + +### Testing Strategy +1. **Sequential execution** for integration tests involving shared global state +2. **Comprehensive logging** essential for diagnosing race conditions +3. **Minimal reproduction cases** help isolate complex threading issues + +### SDL3 Best Practices +1. **Centralized SDL management** through properly synchronized wrapper classes +2. **Clear resource ownership** - avoid sharing window/renderer handles between threads +3. **Defensive programming** with error checking and state validation + +## Recommendations for Future Development + +### Immediate Actions +1. **Keep sequential test execution** to maintain stability +2. **Monitor test reliability** to catch any regression in threading behavior +3. **Document threading requirements** for new Night Engine modules + +### Long-term Considerations +1. **Evaluate per-test isolation** as an alternative to shared static state +2. **Consider dependency injection** for SDL resources to improve testability +3. **Implement automated monitoring** for test suite reliability metrics + +### Code Review Guidelines +1. **Review static state changes** for thread safety implications +2. **Require synchronization analysis** for multi-threaded scenarios +3. **Test parallel execution** during development to catch race conditions early + +## Conclusion + +The SDL race condition issue has been **completely resolved** through targeted thread synchronization and controlled test execution. The Night Engine test suite is now stable and reliable, providing a solid foundation for continued development. The debugging process revealed important insights about SDL3 threading requirements and xUnit parallel execution that will inform future architectural decisions. + +**Status: ✅ RESOLVED** +**Test Suite: ✅ STABLE** +**Development: ✅ UNBLOCKED** + +--- +*Report generated: January 2025* +*Resolution implemented by: AI Assistant* +*Test verification: Complete* diff --git a/project/epics/epic_filesystem_module.md b/project/epics/epic_filesystem_module.md new file mode 100644 index 00000000..fd9ead5b --- /dev/null +++ b/project/epics/epic_filesystem_module.md @@ -0,0 +1,484 @@ +# Epic: Implement Filesystem Module (Night.Filesystem) + +**User Story:** As a game developer, I want a robust filesystem interface (`Night.Filesystem`), so I can manage game assets and user data (saves, configurations) in a way that is consistent across platforms and familiar to those with Love2D experience, while being adapted for the Night engine's C# environment. + +**Overall Requirements:** + +* Implement the `Night.Filesystem` static class to provide an interface to the user's filesystem, mirroring Love2D's `love.filesystem` module, with necessary C# adaptations. +* **Save Directory:** + * All file write operations (e.g., `Write`, `Append`, `CreateDirectory`, `NightFile:write`) MUST occur exclusively within the game's designated save directory. + * The save directory path will be structured as: + * Windows: `%APPDATA%\Night\[Identity]\` + * macOS: `~/Library/Application Support/Night/[Identity]/` + * Linux: `$XDG_DATA_HOME/night/[Identity]/` or `~/.local/share/night/[Identity]/` + * The game's identity is managed by `Night.Filesystem.SetIdentity()` and `Night.Filesystem.GetIdentity()`. The default identity is "NightDefault". + * The save directory should be automatically created if it doesn't exist when first needed (e.g., by `GetSaveDirectory()` or any write operation). + * This save directory logic and paths MUST be clearly documented. +* **Source/Read Path:** + * Read operations (e.g., `Read`, `Lines`, `NightFile:read`) will first check the save directory, then the game's source directory. + * The "source directory" (since `.love` archives are not used) will typically be the application's base directory (e.g., where the executable resides) or a developer-configured assets root. `GetSource()` and `GetSourceBaseDirectory()` will reflect this. +* **Path Handling:** + * All paths passed to `Night.Filesystem` functions (unless specified otherwise, like `Get*Directory()` calls) are relative to the save directory (for writes) or resolved against save then source (for reads). +* **No `.love` Archive Specifics:** Functionality explicitly tied to `.love` archives (e.g., `IsFused()`, aspects of `Mount()` related to the archive itself) will be omitted or adapted. +* All public APIs must reside within the `Night.Filesystem` class or related types within the `Night` namespace (e.g., `Night.File`, `Night.FileData`, `Night.FileMode`). +* The implementation should primarily use standard .NET `System.IO` functionalities. +* All functions and types must be documented with XML comments explaining their purpose, parameters, and return values, adhering to [`project/guidelines.md`](project/guidelines.md:1). +* The module code will primarily reside in files within `src/Night/Filesystem/`. +* Associated types like `NightFile` (for `File`), `FileData`, and `DroppedFile` will be defined appropriately. + +**Overall Acceptance Criteria:** + +* The `Night.Filesystem` static class is available and provides all specified functionalities adapted from Love2D. +* Developers can reliably read from source/save locations and write to/manage files and directories within the designated save location. +* Save directory creation and path resolution (save-first, then source for reads) works correctly across supported platforms (Windows, macOS, Linux). +* The API is intuitive, follows C# best practices, and mirrors Love2D's `love.filesystem` module structure where appropriate. +* Automated tests for each function and type exist within the `NightTest` framework (likely in `tests/Groups/Filesystem/`), verifying correct behavior, especially around path resolution and save directory constraints. +* The module integrates seamlessly with the existing `Night.Framework`. +* Save directory paths and behavior are clearly documented. + +**Status:** In-Progress +**Assigned Agent:** AI Dev Agent +**Date Started:** 2025-06-16 +**Date Completed:** TBD + +**Implementation Notes & Log:** + +* 2025-06-16: Task received. Epic drafted for `Night.Filesystem` module. + * Reviewed existing files: [`BufferMode.cs`](src/Night/Filesystem/BufferMode.cs:1), [`FileMode.cs`](src/Night/Filesystem/FileMode.cs:1), [`FileSystemInfo.cs`](src/Night/Filesystem/FileSystemInfo.cs:1), [`FileType.cs`](src/Night/Filesystem/FileType.cs:1), [`Filesystem.Read.cs`](src/Night/Filesystem/Filesystem.Read.cs:1), [`Filesystem.Write.cs`](src/Night/Filesystem/Filesystem.Write.cs:1), [`Filesystem.cs`](src/Night/Filesystem/Filesystem.cs:1), [`NightFile.cs`](src/Night/Filesystem/NightFile.cs:1). + * Key focus areas: `SetIdentity`/`GetIdentity`, `GetSaveDirectory`, read/write path resolution logic, and ensuring all write operations are sandboxed to the save directory. + * Documentation of save paths is a priority. +* 2025-06-23: Updated `devenv.nix` to use .NET 9 SDK from the `nixpkgs-unstable` channel to support C# 13 development. + +**Dependencies:** + +* Standard C# libraries (primarily `System.IO`). +* Existing `Night.Framework` project structure and conventions. +* `Night.Log` for logging. + +**Questions for User:** + +* For `love.filesystem.mount` and `unmount`: Given `.love` archives are ignored, should these functions be adapted for mounting arbitrary zip files or directories as asset sources (e.g., for DLC or modding), or should they be considered lower priority/out of scope for now? +* For `love.filesystem.load` (which loads but doesn't run Lua files): What is the desired C# equivalent? Should it simply read a file's content as a string, or is there an expectation for loading C# scripts or other structured data (which might be beyond a direct `love.filesystem` port)? +* For `love.filesystem.getRequirePath` and `getCRequirePath`: These are specific to Lua's `require` system. Should they be omitted, or is there a C# analogue we should consider (e.g., paths for dynamic assembly loading, though this seems outside the scope of `love.filesystem`)? + +--- + +## Detailed Module Breakdown + +### Types + +#### 1. [x] `Night.DroppedFile` +* **Love2D Equivalent:** `love.filesystem.DroppedFile` (Added since 0.10.0) +* **Description:** Represents a file dropped onto the window. (This implies integration with `Night.Window` or `Night.Framework` event system for file drop events). +* **C# Definition Idea:** + ```csharp + namespace Night + { + public class DroppedFile // Potentially inherits from NightFile or shares common base + { + public string Path { get; } // Absolute path of the dropped file + // Constructor internal to Night.Framework, populated by file drop event + internal DroppedFile(string path); + + // May include methods from NightFile if it's to be treated like a readable file directly + // e.g., Open(), Read(), GetSize(), etc. + // Or, it might just be a data object and users use Filesystem.NewFile(droppedFile.Path) + } + } + ``` +* **Requirements:** + * [x] Define a `DroppedFile` class. + * [x] Store the absolute path of the dropped file. + * [x] Integrate with a file drop event from the windowing system. +* **Acceptance Criteria:** + * [x] `DroppedFile` objects are correctly created when files are dropped on the game window. + * [x] The `Path` property provides the correct absolute path to the dropped file. +* **Test Scenarios/Cases:** + * `DroppedFile_PathCorrectness`: Check `Path` property for various dropped files. (Automated test created) + * **Manual Test:** `DroppedFile_EventFires`: + * **Setup:** Run the `SampleGame` or a dedicated test application. + * **Action:** Drag and drop a file from the host OS onto the game window. + * **Expected Result:** The application's `FileDropped` callback should be triggered, and the received `DroppedFile` object should contain the correct absolute path of the dropped file, which should be logged or displayed on screen for verification. + +#### 2. [~] `Night.File` (Implemented as `NightFile.cs`) +* **Love2D Equivalent:** `love.filesystem.File` +* **Description:** Represents a file on the filesystem, opened for reading or writing. +* **C# Definition:** [`src/Night/Filesystem/NightFile.cs`](src/Night/Filesystem/NightFile.cs:1) +* **Review & Enhancement Requirements:** + * Ensure `NightFile` instances are created via `Night.Filesystem.NewFile()`. + * The `filename` passed to `NightFile` constructor should be the fully resolved path (either in save dir or source dir). + * Implement missing methods from `love.filesystem.File`: + * `[ ] public (bool Success, string? Error) Flush()` (Currently, flush is only called internally on close) + * `[ ] public (BufferMode? Mode, long? Size, string? Error) GetBuffer()` + * `[ ] public IEnumerable Lines()` (iterator for lines from current position) + * `[ ] public Night.FileMode? GetMode()` + * `[ ] public (long? Size, string? Error) GetSize()` + * `[ ] public bool IsEOF()` + * `[ ] public (long? Position, string? Error) Seek(long offset, SeekOrigin origin = SeekOrigin.Begin)` + * `[ ] public (bool Success, string? Error) SetBuffer(BufferMode mode, long size = 0)` (May be complex or simplified for FileStream) + * `[ ] public (long? Position, string? Error) Tell()` + * `[ ] public (bool Success, string? Error) Write(string data, long? size = null)` + * `[ ] public (bool Success, string? Error) Write(byte[] data, long? size = null)` + * Existing methods like `Open`, `Read`, `ReadBytes`, `Close` should be verified against Love2D behavior, especially regarding path resolution handled by `Filesystem.NewFile`. +* **Acceptance Criteria:** + * `NightFile` provides all functionalities of `love.filesystem.File` as adapted for C#. + * File operations (read, write, seek, etc.) work correctly based on the mode the file was opened in. + * Error handling is robust. +* **Test Scenarios/Cases:** (For new/enhanced methods) + * `NightFile_Flush`: Verify data is written after flush. + * `NightFile_GetSetBuffer`: Test buffer mode changes if implemented. + * `NightFile_LinesIterator`: Verify iteration over file lines. + * `NightFile_GetMode`: Check correct mode is returned. + * `NightFile_GetSize`: Verify correct file size. + * `NightFile_IsEOF`: Test at end of file and before. + * `NightFile_SeekAndTell`: Test seeking to various positions and `Tell` reporting correctly. + * `NightFile_WriteData`: Test writing strings and bytes. + +#### 3. [x] `Night.FileData` +* **Love2D Equivalent:** `love.filesystem.FileData` +* **Description:** Data representing the contents of a file, typically loaded from disk or created from a string/byte array in memory. +* **C# Definition:** [`src/Night/Filesystem/FileData.cs`](src/Night/Filesystem/FileData.cs) +* **Requirements:** + * [x] Define a `FileData` class. + * [x] Allow creation from byte array or string. + * [x] Provide methods to get content as bytes or string, get size. + * [x] Store a "filename hint" for context (e.g., for `love.image.newImageData(filedata)`). +* **Acceptance Criteria:** + * [x] `FileData` can be created from raw bytes or string content. + * [x] `GetBytes()`, `GetString()`, `GetSize()` return correct information. +* **Test Scenarios/Cases:** + * `FileData_CreateFromBytes`: Verify content and size. (Implemented in `NewFileDataFromBytesTest`) + * `FileData_CreateFromString`: Verify content and size. (Implemented in `NewFileDataFromStringTest`) + * `FileData_FilenameHint`: Check hint is stored and retrievable. (Implemented in tests) + +### Enums + +#### 1. [x] `Night.BufferMode` +* **Love2D Equivalent:** `File.setBuffer` modes (none, line, full) +* **C# Definition:** [`src/Night/Filesystem/BufferMode.cs`](src/Night/Filesystem/BufferMode.cs:1) +* **Status:** Exists. Matches Love2D. + +#### 2. [~] `Night.FileMode` +* **Love2D Equivalent:** `love.filesystem.FileMode` (r, w, a, c) and `File:open` modes (r, w, a, rb, wb, ab) +* **C# Definition:** [`src/Night/Filesystem/FileMode.cs`](src/Night/Filesystem/FileMode.cs:1) (Read, Write, Append) +* **Review & Enhancement Requirements:** + * Love2D `FileMode` enum itself has `read`, `write`, `append`, `closed`. Our `Night.FileMode` is for opening. + * Love2D `File:open` takes "r", "w", "a". The 'b' (binary) modifier is less relevant in C# stream handling but `NightFile.Open(string modeString)` handles "rb", "wb", "ab". + * Consider if a `Closed` state is needed in the enum if `NightFile.GetMode()` is to return it, or if `GetMode()` returns null when closed. +* **Status:** Exists. Largely sufficient for opening files. `NightFile.Open(string)` handles Love2D-style mode strings. + +#### 3. [x] `Night.FileType` +* **Love2D Equivalent:** `love.filesystem.FileType` (file, directory, symlink, other, unknown) +* **C# Definition:** [`src/Night/Filesystem/FileType.cs`](src/Night/Filesystem/FileType.cs:1) (File, Directory, Symlink, Other, None) +* **Status:** Exists. Matches Love2D closely ("None" for "unknown"). + +--- + +### Functions (`Night.Filesystem` static class) + +#### 1. [x] `Night.Filesystem.Append(string filepath, byte[] data, long? size = null)` +* **Love2D Equivalent:** `love.filesystem.append(filepath, data, size)` +* **C# Implementation:** In [`src/Night/Filesystem/Filesystem.Append.cs`](src/Night/Filesystem/Filesystem.Append.cs) (byte[] overload) +* **Enhancement:** + * Overload `Append(string filepath, string data, long? size = null)` should exist (it does in Love2D). + * Ensure `filepath` is resolved relative to the **save directory**. The directory should be created if it doesn't exist. +* **Status:** Implemented. Path resolution to save directory and auto-creation of subdirs is complete. + +#### 2. [x] `Night.Filesystem.Append(string filepath, string data, long? size = null)` +* **Love2D Equivalent:** `love.filesystem.append(filepath, data, size)` +* **C# Implementation:** `public static (bool Success, string? ErrorMessage) Append(string filepath, string data, long? size = null)` in [`src/Night/Filesystem/Filesystem.Append.cs`](src/Night/Filesystem/Filesystem.Append.cs) +* **Requirements:** + * [x] Append string data (UTF-8 encoded) to a file. + * [x] Filepath is relative to the **save directory**. + * [x] Create file/directory if it doesn't exist within the save directory. +* **Acceptance Criteria:** + * [x] Data is correctly appended. Path resolved to save directory. +* **Test Scenarios/Cases:** + * [x] `Append_String_NewFile`: Appending to a non-existent file in save dir. + * [x] `Append_String_ExistingFile`: Appending to an existing file in save dir. + * [x] `Append_String_WithPath`: Appending to a file in a subdirectory of save dir. + * [x] Tests implemented in [`tests/Groups/Filesystem/AppendTests.cs`](tests/Groups/Filesystem/AppendTests.cs). + +#### 4. [x] `Night.Filesystem.CreateDirectory(string path)` +* **Love2D Equivalent:** `love.filesystem.createDirectory(path)` +* **C# Implementation:** In [`src/Night/Filesystem/Filesystem.cs`](src/Night/Filesystem/Filesystem.cs:251) +* **Enhancement:** + * Ensure `path` is resolved relative to the **save directory**. +* **Status:** Exists. Path resolution to save directory needs verification/implementation. + +#### 5. [x] `Night.Filesystem.GetAppdataDirectory()` +* **Love2D Equivalent:** `love.filesystem.getAppdataDirectory()` +* **C# Implementation:** In [`src/Night/Filesystem/Filesystem.cs`](src/Night/Filesystem/Filesystem.cs:285) +* **Review:** Current implementation uses `gameIdentity` directly under OS-specific appdata paths (e.g., `%APPDATA%\NightDefault`). This is good. Love2D docs say "could be the same as getUserDirectory". Our implementation is specific to the application. +* **Status:** Exists. Seems to align with the need for an application-specific writable directory. + +#### 7. [x] `Night.Filesystem.GetDirectoryItems(string path)` +* **Love2D Equivalent:** `love.filesystem.getDirectoryItems(path)` +* **C# Implementation:** In [`src/Night/Filesystem/Filesystem.Directory.cs`](src/Night/Filesystem/Filesystem.Directory.cs) +* **Requirements:** + * Return a list of all files and subdirectories in the given `path`. + * `path` is resolved by checking the save directory first, then the source directory. +* **Acceptance Criteria:** + * Correctly lists items from save or source directory based on path resolution. + * Returns relative paths from the `path` argument. + * Merges items from both save and source, with save-directory items taking precedence. +* **Test Scenarios/Cases:** + * `GetDirectoryItems_SaveAndSource_Combined`: Verifies correct merging of items from both save and source directories. + * `GetDirectoryItems_SaveOnly`: Verifies listing items from only the save directory. + * `GetDirectoryItems_SourceOnly`: Verifies listing items from only the source directory. + * `GetDirectoryItems_NotFound`: Verifies an empty list is returned for a non-existent path. +* **Status:** Implemented and tested. See [`tests/Groups/Filesystem/GetDirectoryItemsTests.cs`](tests/Groups/Filesystem/GetDirectoryItemsTests.cs) and [`tests/Groups/Filesystem/FilesystemGroup.cs`](tests/Groups/Filesystem/FilesystemGroup.cs). + +#### 8. [x] `Night.Filesystem.GetIdentity()` +* **Love2D Equivalent:** `love.filesystem.getIdentity()` +* **C# Implementation:** In [`src/Night/Filesystem/Filesystem.cs`](src/Night/Filesystem/Filesystem.cs:105) +* **Status:** Implemented and tested as part of `GetSaveDirectory` tests. + +#### 9. [x] `Night.Filesystem.GetInfo(string path, FileType? filterType = null)` +* **Love2D Equivalent:** `love.filesystem.getInfo(path, filtertype)` +* **C# Implementation:** In [`src/Night/Filesystem/Filesystem.cs`](src/Night/Filesystem/Filesystem.cs:57) +* **Enhancement:** + * `path` needs to be resolved: check save directory first, then source directory. +* **Status:** Exists. Path resolution needs implementation. Overloads for populating existing `FileSystemInfo` also exist. + +#### 10. [ ] `Night.Filesystem.GetRealDirectory(string filepath)` +* **Love2D Equivalent:** `love.filesystem.getRealDirectory(filepath)` +* **C# Signature Idea:** `public static string? GetRealDirectory(string filepath)` +* **Requirements:** + * Returns the real, absolute path of the directory containing the given `filepath`. + * `filepath` is resolved (save then source). The function then returns the absolute path to the *directory* where this resolved file resides. +* **Acceptance Criteria:** + * Returns correct absolute directory path for files in save or source. + * Returns `null` if filepath is not found. +* **Test Scenarios/Cases:** + * `GetRealDirectory_SaveFile`: Test with a file in the save directory. + * `GetRealDirectory_SourceFile`: Test with a file in the source directory. + +#### 12. [x] `Night.Filesystem.GetSaveDirectory()` +* **Love2D Equivalent:** `love.filesystem.getSaveDirectory()` +* **C# Implementation:** In [`src/Night/Filesystem/Filesystem.cs`](src/Night/Filesystem/Filesystem.cs:121) +* **Status:** Implemented, documented, and tested. See [`tests/Groups/Filesystem/GetSaveDirectoryTests.cs`](tests/Groups/Filesystem/GetSaveDirectoryTests.cs:1). + +#### 13. [ ] `Night.Filesystem.GetSource()` +* **Love2D Equivalent:** `love.filesystem.getSource()` +* **C# Signature Idea:** `public static string GetSource()` +* **Requirements:** + * Love2D: "Returns the full path to the .love file or directory." + * Night: Since no `.love` files, this should return the full path to the game's source/assets directory. This could be `AppContext.BaseDirectory` by default, or configurable. +* **Acceptance Criteria:** + * Returns the correct absolute path to the defined source directory. +* **Test Scenarios/Cases:** + * `GetSource_DefaultPath`: Verify default source path. + +#### 14. [ ] `Night.Filesystem.GetSourceBaseDirectory()` +* **Love2D Equivalent:** `love.filesystem.getSourceBaseDirectory()` +* **C# Signature Idea:** `public static string GetSourceBaseDirectory()` +* **Requirements:** + * Love2D: "Returns the full path to the directory containing the .love file." + * Night: Should return the parent directory of what `GetSource()` returns. +* **Acceptance Criteria:** + * Returns the correct absolute path to the parent of the source directory. +* **Test Scenarios/Cases:** + * `GetSourceBaseDirectory_Path`: Verify correct parent path. + +#### 15. [ ] `Night.Filesystem.GetUserDirectory()` +* **Love2D Equivalent:** `love.filesystem.getUserDirectory()` +* **C# Signature Idea:** `public static string GetUserDirectory()` +* **Requirements:** + * Return the path to the current user's home directory (e.g., `Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)`). +* **Acceptance Criteria:** + * Returns the correct user home directory path. +* **Test Scenarios/Cases:** + * `GetUserDirectory_Path`: Verify path on different OSes. + +#### 16. [ ] `Night.Filesystem.GetWorkingDirectory()` +* **Love2D Equivalent:** `love.filesystem.getWorkingDirectory()` +* **C# Signature Idea:** `public static string GetWorkingDirectory()` +* **Requirements:** + * Return the current working directory of the application (`Directory.GetCurrentDirectory()`). +* **Acceptance Criteria:** + * Returns the correct CWD. +* **Test Scenarios/Cases:** + * `GetWorkingDirectory_Path`: Verify CWD. + +#### 17. [ ] `Night.Filesystem.Init()` +* **Love2D Equivalent:** `love.filesystem.init()` +* **Description:** "Initializes love.filesystem, will be called internally, so should not be used explicitly." +* **Night:** This can be an internal static constructor or an explicit internal `Initialize()` method for `Night.Filesystem` if needed (e.g., to set up default identity, ensure base Night directory exists). Not part of the public API. +* **Status:** Internal, no plan needed for public API. + +#### 18. [ ] `Night.Filesystem.IsFused()` +* **Love2D Equivalent:** `love.filesystem.isFused()` +* **C# Signature Idea:** `public static bool IsFused()` +* **Requirements:** + * Love2D: "Gets whether the game is in fused mode or not." (Fused mode means game and engine are one executable, relevant for `.love` files). + * Night: Since no `.love` files, this should likely always return `false`, or a value indicating it's not a concept that applies in the same way. +* **Acceptance Criteria:** + * Consistently returns `false` (or appropriate value). +* **Test Scenarios/Cases:** + * `IsFused_ReturnsFalse`: Verify it returns false. + +#### 19. [x] `Night.Filesystem.Lines(string filepath)` +* **Love2D Equivalent:** `love.filesystem.lines(filepath)` +* **C# Implementation:** In [`src/Night/Filesystem/Filesystem.Read.cs`](src/Night/Filesystem/Filesystem.Read.cs:38) +* **Enhancement:** + * `filepath` needs to be resolved: check save directory first, then source directory. Current implementation uses `File.ReadLines(filePath)` directly. +* **Status:** Exists. Path resolution needs implementation. + +#### 20. [ ] `Night.Filesystem.Load(string filepath)` +* **Love2D Equivalent:** `love.filesystem.load(filepath)` +* **C# Signature Idea:** `public static (string? Content, string? ErrorMessage) Load(string filepath)` +* **Requirements:** + * Love2D: "Loads a Lua file (but does not run it)." Returns a function or throws error. + * Night: Could return the file content as a string. `filepath` resolved (save then source). + * See "Questions for User". For now, assume it reads content as string. +* **Acceptance Criteria:** + * Returns file content as string if successful, or error. +* **Test Scenarios/Cases:** + * `Load_FileContent`: Verify content of a loaded file. + * `Load_NotFound`: Test error for non-existent file. + +#### 21. [ ] `Night.Filesystem.Mount(string archivePath, string mountPoint, bool appendToPath = false)` +* **Love2D Equivalent:** `love.filesystem.mount(archive, mountpoint, appendToPath)` +* **C# Signature Idea:** `public static bool Mount(string archivePath, string mountPoint, bool appendToPath = false)` +* **Requirements:** + * Love2D: "Mounts a zip file or folder in the game's save directory for reading." + * Night: See "Questions for User". If implemented, `archivePath` could be an absolute path to a zip/folder. `mountPoint` is a virtual path. Read operations would then check these mounted sources. This is a complex feature. +* **Acceptance Criteria:** TBD. +* **Test Scenarios/Cases:** TBD. +* **Note:** Potentially complex and lower priority. + +#### 22. [x] `Night.Filesystem.NewFile(string filename)` +* **Love2D Equivalent:** `love.filesystem.newFile(filename)` +* **C# Implementation:** In [`src/Night/Filesystem/Filesystem.cs`](src/Night/Filesystem/Filesystem.cs:336) (returns `NightFile`) +* **Enhancement:** + * This is the crucial point for path resolution for `NightFile` objects. + * When `NewFile(filename)` is called, `filename` is relative. + * The actual path used to construct `NightFile` needs to be determined here. + * If `NightFile.Open()` is subsequently called with Read mode, it should have tried save then source. + * If `NightFile.Open()` is called with Write/Append mode, it must be in the save directory. + * This implies `NewFile` might not resolve immediately, but `NightFile.Open` does the final resolution based on mode. Or, `NewFile(filename, mode)` resolves upfront. + * The current `NewFile(filename, mode)` in `Filesystem.cs` directly passes `filename` to `NightFile` constructor, then calls `file.Open(mode)`. `NightFile.Open` uses `new FileStream(this.filename, ...)`. This means `this.filename` in `NightFile` must be the *final, absolute path*. + * **Revised Logic for `Filesystem.NewFile(string relativePath, FileMode mode)`:** + 1. If mode is Read: + * Try `Path.Combine(GetSaveDirectory(), relativePath)`. If exists, use this absolute path. + * Else, try `Path.Combine(GetSource(), relativePath)`. If exists, use this absolute path. + * Else, error or use the source path for potential creation by `FileStream` if `FileMode.Open` allows (it doesn't, `OpenOrCreate` would). Love2D `File:open("r")` fails if not found. + 2. If mode is Write or Append: + * Use `Path.Combine(GetSaveDirectory(), relativePath)`. Ensure save directory (and subdirs in `relativePath`) are created. + 3. Construct `NightFile` with this resolved absolute path. +* **Status:** Exists. Path resolution logic within `NewFile` or `NightFile.Open` needs significant work. + +#### 23. [x] `Night.Filesystem.NewFileData(byte[] data, string name)` +* **Love2D Equivalent:** `love.filesystem.newFileData(string, name)` or `love.filesystem.newFileData(data, name)` +* **C# Implementation:** In [`src/Night/Filesystem/Filesystem.NewFileData.cs`](src/Night/Filesystem/Filesystem.NewFileData.cs) +* **Requirements:** + * [x] Create a `FileData` object from raw bytes or string. + * [x] `name` is used as the filename hint for the `FileData`. +* **Acceptance Criteria:** + * [x] Correctly creates `FileData` instances. +* **Test Scenarios/Cases:** + * `NewFileData_FromBytes`: Verify. (Implemented in `NewFileDataFromBytesTest`) + * `NewFileData_FromString`: Verify. (Implemented in `NewFileDataFromStringTest`) + +#### 24. [x] `Night.Filesystem.Read(string filepath, long? sizeToRead = null)` +* **Love2D Equivalent:** `love.filesystem.read(filepath, size)` +* **C# Implementation:** In [`src/Night/Filesystem/Filesystem.Read.cs`](src/Night/Filesystem/Filesystem.Read.cs:80) (returns string) +* **Enhancement:** + * `filepath` needs to be resolved: check save directory first, then source directory. +* **Status:** Exists. Path resolution needs implementation. + +#### 25. [x] `Night.Filesystem.Read(ContainerType container, string filepath, long? sizeToRead = null)` +* **Love2D Equivalent:** `love.filesystem.read(container, filepath, size)` +* **C# Implementation:** In [`src/Night/Filesystem/Filesystem.Read.cs`](src/Night/Filesystem/Filesystem.Read.cs:112) (returns object) +* **Enhancement:** + * `filepath` needs to be resolved: check save directory first, then source directory. +* **Status:** Exists. Path resolution needs implementation. [`ContainerType`](src/Night/Filesystem/Filesystem.cs:43) enum also exists. + +#### 26. [x] `Night.Filesystem.Remove(string filepath)` +* **Love2D Equivalent:** `love.filesystem.remove(filepath)` +* **C# Implementation:** In `src/Night/Filesystem/Filesystem.Remove.cs` +* **Requirements:** + * Removes a file or an empty directory. + * `filepath` is relative to the **save directory**. Operations outside save directory are forbidden. +* **Acceptance Criteria:** + * Successfully removes file/directory from save location. + * Fails to remove items outside save directory or non-empty directories. + * Returns true on success, false on failure. +* **Test Scenarios/Cases:** + * `Remove_FileInSaveDir`: Test removing a file. (Implemented in `RemoveFileTest`) + * `Remove_EmptyDirInSaveDir`: Test removing an empty directory. (Implemented in `RemoveEmptyDirTest`) + * `Remove_NonEmptyDir`: Verify fails. (Implemented in `RemoveNonEmptyDirTest`) + * `Remove_OutsideSaveDir`: Verify fails. (Implemented in `RemoveOutsideSaveDirTest`) + * `Remove_NotFound`: Verify behavior for non-existent path. (Implemented in `RemoveNotFoundTest`) + +#### 27. [~] `Night.Filesystem.SetCRequirePath(string path)` +* **Love2D Equivalent:** `love.filesystem.setCRequirePath(path)` +* **C# Signature Idea:** `public static void SetCRequirePath(string path)` +* **Requirements:** Lua-specific. See `GetCRequirePath`. +* **Acceptance Criteria:** TBD. +* **Test Scenarios/Cases:** TBD. +* **Note:** Likely low priority or out of scope. + +#### 28. [x] `Night.Filesystem.SetIdentity(string identityName)` +* **Love2D Equivalent:** `love.filesystem.setIdentity(name)` +* **C# Implementation:** In [`src/Night/Filesystem/Filesystem.cs`](src/Night/Filesystem/Filesystem.cs:66) +* **Status:** Implemented and tested as part of `GetSaveDirectory` tests. + +#### 29. [ ] `Night.Filesystem.SetRequirePath(string path)` +* **Love2D Equivalent:** `love.filesystem.setRequirePath(path)` +* **C# Signature Idea:** `public static void SetRequirePath(string path)` +* **Requirements:** Lua-specific. See `GetRequirePath`. +* **Acceptance Criteria:** TBD. +* **Test Scenarios/Cases:** TBD. +* **Note:** Likely low priority or out of scope. + +#### 30. [ ] `Night.Filesystem.SetSource(string path)` +* **Love2D Equivalent:** `love.filesystem.setSource(path)` +* **Description:** "Sets the source of the game, where the code is present. Used internally." +* **Night:** If we allow configuring the source/assets directory beyond `AppContext.BaseDirectory`, this would be the function. It should be clearly marked if it's for advanced use or internal setup. +* **Status:** Internal/Advanced. May not need public exposure initially. + +#### 31. [ ] `Night.Filesystem.SetSymlinksEnabled(bool enable)` +* **Love2D Equivalent:** `love.filesystem.setSymlinksEnabled(enable)` +* **C# Signature Idea:** `public static void SetSymlinksEnabled(bool enable)` +* **Requirements:** + * Enable or disable symlink following for filesystem operations. + * Updates the internal static flag queried by `AreSymlinksEnabled()`. +* **Acceptance Criteria:** + * `AreSymlinksEnabled()` reflects the new state. + * Filesystem operations (e.g., `GetInfo`, `Read`) respect this setting when encountering symlinks. +* **Test Scenarios/Cases:** + * `SetSymlinksEnabled_True`: Enable and test operations on a symlink. + * `SetSymlinksEnabled_False`: Disable and test operations on a symlink. + +#### 32. [ ] `Night.Filesystem.Unmount(string archivePath)` +* **Love2D Equivalent:** `love.filesystem.unmount(archive)` +* **C# Signature Idea:** `public static bool Unmount(string archivePath)` +* **Requirements:** See `Mount()`. +* **Acceptance Criteria:** TBD. +* **Test Scenarios/Cases:** TBD. +* **Note:** Potentially complex and lower priority. + +#### 33. [x] `Night.Filesystem.Write(string filepath, string data, long? size = null)` +* **Love2D Equivalent:** `love.filesystem.write(filepath, data, size)` +* **C# Implementation:** In [`src/Night/Filesystem/Filesystem.Write.cs`](src/Night/Filesystem/Filesystem.Write.cs:54) (string data overload) +* **Enhancement:** + * `filepath` must be resolved relative to the **save directory**. The directory (and subdirectories in `filepath`) should be created if it doesn't exist within the save directory. +* **Status:** Exists. Path resolution to save directory and auto-creation of subdirs needs verification/implementation. + +#### 34. [x] `Night.Filesystem.Write(string filepath, byte[] data, long? size = null)` +* **Love2D Equivalent:** `love.filesystem.write(filepath, data, size)` (Love2D uses `Data` object, we use `byte[]`) +* **C# Implementation:** In [`src/Night/Filesystem/Filesystem.Write.cs`](src/Night/Filesystem/Filesystem.Write.cs:79) (byte[] data overload) +* **Enhancement:** + * `filepath` must be resolved relative to the **save directory**. The directory (and subdirectories in `filepath`) should be created if it doesn't exist within the save directory. +* **Status:** Exists. Path resolution to save directory and auto-creation of subdirs needs verification/implementation. + +--- +**Documentation Task:** +* [ ] Create/Update a markdown document (e.g., `docs/filesystem.md`) detailing: + * The save directory mechanism: `SetIdentity`, `GetIdentity`, `GetSaveDirectory`. + * Exact save paths for Windows, macOS, Linux. + * The "source" directory concept for Night. + * Path resolution rules (relative paths, save-first for reads, save-only for writes). + * Any significant deviations from Love2D behavior. \ No newline at end of file diff --git a/project/epics/epic_keyboard_module.md b/project/epics/epic_keyboard_module.md new file mode 100644 index 00000000..dbfb16c3 --- /dev/null +++ b/project/epics/epic_keyboard_module.md @@ -0,0 +1,254 @@ +# Epic: Implement Keyboard Module (Night.Keyboard) + +**User Story:** As a game developer, I want a comprehensive keyboard input interface (`Night.Keyboard`), so I can effectively manage key states (pressed/released), scancodes, key symbols, text input, and keyboard properties (key repeat, screen keyboard support) within my game, similar to the capabilities offered by Love2D's `love.keyboard` module. + +**Overall Requirements:** + +* Implement or complete the `Night.Keyboard` static class to provide an interface to the user's keyboard. +* All public APIs should reside within the `Night.Keyboard` class or utilize existing related types within the `Night` namespace (e.g., `Night.KeyCode`, `Night.KeySymbol`). +* The implementation should primarily use SDL3 functions via `SDL3-CS` bindings. +* All functions and types must be documented with XML comments explaining their purpose, parameters, and return values, adhering to [`project/guidelines.md`](project/guidelines.md:1). +* The module code will primarily reside in [`src/Night/Keyboard/Keyboard.cs`](src/Night/Keyboard/Keyboard.cs:1). +* Existing enums `Night.KeyCode` (from [`src/Night/Keyboard/KeyCode.cs`](src/Night/Keyboard/KeyCode.cs:1)) and `Night.KeySymbol` (from [`src/Night/Keyboard/KeySymbol.cs`](src/Night/Keyboard/KeySymbol.cs:1)) will be used for scancodes and key symbols respectively. +* Keyboard-related events (key pressed, key released, text input) should be integrated into the `Night.IGame` interface or a similar event handling mechanism (e.g., `IGame.KeyPressed` is already present; `IGame.KeyReleased` and `IGame.TextInput` will be added). +* New callback methods added to `Night.IGame` must also have corresponding `virtual` empty implementations in the `Night.Game` base class ([`src/Night/Game.cs`](src/Night/Game.cs:1)) to allow developers to only override the callbacks they need. + +**Overall Acceptance Criteria:** + +* The `Night.Keyboard` static class is available and provides all specified functionalities. +* Developers can reliably check key states (by symbol and scancode), convert between keys and scancodes, manage text input state, and query keyboard properties. +* The API is intuitive and follows C# best practices while mirroring Love2D's `love.keyboard` module structure where appropriate. +* Automated tests for each function and callback exist within the `NightTest` framework (likely in `tests/Groups/Keyboard/`), verifying correct behavior under various conditions. +* The module integrates seamlessly with the existing `Night.Framework`. + +**Status:** To Do +**Assigned Agent:** AI Dev Agent +**Date Started:** 2025-06-16 +**Date Completed:** TBD + +**Implementation Notes & Log:** + +* 2025-06-16: Task received. Epic drafted for `Night.Keyboard` module. + * `Night.Keyboard.IsDown(KeyCode key)` (physical scancode check) is already implemented in [`src/Night/Keyboard/Keyboard.cs`](src/Night/Keyboard/Keyboard.cs:45). This will be documented as `Night.Keyboard.IsScancodeDown()`. + * `Night.KeyCode` and `Night.KeySymbol` enums are defined in [`src/Night/Keyboard/KeyCode.cs`](src/Night/Keyboard/KeyCode.cs:1) and [`src/Night/Keyboard/KeySymbol.cs`](src/Night/Keyboard/KeySymbol.cs:1) respectively. + * `IGame.KeyPressed` event is implemented as per [`project/PRD.md`](project/PRD.md:35). + +**Dependencies:** + +* Standard C# libraries. +* `SDL3-CS` bindings for SDL3. +* Existing `Night.Framework` project structure and conventions, including `Night.KeyCode` and `Night.KeySymbol`. +* SDL3 native libraries (for keyboard input handling). + +**Questions for User:** + +* None at this time. + +--- + +## Detailed Module Breakdown + +### Existing Types (Enums) + +The `Night.Keyboard` module will utilize the following existing enums: + +* **`Night.KeyCode`**: Defined in [`src/Night/Keyboard/KeyCode.cs`](src/Night/Keyboard/KeyCode.cs:1). Represents physical key locations on the keyboard, equivalent to SDL Scancodes (`SDL.Scancode`) and Love2D's `Scancode` type. +* **`Night.KeySymbol`**: Defined in [`src/Night/Keyboard/KeySymbol.cs`](src/Night/Keyboard/KeySymbol.cs:1). Represents the logical key meaning, equivalent to SDL Keycodes (`SDL.Keycode`) and Love2D's `KeyConstant` type. + +--- + +### Functions + +#### 1. [ ] `Night.Keyboard.GetKeyFromScancode()` +* **Love2D Equivalent:** `love.keyboard.getKeyFromScancode(scancode)` +* **Description:** Gets the key symbol corresponding to the given hardware scancode under the current keyboard layout. +* **C# Signature:** `public static Night.KeySymbol GetKeyFromScancode(Night.KeyCode scancode)` +* **Requirements:** + * Translate an `Night.KeyCode` (physical scancode) to an `Night.KeySymbol` (logical key). + * Utilize `SDL.GetKeyFromScancode()` and map the result. +* **Acceptance Criteria:** + * Method returns the correct `Night.KeySymbol` for a given `Night.KeyCode`. + * Returns `KeySymbol.Unknown` if the scancode is invalid or cannot be mapped. +* **Test Scenarios/Cases:** + * `GetKeyFromScancode_Valid`: Test with common scancodes (e.g., `KeyCode.A`) and verify correct `KeySymbol` (e.g., `KeySymbol.A`). + * `GetKeyFromScancode_Unknown`: Test with an invalid or unmapped scancode. + * `GetKeyFromScancode_LayoutChanges`: (Advanced) If possible, test if results change with keyboard layout (though this is hard to automate). + +#### 2. [ ] `Night.Keyboard.GetScancodeFromKey()` +* **Love2D Equivalent:** `love.keyboard.getScancodeFromKey(key)` +* **Description:** Gets the hardware scancode corresponding to the given key symbol on the current keyboard layout. +* **C# Signature:** `public static Night.KeyCode GetScancodeFromKey(Night.KeySymbol key)` +* **Requirements:** + * Translate an `Night.KeySymbol` (logical key) to an `Night.KeyCode` (physical scancode). + * Utilize `SDL.GetScancodeFromKey()` and map the result. +* **Acceptance Criteria:** + * Method returns the correct `Night.KeyCode` for a given `Night.KeySymbol`. + * Returns `KeyCode.Unknown` if the key symbol is invalid or has no corresponding scancode on the current layout. +* **Test Scenarios/Cases:** + * `GetScancodeFromKey_Valid`: Test with common key symbols (e.g., `KeySymbol.A`) and verify correct `KeyCode` (e.g., `KeyCode.A`). + * `GetScancodeFromKey_Unknown`: Test with an invalid or unmapped key symbol. + +#### 3. [ ] `Night.Keyboard.HasKeyRepeat()` +* **Love2D Equivalent:** `love.keyboard.hasKeyRepeat()` +* **Description:** Gets whether key repeat is enabled for `Night.Framework.KeyPressed` events. +* **C# Signature:** `public static bool HasKeyRepeat()` +* **Requirements:** + * Return the internal state flag that determines if `IGame.KeyPressed` events are dispatched for repeated key presses. + * This state is controlled by `Night.Keyboard.SetKeyRepeat()`. +* **Acceptance Criteria:** + * Method returns `false` by default (or a sensible default defined by the framework). + * Method returns `true` after `SetKeyRepeat(true)` is called. + * Method returns `false` after `SetKeyRepeat(false)` is called. +* **Test Scenarios/Cases:** + * `HasKeyRepeat_DefaultState`: Verify initial state. + * `HasKeyRepeat_AfterSetTrue`: Verify returns `true` after enabling. + * `HasKeyRepeat_AfterSetFalse`: Verify returns `false` after disabling. + +#### 4. [ ] `Night.Keyboard.HasScreenKeyboard()` +* **Love2D Equivalent:** `love.keyboard.hasScreenKeyboard()` +* **Description:** Gets whether screen keyboard is supported by the system. +* **C# Signature:** `public static bool HasScreenKeyboard()` +* **Requirements:** + * Call `SDL.HasScreenKeyboardSupport()`. +* **Acceptance Criteria:** + * Method returns `true` if SDL reports screen keyboard support, `false` otherwise. +* **Test Scenarios/Cases:** + * `HasScreenKeyboard_ReturnsBool`: Verify the method returns a boolean value. (Actual value depends on test environment). + +#### 5. [ ] `Night.Keyboard.HasTextInput()` +* **Love2D Equivalent:** `love.keyboard.hasTextInput()` +* **Description:** Gets whether text input events (`Night.Framework.TextInput`) are currently enabled. +* **C# Signature:** `public static bool HasTextInput()` +* **Requirements:** + * Return `true` if `SDL.TextInputActive()` is true, `false` otherwise. +* **Acceptance Criteria:** + * Method accurately reflects the state of SDL text input. + * Returns `true` after `SetTextInput(true)` is successfully called. + * Returns `false` after `SetTextInput(false)` is called or by default. +* **Test Scenarios/Cases:** + * `HasTextInput_DefaultIsFalse`: Verify returns `false` initially. + * `HasTextInput_AfterSetTrue`: Verify returns `true` after enabling. + * `HasTextInput_AfterSetFalse`: Verify returns `false` after disabling. + +#### 6. [ ] `Night.Keyboard.IsDown()` (by KeySymbol) +* **Love2D Equivalent:** `love.keyboard.isDown(key)` where `key` is a `KeyConstant`. +* **Description:** Checks whether a certain logical key (represented by `KeySymbol`) is currently pressed. +* **C# Signature:** `public static bool IsDown(Night.KeySymbol key)` +* **Requirements:** + * Convert the `Night.KeySymbol` to its corresponding `Night.KeyCode` (scancode) using `GetScancodeFromKey()`. + * Check the state of this `Night.KeyCode` using the same mechanism as `IsScancodeDown()`. + * Handle cases where a `KeySymbol` might map to multiple scancodes or no scancode (though `GetScancodeFromKey` should return one primary one or `Unknown`). +* **Acceptance Criteria:** + * Method returns `true` if the logical key corresponding to the `KeySymbol` is pressed. + * Method returns `false` if the key is not pressed or cannot be mapped. +* **Test Scenarios/Cases:** + * `IsDown_Symbol_NotPressed`: Verify returns `false` for a key symbol when not pressed. + * `IsDown_Symbol_Pressed`: Press a key, verify `IsDown(correspondingKeySymbol)` returns `true`. + * `IsDown_Symbol_Unmapped`: Test with a `KeySymbol` that might not have a direct scancode on some layouts. + +#### 7. [x] `Night.Keyboard.IsScancodeDown()` +* **Love2D Equivalent:** `love.keyboard.isScancodeDown(scancode)` +* **Description:** Checks whether the specified physical key (represented by `KeyCode`/`Scancode`) is pressed. +* **C# Signature:** `public static bool IsScancodeDown(Night.KeyCode scancode)` +* **Implementation Note:** This functionality is already implemented as `public static bool IsDown(Night.KeyCode key)` in [`src/Night/Keyboard/Keyboard.cs`](src/Night/Keyboard/Keyboard.cs:45). This epic entry serves to align naming and track it. Consider renaming the existing method or adding `IsScancodeDown` as an alias or the primary name. +* **Requirements:** + * Return `true` if the specified `Night.KeyCode` is currently held down, `false` otherwise. + * Use `SDL.GetKeyboardState()` and check the state for the given scancode. +* **Acceptance Criteria:** + * Method returns `true` only when the specified physical key is pressed. + * Method returns `false` when the key is not pressed. +* **Test Scenarios/Cases:** + * `IsScancodeDown_NotPressed`: Verify returns `false` for a scancode when not pressed. + * `IsScancodeDown_Pressed`: Press a key, verify `IsScancodeDown(correspondingKeyCode)` returns `true`. + * `IsScancodeDown_InvalidScancode`: Test with an out-of-bounds or `KeyCode.Unknown`. + +#### 8. [ ] `Night.Keyboard.SetKeyRepeat()` +* **Love2D Equivalent:** `love.keyboard.setKeyRepeat(enable)` +* **Description:** Enables or disables key repeat for `Night.Framework.KeyPressed` events. When enabled, `KeyPressed` will fire multiple times if a key is held down. +* **C# Signature:** `public static void SetKeyRepeat(bool enable)` +* **Requirements:** + * Set an internal framework flag that controls whether repeated SDL key down events trigger repeated `IGame.KeyPressed` callbacks. + * This does not directly call an SDL function to enable/disable OS-level key repeat, but rather controls Night's event dispatching behavior for repeats. +* **Acceptance Criteria:** + * `HasKeyRepeat()` reflects the state set by this method. + * If enabled, holding a key results in multiple `IGame.KeyPressed` events with `isRepeat = true` (after the initial `isRepeat = false`). + * If disabled, holding a key results in only one `IGame.KeyPressed` event (`isRepeat = false`). +* **Test Scenarios/Cases:** + * `SetKeyRepeat_EnableAndVerifyEvent`: Enable, hold key, verify multiple `KeyPressed` events. + * `SetKeyRepeat_DisableAndVerifyEvent`: Disable, hold key, verify single `KeyPressed` event. + * `SetKeyRepeat_ToggleState`: Verify `HasKeyRepeat()` updates correctly. + +#### 9. [ ] `Night.Keyboard.SetTextInput()` +* **Love2D Equivalent:** `love.keyboard.setTextInput(enable)` (also `love.keyboard.setTextInput(enable, x, y, w, h)` for screen keyboard rect) +* **Description:** Enables or disables text input events (`Night.Framework.TextInput`). +* **C# Signature:** `public static void SetTextInput(bool enable)` +* **Requirements:** + * If `enable` is `true`, call `SDL.StartTextInput()`. + * If `enable` is `false`, call `SDL.StopTextInput()`. + * (Future consideration: overload with rectangle for `SDL.SetTextInputRect()`). +* **Acceptance Criteria:** + * `HasTextInput()` reflects the state after calling this method. + * When enabled, `Night.Framework.TextInput` events are generated from user typing. + * When disabled, `Night.Framework.TextInput` events are not generated. +* **Test Scenarios/Cases:** + * `SetTextInput_EnableAndVerifyEvent`: Enable, type text, verify `TextInput` events. + * `SetTextInput_DisableAndVerifyNoEvent`: Disable, type text, verify no `TextInput` events. + * `SetTextInput_ToggleState`: Verify `HasTextInput()` updates correctly. + +--- + +### Callbacks (Events) + +These events would be part of the `Night.IGame` interface or a global event subscription system within `Night.Framework`. +Corresponding `virtual` empty methods should be added to `Night.Game` for any new `IGame` callbacks. + +#### 1. [x] `Night.Framework.KeyPressed` (Event) +* **Love2D Equivalent:** `love.keypressed(key, scancode, isrepeat)` +* **Description:** Called when a key is pressed. +* **C# Delegate/Event Signature (in `IGame`):** `void KeyPressed(Night.KeySymbol key, Night.KeyCode scancode, bool isRepeat);` +* **Implementation Note:** This event is already implemented as per [`project/PRD.md`](project/PRD.md:35). +* **Requirements:** + * Triggered when a key is pressed down. + * `key`: The `Night.KeySymbol` (logical key) that was pressed. + * `scancode`: The `Night.KeyCode` (physical key) that was pressed. + * `isRepeat`: `true` if this is a key repeat event (key was already held down), `false` for the initial press. Behavior controlled by `SetKeyRepeat()`. +* **Acceptance Criteria:** + * Event fires correctly upon key press. + * Parameters `key`, `scancode`, `isRepeat` provide accurate information. + * Repeat behavior respects `SetKeyRepeat()` setting. +* **Test Scenarios/Cases:** + * `KeyPressed_FiresOnPress`: Verify event fires. + * `KeyPressed_CorrectArguments`: Check `key`, `scancode`, `isRepeat` values. + * `KeyPressed_RepeatBehavior`: Test with `SetKeyRepeat(true)` and `SetKeyRepeat(false)`. + +#### 2. [ ] `Night.Framework.KeyReleased` (Event) +* **Love2D Equivalent:** `love.keyreleased(key, scancode)` +* **Description:** Called when a key is released. +* **C# Delegate/Event Signature Idea (in `IGame`):** `void KeyReleased(Night.KeySymbol key, Night.KeyCode scancode);` +* **Requirements:** + * Triggered when a key is released. + * `key`: The `Night.KeySymbol` (logical key) that was released. + * `scancode`: The `Night.KeyCode` (physical key) that was released. +* **Acceptance Criteria:** + * Event fires correctly upon key release. + * Parameters `key`, `scancode` provide accurate information. +* **Test Scenarios/Cases:** + * `KeyReleased_FiresOnRelease`: Verify event fires. + * `KeyReleased_CorrectArguments`: Check `key`, `scancode` values. + * `KeyReleased_AfterHeldKey`: Press, hold, then release a key and verify event. + +#### 3. [ ] `Night.Framework.TextInput` (Event) +* **Love2D Equivalent:** `love.textinput(text)` +* **Description:** Called when text has been input by the user. +* **C# Delegate/Event Signature Idea (in `IGame`):** `void TextInput(string text);` +* **Requirements:** + * Triggered when `SDL.StartTextInput()` is active and the user inputs text. + * `text`: The UTF-8 string of text that was input. +* **Acceptance Criteria:** + * Event fires correctly when text input is enabled and user types. + * Parameter `text` provides the correct input string. + * Does not fire if text input is disabled via `SetTextInput(false)`. +* **Test Scenarios/Cases:** + * `TextInput_FiresOnInput`: Enable text input, type, verify event and text. + * `TextInput_UnicodeCharacters`: Test with various Unicode characters. + * `TextInput_Disabled`: Disable text input, type, verify no event. \ No newline at end of file diff --git a/project/epics/epic_mouse_module.md b/project/epics/epic_mouse_module.md new file mode 100644 index 00000000..301178d6 --- /dev/null +++ b/project/epics/epic_mouse_module.md @@ -0,0 +1,458 @@ +# Epic: Implement Mouse Module (Night.Mouse) + +**User Story:** As a game developer, I want a comprehensive mouse input interface (`Night.Mouse`), so I can effectively manage mouse position, button states, cursor appearance, visibility, and input modes (e.g., relative mode, grabbed mode) within my game, similar to the capabilities offered by Love2D's `love.mouse` module. + +**Overall Requirements:** + +* Implement the `Night.Mouse` static class to provide an interface to the user's mouse. +* All public APIs should reside within the `Night.Mouse` class or related types within the `Night` namespace (e.g., `Night.Mouse.Cursor`, `Night.Mouse.CursorType`). +* The implementation should primarily use SDL3 functions via `SDL3-CS` bindings. +* All functions and types must be documented with XML comments explaining their purpose, parameters, and return values, adhering to [`project/guidelines.md`](project/guidelines.md:1). +* The module code will primarily reside in `src/Night/Mouse/Mouse.cs`. +* Associated types like `Cursor` and `CursorType` will be defined appropriately (e.g., within `Mouse.cs` or `src/Night/Types.cs` if more general, though `CursorType` is specific). +* Mouse-related events (mouse moved, wheel moved) should be integrated into the `Night.IGame` interface or a similar event handling mechanism as established in the project (e.g., like `IGame.KeyPressed`). + +**Overall Acceptance Criteria:** + +* The `Night.Mouse` static class is available and provides all specified functionalities. +* Developers can reliably get mouse position, check button states, manage cursor visibility and appearance, and control mouse grab and relative modes. +* The API is intuitive and follows C# best practices while mirroring Love2D's `love.mouse` module structure. +* Automated tests for each function and callback exist within the `NightTest` framework (likely in `tests/Groups/Mouse/`), verifying correct behavior under various conditions. +* The module integrates seamlessly with the existing `Night.Framework`. + +**Status:** To Do +**Assigned Agent:** AI Dev Agent +**Date Started:** 2025-06-16 +**Date Completed:** TBD + +**Implementation Notes & Log:** + +* 2025-06-16: Task received. Epic drafted for `Night.Mouse` module. + +**Dependencies:** + +* Standard C# libraries. +* `SDL3-CS` bindings for SDL3. +* Existing `Night.Framework` project structure and conventions. +* SDL3 native libraries (for mouse input handling, cursor creation). + +**Questions for User:** + +* None at this time. + +--- + +## Detailed Module Breakdown + +### Types + +#### 1. [ ] `Night.Mouse.Cursor` +* **Love2D Equivalent:** `love.mouse.Cursor` +* **Description:** Represents a hardware cursor. Instances are created via `Night.Mouse.NewCursor` or `Night.Mouse.GetSystemCursor`. +* **C# Definition Idea:** + ```csharp + namespace Night.Mouse + { + public class Cursor : IDisposable + { + // Internal handle to the SDL_Cursor + internal IntPtr SdlCursorHandle { get; private set; } + // Potentially other properties like source ImageData, hotX, hotY if needed for recreation or info + + internal Cursor(IntPtr sdlCursorHandle); + public void Dispose(); // To free the SDL_Cursor + } + } + ``` +* **Requirements:** + * Define a `Cursor` class within the `Night.Mouse` namespace (or `Night` if preferred for types). + * The class should encapsulate an SDL cursor resource (`SDL_Cursor*`). + * Implement `IDisposable` to manage the lifecycle of the native SDL cursor resource. +* **Acceptance Criteria:** + * `Night.Mouse.Cursor` class exists and can be instantiated by `NewCursor` and `GetSystemCursor`. + * `Dispose()` method correctly releases the underlying SDL cursor resource. + * Instances of `Cursor` can be successfully used with `Night.Mouse.SetCursor()`. +* **Test Scenarios/Cases:** + * `Cursor_CreationAndDisposal`: Verify a cursor can be created (via `NewCursor`) and disposed of without errors. + * `Cursor_SetCurrent`: Verify a created `Cursor` object can be set as the current cursor. + +--- + +### Enums + +#### 1. [ ] `Night.Mouse.CursorType` +* **Love2D Equivalent:** `love.mouse.CursorType` +* **Description:** Standard system cursor types. +* **C# Definition Idea:** + ```csharp + namespace Night.Mouse // Or Night.Types + { + public enum CursorType + { + Arrow, + IBeam, + Wait, + Crosshair, + WaitArrow, // Also known as AppStarting in some systems + SizeNWSE, // Diagonal resize 1 (top-left to bottom-right) + SizeNESW, // Diagonal resize 2 (top-right to bottom-left) + SizeWE, // Horizontal resize + SizeNS, // Vertical resize + SizeAll, // Omni-directional resize + No, // Not allowed / No cursor + Hand // Pointing hand + } + } + ``` +* **Requirements:** + * Define a `CursorType` enum. + * Include members corresponding to standard Love2D system cursor types. + * Map these enum values to the appropriate `SDL_SystemCursor` values. +* **Acceptance Criteria:** + * `Night.Mouse.CursorType` enum exists with all specified standard cursor types. + * Each `CursorType` member can be successfully used with `Night.Mouse.GetSystemCursor()`. +* **Test Scenarios/Cases:** + * `CursorType_GetSystemCursors`: Iterate through all `CursorType` values, call `GetSystemCursor` for each, and ensure a non-null `Cursor` object is returned (if supported by the system). + * `CursorType_SetSystemCursors`: For each `CursorType`, get the system cursor and attempt to set it, verifying visibility and appearance if possible (manual check might be needed for appearance). + +--- + +### Functions + +#### 1. [ ] `Night.Mouse.GetCursor()` +* **Love2D Equivalent:** `love.mouse.getCursor()` +* **Description:** Gets the current custom `Cursor` object. Returns `null` if the current cursor is the system cursor or if cursor functionality is not supported. +* **C# Signature:** `public static Night.Mouse.Cursor? GetCursor()` +* **Requirements:** + * Return the currently active custom `Cursor` object set by `SetCursor(Cursor customCursor)`. + * Return `null` if the system cursor is active (i.e., `SetCursor()` was called with `null`, or `SetCursor(systemCursorFromGetSystemCursor)` was called, or no custom cursor has been set). + * Return `null` if `IsCursorSupported()` is false. +* **Acceptance Criteria:** + * Method returns the correct `Cursor` object after `SetCursor(customCursor)` is called. + * Method returns `null` after `SetCursor(null)` is called. + * Method returns `null` if a system cursor (obtained via `GetSystemCursor`) is set. + * Method returns `null` by default before any custom cursor is set. +* **Test Scenarios/Cases:** + * `GetCursor_DefaultIsNull`: Verify returns `null` initially. + * `GetCursor_AfterSetCustomCursor`: Verify returns the set custom cursor. + * `GetCursor_AfterSetSystemCursorIsNull`: Verify returns `null` after setting a system cursor. + * `GetCursor_AfterSetNullCursorIsNull`: Verify returns `null` after `SetCursor(null)`. + +#### 2. [x] `Night.Mouse.GetPosition()` +* **Love2D Equivalent:** `love.mouse.getPosition()` +* **Description:** Returns the current position of the mouse in window coordinates. +* **C# Signature:** `public static (float X, float Y) GetPosition()` +* **Requirements:** + * Return the current x and y coordinates of the mouse cursor relative to the window's client area. + * Coordinates should be floating-point numbers. +* **Acceptance Criteria:** + * Method returns accurate x and y coordinates of the mouse. + * Coordinates update correctly as the mouse moves. + * If the mouse is outside the window, behavior should align with SDL (typically clamps or gives last known position if not grabbed). +* **Test Scenarios/Cases:** + * `GetPosition_Initial`: Check initial position (might be 0,0 or last known). + * `GetPosition_AfterMove`: Simulate mouse move (if possible in test env) or check after manual move. + * `GetPosition_AfterSetPosition`: Verify returns coordinates set by `SetPosition`. + * `GetPosition_RelativeToWindow`: Ensure coordinates are relative to the game window. + +#### 3. [ ] `Night.Mouse.GetRelativeMode()` +* **Love2D Equivalent:** `love.mouse.getRelativeMode()` +* **Description:** Gets whether relative mode is enabled for the mouse. +* **C# Signature:** `public static bool GetRelativeMode()` +* **Requirements:** + * Return `true` if relative mouse mode is enabled, `false` otherwise. +* **Acceptance Criteria:** + * Method returns `false` by default. + * Method returns `true` after `SetRelativeMode(true)` is called. + * Method returns `false` after `SetRelativeMode(false)` is called. +* **Test Scenarios/Cases:** + * `GetRelativeMode_DefaultIsFalse`: Verify returns `false` initially. + * `GetRelativeMode_AfterSetTrue`: Verify returns `true` after enabling. + * `GetRelativeMode_AfterSetFalse`: Verify returns `false` after disabling. + +#### 4. [ ] `Night.Mouse.GetSystemCursor()` +* **Love2D Equivalent:** `love.mouse.getSystemCursor(cursortype)` +* **Description:** Gets a `Cursor` object representing a system-native hardware cursor. +* **C# Signature:** `public static Night.Mouse.Cursor GetSystemCursor(Night.Mouse.CursorType cursorType)` +* **Requirements:** + * Create and return a `Cursor` object for the specified system `CursorType`. + * The returned `Cursor` can be used with `SetCursor()`. + * Handle cases where a specific system cursor might not be available (SDL might return a default). +* **Acceptance Criteria:** + * Method returns a non-null `Cursor` object for valid `CursorType` values. + * The returned `Cursor` can be successfully used with `SetCursor()`. +* **Test Scenarios/Cases:** + * `GetSystemCursor_AllTypes`: For each `CursorType`, get the cursor and verify it's not null. + * `GetSystemCursor_SetAndVerify`: Get a system cursor, set it, and (manually or programmatically if possible) verify the cursor changes. + +#### 5. [ ] `Night.Mouse.GetX()` +* **Love2D Equivalent:** `love.mouse.getX()` +* **Description:** Returns the current x-position of the mouse. +* **C# Signature:** `public static float GetX()` +* **Requirements:** + * Return the x-coordinate of `GetPosition()`. +* **Acceptance Criteria:** + * Method returns the same x-coordinate as `GetPosition().X`. +* **Test Scenarios/Cases:** + * `GetX_MatchesGetPosition`: Verify `GetX()` equals `GetPosition().X`. + * `GetX_AfterSetPosition`: Verify `GetX()` after `SetPosition`. + +#### 6. [ ] `Night.Mouse.GetY()` +* **Love2D Equivalent:** `love.mouse.getY()` +* **Description:** Returns the current y-position of the mouse. +* **C# Signature:** `public static float GetY()` +* **Requirements:** + * Return the y-coordinate of `GetPosition()`. +* **Acceptance Criteria:** + * Method returns the same y-coordinate as `GetPosition().Y`. +* **Test Scenarios/Cases:** + * `GetY_MatchesGetPosition`: Verify `GetY()` equals `GetPosition().Y`. + * `GetY_AfterSetPosition`: Verify `GetY()` after `SetPosition`. + +#### 7. [ ] `Night.Mouse.IsCursorSupported()` +* **Love2D Equivalent:** `love.mouse.isCursorSupported()` (Added since 11.0) +* **Description:** Gets whether custom cursor functionality is supported by the system. +* **C# Signature:** `public static bool IsCursorSupported()` +* **Requirements:** + * Return `true` if the system can create and set custom hardware cursors. + * Return `false` otherwise (e.g., on platforms without such support or if SDL fails to initialize cursor system). +* **Acceptance Criteria:** + * Method returns a boolean indicating system support for custom cursors. + * If `false`, `NewCursor` might fail or `SetCursor` with a custom cursor might not work as expected. +* **Test Scenarios/Cases:** + * `IsCursorSupported_ReturnsBool`: Verify the method returns a boolean value. (Actual value depends on test environment). + * `IsCursorSupported_BehaviorOfNewCursor`: If `false`, test behavior of `NewCursor` (e.g., throws exception or returns null). + +#### 8. [x] `Night.Mouse.IsDown()` +* **Love2D Equivalent:** `love.mouse.isDown(button, ...)` +* **Description:** Checks whether a certain mouse button is currently pressed. +* **C# Signature:** `public static bool IsDown(Night.MouseButton button)` (Assuming `Night.MouseButton` enum exists from `Types.cs` for left, right, middle, x1, x2, etc.) +* **Requirements:** + * Return `true` if the specified `button` is currently held down, `false` otherwise. + * Support multiple buttons (left, right, middle, and extended buttons if available). +* **Acceptance Criteria:** + * Method returns `true` only when the specified button is pressed. + * Method returns `false` when the button is not pressed. + * Correctly identifies different mouse buttons. +* **Test Scenarios/Cases:** + * `IsDown_NotPressed`: Verify returns `false` for all buttons when none are pressed. + * `IsDown_LeftButtonPressed`: Verify returns `true` for left button when pressed, `false` for others. + * `IsDown_RightButtonPressed`: Verify returns `true` for right button when pressed. + * `IsDown_MiddleButtonPressed`: Verify returns `true` for middle button when pressed. + * `IsDown_MultipleButtons`: (If Love2D supports checking multiple, adapt. Signature implies one button at a time). + +#### 9. [ ] `Night.Mouse.IsGrabbed()` +* **Love2D Equivalent:** `love.mouse.isGrabbed()` +* **Description:** Checks if the mouse is grabbed. +* **C# Signature:** `public static bool IsGrabbed()` +* **Requirements:** + * Return `true` if the mouse is currently grabbed (confined to the window), `false` otherwise. +* **Acceptance Criteria:** + * Method returns `false` by default. + * Method returns `true` after `SetGrabbed(true)` is called. + * Method returns `false` after `SetGrabbed(false)` is called. +* **Test Scenarios/Cases:** + * `IsGrabbed_DefaultIsFalse`: Verify returns `false` initially. + * `IsGrabbed_AfterSetTrue`: Verify returns `true` after enabling grab. + * `IsGrabbed_AfterSetFalse`: Verify returns `false` after disabling grab. + +#### 10. [ ] `Night.Mouse.IsVisible()` +* **Love2D Equivalent:** `love.mouse.isVisible()` +* **Description:** Checks if the cursor is visible. +* **C# Signature:** `public static bool IsVisible()` +* **Requirements:** + * Return `true` if the mouse cursor is currently visible, `false` otherwise. +* **Acceptance Criteria:** + * Method returns `true` by default (or based on SDL's default). + * Method returns `false` after `SetVisible(false)` is called. + * Method returns `true` after `SetVisible(true)` is called. +* **Test Scenarios/Cases:** + * `IsVisible_Default`: Verify initial visibility state. + * `IsVisible_AfterSetFalse`: Verify returns `false` after hiding. + * `IsVisible_AfterSetTrue`: Verify returns `true` after showing. + +#### 11. [ ] `Night.Mouse.NewCursor()` +* **Love2D Equivalent:** `love.mouse.newCursor(imageData, hotx, hoty)` +* **Description:** Creates a new hardware `Cursor` object from image data. +* **C# Signature:** `public static Night.Mouse.Cursor? NewCursor(Night.Graphics.ImageData imageData, int hotX, int hotY)` (Assuming `Night.Graphics.ImageData` exists) +* **Requirements:** + * Create a custom hardware cursor from the provided `ImageData`. + * `hotX` and `hotY` define the cursor's hot spot (the point of the cursor that interacts). + * Return the new `Cursor` object. + * Return `null` or throw an exception if cursor creation fails (e.g., `IsCursorSupported()` is false, invalid image data, system limits). +* **Acceptance Criteria:** + * Method returns a valid `Cursor` object when given valid `ImageData`, `hotX`, and `hotY`. + * The created cursor can be used with `SetCursor()`. + * Method handles failure cases gracefully (returns `null` or throws documented exception). + * Hotspot is correctly applied to the created cursor. +* **Test Scenarios/Cases:** + * `NewCursor_ValidData`: Create cursor with valid image and hotspot, verify non-null `Cursor`. + * `NewCursor_InvalidData`: Test with invalid `ImageData` (e.g., null, unsupported format if applicable). + * `NewCursor_Hotspot`: Verify hotspot functionality (might require visual check or specific SDL query if available). + * `NewCursor_WhenNotSupported`: Test behavior if `IsCursorSupported()` is `false`. + +#### 12. [ ] `Night.Mouse.SetCursor()` +* **Love2D Equivalent:** `love.mouse.setCursor(cursor)` or `love.mouse.setCursor()` +* **Description:** Sets the current mouse cursor. +* **C# Signature:** `public static void SetCursor(Night.Mouse.Cursor? cursor)` +* **Requirements:** + * Set the active mouse cursor to the given `Cursor` object. + * If `cursor` is `null`, set the system's default arrow cursor. +* **Acceptance Criteria:** + * Calling with a custom `Cursor` changes the mouse appearance. + * Calling with a `Cursor` obtained from `GetSystemCursor` changes to that system cursor. + * Calling with `null` resets to the default system arrow cursor. + * `GetCursor()` reflects the change (returns the custom cursor or `null`). +* **Test Scenarios/Cases:** + * `SetCursor_Custom`: Set a custom cursor and verify (visual or via `GetCursor`). + * `SetCursor_System`: Set a system cursor and verify. + * `SetCursor_Null`: Set `null` cursor and verify (visual and `GetCursor` returns `null`). + * `SetCursor_InvalidCursorObject`: (Optional) Test with a disposed or invalid cursor object. + +#### 13. [x] `Night.Mouse.SetGrabbed()` +* **Love2D Equivalent:** `love.mouse.setGrabbed(grab)` +* **Description:** Grabs the mouse and confines it to the window. +* **C# Signature:** `public static void SetGrabbed(bool grabbed)` +* **Requirements:** + * If `grabbed` is `true`, confine the mouse cursor to the window boundaries. + * If `grabbed` is `false`, release the mouse cursor. +* **Acceptance Criteria:** + * When `true`, mouse cannot leave the window. + * When `false`, mouse can move freely. + * `IsGrabbed()` reflects the current state. +* **Test Scenarios/Cases:** + * `SetGrabbed_Enable`: Enable grab, verify `IsGrabbed` is true, and (manually) test confinement. + * `SetGrabbed_Disable`: Disable grab, verify `IsGrabbed` is false, and (manually) test freedom. + +#### 14. [ ] `Night.Mouse.SetPosition()` +* **Love2D Equivalent:** `love.mouse.setPosition(x, y)` +* **Description:** Sets the current position of the mouse. +* **C# Signature:** `public static void SetPosition(float x, float y)` +* **Requirements:** + * Move the mouse cursor to the specified `x` and `y` coordinates within the window. + * Coordinates are relative to the window's client area. +* **Acceptance Criteria:** + * `GetPosition()` returns the new coordinates after calling this method. + * The visible mouse cursor moves to the specified position. + * Behavior if coordinates are outside window bounds should match SDL (e.g., clamped). +* **Test Scenarios/Cases:** + * `SetPosition_InsideWindow`: Set position within bounds, verify with `GetPosition`. + * `SetPosition_OutsideWindow`: Set position outside bounds, verify behavior with `GetPosition`. + * `SetPosition_VerifyVisual`: (Manual) Visually confirm cursor movement. + +#### 15. [x] `Night.Mouse.SetRelativeMode()` +* **Love2D Equivalent:** `love.mouse.setRelativeMode(enable)` +* **Description:** Sets whether relative mode is enabled for the mouse. In relative mode, the cursor is hidden, and mouse motion events report relative changes (dx, dy) rather than absolute positions. Useful for FPS controls. +* **C# Signature:** `public static void SetRelativeMode(bool enable)` +* **Requirements:** + * If `enable` is `true`, enable relative mouse mode. Cursor typically becomes hidden, and `MouseMoved` events provide delta movements. + * If `enable` is `false`, disable relative mouse mode. Cursor typically becomes visible, and `MouseMoved` events provide absolute positions. +* **Acceptance Criteria:** + * `GetRelativeMode()` reflects the current state. + * When `true`, cursor is hidden (or behavior defined by SDL). + * When `true`, `MouseMoved` event arguments `dx`, `dy` report relative motion. + * When `false`, cursor visibility is restored (if previously hidden by relative mode). +* **Test Scenarios/Cases:** + * `SetRelativeMode_Enable`: Enable, verify `GetRelativeMode` is true, check cursor visibility, check `MouseMoved` event args. + * `SetRelativeMode_Disable`: Disable, verify `GetRelativeMode` is false, check cursor visibility, check `MouseMoved` event args. + +#### 16. [x] `Night.Mouse.SetVisible()` +* **Love2D Equivalent:** `love.mouse.setVisible(visible)` +* **Description:** Sets the current visibility of the cursor. +* **C# Signature:** `public static void SetVisible(bool visible)` +* **Requirements:** + * If `visible` is `true`, show the mouse cursor. + * If `visible` is `false`, hide the mouse cursor. +* **Acceptance Criteria:** + * `IsVisible()` reflects the current state. + * The mouse cursor's visibility changes accordingly. +* **Test Scenarios/Cases:** + * `SetVisible_False`: Hide cursor, verify `IsVisible` is false, (manual) check visual. + * `SetVisible_True`: Show cursor, verify `IsVisible` is true, (manual) check visual. + * `SetVisible_InteractionWithRelativeMode`: Test visibility changes when relative mode is active/inactive. + +#### 17. [ ] `Night.Mouse.SetX()` +* **Love2D Equivalent:** `love.mouse.setX(x)` +* **Description:** Sets the current X position of the mouse, keeping Y the same. +* **C# Signature:** `public static void SetX(float x)` +* **Requirements:** + * Set the mouse cursor's x-position to `x`, while maintaining its current y-position. +* **Acceptance Criteria:** + * `GetPosition().X` (or `GetX()`) returns the new `x` value. + * `GetPosition().Y` (or `GetY()`) remains unchanged. + * Visible cursor moves accordingly. +* **Test Scenarios/Cases:** + * `SetX_VerifyXAndY`: Set X, then use `GetPosition` to verify new X and old Y. + * `SetX_VisualConfirmation`: (Manual) Visually confirm cursor movement. + +#### 18. [ ] `Night.Mouse.SetY()` +* **Love2D Equivalent:** `love.mouse.setY(y)` +* **Description:** Sets the current Y position of the mouse, keeping X the same. +* **C# Signature:** `public static void SetY(float y)` +* **Requirements:** + * Set the mouse cursor's y-position to `y`, while maintaining its current x-position. +* **Acceptance Criteria:** + * `GetPosition().Y` (or `GetY()`) returns the new `y` value. + * `GetPosition().X` (or `GetX()`) remains unchanged. + * Visible cursor moves accordingly. +* **Test Scenarios/Cases:** + * `SetY_VerifyXAndY`: Set Y, then use `GetPosition` to verify new Y and old X. + * `SetY_VisualConfirmation`: (Manual) Visually confirm cursor movement. + +--- + +### Callbacks (Events) + +These events would likely be part of the `Night.IGame` interface or a global event subscription system within `Night.Framework`. + +#### 1. [ ] `Night.Framework.MouseMoved` (Event) +* **Love2D Equivalent:** `love.mousemoved(x, y, dx, dy, istouch)` +* **Description:** Called when the mouse is moved. +* **C# Delegate/Event Signature Idea (in `IGame` or similar):** + ```csharp + // In IGame interface: + // void MouseMoved(float x, float y, float dx, float dy, bool isTouch); + + // Or as a static event in Night.Framework or Night.Mouse: + // public static event Action MouseMoved; + ``` + (Note: `isTouch` indicates if the event is from a touch input emulating a mouse. SDL provides this.) +* **Requirements:** + * The event should be triggered whenever the mouse cursor moves. + * `x`, `y`: Absolute current position of the mouse. + * `dx`, `dy`: Change in position since the last frame/event. In relative mode, `x` and `y` might be deltas too, or `dx, dy` are the primary values. Clarify SDL behavior for relative mode. + * `isTouch`: Boolean indicating if the event originated from a touch device. +* **Acceptance Criteria:** + * Event fires correctly upon mouse movement. + * Parameters `x, y, dx, dy, isTouch` provide accurate information. + * Behavior in normal mode vs. relative mode is correct for the parameters. +* **Test Scenarios/Cases:** + * `MouseMoved_FiresOnMove`: Verify event fires when mouse is moved. + * `MouseMoved_CorrectArguments_AbsoluteMode`: Check `x, y, dx, dy` values in absolute mode. + * `MouseMoved_CorrectArguments_RelativeMode`: Check `x, y, dx, dy` values in relative mode (dx, dy should be key). + * `MouseMoved_IsTouchParameter`: Test with simulated touch input if possible. + +#### 2. [ ] `Night.Framework.MouseWheelMoved` (Event) +* **Love2D Equivalent:** `love.wheelmoved(x, y)` (Note: Love2D's x,y are dx, dy for wheel) +* **Description:** Called when the mouse wheel is scrolled. +* **C# Delegate/Event Signature Idea (in `IGame` or similar):** + ```csharp + // In IGame interface: + // void MouseWheelMoved(float dx, float dy); // SDL provides float values for precise scrolling + + // Or as a static event: + // public static event Action MouseWheelMoved; + ``` +* **Requirements:** + * The event should be triggered when the mouse wheel is scrolled. + * `dx`: Amount scrolled horizontally (positive for right, negative for left). + * `dy`: Amount scrolled vertically (positive for away from user/up, negative for towards user/down). +* **Acceptance Criteria:** + * Event fires correctly upon mouse wheel movement. + * Parameters `dx, dy` provide accurate scroll direction and magnitude. +* **Test Scenarios/Cases:** + * `MouseWheelMoved_FiresOnScroll`: Verify event fires. + * `MouseWheelMoved_VerticalScroll_Up`: Check `dy` is positive. + * `MouseWheelMoved_VerticalScroll_Down`: Check `dy` is negative. + * `MouseWheelMoved_HorizontalScroll_Left`: Check `dx` is negative (if mouse supports it). + * `MouseWheelMoved_HorizontalScroll_Right`: Check `dx` is positive (if mouse supports it). \ No newline at end of file diff --git a/project/epics/release.md b/project/epics/release.md deleted file mode 100644 index 889683e4..00000000 --- a/project/epics/release.md +++ /dev/null @@ -1,268 +0,0 @@ -Status: In-Progress - -GitHub Actions Release Plan for Night Engine (Night.dll) - Compiled Version Info - -This document outlines the implementation plan for a robust, manually triggerable GitHub Actions-based release process for the Night C# library, focusing on creating GitHub Releases. The version information will be compiled directly into the library. -1. Overview - -The goal is to automate the versioning, building, testing, and packaging of the Night.dll library, culminating in a GitHub Release with the generated packages as assets. The process will be initiated manually via workflow_dispatch, allowing the user to specify the exact Semantic Version for the release. The library will contain a `VersionInfo.cs` file where the Semantic Version is updated by the GitHub Action, and a manually editable `CodeName` is stored. - -Key Information from Repository Digest: - - Solution File: Night.sln (located at the repository root) - Main Library Project: src/Night/Night.csproj (this is the project to be versioned and packaged as Night.dll) - Test Project: tests/Night.Tests/Night.Tests.csproj - Target Framework: net9.0 - .NET SDK Version: 9.0.x (aligning with existing CI) - Default Branch: main - Root Namespace for Library: Night - -2. Prerequisites and Initial Setup - -Before implementing the release workflow, ensure the following are in place: - - .NET SDK: - Ensure your development environment and GitHub Actions runners have access to .NET SDK version 9.0.x. The workflow will use actions/setup-dotnet to configure this. - GitHub CLI (gh): - The GitHub CLI is used for creating GitHub Releases. It's typically available on GitHub-hosted runners. - GitHub Actions Workflow Permissions: - The workflow will need permissions to write to the repository (for committing .csproj and .cs changes, creating tags, and creating GitHub releases). The following permissions block should be included in the workflow file: - - permissions: - contents: write - -3. GitHub Actions Workflow File (`.github/workflows/release.yml`) - -Create/update the file with the following content: - -```yaml -name: Release Night Library (GitHub Release) - -on: - workflow_dispatch: - inputs: - version: - description: 'Semantic Version for the release (e.g., 1.0.0, 1.0.0-beta.1). This is the pure SemVer.' - required: true - type: string - -permissions: - contents: write # To create commits, tags, and releases - -jobs: - release: - name: Build and Create GitHub Release for Night Library - runs-on: ubuntu-latest - env: - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - DOTNET_CLI_TELEMETRY_OPTOUT: true - SOLUTION_FILE_PATH: Night.sln - MAIN_PROJECT_FILE_PATH: src/Night/Night.csproj - VERSION_INFO_FILE_PATH: src/Night/VersionInfo.cs # Path to the version C# file - TEST_PROJECT_FILE_PATH: tests/Night.Tests/Night.Tests.csproj - PACKAGE_OUTPUT_DIR: ./packages - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Required to analyze history - - - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '9.0.x' - - - name: Validate Version Input (SemVer) - run: | - version_input="${{ github.event.inputs.version }}" - semver_regex="^([0-9]+)\\.([0-9]+)\\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$" - if [[ ! "$version_input" =~ $semver_regex ]]; then - echo "Error: Invalid version format. Input must be a pure Semantic Version (e.g., 1.0.0, 1.2.3-beta.1)." - exit 1 - fi - echo "SemVer input '$version_input' is valid." - shell: bash - - - name: Update Version in .csproj - id: update_version_csproj - run: | - $newSemVer = "${{ github.event.inputs.version }}" - $projectFilePath = "${{ env.MAIN_PROJECT_FILE_PATH }}" - Write-Host "Attempting to update in '$projectFilePath' to '$newSemVer'" - [xml]$csproj = Get-Content -Path $projectFilePath -Raw - $versionNode = $csproj.SelectSingleNode("//PropertyGroup/Version") - if (-not $versionNode) { - $propertyGroupNode = $csproj.SelectSingleNode("//PropertyGroup") - if (-not $propertyGroupNode) { - $propertyGroupNode = $csproj.CreateElement("PropertyGroup") - $csproj.Project.AppendChild($propertyGroupNode) | Out-Null - } - $versionNode = $csproj.CreateElement("Version") - $propertyGroupNode.AppendChild($versionNode) | Out-Null - } - $versionNode.'#text' = $newSemVer - $csproj.Save($projectFilePath) - Write-Host "Saved $newSemVer to '$projectFilePath'" - echo "version_tag=v$newSemVer" >> $GITHUB_OUTPUT - shell: pwsh - - - name: Update Version in VersionInfo.cs - run: | - $newSemVer = "${{ github.event.inputs.version }}" - $versionInfoFilePath = "${{ env.VERSION_INFO_FILE_PATH }}" - Write-Host "Attempting to update Version constant in '$versionInfoFilePath' to '$newSemVer'" - $content = Get-Content $versionInfoFilePath -Raw - # Regex to find 'public const string Version = ".*";' and replace the version string part - $updatedContent = $content -replace '(?<=public const string Version = ")([^"]*)(?=";)', $newSemVer - Set-Content -Path $versionInfoFilePath -Value $updatedContent - Write-Host "Updated Version constant in '$versionInfoFilePath'" - shell: pwsh - - - name: Commit Version Changes - run: | - git config --global user.name "${{ github.actor }}" - git config --global user.email "${{ github.actor }}@users.noreply.github.com" - git add "${{ env.MAIN_PROJECT_FILE_PATH }}" # .csproj - git add "${{ env.VERSION_INFO_FILE_PATH }}" # VersionInfo.cs - git commit -m "Update version to ${{ github.event.inputs.version }} [skip ci]" - echo "Committed version updates for ${{ github.event.inputs.version }}" - shell: bash - - - name: Create Git Tag - run: | - git tag "${{ steps.update_version_csproj.outputs.version_tag }}" - echo "Created git tag ${{ steps.update_version_csproj.outputs.version_tag }}" - shell: bash - - - name: Push Commit and Tag - run: | - git push origin HEAD:main --follow-tags - echo "Pushed commit and tag to remote." - shell: bash - - - name: Build Solution - run: dotnet build "${{ env.SOLUTION_FILE_PATH }}" -c Release /p:Version="${{ github.event.inputs.version }}" - - - name: Run Tests - run: dotnet test "${{ env.SOLUTION_FILE_PATH }}" --no-build -c Release - - - name: Create Package Output Directory - run: mkdir -p "${{ env.PACKAGE_OUTPUT_DIR }}" - - - name: Package Library - run: | - dotnet pack "${{ env.MAIN_PROJECT_FILE_PATH }}" ` - --no-build ` - -c Release ` - -o "${{ env.PACKAGE_OUTPUT_DIR }}" ` - /p:Version="${{ github.event.inputs.version }}" ` - /p:IncludeSymbols=true ` - /p:SymbolPackageFormat=snupkg - shell: pwsh - - - name: List Packaged Files - run: ls -R "${{ env.PACKAGE_OUTPUT_DIR }}" - shell: bash - - - name: Create GitHub Release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION_TAG: ${{ steps.update_version_csproj.outputs.version_tag }} - RELEASE_VERSION: ${{ github.event.inputs.version }} - run: | - gh release create "$VERSION_TAG" \ - "${{ env.PACKAGE_OUTPUT_DIR }}"/*.nupkg \ - "${{ env.PACKAGE_OUTPUT_DIR }}"/*.snupkg \ - --title "Release $RELEASE_VERSION" \ - --notes "Night Engine Release $RELEASE_VERSION" \ - --draft=false \ - --prerelease=$([[ "$RELEASE_VERSION" == *-* ]] && echo true || echo false) - shell: bash -``` - -4. C# Library Version Information (`src/Night/VersionInfo.cs`) - -The library's version information will be stored and retrieved from a compiled C# file. - - Create `src/Night/VersionInfo.cs`: - This file will contain the version information. The `Version` constant will be updated by the GitHub Actions workflow. The `CodeName` constant is for manual developer updates. - - ```csharp - // In src/Night/VersionInfo.cs - namespace Night - { - public static class VersionInfo - { - // This SemVer value is updated by the GitHub Action (e.g., "1.0.0", "1.2.3-beta.1") - // It is used for AssemblyInformationalVersion and runtime GetVersion() - public const string Version = "0.0.0-dev"; // Initial placeholder - - // This value is manually updated by the developer during development cycles. - // It is not automatically used in the release title or notes by default. - public const string CodeName = "Initial Development"; // Manual placeholder - - /// - /// Gets the Semantic Version of the Night library. - /// This version is set during the release process. - /// - /// The library's semantic version string. - public static string GetVersion() - { - return Version; - } - - // Example: If you want a way to get codename, you can add a method like this: - // public static string GetCodeName() - // { - // return CodeName; - // } - // - // public static string GetFullVersionDisplay() - // { - // return $"{Version} ('{CodeName}')"; - // } - } - } - ``` - - Update `.csproj` File (`src/Night/Night.csproj`): - Ensure `VERSION.txt` is NO LONGER included if it was previously. The `VersionInfo.cs` file is compiled by default as a `.cs` file within the project. No specific `` tag is needed for it. The `` tag in the `.csproj` will still be updated by the workflow, which influences `AssemblyVersion`, `FileVersion`, and `AssemblyInformationalVersion` if not otherwise specified. The `AssemblyInformationalVersion` will effectively be the same as `VersionInfo.Version` after the workflow runs. - -5. Step-by-Step Implementation Instructions for You (Agent) - - Update Workflow File: - Create or update the `.github/workflows/release.yml` file with the YAML content from Section 3. - - Create `src/Night/VersionInfo.cs`: - Create the `src/Night/VersionInfo.cs` file with the C# code from Section 4. Commit with the initial placeholders. - - Delete `src/Night/VERSION.txt` (if it exists): - Ensure this file is removed from the `src/Night/` directory and from source control if previously committed. - - Update `src/Night/Night.csproj`: - If the `...` item group exists from a previous plan, remove it. The workflow updates `` in the .csproj directly. - - Commit and Push Changes: - Commit all changes: - `git add .github/workflows/release.yml src/Night/VersionInfo.cs src/Night/Night.csproj` - (If `VERSION.txt` was tracked, also `git rm src/Night/VERSION.txt`) - `git commit -m "refactor: Implement compiled versioning with VersionInfo.cs and update release workflow"` - `git push origin main` - -6. User Guide for Running the Release Workflow - - Navigate to Actions in your GitHub repository. - Select the "Release Night Library (GitHub Release)" workflow. - Click `Run workflow`. - Enter the pure Semantic Version (e.g., 1.0.0, 0.2.0-beta.1) in the input field. This version will be used for Git tagging, .csproj updates, `VersionInfo.cs` updates, and the GitHub release title. - Click `Run workflow`. - -7. Tailored Considerations and Best Practices - - `AssemblyInformationalVersion`: The `` tag set in the `.csproj` by the workflow directly influences the `AssemblyInformationalVersionAttribute` during the build. The `VersionInfo.GetVersion()` method will return the same SemVer string that is also effectively in `AssemblyInformationalVersionAttribute`. - `CodeName` Usage: The `CodeName` constant in `VersionInfo.cs` is for your internal tracking and development. It is not automatically included in release artifacts or titles by this workflow. You can manually include it in `CHANGELOG.md` or release notes if desired. - Branching Strategy: Unchanged. All operations target the `main` branch by default in the workflow. - Testing the Workflow: Test with pre-release SemVer strings. - -This revised plan focuses on a compiled-in version string, updated by the GitHub Action, and includes a manual codename field, removing the need for an external `VERSION.txt`. diff --git a/project/guidelines.md b/project/guidelines.md new file mode 100644 index 00000000..3a5b98fc --- /dev/null +++ b/project/guidelines.md @@ -0,0 +1,181 @@ +# Project Guidelines + +The "Night" engine project will ALWAYS adhere to the **Google C# Style Guide**. Key aspects of this guide, supplemented by project-specific interpretations, are outlined below. + +- **Formatting & Style:** + - **Indentation:** 2 spaces, no tabs. + - **Column Limit:** 100 characters. + - **Whitespace, Braces, Line Wrapping:** Adhere to the detailed rules in the Google C# Style Guide. This includes rules like no line break before an opening brace, and braces used even when optional. + - Format the `using` directives with specific spacing. Place all System.* directives first, followed by a blank line. Then, group other using directives (like third-party libraries or project-specific namespaces) logically, and insert a blank line between each distinct group. For example, list System usings, then a blank line, then Night usings, then a blank line, then SDL3 usings, rather than listing them all contiguously. + -`using` directives should NEVER have any comments associated with them or on the same line +- **Naming Conventions:** + - **General Rules Summary:** + - Names of classes, methods, enumerations, public fields, public properties, namespaces: `PascalCase`. + - Names of local variables, parameters: `camelCase`. + - Names of private, protected, internal, and protected internal fields and properties: `_camelCase` (e.g., `_privateField`). + - Naming convention is unaffected by modifiers such as `const`, `static`, `readonly`, etc.. + - For casing, a “word” is anything written without internal spaces, including acronyms (e.g., `MyRpc` not `MyRPC`). + - Names of interfaces start with `I` (e.g., `IInterface`). + - Filenames and directory names are `PascalCase` (e.g., `MyFile.cs`). +- **Code Organization:** + - **Modifier Order:** `public protected internal private new abstract virtual override sealed static readonly extern unsafe volatile async`. + - **Namespace `using` Declarations:** Place at the top of the file, before any namespace declarations. Order alphabetically, with `System` imports always first. + - **Class Member Ordering:** Follow the prescribed order: Nested types, static/const/readonly fields, instance fields/properties, constructors/finalizers, methods. Within each group, elements are ordered by access: Public, Internal, Protected internal, Protected, Private. +- **Key Principles (Project-Specific additions and emphasis):** + - **API Design (Night framework and Night Engine):** Strive for an API design that is idiomatic to C# while closely mirroring the spirit, structure, and ease of use of the Love2D API for the features being implemented. + - **Clarity over Premature Optimization:** For the prototype, prioritize clear, understandable, and maintainable code. + +- **Logging:** Utilize the `Night.Log.LogManager` to obtain logger instances (e.g., `LogManager.GetLogger("MyCategory")`). + Use appropriate log levels (`Info`, `Debug`, `Warn`, `Error`, `Fatal`) for messages. + Refer to `project/epics/logger-tasks.md` for sink configuration and advanced usage. + +## NightTest Framework Testing Guidelines + +This section outlines how to write tests for the `Night` framework using the `NightTest` project. The testing approach combines xUnit for test execution and orchestration with custom `IGame` implementations for the actual test logic. + +**1. Core Concepts & Directory Structure:** + +- **xUnit Test Classes (Test Groups):** + - These classes are responsible for running one or more `IGame` test cases. + - They reside in subdirectories under [`tests/Groups/`](tests/Groups/) (e.g., [`tests/Groups/Timer/TimerGroup.cs`](tests/Groups/Timer/TimerGroup.cs), [`tests/Groups/Graphics/GraphicsGroup.cs`](tests/Groups/Graphics/GraphicsGroup.cs)). + - They **must** inherit from [`NightTest.Core.TestGroup`](tests/Core/TestGroup.cs). + - They use xUnit's `[Fact]` attribute to define individual test methods. Each `[Fact]` method typically runs one specific `IGame` test case. +- **`IGame` Test Case Classes:** + - These classes contain the actual test logic and implement `Night.IGame` and [`NightTest.Core.ITestCase`](tests/Core/ITestCase.cs). + - They **must** inherit from [`NightTest.Core.GameTestCase`](tests/Core/GameTestCase.cs) or [`NightTest.Core.ManualTestCase`](tests/Core/ManualTestCase.cs). + - They are typically located alongside their corresponding xUnit test class or in a related file (e.g., `TimerTests.cs` contains multiple `IGame` test cases like `GetTimeTest`, run by `TimerGroup.cs`). +- **Core Infrastructure:** + - [`tests/Core/`](tests/Core/): Contains base classes and core types for the testing framework. + - [`ITestCase.cs`](tests/Core/ITestCase.cs): Interface defining metadata for an `IGame` test case. + - [`GameTestCase.cs`](tests/Core/GameTestCase.cs): Base class for automated `IGame` test cases, providing common functionality (stopwatch, status tracking, completion helpers). + - [`ManualTestCase.cs`](tests/Core/ManualTestCase.cs): Base class for manual `IGame` test cases, extending `GameTestCase` with UI for manual pass/fail confirmation. + - [`TestGroup.cs`](tests/Core/TestGroup.cs): Base class for xUnit test classes, providing the `Run_GameTestCase` helper method. + - [`TestTypes.cs`](tests/Core/TestTypes.cs): Enums for `TestType` (Automated, Manual) and `TestStatus` (NotRun, Passed, Failed, Skipped). + +**2. Workflow for Adding New Tests:** + +**Step 1: Create the `IGame` Test Case Class** + +For each specific feature or function you want to test (e.g., a new `Night.Graphics` method): + +- **Location:** Create the class in a relevant file within a subdirectory of `tests/Groups/` (e.g., for a new graphics test, it might go in `tests/Groups/Graphics/NewGraphicsFeatureTest.cs` or be added to an existing file like `GraphicsTests.cs` if it contains multiple small test case classes). +- **Inheritance:** + - For automated tests: Inherit from [`NightTest.Core.GameTestCase`](tests/Core/GameTestCase.cs). + - For tests requiring manual user confirmation: Inherit from [`NightTest.Core.ManualTestCase`](tests/Core/ManualTestCase.cs). +- **Implement `ITestCase` Properties (Abstract in `GameTestCase`):** + - `public override string Name { get; }`: Provide a unique, descriptive name (e.g., `"Graphics.DrawSpriteAlpha"`). + - `public override string Description { get; }`: Describe what the test does. + - The `Type` property is automatically set to `TestType.Automated` by `GameTestCase` or `TestType.Manual` by `ManualTestCase`. +- **Implement `IGame` Logic (Override methods from `GameTestCase`):** + - `protected override void Load()`: Initialize resources, set up initial state for your test. + - `protected override void Update(double deltaTime)`: Implement the core test logic. + - Use helper methods from `GameTestCase` like `CheckCompletionAfterDuration()` or `CheckCompletionAfterFrames()` to define pass/fail conditions and automatically set `CurrentStatus`, `Details`, and call `EndTest()`. + - For manual tests inheriting from `ManualTestCase`, call `RequestManualConfirmation("Your question to the user?")` when ready for user input. The base class handles the UI and timeout. + - `protected override void Draw()`: Implement any rendering needed for the test to be visually inspected or to function. + - `public override void KeyPressed(...)`, `MousePressed(...)`, etc.: Override if your test needs specific input handling beyond what `ManualTestCase` provides. +- **Test Completion:** + - Ensure your test logic eventually leads to `CurrentStatus` being set (e.g., to `TestStatus.Passed` or `TestStatus.Failed`) and `Details` populated. + - The `EndTest()` method (called by completion helpers or directly) will stop the `TestStopwatch` and call `Night.Window.Close()`, which signals the `Night.Framework.Run()` method (invoked by `TestGroup.Run_GameTestCase`) to return. + +**Step 2: Create/Update the xUnit Test Class (Test Group)** + +For each "module" or logical grouping of tests (e.g., `Timer`, `Graphics`, `MyModule` corresponding to a `Night` namespace like `Night.Timer`): + +- **Location:** Ensure an xUnit test class exists in the corresponding subdirectory under `tests/Groups/` (e.g., `tests/Groups/MyModule/MyModuleGroup.cs`). If it doesn't exist for a new module, create it. The filename should typically be `[ModuleName]Group.cs`. +- **Inheritance:** The class **must** inherit from [`NightTest.Core.TestGroup`](tests/Core/TestGroup.cs). +- **Constructor:** It must have a constructor that accepts `Xunit.Abstractions.ITestOutputHelper outputHelper` and passes it to the `base(outputHelper)` constructor. +- **Add `[Fact]` Method:** For each new `IGame` test case you created in Step 1, add a new public void method annotated with `[Fact]`. + - **Naming:** Conventionally, `Run_YourIGameTestCaseName()` (e.g., `Run_MyAutomatedFeatureTest()`). + - **Implementation:** + 1. Call `Run_GameTestCase(new MyAutomatedFeatureTest());`. This method is inherited from `NightTest.Core.TestGroup` and handles the execution, logging, and assertion. + - **Traits:** Add `[Trait("TestType", "Automated")]` or `[Trait("TestType", "Manual")]` to the `[Fact]` method. This should match the `Type` of the `IGame` test case being run (which is determined by whether it inherits `GameTestCase` or `ManualTestCase`) and is used for filtering tests via the xUnit runner. + +**3. Running Tests:** + +- Tests can be run using the .NET CLI (`dotnet test`) or through the Test Explorer in Visual Studio. +- Use xUnit's filtering capabilities to run specific tests (e.g., `dotnet test --filter TestType=Automated`). + +**4. Key Considerations:** + +- **`Run_GameTestCase` Method:** The `NightTest.Core.TestGroup.Run_GameTestCase` method will: + - Log the start and end of the `IGame` test case using `ITestOutputHelper`. + - Call `Night.Framework.Run(testCase)`, which blocks until the `IGame` test case calls `Night.Window.Close()` (typically via `EndTest()` in `GameTestCase`). + - Log the `CurrentStatus`, `Details`, and `TestStopwatch.ElapsedMilliseconds` from the `GameTestCase` instance. + - Assert that `testCase.CurrentStatus == TestStatus.Passed`. If not, the xUnit test will fail. +- **Error Handling:** Unhandled exceptions during `Night.Framework.Run(testCase)` are caught by `Run_GameTestCase`, which will then call `testCase.RecordFailure()` and fail the xUnit test. +- **Clarity and Focus:** Each `IGame` test case should be focused on testing a specific piece of functionality. The `Name` and `Description` properties should clearly state its purpose. + +By following these guidelines, tests for the `Night` framework can be added systematically, leveraging the provided base classes and xUnit integration. + +## Mapping Native SDL3 Functions to SDL3-CS (C#) Bindings + +When working with the `lib/SDL3-CS` C# wrapper for SDL3, it's often necessary to find the C# equivalent of a native SDL3 C function, enum, or struct. This section provides guidance on that process. The `lib/SDL3-CS` bindings are located in the `lib/SDL3-CS/SDL3-CS/` directory. + +**1. Naming Conventions:** + +- **Functions:** Native SDL3 functions (e.g., `SDL_CreateWindow`, `SDL_PollEvent`) are generally mapped to C# methods within the static `SDL` class using PascalCase. The `SDL_` prefix is removed, and the rest of the name is converted to PascalCase. + - `SDL_CreateWindow` becomes `SDL.CreateWindow()` + - `SDL_PollEvent` becomes `SDL.PollEvent()` +- **Enums and Structs:** Native SDL3 enums and structs (e.g., `SDL_WindowFlags`, `SDL_Event`, `SDL_Keycode`) are typically mapped to C# enums or structs within the `SDL` static class (or directly in the `SDL3` namespace if they are complex types used by the static class members), also using PascalCase. + - `SDL_WindowFlags` becomes `SDL.WindowFlags` (enum) + - `SDL_Event` becomes `SDL.Event` (struct) + - `SDL_Keycode` becomes `SDL.Keycode` (enum) +- **Constants:** Native SDL3 `#define` constants (e.g., `SDL_INIT_VIDEO`) are usually mapped to enum members or `public const int` fields within the relevant C# enum or static class. + - `SDL_INIT_VIDEO` becomes `SDL.InitFlags.Video` + +**2. File Structure of `lib/SDL3-CS/SDL3-CS/SDL/`:** + +The C# source files for the core SDL3 bindings are primarily located under `lib/SDL3-CS/SDL3-CS/SDL/`. This directory is further organized into subdirectories that often mirror SDL3's own categorization of its API (e.g., `Basics`, `Video`, `Audio`, `Input Events`, `GPU`). + +- **P/Invoke Declarations:** The actual `[LibraryImport]` or `[DllImport]` attributes for native functions are often found in files named `PInvoke.cs` within the relevant subdirectory (e.g., [`lib/SDL3-CS/SDL3-CS/SDL/Basics/init/PInvoke.cs`](lib/SDL3-CS/SDL3-CS/SDL/Basics/init/PInvoke.cs:1) for `SDL_Init`, [`lib/SDL3-CS/SDL3-CS/SDL/Video/video/PInvoke.cs`](lib/SDL3-CS/SDL3-CS/SDL/Video/video/PInvoke.cs:1) for windowing functions). +- **Enum and Struct Definitions:** These are typically in their own dedicated `.cs` files, named after the type (e.g., [`lib/SDL3-CS/SDL3-CS/SDL/Video/video/WindowFlags.cs`](lib/SDL3-CS/SDL3-CS/SDL/Video/video/WindowFlags.cs:1), [`lib/SDL3-CS/SDL3-CS/SDL/Input Events/events/Event.cs`](lib/SDL3-CS/SDL3-CS/SDL/Input%20Events/events/Event.cs:1), [`lib/SDL3-CS/SDL3-CS/SDL/Input Events/keycode/Keycode.cs`](lib/SDL3-CS/SDL3-CS/SDL/Input%20Events/keycode/Keycode.cs:1)). +- **Partial Class `SDL`:** The main C# static class `SDL` (in the `SDL3` namespace) is defined as a `partial class`. This means its members (P/Invoke methods, nested enums/structs, helper functions) are spread across multiple files within these subdirectories but are all part of the single `SDL3.SDL` static class from the perspective of an API consumer. + +**3. Strategy for Finding C# Equivalents:** + +- **Identify the Native SDL3 Element:** Start with the name of the native C function, enum, struct, or constant you need (e.g., `SDL_GetWindowFlags`, `SDL_EventType`, `SDL_SCANCODE_A`). +- **Apply C# Naming Conventions:** + - Remove `SDL_` prefix. + - Convert to `PascalCase` (e.g., `GetWindowFlags`, `EventType`, `ScancodeA`). Note that for constants like scancodes, the C# enum member might be simpler (e.g. `SDL.Scancode.A`). +- **Determine the SDL Subsystem:** Understand which part of SDL the function belongs to (e.g., Video, Events, Keyboard, Mouse). This will guide you to the likely subdirectory in `lib/SDL3-CS/SDL3-CS/SDL/`. + - Example: `SDL_CreateWindow` is a Video function. Look in `lib/SDL3-CS/SDL3-CS/SDL/Video/video/`. + - Example: `SDL_PollEvent` is an Event function. Look in `lib/SDL3-CS/SDL3-CS/SDL/Input Events/events/`. +- **Search within the Subsystem Directory:** + - For functions, check `PInvoke.cs` files first. + - For enums/structs, look for a `.cs` file matching the PascalCase name (e.g., `EventType.cs`, `WindowFlags.cs`). + - The C# equivalent will typically be a static method or nested type of `SDL3.SDL` (e.g., `SDL.CreateWindow()`, `SDL.EventType`, `SDL.WindowFlags`). +- **Use Code Search:** If the location isn't immediately obvious: + - Use your IDE's search functionality (or a command-line tool like `grep` or `rg`) within the `lib/SDL3-CS/SDL3-CS/SDL/` directory. + - Search for the PascalCase C# name (e.g., `CreateWindow`). + - Search for the native C name (e.g., `SDL_CreateWindow`) as it often appears in comments or `EntryPoint` attributes of P/Invoke declarations (e.g., `[LibraryImport(SDLLibrary, EntryPoint = "SDL_CreateWindow")]`). +- **Consult SDL3 Wiki & SDL3-CS Examples:** + - The official [SDL Wiki](https://wiki.libsdl.org/SDL3/FrontPage) provides documentation for the native SDL3 API. Understanding the native function's purpose and parameters helps. + - The `SDL3-CS` repository includes examples in `lib/SDL3-CS/SDL3-CS.Examples/` which demonstrate common usage patterns. + +**4. Key C# Idioms and Marshalling in SDL3-CS:** + +Be aware of common C# idioms used in the bindings: + +- **Return Values for Success/Failure:** Many SDL C functions return `0` for success and a negative value for error. In SDL3-CS, these are often converted to `bool`, where `true` indicates success and `false` indicates failure. Use `SDL.GetError()` to get detailed error information. (e.g., `SDL.Init()` returns `bool`). +- **String Marshalling:** + - `const char*` input parameters in C are often marshalled as `string` in C#. + - `char*` (for output strings from SDL) or `const char*` return values from SDL might be marshalled as `string`, or sometimes as `IntPtr` requiring manual marshalling (e.g., using `Marshal.PtrToStringUTF8()` or `SDL.PtrToStringUTF8()` if available). SDL3-CS aims for direct `string` usage where idiomatic. +- **Pointer Parameters (`*`, `**`):** + - Pointers to simple types or structs passed by value to C functions might become `ref` or `out` parameters in C# for structs, or direct value types (`int`, `float`). + - `SDL_Event*` in C (like in `SDL_PollEvent(SDL_Event* event)`) becomes `out SDL.Event e` or `ref SDL.Event e` in C#. + - Opaque pointers (handles like `SDL_Window*`, `SDL_Renderer*`) are typically represented as `IntPtr` in C# or wrapped in dedicated C# classes/structs if the binding provides higher-level abstractions. SDL3-CS often uses `IntPtr` for these handles. +- **Enums:** C enums are mapped to C# enums, often with the `[Flags]` attribute if they are bitmasks. +- **Callbacks:** C function pointers for callbacks are mapped to C# delegates. +- **Helper Methods:** The `SDL` static class in SDL3-CS includes various helper methods for marshalling and pointer manipulation (e.g., `SDL.PointerToStructure()`, `SDL.StructureToPointer()`, `SDL.StringToPointer()`). These can be useful if you need to interact with more complex native patterns not fully abstracted by a direct C# method. + +- **Troubleshooting SDL Extension Libraries (e.g., SDL3_image, SDL3_ttf) with SDL3-CS:** + - SDL extension libraries (like `SDL3_image` for image loading or `SDL3_ttf` for font rendering) provide specialized functionality on top of the core SDL3 library. While `SDL3-CS` provides bindings for these, their interaction with core SDL3 features (like the properties system for `SDL_Texture`) might not always be straightforward or fully documented externally. + - **Problem Identification:** If a function from an SDL extension library (e.g., `SDL3.Image.LoadTexture()`) returns an SDL object (like an `SDL_Texture`), but subsequent attempts to use standard SDL3 mechanisms on that object (e.g., `SDL.GetTextureProperties()` to get dimensions) fail or don't yield expected results, it might indicate that the extension library handles or exposes information differently. + - **Investigation Strategy:** + 1. **Consult Official SDL Wiki:** First, check the official SDL Wiki (or the specific extension library's documentation, if available and linked) for guidance on the function in question and how it interacts with core SDL types. However, be aware that C# binding specifics might not be covered. + 2. **Examine SDL3-CS Bindings Directly:** If official documentation is insufficient or doesn't clarify the C# binding behavior, the most reliable source of truth is the `SDL3-CS` library's source code itself (located in `lib/SDL3-CS/SDL3-CS/`). + - Look for the C# wrapper function corresponding to the native SDL extension library function you're using (e.g., in `lib/SDL3-CS/SDL3-CS/Image/PInvoke.cs` for `SDL3_image` functions). + - See if the extension library offers alternative C# functions within its own namespace (e.g., `SDL3.Image.Load()` to load to an `SDL_Surface` first, from which dimensions can be reliably obtained before converting to an `SDL_Texture`). + - Check how the C# structs for relevant types (e.g., `SDL.Surface` in `lib/SDL3-CS/SDL3-CS/SDL/Video/surface/Surface.cs`) are defined to understand how to access their members (like `Width`, `Height`) after marshalling an `IntPtr`. + 3. **Consider Intermediate Steps:** Sometimes, an extension library might require or work more reliably with an intermediate step. For example, instead of directly loading an image to an `SDL_Texture`, loading it to an `SDL_Surface` first (using a function from the image extension library), then getting information from the `SDL_Surface` (which is a well-defined core SDL structure), and finally creating the `SDL_Texture` from the `SDL_Surface` using a core SDL function (e.g., `SDL.CreateTextureFromSurface()`) can be a more robust approach. Remember to manage the lifecycle of intermediate objects (like freeing the `SDL_Surface` after the texture is created). + 4. **Error Checking:** Always check return values from SDL functions. For functions from extension libraries, use the standard `SDL.GetError()` to retrieve error messages, as specific `Extension.GetError()` functions may not exist or be necessary. diff --git a/project/love2d-api/love2d-testing.txt b/project/love2d-api/love2d-testing.txt deleted file mode 100644 index d343498c..00000000 --- a/project/love2d-api/love2d-testing.txt +++ /dev/null @@ -1,12537 +0,0 @@ -Directory structure: -└── testing/ - ├── readme.md - ├── conf.lua - ├── main.lua - ├── todo.md - ├── classes/ - │ ├── TestMethod.lua - │ ├── TestModule.lua - │ └── TestSuite.lua - ├── examples/ - │ ├── lovetest_runAllTests.html - │ ├── lovetest_runAllTests.md - │ └── lovetest_runAllTests.xml - ├── output/ - │ ├── notes.txt - │ ├── actual/ - │ │ └── notes.txt - │ ├── difference/ - │ │ └── notes.txt - │ └── expected/ - │ └── notes.txt - ├── resources/ - │ ├── alsoft.conf - │ ├── click.ogg - │ ├── clickmono.ogg - │ ├── font.bmp - │ ├── font.ttf - │ ├── love.dxt1 - │ ├── mappings.txt - │ ├── pop.ogg - │ ├── sample.ogv - │ ├── test.txt - │ ├── test.zip - │ ├── tone.ogg - │ └── vk_layer_settings.txt - └── tests/ - ├── audio.lua - ├── data.lua - ├── event.lua - ├── filesystem.lua - ├── font.lua - ├── graphics.lua - ├── image.lua - ├── joystick.lua - ├── keyboard.lua - ├── love.lua - ├── math.lua - ├── mouse.lua - ├── physics.lua - ├── sensor.lua - ├── sound.lua - ├── system.lua - ├── thread.lua - ├── timer.lua - ├── touch.lua - ├── video.lua - └── window.lua - -================================================ -File: readme.md -================================================ -# Lövetest -Test suite for the [Löve](https://github.com/love2d/love) APIs, based off of [this issue](https://github.com/love2d/love/issues/1745). - -Currently written for [Löve 12](https://github.com/love2d/love/tree/12.0-development), which is still in development. As such the test suite may fail if you try to run it with an older version of Löve due to it trying to call methods that don't exist. - -While the test suite is part of the main Löve repo, the test suite has it's own repo [here](https://github.com/ellraiser/love-test) so that it can be used with other builds like [love-potion](https://github.com/lovebrew/lovepotion). If you would like to contribute to the test suite please raise a PR on the [love-test](https://github.com/ellraiser/love-test) repo. - ---- - -## Features -- [x] Simple pass/fail tests written in Lua with minimal setup -- [x] Ability to run all tests with a simple command -- [x] Ability to see how many tests are passing/failing -- [x] Ability to run a subset of tests -- [x] Ability to easily run an individual test -- [x] Ability to see all visual results at a glance -- [x] Compare graphics test output with an expected output -- [x] Automatic testing that happens after every commit -- [x] No platform-specific dependencies / scripts - ---- - -## Coverage -This is the status of all module tests. -See the **Todo** section for outstanding tasks if you want to contribute! -| Module | Done | Skip | Modules | Done | Skip | -| ----------------- | ---- | ---- | ---------------- | ---- | ---- | -| 🟢 audio | 31 | 0 | 🟢 mouse | 18 | 0 | -| 🟢 data | 12 | 0 | 🟢 physics | 26 | 0 | -| 🟢 event | 4 | 2 | 🟢 sensor | 1 | 0 | -| 🟢 filesystem | 33 | 2 | 🟢 sound | 4 | 0 | -| 🟢 font | 7 | 0 | 🟢 system | 7 | 2 | -| 🟢 graphics | 105 | 1 | 🟢 thread | 5 | 0 | -| 🟢 image | 5 | 0 | 🟢 timer | 6 | 0 | -| 🟢 joystick | 6 | 0 | 🟢 touch | 3 | 0 | -| 🟢 keyboard | 10 | 0 | 🟢 video | 2 | 0 | -| 🟢 love | 6 | 0 | 🟢 window | 34 | 2 | -| 🟢 math | 20 | 0 | - -> The following modules are covered but at a basic level as we can't emulate hardware input nicely for all platforms + virtual runners: -> `joystick`, `keyboard`, `mouse`, `sensor` and `touch` - ---- - -## Running Tests -The testsuite aims to keep things as simple as possible, and just runs all the tests inside Löve to match how they'd be used by developers in-engine. -To run the tests, download the repo and then run the main.lua as you would a Löve game, i.e: - -WINDOWS: `& 'c:\Program Files\LOVE\love.exe' PATH_TO_TESTING_FOLDER/main.lua --console` -MACOS: `/Applications/love.app/Contents/MacOS/love PATH_TO_TESTING_FOLDER/main.lua` -LINUX: `./love.AppImage PATH_TO_TESTING_FOLDER/main.lua` - -By default all tests will be run for all modules. -If you want to specify a module/s you can use: -`--modules filesystem,audio` -If you want to specify only 1 specific method only you can use: -`--method filesystem write` - -All results will be printed in the console per method as PASS, FAIL, or SKIP with total assertions met on a module level and overall level. - -When finished, the following files will be generated in the `/output` directory with a summary of the test results: -- an `XML` file in the style of [JUnit XML](https://www.ibm.com/docs/en/developer-for-zos/14.1?topic=formats-junit-xml-format) -- a `HTML` file that shows the report + any visual test results -- a `Markdown` file you can use with [this github action](https://github.com/ellraiser/love-test-report) -> An example of all types of output can be found in the `/examples` -> The visual results of any graphic tests can be found in `/output/actual` - ---- - -## Architecture -Each method and object has it's own test method written in `/tests` under the matching module name. - -When you run the tests, a single TestSuite object is created which handles the progress + totals for all the tests. -Each module has a TestModule object created, and each test method has a TestMethod object created which keeps track of assertions for that method. You can currently do the following assertions: -- **assertNotNil**(value) -- **assertEquals**(expected, actual, label) -- **assertTrue**(value, label) -- **assertFalse**(value, label) -- **assertNotEquals**(expected, actual, label) -- **assertRange**(actual, min, max, label) -- **assertMatch**({option1, option2, option3 ...}, actual, label) -- **assertGreaterEqual**(expected, actual, label) -- **assertLessEqual**(expected, actual, label) -- **assertObject**(table) -- **assertCoords**(expected, actual, label) - -Example test method: -```lua --- love.filesystem.read test method --- all methods should be put under love.test.MODULE.METHOD, matching the API -love.test.filesystem.read = function(test) - -- setup any data needed then run any asserts using the passed test object - local content, size = love.filesystem.read('resources/test.txt') - test:assertNotNil(content) - test:assertEquals('helloworld', content, 'check content match') - test:assertEquals(10, size, 'check size match') - content, size = love.filesystem.read('resources/test.txt', 5) - test:assertNotNil(content) - test:assertEquals('hello', content, 'check content match') - test:assertEquals(5, size, 'check size match') - -- no need to return anything or cleanup, GCC is called after each method -end -``` - -Each test is run inside it's own coroutine - you can use `test:waitFrames(frames)` or `test:waitSeconds(seconds)` to pause the test for a small period if you need to check things that won't happen for a few frames/seconds. - -After each test method is ran, the assertions are totalled up, printed, and we move onto the next method! Once all methods in the suite are run a total pass/fail/skip is given for that module and we move onto the next module (if any) - -For sanity-checking, if it's currently not covered or it's not possible to test the method we can set the test to be skipped with `test:skipTest(reason)` - this way we still see the method listed in the test output without it affected the pass/fail totals - ---- - -## Todo -If you would like to contribute to the test suite please raise a PR with the main [love-test](https://github.com/ellraiser/love-test) repo. - -There is a list of outstanding methods that require test coverage in `todo.md`, expanding on any existing tests is also very welcome! - ---- - -## Graphics Tolerance -By default all graphic tests are run with pixel precision and 0 rgba tolerance. - -However there are a couple of methods that on some platforms require some slight tolerance to allow for tiny differences in rendering. -| Test | OS | Exception | Reason | -| -------------------------- | --------- | ------------------- | ------ | -| love.graphics.drawInstanced | Windows | 1rgba tolerance | On Windows there's a couple pixels a tiny bit off, most likely due to complexity of the mesh drawn | -| love.graphics.setBlendMode | Win/Lin | 1rgba tolerance | Blendmodes have some small varience on some machines | - ---- - -## Runner Exceptions -The automated tests through Github work for the most part however there are a few exceptions that have to be accounted for due to limitations of the VMs and the graphics emulation used. - -These exceptions are either skipped, or handled by using a 1px or 1/255rgba tolerance - when run locally on real hardware, these tests pass fine at the default 0 tolerance. -You can specify the test suite is being run on a runner by adding the `--isRunner` flag in your workflow file, i.e.: -`& 'c:\Program Files\LOVE\love.exe' PATH_TO_TESTING_FOLDER/main.lua --console --all --isRunner` -| Test | OS | Exception | Reason | -| -------------------------- | --------- | ------------------- | ------ | -| love.graphics.setWireframe | MacOS | 1px tolerance | Wireframes are offset by 1,1 when drawn | -| love.graphica.arc | MacOS | Skipped | Arc curves are drawn slightly off at really low scale | -| love.graphics.setLineStyle | Linux | 1rgba tolerance | 'Rough' lines blend differently with the background rgba | -| love.audio.RecordingDevice | All | Skipped | Recording devices can't be emulated on runners | - - - -================================================ -File: conf.lua -================================================ -function love.conf(t) - print("love.conf") - t.console = true - t.window.name = 'love.test' - t.window.width = 360 - t.window.height = 240 - t.window.resizable = true - t.window.depth = true - t.window.stencil = true - t.window.usedpiscale = false -end - --- custom crash message here to catch anything that might occur with modules --- loading before we hit main.lua -local function error_printer(msg, layer) - print((debug.traceback("Error: " .. tostring(msg), 1+(layer or 1)):gsub("\n[^\n]+$", ""))) -end -function love.errorhandler(msg) - msg = tostring(msg) - error_printer(msg, 2) -end - - - -================================================ -File: main.lua -================================================ --- load test objs -require('classes.TestSuite') -require('classes.TestModule') -require('classes.TestMethod') - --- create testsuite obj -love.test = TestSuite:new() - --- load test scripts if module is active --- this is so in future if we have per-module disabling it'll still run -if love ~= nil then require('tests.love') end -if love.audio ~= nil then require('tests.audio') end -if love.data ~= nil then require('tests.data') end -if love.event ~= nil then require('tests.event') end -if love.filesystem ~= nil then require('tests.filesystem') end -if love.font ~= nil then require('tests.font') end -if love.graphics ~= nil then require('tests.graphics') end -if love.image ~= nil then require('tests.image') end -if love.joystick ~= nil then require('tests.joystick') end -if love.keyboard ~= nil then require('tests.keyboard') end -if love.math ~= nil then require('tests.math') end -if love.mouse ~= nil then require('tests.mouse') end -if love.physics ~= nil then require('tests.physics') end -if love.sensor ~= nil then require('tests.sensor') end -if love.sound ~= nil then require('tests.sound') end -if love.system ~= nil then require('tests.system') end -if love.thread ~= nil then require('tests.thread') end -if love.timer ~= nil then require('tests.timer') end -if love.touch ~= nil then require('tests.touch') end -if love.video ~= nil then require('tests.video') end -if love.window ~= nil then require('tests.window') end - --- love.load --- load given arguments and run the test suite -love.load = function(args) - - -- setup basic img to display - if love.window ~= nil then - love.window.updateMode(360, 240, { - fullscreen = false, - resizable = true, - centered = true - }) - - -- set up some graphics to draw if enabled - if love.graphics ~= nil then - love.graphics.setDefaultFilter("nearest", "nearest") - love.graphics.setLineStyle('rough') - love.graphics.setLineWidth(1) - Logo = { - texture = love.graphics.newImage('resources/love.png'), - img = nil - } - Logo.img = love.graphics.newQuad(0, 0, 64, 64, Logo.texture) - Font = love.graphics.newFont('resources/font.ttf', 8, 'normal') - TextCommand = 'Loading...' - TextRun = '' - end - - end - - -- mount for output later - if love.filesystem.mountFullPath then - love.filesystem.mountFullPath(love.filesystem.getSource() .. "/output", "tempoutput", "readwrite") - end - - -- get all args with any comma lists split out as seperate - local arglist = {} - for a=1,#args do - local splits = UtilStringSplit(args[a], '([^,]+)') - for s=1,#splits do - table.insert(arglist, splits[s]) - end - end - - -- convert args to the cmd to run, modules, method (if any) and disabled - local testcmd = '--all' - local module = '' - local method = '' - local cmderr = 'Invalid flag used' - local modules = { - 'audio', 'data', 'event', 'filesystem', 'font', 'graphics', 'image', - 'joystick', 'keyboard', 'love', 'math', 'mouse', 'physics', 'sensor', - 'sound', 'system', 'thread', 'timer', 'touch', 'video', 'window' - } - GITHUB_RUNNER = false - for a=1,#arglist do - if testcmd == '--method' then - if module == '' and (arglist[a] == 'love' or love[ arglist[a] ] ~= nil) then - module = arglist[a] - table.insert(modules, module) - elseif module ~= '' and love[module] ~= nil and method == '' then - if love.test[module][arglist[a]] ~= nil then method = arglist[a] end - end - end - if testcmd == '--modules' then - if (arglist[a] == 'love' or love[ arglist[a] ] ~= nil) and arglist[a] ~= '--isRunner' then - table.insert(modules, arglist[a]) - end - end - if arglist[a] == '--method' then - testcmd = arglist[a] - modules = {} - end - if arglist[a] == '--modules' then - testcmd = arglist[a] - modules = {} - end - if arglist[a] == '--isRunner' then - GITHUB_RUNNER = true - end - end - - -- method uses the module + method given - if testcmd == '--method' then - local testmodule = TestModule:new(module, method) - table.insert(love.test.modules, testmodule) - if module ~= '' and method ~= '' then - love.test.module = testmodule - love.test.module:log('grey', '--method "' .. module .. '" "' .. method .. '"') - love.test.output = 'lovetest_method_' .. module .. '_' .. method - else - if method == '' then cmderr = 'No valid method specified' end - if module == '' then cmderr = 'No valid module specified' end - end - end - - -- modules runs all methods for all the modules given - if testcmd == '--modules' then - local modulelist = {} - for m=1,#modules do - local testmodule = TestModule:new(modules[m]) - table.insert(love.test.modules, testmodule) - table.insert(modulelist, modules[m]) - end - if #modulelist > 0 then - love.test.module = love.test.modules[1] - love.test.module:log('grey', '--modules "' .. table.concat(modulelist, '" "') .. '"') - love.test.output = 'lovetest_modules_' .. table.concat(modulelist, '_') - else - cmderr = 'No modules specified' - end - end - - -- otherwise default runs all methods for all modules - if arglist[1] == nil or arglist[1] == '' or arglist[1] == '--all' then - for m=1,#modules do - local testmodule = TestModule:new(modules[m]) - table.insert(love.test.modules, testmodule) - end - love.test.module = love.test.modules[1] - love.test.module:log('grey', '--all') - love.test.output = 'lovetest_all' - end - - if GITHUB_RUNNER then - love.test.module:log('grey', '--isRunner') - end - - -- invalid command - if love.test.module == nil then - print(cmderr) - love.event.quit(0) - else - -- start first module - TextCommand = testcmd - love.test.module:runTests() - end - -end - --- love.update --- run test suite logic -love.update = function(delta) - love.test:runSuite(delta) -end - - --- love.draw --- draw a little logo to the screen -love.draw = function() - local lw = (love.graphics.getWidth() - 128) / 2 - local lh = (love.graphics.getHeight() - 128) / 2 - love.graphics.draw(Logo.texture, Logo.img, lw, lh, 0, 2, 2) - love.graphics.setFont(Font) - love.graphics.print(TextCommand, 4, 12, 0, 2, 2) - love.graphics.print(TextRun, 4, 32, 0, 2, 2) -end - - --- love.quit --- add a hook to allow test modules to fake quit -love.quit = function() - if love.test.module ~= nil and love.test.module.fakequit then - return true - else - return false - end -end - - --- added so bad threads dont fail -function love.threaderror(thread, errorstr) end - - --- string split helper -function UtilStringSplit(str, splitter) - local splits = {} - for word in string.gmatch(str, splitter) do - table.insert(splits, word) - end - return splits -end - - --- string time formatter -function UtilTimeFormat(seconds) - return string.format("%.3f", tostring(seconds)) -end - - - -================================================ -File: todo.md -================================================ -# TODO -These are all the outstanding methods that require test coverage, along with a few bits that still need doing / discussion. - -## General -- ability to test loading different combinations of modules if needed? -- check expected behaviour of mount + unmount with common path - try uncommenting love.filesystem.unmountCommonPath and you'll see the issues -- revisit love.audio.setPlaybackDevice when we update openal soft for MacOS - -## Graphics -- love.graphics.copyBuffer() -- love.graphics.copyBufferToTexture() -- love.graphics.copyTextureToBuffer() -- love.graphics.readbackTexture() -- love.graphics.readbackTextureAsync() -- love.graphics.readbackBuffer() -- love.graphics.readbackBufferAsync() -- love.graphics.newComputeShader() -- love.graphics.dispatchThreadgroups() -- love.graphics.dispatchIndirect() -- love.graphics.drawFromShader() -- love.graphics.drawFromShaderIndirect() -- love.graphics.drawIndirect() -- love.graphics.getQuadIndexBuffer() -- love.graphics.setBlendState() -- love.graphics.resetProjection() -- love.graphics.Mesh:getAttachedAttributes() -- love.graphics.Shader:hasStage() - - - -================================================ -File: classes/TestMethod.lua -================================================ --- @class - TestMethod --- @desc - used to run a specific method from a module's /test/ suite --- each assertion is tracked and then printed to output -TestMethod = { - - - -- @method - TestMethod:new() - -- @desc - create a new TestMethod object - -- @param {string} method - string of method name to run - -- @param {TestMethod} testmethod - parent testmethod this test belongs to - -- @return {table} - returns the new Test object - new = function(self, method, testmodule) - local test = { - testmodule = testmodule, - method = method, - asserts = {}, - start = love.timer.getTime(), - finish = 0, - count = 0, - passed = false, - skipped = false, - skipreason = '', - rgba_tolerance = 0, - pixel_tolerance = 0, - fatal = '', - message = nil, - result = {}, - colors = { - red = {1, 0, 0, 1}, - redpale = {1, 0.5, 0.5, 1}, - red07 = {0.7, 0, 0, 1}, - green = {0, 1, 0, 1}, - greenhalf = {0, 0.5, 0, 1}, - greenfade = {0, 1, 0, 0.5}, - blue = {0, 0, 1, 1}, - bluefade = {0, 0, 1, 0.5}, - yellow = {1, 1, 0, 1}, - pink = {1, 0, 1, 1}, - black = {0, 0, 0, 1}, - white = {1, 1, 1, 1}, - lovepink = {214/255, 86/255, 151/255, 1}, - loveblue = {83/255, 168/255, 220/255, 1} - }, - imgs = 1, - delay = 0, - delayed = false, - store = {}, - co = nil - } - setmetatable(test, self) - self.__index = self - return test - end, - - - -- @method - TestMethod:assertEquals() - -- @desc - used to assert two values are equals - -- @param {any} expected - expected value of the test - -- @param {any} actual - actual value of the test - -- @param {string} label - label for this test to use in exports - -- @return {nil} - assertEquals = function(self, expected, actual, label) - self.count = self.count + 1 - table.insert(self.asserts, { - key = 'assert ' .. tostring(self.count), - passed = expected == actual, - message = 'expected \'' .. tostring(expected) .. '\' got \'' .. - tostring(actual) .. '\'', - test = label or 'no label given' - }) - end, - - - -- @method - TestMethod:assertTrue() - -- @desc - used to assert a value is true - -- @param {any} value - value to test - -- @param {string} label - label for this test to use in exports - -- @return {nil} - assertTrue = function(self, value, label) - self.count = self.count + 1 - table.insert(self.asserts, { - key = 'assert ' .. tostring(self.count), - passed = value == true, - message = 'expected \'true\' got \'' .. - tostring(value) .. '\'', - test = label or 'no label given' - }) - end, - - - -- @method - TestMethod:assertFalse() - -- @desc - used to assert a value is false - -- @param {any} value - value to test - -- @param {string} label - label for this test to use in exports - -- @return {nil} - assertFalse = function(self, value, label) - self.count = self.count + 1 - table.insert(self.asserts, { - key = 'assert ' .. tostring(self.count), - passed = value == false, - message = 'expected \'false\' got \'' .. - tostring(value) .. '\'', - test = label or 'no label given' - }) - end, - - - -- @method - TestMethod:assertNotEquals() - -- @desc - used to assert two values are not equal - -- @param {any} expected - expected value of the test - -- @param {any} actual - actual value of the test - -- @param {string} label - label for this test to use in exports - -- @return {nil} - assertNotEquals = function(self, expected, actual, label) - self.count = self.count + 1 - table.insert(self.asserts, { - key = 'assert ' .. tostring(self.count), - passed = expected ~= actual, - message = 'avoiding \'' .. tostring(expected) .. '\' got \'' .. - tostring(actual) .. '\'', - test = label or 'no label given' - }) - end, - - - -- @method - TestMethod:assertRange() - -- @desc - used to check a value is within an expected range - -- @param {number} actual - actual value of the test - -- @param {number} min - minimum value the actual should be >= to - -- @param {number} max - maximum value the actual should be <= to - -- @param {string} label - label for this test to use in exports - -- @return {nil} - assertRange = function(self, actual, min, max, label) - self.count = self.count + 1 - table.insert(self.asserts, { - key = 'assert ' .. tostring(self.count), - passed = actual >= min and actual <= max, - message = 'value \'' .. tostring(actual) .. '\' out of range \'' .. - tostring(min) .. '-' .. tostring(max) .. '\'', - test = label or 'no label given' - }) - end, - - - -- @method - TestMethod:assertMatch() - -- @desc - used to check a value is within a list of values - -- @param {number} list - list of valid values for the test - -- @param {number} actual - actual value of the test to check is in the list - -- @param {string} label - label for this test to use in exports - -- @return {nil} - assertMatch = function(self, list, actual, label) - self.count = self.count + 1 - local found = false - for l=1,#list do - if list[l] == actual then found = true end; - end - table.insert(self.asserts, { - key = 'assert ' .. tostring(self.count), - passed = found == true, - message = 'value \'' .. tostring(actual) .. '\' not found in \'' .. - table.concat(list, ',') .. '\'', - test = label or 'no label given' - }) - end, - - - -- @method - TestMethod:assertGreaterEqual() - -- @desc - used to check a value is >= than a certain target value - -- @param {any} target - value to check the test agaisnt - -- @param {any} actual - actual value of the test - -- @param {string} label - label for this test to use in exports - -- @return {nil} - assertGreaterEqual = function(self, target, actual, label) - self.count = self.count + 1 - local passing = false - if target ~= nil and actual ~= nil then - passing = actual >= target - end - table.insert(self.asserts, { - key = 'assert ' .. tostring(self.count), - passed = passing, - message = 'value \'' .. tostring(actual) .. '\' not >= \'' .. - tostring(target) .. '\'', - test = label or 'no label given' - }) - end, - - - -- @method - TestMethod:assertLessEqual() - -- @desc - used to check a value is <= than a certain target value - -- @param {any} target - value to check the test agaisnt - -- @param {any} actual - actual value of the test - -- @param {string} label - label for this test to use in exports - -- @return {nil} - assertLessEqual = function(self, target, actual, label) - self.count = self.count + 1 - local passing = false - if target ~= nil and actual ~= nil then - passing = actual <= target - end - table.insert(self.asserts, { - key = 'assert ' .. tostring(self.count), - passed = passing, - message = 'value \'' .. tostring(actual) .. '\' not <= \'' .. - tostring(target) .. '\'', - test = label or 'no label given' - }) - end, - - - -- @method - TestMethod:assertObject() - -- @desc - used to check a table is a love object, this runs 3 seperate - -- tests to check table has the basic properties of an object - -- @note - actual object functionality tests have their own methods - -- @param {table} obj - table to check is a valid love object - -- @return {nil} - assertObject = function(self, obj) - self:assertNotNil(obj) - self:assertEquals('userdata', type(obj), 'check is userdata') - if obj ~= nil then - self:assertNotEquals(nil, obj:type(), 'check has :type()') - end - end, - - - -- @method - TestMethod:assertCoords() - -- @desc - used to check a pair of values (usually coordinates) - -- @param {table} obj - table to check is a valid love object - -- @return {nil} - assertCoords = function(self, expected, actual, label) - self.count = self.count + 1 - local passing = false - if expected ~= nil and actual ~= nil then - if expected[1] == actual[1] and expected[2] == actual[2] then - passing = true - end - end - table.insert(self.asserts, { - key = 'assert ' .. tostring(self.count), - passed = passing, - message = 'expected \'' .. tostring(expected[1]) .. 'x,' .. - tostring(expected[2]) .. 'y\' got \'' .. - tostring(actual[1]) .. 'x,' .. tostring(actual[2]) .. 'y\'', - test = label or 'no label given' - }) - end, - - - -- @method - TestMethod:assertNotNil() - -- @desc - quick assert for value not nil - -- @param {any} value - value to check not nil - -- @return {nil} - assertNotNil = function (self, value, err) - self:assertNotEquals(nil, value, 'check not nil') - if err ~= nil then - table.insert(self.asserts, { - key = 'assert ' .. tostring(self.count), - passed = false, - message = err, - test = 'assert not nil catch' - }) - end - end, - - - -- @method - TestMethod:compareImg() - -- @desc - compares a given image to the 'expected' version, with a tolerance of - -- 1px in any direction, and then saves it as the 'actual' version for - -- report viewing - -- @param {table} imgdata - imgdata to save as a png - -- @return {nil} - compareImg = function(self, imgdata) - local expected_path = 'tempoutput/expected/love.test.graphics.' .. - self.method .. '-' .. tostring(self.imgs) .. '.png' - local ok, chunk, _ = pcall(love.image.newImageData, expected_path) - if ok == false then return self:assertEquals(true, false, chunk) end - local expected = chunk - local iw = imgdata:getWidth()-1 - local ih = imgdata:getHeight()-1 - local differences = {} - local rgba_tolerance = self.rgba_tolerance * (1/255) - - -- for each pixel, compare the expected vs the actual pixel data - -- by default rgba_tolerance is 0 - for ix=0,iw do - for iy=0,ih do - local ir, ig, ib, ia = imgdata:getPixel(ix, iy) - local points = { - {expected:getPixel(ix, iy)} - } - if self.pixel_tolerance > 0 then - if ix > 0 and iy < ih-1 then table.insert(points, {expected:getPixel(ix-1, iy+1)}) end - if ix > 0 then table.insert(points, {expected:getPixel(ix-1, iy)}) end - if ix > 0 and iy > 0 then table.insert(points, {expected:getPixel(ix-1, iy-1)}) end - if iy < ih-1 then table.insert(points, {expected:getPixel(ix, iy+1)}) end - if iy > 0 then table.insert(points, {expected:getPixel(ix, iy-1)}) end - if ix < iw-1 and iy < ih-1 then table.insert(points, {expected:getPixel(ix+1, iy+1)}) end - if ix < iw-1 then table.insert(points, {expected:getPixel(ix+1, iy)}) end - if ix < iw-1 and iy > 0 then table.insert(points, {expected:getPixel(ix+1, iy-1)}) end - end - local has_match_r = false - local has_match_g = false - local has_match_b = false - local has_match_a = false - for t=1,#points do - local epoint = points[t] - if ir >= epoint[1] - rgba_tolerance and ir <= epoint[1] + rgba_tolerance then has_match_r = true; end - if ig >= epoint[2] - rgba_tolerance and ig <= epoint[2] + rgba_tolerance then has_match_g = true; end - if ib >= epoint[3] - rgba_tolerance and ib <= epoint[3] + rgba_tolerance then has_match_b = true; end - if ia >= epoint[4] - rgba_tolerance and ia <= epoint[4] + rgba_tolerance then has_match_a = true; end - end - local matching = has_match_r and has_match_g and has_match_b and has_match_a - local ymatch = '' - local nmatch = '' - if has_match_r then ymatch = ymatch .. 'r' else nmatch = nmatch .. 'r' end - if has_match_g then ymatch = ymatch .. 'g' else nmatch = nmatch .. 'g' end - if has_match_b then ymatch = ymatch .. 'b' else nmatch = nmatch .. 'b' end - if has_match_a then ymatch = ymatch .. 'a' else nmatch = nmatch .. 'a' end - local pixel = tostring(ir)..','..tostring(ig)..','..tostring(ib)..','..tostring(ia) - self:assertEquals(true, matching, 'compare image pixel (' .. pixel .. ') at ' .. - tostring(ix) .. ',' .. tostring(iy) .. ', matching = ' .. ymatch .. - ', not matching = ' .. nmatch .. ' (' .. self.method .. '-' .. tostring(self.imgs) .. ')' - ) - -- add difference co-ord for rendering later - if matching ~= true then - table.insert(differences, ix+1) - table.insert(differences, iy+1) - end - end - end - local path = 'tempoutput/actual/love.test.graphics.' .. - self.method .. '-' .. tostring(self.imgs) .. '.png' - imgdata:encode('png', path) - - -- if we have differences draw them to a new canvas to display in HTML report - local dpath = 'tempoutput/difference/love.test.graphics.' .. - self.method .. '-' .. tostring(self.imgs) .. '.png' - if #differences > 0 then - local difference = love.graphics.newCanvas(iw+1, ih+1) - love.graphics.setCanvas(difference) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 0, 1, 1) - love.graphics.points(differences) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - love.graphics.readbackTexture(difference):encode('png', dpath) - - -- otherwise clear the old difference file (if any) to stop it coming up - -- in future reports when there's no longer a difference - elseif love.filesystem.openFile(dpath, 'r') then - love.filesystem.remove(dpath) - end - - self.imgs = self.imgs + 1 - end, - - - -- @method - TestMethod:exportImg() - -- @desc - exports the given imgdata to the 'output/expected/' folder, to use when - -- writing new graphics tests to set the expected image output - -- @NOTE - you should not leave this method in when you are finished this is - -- for test writing only - -- @param {table} imgdata - imgdata to save as a png - -- @param {integer} imgdata - index of the png, graphic tests are run sequentially - -- and each test image is numbered in order that its - -- compared to, so set the number here to match - -- @return {nil} - exportImg = function(self, imgdata, index) - local path = 'tempoutput/expected/love.test.graphics.' .. - self.method .. '-' .. tostring(index) .. '.png' - imgdata:encode('png', path) - end, - - - -- @method - TestMethod:skipTest() - -- @desc - used to mark this test as skipped for a specific reason - -- @param {string} reason - reason why method is being skipped - -- @return {nil} - skipTest = function(self, reason) - self.skipped = true - self.skipreason = reason - end, - - - -- @method - TestMethod:waitFrames() - -- @desc - yields the method for x amount of frames - -- @param {number} frames - no. frames to wait - -- @return {nil} - waitFrames = function(self, frames) - for _=1,frames do coroutine.yield() end - end, - - - -- @method - TestMethod:waitSeconds() - -- @desc - yields the method for x amount of seconds - -- @param {number} seconds - no. seconds to wait - -- @return {nil} - waitSeconds = function(self, seconds) - local start = love.timer.getTime() - while love.timer.getTime() < start + seconds do - coroutine.yield() - end - end, - - - -- @method - TestMethod:isOS() - -- @desc - checks for a specific OS (or list of OSs) - -- @param {string/s} - each arg passed will be checked as a valid OS, as long - -- as one passed the function will return true - -- @return {boolean} - returns true if one of the OSs given matches actual OS - isOS = function(self, ...) - for os=1,select("#", ...) do - if select(os, ...) == love.test.current_os then return true end - end - return false - end, - - -- @method - TestMethod:isLuaVersion() - -- @desc - checks for a specific Lua version (or list of versions) - -- @param {number} - the minimum Lua version to check against - -- @return {boolean} - returns true if the current Lua version is at least the given version - isAtLeastLuaVersion = function(self, version) - return love.test.lua_version >= version - end, - - -- @method - TestMethod:isLuaJITEnabled() - -- @desc - checks if LuaJIT is enabled - -- @return {boolean} - returns true if LuaJIT is enabled - isLuaJITEnabled = function(self) - return love.test.has_lua_jit - end, - - -- @method - TestMethod:evaluateTest() - -- @desc - evaluates the results of all assertions for a final restult - -- @return {nil} - evaluateTest = function(self) - local failure = '' - local failures = 0 - - -- check all asserts for failures, additional failures are also printed - local assert_failures = {} - for a=1,#self.asserts do - if not self.asserts[a].passed and not self.skipped then - if failure == '' then failure = self.asserts[a] end - table.insert(assert_failures, self.asserts[a]) - failures = failures + 1 - end - end - if self.fatal ~= '' then failure = self.fatal end - local passed = tostring(#self.asserts - failures) - local total = '(' .. passed .. '/' .. tostring(#self.asserts) .. ')' - - -- skipped tests have a special log - if self.skipped then - self.testmodule.skipped = self.testmodule.skipped + 1 - love.test.totals[3] = love.test.totals[3] + 1 - self.result = { - total = '', - result = "SKIP", - passed = false, - message = '(0/0) - method skipped [' .. self.skipreason .. ']', - failures = {} - } - else - - -- if no failure but has asserts, then passed - if failure == '' and #self.asserts > 0 then - self.passed = true - self.testmodule.passed = self.testmodule.passed + 1 - love.test.totals[1] = love.test.totals[1] + 1 - self.result = { - total = total, - result = 'PASS', - passed = true, - message = nil, - failures = {} - } - - -- otherwise it failed - else - self.passed = false - self.testmodule.failed = self.testmodule.failed + 1 - love.test.totals[2] = love.test.totals[2] + 1 - - -- no asserts means invalid test - if #self.asserts == 0 then - local msg = 'no asserts defined' - if self.fatal ~= '' then msg = self.fatal end - self.result = { - total = total, - result = 'FAIL', - passed = false, - key = 'test', - message = msg, - failures = {} - } - - -- otherwise we had failures, log the first and supply the list of - -- additional failures if any for printResult() - else - local key = failure['key'] - if failure['test'] ~= nil then - key = key .. ' [' .. failure['test'] .. ']' - end - local msg = failure['message'] - if self.fatal ~= '' then - key = 'code' - msg = self.fatal - end - self.result = { - total = total, - result = 'FAIL', - passed = false, - key = key, - message = msg, - failures = assert_failures - } - end - end - end - self:printResult() - end, - - - -- @method - TestMethod:printResult() - -- @desc - prints the result of the test to the console as well as appends - -- the XML + HTML for the test to the testsuite output - -- @return {nil} - printResult = function(self) - - -- get total timestamp - self.finish = love.timer.getTime() - self.start - love.test.time = love.test.time + self.finish - self.testmodule.time = self.testmodule.time + self.finish - local endtime = UtilTimeFormat(love.timer.getTime() - self.start) - - -- get failure/skip message for output (if any) - local failure = '' - local output = '' - if not self.passed and not self.skipped then - failure = '\t\t\t' .. self.result.key .. ' ' .. self.result.message .. '\n' - output = self.result.key .. ' ' .. self.result.message - -- append failures if any to report md - love.test.mdfailures = love.test.mdfailures .. '> 🔴 ' .. self.method .. ' \n' .. - '> ' .. output .. ' \n\n' - end - if output == '' and self.skipped then - failure = '\t\t\t\n' - output = self.skipreason - end - - - -- append XML for the test class result - self.testmodule.xml = self.testmodule.xml .. '\t\t\n' .. - failure .. '\t\t\n' - - -- unused currently, adds a preview image for certain graphics methods to the output - local preview = '' - if self.testmodule.module == 'graphics' then - local filename = 'love.test.graphics.' .. self.method - for f=1,5 do - local fstr = tostring(f) - if love.filesystem.openFile('tempoutput/actual/' .. filename .. '-' .. fstr .. '.png', 'r') then - preview = preview .. '
' - preview = preview .. '
' .. '

Expected

' .. - '
' .. '

Actual

' - if love.filesystem.openFile('tempoutput/difference/' .. filename .. '-' .. fstr .. '.png', 'r') then - preview = preview .. '
' .. '

Difference

' - end - preview = preview .. '
' - end - end - end - - -- append HTML for the test class result - local status = '' - local cls = 'red' - if self.passed then status = '
'; cls = 'green' end - if self.skipped then status = ''; cls = 'yellow' end - self.testmodule.html = self.testmodule.html .. - '' .. - '' .. status .. '' .. - '' .. self.method .. '' .. - '' .. endtime .. 's' .. - '' .. output .. preview .. '' .. - '' - - -- add message if assert failed - local msg = '' - if self.result.message ~= nil and not self.skipped then - msg = ' - ' .. self.result.key .. - ' failed - (' .. self.result.message .. ')' - end - if self.skipped then - msg = self.result.message - end - - -- log final test result to console - -- i know its hacky but its neat soz - local tested = 'love.' .. self.testmodule.module .. '.' .. self.method .. '()' - local matching = string.sub(self.testmodule.spacer, string.len(tested), 40) - self.testmodule:log( - self.testmodule.colors[self.result.result], - ' ' .. tested .. matching, - ' ==> ' .. self.result.result .. ' - ' .. endtime .. 's ' .. - self.result.total .. msg - ) - - -- if we failed on multiple asserts, list them here - makes it easier for - -- debugging new methods added that are failing multiple asserts - if #self.result.failures > 1 then - for f=2,#self.result.failures do - local addf = self.result.failures[f] - self.testmodule:log( - self.testmodule.colors[self.result.result], - ' ' .. tested .. matching, - ' ==> ' .. - addf['key'] .. ' [' .. addf['test'] .. '] failed - ' .. addf['message'] - ) - end - end - - end - - -} - - - -================================================ -File: classes/TestModule.lua -================================================ --- @class - TestModule --- @desc - used to run tests for a given module, each test method will spawn --- a love.test.Test object -TestModule = { - - - -- @method - TestModule:new() - -- @desc - create a new Suite object - -- @param {string} module - string of love module the suite is for - -- @return {table} - returns the new Suite object - new = function(self, module, method) - local testmodule = { - time = 0, - spacer = ' ', - colors = { - PASS = 'green', FAIL = 'red', SKIP = 'grey' - }, - colormap = { - grey = '\27[37m', - green = '\27[32m', - red = '\27[31m', - yellow = '\27[33m' - }, - xml = '', - html = '', - tests = {}, - running = {}, - called = {}, - passed = 0, - failed = 0, - skipped = 0, - module = module, - method = method, - index = 1, - start = false, - } - setmetatable(testmodule, self) - self.__index = self - return testmodule - end, - - - -- @method - TestModule:log() - -- @desc - log to console with specific colors, split out to make it easier - -- to adjust all console output across the tests - -- @param {string} color - color key to use for the log - -- @param {string} line - main message to write (LHS) - -- @param {string} result - result message to write (RHS) - -- @return {nil} - log = function(self, color, line, result) - if result == nil then result = '' end - print(self.colormap[color] .. line .. result) - end, - - - -- @method - TestModule:runTests() - -- @desc - starts the running of tests and sets up the list of methods to test - -- @param {string} module - module to set for the test suite - -- @param {string} method - specific method to test, if nil all methods tested - -- @return {nil} - runTests = function(self) - self.running = {} - self.passed = 0 - self.failed = 0 - if self.method ~= nil then - table.insert(self.running, self.method) - else - for i,_ in pairs(love.test[self.module]) do - table.insert(self.running, i) - end - table.sort(self.running) - end - self.index = 1 - self.start = true - self:log('yellow', '\nlove.' .. self.module .. '.testmodule.start') - end, - - - -- @method - TestModule:printResult() - -- @desc - prints the result of the module to the console as well as appends - -- the XML + HTML for the test to the testsuite output - -- @return {nil} - printResult = function(self) - local finaltime = UtilTimeFormat(self.time) - local status = '
' - if self.failed == 0 then status = '
' end - -- add md row to main output - love.test.mdrows = love.test.mdrows .. '| ' .. status .. - ' ' .. self.module .. - ' | ' .. tostring(self.passed) .. - ' | ' .. tostring(self.failed) .. - ' | ' .. tostring(self.skipped) .. - ' | ' .. finaltime .. 's |' .. '\n' - -- add xml to main output - love.test.xml = love.test.xml .. '\t\n' .. self.xml .. '\t\n' - -- add html to main output - local module_cls = 'toggle close' - local module_txt = 'â–¶' - local wrap_cls = '' - if self.failed > 0 then - module_cls = 'toggle open' - module_txt = 'â–¼' - wrap_cls = 'fail' - end - love.test.html = love.test.html .. '
' .. - '
' .. module_txt .. '
' .. - '

' .. status .. ' love.' .. self.module .. '

    ' .. - '
  • ' .. tostring(self.passed) .. ' Passed
  • ' .. - '
  • ' .. tostring(self.failed) .. ' Failed
  • ' .. - '
  • ' .. tostring(self.skipped) .. ' Skipped
  • ' .. - '
  • ' .. finaltime .. 's
  • ' .. '


' .. - '' .. - self.html .. '
MethodTimeDetails
' - -- print module results to console - self:log('yellow', 'love.' .. self.module .. '.testmodule.end') - local failedcol = '\27[31m' - if self.failed == 0 then failedcol = '\27[37m' end - self:log('green', tostring(self.passed) .. ' PASSED' .. ' || ' .. - failedcol .. tostring(self.failed) .. ' FAILED || \27[37m' .. - tostring(self.skipped) .. ' SKIPPED || ' .. finaltime .. 's') - self.start = false - self.fakequit = false - end - - -} - - - -================================================ -File: classes/TestSuite.lua -================================================ -TestSuite = { - - - -- @method - TestSuite:new() - -- @desc - creates a new TestSuite object that handles all the tests - -- @return {table} - returns the new TestSuite object - new = function(self) - local test = { - - -- testsuite internals - modules = {}, - module = nil, - test = nil, - testcanvas = nil, - current = 1, - output = '', - totals = {0, 0, 0}, - time = 0, - xml = '', - html = '', - mdrows = '', - mdfailures = '', - delayed = nil, - fakequit = false, - windowmode = true, - current_os = love._os, - lua_version = tonumber(_VERSION:match("%d%.%d")), - has_lua_jit = type(jit) == 'table', - - -- love modules to test - audio = {}, - data = {}, - event = {}, - filesystem = {}, - font = {}, - graphics = {}, - image = {}, - joystick = {}, - love = {}, - keyboard = {}, - math = {}, - mouse = {}, - physics = {}, - sensor = {}, - sound = {}, - system = {}, - thread = {}, - timer = {}, - touch = {}, - video = {}, - window = {} - - } - setmetatable(test, self) - self.__index = self - return test - end, - - - -- @method - TestSuite:runSuite() - -- @desc - called in love.update, runs through every method or every module - -- @param {number} delta - delta from love.update to track time elapsed - -- @return {nil} - runSuite = function(self, delta) - - -- stagger between tests - if self.module ~= nil then - - if self.module.start then - - -- work through each test method 1 by 1 - if self.module.index <= #self.module.running then - - -- run method once - if self.module.called[self.module.index] == nil then - self.module.called[self.module.index] = true - local method = self.module.running[self.module.index] - self.test = TestMethod:new(method, self.module) - TextRun = 'love.' .. self.module.module .. '.' .. method - - self.test.co = coroutine.create(function() - local ok, chunk, err = pcall( - love.test[love.test.module.module][method], - love.test.test - ) - if ok == false then - love.test.test['passed'] = false - love.test.test['fatal'] = tostring(chunk) .. tostring(err) - end - end) - - - -- once called we have a corouting, so just call resume every frame - -- until we have finished - else - - -- move onto next yield if any - -- pauses can be set with TestMethod:waitFrames(frames) - coroutine.resume(self.test.co) - - -- when wait finished (or no yields) - if coroutine.status(self.test.co) == 'dead' then - -- now we're all done evaluate the test - local ok, chunk, err = pcall(self.test.evaluateTest, self.test) - if ok == false then - self.test.passed = false - self.test.fatal = tostring(chunk) .. tostring(err) - end - -- save having to :release() anything we made in the last test - collectgarbage("collect") - -- move onto the next test - self.module.index = self.module.index + 1 - end - - end - - -- once all tests have run - else - - -- print module results and add to output - self.module:printResult() - - -- if we have more modules to go run the next one - self.current = self.current + 1 - if #self.modules >= self.current then - self.module = self.modules[self.current] - self.module:runTests() - - -- otherwise print the final results and export output - else - self:printResult() - love.event.quit(0) - end - - end - end - end - - end, - - - -- @method - TestSuite:printResult() - -- @desc - prints the result of the whole test suite as well as writes - -- the MD, XML + HTML of the testsuite output - -- @return {nil} - printResult = function(self) - local finaltime = UtilTimeFormat(self.time) - - -- in case we dont have love.graphics loaded, for future module specific disabling - local name = 'NONE' - local version = 'NONE' - local vendor = 'NONE' - local device = 'NONE' - if love.graphics then - name, version, vendor, device = love.graphics.getRendererInfo() - end - - local md = '\n\n### Info\n' .. - '**' .. tostring(self.totals[1] + self.totals[2] + self.totals[3]) .. '** tests were completed in **' .. - finaltime .. 's** with **' .. - tostring(self.totals[1]) .. '** passed, **' .. - tostring(self.totals[2]) .. '** failed, and **' .. - tostring(self.totals[3]) .. '** skipped\n\n' .. - 'Renderer: ' .. name .. ' | ' .. version .. ' | ' .. vendor .. ' | ' .. device .. '\n\n' .. - '### Report\n' .. - '| Module | Pass | Fail | Skip | Time |\n' .. - '| --------------------- | ------ | ------ | ------- | ------ |\n' .. - self.mdrows .. '\n### Failures\n' .. self.mdfailures - - local xml = '\n' - - local status = '
' - if self.totals[2] == 0 then status = '
' end - local html = [[ - - - - - - ]] - local wrap_cls = '' - if self.totals[2] > 0 then wrap_cls = 'fail' end - html = html .. '

' .. status .. ' love.test report

' .. - '

Renderer: ' .. name .. ' | ' .. version .. ' | ' .. vendor .. ' | ' .. device .. '

' .. - '
    ' - html = html .. - '
  • ' .. tostring(self.totals[1]) .. ' Passed
  • ' .. - '
  • ' .. tostring(self.totals[2]) .. ' Failed
  • ' .. - '
  • ' .. tostring(self.totals[3]) .. ' Skipped
  • ' .. - '
  • ' .. finaltime .. 's


' - - love.filesystem.write('tempoutput/' .. self.output .. '.xml', xml .. self.xml .. '') - love.filesystem.write('tempoutput/' .. self.output .. '.html', html .. self.html .. '
') - love.filesystem.write('tempoutput/' .. self.output .. '.md', md) - - self.module:log('grey', '\nFINISHED - ' .. finaltime .. 's\n') - local failedcol = '\27[31m' - if self.totals[2] == 0 then failedcol = '\27[37m' end - self.module:log('green', tostring(self.totals[1]) .. ' PASSED' .. ' || ' .. failedcol .. tostring(self.totals[2]) .. ' FAILED || \27[37m' .. tostring(self.totals[3]) .. ' SKIPPED') - - end - - -} - - - -================================================ -File: examples/lovetest_runAllTests.html -================================================ -

🔴 love.test

  • 🟢 343 Tests
  • 🔴 2 Failures
  • 🟡 10 Skipped
  • 14.567s


🟢 love.audio

  • 🟢 31 Tests
  • 🔴 0 Failures
  • 🟡 0 Skipped
  • 1.328s


    • MethodTimeDetails
      🟢RecordingDevice0.816s
      🟢Source0.021s
      🟢getActiveEffects0.018s
      🟢getActiveSourceCount0.018s
      🟢getDistanceModel0.017s
      🟢getDopplerScale0.017s
      🟢getEffect0.016s
      🟢getMaxSceneEffects0.017s
      🟢getMaxSourceEffects0.017s
      🟢getOrientation0.017s
      🟢getPlaybackDevice0.017s
      🟢getPlaybackDevices0.017s
      🟢getPosition0.017s
      🟢getRecordingDevices0.017s
      🟢getVelocity0.017s
      🟢getVolume0.017s
      🟢isEffectsSupported0.017s
      🟢newQueueableSource0.017s
      🟢newSource0.018s
      🟢pause0.018s
      🟢play0.018s
      🟢setDistanceModel0.017s
      🟢setDopplerScale0.017s
      🟢setEffect0.017s
      🟢setMixWithSystem0.017s
      🟢setOrientation0.017s
      🟢setPlaybackDevice0.017s
      🟢setPosition0.017s
      🟢setVelocity0.017s
      🟢setVolume0.017s
      🟢stop0.018s

      🟢 love.data

      • 🟢 12 Tests
      • 🔴 0 Failures
      • 🟡 0 Skipped
      • 0.197s


        • MethodTimeDetails
          🟢ByteData0.017s
          🟢CompressedData0.017s
          🟢compress0.016s
          🟢decode0.017s
          🟢decompress0.013s
          🟢encode0.017s
          🟢getPackedSize0.015s
          🟢hash0.019s
          🟢newByteData0.017s
          🟢newDataView0.017s
          🟢pack0.017s
          🟢unpack0.018s

          🟢 love.event

          • 🟢 4 Tests
          • 🔴 0 Failures
          • 🟡 2 Skipped
          • 0.100s


            • MethodTimeDetails
              🟢clear0.017s
              🟢poll0.017s
              🟡pump0.016sused internally
              🟢push0.017s
              🟢quit0.016s
              🟡wait0.018sused internally

              🟢 love.filesystem

              • 🟢 33 Tests
              • 🔴 0 Failures
              • 🟡 2 Skipped
              • 0.601s


                • MethodTimeDetails
                  🟢File0.018s
                  🟢FileData0.017s
                  🟢append0.019s
                  🟢areSymlinksEnabled0.017s
                  🟢createDirectory0.018s
                  🟢getAppdataDirectory0.017s
                  🟢getCRequirePath0.016s
                  🟢getDirectoryItems0.019s
                  🟢getFullCommonPath0.017s
                  🟢getIdentity0.017s
                  🟢getInfo0.019s
                  🟢getRealDirectory0.018s
                  🟢getRequirePath0.016s
                  🟢getSaveDirectory0.017s
                  🟡getSource0.018sused internally
                  🟢getSourceBaseDirectory0.017s
                  🟢getUserDirectory0.016s
                  🟢getWorkingDirectory0.018s
                  🟢isFused0.018s
                  🟢lines0.017s
                  🟢load0.018s
                  🟢mount0.017s
                  🟢mountCommonPath0.017s
                  🟢mountFullPath0.017s
                  🟢newFileData0.016s
                  🟢openFile0.017s
                  🟢read0.017s
                  🟢remove0.019s
                  🟢setCRequirePath0.017s
                  🟢setIdentity0.016s
                  🟢setRequirePath0.017s
                  🟡setSource0.016sused internally
                  🟢unmount0.018s
                  🟢unmountFullPath0.016s
                  🟢write0.018s

                  🟢 love.font

                  • 🟢 7 Tests
                  • 🔴 0 Failures
                  • 🟡 0 Skipped
                  • 0.116s


                    • MethodTimeDetails
                      🟢GlyphData0.016s
                      🟢Rasterizer0.018s
                      🟢newBMFontRasterizer0.016s
                      🟢newGlyphData0.016s
                      🟢newImageRasterizer0.017s
                      🟢newRasterizer0.017s
                      🟢newTrueTypeRasterizer0.016s

                      🔴 love.graphics

                      • 🟢 104 Tests
                      • 🔴 1 Failures
                      • 🟡 2 Skipped
                      • 3.463s


                        • MethodTimeDetails
                          🟢Buffer0.017s
                          🟢Canvas0.086s

                          Expected

                          Actual

                          Expected

                          Actual

                          🟢Font0.021s

                          Expected

                          Actual

                          Expected

                          Actual

                          🟢Image0.041s

                          Expected

                          Actual

                          🟢Mesh0.018s
                          🟢ParticleSystem0.040s

                          Expected

                          Actual

                          🟢Quad0.025s

                          Expected

                          Actual

                          🔴Shader0.028sassert 4 [check shader valid] expected '' got 'vertex shader: -pixel shader: -'

                          Expected

                          Actual

                          🟡ShaderStorageBuffer0.017sGLSL 4 and shader storage buffers are not supported on this system
                          🟢SpriteBatch0.085s

                          Expected

                          Actual

                          Expected

                          Actual

                          Expected

                          Actual

                          🟢Text0.026s

                          Expected

                          Actual

                          🟢Video0.885s

                          Expected

                          Actual

                          🟢applyTransform0.034s

                          Expected

                          Actual

                          🟢arc0.021s

                          Expected

                          Actual

                          Expected

                          Actual

                          Expected

                          Actual

                          🟢captureScreenshot0.349s
                          🟢circle0.018s

                          Expected

                          Actual

                          🟢clear0.018s

                          Expected

                          Actual

                          🟡discard0.017scant test this worked
                          🟢draw0.032s

                          Expected

                          Actual

                          🟢drawInstanced0.034s

                          Expected

                          Actual

                          🟢drawLayer0.031s

                          Expected

                          Actual

                          🟢ellipse0.019s

                          Expected

                          Actual

                          🟢flushBatch0.018s
                          🟢getBackgroundColor0.017s
                          🟢getBlendMode0.017s
                          🟢getCanvas0.017s
                          🟢getColor0.017s
                          🟢getColorMask0.017s
                          🟢getDPIScale0.016s
                          🟢getDefaultFilter0.016s
                          🟢getDepthMode0.016s
                          🟢getDimensions0.017s
                          🟢getFont0.017s
                          🟢getFrontFaceWinding0.017s
                          🟢getHeight0.016s
                          🟢getLineJoin0.017s
                          🟢getLineStyle0.017s
                          🟢getLineWidth0.017s
                          🟢getMeshCullMode0.017s
                          🟢getPixelDimensions0.017s
                          🟢getPixelHeight0.018s
                          🟢getPixelWidth0.016s
                          🟢getPointSize0.017s
                          🟢getRendererInfo0.016s
                          🟢getScissor0.017s
                          🟢getShader0.016s
                          🟢getStackDepth0.018s
                          🟢getStats0.017s
                          🟢getStencilState0.017s
                          🟢getSupported0.017s
                          🟢getSystemLimits0.017s
                          🟢getTextureFormats0.017s
                          🟢getTextureTypes0.017s
                          🟢getWidth0.017s
                          🟢intersectScissor0.018s

                          Expected

                          Actual

                          🟢inverseTransformPoint0.017s
                          🟢isActive0.017s
                          🟢isGammaCorrect0.017s
                          🟢isWireframe0.017s
                          🟢line0.018s

                          Expected

                          Actual

                          🟢newArrayImage0.018s
                          🟢newCanvas0.017s
                          🟢newCubeImage0.020s
                          🟢newFont0.018s
                          🟢newImage0.016s
                          🟢newImageFont0.018s
                          🟢newMesh0.017s
                          🟢newParticleSystem0.017s
                          🟢newQuad0.017s
                          🟢newShader0.028s
                          🟢newSpriteBatch0.017s
                          🟢newTextBatch0.018s
                          🟢newVideo0.021s
                          🟢newVolumeImage0.018s
                          🟢origin0.019s

                          Expected

                          Actual

                          🟢points0.022s

                          Expected

                          Actual

                          🟢polygon0.017s

                          Expected

                          Actual

                          🟢pop0.017s

                          Expected

                          Actual

                          🟢print0.028s

                          Expected

                          Actual

                          🟢printf0.020s

                          Expected

                          Actual

                          🟢push0.019s

                          Expected

                          Actual

                          🟢rectangle0.024s

                          Expected

                          Actual

                          Expected

                          Actual

                          🟢replaceTransform0.020s

                          Expected

                          Actual

                          🟢reset0.017s
                          🟢rotate0.035s

                          Expected

                          Actual

                          🟢scale0.019s

                          Expected

                          Actual

                          🟢setBackgroundColor0.017s
                          🟢setBlendMode0.038s

                          Expected

                          Actual

                          🟢setCanvas0.018s

                          Expected

                          Actual

                          🟢setColor0.020s

                          Expected

                          Actual

                          🟢setColorMask0.028s

                          Expected

                          Actual

                          🟢setDefaultFilter0.017s
                          🟢setDepthMode0.017s
                          🟢setFont0.020s

                          Expected

                          Actual

                          🟢setFrontFaceWinding0.017s
                          🟢setLineJoin0.033s

                          Expected

                          Actual

                          🟢setLineStyle0.019s

                          Expected

                          Actual

                          🟢setLineWidth0.019s

                          Expected

                          Actual

                          🟢setMeshCullMode0.018s
                          🟢setScissor0.019s

                          Expected

                          Actual

                          🟢setShader0.031s

                          Expected

                          Actual

                          🟢setStencilState0.018s

                          Expected

                          Actual

                          🟢setWireframe0.018s

                          Expected

                          Actual

                          🟢shear0.020s

                          Expected

                          Actual

                          Expected

                          Actual

                          🟢transformPoint0.017s
                          🟢translate0.018s

                          Expected

                          Actual

                          🟢validateShader0.026s

                          🟢 love.image

                          • 🟢 5 Tests
                          • 🔴 0 Failures
                          • 🟡 0 Skipped
                          • 0.093s


                            • MethodTimeDetails
                              🟢CompressedImageData0.017s
                              🟢ImageData0.026s
                              🟢isCompressed0.017s
                              🟢newCompressedData0.017s
                              🟢newImageData0.017s

                              🟢 love.joystick

                              • 🟢 6 Tests
                              • 🔴 0 Failures
                              • 🟡 0 Skipped
                              • 0.116s


                                • MethodTimeDetails
                                  🟢getGamepadMappingString0.017s
                                  🟢getJoystickCount0.017s
                                  🟢getJoysticks0.017s
                                  🟢loadGamepadMappings0.023s
                                  🟢saveGamepadMappings0.024s
                                  🟢setGamepadMapping0.018s

                                  🟢 love.keyboard

                                  • 🟢 10 Tests
                                  • 🔴 0 Failures
                                  • 🟡 0 Skipped
                                  • 0.170s


                                    • MethodTimeDetails
                                      🟢getKeyFromScancode0.016s
                                      🟢getScancodeFromKey0.018s
                                      🟢hasKeyRepeat0.018s
                                      🟢hasScreenKeyboard0.017s
                                      🟢hasTextInput0.017s
                                      🟢isDown0.017s
                                      🟢isModifierActive0.017s
                                      🟢isScancodeDown0.018s
                                      🟢setKeyRepeat0.018s
                                      🟢setTextInput0.017s

                                      🟢 love.love

                                      • 🟢 6 Tests
                                      • 🔴 0 Failures
                                      • 🟡 0 Skipped
                                      • 0.100s


                                        • MethodTimeDetails
                                          🟢errhand0.017s
                                          🟢getVersion0.016s
                                          🟢hasDeprecationOutput0.017s
                                          🟢isVersionCompatible0.017s
                                          🟢run0.017s
                                          🟢setDeprecationOutput0.017s

                                          🟢 love.math

                                          • 🟢 20 Tests
                                          • 🔴 0 Failures
                                          • 🟡 0 Skipped
                                          • 0.334s


                                            • MethodTimeDetails
                                              🟢BezierCurve0.017s
                                              🟢RandomGenerator0.016s
                                              🟢Transform0.016s
                                              🟢colorFromBytes0.017s
                                              🟢colorToBytes0.016s
                                              🟢gammaToLinear0.017s
                                              🟢getRandomSeed0.018s
                                              🟢getRandomState0.016s
                                              🟢isConvex0.017s
                                              🟢linearToGamma0.017s
                                              🟢newBezierCurve0.016s
                                              🟢newRandomGenerator0.017s
                                              🟢newTransform0.016s
                                              🟢perlinNoise0.017s
                                              🟢random0.017s
                                              🟢randomNormal0.017s
                                              🟢setRandomSeed0.017s
                                              🟢setRandomState0.017s
                                              🟢simplexNoise0.017s
                                              🟢triangulate0.017s

                                              🔴 love.mouse

                                              • 🟢 17 Tests
                                              • 🔴 1 Failures
                                              • 🟡 0 Skipped
                                              • 0.301s


                                                • MethodTimeDetails
                                                  🟢getCursor0.016s
                                                  🟢getPosition0.017s
                                                  🟢getRelativeMode0.016s
                                                  🟢getSystemCursor0.017s
                                                  🟢getX0.017s
                                                  🟢getY0.016s
                                                  🟢isCursorSupported0.017s
                                                  🟢isDown0.016s
                                                  🟢isGrabbed0.017s
                                                  🟢isVisible0.017s
                                                  🟢newCursor0.016s
                                                  🟢setCursor0.017s
                                                  🔴setGrabbed0.016sassert 2 [check now grabbed] expected 'true' got 'false'
                                                  🟢setPosition0.018s
                                                  🟢setRelativeMode0.017s
                                                  🟢setVisible0.018s
                                                  🟢setX0.018s
                                                  🟢setY0.017s

                                                  🟢 love.physics

                                                  • 🟢 26 Tests
                                                  • 🔴 0 Failures
                                                  • 🟡 0 Skipped
                                                  • 0.435s


                                                    • MethodTimeDetails
                                                      🟢Body0.017s
                                                      🟢Contact0.018s
                                                      🟢Joint0.017s
                                                      🟢Shape0.017s
                                                      🟢World0.016s
                                                      🟢getDistance0.017s
                                                      🟢getMeter0.017s
                                                      🟢newBody0.017s
                                                      🟢newChainShape0.013s
                                                      🟢newCircleShape0.017s
                                                      🟢newDistanceJoint0.017s
                                                      🟢newEdgeShape0.017s
                                                      🟢newFrictionJoint0.017s
                                                      🟢newGearJoint0.018s
                                                      🟢newMotorJoint0.017s
                                                      🟢newMouseJoint0.016s
                                                      🟢newPolygonShape0.017s
                                                      🟢newPrismaticJoint0.017s
                                                      🟢newPulleyJoint0.016s
                                                      🟢newRectangleShape0.018s
                                                      🟢newRevoluteJoint0.017s
                                                      🟢newRopeJoint0.017s
                                                      🟢newWeldJoint0.017s
                                                      🟢newWheelJoint0.018s
                                                      🟢newWorld0.018s
                                                      🟢setMeter0.016s

                                                      🟢 love.sensor

                                                      • 🟢 1 Tests
                                                      • 🔴 0 Failures
                                                      • 🟡 0 Skipped
                                                      • 0.017s


                                                        • MethodTimeDetails
                                                          🟢hasSensor0.017s

                                                          🟢 love.sound

                                                          • 🟢 4 Tests
                                                          • 🔴 0 Failures
                                                          • 🟡 0 Skipped
                                                          • 0.075s


                                                            • MethodTimeDetails
                                                              🟢Decoder0.018s
                                                              🟢SoundData0.022s
                                                              🟢newDecoder0.017s
                                                              🟢newSoundData0.019s

                                                              🟢 love.system

                                                              • 🟢 7 Tests
                                                              • 🔴 0 Failures
                                                              • 🟡 2 Skipped
                                                              • 0.150s


                                                                • MethodTimeDetails
                                                                  🟢getClipboardText0.016s
                                                                  🟢getOS0.017s
                                                                  🟢getPowerInfo0.017s
                                                                  🟢getPreferredLocales0.017s
                                                                  🟢getProcessorCount0.016s
                                                                  🟢hasBackgroundMusic0.017s
                                                                  🟡openURL0.017scant test this worked
                                                                  🟢setClipboardText0.016s
                                                                  🟡vibrate0.016scant test this worked

                                                                  🟢 love.thread

                                                                  • 🟢 5 Tests
                                                                  • 🔴 0 Failures
                                                                  • 🟡 0 Skipped
                                                                  • 0.306s


                                                                    • MethodTimeDetails
                                                                      🟢Channel0.231s
                                                                      🟢Thread0.024s
                                                                      🟢getChannel0.018s
                                                                      🟢newChannel0.017s
                                                                      🟢newThread0.015s

                                                                      🟢 love.timer

                                                                      • 🟢 6 Tests
                                                                      • 🔴 0 Failures
                                                                      • 🟡 0 Skipped
                                                                      • 0.298s


                                                                        • MethodTimeDetails
                                                                          🟢getAverageDelta0.017s
                                                                          🟢getDelta0.016s
                                                                          🟢getFPS0.015s
                                                                          🟢getTime0.118s
                                                                          🟢sleep0.117s
                                                                          🟢step0.016s

                                                                          🟢 love.touch

                                                                          • 🟢 3 Tests
                                                                          • 🔴 0 Failures
                                                                          • 🟡 0 Skipped
                                                                          • 0.051s


                                                                            • MethodTimeDetails
                                                                              🟢getPosition0.017s
                                                                              🟢getPressure0.018s
                                                                              🟢getTouches0.017s

                                                                              🟢 love.video

                                                                              • 🟢 2 Tests
                                                                              • 🔴 0 Failures
                                                                              • 🟡 0 Skipped
                                                                              • 0.038s


                                                                                • MethodTimeDetails
                                                                                  🟢VideoStream0.018s
                                                                                  🟢newVideoStream0.020s

                                                                                  🟢 love.window

                                                                                  • 🟢 34 Tests
                                                                                  • 🔴 0 Failures
                                                                                  • 🟡 2 Skipped
                                                                                  • 6.275s


                                                                                    • MethodTimeDetails
                                                                                      🟢focus0.017s
                                                                                      🟢fromPixels0.017s
                                                                                      🟢getDPIScale0.017s
                                                                                      🟢getDesktopDimensions0.016s
                                                                                      🟢getDisplayCount0.016s
                                                                                      🟢getDisplayName0.016s
                                                                                      🟢getDisplayOrientation0.016s
                                                                                      🟢getFullscreen1.351s
                                                                                      🟢getFullscreenModes0.013s
                                                                                      🟢getIcon0.019s
                                                                                      🟢getMode0.017s
                                                                                      🟢getPosition0.017s
                                                                                      🟢getSafeArea0.017s
                                                                                      🟢getTitle0.018s
                                                                                      🟢getVSync0.016s
                                                                                      🟢hasFocus0.017s
                                                                                      🟢hasMouseFocus0.017s
                                                                                      🟢isDisplaySleepEnabled0.016s
                                                                                      🟢isMaximized0.186s
                                                                                      🟢isMinimized0.816s
                                                                                      🟢isOpen0.014s
                                                                                      🟢isVisible0.017s
                                                                                      🟢maximize0.186s
                                                                                      🟢minimize0.807s
                                                                                      🟡requestAttention0.016scant test this worked
                                                                                      🟢restore0.966s
                                                                                      🟢setDisplaySleepEnabled0.018s
                                                                                      🟢setFullscreen1.356s
                                                                                      🟢setIcon0.015s
                                                                                      🟢setMode0.020s
                                                                                      🟢setPosition0.183s
                                                                                      🟢setTitle0.018s
                                                                                      🟢setVSync0.015s
                                                                                      🟡showMessageBox0.002scant test this worked
                                                                                      🟢toPixels0.002s
                                                                                      🟢updateMode0.010s
- - -================================================ -File: examples/lovetest_runAllTests.md -================================================ - - -### Info -**355** tests were completed in **14.567s** with **343** passed, **2** failed, and **10** skipped - -Renderer: OpenGL | 4.1 Metal - 76.3 | Apple | Apple M1 Max - -### Report -| Module | Pass | Fail | Skip | Time | -| --------------------- | ------ | ------ | ------- | ------ | -| 🟢 audio | 31 | 0 | 0 | 1.328s | -| 🟢 data | 12 | 0 | 0 | 0.197s | -| 🟢 event | 4 | 0 | 2 | 0.100s | -| 🟢 filesystem | 33 | 0 | 2 | 0.601s | -| 🟢 font | 7 | 0 | 0 | 0.116s | -| 🔴 graphics | 104 | 1 | 2 | 3.463s | -| 🟢 image | 5 | 0 | 0 | 0.093s | -| 🟢 joystick | 6 | 0 | 0 | 0.116s | -| 🟢 keyboard | 10 | 0 | 0 | 0.170s | -| 🟢 love | 6 | 0 | 0 | 0.100s | -| 🟢 math | 20 | 0 | 0 | 0.334s | -| 🔴 mouse | 17 | 1 | 0 | 0.301s | -| 🟢 physics | 26 | 0 | 0 | 0.435s | -| 🟢 sensor | 1 | 0 | 0 | 0.017s | -| 🟢 sound | 4 | 0 | 0 | 0.075s | -| 🟢 system | 7 | 0 | 2 | 0.150s | -| 🟢 thread | 5 | 0 | 0 | 0.306s | -| 🟢 timer | 6 | 0 | 0 | 0.298s | -| 🟢 touch | 3 | 0 | 0 | 0.051s | -| 🟢 video | 2 | 0 | 0 | 0.038s | -| 🟢 window | 34 | 0 | 2 | 6.275s | - -### Failures -> 🔴 Shader -> assert 4 [check shader valid] expected '' got 'vertex shader: -pixel shader: -' - -> 🔴 setGrabbed -> assert 2 [check now grabbed] expected 'true' got 'false' - - - - -================================================ -File: examples/lovetest_runAllTests.xml -================================================ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - assert 4 [check shader valid] expected '' got 'vertex shader: -pixel shader: -' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - assert 2 [check now grabbed] expected 'true' got 'false' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -================================================ -File: output/notes.txt -================================================ -# Testing Output -Any tests run will output an XML, MD, and HTML file here, assuming the tests are run with readwrite permissions for this repo - - -================================================ -File: output/actual/notes.txt -================================================ -# Actual Graphics Output -The images generated by the tests - - -================================================ -File: output/difference/notes.txt -================================================ -# Graphic Differences -If a graphics test fails then a "difference" image will be created to highlight -the differences between the actual + expected image, for use in the HTML report - - -================================================ -File: output/expected/notes.txt -================================================ -# Expected Graphics Output -The images expected by the tests - - -================================================ -File: resources/alsoft.conf -================================================ -# OpenAL config file. -# -# Option blocks may appear multiple times, and duplicated options will take the -# last value specified. Environment variables may be specified within option -# values, and are automatically substituted when the config file is loaded. -# Environment variable names may only contain alpha-numeric characters (a-z, -# A-Z, 0-9) and underscores (_), and are prefixed with $. For example, -# specifying "$HOME/file.ext" would typically result in something like -# "/home/user/file.ext". To specify an actual "$" character, use "$$". -# -# Device-specific values may be specified by including the device name in the -# block name, with "general" replaced by the device name. That is, general -# options for the device "Name of Device" would be in the [Name of Device] -# block, while ALSA options would be in the [alsa/Name of Device] block. -# Options marked as "(global)" are not influenced by the device. -# -# The system-wide settings can be put in /etc/openal/alsoft.conf and user- -# specific override settings in $HOME/.alsoftrc. -# For Windows, these settings should go into $AppData\alsoft.ini -# -# Option and block names are case-senstive. The supplied values are only hints -# and may not be honored (though generally it'll try to get as close as -# possible). Note: options that are left unset may default to app- or system- -# specified values. These are the current available settings: - -## -## General stuff -## -[general] - -## disable-cpu-exts: (global) -# Disables use of specialized methods that use specific CPU intrinsics. -# Certain methods may utilize CPU extensions for improved performance, and -# this option is useful for preventing some or all of those methods from being -# used. The available extensions are: sse, sse2, sse3, sse4.1, and neon. -# Specifying 'all' disables use of all such specialized methods. -#disable-cpu-exts = - -## drivers: (global) -# Sets the backend driver list order, comma-seperated. Unknown backends and -# duplicated names are ignored. Unlisted backends won't be considered for use -# unless the list is ended with a comma (e.g. 'oss,' will try OSS first before -# other backends, while 'oss' will try OSS only). Backends prepended with - -# won't be considered for use (e.g. '-oss,' will try all available backends -# except OSS). An empty list means to try all backends. -drivers = wave - -## channels: -# Sets the output channel configuration. If left unspecified, one will try to -# be detected from the system, and defaulting to stereo. The available values -# are: mono, stereo, quad, surround51, surround51rear, surround61, surround71, -# ambi1, ambi2, ambi3. Note that the ambi* configurations provide ambisonic -# channels of the given order (using ACN ordering and SN3D normalization by -# default), which need to be decoded to play correctly on speakers. -#channels = - -## sample-type: -# Sets the output sample type. Currently, all mixing is done with 32-bit float -# and converted to the output sample type as needed. Available values are: -# int8 - signed 8-bit int -# uint8 - unsigned 8-bit int -# int16 - signed 16-bit int -# uint16 - unsigned 16-bit int -# int32 - signed 32-bit int -# uint32 - unsigned 32-bit int -# float32 - 32-bit float -#sample-type = float32 - -## frequency: -# Sets the output frequency. If left unspecified it will try to detect a -# default from the system, otherwise it will default to 44100. -#frequency = - -## period_size: -# Sets the update period size, in frames. This is the number of frames needed -# for each mixing update. Acceptable values range between 64 and 8192. -#period_size = 1024 - -## periods: -# Sets the number of update periods. Higher values create a larger mix ahead, -# which helps protect against skips when the CPU is under load, but increases -# the delay between a sound getting mixed and being heard. Acceptable values -# range between 2 and 16. -#periods = 3 - -## stereo-mode: -# Specifies if stereo output is treated as being headphones or speakers. With -# headphones, HRTF or crossfeed filters may be used for better audio quality. -# Valid settings are auto, speakers, and headphones. -#stereo-mode = auto - -## stereo-encoding: -# Specifies the encoding method for non-HRTF stereo output. 'panpot' (default) -# uses standard amplitude panning (aka pair-wise, stereo pair, etc) between -# -30 and +30 degrees, while 'uhj' creates stereo-compatible two-channel UHJ -# output, which encodes some surround sound information into stereo output -# that can be decoded with a surround sound receiver. If crossfeed filters are -# used, UHJ is disabled. -#stereo-encoding = panpot - -## ambi-format: -# Specifies the channel order and normalization for the "ambi*" set of channel -# configurations. Valid settings are: fuma, acn+sn3d, acn+n3d -#ambi-format = acn+sn3d - -## hrtf: -# Controls HRTF processing. These filters provide better spatialization of -# sounds while using headphones, but do require a bit more CPU power. The -# default filters will only work with 44100hz or 48000hz stereo output. While -# HRTF is used, the cf_level option is ignored. Setting this to auto (default) -# will allow HRTF to be used when headphones are detected or the app requests -# it, while setting true or false will forcefully enable or disable HRTF -# respectively. -#hrtf = auto - -## default-hrtf: -# Specifies the default HRTF to use. When multiple HRTFs are available, this -# determines the preferred one to use if none are specifically requested. Note -# that this is the enumerated HRTF name, not necessarily the filename. -#default-hrtf = - -## hrtf-paths: -# Specifies a comma-separated list of paths containing HRTF data sets. The -# format of the files are described in docs/hrtf.txt. The files within the -# directories must have the .mhr file extension to be recognized. By default, -# OS-dependent data paths will be used. They will also be used if the list -# ends with a comma. On Windows this is: -# $AppData\openal\hrtf -# And on other systems, it's (in order): -# $XDG_DATA_HOME/openal/hrtf (defaults to $HOME/.local/share/openal/hrtf) -# $XDG_DATA_DIRS/openal/hrtf (defaults to /usr/local/share/openal/hrtf and -# /usr/share/openal/hrtf) -#hrtf-paths = - -## cf_level: -# Sets the crossfeed level for stereo output. Valid values are: -# 0 - No crossfeed -# 1 - Low crossfeed -# 2 - Middle crossfeed -# 3 - High crossfeed (virtual speakers are closer to itself) -# 4 - Low easy crossfeed -# 5 - Middle easy crossfeed -# 6 - High easy crossfeed -# Users of headphones may want to try various settings. Has no effect on non- -# stereo modes. -#cf_level = 0 - -## resampler: (global) -# Selects the resampler used when mixing sources. Valid values are: -# point - nearest sample, no interpolation -# linear - extrapolates samples using a linear slope between samples -# cubic - extrapolates samples using a Catmull-Rom spline -# bsinc12 - extrapolates samples using a band-limited Sinc filter (varying -# between 12 and 24 points, with anti-aliasing) -# bsinc24 - extrapolates samples using a band-limited Sinc filter (varying -# between 24 and 48 points, with anti-aliasing) -#resampler = linear - -## rt-prio: (global) -# Sets real-time priority for the mixing thread. Not all drivers may use this -# (eg. PortAudio) as they already control the priority of the mixing thread. -# 0 and negative values will disable it. Note that this may constitute a -# security risk since a real-time priority thread can indefinitely block -# normal-priority threads if it fails to wait. As such, the default is -# disabled. -#rt-prio = 0 - -## sources: -# Sets the maximum number of allocatable sources. Lower values may help for -# systems with apps that try to play more sounds than the CPU can handle. -#sources = 256 - -## slots: -# Sets the maximum number of Auxiliary Effect Slots an app can create. A slot -# can use a non-negligible amount of CPU time if an effect is set on it even -# if no sources are feeding it, so this may help when apps use more than the -# system can handle. -#slots = 64 - -## sends: -# Limits the number of auxiliary sends allowed per source. Setting this higher -# than the default has no effect. -#sends = 16 - -## front-stablizer: -# Applies filters to "stablize" front sound imaging. A psychoacoustic method -# is used to generate a front-center channel signal from the front-left and -# front-right channels, improving the front response by reducing the combing -# artifacts and phase errors. Consequently, it will only work with channel -# configurations that include front-left, front-right, and front-center. -#front-stablizer = false - -## output-limiter: -# Applies a gain limiter on the final mixed output. This reduces the volume -# when the output samples would otherwise clamp, avoiding excessive clipping -# noise. -#output-limiter = true - -## dither: -# Applies dithering on the final mix, for 8- and 16-bit output by default. -# This replaces the distortion created by nearest-value quantization with low- -# level whitenoise. -#dither = true - -## dither-depth: -# Quantization bit-depth for dithered output. A value of 0 (or less) will -# match the output sample depth. For int32, uint32, and float32 output, 0 will -# disable dithering because they're at or beyond the rendered precision. The -# maximum dither depth is 24. -#dither-depth = 0 - -## volume-adjust: -# A global volume adjustment for source output, expressed in decibels. The -# value is logarithmic, so +6 will be a scale of (approximately) 2x, +12 will -# be a scale of 4x, etc. Similarly, -6 will be x1/2, and -12 is about x1/4. A -# value of 0 means no change. -#volume-adjust = 0 - -## excludefx: (global) -# Sets which effects to exclude, preventing apps from using them. This can -# help for apps that try to use effects which are too CPU intensive for the -# system to handle. Available effects are: eaxreverb,reverb,autowah,chorus, -# compressor,distortion,echo,equalizer,flanger,modulator,dedicated,pshifter, -# fshifter -#excludefx = - -## default-reverb: (global) -# A reverb preset that applies by default to all sources on send 0 -# (applications that set their own slots on send 0 will override this). -# Available presets are: None, Generic, PaddedCell, Room, Bathroom, -# Livingroom, Stoneroom, Auditorium, ConcertHall, Cave, Arena, Hangar, -# CarpetedHallway, Hallway, StoneCorridor, Alley, Forest, City, Moutains, -# Quarry, Plain, ParkingLot, SewerPipe, Underwater, Drugged, Dizzy, Psychotic. -#default-reverb = - -## trap-alc-error: (global) -# Generates a SIGTRAP signal when an ALC device error is generated, on systems -# that support it. This helps when debugging, while trying to find the cause -# of a device error. On Windows, a breakpoint exception is generated. -#trap-alc-error = false - -## trap-al-error: (global) -# Generates a SIGTRAP signal when an AL context error is generated, on systems -# that support it. This helps when debugging, while trying to find the cause -# of a context error. On Windows, a breakpoint exception is generated. -#trap-al-error = false - -## -## Ambisonic decoder stuff -## -[decoder] - -## hq-mode: -# Enables a high-quality ambisonic decoder. This mode is capable of frequency- -# dependent processing, creating a better reproduction of 3D sound rendering -# over surround sound speakers. Enabling this also requires specifying decoder -# configuration files for the appropriate speaker configuration you intend to -# use (see the quad, surround51, etc options below). Currently, up to third- -# order decoding is supported. -hq-mode = false - -## distance-comp: -# Enables compensation for the speakers' relative distances to the listener. -# This applies the necessary delays and attenuation to make the speakers -# behave as though they are all equidistant, which is important for proper -# playback of 3D sound rendering. Requires the proper distances to be -# specified in the decoder configuration file. -distance-comp = true - -## nfc: -# Enables near-field control filters. This simulates and compensates for low- -# frequency effects caused by the curvature of nearby sound-waves, which -# creates a more realistic perception of sound distance. Note that the effect -# may be stronger or weaker than intended if the application doesn't use or -# specify an appropriate unit scale, or if incorrect speaker distances are set -# in the decoder configuration file. Requires hq-mode to be enabled. -nfc = true - -## nfc-ref-delay -# Specifies the reference delay value for ambisonic output. When channels is -# set to one of the ambi* formats, this option enables NFC-HOA output with the -# specified Reference Delay parameter. The specified value can then be shared -# with an appropriate NFC-HOA decoder to reproduce correct near-field effects. -# Keep in mind that despite being designed for higher-order ambisonics, this -# applies to first-order output all the same. When left unset, normal output -# is created with no near-field simulation. -nfc-ref-delay = - -## quad: -# Decoder configuration file for Quadraphonic channel output. See -# docs/ambdec.txt for a description of the file format. -quad = - -## surround51: -# Decoder configuration file for 5.1 Surround (Side and Rear) channel output. -# See docs/ambdec.txt for a description of the file format. -surround51 = - -## surround61: -# Decoder configuration file for 6.1 Surround channel output. See -# docs/ambdec.txt for a description of the file format. -surround61 = - -## surround71: -# Decoder configuration file for 7.1 Surround channel output. See -# docs/ambdec.txt for a description of the file format. Note: This can be used -# to enable 3D7.1 with the appropriate configuration and speaker placement, -# see docs/3D7.1.txt. -surround71 = - -## -## Reverb effect stuff (includes EAX reverb) -## -[reverb] - -## boost: (global) -# A global amplification for reverb output, expressed in decibels. The value -# is logarithmic, so +6 will be a scale of (approximately) 2x, +12 will be a -# scale of 4x, etc. Similarly, -6 will be about half, and -12 about 1/4th. A -# value of 0 means no change. -#boost = 0 - -## -## PulseAudio backend stuff -## -[pulse] - -## spawn-server: (global) -# Attempts to autospawn a PulseAudio server whenever needed (initializing the -# backend, enumerating devices, etc). Setting autospawn to false in Pulse's -# client.conf will still prevent autospawning even if this is set to true. -#spawn-server = true - -## allow-moves: (global) -# Allows PulseAudio to move active streams to different devices. Note that the -# device specifier (seen by applications) will not be updated when this -# occurs, and neither will the AL device configuration (sample rate, format, -# etc). -#allow-moves = false - -## fix-rate: -# Specifies whether to match the playback stream's sample rate to the device's -# sample rate. Enabling this forces OpenAL Soft to mix sources and effects -# directly to the actual output rate, avoiding a second resample pass by the -# PulseAudio server. -#fix-rate = false - -## -## ALSA backend stuff -## -[alsa] - -## device: (global) -# Sets the device name for the default playback device. -#device = default - -## device-prefix: (global) -# Sets the prefix used by the discovered (non-default) playback devices. This -# will be appended with "CARD=c,DEV=d", where c is the card id and d is the -# device index for the requested device name. -#device-prefix = plughw: - -## device-prefix-*: (global) -# Card- and device-specific prefixes may be used to override the device-prefix -# option. The option may specify the card id (eg, device-prefix-NVidia), or -# the card id and device index (eg, device-prefix-NVidia-0). The card id is -# case-sensitive. -#device-prefix- = - -## capture: (global) -# Sets the device name for the default capture device. -#capture = default - -## capture-prefix: (global) -# Sets the prefix used by the discovered (non-default) capture devices. This -# will be appended with "CARD=c,DEV=d", where c is the card id and d is the -# device number for the requested device name. -#capture-prefix = plughw: - -## capture-prefix-*: (global) -# Card- and device-specific prefixes may be used to override the -# capture-prefix option. The option may specify the card id (eg, -# capture-prefix-NVidia), or the card id and device index (eg, -# capture-prefix-NVidia-0). The card id is case-sensitive. -#capture-prefix- = - -## mmap: -# Sets whether to try using mmap mode (helps reduce latencies and CPU -# consumption). If mmap isn't available, it will automatically fall back to -# non-mmap mode. True, yes, on, and non-0 values will attempt to use mmap. 0 -# and anything else will force mmap off. -#mmap = true - -## allow-resampler: -# Specifies whether to allow ALSA's built-in resampler. Enabling this will -# allow the playback device to be set to a different sample rate than the -# actual output, causing ALSA to apply its own resampling pass after OpenAL -# Soft resamples and mixes the sources and effects for output. -#allow-resampler = false - -## -## OSS backend stuff -## -[oss] - -## device: (global) -# Sets the device name for OSS output. -#device = /dev/dsp - -## capture: (global) -# Sets the device name for OSS capture. -#capture = /dev/dsp - -## -## Solaris backend stuff -## -[solaris] - -## device: (global) -# Sets the device name for Solaris output. -#device = /dev/audio - -## -## QSA backend stuff -## -[qsa] - -## -## JACK backend stuff -## -[jack] - -## spawn-server: (global) -# Attempts to autospawn a JACK server whenever needed (initializing the -# backend, opening devices, etc). -#spawn-server = false - -## buffer-size: -# Sets the update buffer size, in samples, that the backend will keep buffered -# to handle the server's real-time processing requests. This value must be a -# power of 2, or else it will be rounded up to the next power of 2. If it is -# less than JACK's buffer update size, it will be clamped. This option may -# be useful in case the server's update size is too small and doesn't give the -# mixer time to keep enough audio available for the processing requests. -#buffer-size = 0 - -## -## WASAPI backend stuff -## -[wasapi] - -## -## DirectSound backend stuff -## -[dsound] - -## -## Windows Multimedia backend stuff -## -[winmm] - -## -## PortAudio backend stuff -## -[port] - -## device: (global) -# Sets the device index for output. Negative values will use the default as -# given by PortAudio itself. -#device = -1 - -## capture: (global) -# Sets the device index for capture. Negative values will use the default as -# given by PortAudio itself. -#capture = -1 - -## -## Wave File Writer stuff -## -[wave] - -## file: (global) -# Sets the filename of the wave file to write to. An empty name prevents the -# backend from opening, even when explicitly requested. -# THIS WILL OVERWRITE EXISTING FILES WITHOUT QUESTION! -file = output.wav - -## bformat: (global) -# Creates AMB format files using first-order ambisonics instead of a standard -# single- or multi-channel .wav file. -#bformat = false - - -================================================ -File: resources/click.ogg -================================================ -[Non-text file] - - -================================================ -File: resources/clickmono.ogg -================================================ -[Non-text file] - - -================================================ -File: resources/font.bmp -================================================ -[Non-text file] - - -================================================ -File: resources/font.ttf -================================================ -[Non-text file] - - -================================================ -File: resources/love.dxt1 -================================================ -[Non-text file] - - -================================================ -File: resources/mappings.txt -================================================ -03000000300f00000a01000000000000,3 In 1 Conversion Box,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b8,x:b3,y:b0,platform:Windows, -03000000fa2d00000100000000000000,3dRudder Foot Motion Controller,leftx:a0,lefty:a1,rightx:a5,righty:a2,platform:Windows, -03000000d0160000040d000000000000,4Play Adapter,a:b1,b:b3,back:b4,dpdown:b11,dpleft:b12,dpright:b13,dpup:b10,leftshoulder:b6,leftstick:b14,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b15,righttrigger:b9,rightx:a3,righty:a4,start:b5,x:b0,y:b2,platform:Windows, -03000000d0160000050d000000000000,4Play Adapter,a:b1,b:b3,back:b4,dpdown:b11,dpleft:b12,dpright:b13,dpup:b10,leftshoulder:b6,leftstick:b14,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b15,righttrigger:b9,rightx:a3,righty:a4,start:b5,x:b0,y:b2,platform:Windows, -03000000d0160000060d000000000000,4Play Adapter,a:b1,b:b3,back:b4,dpdown:b11,dpleft:b12,dpright:b13,dpup:b10,leftshoulder:b6,leftstick:b14,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b15,righttrigger:b9,rightx:a3,righty:a4,start:b5,x:b0,y:b2,platform:Windows, -03000000d0160000070d000000000000,4Play Adapter,a:b1,b:b3,back:b4,dpdown:b11,dpleft:b12,dpright:b13,dpup:b10,leftshoulder:b6,leftstick:b14,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b15,righttrigger:b9,rightx:a3,righty:a4,start:b5,x:b0,y:b2,platform:Windows, -03000000d0160000600a000000000000,4Play Adapter,a:b1,b:b3,back:b4,dpdown:b11,dpleft:b12,dpright:b13,dpup:b10,leftshoulder:b6,leftstick:b14,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b15,righttrigger:b9,rightx:a3,righty:a4,start:b5,x:b0,y:b2,platform:Windows, -03000000c82d00000031000000000000,8BitDo Adapter,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d00000531000000000000,8BitDo Adapter 2,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d00000951000000000000,8BitDo Dogbone,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a2,rightx:a3,righty:a5,start:b11,platform:Windows, -03000000008000000210000000000000,8BitDo F30,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Windows, -030000003512000011ab000000000000,8BitDo F30 Arcade Joystick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000c82d00001028000000000000,8BitDo F30 Arcade Joystick,a:b0,b:b1,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d000011ab000000000000,8BitDo F30 Arcade Joystick,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows, -03000000801000000900000000000000,8BitDo F30 Arcade Stick,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d00001038000000000000,8BitDo F30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000090000000000000,8BitDo FC30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00001251000000000000,8BitDo Lite 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00001151000000000000,8BitDo Lite SE,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000150000000000000,8BitDo M30,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:b7,rightx:a3,righty:a5,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000151000000000000,8BitDo M30,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a2,rightshoulder:b6,righttrigger:b7,rightx:a3,righty:a5,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d00000650000000000000,8BitDo M30,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b8,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:b7,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d00005106000000000000,8BitDo M30,a:b0,b:b1,back:b10,dpdown:+a2,dpleft:-a0,dpright:+a0,dpup:-a2,guide:b2,leftshoulder:b8,lefttrigger:b9,rightshoulder:b6,righttrigger:b7,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d00002090000000000000,8BitDo Micro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000310000000000000,8BitDo N30,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d00000451000000000000,8BitDo N30,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a2,rightx:a3,righty:a5,start:b11,platform:Windows, -03000000c82d00002028000000000000,8BitDo N30,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00008010000000000000,8BitDo N30,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d0000e002000000000000,8BitDo N30,a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a1,start:b6,platform:Windows, -03000000c82d00000190000000000000,8BitDo N30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00001590000000000000,8BitDo N30 Pro 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00006528000000000000,8BitDo N30 Pro 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000290000000000000,8BitDo N64,+rightx:b9,+righty:b3,-rightx:b4,-righty:b8,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,platform:Windows, -03000000c82d00003038000000000000,8BitDo N64,+rightx:b9,+righty:b3,-rightx:b4,-righty:b8,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,platform:Windows, -03000000c82d00006928000000000000,8BitDo N64,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a3,righty:a4,start:b11,platform:Windows, -03000000c82d00002590000000000000,8BitDo NEOGEO,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -030000003512000012ab000000000000,8BitDo NES30,a:b2,b:b1,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b3,y:b0,platform:Windows, -03000000c82d000012ab000000000000,8BitDo NES30,a:b1,b:b0,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Windows, -03000000022000000090000000000000,8BitDo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows, -03000000203800000900000000000000,8BitDo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00002038000000000000,8BitDo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000751000000000000,8BitDo P30,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a2,rightshoulder:b7,righttrigger:b9,rightx:a3,righty:a5,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d00000851000000000000,8BitDo P30,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a2,rightshoulder:b7,righttrigger:b9,rightx:a3,righty:a5,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d00000360000000000000,8BitDo Pro 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000361000000000000,8BitDo Pro 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000660000000000000,8BitDo Pro 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000131000000000000,8BitDo Receiver,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000231000000000000,8BitDo Receiver,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000331000000000000,8BitDo Receiver,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000431000000000000,8BitDo Receiver,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00002867000000000000,8BitDo S30,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b8,lefttrigger:b9,leftx:a0,lefty:a2,rightshoulder:b6,righttrigger:b7,rightx:a3,righty:a5,start:b10,x:b3,y:b4,platform:Windows, -03000000c82d00000130000000000000,8BitDo SF30,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000060000000000000,8BitDo SF30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000061000000000000,8BitDo SF30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Windows, -03000000102800000900000000000000,8BitDo SFC30,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d000021ab000000000000,8BitDo SFC30,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00003028000000000000,8BitDo SFC30,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Windows, -030000003512000020ab000000000000,8BitDo SN30,a:b0,b:b1,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000030000000000000,8BitDo SN30,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000351000000000000,8BitDo SN30,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a2,rightshoulder:b7,rightx:a3,righty:a5,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00001290000000000000,8BitDo SN30,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d000020ab000000000000,8BitDo SN30,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00004028000000000000,8BitDo SN30,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00006228000000000000,8BitDo SN30,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000021000000000000,8BitDo SN30 Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d00000160000000000000,8BitDo SN30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000161000000000000,8BitDo SN30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000260000000000000,8BitDo SN30 Pro Plus,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00000261000000000000,8BitDo SN30 Pro Plus,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00001230000000000000,8BitDo Ultimate,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,paddle1:b2,paddle2:b5,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d00001530000000000000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d00001630000000000000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d00001730000000000000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d00001130000000000000,8BitDo Ultimate Wired,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,misc1:b26,paddle1:b24,paddle2:b25,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d00001330000000000000,8BitDo Ultimate Wireless,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b26,paddle1:b23,paddle2:b19,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d00000121000000000000,8BitDo Xbox One SN30 Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000a00500003232000000000000,8BitDo Zero,a:b0,b:b1,back:b10,dpdown:+a2,dpleft:-a0,dpright:+a0,dpup:-a2,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Windows, -03000000c82d00001890000000000000,8BitDo Zero 2,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Windows, -03000000c82d00003032000000000000,8BitDo Zero 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Windows, -030000008f0e00001200000000000000,Acme GA02,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b2,y:b3,platform:Windows, -03000000c01100000355000000000000,Acrux,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000fa190000f0ff000000000000,Acteck AGJ 3200,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000d1180000402c000000000000,ADT1,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a3,rightx:a2,righty:a5,x:b3,y:b4,platform:Windows, -030000006f0e00008801000000000000,Afterglow Deluxe Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000341a00003608000000000000,Afterglow PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006f0e00000263000000000000,Afterglow PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006f0e00001101000000000000,Afterglow PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006f0e00001401000000000000,Afterglow PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006f0e00001402000000000000,Afterglow PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006f0e00001901000000000000,Afterglow PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006f0e00001a01000000000000,Afterglow PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006f0e00001301000000000000,Afterglow Xbox Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000006f0e00001302000000000000,Afterglow Xbox Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000006f0e00001304000000000000,Afterglow Xbox Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000006f0e00001413000000000000,Afterglow Xbox Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006f0e00003901000000000000,Afterglow Xbox Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000ab1200000103000000000000,Afterglow Xbox Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000ad1b000000f9000000000000,Afterglow Xbox Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000100000008200000000000000,Akishop Customs PS360,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows, -030000007c1800000006000000000000,Alienware Dual Compatible PlayStation Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b3,platform:Windows, -03000000491900001904000000000000,Amazon Luna Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,misc1:b9,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b7,x:b2,y:b3,platform:Windows, -03000000710100001904000000000000,Amazon Luna Controller,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b11,leftshoulder:b5,leftstick:b8,leftx:a0,lefty:a1,misc1:b9,rightshoulder:b4,rightstick:b7,rightx:a3,righty:a4,start:b6,x:b3,y:b2,platform:Windows, -03000000830500000160000000000000,Arcade,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b2,righttrigger:b3,x:b4,y:b4,platform:Windows, -03000000120c0000100e000000000000,Armor 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000490b00004406000000000000,ASCII Seamic Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b9,x:b3,y:b4,platform:Windows, -03000000869800002500000000000000,Astro C40 TR PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000a30c00002700000000000000,Astro City Mini,a:b2,b:b1,back:b8,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,rightshoulder:b4,righttrigger:b5,start:b9,x:b3,y:b0,platform:Windows, -03000000a30c00002800000000000000,Astro City Mini,a:b2,b:b1,back:b8,leftx:a3,lefty:a4,rightshoulder:b4,righttrigger:b5,start:b9,x:b3,y:b0,platform:Windows, -03000000050b00000579000000000000,ASUS ROG Kunai 3,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000050b00000679000000000000,ASUS ROG Kunai 3,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,misc1:b15,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000503200000110000000000000,Atari VCS Classic Controller,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,start:b3,platform:Windows, -03000000503200000210000000000000,Atari VCS Modern Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,rightx:a3,righty:a4,start:b9,x:b2,y:b3,platform:Windows, -03000000e4150000103f000000000000,Batarang,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000d6200000e557000000000000,Batarang PlayStation Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000c01100001352000000000000,Battalife Joystick,a:b6,b:b7,back:b2,leftshoulder:b0,leftx:a0,lefty:a1,rightshoulder:b1,start:b3,x:b4,y:b5,platform:Windows, -030000006f0e00003201000000000000,Battlefield 4 PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000ad1b000001f9000000000000,BB 070,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000d62000002a79000000000000,BDA PS4 Fightpad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000bc2000005250000000000000,Beitong G3,a:b0,b:b1,back:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b11,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b5,righttrigger:b9,rightx:a3,righty:a4,start:b15,x:b3,y:b4,platform:Windows, -030000000d0500000208000000000000,Belkin Nostromo N40,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:b7,rightx:a5,righty:a2,start:b9,x:b2,y:b3,platform:Windows, -03000000bc2000006012000000000000,Betop 2126F,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000bc2000000055000000000000,Betop BFM,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000bc2000006312000000000000,Betop Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000bc2000006321000000000000,Betop Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000bc2000006412000000000000,Betop Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000c01100000555000000000000,Betop Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000c01100000655000000000000,Betop Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000790000000700000000000000,Betop Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b3,y:b0,platform:Windows, -03000000808300000300000000000000,Betop Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b3,y:b0,platform:Windows, -030000006f0e00006401000000000000,BF One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,rightx:a2,righty:a5,start:b7,x:b2,y:b3,platform:Windows, -03000000300f00000202000000000000,Bigben,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a5,righty:a2,start:b7,x:b2,y:b3,platform:Windows, -030000006b1400000209000000000000,Bigben,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006b1400000055000000000000,Bigben PS3 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -030000006b1400000103000000000000,Bigben PS3 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Windows, -03000000120c0000200e000000000000,Brook Mars PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000210e000000000000,Brook Mars PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000f10e000000000000,Brook PS2 Adapter,a:b1,b:b2,back:b13,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000310c000000000000,Brook Super Converter,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,start:b9,x:b3,y:b0,platform:Windows, -03000000d81d00000b00000000000000,Buffalo BSGP1601 Series,a:b5,b:b3,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b8,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b9,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b13,x:b4,y:b2,platform:Windows, -030000005b1c00002400000000000000,Capcom Home Arcade Controller,a:b3,b:b4,back:b7,leftshoulder:b2,leftx:a0,lefty:a1,rightshoulder:b5,start:b6,x:b0,y:b1,platform:Windows, -030000005b1c00002500000000000000,Capcom Home Arcade Controller,a:b3,b:b4,back:b7,leftshoulder:b2,leftx:a0,lefty:a1,rightshoulder:b5,start:b6,x:b0,y:b1,platform:Windows, -030000006d04000042c2000000000000,ChillStream,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000e82000006058000000000000,Cideko AK08b,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000457500000401000000000000,Cobra,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000b0400003365000000000000,Competition Pro,a:b0,b:b1,back:b2,leftx:a0,lefty:a1,start:b3,platform:Windows, -030000004c050000c505000000000000,CronusMax Adapter,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000d814000007cd000000000000,Cthulhu,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000d8140000cefa000000000000,Cthulhu,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000260900008888000000000000,Cyber Gadget GameCube Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:a4,rightx:a2,righty:a3~,start:b7,x:b2,y:b3,platform:Windows, -030000003807000002cb000000000000,Cyborg,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000a306000022f6000000000000,Cyborg V.3 Rumble,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:+a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:-a3,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Windows, -03000000f806000000a3000000000000,DA Leader,a:b7,b:b6,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b0,leftstick:b8,lefttrigger:b1,leftx:a0,lefty:a1,rightshoulder:b2,rightstick:b9,righttrigger:b3,rightx:a2,righty:a3,start:b12,x:b4,y:b5,platform:Windows, -030000001a1c00000001000000000000,Datel Arcade Joystick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000451300000830000000000000,Defender Game Racer X7,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -03000000791d00000103000000000000,Dual Box Wii,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000c0160000e105000000000000,Dual Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Windows, -030000004f040000070f000000000000,Dual Power,a:b8,b:b9,back:b4,dpdown:b1,dpleft:b2,dpright:b3,dpup:b0,leftshoulder:b13,leftstick:b6,lefttrigger:b14,leftx:a0,lefty:a1,rightshoulder:b12,rightstick:b7,righttrigger:b15,start:b5,x:b10,y:b11,platform:Windows, -030000004f04000012b3000000000000,Dual Power 3,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Windows, -030000004f04000020b3000000000000,Dual Trigger,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Windows, -03000000bd12000002e0000000000000,Dual Vibration Joystick,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b9,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b10,righttrigger:b5,rightx:a3,righty:a2,start:b11,x:b3,y:b0,platform:Windows, -03000000ff1100003133000000000000,DualForce,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b0,y:b1,platform:Windows, -030000008f0e00000910000000000000,DualShock 2,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b9,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b10,righttrigger:b5,rightx:a3,righty:a2,start:b11,x:b3,y:b0,platform:Windows, -03000000317300000100000000000000,DualShock 3,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Windows, -030000006f0e00003001000000000000,EA Sports PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000fc0400000250000000000000,Easy Grip,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b9,x:b3,y:b4,platform:Windows, -030000006e0500000a20000000000000,Elecom DUX60 MMO,a:b2,b:b3,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b8,leftstick:b14,lefttrigger:b12,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b15,righttrigger:b13,rightx:a3,righty:a4,start:b20,x:b0,y:b1,platform:Windows, -03000000b80500000410000000000000,Elecom Gamepad,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b1,platform:Windows, -03000000b80500000610000000000000,Elecom Gamepad,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b1,platform:Windows, -030000006e0500000520000000000000,Elecom P301U PlayStation Controller Adapter,a:b2,b:b3,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,start:b11,x:b0,y:b1,platform:Windows, -03000000411200004450000000000000,Elecom U1012,a:b2,b:b3,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,start:b11,x:b0,y:b1,platform:Windows, -030000006e0500000320000000000000,Elecom U3613M,a:b2,b:b3,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,start:b11,x:b0,y:b1,platform:Windows, -030000006e0500000e20000000000000,Elecom U3912T,a:b2,b:b3,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,start:b11,x:b0,y:b1,platform:Windows, -030000006e0500000f20000000000000,Elecom U4013S,a:b2,b:b3,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,start:b11,x:b0,y:b1,platform:Windows, -030000006e0500001320000000000000,Elecom U4113,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006e0500001020000000000000,Elecom U4113S,a:b2,b:b3,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a3,righty:a2,start:b11,x:b0,y:b1,platform:Windows, -030000006e0500000720000000000000,Elecom W01U,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b1,platform:Windows, -030000007d0400000640000000000000,Eliminator AfterShock,a:b1,b:b2,back:b9,dpdown:+a3,dpleft:-a5,dpright:+a5,dpup:-a3,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a4,righty:a2,start:b8,x:b0,y:b3,platform:Windows, -03000000120c0000f61c000000000000,Elite,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000430b00000300000000000000,EMS Production PS2 Adapter,a:b2,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows, -03000000242f000000b7000000000000,ESM 9110,a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Windows, -03000000101c0000181c000000000000,Essential,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b4,leftx:a1,lefty:a0,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows, -030000008f0e00000f31000000000000,EXEQ,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Windows, -03000000341a00000108000000000000,EXEQ RF Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -030000006f0e00008401000000000000,Faceoff Deluxe Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006f0e00008101000000000000,Faceoff Deluxe Pro Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006f0e00008001000000000000,Faceoff Pro Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000021000000090000000000000,FC30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b8,leftstick:b13,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b9,rightstick:b14,righttrigger:b7,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows, -0300000011040000c600000000000000,FC801,a:b0,b:b1,back:b6,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,start:b7,x:b2,y:b3,platform:Windows, -03000000852100000201000000000000,FF GP1,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000ad1b000028f0000000000000,Fightpad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b11,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000ad1b00002ef0000000000000,Fightpad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000ad1b000038f0000000000000,Fightpad TE,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b8,rightshoulder:b5,righttrigger:b9,start:b7,x:b2,y:b3,platform:Windows, -03005036852100000000000000000000,Final Fantasy XIV Online Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000f806000001a3000000000000,Firestorm,a:b9,b:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b0,leftstick:b10,lefttrigger:b1,leftx:a0,lefty:a1,rightshoulder:b2,rightstick:b11,righttrigger:b3,start:b12,x:b8,y:b4,platform:Windows, -03000000b50700000399000000000000,Firestorm 2,a:b2,b:b4,back:b10,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b8,righttrigger:b9,start:b11,x:b3,y:b5,platform:Windows, -03000000b50700001302000000000000,Firestorm D3,a:b0,b:b2,leftshoulder:b4,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:b7,x:b1,y:b3,platform:Windows, -03000000b40400001024000000000000,Flydigi Apex,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Windows, -03000000151900004000000000000000,Flydigi Vader 2,a:b27,b:b26,back:b19,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b23,leftstick:b17,lefttrigger:b21,leftx:a0,lefty:a1,misc1:b15,paddle1:b11,paddle2:b10,paddle3:b13,paddle4:b12,rightshoulder:b22,rightstick:b16,righttrigger:b20,rightx:a3,righty:a4,start:b18,x:b25,y:b24,platform:Windows, -03000000b40400001124000000000000,Flydigi Vader 2,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b12,lefttrigger:b8,leftx:a0,lefty:a1,misc1:b14,paddle1:b4,paddle2:b5,paddle3:b16,paddle4:b17,rightshoulder:b7,rightstick:b13,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b2,y:b3,platform:Windows, -03000000b40400001224000000000000,Flydigi Vader 2,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b12,lefttrigger:b8,leftx:a0,lefty:a1,misc1:b2,paddle1:b16,paddle2:b17,paddle3:b14,paddle4:b15,rightshoulder:b7,rightstick:b13,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -030000008305000000a0000000000000,G08XU,a:b0,b:b1,back:b4,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b5,x:b2,y:b3,platform:Windows, -0300000066f700000100000000000000,Game VIB Joystick,a:b2,b:b3,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a3,righty:a2,start:b11,x:b0,y:b1,platform:Windows, -03000000260900002625000000000000,GameCube Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,lefttrigger:a4,leftx:a0,lefty:a1,righttrigger:a5,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Windows, -03000000341a000005f7000000000000,GameCube Controller,a:b2,b:b3,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b1,y:b0,platform:Windows, -03000000430b00000500000000000000,GameCube Controller,a:b0,b:b2,dpdown:b10,dpleft:b8,dpright:b9,dpup:b11,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:a3,rightx:a5,righty:a2,start:b7,x:b1,y:b3,platform:Windows, -03000000790000004718000000000000,GameCube Controller,a:b1,b:b0,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b2,y:b3,platform:Windows, -03000000790000004618000000000000,GameCube Controller Adapter,a:b1,b:b0,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b2,y:b3,platform:Windows, -030000008f0e00000d31000000000000,Gamepad 3 Turbo,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000ac0500003d03000000000000,GameSir G3,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000ac0500005b05000000000000,GameSir G3w,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000ac0500002d02000000000000,GameSir G4,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Windows, -03000000ac0500004d04000000000000,GameSir G4,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000ac0500001a06000000000000,GameSir-T3 2.02,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -030000004c0e00001035000000000000,Gamester,a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Windows, -030000000d0f00001110000000000000,GameStick Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Windows, -0300000047530000616d000000000000,GameStop,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -03000000c01100000140000000000000,GameStop PS4 Fun Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000b62500000100000000000000,Gametel GT004 01,a:b3,b:b0,dpdown:b10,dpleft:b9,dpright:b8,dpup:b11,leftshoulder:b4,rightshoulder:b5,start:b7,x:b1,y:b2,platform:Windows, -030000008f0e00001411000000000000,Gamo2 Divaller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000a857000000000000,Gator Claw,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000c9110000f055000000000000,GC100XF,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -030000008305000009a0000000000000,Genius,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -030000008305000031b0000000000000,Genius Maxfire Blaze 3,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -03000000451300000010000000000000,Genius Maxfire Grandias 12,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -030000005c1a00003330000000000000,Genius MaxFire Grandias 12V,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b4,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b2,y:b3,platform:Windows, -03000000300f00000b01000000000000,GGE909 Recoil,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows, -03000000f0250000c283000000000000,Gioteck PlayStation Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000f025000021c1000000000000,Gioteck PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000f025000031c1000000000000,Gioteck PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000f0250000c383000000000000,Gioteck VX2 PlayStation Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000f0250000c483000000000000,Gioteck VX2 PlayStation Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -030000004f04000026b3000000000000,GP XID,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -0300000079000000d418000000000000,GPD Win,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000c6240000025b000000000000,GPX,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000007d0400000840000000000000,Gravis Destroyer Tilt,+leftx:h0.2,+lefty:h0.4,-leftx:h0.8,-lefty:h0.1,a:b1,b:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,x:b0,y:b3,platform:Windows, -030000007d0400000540000000000000,Gravis Eliminator Pro,a:b1,b:b2,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows, -03000000280400000140000000000000,Gravis GamePad Pro,a:b1,b:b2,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a3,dpup:-a4,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows, -030000008f0e00000610000000000000,GreenAsia,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b9,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b10,righttrigger:b5,rightx:a5,righty:a2,start:b11,x:b3,y:b0,platform:Windows, -03000000ac0500006b05000000000000,GT2a,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Windows, -03000000341a00000302000000000000,Hama Scorpad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00004900000000000000,Hatsune Miku Sho PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000001008000001e1000000000000,Havit HV G60,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b3,y:b0,platform:Windows, -030000000d0f00000c00000000000000,HEXT,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000d81400000862000000000000,HitBox Edition Cthulhu,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b5,lefttrigger:b4,rightshoulder:b7,righttrigger:b6,start:b9,x:b0,y:b3,platform:Windows, -03000000632500002605000000000000,HJD X,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -030000000d0f00000a00000000000000,Hori DOA,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b9,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000000d0f00008500000000000000,Hori Fighting Commander 2016 PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00002500000000000000,Hori Fighting Commander 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00002d00000000000000,Hori Fighting Commander 3 Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00005f00000000000000,Hori Fighting Commander 4 PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00005e00000000000000,Hori Fighting Commander 4 PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00008400000000000000,Hori Fighting Commander 5,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00005100000000000000,Hori Fighting Commander PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00008600000000000000,Hori Fighting Commander Xbox 360,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000000d0f0000ba00000000000000,Hori Fighting Commander Xbox 360,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000000d0f00008800000000000000,Hori Fighting Stick mini 4 (PS3),a:b1,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b8,x:b0,y:b3,platform:Windows, -030000000d0f00008700000000000000,Hori Fighting Stick mini 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00001000000000000000,Hori Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00003200000000000000,Hori Fightstick 3W,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f0000c000000000000000,Hori Fightstick 4,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000000d0f00000d00000000000000,Hori Fightstick EX2,a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Windows, -030000000d0f00003701000000000000,Hori Fightstick Mini,a:b1,b:b0,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,rightx:a3,righty:a4,start:b7,x:b3,y:b2,platform:Windows, -030000000d0f00004000000000000000,Hori Fightstick Mini 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b5,lefttrigger:b4,rightshoulder:b7,righttrigger:b6,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00002100000000000000,Hori Fightstick V3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00002700000000000000,Hori Fightstick V3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f0000a000000000000000,Hori Grip TAC4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b13,x:b0,y:b3,platform:Windows, -030000000d0f0000a500000000000000,Hori Miku Project Diva X HD PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f0000a600000000000000,Hori Miku Project Diva X HD PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00000101000000000000,Hori Mini Hatsune Miku FT,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00005400000000000000,Hori Pad 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00000900000000000000,Hori Pad 3 Turbo,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00004d00000000000000,Hori Pad A,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00003801000000000000,Hori PC Engine Mini Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,start:b9,platform:Windows, -030000000d0f00009200000000000000,Hori Pokken Tournament DX Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00002301000000000000,Hori PS4 Controller Light,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Windows, -030000000d0f00001100000000000000,Hori Real Arcade Pro 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,rightstick:b11,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00002600000000000000,Hori Real Arcade Pro 3P,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00004b00000000000000,Hori Real Arcade Pro 3W,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00006a00000000000000,Hori Real Arcade Pro 4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00006b00000000000000,Hori Real Arcade Pro 4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00008a00000000000000,Hori Real Arcade Pro 4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00008b00000000000000,Hori Real Arcade Pro 4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00006f00000000000000,Hori Real Arcade Pro 4 VLX,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00007000000000000000,Hori Real Arcade Pro 4 VLX,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,rightstick:b11,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00003d00000000000000,Hori Real Arcade Pro N3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b10,leftstick:b4,lefttrigger:b11,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b6,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f0000ae00000000000000,Hori Real Arcade Pro N4,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000000d0f00008c00000000000000,Hori Real Arcade Pro P4,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000000d0f0000aa00000000000000,Hori Real Arcade Pro S,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f0000d800000000000000,Hori Real Arcade Pro S,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Windows, -030000000d0f00002200000000000000,Hori Real Arcade Pro V3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00005b00000000000000,Hori Real Arcade Pro V4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00005c00000000000000,Hori Real Arcade Pro V4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f0000af00000000000000,Hori Real Arcade Pro VHS,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00001b00000000000000,Hori Real Arcade Pro VX,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b9,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000ad1b000002f5000000000000,Hori Real Arcade Pro VX,a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b11,rightx:a2,righty:a5,start:b6,x:b2,y:b3,platform:Windows, -030000000d0f00009c00000000000000,Hori TAC Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f0000c900000000000000,Hori Taiko Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00006400000000000000,Horipad 3TP,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00001300000000000000,Horipad 3W,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00005500000000000000,Horipad 4 FPS,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00006e00000000000000,Horipad 4 PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00006600000000000000,Horipad 4 PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f00004200000000000000,Horipad A,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000ad1b000001f5000000000000,Horipad EXT2,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000000d0f0000ee00000000000000,Horipad Mini 4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f0000c100000000000000,Horipad Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000000d0f0000f600000000000000,Horipad Nintendo Switch Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -030000000d0f00006700000000000000,Horipad One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000000d0f0000dc00000000000000,Horipad Switch,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000242e00000b20000000000000,Hyperkin Admiral N64 Controller,+rightx:b11,+righty:b13,-rightx:b8,-righty:b12,a:b1,b:b0,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b14,leftx:a0,lefty:a1,rightshoulder:b5,start:b9,platform:Windows, -03000000242e0000ff0b000000000000,Hyperkin N64 Adapter,a:b1,b:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightx:a2,righty:a3,start:b9,platform:Windows, -03000000790000004e95000000000000,Hyperkin N64 Controller Adapter,a:b1,b:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b7,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b5,rightx:a5,righty:a2,start:b9,platform:Windows, -03000000242e00006a38000000000000,Hyperkin Trooper 2,a:b0,b:b1,back:b4,leftshoulder:b2,leftx:a0,lefty:a1,rightshoulder:b3,start:b5,platform:Windows, -03000000d81d00000e00000000000000,iBuffalo AC02 Arcade Joystick,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b2,rightstick:b11,righttrigger:b3,rightx:a2,righty:a5,start:b8,x:b4,y:b5,platform:Windows, -03000000d81d00000f00000000000000,iBuffalo BSGP1204 Series,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000d81d00001000000000000000,iBuffalo BSGP1204P Series,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -030000005c0a00000285000000000000,iDroidCon,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b4,y:b6,platform:Windows, -03000000696400006964000000000000,iDroidCon Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000511d00000230000000000000,iGUGU Gamecore,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b1,leftstick:b4,lefttrigger:b3,leftx:a0,lefty:a1,rightshoulder:b0,righttrigger:b2,platform:Windows, -03000000b50700001403000000000000,Impact Black,a:b2,b:b3,back:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows, -030000006f0e00002401000000000000,Injustice Fightstick PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows, -03000000830500005130000000000000,InterAct ActionPad,a:b0,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b7,rightshoulder:b5,righttrigger:b2,start:b9,x:b3,y:b4,platform:Windows, -03000000ef0500000300000000000000,InterAct AxisPad,a:b2,b:b3,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a3,righty:a2,start:b11,x:b0,y:b1,platform:Windows, -03000000fd0500000230000000000000,InterAct AxisPad,a:b2,b:b3,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a5,start:b11,x:b0,y:b1,platform:Windows, -03000000fd0500000030000000000000,Interact GoPad,a:b3,b:b4,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b2,righttrigger:b5,x:b0,y:b1,platform:Windows, -03000000fd0500003902000000000000,InterAct Hammerhead,a:b3,b:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b2,lefttrigger:b8,rightshoulder:b7,rightstick:b5,righttrigger:b9,start:b10,x:b0,y:b1,platform:Windows, -03000000fd0500002a26000000000000,InterAct Hammerhead FX,a:b3,b:b4,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b0,y:b1,platform:Windows, -03000000fd0500002f26000000000000,InterAct Hammerhead FX,a:b4,b:b5,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b1,y:b2,platform:Windows, -03000000fd0500005302000000000000,InterAct ProPad,a:b3,b:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b2,righttrigger:b5,x:b0,y:b1,platform:Windows, -03000000ac0500002c02000000000000,Ipega Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b8,leftstick:b13,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b9,rightstick:b14,righttrigger:b7,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000491900000204000000000000,Ipega PG9023,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000491900000304000000000000,Ipega PG9087,+righty:+a5,-righty:-a4,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,start:b11,x:b3,y:b4,platform:Windows, -030000007e0500000620000000000000,Joy-Con (L),+leftx:h0.2,+lefty:h0.4,-leftx:h0.8,-lefty:h0.1,a:b0,b:b1,back:b13,leftshoulder:b4,leftstick:b10,rightshoulder:b5,start:b8,x:b2,y:b3,platform:Windows, -030000007e0500000720000000000000,Joy-Con (R),+leftx:h0.2,+lefty:h0.4,-leftx:h0.8,-lefty:h0.1,a:b0,b:b1,back:b12,leftshoulder:b4,leftstick:b11,rightshoulder:b5,start:b9,x:b2,y:b3,platform:Windows, -03000000250900000017000000000000,Joypad Adapter,a:b2,b:b1,back:b9,leftshoulder:b5,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b6,start:b8,x:b3,y:b0,platform:Windows, -03000000bd12000003c0000000000000,Joypad Alpha Shock,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000ff1100004033000000000000,JPD FFB,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a2,start:b15,x:b3,y:b0,platform:Windows, -03000000242f00002d00000000000000,JYS Adapter,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000242f00008a00000000000000,JYS Adapter,a:b1,b:b4,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b0,y:b3,platform:Windows, -03000000c4100000c082000000000000,KADE,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000828200000180000000000000,Keio,a:b4,b:b5,back:b8,leftshoulder:b2,lefttrigger:b3,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:b7,start:b9,x:b0,y:b1,platform:Windows, -03000000790000000200000000000000,King PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b3,y:b0,platform:Windows, -03000000bd12000001e0000000000000,Leadership,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows, -030000006f0e00000103000000000000,Logic3,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000006f0e00000104000000000000,Logic3,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000008f0e00001300000000000000,Logic3,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -030000006d040000d1ca000000000000,Logitech ChillStream,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006d040000d2ca000000000000,Logitech Cordless Precision,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006d04000011c2000000000000,Logitech Cordless Wingman,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b5,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b2,righttrigger:b7,rightx:a3,righty:a4,x:b4,platform:Windows, -030000006d04000016c2000000000000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006d0400001dc2000000000000,Logitech F310,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000006d04000018c2000000000000,Logitech F510,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006d0400001ec2000000000000,Logitech F510,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000006d04000019c2000000000000,Logitech F710,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006d0400001fc2000000000000,Logitech F710,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000006d0400001ac2000000000000,Logitech Precision,a:b1,b:b2,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows, -030000006d04000009c2000000000000,Logitech WingMan,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b8,x:b3,y:b4,platform:Windows, -030000006d0400000bc2000000000000,Logitech WingMan Action Pad,a:b0,b:b1,back:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b8,lefttrigger:a5~,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b5,righttrigger:a2~,start:b8,x:b3,y:b4,platform:Windows, -030000006d0400000ac2000000000000,Logitech WingMan RumblePad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,rightx:a3,righty:a4,x:b3,y:b4,platform:Windows, -03000000380700005645000000000000,Lynx,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000222200006000000000000000,Macally,a:b1,b:b2,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b33,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000380700003888000000000000,Mad Catz Arcade Fightstick TE S Plus PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000380700008532000000000000,Mad Catz Arcade Fightstick TE S PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000380700006352000000000000,Mad Catz CTRLR,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000380700006652000000000000,Mad Catz CTRLR,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Windows, -03000000380700005032000000000000,Mad Catz Fightpad Pro PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000380700005082000000000000,Mad Catz Fightpad Pro PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000380700008031000000000000,Mad Catz FightStick Alpha PS3 ,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000003807000038b7000000000000,Mad Catz Fightstick TE,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b8,rightshoulder:b5,righttrigger:b9,start:b7,x:b2,y:b3,platform:Windows, -03000000380700008433000000000000,Mad Catz Fightstick TE S PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000380700008483000000000000,Mad Catz Fightstick TE S PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000380700008134000000000000,Mad Catz Fightstick TE2 PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b7,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b4,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000380700008184000000000000,Mad Catz Fightstick TE2 PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b5,leftstick:b10,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b4,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000380700006252000000000000,Mad Catz Micro CTRLR,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Windows, -03000000380700008232000000000000,Mad Catz PlayStation Brawlpad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000380700008731000000000000,Mad Catz PlayStation Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000003807000056a8000000000000,Mad Catz PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000380700001888000000000000,Mad Catz SFIV Fightstick PS3,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b5,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b4,righttrigger:b6,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -03000000380700008081000000000000,Mad Catz SFV Arcade Fightstick Alpha PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000380700001847000000000000,Mad Catz Street Fighter 4 Xbox 360 FightStick,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b8,rightshoulder:b5,righttrigger:b9,start:b7,x:b2,y:b3,platform:Windows, -03000000380700008034000000000000,Mad Catz TE2 PS3 Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000380700008084000000000000,Mad Catz TE2 PS4 Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000002a0600001024000000000000,Matricom,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b2,y:b3,platform:Windows, -030000009f000000adbb000000000000,MaxJoypad Virtual Controller,a:b1,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows, -03000000250900000128000000000000,Mayflash Arcade Stick,a:b1,b:b2,back:b8,leftshoulder:b0,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b3,righttrigger:b7,start:b9,x:b5,y:b6,platform:Windows, -030000008f0e00001330000000000000,Mayflash Controller Adapter,a:b1,b:b2,back:b8,dpdown:h0.8,dpleft:h0.2,dpright:h0.1,dpup:h0.4,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a3~,righty:a2,start:b9,x:b0,y:b3,platform:Windows, -03000000242f00003700000000000000,Mayflash F101,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows, -03000000790000003018000000000000,Mayflash F300 Arcade Joystick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows, -03000000242f00003900000000000000,Mayflash F300 Elite Arcade Joystick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000790000004418000000000000,Mayflash GameCube Controller,a:b1,b:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Windows, -03000000790000004318000000000000,Mayflash GameCube Controller Adapter,a:b1,b:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Windows, -03000000242f00007300000000000000,Mayflash Magic NS,a:b1,b:b4,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b0,y:b3,platform:Windows, -0300000079000000d218000000000000,Mayflash Magic NS,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000d620000010a7000000000000,Mayflash Magic NS,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000242f0000f400000000000000,Mayflash N64 Controller Adapter,a:b1,b:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightx:a2,righty:a5,start:b9,platform:Windows, -03000000790000007918000000000000,Mayflash N64 Controller Adapter,a:b1,b:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b9,leftx:a0,lefty:a1,righttrigger:b7,rightx:a3,righty:a2,start:b8,platform:Windows, -030000008f0e00001030000000000000,Mayflash Saturn Adapter,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,lefttrigger:b7,rightshoulder:b6,righttrigger:b2,start:b9,x:b3,y:b4,platform:Windows, -0300000025090000e803000000000000,Mayflash Wii Classic Adapter,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:a4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:a5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Windows, -03000000790000000318000000000000,Mayflash Wii DolphinBar,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b11,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b1,platform:Windows, -03000000790000000018000000000000,Mayflash Wii U Pro Adapter,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000790000002418000000000000,Mega Drive Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,rightshoulder:b2,start:b9,x:b3,y:b4,platform:Windows, -0300000079000000ae18000000000000,Mega Drive Controller,a:b0,b:b1,back:b7,dpdown:b14,dpleft:b15,dpright:b13,dpup:b2,rightshoulder:b6,righttrigger:b2,start:b9,x:b3,y:b4,platform:Windows, -03000000c0160000990a000000000000,Mega Drive Controller,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,righttrigger:b2,start:b3,platform:Windows, -030000005e0400002800000000000000,Microsoft Dual Strike,a:b3,b:b2,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b8,rightshoulder:b7,rightx:a0,righty:a1~,start:b5,x:b1,y:b0,platform:Windows, -030000005e0400000300000000000000,Microsoft SideWinder,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b8,x:b3,y:b4,platform:Windows, -030000005e0400000700000000000000,Microsoft SideWinder,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b9,x:b3,y:b4,platform:Windows, -030000005e0400000e00000000000000,Microsoft SideWinder Freestyle Pro,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b2,righttrigger:b5,start:b8,x:b3,y:b4,platform:Windows, -030000005e0400002700000000000000,Microsoft SideWinder Plug and Play,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,lefttrigger:b4,righttrigger:b5,x:b2,y:b3,platform:Windows, -03000000280d00000202000000000000,Miller Lite Cantroller,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a1,start:b5,x:b2,y:b3,platform:Windows, -03000000ad1b000023f0000000000000,MLG,a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a6,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Windows, -03000000ad1b00003ef0000000000000,MLG Fightstick TE,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b8,rightshoulder:b5,righttrigger:b9,start:b7,x:b2,y:b3,platform:Windows, -03000000380700006382000000000000,MLG PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000004523000015e0000000000000,Mobapad Chitu HD,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -03000000491900000904000000000000,Mobapad Chitu HD,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,misc1:b15,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000ffff00000000000000000000,Mocute M053,a:b3,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b11,leftstick:b7,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b6,righttrigger:b4,rightx:a3,righty:a4,start:b8,x:b1,y:b0,platform:Windows, -03000000d6200000e589000000000000,Moga 2,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,rightx:a2,righty:a5,start:b7,x:b2,y:b3,platform:Windows, -03000000d62000007162000000000000,Moga Pro,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,rightx:a2,righty:a5,start:b7,x:b2,y:b3,platform:Windows, -03000000d6200000ad0d000000000000,Moga Pro,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000c62400002a89000000000000,Moga XP5A Plus,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000c62400002b89000000000000,Moga XP5A Plus,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000c62400001a89000000000000,Moga XP5X Plus,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000c62400001b89000000000000,Moga XP5X Plus,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000250900006688000000000000,MP-8866 Super Dual Box,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows, -03000000091200004488000000000000,MUSIA PlayStation 2 Input Display,a:b0,b:b2,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b8,leftstick:b6,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b9,rightstick:b7,righttrigger:b11,rightx:a2,righty:a3,start:b5,x:b1,y:b3,platform:Windows, -03000000f70600000100000000000000,N64 Adaptoid,+rightx:b2,+righty:b1,-rightx:b4,-righty:b5,a:b0,b:b3,dpdown:b11,dpleft:b12,dpright:b13,dpup:b10,leftshoulder:b6,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b7,start:b8,platform:Windows, -030000006b140000010c000000000000,Nacon GC 400ES,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -030000006b1400001106000000000000,Nacon Revolution 3 PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -0300000085320000170d000000000000,Nacon Revolution 5 Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Windows, -0300000085320000190d000000000000,Nacon Revolution 5 Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Windows, -030000006b140000100d000000000000,Nacon Revolution Infinity PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006b140000080d000000000000,Nacon Revolution Unlimited Pro Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000bd12000001c0000000000000,Nebular,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a5,righty:a2,start:b9,x:b3,y:b0,platform:Windows, -03000000eb0300000000000000000000,NeGcon Adapter,a:a2,b:b13,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,lefttrigger:a4,leftx:a1,righttrigger:b11,start:b3,x:a3,y:b12,platform:Windows, -0300000038070000efbe000000000000,NEO SE,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -0300000092120000474e000000000000,NeoGeo X Arcade Stick,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,start:b9,x:b3,y:b2,platform:Windows, -03000000921200004b46000000000000,NES 2 port Adapter,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,start:b11,platform:Windows, -03000000000f00000100000000000000,NES Controller,a:b1,b:b0,back:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,start:b3,platform:Windows, -03000000921200004346000000000000,NES Controller,a:b0,b:b1,back:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,start:b3,platform:Windows, -03000000790000004518000000000000,NEXILUX GameCube Controller Adapter,a:b1,b:b0,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b2,y:b3,platform:Windows, -030000001008000001e5000000000000,NEXT SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,righttrigger:b6,start:b9,x:b3,y:b0,platform:Windows, -03000000050b00000045000000000000,Nexus,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b10,x:b2,y:b3,platform:Windows, -03000000152000000182000000000000,NGDS,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b3,y:b0,platform:Windows, -030000007e0500000920000000000000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -030000000d0500000308000000000000,Nostromo N45,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b12,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b10,x:b2,y:b3,platform:Windows, -030000007e0500001920000000000000,NSO N64 Controller,+rightx:b8,+righty:b2,-rightx:b3,-righty:b7,a:b1,b:b0,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,righttrigger:b10,start:b9,platform:Windows, -030000007e0500001720000000000000,NSO SNES Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b15,start:b9,x:b2,y:b3,platform:Windows, -03000000550900001472000000000000,NVIDIA Controller,a:b11,b:b10,back:b13,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b7,leftstick:b5,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b4,righttrigger:a5,rightx:a3,righty:a6,start:b3,x:b9,y:b8,platform:Windows, -03000000550900001072000000000000,NVIDIA Shield,a:b9,b:b8,back:b11,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b3,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b4,rightstick:b2,righttrigger:a4,rightx:a2,righty:a5,start:b0,x:b7,y:b6,platform:Windows, -030000005509000000b4000000000000,NVIDIA Virtual,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:+a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:-a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000120c00000288000000000000,Nyko Air Flo Xbox Controller,a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Windows, -030000004b120000014d000000000000,Nyko Airflo,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:a3,leftstick:a0,lefttrigger:b6,rightshoulder:b5,rightstick:a2,righttrigger:b7,start:b9,x:b2,y:b3,platform:Windows, -03000000d62000001d57000000000000,Nyko Airflo PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000791d00000900000000000000,Nyko Playpad,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Windows, -03000000782300000a10000000000000,Onlive Controller,a:b15,b:b14,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b11,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a3,righty:a4,start:b6,x:b13,y:b12,platform:Windows, -030000000d0f00000401000000000000,Onyx,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000008916000001fd000000000000,Onza CE,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a3,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000008916000000fd000000000000,Onza TE,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000d62000006d57000000000000,OPP PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006b14000001a1000000000000,Orange Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b2,y:b3,platform:Windows, -03000000362800000100000000000000,OUYA Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:b13,rightx:a3,righty:a4,x:b1,y:b2,platform:Windows, -03000000120c0000f60e000000000000,P4 Gamepad,a:b1,b:b2,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b5,lefttrigger:b7,rightshoulder:b4,righttrigger:b6,start:b9,x:b0,y:b3,platform:Windows, -03000000790000002201000000000000,PC Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -030000006f0e00008501000000000000,PDP Fightpad Pro GameCube Controller,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -030000006f0e00000901000000000000,PDP PS3 Versus Fighting,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows, -030000008f0e00004100000000000000,PlaySega,a:b1,b:b0,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b7,rightshoulder:b5,righttrigger:b2,start:b8,x:b4,y:b3,platform:Windows, -03000000666600006706000000000000,PlayStation Adapter,a:b2,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b6,leftstick:b9,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b10,righttrigger:b5,rightx:a2,righty:a3,start:b11,x:b3,y:b0,platform:Windows, -03000000e30500009605000000000000,PlayStation Adapter,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows, -030000004c050000da0c000000000000,PlayStation Classic Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b4,rightshoulder:b7,righttrigger:b5,start:b9,x:b3,y:b0,platform:Windows, -03000000632500002306000000000000,PlayStation Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Windows, -03000000f0250000c183000000000000,PlayStation Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000d9040000160f000000000000,PlayStation Controller Adapter,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows, -030000004c0500003713000000000000,PlayStation Vita,a:b1,b:b2,back:b8,dpdown:b13,dpleft:b15,dpright:b14,dpup:b12,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Windows, -03000000d620000011a7000000000000,PowerA Core Plus GameCube Controller,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -03000000dd62000015a7000000000000,PowerA Fusion Nintendo Switch Arcade Stick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000d620000012a7000000000000,PowerA Fusion Nintendo Switch Fight Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000dd62000016a7000000000000,PowerA Fusion Pro Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000d620000013a7000000000000,PowerA Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000d62000006dca000000000000,PowerA Pro Ex,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -0300000062060000d570000000000000,PowerA PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000d620000014a7000000000000,PowerA Spectra Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006d04000084ca000000000000,Precision,a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b4,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Windows, -03000000d62000009557000000000000,Pro Elite PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000c62400001a53000000000000,Pro Ex Mini,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000d62000009f31000000000000,Pro Ex mini PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000d6200000c757000000000000,Pro Ex mini PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000110e000000000000,Pro5,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000100800000100000000000000,PS1 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows, -030000008f0e00007530000000000000,PS1 Controller,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b1,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000100800000300000000000000,PS2 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a4,righty:a2,start:b9,x:b3,y:b0,platform:Windows, -03000000250900000088000000000000,PS2 Controller,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows, -03000000250900006888000000000000,PS2 Controller,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b6,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows, -03000000250900008888000000000000,PS2 Controller,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows, -030000006b1400000303000000000000,PS2 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -030000009d0d00001330000000000000,PS2 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -03000000151a00006222000000000000,PS2 Dual Plus Adapter,a:b2,b:b1,back:b9,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows, -03000000120a00000100000000000000,PS3 Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Windows, -03000000120c00001307000000000000,PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120c00001cf1000000000000,PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000f90e000000000000,PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000250900000118000000000000,PS3 Controller,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows, -03000000250900000218000000000000,PS3 Controller,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows, -03000000250900000500000000000000,PS3 Controller,a:b2,b:b1,back:b9,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b0,y:b3,platform:Windows, -030000004c0500006802000000000000,PS3 Controller,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b10,lefttrigger:a3~,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:a4~,rightx:a2,righty:a5,start:b8,x:b3,y:b0,platform:Windows, -030000004f1f00000800000000000000,PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows, -03000000632500007505000000000000,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000888800000803000000000000,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b9,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:b7,rightx:a3,righty:a4,start:b11,x:b3,y:b0,platform:Windows, -03000000888800000804000000000000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Windows, -030000008f0e00000300000000000000,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b3,y:b0,platform:Windows, -030000008f0e00001431000000000000,PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000ba2200002010000000000000,PS3 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a5,righty:a2,start:b9,x:b3,y:b2,platform:Windows, -03000000120c00000807000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000111e000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000121e000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000130e000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000150e000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000180e000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000181e000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000191e000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120c00001e0e000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000a957000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000aa57000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000f21c000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000f31c000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000f41c000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000f51c000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120c0000f70e000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000120e0000120c000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000160e0000120c000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000001a1e0000120c000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000004c050000a00b000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000004c050000c405000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Windows, -030000004c050000cc09000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000004c050000e60c000000000000,PS5 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,misc1:b14,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Windows, -030000004c050000f20d000000000000,PS5 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,misc1:b14,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Windows, -03000000830500005020000000000000,PSX,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,start:b11,x:b2,y:b3,platform:Windows, -03000000300f00000111000000000000,Qanba 2,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000300f00000211000000000000,Qanba 2P,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -03000000300f00000011000000000000,Qanba Arcade Stick 1008,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b10,x:b0,y:b3,platform:Windows, -03000000300f00001611000000000000,Qanba Arcade Stick 4018,a:b1,b:b2,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b8,x:b0,y:b3,platform:Windows, -03000000222c00000025000000000000,Qanba Dragon Arcade Joystick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000222c00000020000000000000,Qanba Drone Arcade Stick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,rightshoulder:b5,righttrigger:a4,start:b9,x:b0,y:b3,platform:Windows, -03000000300f00001211000000000000,Qanba Joystick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000300f00001210000000000000,Qanba Joystick Plus,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b2,y:b3,platform:Windows, -03000000341a00000104000000000000,Qanba Joystick Q4RAF,a:b5,b:b6,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b0,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b3,righttrigger:b7,start:b9,x:b1,y:b2,platform:Windows, -03000000222c00000223000000000000,Qanba Obsidian Arcade Stick PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000222c00000023000000000000,Qanba Obsidian Arcade Stick PS4,a:b1,b:b2,back:b13,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000008a2400006682000000000000,R1 Mobile Controller,a:b3,b:b1,back:b7,leftx:a0,lefty:a1,start:b6,x:b4,y:b0,platform:Windows, -03000000086700006626000000000000,RadioShack,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b3,y:b0,platform:Windows, -03000000ff1100004733000000000000,Ramox FPS Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b3,y:b0,platform:Windows, -030000009b2800002300000000000000,Raphnet 3DO Adapter,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b2,start:b3,platform:Windows, -030000009b2800006900000000000000,Raphnet 3DO Adapter,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b2,start:b3,platform:Windows, -030000009b2800000800000000000000,Raphnet Dreamcast Adapter,a:b2,b:b1,dpdown:b5,dpleft:b6,dpright:b7,dpup:b4,lefttrigger:a2,leftx:a0,righttrigger:a3,righty:a1,start:b3,x:b10,y:b9,platform:Windows, -030000009b2800006200000000000000,Raphnet GameCube Adapter,a:b0,b:b7,dpdown:b11,dpleft:b12,dpright:b13,dpup:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b2,righttrigger:b5,rightx:a3,righty:a4,start:b3,x:b1,y:b8,platform:Windows, -030000009b2800003200000000000000,Raphnet GC and N64 Adapter,a:b0,b:b7,dpdown:b11,dpleft:b12,dpright:b13,dpup:b10,lefttrigger:+a5,leftx:a0,lefty:a1,rightshoulder:b2,righttrigger:+a2,rightx:a3,righty:a4,start:b3,x:b1,y:b8,platform:Windows, -030000009b2800006000000000000000,Raphnet GC and N64 Adapter,a:b0,b:b7,dpdown:b11,dpleft:b12,dpright:b13,dpup:b10,lefttrigger:+a5,leftx:a0,lefty:a1,rightshoulder:b2,righttrigger:+a2,rightx:a3,righty:a4,start:b3,x:b1,y:b8,platform:Windows, -030000009b2800001800000000000000,Raphnet Jaguar Adapter,a:b2,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b0,righttrigger:b10,start:b3,x:b11,y:b12,platform:Windows, -030000009b2800006300000000000000,Raphnet N64 Adapter,+rightx:b9,+righty:b7,-rightx:b8,-righty:b6,a:b0,b:b1,dpdown:b11,dpleft:b12,dpright:b13,dpup:b10,leftshoulder:b4,lefttrigger:b2,leftx:a0,lefty:a1,rightshoulder:b5,start:b3,platform:Windows, -030000009b2800000200000000000000,Raphnet NES Adapter,a:b7,b:b6,back:b5,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a1,start:b4,platform:Windows, -030000009b2800004400000000000000,Raphnet PS1 and PS2 Adapter,a:b1,b:b2,back:b5,dpdown:b13,dpleft:b14,dpright:b15,dpup:b12,leftshoulder:b6,leftstick:b10,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b9,rightx:a3,righty:a4,start:b4,x:b0,y:b3,platform:Windows, -030000009b2800004300000000000000,Raphnet Saturn,a:b0,b:b1,dpdown:b13,dpleft:b14,dpright:b15,dpup:b12,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b8,x:b3,y:b4,platform:Windows, -030000009b2800000500000000000000,Raphnet Saturn Adapter 2.0,a:b1,b:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b4,rightshoulder:b7,righttrigger:b5,start:b9,x:b0,y:b3,platform:Windows, -030000009b2800000300000000000000,Raphnet SNES Adapter,a:b0,b:b4,back:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b1,y:b5,platform:Windows, -030000009b2800005600000000000000,Raphnet SNES Adapter,a:b1,b:b4,back:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b3,x:b0,y:b5,platform:Windows, -030000009b2800005700000000000000,Raphnet SNES Adapter,a:b1,b:b4,back:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b3,x:b0,y:b5,platform:Windows, -030000009b2800001e00000000000000,Raphnet Vectrex Adapter,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a1,lefty:a2,x:b2,y:b3,platform:Windows, -030000009b2800002b00000000000000,Raphnet Wii Classic Adapter,a:b1,b:b4,back:b2,dpdown:b13,dpleft:b14,dpright:b15,dpup:b12,guide:b10,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a3,righty:a4,start:b3,x:b0,y:b5,platform:Windows, -030000009b2800002c00000000000000,Raphnet Wii Classic Adapter,a:b1,b:b4,back:b2,dpdown:b13,dpleft:b14,dpright:b15,dpup:b12,guide:b10,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a3,righty:a4,start:b3,x:b0,y:b5,platform:Windows, -030000009b2800008000000000000000,Raphnet Wii Classic Adapter,a:b1,b:b4,back:b2,dpdown:b13,dpleft:b14,dpright:b15,dpup:b12,guide:b10,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a3,righty:a4,start:b3,x:b0,y:b5,platform:Windows, -03000000321500000003000000000000,Razer Hydra,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000f8270000bf0b000000000000,Razer Kishi,a:b6,b:b7,back:b16,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b18,leftshoulder:b12,leftstick:b19,lefttrigger:b14,leftx:a0,lefty:a1,rightshoulder:b13,rightstick:b20,righttrigger:b15,rightx:a3,righty:a4,start:b17,x:b9,y:b10,platform:Windows, -03000000321500000204000000000000,Razer Panthera PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000321500000104000000000000,Razer Panthera PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000321500000010000000000000,Razer Raiju,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000321500000507000000000000,Razer Raiju Mobile,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000321500000707000000000000,Razer Raiju Mobile,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000321500000710000000000000,Razer Raiju TE,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000321500000a10000000000000,Razer Raiju TE,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000321500000410000000000000,Razer Raiju UE,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000321500000910000000000000,Razer Raiju UE,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000321500000011000000000000,Razer Raion PS4 Fightpad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000321500000009000000000000,Razer Serval,+lefty:+a2,-lefty:-a1,a:b0,b:b1,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,leftx:a0,rightshoulder:b5,rightstick:b9,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000921200004547000000000000,Retro Bit Sega Genesis Controller Adapter,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,lefttrigger:b7,rightshoulder:b5,righttrigger:b2,start:b6,x:b3,y:b4,platform:Windows, -03000000790000001100000000000000,Retro Controller,a:b1,b:b2,back:b8,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,leftshoulder:b6,lefttrigger:b7,rightshoulder:b4,righttrigger:b5,start:b9,x:b0,y:b3,platform:Windows, -03000000830500006020000000000000,Retro Controller,a:b0,b:b1,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b8,righttrigger:b9,start:b7,x:b2,y:b3,platform:Windows, -0300000003040000c197000000000000,Retrode Adapter,a:b0,b:b4,back:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b3,x:b1,y:b5,platform:Windows, -03000000bd12000013d0000000000000,Retrolink Sega Saturn Classic Controller,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b5,lefttrigger:b6,rightshoulder:b2,righttrigger:b7,start:b8,x:b3,y:b4,platform:Windows, -03000000bd12000015d0000000000000,Retrolink SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Windows, -03000000341200000400000000000000,RetroUSB N64 RetroPort,+rightx:b8,+righty:b10,-rightx:b9,-righty:b11,a:b7,b:b6,dpdown:b2,dpleft:b1,dpright:b0,dpup:b3,leftshoulder:b13,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b12,start:b4,platform:Windows, -0300000000f000000300000000000000,RetroUSB RetroPad,a:b1,b:b5,back:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b3,x:b0,y:b4,platform:Windows, -0300000000f00000f100000000000000,RetroUSB Super RetroPort,a:b1,b:b5,back:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b3,x:b0,y:b4,platform:Windows, -03000000830500000960000000000000,Revenger,a:b0,b:b1,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b2,righttrigger:b3,x:b4,y:b5,platform:Windows, -030000006b140000010d000000000000,Revolution Pro Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000006b140000020d000000000000,Revolution Pro Controller 2,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000006b140000130d000000000000,Revolution Pro Controller 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000006f0e00001f01000000000000,Rock Candy,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000006f0e00004601000000000000,Rock Candy,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000c6240000fefa000000000000,Rock Candy Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000006f0e00008701000000000000,Rock Candy Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006f0e00001e01000000000000,Rock Candy PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006f0e00002801000000000000,Rock Candy PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006f0e00002f01000000000000,Rock Candy PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000050b0000e318000000000000,ROG Chakram,a:b1,b:b0,leftx:a0,lefty:a1,x:b2,y:b3,platform:Windows, -03000000050b0000e518000000000000,ROG Chakram,a:b1,b:b0,leftx:a0,lefty:a1,x:b2,y:b3,platform:Windows, -03000000050b00005819000000000000,ROG Chakram Core,a:b1,b:b0,leftx:a0,lefty:a1,x:b2,y:b3,platform:Windows, -03000000050b0000181a000000000000,ROG Chakram X,a:b1,b:b0,leftx:a0,lefty:a1,x:b2,y:b3,platform:Windows, -03000000050b00001a1a000000000000,ROG Chakram X,a:b1,b:b0,leftx:a0,lefty:a1,x:b2,y:b3,platform:Windows, -03000000050b00001c1a000000000000,ROG Chakram X,a:b1,b:b0,leftx:a0,lefty:a1,x:b2,y:b3,platform:Windows, -030000004f04000001d0000000000000,Rumble Force,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Windows, -030000008916000000fe000000000000,Sabertooth,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000c6240000045d000000000000,Sabertooth,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000a30600001af5000000000000,Saitek Cyborg,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Windows, -03000000a306000023f6000000000000,Saitek Cyborg V.1 Game,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Windows, -03000000300f00001201000000000000,Saitek Dual Analog,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows, -03000000a30600000701000000000000,Saitek P220,a:b2,b:b3,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b7,rightshoulder:b4,righttrigger:b5,x:b0,y:b1,platform:Windows, -03000000a30600000cff000000000000,Saitek P2500 Force Rumble,a:b2,b:b3,back:b11,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,start:b10,x:b0,y:b1,platform:Windows, -03000000a30600000d5f000000000000,Saitek P2600,a:b1,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a3,righty:a2,start:b8,x:b0,y:b3,platform:Windows, -03000000a30600000dff000000000000,Saitek P2600,a:b1,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a5,righty:a2,start:b8,x:b0,y:b3,platform:Windows, -03000000a30600000c04000000000000,Saitek P2900,a:b1,b:b2,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b3,platform:Windows, -03000000a306000018f5000000000000,Saitek P3200,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Windows, -03000000300f00001001000000000000,Saitek P480 Rumble,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows, -03000000a30600000901000000000000,Saitek P880,a:b2,b:b3,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b8,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b4,rightstick:b9,righttrigger:b5,rightx:a3,righty:a2,x:b0,y:b1,platform:Windows, -03000000a30600000b04000000000000,Saitek P990,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b3,platform:Windows, -03000000a30600002106000000000000,Saitek PS1000 PlayStation Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Windows, -03000000a306000020f6000000000000,Saitek PS2700 PlayStation Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Windows, -03000000300f00001101000000000000,Saitek Rumble,a:b2,b:b3,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows, -03000000e804000000a0000000000000,Samsung EIGP20,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000c01100000252000000000000,Sanwa Easy Grip,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b9,x:b3,y:b4,platform:Windows, -03000000c01100004350000000000000,Sanwa Micro Grip P3,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,x:b3,y:b2,platform:Windows, -03000000411200004550000000000000,Sanwa Micro Grip Pro,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a1,righty:a2,start:b9,x:b1,y:b3,platform:Windows, -03000000c01100004150000000000000,Sanwa Micro Grip Pro,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Windows, -03000000c01100004450000000000000,Sanwa Online Grip,a:b0,b:b1,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b8,rightstick:b11,righttrigger:b9,rightx:a3,righty:a2,start:b14,x:b3,y:b4,platform:Windows, -03000000730700000401000000000000,Sanwa PlayOnline Mobile,a:b0,b:b1,back:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,start:b3,platform:Windows, -03000000830500006120000000000000,Sanwa Smart Grip II,a:b0,b:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,x:b1,y:b3,platform:Windows, -03000000c01100000051000000000000,Satechi Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Windows, -030000004f04000028b3000000000000,Score A,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000952e00002577000000000000,Scuf PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000a30c00002500000000000000,Sega Genesis Mini 3B Controller,a:b2,b:b1,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,righttrigger:b5,start:b9,platform:Windows, -03000000a30c00002400000000000000,Sega Mega Drive Mini 6B Controller,a:b2,b:b1,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,rightshoulder:b4,righttrigger:b5,start:b9,x:b3,y:b0,platform:Windows, -03000000d804000086e6000000000000,Sega Multi Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b7,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b8,x:b3,y:b4,platform:Windows, -0300000000050000289b000000000000,Sega Saturn Adapter,a:b1,b:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b4,rightshoulder:b7,righttrigger:b5,start:b9,x:b0,y:b3,platform:Windows, -0300000000f000000800000000000000,Sega Saturn Controller,a:b1,b:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,rightshoulder:b7,righttrigger:b3,start:b0,x:b5,y:b6,platform:Windows, -03000000730700000601000000000000,Sega Saturn Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b9,x:b3,y:b4,platform:Windows, -03000000b40400000a01000000000000,Sega Saturn Controller,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b7,rightshoulder:b5,righttrigger:b2,start:b8,x:b3,y:b4,platform:Windows, -030000003b07000004a1000000000000,SFX,a:b0,b:b2,back:b7,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b9,righttrigger:b5,start:b8,x:b1,y:b3,platform:Windows, -03000000f82100001900000000000000,Shogun Bros Chameleon X1,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows, -03000000120c00001c1e000000000000,SnakeByte 4S PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -0300000081170000960a000000000000,SNES Controller,a:b4,b:b0,back:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b3,x:b5,y:b1,platform:Windows, -03000000811700009d0a000000000000,SNES Controller,a:b0,b:b4,back:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b1,y:b5,platform:Windows, -030000008b2800000300000000000000,SNES Controller,a:b0,b:b4,back:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b1,y:b5,platform:Windows, -03000000921200004653000000000000,SNES Controller,a:b0,b:b4,back:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b3,x:b1,y:b5,platform:Windows, -03000000ff000000cb01000000000000,Sony PlayStation Portable,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,start:b7,x:b2,y:b3,platform:Windows, -03000000341a00000208000000000000,Speedlink 6555,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:-a4,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a4,rightx:a3,righty:a2,start:b7,x:b2,y:b3,platform:Windows, -03000000341a00000908000000000000,Speedlink 6566,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -03000000380700001722000000000000,Speedlink Competition Pro,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,x:b2,y:b3,platform:Windows, -030000008f0e00000800000000000000,Speedlink Strike FX,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000c01100000591000000000000,Speedlink Torid,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000d11800000094000000000000,Stadia Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:b12,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:b11,rightx:a3,righty:a4,start:b9,x:b2,y:b3,platform:Windows, -03000000de280000fc11000000000000,Steam Virtual Gamepad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000de280000ff11000000000000,Steam Virtual Gamepad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:+a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:-a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000120c0000160e000000000000,Steel Play Metaltech PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000110100001914000000000000,SteelSeries,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftstick:b13,lefttrigger:b6,leftx:a0,lefty:a1,rightstick:b14,righttrigger:b7,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000381000001214000000000000,SteelSeries Free,a:b0,b:b1,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Windows, -03000000110100003114000000000000,SteelSeries Stratus Duo,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000381000003014000000000000,SteelSeries Stratus Duo,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000381000003114000000000000,SteelSeries Stratus Duo,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000381000001814000000000000,SteelSeries Stratus XL,a:b0,b:b1,back:b18,dpdown:b13,dpleft:b14,dpright:b15,dpup:b12,guide:b19,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b2,y:b3,platform:Windows, -03000000790000001c18000000000000,STK 7024X,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000380700003847000000000000,Street Fighter Fightstick TE,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b11,start:b7,x:b2,y:b3,platform:Windows, -030000001f08000001e4000000000000,Super Famicom Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Windows, -03000000790000000418000000000000,Super Famicom Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b33,rightshoulder:b5,start:b7,x:b2,y:b3,platform:Windows, -03000000341200001300000000000000,Super Racer,a:b2,b:b3,back:b8,leftshoulder:b5,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b4,righttrigger:b7,x:b0,y:b1,platform:Windows, -03000000457500002211000000000000,Szmy Power PC Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000004f0400000ab1000000000000,T16000M,a:b0,b:b1,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b11,leftshoulder:b4,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:b7,start:b10,x:b2,y:b3,platform:Windows, -030000000d0f00007b00000000000000,TAC GEAR,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000e40a00000207000000000000,Taito Egret II Mini Controller,a:b4,b:b2,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,guide:b9,rightshoulder:b0,righttrigger:b1,start:b7,x:b8,y:b3,platform:Windows, -03000000d814000001a0000000000000,TE Kitty,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000fa1900000706000000000000,Team 5,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000b50700001203000000000000,Techmobility X6-38V,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows, -03000000ba2200000701000000000000,Technology Innovation PS2 Adapter,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b3,y:b2,platform:Windows, -03000000c61100001000000000000000,Tencent Xianyou Gamepad,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,x:b3,y:b4,platform:Windows, -03000000790000002601000000000000,TGZ,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b3,y:b0,platform:Windows, -03000000591c00002400000000000000,THEC64 Joystick,a:b0,b:b1,back:b6,leftshoulder:b4,leftx:a0,lefty:a4,rightshoulder:b5,start:b7,x:b2,y:b3,platform:Windows, -03000000591c00002600000000000000,THEGamepad,a:b2,b:b1,back:b6,leftx:a0,lefty:a1,start:b7,x:b3,y:b0,platform:Windows, -030000004f04000015b3000000000000,Thrustmaster Dual Analog 4,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Windows, -030000004f04000023b3000000000000,Thrustmaster Dual Trigger PlayStation Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000004f0400000ed0000000000000,ThrustMaster eSwap Pro Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000004f04000008d0000000000000,ThrustMaster Ferrari 150 PlayStation Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000004f04000000b3000000000000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b11,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b10,x:b1,y:b3,platform:Windows, -030000004f04000004b3000000000000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Windows, -030000004f04000003d0000000000000,ThrustMaster Run N Drive PlayStation Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b7,leftshoulder:a3,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:a4,rightstick:b11,righttrigger:b5,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000004f04000009d0000000000000,ThrustMaster Run N Drive PlayStation Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -030000006d04000088ca000000000000,Thunderpad,a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b4,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Windows, -03000000666600000288000000000000,TigerGame PlayStation Adapter,a:b2,b:b1,back:b9,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows, -03000000666600000488000000000000,TigerGame PlayStation Adapter,a:b2,b:b1,back:b9,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows, -030000004f04000007d0000000000000,TMini,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000571d00002100000000000000,Tomee NES Controller Adapter,a:b1,b:b0,back:b2,dpdown:+a4,dpleft:-a0,dpright:+a0,dpup:-a4,start:b3,platform:Windows, -03000000571d00002000000000000000,Tomee SNES Controller Adapter,a:b0,b:b1,back:b6,dpdown:+a4,dpleft:-a0,dpright:+a0,dpup:-a4,leftshoulder:b4,rightshoulder:b5,start:b7,x:b2,y:b3,platform:Windows, -03000000d62000006000000000000000,Tournament PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000c01100000055000000000000,Tronsmart,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -030000005f140000c501000000000000,Trust Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000b80500000210000000000000,Trust Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -030000004f04000087b6000000000000,TWCS Throttle,dpdown:b8,dpleft:b9,dpright:b7,dpup:b6,leftstick:b5,lefttrigger:-a5,leftx:a0,lefty:a1,righttrigger:+a5,platform:Windows, -03000000411200000450000000000000,Twin Shock,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b9,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b10,righttrigger:b5,rightx:a3,righty:a4,start:b11,x:b3,y:b0,platform:Windows, -03000000d90400000200000000000000,TwinShock PS2 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows, -03000000151900005678000000000000,Uniplay U6,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000101c0000171c000000000000,uRage Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -030000000b0400003065000000000000,USB Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b3,y:b0,platform:Windows, -03000000242f00006e00000000000000,USB Controller,a:b1,b:b4,back:b10,leftshoulder:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b9,righttrigger:b7,rightx:a2,righty:a5,start:b11,x:b0,y:b3,platform:Windows, -03000000300f00000701000000000000,USB Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows, -03000000341a00002308000000000000,USB Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -03000000666600000188000000000000,USB Controller,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows, -030000006b1400000203000000000000,USB Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -03000000790000000a00000000000000,USB Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b3,y:b0,platform:Windows, -03000000b404000081c6000000000000,USB Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b3,y:b0,platform:Windows, -03000000b50700001503000000000000,USB Controller,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a5,righty:a2,start:b9,x:b0,y:b1,platform:Windows, -03000000bd12000012d0000000000000,USB Controller,a:b0,b:b1,back:b6,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,start:b7,x:b2,y:b3,platform:Windows, -03000000ff1100004133000000000000,USB Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a4,righty:a2,start:b9,x:b3,y:b0,platform:Windows, -03000000632500002305000000000000,USB Vibration Joystick,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows, -03000000790000001a18000000000000,Venom,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows, -03000000790000001b18000000000000,Venom Arcade Joystick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows, -030000006f0e00000302000000000000,Victrix PS4 Pro Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows, -030000006f0e00000702000000000000,Victrix PS4 Pro Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows, -0300000034120000adbe000000000000,vJoy Device,a:b0,b:b1,back:b15,dpdown:b6,dpleft:b7,dpright:b8,dpup:b5,guide:b16,leftshoulder:b9,leftstick:b13,lefttrigger:b11,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b14,righttrigger:b12,rightx:a3,righty:a4,start:b4,x:b2,y:b3,platform:Windows, -03000000120c0000ab57000000000000,Warrior Joypad JS083,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000007e0500003003000000000000,Wii U Pro,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,leftshoulder:b6,leftstick:b11,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b12,righttrigger:b5,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Windows, -0300000032150000030a000000000000,Wildcat,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -0300000032150000140a000000000000,Wolverine,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000002e160000efbe000000000000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b10,rightshoulder:b5,righttrigger:b11,start:b7,x:b2,y:b3,platform:Windows, -03000000380700001647000000000000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000380700002045000000000000,Xbox 360 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -03000000380700002644000000000000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b7,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b11,rightx:a2,righty:a5,start:b8,x:b2,y:b3,platform:Windows, -03000000380700002647000000000000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000003807000026b7000000000000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000380700003647000000000000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b11,rightx:a7,righty:a5,start:b7,x:b2,y:b3,platform:Windows, -030000005e0400001907000000000000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000005e0400008e02000000000000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:+a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:-a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000005e0400009102000000000000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000ad1b000000fd000000000000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000ad1b000001fd000000000000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000ad1b000016f0000000000000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000ad1b00008e02000000000000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000c62400000053000000000000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000c6240000fdfa000000000000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000380700002847000000000000,Xbox 360 Fightpad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b11,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000005e040000a102000000000000,Xbox 360 Wireless Receiver,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:+a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:-a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000005e0400000a0b000000000000,Xbox Adaptive Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:+a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:-a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000120c00000a88000000000000,Xbox Controller,a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b11,rightx:a2,righty:a4,start:b6,x:b2,y:b3,platform:Windows, -03000000120c00001088000000000000,Xbox Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2~,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5~,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000002a0600002000000000000000,Xbox Controller,a:b0,b:b1,back:b13,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,leftshoulder:b5,leftstick:b14,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b4,rightstick:b15,righttrigger:b7,rightx:a2,righty:a5,start:b12,x:b2,y:b3,platform:Windows, -03000000300f00008888000000000000,Xbox Controller,a:b0,b:b1,back:b7,dpdown:b13,dpleft:b10,dpright:b11,dpup:b12,leftshoulder:b5,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b4,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Windows, -03000000380700001645000000000000,Xbox Controller,a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b4,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Windows, -03000000380700002645000000000000,Xbox Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000380700003645000000000000,Xbox Controller,a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b4,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Windows, -03000000380700008645000000000000,Xbox Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000005e0400000202000000000000,Xbox Controller,a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b4,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Windows, -030000005e0400008502000000000000,Xbox Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000005e0400008702000000000000,Xbox Controller,a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b4,rightstick:b9,righttrigger:b7,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Windows, -030000005e0400008902000000000000,Xbox Controller,a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b10,leftstick:b8,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b9,righttrigger:b4,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Windows, -030000000d0f00006300000000000000,Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b9,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000005e0400000c0b000000000000,Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000005e040000d102000000000000,Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000005e040000dd02000000000000,Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000005e040000e002000000000000,Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000005e040000e302000000000000,Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000005e040000ea02000000000000,Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:+a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:-a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000005e040000fd02000000000000,Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000005e040000ff02000000000000,Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:+a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:-a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000006f0e0000a802000000000000,Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000006f0e0000c802000000000000,Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000c62400003a54000000000000,Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000005e040000130b000000000000,Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -03000000341a00000608000000000000,Xeox,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -03000000450c00002043000000000000,Xeox SL6556BK,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows, -030000006f0e00000300000000000000,XGear,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a5,righty:a2,start:b9,x:b3,y:b0,platform:Windows, -03000000e0ff00000201000000000000,Xiaomi Black Shark (L),back:b0,dpdown:b11,dpleft:b9,dpright:b10,dpup:b8,leftshoulder:b5,lefttrigger:b7,leftx:a0,lefty:a1,platform:Windows, -03000000172700004431000000000000,Xiaomi Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b20,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a7,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Windows, -03000000172700003350000000000000,Xiaomi XMGP01YM,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000bc2000005060000000000000,Xiaomi XMGP01YM,+lefty:+a2,+righty:+a5,-lefty:-a1,-righty:-a4,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,start:b11,x:b3,y:b4,platform:Windows, -xinput,XInput Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows, -030000007d0400000340000000000000,Xterminator Digital Gamepad,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:-a4,lefttrigger:+a4,leftx:a0,lefty:a1,paddle1:b7,paddle2:b6,rightshoulder:b5,rightstick:b9,righttrigger:b2,rightx:a3,righty:a5,start:b8,x:b3,y:b4,platform:Windows, -03000000790000004f18000000000000,ZDT Android Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows, -03000000120c00000500000000000000,Zeroplus Adapter,a:b2,b:b1,back:b11,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b9,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b0,righttrigger:b5,rightx:a3,righty:a2,start:b8,x:b3,y:b0,platform:Windows, -03000000120c0000101e000000000000,Zeroplus P4 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows, -030000008f0e00000300000009010000,2 In 1 Joystick,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Mac OS X, -03000000c82d00000031000001000000,8BitDo Adapter,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000c82d00000531000000020000,8BitDo Adapter 2,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000c82d00000951000000010000,8BitDo Dogbone,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a1,rightx:a2,righty:a3,start:b11,platform:Mac OS X, -03000000c82d00000090000001000000,8BitDo FC30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00001038000000010000,8BitDo FC30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00001251000000010000,8BitDo Lite 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00001251000000020000,8BitDo Lite 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00001151000000010000,8BitDo Lite SE,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00001151000000020000,8BitDo Lite SE,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000a30c00002400000006020000,8BitDo M30,a:b2,b:b1,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,guide:b9,leftshoulder:b6,lefttrigger:b5,rightshoulder:b4,righttrigger:b7,start:b8,x:b3,y:b0,platform:Mac OS X, -03000000c82d00000151000000010000,8BitDo M30,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:b7,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000c82d00000650000001000000,8BitDo M30,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b8,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:b7,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000c82d00005106000000010000,8BitDo M30,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,guide:b2,leftshoulder:b6,lefttrigger:a5,rightshoulder:b7,righttrigger:a4,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00002090000000010000,8BitDo Micro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00000451000000010000,8BitDo N30,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a1,rightx:a2,righty:a3,start:b11,platform:Mac OS X, -03000000c82d00001590000001000000,8BitDo N30 Pro 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00006528000000010000,8BitDo N30 Pro 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00006928000000010000,8BitDo N64,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a2,righty:a3,start:b11,platform:Mac OS X, -03000000c82d00002590000000010000,8BitDo NEOGEO,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000c82d00002590000001000000,8BitDo NEOGEO,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000c82d00002690000000010000,8BitDo NEOGEOa:b0,+leftx:h0.2,+lefty:h0.4,-leftx:h0.8,-lefty:h0.1,b:b1,back:b10,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Mac OS X, -030000003512000012ab000001000000,8BitDo NES30,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d000012ab000001000000,8BitDo NES30,a:b0,b:b1,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000c82d00002028000000010000,8BitDo NES30,a:b0,b:b1,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000022000000090000001000000,8BitDo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000203800000900000000010000,8BitDo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00000190000001000000,8BitDo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00000751000000010000,8BitDo P30,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000c82d00000851000000010000,8BitDo P30,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000c82d00000660000000010000,8BitDo Pro 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00000660000000020000,8BitDo Pro 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00000131000001000000,8BitDo Receiver,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00000231000001000000,8BitDo Receiver,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00000331000001000000,8BitDo Receiver,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00000431000001000000,8BitDo Receiver,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00002867000000010000,8BitDo S30,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b8,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:b7,rightx:a2,righty:a3,start:b10,x:b3,y:b4,platform:Mac OS X, -03000000102800000900000000000000,8BitDo SFC30 Joystick,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00000351000000010000,8BitDo SN30,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00001290000001000000,8BitDo SN30,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00004028000000010000,8BitDo SN30,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00000160000001000000,8BitDo SN30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00000161000000010000,8BitDo SN30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a5,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00000260000001000000,8BitDo SN30 Pro Plus,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00000261000000010000,8BitDo SN30 Pro Plus,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00001230000000010000,8BitDo Ultimate,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,paddle1:b2,paddle2:b5,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000c82d00001530000001000000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000c82d00001630000001000000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000c82d00001730000001000000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000c82d00001130000000020000,8BitDo Ultimate Wired,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b26,paddle1:b24,paddle2:b25,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000c82d00001330000001000000,8BitDo Ultimate Wireless,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b26,paddle1:b23,paddle2:b19,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000c82d00001330000000020000,8BitDo Ultimate Wireless Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b26,paddle1:b23,paddle2:b19,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000a00500003232000008010000,8BitDo Zero,a:b0,b:b1,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000a00500003232000009010000,8BitDo Zero,a:b0,b:b1,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000c82d00001890000001000000,8BitDo Zero 2,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000c82d00003032000000010000,8BitDo Zero 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a2,righty:a31,start:b11,x:b4,y:b3,platform:Mac OS X, -03000000491900001904000001010000,Amazon Luna Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,misc1:b9,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b7,x:b2,y:b3,platform:Mac OS X, -03000000710100001904000000010000,Amazon Luna Controller,a:b0,b:b1,back:b11,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b9,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Mac OS X, -03000000a30c00002700000003030000,Astro City Mini,a:b2,b:b1,back:b8,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,rightshoulder:b4,righttrigger:b5,start:b9,x:b3,y:b0,platform:Mac OS X, -03000000a30c00002800000003030000,Astro City Mini,a:b2,b:b1,back:b8,leftx:a3,lefty:a4,rightshoulder:b4,righttrigger:b5,start:b9,x:b3,y:b0,platform:Mac OS X, -03000000050b00000045000031000000,ASUS Gamepad,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X, -03000000050b00000579000000010000,ASUS ROG Kunai 3,a:b0,b:b1,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b14,leftshoulder:b6,leftstick:b15,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b42,paddle1:b9,paddle2:b11,rightshoulder:b7,rightstick:b16,righttrigger:a4,rightx:a2,righty:a3,start:b13,x:b3,y:b4,platform:Mac OS X, -03000000050b00000679000000010000,ASUS ROG Kunai 3,a:b0,b:b1,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b14,leftshoulder:b6,leftstick:b15,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b23,rightshoulder:b7,rightstick:b16,righttrigger:a4,rightx:a2,righty:a3,start:b13,x:b3,y:b4,platform:Mac OS X, -03000000503200000110000047010000,Atari VCS Classic Controller,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b3,start:b2,platform:Mac OS X, -03000000503200000210000047010000,Atari VCS Modern Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a4,rightx:a2,righty:a3,start:b8,x:b2,y:b3,platform:Mac OS X, -03000000c62400001a89000000010000,BDA MOGA XP5-X Plus,a:b0,b:b1,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b14,leftshoulder:b6,leftstick:b15,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b16,righttrigger:a4,rightx:a2,righty:a3,start:b13,x:b3,y:b4,platform:Mac OS X, -03000000c62400001b89000000010000,BDA MOGA XP5-X Plus,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000d62000002a79000000010000,BDA PS4 Fightpad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000120c0000200e000000010000,Brook Mars PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000120c0000210e000000010000,Brook Mars PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -030000008305000031b0000000000000,Cideko AK08b,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000d8140000cecf000000000000,Cthulhu,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000260900008888000088020000,Cyber Gadget GameCube Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:a5,rightx:a2,righty:a3~,start:b7,x:b2,y:b3,platform:Mac OS X, -03000000a306000022f6000001030000,Cyborg V3 Rumble Pad PlayStation Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:+a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:-a3,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000791d00000103000009010000,Dual Box Wii Classic Adapter,a:b2,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,guide:b10,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Mac OS X, -030000006e0500000720000010020000,Elecom JC-W01U,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b1,platform:Mac OS X, -030000006f0e00008401000003010000,Faceoff Deluxe Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b13,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000151900004000000001000000,Flydigi Vader 2,a:b14,b:b15,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b21,leftshoulder:b6,leftstick:b12,lefttrigger:a5,leftx:a0,lefty:a1,paddle1:b2,paddle2:b5,paddle3:b16,paddle4:b17,rightshoulder:b7,rightstick:b13,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b0,y:b1,platform:Mac OS X, -03000000b40400001124000001040000,Flydigi Vader 2,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b12,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b14,paddle1:b2,paddle2:b5,paddle3:b16,paddle4:b17,rightshoulder:b7,rightstick:b13,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000b40400001224000003030000,Flydigi Vader 2,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b12,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b2,paddle1:b16,paddle2:b17,paddle3:b14,paddle4:b15,rightshoulder:b7,rightstick:b13,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000790000004618000000010000,GameCube Controller Adapter,a:b4,b:b0,dpdown:b56,dpleft:b60,dpright:b52,dpup:b48,lefttrigger:a12,leftx:a0,lefty:a4,rightshoulder:b28,righttrigger:a16,rightx:a20,righty:a8,start:b36,x:b8,y:b12,platform:Mac OS X, -03000000ac0500001a06000002020000,GameSir-T3 2.02,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000ad1b000001f9000000000000,Gamestop BB070 X360 Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, -0500000047532047616d657061640000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X, -03000000c01100000140000000010000,GameStop PS4 Fun Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -030000006f0e00000102000000000000,GameStop Xbox 360 Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, -03000000ff1100003133000007010000,GameWare PC Control Pad,a:b2,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a3,righty:a4,start:b11,x:b3,y:b0,platform:Mac OS X, -030000007d0400000540000001010000,Gravis Eliminator Pro,a:b1,b:b2,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000280400000140000000020000,Gravis GamePad Pro,a:b1,b:b2,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Mac OS X, -030000008f0e00000300000007010000,GreenAsia Joystick,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Mac OS X, -030000000d0f00002d00000000100000,Hori Fighting Commander 3 Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -030000000d0f00005f00000000000000,Hori Fighting Commander 4 PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -030000000d0f00005f00000000010000,Hori Fighting Commander 4 PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -030000000d0f00005e00000000000000,Hori Fighting Commander 4 PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -030000000d0f00005e00000000010000,Hori Fighting Commander 4 PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -030000000d0f00008400000000010000,Hori Fighting Commander PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -030000000d0f00008500000000010000,Hori Fighting Commander PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000341a00000302000014010000,Hori Fighting Stick Mini,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Mac OS X, -030000000d0f00008800000000010000,Hori Fighting Stick mini 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,rightstick:b11,righttrigger:b7,start:b9,x:b0,y:b3,platform:Mac OS X, -030000000d0f00008700000000010000,Hori Fighting Stick mini 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,rightstick:b11,righttrigger:b7,start:b9,x:b0,y:b3,platform:Mac OS X, -030000000d0f00004d00000000000000,Hori Gem Pad 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -030000000d0f00003801000008010000,Hori PC Engine Mini Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,start:b9,platform:Mac OS X, -030000000d0f00009200000000010000,Hori Pokken Tournament DX Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Mac OS X, -030000000d0f0000aa00000072050000,Hori Real Arcade Pro for Nintendo Switch,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Mac OS X, -030000000d0f00000002000015010000,Hori Switch Split Pad Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -030000000d0f00006e00000000010000,Horipad 4 PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -030000000d0f00006600000000010000,Horipad 4 PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -030000000d0f00006600000000000000,Horipad FPS Plus 4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -030000000d0f0000ee00000000010000,Horipad Mini 4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000242e0000ff0b000000010000,Hyperkin N64 Adapter,a:b1,b:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightx:a2,righty:a3,start:b9,platform:Mac OS X, -03000000790000004e95000000010000,Hyperkin N64 Controller Adapter,a:b1,b:b2,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b7,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightx:a5,righty:a2,start:b9,platform:Mac OS X, -03000000830500006020000000000000,iBuffalo Super Famicom Controller,a:b1,b:b0,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b3,y:b2,platform:Mac OS X, -03000000ef0500000300000000020000,InterAct AxisPad,a:b2,b:b3,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a3,righty:a2,start:b11,x:b0,y:b1,platform:Mac OS X, -03000000fd0500000030000010010000,Interact GoPad,a:b3,b:b4,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b2,righttrigger:b5,x:b0,y:b1,platform:Mac OS X, -030000007e0500000620000001000000,Joy-Con (L),+leftx:h0.2,+lefty:h0.4,-leftx:h0.8,-lefty:h0.1,a:b0,b:b1,back:b13,leftshoulder:b4,leftstick:b10,rightshoulder:b5,start:b8,x:b2,y:b3,platform:Mac OS X, -030000007e0500000720000001000000,Joy-Con (R),+leftx:h0.2,+lefty:h0.4,-leftx:h0.8,-lefty:h0.1,a:b0,b:b1,back:b12,leftshoulder:b4,leftstick:b11,rightshoulder:b5,start:b9,x:b2,y:b3,platform:Mac OS X, -03000000242f00002d00000007010000,JYS Adapter,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Mac OS X, -030000006d04000019c2000000000000,Logitech Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -030000006d04000016c2000000020000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -030000006d04000016c2000000030000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -030000006d04000016c2000014040000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -030000006d04000016c2000000000000,Logitech F310,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -030000006d04000018c2000000000000,Logitech F510,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -030000006d04000019c2000005030000,Logitech F710,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -030000006d0400001fc2000000000000,Logitech F710,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, -030000006d04000018c2000000010000,Logitech RumblePad 2,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3~,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000380700005032000000010000,Mad Catz PS3 Fightpad Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000380700008433000000010000,Mad Catz PS3 Fightstick TE S Plus,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000380700005082000000010000,Mad Catz PS4 Fightpad Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000380700008483000000010000,Mad Catz PS4 Fightstick TE S Plus,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000790000000600000007010000,Marvo GT-004,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Mac OS X, -030000008f0e00001330000011010000,Mayflash Controller Adapter,a:b2,b:b4,back:b16,dpdown:h0.8,dpleft:h0.2,dpright:h0.1,dpup:h0.4,leftshoulder:b12,lefttrigger:b16,leftx:a0,lefty:a2,rightshoulder:b14,rightx:a6~,righty:a4,start:b18,x:b0,y:b6,platform:Mac OS X, -03000000790000004318000000010000,Mayflash GameCube Adapter,a:b4,b:b0,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a12,leftx:a0,lefty:a4,rightshoulder:b28,righttrigger:a16,rightx:a20,righty:a8,start:b36,x:b8,y:b12,platform:Mac OS X, -03000000790000004418000000010000,Mayflash GameCube Controller,a:b1,b:b2,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000242f00007300000000020000,Mayflash Magic NS,a:b1,b:b4,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b0,y:b3,platform:Mac OS X, -0300000079000000d218000026010000,Mayflash Magic NS,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Mac OS X, -03000000d620000010a7000003010000,Mayflash Magic NS,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -030000008f0e00001030000011010000,Mayflash Saturn Adapter,a:b0,b:b2,dpdown:b28,dpleft:b30,dpright:b26,dpup:b24,leftshoulder:b10,lefttrigger:b14,rightshoulder:b12,righttrigger:b4,start:b18,x:b6,y:b8,platform:Mac OS X, -0300000025090000e803000000000000,Mayflash Wii Classic Adapter,a:b1,b:b0,back:b8,dpdown:b13,dpleft:b12,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Mac OS X, -03000000790000000318000000010000,Mayflash Wii DolphinBar,a:b8,b:b12,back:b32,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b44,leftshoulder:b16,lefttrigger:b24,leftx:a0,lefty:a4,rightshoulder:b20,righttrigger:b28,rightx:a8,righty:a12,start:b36,x:b0,y:b4,platform:Mac OS X, -03000000790000000018000000000000,Mayflash Wii U Pro Adapter,a:b4,b:b8,back:b32,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b16,leftstick:b40,lefttrigger:b24,leftx:a0,lefty:a4,rightshoulder:b20,rightstick:b44,righttrigger:b28,rightx:a8,righty:a12,start:b36,x:b0,y:b12,platform:Mac OS X, -03000000790000000018000000010000,Mayflash Wii U Pro Adapter,a:b4,b:b8,back:b32,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b16,leftstick:b40,lefttrigger:b24,leftx:a0,lefty:a4,rightshoulder:b20,rightstick:b44,righttrigger:b28,rightx:a8,righty:a12,start:b36,x:b0,y:b12,platform:Mac OS X, -030000005e0400002800000002010000,Microsoft Dual Strike,a:b3,b:b2,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b8,rightshoulder:b7,rightx:a0,righty:a1~,start:b5,x:b1,y:b0,platform:Mac OS X, -030000005e0400000300000006010000,Microsoft SideWinder,a:b0,b:b1,back:b9,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b8,x:b3,y:b4,platform:Mac OS X, -030000005e0400000700000006010000,Microsoft SideWinder,a:b0,b:b1,back:b8,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b9,x:b3,y:b4,platform:Mac OS X, -030000005e0400002700000001010000,Microsoft SideWinder Plug and Play,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,lefttrigger:b4,righttrigger:b5,x:b2,y:b3,platform:Mac OS X, -030000004523000015e0000072050000,Mobapad Chitu HD,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X, -03000000d62000007162000001000000,Moga Pro 2,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Mac OS X, -03000000c62400002a89000000010000,MOGA XP5A Plus,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b21,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000c62400002b89000000010000,MOGA XP5A Plus,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000632500007505000000020000,NeoGeo mini PAD Controller,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,start:b9,x:b2,y:b3,platform:Mac OS X, -03000000921200004b46000003020000,NES 2-port Adapter,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,start:b11,platform:Mac OS X, -030000001008000001e5000006010000,NEXT SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,righttrigger:b6,start:b9,x:b3,y:b0,platform:Mac OS X, -030000007e0500000920000000000000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X, -030000007e0500000920000001000000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X, -050000007e05000009200000ff070000,Nintendo Switch Pro Controller,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b3,y:b2,platform:Mac OS X, -030000007e0500001920000001000000,NSO N64 Controller,+rightx:b8,+righty:b7,-rightx:b3,-righty:b2,a:b1,b:b0,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,righttrigger:b10,start:b9,platform:Mac OS X, -030000007e0500001720000001000000,NSO SNES Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b15,start:b9,x:b2,y:b3,platform:Mac OS X, -03000000550900001472000025050000,NVIDIA Controller,a:b0,b:b1,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b4,leftstick:b7,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a5,start:b6,x:b2,y:b3,platform:Mac OS X, -030000004b120000014d000000010000,Nyko Airflo EX,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b2,y:b3,platform:Mac OS X, -030000006f0e00000901000002010000,PDP PS3 Versus Fighting,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Mac OS X, -030000008f0e00000300000000000000,Piranha Xtreme PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Mac OS X, -03000000666600006706000088020000,PlayStation Adapter,a:b2,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,rightx:a2,righty:a3,start:b11,x:b3,y:b0,platform:Mac OS X, -030000004c050000da0c000000010000,PlayStation Classic Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b4,rightshoulder:b7,righttrigger:b5,start:b9,x:b3,y:b0,platform:Mac OS X, -030000004c0500003713000000010000,PlayStation Vita,a:b1,b:b2,back:b8,dpdown:b13,dpleft:b15,dpright:b14,dpup:b12,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000d620000011a7000000020000,PowerA Core Plus Gamecube Controller,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X, -03000000d620000011a7000010050000,PowerA Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000d62000006dca000000010000,PowerA Pro Ex,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000100800000300000006010000,PS2 Adapter,a:b2,b:b1,back:b8,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a4,righty:a3,start:b9,x:b3,y:b0,platform:Mac OS X, -030000004c0500006802000000000000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Mac OS X, -030000004c0500006802000000010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Mac OS X, -030000004c0500006802000072050000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Mac OS X, -030000004c050000a00b000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -030000004c050000c405000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -030000004c050000c405000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -030000004c050000cc09000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -030000004c050000e60c000000010000,PS5 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,misc1:b14,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Mac OS X, -030000004c050000f20d000000010000,PS5 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,misc1:b14,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Mac OS X, -050000004c050000e60c000000010000,PS5 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Mac OS X, -050000004c050000f20d000000010000,PS5 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Mac OS X, -030000005e040000e002000001000000,PXN P30 Pro Mobile,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Mac OS X, -03000000222c00000225000000010000,Qanba Dragon Arcade Joystick (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000222c00000020000000010000,Qanba Drone Arcade Stick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -030000008916000000fd000000000000,Razer Onza TE,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, -03000000321500000204000000010000,Razer Panthera PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000321500000104000000010000,Razer Panthera PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000321500000010000000010000,Razer Raiju,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000321500000507000001010000,Razer Raiju Mobile,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b21,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000321500000011000000010000,Razer Raion PS4 Fightpad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000321500000009000000020000,Razer Serval,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Mac OS X, -030000003215000000090000163a0000,Razer Serval,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Mac OS X, -0300000032150000030a000000000000,Razer Wildcat,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, -03000000632500008005000000010000,Redgear,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Mac OS X, -030000000d0f0000c100000072050000,Retro Bit Sega Genesis 6B Controller,a:b2,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,lefttrigger:b8,rightshoulder:b6,righttrigger:b7,start:b9,x:b3,y:b0,platform:Mac OS X, -03000000921200004547000000020000,Retro Bit Sega Genesis Controller Adapter,a:b0,b:b2,dpdown:+a2,dpleft:-a0,dpright:+a0,dpup:-a2,lefttrigger:b14,rightshoulder:b10,righttrigger:b4,start:b12,x:b6,y:b8,platform:Mac OS X, -03000000790000001100000000000000,Retro Controller,a:b1,b:b2,back:b8,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,leftshoulder:b6,lefttrigger:b7,rightshoulder:b4,righttrigger:b5,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000790000001100000005010000,Retro Controller,a:b1,b:b2,back:b8,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,leftshoulder:b6,lefttrigger:b7,rightshoulder:b5,righttrigger:b4,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000830500006020000000010000,Retro Controller,a:b0,b:b1,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b8,righttrigger:b9,start:b7,x:b2,y:b3,platform:Mac OS X, -0300000003040000c197000000000000,Retrode Adapter,a:b0,b:b4,back:b2,dpdown:+a4,dpleft:-a0,dpright:+a0,dpup:-a4,leftshoulder:b6,rightshoulder:b7,start:b3,x:b1,y:b5,platform:Mac OS X, -03000000790000001100000006010000,Retrolink SNES Controller,a:b2,b:b1,back:b8,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Mac OS X, -03000000341200000400000000000000,RetroUSB N64 RetroPort,+rightx:b8,+righty:b10,-rightx:b9,-righty:b11,a:b7,b:b6,dpdown:b2,dpleft:b1,dpright:b0,dpup:b3,leftshoulder:b13,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b12,start:b4,platform:Mac OS X, -030000006b140000010d000000010000,Revolution Pro Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -030000006b140000130d000000010000,Revolution Pro Controller 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -030000004c0500006802000002100000,Rii RK707,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b2,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b3,righttrigger:b9,rightx:a2,righty:a3,start:b1,x:b15,y:b12,platform:Mac OS X, -030000006f0e00008701000005010000,Rock Candy Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000c6240000fefa000000000000,Rock Candy PS3,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, -03000000e804000000a000001b010000,Samsung EIGP20,a:b1,b:b3,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b20,leftshoulder:b11,leftx:a1,lefty:a3,rightshoulder:b12,rightx:a4,righty:a5,start:b16,x:b7,y:b9,platform:Mac OS X, -03000000730700000401000000010000,Sanwa PlayOnline Mobile,a:b0,b:b1,back:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,start:b3,platform:Mac OS X, -03000000a30c00002500000006020000,Sega Genesis Mini 3B Controller,a:b2,b:b1,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,righttrigger:b5,start:b9,platform:Mac OS X, -03000000811700007e05000000000000,Sega Saturn,a:b2,b:b4,dpdown:b16,dpleft:b15,dpright:b14,dpup:b17,leftshoulder:b8,lefttrigger:a5,leftx:a0,lefty:a2,rightshoulder:b9,righttrigger:a4,start:b13,x:b0,y:b6,platform:Mac OS X, -03000000b40400000a01000000000000,Sega Saturn,a:b0,b:b1,back:b5,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,guide:b2,leftshoulder:b6,rightshoulder:b7,start:b8,x:b3,y:b4,platform:Mac OS X, -030000003512000021ab000000000000,SFC30 Joystick,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Mac OS X, -0300000000f00000f100000000000000,SNES RetroPort,a:b2,b:b3,back:b4,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b5,rightshoulder:b7,start:b6,x:b0,y:b1,platform:Mac OS X, -030000004c050000a00b000000000000,Sony DualShock 4 Adapter,a:b1,b:b2,back:b13,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -030000004c050000cc09000000000000,Sony DualShock 4 V2,a:b1,b:b2,back:b13,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000d11800000094000000010000,Stadia Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Mac OS X, -030000005e0400008e02000001000000,Steam Virtual Gamepad,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, -03000000110100002014000000000000,SteelSeries Nimbus,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,x:b2,y:b3,platform:Mac OS X, -03000000110100002014000001000000,SteelSeries Nimbus,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,x:b2,y:b3,platform:Mac OS X, -03000000381000002014000001000000,SteelSeries Nimbus,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,x:b2,y:b3,platform:Mac OS X, -05000000484944204465766963650000,SteelSeries Nimbus Plus,a:b0,b:b1,back:b15,dpdown:b11,dpleft:b13,dpright:b12,dpup:b10,guide:b16,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3~,start:b14,x:b2,y:b3,platform:Mac OS X, -050000004e696d6275732b0000000000,SteelSeries Nimbus Plus,a:b0,b:b1,back:b15,dpdown:b11,dpleft:b13,dpright:b12,dpup:b10,guide:b16,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3~,start:b14,x:b2,y:b3,platform:Mac OS X, -03000000381000003014000000000000,SteelSeries Stratus Duo,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, -03000000381000003114000000000000,SteelSeries Stratus Duo,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, -03000000110100001714000000000000,SteelSeries Stratus XL,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,start:b12,x:b2,y:b3,platform:Mac OS X, -03000000110100001714000020010000,SteelSeries Stratus XL,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,start:b12,x:b2,y:b3,platform:Mac OS X, -030000000d0f0000f600000000010000,Switch Hori Pad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X, -03000000457500002211000000010000,SZMY Power PC Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000790000001c18000003100000,TGZ Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000591c00002400000021000000,THEC64 Joystick,a:b0,b:b1,back:b6,leftshoulder:b4,leftx:a0,lefty:a4,rightshoulder:b5,start:b7,x:b2,y:b3,platform:Mac OS X, -03000000591c00002600000021000000,THEGamepad,a:b2,b:b1,back:b6,dpdown:+a4,dpleft:-a0,dpright:+a0,dpup:-a4,leftshoulder:b4,rightshoulder:b5,start:b7,x:b3,y:b0,platform:Mac OS X, -030000004f04000015b3000000000000,Thrustmaster Dual Analog 3.2,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Mac OS X, -030000004f0400000ed0000000020000,ThrustMaster eSwap Pro Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -030000004f04000000b3000000000000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b11,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:b7,rightx:a2,righty:a3,start:b10,x:b1,y:b3,platform:Mac OS X, -03000000571d00002100000021000000,Tomee NES Controller Adapter,a:b1,b:b0,back:b2,dpdown:+a4,dpleft:-a0,dpright:+a0,dpup:-a4,start:b3,platform:Mac OS X, -03000000bd12000015d0000000010000,Tomee Retro Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Mac OS X, -03000000bd12000015d0000000000000,Tomee SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Mac OS X, -03000000571d00002000000021000000,Tomee SNES Controller Adapter,a:b0,b:b1,back:b6,dpdown:+a4,dpleft:-a0,dpright:+a0,dpup:-a4,leftshoulder:b4,rightshoulder:b5,start:b7,x:b2,y:b3,platform:Mac OS X, -030000005f140000c501000000020000,Trust Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Mac OS X, -03000000100800000100000000000000,Twin USB Joystick,a:b4,b:b2,back:b16,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b12,leftstick:b20,lefttrigger:b8,leftx:a0,lefty:a2,rightshoulder:b14,rightstick:b22,righttrigger:b10,rightx:a6,righty:a4,start:b18,x:b6,y:b0,platform:Mac OS X, -03000000632500002605000000010000,Uberwith Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000151900005678000010010000,Uniplay U6,a:b3,b:b6,back:b25,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b17,leftstick:b31,lefttrigger:b21,leftx:a1,lefty:a3,rightshoulder:b19,rightstick:b33,righttrigger:b23,rightx:a4,righty:a5,start:b27,x:b11,y:b13,platform:Mac OS X, -030000006f0e00000302000025040000,Victrix PS4 Pro Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Mac OS X, -030000006f0e00000702000003060000,Victrix PS4 Pro Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Mac OS X, -050000005769696d6f74652028303000,Wii Remote,a:b4,b:b5,back:b7,dpdown:b3,dpleft:b0,dpright:b1,dpup:b2,guide:b8,leftshoulder:b11,lefttrigger:b12,leftx:a0,lefty:a1,start:b6,x:b10,y:b9,platform:Mac OS X, -050000005769696d6f74652028313800,Wii U Pro Controller,a:b16,b:b15,back:b7,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b8,leftshoulder:b19,leftstick:b23,lefttrigger:b21,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b24,righttrigger:b22,rightx:a2,righty:a3,start:b6,x:b18,y:b17,platform:Mac OS X, -030000005e0400008e02000000000000,Xbox 360 Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, -030000006f0e00000104000000000000,Xbox 360 Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, -03000000c6240000045d000000000000,Xbox 360 Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, -030000005e0400000a0b000000000000,Xbox Adaptive Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, -030000005e040000050b000003090000,Xbox Elite Controller Series 2,a:b0,b:b1,back:b31,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b53,leftshoulder:b6,leftstick:b13,lefttrigger:a6,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -030000005e040000130b000011050000,Xbox One Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -030000005e040000200b000011050000,Xbox One Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -030000005e040000200b000013050000,Xbox One Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -030000005e040000200b000015050000,Xbox One Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -030000005e040000d102000000000000,Xbox One Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, -030000005e040000dd02000000000000,Xbox One Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, -030000005e040000e002000000000000,Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Mac OS X, -030000005e040000e002000003090000,Xbox One Controller,a:b0,b:b1,back:b16,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -030000005e040000e302000000000000,Xbox One Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, -030000005e040000ea02000000000000,Xbox One Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, -030000005e040000fd02000003090000,Xbox One Controller,a:b0,b:b1,back:b16,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000c62400003a54000000000000,Xbox One PowerA Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, -030000005e040000130b000001050000,Xbox Series Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -030000005e040000130b000005050000,Xbox Series Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -030000005e040000130b000009050000,Xbox Series Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b15,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -030000005e040000130b000013050000,Xbox Series Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -030000005e040000130b000015050000,Xbox Series Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000172700004431000029010000,XiaoMi Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a6,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Mac OS X, -03000000120c0000100e000000010000,Zeroplus P4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -03000000120c0000101e000000010000,Zeroplus P4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, -030000005e0400008e02000020010000,8BitDo Adapter,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000c82d00000031000011010000,8BitDo Adapter,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000c82d00000951000000010000,8BitDo Dogbone,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a1,rightx:a2,righty:a3,start:b11,platform:Linux, -03000000021000000090000011010000,8BitDo FC30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00000090000011010000,8BitDo FC30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -05000000c82d00001038000000010000,8BitDo FC30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00001251000011010000,8BitDo Lite 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -05000000c82d00001251000000010000,8BitDo Lite 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00001151000011010000,8BitDo Lite SE,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -05000000c82d00001151000000010000,8BitDo Lite SE,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00000151000000010000,8BitDo M30,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:b7,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000c82d00000650000011010000,8BitDo M30,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,start:b11,x:b3,y:b4,platform:Linux, -05000000c82d00005106000000010000,8BitDo M30,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b8,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:b7,start:b11,x:b3,y:b4,platform:Linux, -03000000c82d00002090000011010000,8BitDo Micro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -05000000c82d00002090000000010000,8BitDo Micro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00000451000000010000,8BitDo N30,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a1,rightx:a2,righty:a3,start:b11,platform:Linux, -03000000c82d00001590000011010000,8BitDo N30 Pro 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -05000000c82d00006528000000010000,8BitDo N30 Pro 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -05000000c82d00006928000000010000,8BitDo N64,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a2,righty:a3,start:b11,platform:Linux, -05000000c82d00002590000001000000,8BitDo NEOGEO,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000008000000210000011010000,8BitDo NES30,a:b1,b:b2,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux, -03000000c82d00000310000011010000,8BitDo NES30,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b7,lefttrigger:b6,rightshoulder:b9,righttrigger:b8,start:b11,x:b3,y:b4,platform:Linux, -05000000c82d00008010000000010000,8BitDo NES30,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b7,lefttrigger:b6,rightshoulder:b9,righttrigger:b8,start:b11,x:b3,y:b4,platform:Linux, -03000000022000000090000011010000,8BitDo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00000190000011010000,8BitDo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -05000000203800000900000000010000,8BitDo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -05000000c82d00002038000000010000,8BitDo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00000751000000010000,8BitDo P30,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:a8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -05000000c82d00000851000000010000,8BitDo P30,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:a8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000c82d00000660000011010000,8BitDo Pro 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00001030000011010000,8BitDo Pro 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -05000000c82d00000660000000010000,8BitDo Pro 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00000020000000000000,8BitDo Pro 2 for Xbox,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -06000000c82d00000020000006010000,8BitDo Pro 2 for Xbox,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000c82d00000131000011010000,8BitDo Receiver,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00000231000011010000,8BitDo Receiver,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00000331000011010000,8BitDo Receiver,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00000431000011010000,8BitDo Receiver,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00002867000000010000,8BitDo S30,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b8,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:b7,rightx:a2,righty:a3,start:b10,x:b3,y:b4,platform:Linux, -05000000c82d00000060000000010000,8BitDo SF30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -05000000c82d00000061000000010000,8BitDo SF30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -030000003512000012ab000010010000,8BitDo SFC30,a:b2,b:b1,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b3,y:b0,platform:Linux, -030000003512000021ab000010010000,8BitDo SFC30,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d000021ab000010010000,8BitDo SFC30,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Linux, -05000000102800000900000000010000,8BitDo SFC30,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Linux, -05000000c82d00003028000000010000,8BitDo SFC30,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Linux, -05000000c82d00000351000000010000,8BitDo SN30,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00000160000000000000,8BitDo SN30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00000160000011010000,8BitDo SN30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00000161000000000000,8BitDo SN30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00001290000011010000,8BitDo SN30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Linux, -05000000c82d00000161000000010000,8BitDo SN30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -05000000c82d00006228000000010000,8BitDo SN30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00000260000011010000,8BitDo SN30 Pro Plus,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -05000000c82d00000261000000010000,8BitDo SN30 Pro Plus,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -05000000202800000900000000010000,8BitDo SNES30,a:b1,b:b0,back:b10,dpdown:b122,dpleft:b119,dpright:b120,dpup:b117,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Linux, -05000000c82d00001230000000010000,8BitDo Ultimate,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000c82d00001530000011010000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000c82d00001630000011010000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000c82d00001730000011010000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000c82d00001130000011010000,8BitDo Ultimate Wired,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b26,paddle1:b24,paddle2:b25,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000c82d00000760000011010000,8BitDo Ultimate Wireless,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -03000000c82d00001230000011010000,8BitDo Ultimate Wireless,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000c82d00001330000011010000,8BitDo Ultimate Wireless,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b26,paddle1:b23,paddle2:b19,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000c82d00000631000014010000,8BitDo Ultimate Wireless Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000c82d00000121000011010000,8BitDo Xbox One SN30 Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -05000000c82d00000121000000010000,8BitDo Xbox One SN30 Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -05000000a00500003232000001000000,8BitDo Zero,a:b0,b:b1,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Linux, -05000000a00500003232000008010000,8BitDo Zero,a:b0,b:b1,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Linux, -03000000c82d00001890000011010000,8BitDo Zero 2,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Linux, -050000005e040000e002000030110000,8BitDo Zero 2,a:b0,b:b1,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b2,y:b3,platform:Linux, -05000000c82d00003032000000010000,8BitDo Zero 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux, -03000000c01100000355000011010000,Acrux Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000006f0e00008801000011010000,Afterglow Deluxe Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000006f0e00003901000000430000,Afterglow Prismatic Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006f0e00003901000013020000,Afterglow Prismatic Controller 048-007-NA,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006f0e00001302000000010000,Afterglow Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006f0e00003901000020060000,Afterglow Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000100000008200000011010000,Akishop Customs PS360,a:b1,b:b2,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux, -030000007c1800000006000010010000,Alienware Dual Compatible Game PlayStation Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b3,platform:Linux, -05000000491900000204000021000000,Amazon Fire Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b17,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b12,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000491900001904000011010000,Amazon Luna Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,misc1:b9,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b7,x:b2,y:b3,platform:Linux, -05000000710100001904000000010000,Amazon Luna Controller,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Linux, -03000000790000003018000011010000,Arcade Fightstick F300,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux, -03000000a30c00002700000011010000,Astro City Mini,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,rightshoulder:b4,righttrigger:b5,start:b9,x:b3,y:b0,platform:Linux, -03000000a30c00002800000011010000,Astro City Mini,a:b2,b:b1,back:b8,leftx:a0,lefty:a1,rightshoulder:b4,righttrigger:b5,start:b9,x:b3,y:b0,platform:Linux, -05000000050b00000045000031000000,ASUS Gamepad,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b10,x:b2,y:b3,platform:Linux, -05000000050b00000045000040000000,ASUS Gamepad,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b10,x:b2,y:b3,platform:Linux, -03000000050b00000579000011010000,ASUS ROG Kunai 3,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b36,paddle1:b52,paddle2:b53,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -05000000050b00000679000000010000,ASUS ROG Kunai 3,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b21,paddle1:b22,paddle2:b23,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000503200000110000000000000,Atari Classic Controller,a:b0,b:b1,back:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b4,start:b3,platform:Linux, -03000000503200000110000011010000,Atari Classic Controller,a:b0,b:b1,back:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b4,start:b3,platform:Linux, -05000000503200000110000000000000,Atari Classic Controller,a:b0,b:b1,back:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b4,start:b3,platform:Linux, -05000000503200000110000044010000,Atari Classic Controller,a:b0,b:b1,back:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b4,start:b3,platform:Linux, -05000000503200000110000046010000,Atari Classic Controller,a:b0,b:b1,back:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b4,start:b3,platform:Linux, -03000000503200000210000000000000,Atari Controller,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a4,rightx:a2,righty:a3,start:b8,x:b2,y:b3,platform:Linux, -03000000503200000210000011010000,Atari Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b3,y:b2,platform:Linux, -05000000503200000210000000000000,Atari Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b3,y:b2,platform:Linux, -05000000503200000210000045010000,Atari Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b3,y:b2,platform:Linux, -05000000503200000210000046010000,Atari Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b3,y:b2,platform:Linux, -05000000503200000210000047010000,Atari VCS Modern Controller,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:+a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:-a4,rightx:a2,righty:a3,start:b8,x:b2,y:b3,platform:Linux, -03000000c62400001b89000011010000,BDA MOGA XP5X Plus,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000d62000002a79000011010000,BDA PS4 Fightpad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -03000000c21100000791000011010000,Be1 GC101 Controller 1.03,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -03000000c31100000791000011010000,Be1 GC101 Controller 1.03,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -030000005e0400008e02000003030000,Be1 GC101 Xbox 360,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000bc2000004d50000011010000,BEITONG A1T2 BFM,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -05000000bc2000000055000001000000,BETOP AX1 BFM,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000bc2000006412000011010000,Betop Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b30,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -030000006b1400000209000011010000,Bigben,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000120c0000200e000011010000,Brook Mars PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -03000000120c0000210e000011010000,Brook Mars PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000120c0000f70e000011010000,Brook Universal Fighting Board,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,rightstick:b11,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux, -03000000e82000006058000001010000,Cideko AK08b,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -030000000b0400003365000000010000,Competition Pro,a:b0,b:b1,back:b2,leftx:a0,lefty:a1,start:b3,platform:Linux, -03000000260900008888000000010000,Cyber Gadget GameCube Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:a5,rightx:a2,righty:a3~,start:b7,x:b2,y:b3,platform:Linux, -03000000a306000022f6000011010000,Cyborg V3 Rumble,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:+a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:-a3,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Linux, -03000000791d00000103000010010000,Dual Box Wii Classic Adapter,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -030000006f0e00003001000001010000,EA Sports PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000c11100000191000011010000,EasySMX,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -03000000242f00009100000000010000,EasySMX ESM-9101,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006e0500000320000010010000,Elecom U3613M,a:b2,b:b3,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,start:b11,x:b0,y:b1,platform:Linux, -030000006e0500000720000010010000,Elecom W01U,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b1,platform:Linux, -030000007d0400000640000010010000,Eliminator AfterShock,a:b1,b:b2,back:b9,dpdown:+a3,dpleft:-a5,dpright:+a5,dpup:-a3,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a4,righty:a2,start:b8,x:b0,y:b3,platform:Linux, -03000000430b00000300000000010000,EMS Production PS2 Adapter,a:b2,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a5,righty:a2,start:b9,x:b3,y:b0,platform:Linux, -030000006f0e00008401000011010000,Faceoff Deluxe Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000006f0e00008101000011010000,Faceoff Deluxe Pro Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000006f0e00008001000011010000,Faceoff Pro Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03005036852100000201000010010000,Final Fantasy XIV Online Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000b40400001124000011010000,Flydigi Vader 2,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b12,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b14,paddle1:b2,paddle2:b5,paddle3:b16,paddle4:b17,rightshoulder:b7,rightstick:b13,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000b40400001224000011010000,Flydigi Vader 2,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b12,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b2,paddle1:b16,paddle2:b17,paddle3:b14,paddle4:b15,rightshoulder:b7,rightstick:b13,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -05000000151900004000000001000000,Flydigi Vader 2,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b21,leftshoulder:b6,leftstick:b12,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b14,paddle1:b2,paddle2:b5,paddle3:b16,paddle4:b17,rightshoulder:b7,rightstick:b13,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -030000007e0500003703000000000000,GameCube Adapter,a:b0,b:b1,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b3,y:b2,platform:Linux, -19000000030000000300000002030000,GameForce Controller,a:b1,b:b0,back:b8,dpdown:b11,dpleft:b12,dpright:b13,dpup:b10,guide:b16,leftshoulder:b4,leftstick:b14,lefttrigger:b6,leftx:a1,lefty:a0,rightshoulder:b5,rightstick:b15,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b2,y:b3,platform:Linux, -03000000ac0500005b05000010010000,GameSir G3w,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -03000000bc2000000055000011010000,GameSir G3w,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000558500001b06000010010000,GameSir G4 Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -05000000ac0500002d0200001b010000,GameSir G4s,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b33,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000ac0500007a05000011010000,GameSir G5,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b16,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000bc2000005656000011010000,GameSir T4w,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000ac0500001a06000011010000,GameSir-T3 2.02,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -0500000047532047616d657061640000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -030000006f0e00000104000000010000,Gamestop Logic3 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000008f0e00000800000010010000,Gasia PlayStation Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -03000000451300000010000010010000,Genius Maxfire Grandias 12,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -190000004b4800000010000000010000,GO-Advance Controller,a:b1,b:b0,back:b10,dpdown:b7,dpleft:b8,dpright:b9,dpup:b6,leftshoulder:b4,lefttrigger:b12,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b13,start:b15,x:b2,y:b3,platform:Linux, -190000004b4800000010000001010000,GO-Advance Controller,a:b1,b:b0,back:b12,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,leftshoulder:b4,leftstick:b13,lefttrigger:b14,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b16,righttrigger:b15,start:b17,x:b2,y:b3,platform:Linux, -190000004b4800000011000000010000,GO-Super Controller,a:b1,b:b0,back:b12,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b16,leftshoulder:b4,leftstick:b14,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b15,righttrigger:b7,rightx:a2,righty:a3,start:b13,x:b2,y:b3,platform:Linux, -03000000f0250000c183000010010000,Goodbetterbest Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -0300000079000000d418000000010000,GPD Win 2 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e0400008e02000001010000,GPD Win Max 2 (6800U) Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000007d0400000540000000010000,Gravis Eliminator Pro,a:b1,b:b2,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux, -03000000280400000140000000010000,Gravis GamePad Pro,a:b1,b:b2,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux, -030000008f0e00000610000000010000,GreenAsia Electronics Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b9,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b10,righttrigger:b5,rightx:a3,righty:a2,start:b11,x:b3,y:b0,platform:Linux, -030000008f0e00001200000010010000,GreenAsia Joystick,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b2,y:b3,platform:Linux, -0500000047532067616d657061640000,GS gamepad,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -03000000f0250000c383000010010000,GT VX2,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -06000000adde0000efbe000002010000,Hidromancer Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000d81400000862000011010000,HitBox PS3 PC Analog Mode,a:b1,b:b2,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,guide:b9,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b12,x:b0,y:b3,platform:Linux, -03000000c9110000f055000011010000,HJC Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -030000000d0f00000d00000000010000,Hori,a:b0,b:b6,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b3,rightshoulder:b7,start:b9,x:b1,y:b2,platform:Linux, -030000000d0f00006d00000020010000,Hori EDGE 301,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:+a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:+a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000000d0f00008400000011010000,Hori Fighting Commander,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -030000000d0f00005f00000011010000,Hori Fighting Commander 4 PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000000d0f00005e00000011010000,Hori Fighting Commander 4 PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -030000000d0f00005001000009040000,Hori Fighting Commander OCTA Xbox One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000000d0f00008500000010010000,Hori Fighting Commander PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000000d0f00008600000002010000,Hori Fighting Commander Xbox 360,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -030000000d0f00003701000013010000,Hori Fighting Stick Mini,a:b1,b:b0,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,start:b7,x:b3,y:b2,platform:Linux, -030000000d0f00008800000011010000,Hori Fighting Stick mini 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,rightstick:b11,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux, -030000000d0f00008700000011010000,Hori Fighting Stick mini 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,rightshoulder:b5,rightstick:b11,righttrigger:a4,start:b9,x:b0,y:b3,platform:Linux, -030000000d0f00001000000011010000,Hori Fightstick 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux, -03000000ad1b000003f5000033050000,Hori Fightstick VX,+leftx:h0.2,+lefty:h0.4,-leftx:h0.8,-lefty:h0.1,a:b0,b:b1,back:b8,guide:b10,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b2,y:b3,platform:Linux, -030000000d0f00004d00000011010000,Hori Gem Pad 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000ad1b000001f5000033050000,Hori Pad EX Turbo 2,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000000d0f00003801000011010000,Hori PC Engine Mini Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,start:b9,platform:Linux, -030000000d0f00009200000011010000,Hori Pokken Tournament DX Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux, -030000000d0f00001100000011010000,Hori Real Arcade Pro 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000000d0f00002200000011010000,Hori Real Arcade Pro 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux, -030000000d0f00006a00000011010000,Hori Real Arcade Pro 4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -030000000d0f00006b00000011010000,Hori Real Arcade Pro 4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000000d0f00001600000000010000,Hori Real Arcade Pro EXSE,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b2,y:b3,platform:Linux, -030000000d0f0000aa00000011010000,Hori Real Arcade Pro for Nintendo Switch,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -030000000d0f00008501000015010000,Hori Switch Split Pad Pro,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000000d0f00006e00000011010000,Horipad 4 PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000000d0f00006600000011010000,Horipad 4 PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -030000000d0f0000ee00000011010000,Horipad Mini 4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -030000000d0f0000c100000011010000,Horipad Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000000d0f00006700000001010000,Horipad One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -050000000d0f0000f600000001000000,Horipad Switch Pro Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -03000000341a000005f7000010010000,HuiJia GameCube Controller Adapter,a:b1,b:b2,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Linux, -05000000242e00000b20000001000000,Hyperkin Admiral N64 Controller,+rightx:b11,+righty:b13,-rightx:b8,-righty:b12,a:b1,b:b0,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b14,leftx:a0,lefty:a1,rightshoulder:b5,start:b9,platform:Linux, -03000000242e0000ff0b000011010000,Hyperkin N64 Adapter,a:b1,b:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightx:a2,righty:a3,start:b9,platform:Linux, -03000000242e00006a38000010010000,Hyperkin Trooper 2,a:b0,b:b1,back:b4,leftshoulder:b2,leftx:a0,lefty:a1,rightshoulder:b3,start:b5,platform:Linux, -03000000242e00008816000001010000,Hyperkin X91,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000f00300008d03000011010000,HyperX Clutch,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000830500006020000010010000,iBuffalo Super Famicom Controller,a:b1,b:b0,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b3,y:b2,platform:Linux, -050000006964726f69643a636f6e0000,idroidcon Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000b50700001503000010010000,Impact,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Linux, -03000000d80400008200000003000000,IMS PCU0,a:b1,b:b0,back:b4,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,start:b5,x:b3,y:b2,platform:Linux, -03000000120c00000500000010010000,InterAct AxisPad,a:b2,b:b3,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a3,righty:a2,start:b11,x:b0,y:b1,platform:Linux, -03000000ef0500000300000000010000,InterAct AxisPad,a:b2,b:b3,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a3,righty:a2,start:b11,x:b0,y:b1,platform:Linux, -03000000fd0500000030000000010000,InterAct GoPad,a:b3,b:b4,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b2,righttrigger:b5,x:b0,y:b1,platform:Linux, -03000000fd0500002a26000000010000,InterAct HammerHead FX,a:b3,b:b4,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b2,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b5,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b0,y:b1,platform:Linux, -0500000049190000020400001b010000,Ipega PG 9069,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b161,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000632500007505000011010000,Ipega PG 9099,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -0500000049190000030400001b010000,Ipega PG9099,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -05000000491900000204000000000000,Ipega PG9118,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000300f00001001000010010000,Jess Tech Dual Analog Rumble,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Linux, -03000000300f00000b01000010010000,Jess Tech GGE909 PC Recoil,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux, -03000000ba2200002010000001010000,Jess Technology Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux, -030000007e0500000620000001000000,Joy-Con (L),+leftx:h0.2,+lefty:h0.4,-leftx:h0.8,-lefty:h0.1,a:b0,b:b1,back:b13,leftshoulder:b4,leftstick:b10,rightshoulder:b5,start:b8,x:b2,y:b3,platform:Linux, -050000007e0500000620000001000000,Joy-Con (L),+leftx:h0.2,+lefty:h0.4,-leftx:h0.8,-lefty:h0.1,a:b0,b:b1,back:b13,leftshoulder:b4,leftstick:b10,rightshoulder:b5,start:b8,x:b2,y:b3,platform:Linux, -030000007e0500000720000001000000,Joy-Con (R),+leftx:h0.2,+lefty:h0.4,-leftx:h0.8,-lefty:h0.1,a:b0,b:b1,back:b12,leftshoulder:b4,leftstick:b11,rightshoulder:b5,start:b9,x:b2,y:b3,platform:Linux, -050000007e0500000720000001000000,Joy-Con (R),+leftx:h0.2,+lefty:h0.4,-leftx:h0.8,-lefty:h0.1,a:b0,b:b1,back:b12,leftshoulder:b4,leftstick:b11,rightshoulder:b5,start:b9,x:b2,y:b3,platform:Linux, -03000000bd12000003c0000010010000,Joypad Alpha Shock,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000242f00002d00000011010000,JYS Adapter,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -03000000242f00008a00000011010000,JYS Adapter,a:b1,b:b4,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b0,y:b3,platform:Linux, -030000006f0e00000103000000020000,Logic3 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006d040000d1ca000000000000,Logitech Chillstream,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000006d040000d1ca000011010000,Logitech Chillstream,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000006d04000019c2000010010000,Logitech Cordless RumblePad 2,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000006d04000016c2000010010000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000006d04000016c2000011010000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000006d0400001dc2000014400000,Logitech F310,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006d0400001ec2000019200000,Logitech F510,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006d0400001ec2000020200000,Logitech F510,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006d04000019c2000011010000,Logitech F710,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000006d0400001fc2000005030000,Logitech F710,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006d04000018c2000010010000,Logitech RumblePad 2,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000006d04000011c2000010010000,Logitech WingMan Cordless RumblePad,a:b0,b:b1,back:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b6,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b10,rightx:a3,righty:a4,start:b8,x:b3,y:b4,platform:Linux, -030000006d0400000ac2000010010000,Logitech WingMan RumblePad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,rightx:a3,righty:a4,x:b3,y:b4,platform:Linux, -05000000380700006652000025010000,Mad Catz CTRLR,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000380700008532000010010000,Mad Catz Fightpad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b5,rightshoulder:b6,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux, -03000000380700005032000011010000,Mad Catz Fightpad Pro PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000380700005082000011010000,Mad Catz Fightpad Pro PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -03000000ad1b00002ef0000090040000,Mad Catz Fightpad SFxT,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,start:b7,x:b2,y:b3,platform:Linux, -03000000380700008034000011010000,Mad Catz Fightstick PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000380700008084000011010000,Mad Catz Fightstick PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -03000000380700008433000011010000,Mad Catz Fightstick TE S PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000380700008483000011010000,Mad Catz Fightstick TE S PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -03000000380700001888000010010000,Mad Catz Joystick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000380700003888000010010000,Mad Catz Joystick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:a0,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000380700001647000010040000,Mad Catz Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000380700003847000090040000,Mad Catz Xbox 360 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -03000000ad1b000016f0000090040000,Mad Catz Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000120c00000500000000010000,Manta Dualshock 2,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b2,y:b3,platform:Linux, -030000008f0e00001330000010010000,Mayflash Controller Adapter,a:b1,b:b2,back:b8,dpdown:h0.8,dpleft:h0.2,dpright:h0.1,dpup:h0.4,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a3~,righty:a2,start:b9,x:b0,y:b3,platform:Linux, -03000000790000004318000010010000,Mayflash GameCube Adapter,a:b1,b:b0,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b2,y:b3,platform:Linux, -03000000790000004418000010010000,Mayflash GameCube Controller,a:b1,b:b0,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b2,y:b3,platform:Linux, -03000000242f00007300000011010000,Mayflash Magic NS,a:b1,b:b4,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b0,y:b3,platform:Linux, -0300000079000000d218000011010000,Mayflash Magic NS,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000d620000010a7000011010000,Mayflash Magic NS,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000242f0000f700000001010000,Mayflash Magic S Pro,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000008f0e00001030000010010000,Mayflash Saturn Adapter,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,lefttrigger:b7,rightshoulder:b6,righttrigger:b2,start:b9,x:b3,y:b4,platform:Linux, -0300000025090000e803000001010000,Mayflash Wii Classic Adapter,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:a4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:a5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Linux, -03000000790000000318000011010000,Mayflash Wii DolphinBar,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b11,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b1,platform:Linux, -03000000790000000018000011010000,Mayflash Wii U Pro Adapter,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000b50700001203000010010000,Mega World Logic 3 Controller,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Linux, -03000000b50700004f00000000010000,Mega World Logic 3 Controller,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b1,platform:Linux, -03000000780000000600000010010000,Microntek Joystick,a:b2,b:b1,back:b8,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,start:b9,x:b3,y:b0,platform:Linux, -030000005e0400002800000000010000,Microsoft Dual Strike,a:b3,b:b2,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b8,rightshoulder:b7,rightx:a0,righty:a1~,start:b5,x:b1,y:b0,platform:Linux, -030000005e0400000300000000010000,Microsoft SideWinder,a:b0,b:b1,back:b9,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b8,x:b3,y:b4,platform:Linux, -030000005e0400000700000000010000,Microsoft SideWinder,a:b0,b:b1,back:b8,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b9,x:b3,y:b4,platform:Linux, -030000005e0400000e00000000010000,Microsoft SideWinder Freestyle Pro,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,rightshoulder:b7,start:b8,x:b3,y:b4,platform:Linux, -030000005e0400002700000000010000,Microsoft SideWinder Plug and Play,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,lefttrigger:b4,righttrigger:b5,x:b2,y:b3,platform:Linux, -030000005e0400008502000000010000,Microsoft Xbox,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b2,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b3,y:b4,platform:Linux, -030000005e0400008902000021010000,Microsoft Xbox,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b2,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b3,y:b4,platform:Linux, -030000005e0400008e02000001000000,Microsoft Xbox 360,a:b0,b:b1,back:b6,dpdown:h0.1,dpleft:h0.2,dpright:h0.8,dpup:h0.4,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e0400008e02000004010000,Microsoft Xbox 360,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e0400008e02000056210000,Microsoft Xbox 360,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e0400008e02000062230000,Microsoft Xbox 360,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000d102000001010000,Microsoft Xbox One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000d102000003020000,Microsoft Xbox One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000dd02000003020000,Microsoft Xbox One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000ea02000008040000,Microsoft Xbox One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -060000005e040000120b000009050000,Microsoft Xbox One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000e302000003020000,Microsoft Xbox One Elite,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000000b000007040000,Microsoft Xbox One Elite 2,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,paddle1:b12,paddle2:b14,paddle3:b13,paddle4:b15,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000000b000008040000,Microsoft Xbox One Elite 2,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,paddle1:b12,paddle2:b14,paddle3:b13,paddle4:b15,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -050000005e040000050b000003090000,Microsoft Xbox One Elite 2,a:b0,b:b1,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a6,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -050000005e0400008e02000030110000,Microsoft Xbox One Elite 2,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,paddle1:b11,paddle2:b13,paddle3:b12,paddle4:b14,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000120b00000b050000,Microsoft Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000030000000300000002000000,Miroof,a:b1,b:b0,back:b6,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,start:b7,x:b3,y:b2,platform:Linux, -03000000790000001c18000010010000,Mobapad Chitu HD,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -050000004d4f435554452d3035335800,Mocute 053X,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -05000000e80400006e0400001b010000,Mocute 053X M59,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -050000004d4f435554452d3035305800,Mocute 054X,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -05000000d6200000e589000001000000,Moga 2,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Linux, -05000000d6200000ad0d000001000000,Moga Pro,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Linux, -05000000d62000007162000001000000,Moga Pro 2,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Linux, -03000000c62400002b89000011010000,MOGA XP5A Plus,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -05000000c62400002a89000000010000,MOGA XP5A Plus,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b22,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -05000000c62400001a89000000010000,MOGA XP5X Plus,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000250900006688000000010000,MP8866 Super Dual Box,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Linux, -030000005e0400008e02000010020000,MSI GC20 V2,a:b0,b:b1,back:b6,dpdown:b13,dpleft:b14,dpright:b15,dpup:b12,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006b1400000906000014010000,Nacon Asymmetric Wireless PS4 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006b140000010c000010010000,Nacon GC 400ES,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -03000000853200000706000012010000,Nacon GC-100,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000000d0f00000900000010010000,Natec Genesis P44,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000004f1f00000800000011010000,NeoGeo PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux, -0300000092120000474e000000010000,NeoGeo X Arcade Stick,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,start:b9,x:b3,y:b2,platform:Linux, -03000000790000004518000010010000,Nexilux GameCube Controller Adapter,a:b1,b:b0,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b2,y:b3,platform:Linux, -030000001008000001e5000010010000,NEXT SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,righttrigger:b6,start:b9,x:b3,y:b0,platform:Linux, -060000007e0500003713000000000000,Nintendo 3DS,a:b0,b:b1,back:b8,dpdown:b11,dpleft:b12,dpright:b13,dpup:b10,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Linux, -030000009b2800008000000020020000,Nintendo Classic Controller,a:b1,b:b4,back:b2,dpdown:b13,dpleft:b14,dpright:b15,dpup:b12,leftshoulder:b6,rightshoulder:b7,start:b3,x:b0,y:b5,platform:Linux, -030000007e0500003703000000016800,Nintendo GameCube Controller,a:b0,b:b2,dpdown:b6,dpleft:b4,dpright:b5,dpup:b7,lefttrigger:a4,leftx:a0,lefty:a1~,rightshoulder:b9,righttrigger:a5,rightx:a2,righty:a3~,start:b8,x:b1,y:b3,platform:Linux, -03000000790000004618000010010000,Nintendo GameCube Controller Adapter,a:b1,b:b0,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,rightx:a5~,righty:a2~,start:b9,x:b2,y:b3,platform:Linux, -060000004e696e74656e646f20537700,Nintendo Switch Combined Joy-Cons,a:b0,b:b1,back:b9,dpdown:b15,dpleft:b16,dpright:b17,dpup:b14,guide:b11,leftshoulder:b5,leftstick:b12,lefttrigger:b7,leftx:a0,lefty:a1,misc1:b4,rightshoulder:b6,rightstick:b13,righttrigger:b8,rightx:a2,righty:a3,start:b10,x:b3,y:b2,platform:Linux, -060000007e0500000620000000000000,Nintendo Switch Combined Joy-Cons,a:b0,b:b1,back:b9,dpdown:b15,dpleft:b16,dpright:b17,dpup:b14,guide:b11,leftshoulder:b5,leftstick:b12,lefttrigger:b7,leftx:a0,lefty:a1,misc1:b4,rightshoulder:b6,rightstick:b13,righttrigger:b8,rightx:a2,righty:a3,start:b10,x:b3,y:b2,platform:Linux, -060000007e0500000820000000000000,Nintendo Switch Combined Joy-Cons,a:b0,b:b1,back:b9,dpdown:b15,dpleft:b16,dpright:b17,dpup:b14,guide:b11,leftshoulder:b5,leftstick:b12,lefttrigger:b7,leftx:a0,lefty:a1,misc1:b4,rightshoulder:b6,rightstick:b13,righttrigger:b8,rightx:a2,righty:a3,start:b10,x:b3,y:b2,platform:Linux, -050000004c69632050726f20436f6e00,Nintendo Switch Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -050000007e0500000620000001800000,Nintendo Switch Left Joy-Con,a:b16,b:b15,back:b4,leftshoulder:b6,leftstick:b12,leftx:a1,lefty:a0~,rightshoulder:b8,start:b9,x:b14,y:b17,platform:Linux, -030000007e0500000920000000026803,Nintendo Switch Pro Controller,a:b1,b:b0,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Linux, -030000007e0500000920000011810000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b11,leftshoulder:b5,leftstick:b12,lefttrigger:b7,leftx:a0,lefty:a1,misc1:b4,rightshoulder:b6,rightstick:b13,righttrigger:b8,rightx:a2,righty:a3,start:b10,x:b3,y:b2,platform:Linux, -050000007e0500000920000001000000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -050000007e0500000920000001800000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b11,leftshoulder:b5,leftstick:b12,lefttrigger:b7,leftx:a0,lefty:a1,misc1:b4,rightshoulder:b6,rightstick:b13,righttrigger:b8,rightx:a2,righty:a3,start:b10,x:b3,y:b2,platform:Linux, -050000007e0500000720000001800000,Nintendo Switch Right Joy-Con,a:b1,b:b2,back:b9,leftshoulder:b4,leftstick:b10,leftx:a1~,lefty:a0,rightshoulder:b6,start:b8,x:b0,y:b3,platform:Linux, -05000000010000000100000003000000,Nintendo Wii Remote,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -050000007e0500003003000001000000,Nintendo Wii U Pro Controller,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Linux, -030000000d0500000308000010010000,Nostromo n45 Dual Analog,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b12,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b10,x:b2,y:b3,platform:Linux, -030000007e0500001920000011810000,NSO N64 Controller,+rightx:b10,+righty:b8,-rightx:b9,-righty:b7,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b11,leftshoulder:b3,lefttrigger:b2,leftx:a0,lefty:a1,misc1:b12,rightshoulder:b4,righttrigger:b5,start:b6,platform:Linux, -050000007e0500001920000001000000,NSO N64 Controller,+rightx:b8,+righty:b7,-rightx:b3,-righty:b2,a:b1,b:b0,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,righttrigger:b10,start:b9,platform:Linux, -050000007e0500001920000001800000,NSO N64 Controller,+rightx:b10,+righty:b8,-rightx:b9,-righty:b7,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b11,leftshoulder:b3,lefttrigger:b2,leftx:a0,lefty:a1,misc1:b12,rightshoulder:b4,righttrigger:b5,start:b6,platform:Linux, -030000007e0500001720000011810000,NSO SNES Controller,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b3,y:b2,platform:Linux, -050000007e0500001720000001000000,NSO SNES Controller,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,lefttrigger:b7,rightshoulder:b6,righttrigger:b8,start:b10,x:b3,y:b2,platform:Linux, -050000007e0500001720000001800000,NSO SNES Controller,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b3,y:b2,platform:Linux, -03000000550900001072000011010000,NVIDIA Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b13,leftshoulder:b4,leftstick:b8,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Linux, -03000000550900001472000011010000,NVIDIA Controller v01.04,a:b0,b:b1,back:b14,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b16,leftshoulder:b4,leftstick:b7,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a5,start:b6,x:b2,y:b3,platform:Linux, -05000000550900001472000001000000,NVIDIA Controller v01.04,a:b0,b:b1,back:b14,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b16,leftshoulder:b4,leftstick:b7,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a5,start:b6,x:b2,y:b3,platform:Linux, -03000000451300000830000010010000,NYKO CORE,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -19000000010000000100000001010000,ODROID Go 2,a:b1,b:b0,dpdown:b7,dpleft:b8,dpright:b9,dpup:b6,guide:b10,leftshoulder:b4,leftstick:b12,lefttrigger:b11,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b13,righttrigger:b14,start:b15,x:b2,y:b3,platform:Linux, -19000000010000000200000011000000,ODROID Go 2,a:b1,b:b0,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b12,leftshoulder:b4,leftstick:b14,lefttrigger:b13,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b15,righttrigger:b16,start:b17,x:b2,y:b3,platform:Linux, -03000000c0160000dc27000001010000,OnyxSoft Dual JoyDivision,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b6,x:b2,y:b3,platform:Linux, -05000000362800000100000002010000,OUYA Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,x:b1,y:b2,platform:Linux, -05000000362800000100000003010000,OUYA Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,x:b1,y:b2,platform:Linux, -05000000362800000100000004010000,OUYA Controller,a:b0,b:b3,back:b14,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,leftshoulder:b4,leftstick:b6,lefttrigger:b12,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:b13,rightx:a3,righty:a4,start:b16,x:b1,y:b2,platform:Linux, -03000000830500005020000010010000,Padix Rockfire PlayStation Bridge,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,start:b11,x:b2,y:b3,platform:Linux, -03000000ff1100003133000010010000,PC Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -030000006f0e0000b802000001010000,PDP Afterglow Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006f0e0000b802000013020000,PDP Afterglow Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006f0e00006401000001010000,PDP Battlefield One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006f0e0000d702000006640000,PDP Black Camo Wired Xbox Series X Controller,a:b0,b:b1,back:b6,dpdown:b13,dpleft:b14,dpright:b13,dpup:b14,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006f0e00003101000000010000,PDP EA Sports Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006f0e00008501000011010000,PDP Fightpad Pro Gamecube Controller,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -030000006f0e0000c802000012010000,PDP Kingdom Hearts Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006f0e00002801000011010000,PDP PS3 Rock Candy Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000006f0e00000901000011010000,PDP PS3 Versus Fighting,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux, -03000000ad1b000004f9000000010000,PDP Xbox 360 Versus Fighting,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,start:b7,x:b2,y:b3,platform:Linux, -030000006f0e0000a802000023020000,PDP Xbox One Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -030000006f0e0000a702000023020000,PDP Xbox One Raven Black,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006f0e0000d802000006640000,PDP Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006f0e0000ef02000007640000,PDP Xbox Series Kinetic Wired Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000666600006706000000010000,PlayStation Adapter,a:b2,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b6,leftstick:b9,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b10,righttrigger:b5,rightx:a2,righty:a3,start:b11,x:b3,y:b0,platform:Linux, -030000004c050000da0c000011010000,PlayStation Controller,a:b2,b:b1,back:b8,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,start:b9,x:b3,y:b0,platform:Linux, -03000000d9040000160f000000010000,PlayStation Controller Adapter,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux, -030000004c0500003713000011010000,PlayStation Vita,a:b1,b:b2,back:b8,dpdown:b13,dpleft:b15,dpright:b14,dpup:b12,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Linux, -03000000c62400000053000000010000,PowerA,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000c62400003a54000001010000,PowerA 1428124-01,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000d620000011a7000011010000,PowerA Core Plus Gamecube Controller,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -03000000dd62000015a7000011010000,PowerA Fusion Nintendo Switch Arcade Stick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000d620000012a7000011010000,PowerA Fusion Nintendo Switch Fight Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000d62000000140000001010000,PowerA Fusion Pro 2 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000dd62000016a7000000000000,PowerA Fusion Pro Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000c62400001a53000000010000,PowerA Mini Pro Ex,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000d620000013a7000011010000,PowerA Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000d62000006dca000011010000,PowerA Pro Ex,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000d620000014a7000011010000,PowerA Spectra Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000c62400001a58000001010000,PowerA Xbox One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000d62000000220000001010000,PowerA Xbox One Controller,a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Linux, -03000000d62000000228000001010000,PowerA Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000c62400001a54000001010000,PowerA Xbox One Mini Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000d62000000240000001010000,PowerA Xbox One Spectra Infinity,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000d62000000f20000001010000,PowerA Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b7,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006d040000d2ca000011010000,Precision Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000ff1100004133000010010000,PS2 Controller,a:b2,b:b1,back:b8,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,start:b9,x:b3,y:b0,platform:Linux, -03000000341a00003608000011010000,PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000004c0500006802000010010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux, -030000004c0500006802000010810000,PS3 Controller,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux, -030000004c0500006802000011010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux, -030000004c0500006802000011810000,PS3 Controller,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux, -030000005f1400003102000010010000,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -030000006f0e00001402000011010000,PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000008f0e00000300000010010000,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -050000004c0500006802000000000000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux, -050000004c0500006802000000010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:a12,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:a13,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux, -050000004c0500006802000000800000,PS3 Controller,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux, -050000004c0500006802000000810000,PS3 Controller,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux, -05000000504c415953544154494f4e00,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux, -060000004c0500006802000000010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux, -030000004c050000a00b000011010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -030000004c050000a00b000011810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux, -030000004c050000c405000011010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -030000004c050000c405000011810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux, -030000004c050000cc09000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -030000004c050000cc09000011010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -030000004c050000cc09000011810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux, -03000000c01100000140000011010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -050000004c050000c405000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -050000004c050000c405000000810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux, -050000004c050000c405000001800000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux, -050000004c050000cc09000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -050000004c050000cc09000000810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux, -050000004c050000cc09000001800000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux, -030000004c050000e60c000011010000,PS5 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,misc1:b14,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Linux, -030000004c050000e60c000011810000,PS5 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux, -030000004c050000f20d000011010000,PS5 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,misc1:b14,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Linux, -050000004c050000e60c000000010000,PS5 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Linux, -050000004c050000e60c000000810000,PS5 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux, -050000004c050000f20d000000010000,PS5 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Linux, -03000000300f00001211000011010000,Qanba Arcade Joystick,a:b2,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b5,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b6,start:b9,x:b1,y:b3,platform:Linux, -03000000222c00000225000011010000,Qanba Dragon Arcade Joystick (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000222c00000025000011010000,Qanba Dragon Arcade Joystick (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -03000000222c00000020000011010000,Qanba Drone Arcade PS4 Joystick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,rightshoulder:b5,righttrigger:a4,start:b9,x:b0,y:b3,platform:Linux, -03000000300f00001210000010010000,Qanba Joystick Plus,a:b0,b:b1,back:b8,leftshoulder:b5,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b4,righttrigger:b6,start:b9,x:b2,y:b3,platform:Linux, -03000000222c00000223000011010000,Qanba Obsidian Arcade Joystick (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000222c00000023000011010000,Qanba Obsidian Arcade Joystick (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -030000009b2800000300000001010000,Raphnet 4nes4snes,a:b0,b:b4,back:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b1,y:b5,platform:Linux, -030000009b2800004200000001010000,Raphnet Dual NES Adapter,a:b0,b:b1,back:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,start:b3,platform:Linux, -030000009b2800003200000001010000,Raphnet GC and N64 Adapter,a:b0,b:b7,dpdown:b11,dpleft:b12,dpright:b13,dpup:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b2,righttrigger:b5,rightx:a3,righty:a4,start:b3,x:b1,y:b8,platform:Linux, -030000009b2800006000000001010000,Raphnet GC and N64 Adapter,a:b0,b:b7,dpdown:b11,dpleft:b12,dpright:b13,dpup:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b2,righttrigger:b5,rightx:a3,righty:a4,start:b3,x:b1,y:b8,platform:Linux, -03000000f8270000bf0b000011010000,Razer Kishi,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -030000008916000001fd000024010000,Razer Onza Classic Edition,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000321500000204000011010000,Razer Panthera PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000321500000104000011010000,Razer Panthera PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -03000000321500000810000011010000,Razer Panthera PS4 Evo Arcade Stick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b13,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -03000000321500000010000011010000,Razer Raiju,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -03000000321500000507000000010000,Razer Raiju Mobile,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b21,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -05000000321500000a10000001000000,Razer Raiju Tournament Edition,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b13,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -03000000321500000011000011010000,Razer Raion PS4 Fightpad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -030000008916000000fe000024010000,Razer Sabertooth,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000c6240000045d000024010000,Razer Sabertooth,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000c6240000045d000025010000,Razer Sabertooth,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000321500000009000011010000,Razer Serval,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Linux, -050000003215000000090000163a0000,Razer Serval,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Linux, -0300000032150000030a000001010000,Razer Wildcat,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000321500000b10000011010000,Razer Wolverine PS5 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Linux, -03000000790000001100000010010000,Retro Controller,a:b1,b:b2,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b7,rightshoulder:b4,righttrigger:b5,start:b9,x:b0,y:b3,platform:Linux, -0300000003040000c197000011010000,Retrode Adapter,a:b0,b:b4,back:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b3,x:b1,y:b5,platform:Linux, -190000004b4800000111000000010000,RetroGame Joypad,a:b1,b:b0,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,leftshoulder:b4,leftstick:b11,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -0300000081170000990a000001010000,Retronic Adapter,a:b0,leftx:a0,lefty:a1,platform:Linux, -0300000000f000000300000000010000,RetroPad,a:b1,b:b5,back:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b0,y:b4,platform:Linux, -00000000526574726f53746f6e653200,RetroStone 2 Controller,a:b1,b:b0,back:b10,dpdown:b15,dpleft:b16,dpright:b17,dpup:b14,leftshoulder:b6,lefttrigger:b8,rightshoulder:b7,righttrigger:b9,start:b11,x:b4,y:b3,platform:Linux, -03000000341200000400000000010000,RetroUSB N64 RetroPort,+rightx:b8,+righty:b10,-rightx:b9,-righty:b11,a:b7,b:b6,dpdown:b2,dpleft:b1,dpright:b0,dpup:b3,leftshoulder:b13,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b12,start:b4,platform:Linux, -030000006b140000010d000011010000,Revolution Pro Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -030000006b140000130d000011010000,Revolution Pro Controller 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -030000006f0e00001f01000000010000,Rock Candy,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006f0e00008701000011010000,Rock Candy Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000006f0e00001e01000011010000,Rock Candy PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000c6240000fefa000000010000,Rock Candy Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000006f0e00004601000001010000,Rock Candy Xbox One Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000a306000023f6000011010000,Saitek Cyborg V1 PlayStation Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Linux, -03000000a30600001005000000010000,Saitek P150,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b7,lefttrigger:b6,rightshoulder:b2,righttrigger:b5,x:b3,y:b4,platform:Linux, -03000000a30600000701000000010000,Saitek P220,a:b2,b:b3,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b7,rightshoulder:b4,righttrigger:b5,x:b0,y:b1,platform:Linux, -03000000a30600000cff000010010000,Saitek P2500 Force Rumble,a:b2,b:b3,back:b11,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a3,righty:a2,start:b10,x:b0,y:b1,platform:Linux, -03000000a30600000c04000011010000,Saitek P2900,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b12,x:b0,y:b3,platform:Linux, -03000000a306000018f5000010010000,Saitek P3200 Rumble,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Linux, -03000000300f00001201000010010000,Saitek P380,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Linux, -03000000a30600000901000000010000,Saitek P880,a:b2,b:b3,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a3,righty:a2,x:b0,y:b1,platform:Linux, -03000000a30600000b04000000010000,Saitek P990 Dual Analog,a:b1,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b8,x:b0,y:b3,platform:Linux, -03000000a306000020f6000011010000,Saitek PS2700 Rumble,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Linux, -05000000e804000000a000001b010000,Samsung EIGP20,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000d81d00000e00000010010000,Savior,a:b0,b:b1,back:b8,leftshoulder:b6,leftstick:b10,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b2,rightstick:b11,righttrigger:b3,start:b9,x:b4,y:b5,platform:Linux, -03000000a30c00002500000011010000,Sega Genesis Mini 3B Controller,a:b2,b:b1,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,righttrigger:b5,start:b9,platform:Linux, -03000000790000001100000011010000,Sega Saturn,a:b1,b:b2,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b7,rightshoulder:b5,righttrigger:b4,start:b9,x:b0,y:b3,platform:Linux, -03000000790000002201000011010000,Sega Saturn,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b6,righttrigger:b7,start:b9,x:b2,y:b3,platform:Linux, -03000000b40400000a01000000010000,Sega Saturn,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b7,rightshoulder:b5,righttrigger:b2,start:b8,x:b3,y:b4,platform:Linux, -030000001f08000001e4000010010000,SFC Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Linux, -03000000632500002305000010010000,ShanWan Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -03000000632500002605000010010000,Shanwan Gamepad,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000632500007505000010010000,Shanwan Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -03000000bc2000000055000010010000,Shanwan Gamepad,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000f025000021c1000010010000,Shanwan Gioteck PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -03000000341a00000908000010010000,SL6566,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -050000004c050000cc09000001000000,Sony DualShock 4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -03000000ff000000cb01000010010000,Sony PlayStation Portable,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,start:b7,x:b2,y:b3,platform:Linux, -03000000250900000500000000010000,Sony PS2 pad with SmartJoy Adapter,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Linux, -030000005e0400008e02000073050000,Speedlink Torid,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e0400008e02000020200000,SpeedLink Xeox Pro Analog,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000d11800000094000011010000,Stadia Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Linux, -05000000d11800000094000000010000,Stadia Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Linux, -03000000de2800000112000001000000,Steam Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,paddle1:b11,paddle2:b10,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux, -03000000de2800000112000011010000,Steam Controller,a:b2,b:b3,back:b10,dpdown:+a5,dpleft:-a4,dpright:+a4,dpup:-a5,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a7,leftx:a0,lefty:a1,paddle1:b15,paddle2:b16,rightshoulder:b7,rightstick:b14,righttrigger:a6,rightx:a2,righty:a3,start:b11,x:b4,y:b5,platform:Linux, -03000000de2800000211000001000000,Steam Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,paddle1:b11,paddle2:b10,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux, -03000000de2800000211000011010000,Steam Controller,a:b2,b:b3,back:b10,dpdown:b18,dpleft:b19,dpright:b20,dpup:b17,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,paddle1:b16,paddle2:b15,rightshoulder:b7,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b5,platform:Linux, -03000000de2800004211000001000000,Steam Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,paddle1:b11,paddle2:b10,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux, -03000000de2800004211000011010000,Steam Controller,a:b2,b:b3,back:b10,dpdown:b18,dpleft:b19,dpright:b20,dpup:b17,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a7,leftx:a0,lefty:a1,paddle1:b16,paddle2:b15,rightshoulder:b7,righttrigger:a6,rightx:a2,righty:a3,start:b11,x:b4,y:b5,platform:Linux, -03000000de280000fc11000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -05000000de2800000212000001000000,Steam Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,paddle1:b11,paddle2:b10,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux, -05000000de2800000511000001000000,Steam Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,paddle1:b11,paddle2:b10,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux, -05000000de2800000611000001000000,Steam Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,paddle1:b11,paddle2:b10,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux, -03000000de2800000512000010010000,Steam Deck,a:b3,b:b4,back:b11,dpdown:b17,dpleft:b18,dpright:b19,dpup:b16,guide:b13,leftshoulder:b7,leftstick:b14,lefttrigger:a9,leftx:a0,lefty:a1,rightshoulder:b8,rightstick:b15,righttrigger:a8,rightx:a2,righty:a3,start:b12,x:b5,y:b6,platform:Linux, -03000000de280000ff11000001000000,Steam Virtual Gamepad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -050000004e696d6275732b0000000000,SteelSeries Nimbus Plus,a:b0,b:b1,back:b10,guide:b11,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,start:b12,x:b2,y:b3,platform:Linux, -03000000381000003014000075010000,SteelSeries Stratus Duo,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000381000003114000075010000,SteelSeries Stratus Duo,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -0500000011010000311400001b010000,SteelSeries Stratus Duo,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b32,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -05000000110100001914000009010000,SteelSeries Stratus XL,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000ad1b000038f0000090040000,Street Fighter IV Fightstick TE,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000003b07000004a1000000010000,Suncom SFX Plus,a:b0,b:b2,back:b7,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b4,rightshoulder:b9,righttrigger:b5,start:b8,x:b1,y:b3,platform:Linux, -03000000666600000488000000010000,Super Joy Box 5 Pro,a:b2,b:b1,back:b9,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Linux, -0300000000f00000f100000000010000,Super RetroPort,a:b1,b:b5,back:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b3,x:b0,y:b4,platform:Linux, -030000008f0e00000d31000010010000,SZMY Power 3 Turbo,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000457500000401000011010000,SZMY Power DS4 Wired Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -03000000457500002211000010010000,SZMY Power Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -030000008f0e00001431000010010000,SZMY Power PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -03000000ba2200000701000001010000,Technology Innovation PS2 Adapter,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a5,righty:a2,start:b9,x:b3,y:b2,platform:Linux, -03000000790000001c18000011010000,TGZ Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000591c00002400000010010000,THEC64 Joystick,a:b0,b:b1,back:b6,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,start:b7,x:b2,y:b3,platform:Linux, -03000000591c00002600000010010000,THEGamepad,a:b2,b:b1,back:b6,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,start:b7,x:b3,y:b0,platform:Linux, -030000004f04000015b3000001010000,Thrustmaster Dual Analog 3.2,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Linux, -030000004f04000015b3000010010000,Thrustmaster Dual Analog 4,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Linux, -030000004f04000020b3000010010000,Thrustmaster Dual Trigger,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Linux, -030000004f04000023b3000000010000,Thrustmaster Dual Trigger PlayStation Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -030000004f0400000ed0000011010000,Thrustmaster eSwap Pro Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -03000000b50700000399000000010000,Thrustmaster Firestorm Digital 2,a:b2,b:b4,back:b11,leftshoulder:b6,leftstick:b10,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b8,rightstick:b0,righttrigger:b9,start:b1,x:b3,y:b5,platform:Linux, -030000004f04000003b3000010010000,Thrustmaster Firestorm Dual Analog 2,a:b0,b:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b9,rightx:a2,righty:a3,x:b1,y:b3,platform:Linux, -030000004f04000000b3000010010000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b11,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b10,x:b1,y:b3,platform:Linux, -030000004f04000004b3000010010000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Linux, -030000004f04000026b3000002040000,Thrustmaster GP XID,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000c6240000025b000002020000,Thrustmaster GPX,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000004f04000008d0000000010000,Thrustmaster Run N Drive PlayStation Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -030000004f04000009d0000000010000,Thrustmaster Run N Drive PlayStation Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000004f04000007d0000000010000,Thrustmaster T Mini,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux, -030000004f04000012b3000010010000,Thrustmaster Vibrating Gamepad,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Linux, -03000000571d00002000000010010000,Tomee SNES Adapter,a:b0,b:b1,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b2,y:b3,platform:Linux, -03000000bd12000015d0000010010000,Tomee SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Linux, -03000000d814000007cd000011010000,Toodles 2008 Chimp PC PS3,a:b0,b:b1,back:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b3,y:b2,platform:Linux, -030000005e0400008e02000070050000,Torid,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000c01100000591000011010000,Torid,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -03000000680a00000300000003000000,TRBot Virtual Joypad,a:b11,b:b12,back:b15,dpdown:b6,dpleft:b3,dpright:b4,dpup:b5,leftshoulder:b17,leftstick:b21,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b22,righttrigger:a2,rightx:a3,righty:a4,start:b16,x:b13,y:b14,platform:Linux, -03000000780300000300000003000000,TRBot Virtual Joypad,a:b11,b:b12,back:b15,dpdown:b6,dpleft:b3,dpright:b4,dpup:b5,leftshoulder:b17,leftstick:b21,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b22,righttrigger:a2,rightx:a3,righty:a4,start:b16,x:b13,y:b14,platform:Linux, -03000000e00d00000300000003000000,TRBot Virtual Joypad,a:b11,b:b12,back:b15,dpdown:b6,dpleft:b3,dpright:b4,dpup:b5,leftshoulder:b17,leftstick:b21,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b22,righttrigger:a2,rightx:a3,righty:a4,start:b16,x:b13,y:b14,platform:Linux, -03000000f00600000300000003000000,TRBot Virtual Joypad,a:b11,b:b12,back:b15,dpdown:b6,dpleft:b3,dpright:b4,dpup:b5,leftshoulder:b17,leftstick:b21,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b22,righttrigger:a2,rightx:a3,righty:a4,start:b16,x:b13,y:b14,platform:Linux, -030000005f140000c501000010010000,Trust Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux, -06000000f51000000870000003010000,Turtle Beach Recon,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000100800000100000010010000,Twin PS2 Adapter,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux, -03000000151900005678000010010000,Uniplay U6,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000100800000300000010010000,USB Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux, -03000000790000000600000007010000,USB gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b3,y:b0,platform:Linux, -03000000790000001100000000010000,USB Gamepad,a:b2,b:b1,back:b8,dpdown:a0,dpleft:a1,dpright:a2,dpup:a4,start:b9,platform:Linux, -030000006f0e00000302000011010000,Victrix Pro Fightstick PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux, -030000006f0e00000702000011010000,Victrix Pro Fightstick PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux, -05000000ac0500003232000001000000,VR Box Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b2,y:b3,platform:Linux, -05000000434f4d4d414e440000000000,VX Gaming Command Series,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -0000000058626f782033363020576900,Xbox 360 Controller,a:b0,b:b1,back:b14,dpdown:b11,dpleft:b12,dpright:b13,dpup:b10,guide:b7,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Linux, -030000005e0400001907000000010000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e0400008e02000010010000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e0400008e02000014010000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e0400009102000007010000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000a102000000010000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000a102000007010000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000a102000030060000,Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e0400008e02000000010000,Xbox 360 EasySMX,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000a102000014010000,Xbox 360 Receiver,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e0400000202000000010000,Xbox Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b2,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b3,y:b4,platform:Linux, -030000006f0e00001304000000010000,Xbox Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000ffff0000ffff000000010000,Xbox Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b2,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b3,y:b4,platform:Linux, -0000000058626f782047616d65706100,Xbox Gamepad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Linux, -030000005e0400000a0b000005040000,Xbox One Controller,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b11,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Linux, -030000005e040000d102000002010000,Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000ea02000000000000,Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000ea02000001030000,Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -050000005e040000e002000003090000,Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -050000005e040000fd02000003090000,Xbox One Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b16,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -050000005e040000fd02000030110000,Xbox One Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -050000005e040000e302000002090000,Xbox One Elite,a:b0,b:b1,back:b136,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a6,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -050000005e040000220b000013050000,Xbox One Elite 2 Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -050000005e040000050b000002090000,Xbox One Elite Series 2,a:b0,b:b1,back:b136,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a6,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -030000005e040000ea02000011050000,Xbox One S Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -060000005e040000ea0200000b050000,Xbox One S Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -060000005e040000ea0200000d050000,Xbox One S Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000120b000001050000,Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000120b000005050000,Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000120b000007050000,Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000120b000009050000,Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000120b00000d050000,Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000120b00000f050000,Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:b13,dpleft:b14,dpright:b15,dpup:b12,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000130b000005050000,Xbox Series Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -050000005e040000130b000001050000,Xbox Series Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -050000005e040000130b000005050000,Xbox Series Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -050000005e040000130b000007050000,Xbox Series Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b15,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -050000005e040000130b000009050000,Xbox Series Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b15,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -050000005e040000130b000011050000,Xbox Series Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b15,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -050000005e040000130b000013050000,Xbox Series Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b15,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -050000005e040000130b000015050000,Xbox Series Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b15,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -060000005e040000120b000007050000,Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -060000005e040000120b00000b050000,Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -060000005e040000120b00000f050000,Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -060000005e040000120b00000d050000,Xbox Series X Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -050000005e040000200b000013050000,Xbox Wireless Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux, -03000000450c00002043000010010000,XEOX SL6556 BK,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux, -05000000172700004431000029010000,XiaoMi Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b20,leftshoulder:b6,leftstick:b13,lefttrigger:a7,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a6,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Linux, -03000000c0160000e105000001010000,XinMo Dual Arcade,a:b4,b:b3,back:b6,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b9,leftshoulder:b2,leftx:a0,lefty:a1,rightshoulder:b5,start:b7,x:b1,y:b0,platform:Linux, -xinput,XInput Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -03000000120c0000100e000011010000,Zeroplus P4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -03000000120c0000101e000011010000,Zeroplus P4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux, -38653964633230666463343334313533,8BitDo Adapter,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -36666264316630653965636634386234,8BitDo Adapter 2,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b19,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -38426974446f20417263616465205374,8BitDo Arcade Stick,a:b0,b:b1,back:b15,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,guide:b5,leftshoulder:b9,lefttrigger:a4,rightshoulder:b10,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -61393962646434393836356631636132,8BitDo Arcade Stick,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b20,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b19,y:b2,platform:Android, -64323139346131306233636562663738,8BitDo Arcade Stick,a:b0,b:b1,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b3,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,righttrigger:b10,rightx:a2,righty:a3,start:b18,x:b19,y:b2,platform:Android, -64643565386136613265663236636564,8BitDo Arcade Stick,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b20,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b19,y:b2,platform:Android, -33313433353539306634656436353432,8BitDo Dogbone,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a1,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -38426974446f20446f67626f6e65204d,8BitDo Dogbone,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a1,rightx:a2,righty:a3,start:b6,platform:Android, -34343439373236623466343934376233,8BitDo FC30 Pro,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b28,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b4,rightstick:b29,righttrigger:b7,start:b5,x:b30,y:b2,platform:Android, -38426974446f2038426974446f204c69,8BitDo Lite,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -30643332373663313263316637356631,8BitDo Lite 2,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -38426974446f204c6974652032000000,8BitDo Lite 2,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -62656331626461363634633735353032,8BitDo Lite 2,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -38393936616436383062666232653338,8BitDo Lite SE,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -38426974446f204c6974652053450000,8BitDo Lite SE,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -39356430616562366466646636643435,8BitDo Lite SE,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -05000000c82d000006500000ffff3f00,8BitDo M30,a:b1,b:b0,back:b4,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,guide:b17,leftshoulder:b9,lefttrigger:a5,rightshoulder:b10,righttrigger:a4,start:b6,x:b3,y:b2,platform:Android, -05000000c82d000051060000ffff3f00,8BitDo M30,a:b1,b:b0,back:b4,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,guide:b17,leftshoulder:b9,lefttrigger:a4,rightshoulder:b10,righttrigger:a5,start:b6,x:b3,y:b2,platform:Android, -32323161363037623637326438643634,8BitDo M30,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a1,rightshoulder:b9,righttrigger:b10,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -33656266353630643966653238646264,8BitDo M30,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b20,righttrigger:a5,start:b10,x:b19,y:b2,platform:Android, -38426974446f204d3330204d6f646b69,8BitDo M30,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a1,rightshoulder:b9,righttrigger:b10,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -39366630663062373237616566353437,8BitDo M30,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,righttrigger:b18,start:b6,x:b2,y:b3,platform:Android, -64653533313537373934323436343563,8BitDo M30,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:a4,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b9,righttrigger:b10,start:b6,x:b2,y:b3,platform:Android, -66356438346136366337386437653934,8BitDo M30,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,righttrigger:b10,start:b18,x:b19,y:b2,platform:Android, -66393064393162303732356665666366,8BitDo M30,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,righttrigger:a5,start:b6,x:b2,y:b3,platform:Android, -38426974446f204d6963726f2067616d,8BitDo Micro,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,lefttrigger:a4,leftx:b0,lefty:b1,rightshoulder:b10,righttrigger:a5,rightx:b2,righty:b3,start:b6,x:b3,y:b2,platform:Android, -61653365323561356263373333643266,8BitDo Micro,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,lefttrigger:a4,leftx:b0,lefty:b1,rightshoulder:b10,righttrigger:a5,rightx:b2,righty:b3,start:b6,x:b3,y:b2,platform:Android, -62613137616239666338343866326336,8BitDo Micro,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,lefttrigger:a4,leftx:b0,lefty:b1,rightshoulder:b10,righttrigger:a5,rightx:b2,righty:b3,start:b6,x:b3,y:b2,platform:Android, -33663431326134333366393233616633,8BitDo N30,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a1,rightx:a2,righty:a3,start:b6,platform:Android, -38426974446f204e3330204d6f646b69,8BitDo N30,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a1,rightx:a2,righty:a3,start:b6,platform:Android, -05000000c82d000015900000ffff3f00,8BitDo N30 Pro 2,a:b1,b:b0,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -05000000c82d000065280000ffff3f00,8BitDo N30 Pro 2,a:b1,b:b0,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b17,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -38323035343766666239373834336637,8BitDo N64,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,righttrigger:b18,rightx:a2,righty:a3,start:b6,platform:Android, -38426974446f204e3634204d6f646b69,8BitDo N64,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,righttrigger:b18,rightx:a2,righty:a3,start:b6,platform:Android, -32363135613966656338666638666237,8BitDo NEOGEO,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftx:a0,lefty:a1,rightshoulder:b10,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -35363534633333373639386466346631,8BitDo NEOGEO,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftx:a0,lefty:a1,rightshoulder:b10,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -38426974446f204e454f47454f204750,8BitDo NEOGEO,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftx:a0,lefty:a1,rightshoulder:b10,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -39383963623932353561633733306334,8BitDo NEOGEO,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftx:a0,lefty:a1,rightshoulder:b10,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000000220000000900000ffff3f00,8BitDo NES30 Pro,a:b1,b:b0,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -050000002038000009000000ffff3f00,8BitDo NES30 Pro,a:b1,b:b0,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -38313433643131656262306631373166,8BitDo P30,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -38326536643339353865323063616339,8BitDo P30,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -38426974446f2050333020636c617373,8BitDo P30,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -35376664343164386333616535333434,8BitDo Pro 2,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b17,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b18,righttrigger:a5,rightx:a3,start:b10,x:b19,y:b2,platform:Android, -38426974446f2038426974446f205072,8BitDo Pro 2,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -38426974446f2050726f203200000000,8BitDo Pro 2,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -62373739366537363166326238653463,8BitDo Pro 2,a:b1,b:b0,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,x:b3,y:b2,platform:Android, -38386464613034326435626130396565,8BitDo Receiver,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b19,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -38426974446f2038426974446f205265,8BitDo Receiver,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b19,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -66303230343038613365623964393766,8BitDo Receiver,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b19,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -38426974446f20533330204d6f646b69,8BitDo S30,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:a4,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b9,righttrigger:b10,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -66316462353561376330346462316137,8BitDo S30,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:a4,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b9,righttrigger:b10,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -05000000c82d000000600000ffff3f00,8BitDo SF30 Pro,a:b1,b:b0,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:b15,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b16,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -05000000c82d000000610000ffff3f00,8BitDo SF30 Pro,a:b1,b:b0,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -38426974646f20534633302050726f00,8BitDo SF30 Pro,a:b1,b:b0,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b15,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b16,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b2,y:b17,platform:Android, -61623334636338643233383735326439,8BitDo SFC30,a:b0,b:b1,back:b4,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b3,rightshoulder:b31,start:b5,x:b30,y:b2,platform:Android, -05000000c82d000012900000ffff3f00,8BitDo SN30,a:b1,b:b0,back:b4,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b9,rightshoulder:b10,start:b6,x:b3,y:b2,platform:Android, -05000000c82d000062280000ffff3f00,8BitDo SN30,a:b1,b:b0,back:b4,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b9,rightshoulder:b10,start:b6,x:b3,y:b2,platform:Android, -38316230613931613964356666353839,8BitDo SN30,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftx:a0,lefty:a1,rightshoulder:b10,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -38426974446f20534e3330204d6f646b,8BitDo SN30,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftx:a0,lefty:a1,rightshoulder:b10,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -65323563303231646531383162646335,8BitDo SN30,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftx:a0,lefty:a1,rightshoulder:b10,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -35383531346263653330306238353131,8BitDo SN30 PP,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -05000000c82d000001600000ffff3f00,8BitDo SN30 Pro,a:b1,b:b0,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -05000000c82d000002600000ffff0f00,8BitDo SN30 Pro Plus,a:b1,b:b0,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b17,leftshoulder:b9,leftstick:b7,lefttrigger:b15,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b16,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -36653638656632326235346264663661,8BitDo SN30 Pro Plus,a:b0,b:b1,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b15,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b6,righttrigger:b10,rightx:a2,righty:a3,start:b18,x:b19,y:b2,platform:Android, -38303232393133383836366330346462,8BitDo SN30 Pro Plus,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b17,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b18,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b19,y:b2,platform:Android, -38346630346135363335366265656666,8BitDo SN30 Pro Plus,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -38426974446f20534e33302050726f2b,8BitDo SN30 Pro Plus,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b19,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -536f6e7920436f6d707574657220456e,8BitDo SN30 Pro Plus,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -66306331643531333230306437353936,8BitDo SN30 Pro Plus,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -050000002028000009000000ffff3f00,8BitDo SNES30,a:b1,b:b0,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -050000003512000020ab000000780f00,8BitDo SNES30,a:b21,b:b20,back:b30,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b26,rightshoulder:b27,start:b31,x:b24,y:b23,platform:Android, -33666663316164653937326237613331,8BitDo Zero,a:b0,b:b1,back:b15,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b9,rightshoulder:b10,start:b6,x:b2,y:b3,platform:Android, -38426974646f205a65726f2047616d65,8BitDo Zero,a:b0,b:b1,back:b15,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b9,rightshoulder:b10,start:b6,x:b2,y:b3,platform:Android, -05000000c82d000018900000ffff0f00,8BitDo Zero 2,a:b1,b:b0,back:b4,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b9,rightshoulder:b10,start:b6,x:b3,y:b2,platform:Android, -05000000c82d000030320000ffff0f00,8BitDo Zero 2,a:b1,b:b0,back:b4,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b9,rightshoulder:b10,start:b6,x:b3,y:b2,platform:Android, -33663434393362303033616630346337,8BitDo Zero 2,a:b0,b:b1,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftx:a0,lefty:a1,rightshoulder:b20,start:b18,x:b19,y:b2,platform:Android, -34656330626361666438323266633963,8BitDo Zero 2,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b20,start:b10,x:b19,y:b2,platform:Android, -63396666386564393334393236386630,8BitDo Zero 2,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftx:a0,lefty:a1,rightshoulder:b10,start:b6,x:b3,y:b2,platform:Android, -63633435623263373466343461646430,8BitDo Zero 2,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftx:a0,lefty:a1,rightshoulder:b10,start:b6,x:b2,y:b3,platform:Android, -32333634613735616163326165323731,Amazon Luna Controller,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,x:b2,y:b3,platform:Android, -417374726f2063697479206d696e6920,Astro City Mini,a:b23,b:b22,back:b29,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,rightshoulder:b25,righttrigger:b26,start:b30,x:b24,y:b21,platform:Android, -35643263313264386134376362363435,Atari VCS Classic Controller,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,start:b6,platform:Android, -32353831643566306563643065356239,Atari VCS Modern Controller,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -32303165626138343962363666346165,Brook Mars PS4 Controller,a:b1,b:b19,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b15,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b6,righttrigger:b10,rightx:a2,righty:a3,start:b18,x:b0,y:b2,platform:Android, -38383337343564366131323064613561,Brook Mars PS4 Controller,a:b1,b:b19,back:b17,leftshoulder:b3,leftstick:b15,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b6,righttrigger:b10,rightx:a2,righty:a3,start:b18,x:b0,y:b2,platform:Android, -34313430343161653665353737323365,Elecom JC-W01U,a:b23,b:b24,back:b29,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b25,lefttrigger:b27,leftx:a0,lefty:a1,rightshoulder:b26,righttrigger:b28,rightx:a2,righty:a3,start:b30,x:b21,y:b22,platform:Android, -4875694a6961204a432d573031550000,Elecom JC-W01U,a:b23,b:b24,back:b29,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b25,lefttrigger:b27,leftx:a0,lefty:a1,rightshoulder:b26,righttrigger:b28,rightx:a2,righty:a3,start:b30,x:b21,y:b22,platform:Android, -30363230653635633863366338623265,Evo VR,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,lefty:a1,x:b2,y:b3,platform:Android, -05000000b404000011240000dfff3f00,Flydigi Vader 2,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,paddle1:b17,paddle2:b18,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -05000000bc20000000550000ffff3f00,GameSir G3w,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -34323662653333636330306631326233,Google Nexus,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -35383633353935396534393230616564,Google Stadia Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -05000000d6020000e5890000dfff3f00,GPD XD Plus,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a4,rightx:a2,righty:a5,start:b6,x:b2,y:b3,platform:Android, -05000000d6020000e5890000dfff3f80,GPD XD Plus,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a3,rightx:a4,righty:a5,start:b6,x:b2,y:b3,platform:Android, -66633030656131663837396562323935,Hori Battle,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Android, -35623466343433653739346434636330,Hori Fighting Commander 3 Pro,a:b1,b:b19,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,righttrigger:b10,rightx:a2,righty:a3,start:b18,x:b0,y:b2,platform:Android, -484f524920434f2e2c4c54442e203130,Hori Fighting Commander 3 Pro,a:b1,b:b19,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b20,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b3,righttrigger:b9,rightx:a2,righty:a3,start:b18,x:b0,y:b2,platform:Android, -484f524920434f2e2c4c544420205041,Hori Gem Pad 3,a:b1,b:b17,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b4,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b6,righttrigger:b10,rightx:a2,righty:a3,start:b16,x:b0,y:b2,platform:Android, -65656436646661313232656661616130,Hori PC Engine Mini Controller,a:b1,b:b19,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,start:b18,platform:Android, -31303433326562636431653534636633,Hori Real Arcade Pro 3,a:b1,b:b19,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,righttrigger:b10,rightx:a2,righty:a3,start:b18,x:b0,y:b2,platform:Android, -32656664353964393561366362333636,Hori Switch Split Pad Pro,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Android, -30306539356238653637313730656134,HORIPAD Switch Pro Controller,a:b0,b:b1,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b3,leftstick:b15,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b6,righttrigger:b10,rightx:a2,righty:a3,start:b18,x:b19,y:b2,platform:Android, -48797065726b696e2050616400000000,Hyperkin Admiral N64 Controller,+rightx:b6,+righty:b7,-rightx:b17,-righty:b5,a:b1,b:b0,leftshoulder:b3,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b20,start:b18,platform:Android, -62333331353131353034386136626636,Hyperkin Admiral N64 Controller,+rightx:b6,+righty:b7,-rightx:b17,-righty:b5,a:b1,b:b0,leftshoulder:b3,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b20,start:b18,platform:Android, -31306635363562663834633739396333,Hyperkin N64 Adapter,a:b1,b:b19,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,rightx:a2,righty:a3,start:b18,platform:Android, -5368616e57616e202020202048797065,Hyperkin N64 Adapter,a:b1,b:b19,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,rightx:a2,righty:a3,start:b18,platform:Android, -0500000083050000602000000ffe0000,iBuffalo SNES Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b15,rightshoulder:b16,start:b10,x:b2,y:b3,platform:Android, -5553422c322d6178697320382d627574,iBuffalo Super Famicom Controller,a:b1,b:b0,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b17,rightshoulder:b18,start:b10,x:b3,y:b2,platform:Android, -64306137363261396266353433303531,InterAct GoPad,a:b24,b:b25,leftshoulder:b23,lefttrigger:b27,leftx:a0,lefty:a1,rightshoulder:b26,righttrigger:b28,x:b21,y:b22,platform:Android, -532e542e442e20496e74657261637420,InterAct HammerHead FX,a:b23,b:b24,back:b30,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b26,leftstick:b22,lefttrigger:b28,leftx:a0,lefty:a1,rightshoulder:b27,rightstick:b25,righttrigger:b29,rightx:a2,righty:a3,start:b31,x:b20,y:b21,platform:Android, -65346535636333663931613264643164,Joy-Con,a:b21,b:b22,back:b29,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b25,lefttrigger:b27,leftx:a0,lefty:a1,rightshoulder:b26,righttrigger:b28,rightx:a2,righty:a3,start:b30,x:b23,y:b24,platform:Android, -33346566643039343630376565326335,Joy-Con (L),a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b15,rightshoulder:b20,start:b17,x:b19,y:b2,platform:Android, -35313531613435623366313835326238,Joy-Con (L),a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b15,rightshoulder:b20,start:b17,x:b19,y:b2,platform:Android, -4a6f792d436f6e20284c290000000000,Joy-Con (L),a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,rightshoulder:b20,start:b17,x:b19,y:b2,platform:Android, -38383665633039363066383334653465,Joy-Con (R),a:b0,b:b1,back:b5,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b15,rightshoulder:b20,start:b18,x:b19,y:b2,platform:Android, -39363561613936303237333537383931,Joy-Con (R),a:b0,b:b1,back:b5,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b15,rightshoulder:b20,start:b18,x:b19,y:b2,platform:Android, -4a6f792d436f6e202852290000000000,Joy-Con (R),a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,rightshoulder:b20,start:b18,x:b19,y:b2,platform:Android, -39656136363638323036303865326464,JYS Aapter,a:b1,b:b19,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b15,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b6,righttrigger:b10,rightx:a2,righty:a3,start:b18,x:b0,y:b2,platform:Android, -63316564383539663166353034616434,JYS Adapter,a:b1,b:b3,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b0,y:b2,platform:Android, -64623163333561643339623235373232,Logitech F310,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -35623364393661626231343866613337,Logitech F710,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -4c6f6769746563682047616d65706164,Logitech F710,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -64396331333230326333313330336533,Logitech F710,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -39653365373864633935383236363438,Logitech G Cloud,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -416d617a6f6e2047616d6520436f6e74,Luna Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Android, -4c756e612047616d6570616400000000,Luna Controller,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -30363066623539323534363639323363,Magic NS,a:b1,b:b19,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b15,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b6,righttrigger:b10,rightx:a2,righty:a3,start:b18,x:b0,y:b2,platform:Android, -31353762393935386662336365626334,Magic NS,a:b1,b:b19,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b15,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b6,righttrigger:b10,rightx:a2,righty:a3,start:b18,x:b0,y:b2,platform:Android, -39623565346366623931666633323530,Magic NS,a:b1,b:b3,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b0,y:b2,platform:Android, -6d6179666c617368206c696d69746564,Mayflash GameCube Adapter,a:b22,b:b21,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:b25,leftx:a0,lefty:a1,rightshoulder:b28,righttrigger:b26,rightx:a5,righty:a2,start:b30,x:b23,y:b24,platform:Android, -436f6e74726f6c6c6572000000000000,Mayflash N64 Adapter,a:b1,b:b19,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b3,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,rightx:a2,righty:a3,start:b18,platform:Android, -65666330633838383061313633326461,Mayflash N64 Adapter,a:b1,b:b19,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b3,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,rightx:a2,righty:a3,start:b18,platform:Android, -37316565396364386635383230353365,Mayflash Saturn Adapter,a:b21,b:b22,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b26,lefttrigger:b28,rightshoulder:b27,righttrigger:b23,start:b30,x:b24,y:b25,platform:Android, -4875694a696120205553422047616d65,Mayflash Saturn Adapter,a:b21,b:b22,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b26,lefttrigger:b28,rightshoulder:b27,righttrigger:b23,start:b30,x:b24,y:b25,platform:Android, -535a4d792d706f776572204c54442043,Mayflash Wii Classic Adapter,a:b23,b:b22,back:b29,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b31,leftshoulder:b27,lefttrigger:b25,leftx:a0,lefty:a1,rightshoulder:b28,righttrigger:b26,rightx:a2,righty:a3,start:b30,x:b24,y:b21,platform:Android, -30653962643666303631376438373532,Mayflash Wii DolphinBar,a:b23,b:b24,back:b29,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b0,leftshoulder:b25,lefttrigger:b27,leftx:a0,lefty:a1,rightshoulder:b26,righttrigger:b28,rightx:a2,righty:a3,start:b30,x:b21,y:b22,platform:Android, -39346131396233376535393665363161,Mayflash Wii U Pro Adapter,a:b22,b:b23,back:b29,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b25,leftstick:b31,lefttrigger:b27,rightshoulder:b26,rightstick:b0,righttrigger:b28,rightx:a0,righty:a1,start:b30,x:b21,y:b24,platform:Android, -31323564663862633234646330373138,Mega Drive,a:b23,b:b22,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,rightshoulder:b25,righttrigger:b26,start:b30,x:b24,y:b21,platform:Android, -37333564393261653735306132613061,Mega Drive,a:b21,b:b22,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b26,lefttrigger:b28,rightshoulder:b27,righttrigger:b23,start:b30,x:b24,y:b25,platform:Android, -64363363336633363736393038313464,Mega Drive,a:b1,b:b0,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,start:b9,x:b2,y:b3,platform:Android, -33323763323132376537376266393366,Microsoft Dual Strike,a:b24,b:b23,back:b25,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b27,lefttrigger:b29,rightshoulder:b78,rightx:a0,righty:a1~,start:b26,x:b22,y:b21,platform:Android, -30306461613834333439303734316539,Microsoft SideWinder Pro,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b20,lefttrigger:b9,rightshoulder:b19,righttrigger:b10,start:b17,x:b2,y:b3,platform:Android, -32386235353630393033393135613831,Microsoft Xbox Series Controller,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -4d4f42415041442050726f2d48440000,Mobapad Chitu HD,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -4d4f435554452d303533582d4d35312d,Mocute 053X,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -33343361376163623438613466616531,Mocute M053,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -39306635663061636563316166303966,Mocute M053,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -7573622067616d657061642020202020,NEXT SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,righttrigger:b6,start:b9,x:b3,y:b0,platform:Android, -050000007e05000009200000ffff0f00,Nintendo Switch Pro Controller,a:b0,b:b1,back:b15,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b3,leftstick:b4,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b6,righttrigger:b10,rightx:a2,righty:a3,start:b16,x:b17,y:b2,platform:Android, -34323437396534643531326161633738,Nintendo Switch Pro Controller,a:b0,b:b1,back:b15,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b9,leftstick:b7,lefttrigger:b17,misc1:b5,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -50726f20436f6e74726f6c6c65720000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b2,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b10,rightx:a2,righty:a3,start:b18,y:b3,platform:Android, -36326533353166323965623661303933,NSO N64 Controller,+rightx:b17,+righty:b10,-rightx:b2,-righty:b19,a:b1,b:b0,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b3,lefttrigger:b9,leftx:a0,lefty:a1,misc1:b7,rightshoulder:b20,righttrigger:b15,start:b18,platform:Android, -4e363420436f6e74726f6c6c65720000,NSO N64 Controller,+rightx:b17,+righty:b10,-rightx:b2,-righty:b19,a:b1,b:b0,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b3,lefttrigger:b9,leftx:a0,lefty:a1,misc1:b7,rightshoulder:b20,righttrigger:b15,start:b18,platform:Android, -534e455320436f6e74726f6c6c657200,NSO SNES Controller,a:b0,b:b1,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,rightshoulder:b20,start:b18,x:b19,y:b2,platform:Android, -64623863346133633561626136366634,NSO SNES Controller,a:b0,b:b1,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,rightshoulder:b20,start:b18,x:b19,y:b2,platform:Android, -050000005509000003720000cf7f3f00,NVIDIA Controller,a:b0,b:b1,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000005509000010720000ffff3f00,NVIDIA Controller,a:b0,b:b1,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000005509000014720000df7f3f00,NVIDIA Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a4,rightx:a2,righty:a5,start:b6,x:b2,y:b3,platform:Android, -050000005509000014720000df7f3f80,NVIDIA Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a3,rightx:a4,righty:a5,start:b6,x:b2,y:b3,platform:Android, -37336435666338653565313731303834,NVIDIA Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -4e564944494120436f72706f72617469,NVIDIA Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -61363931656135336130663561616264,NVIDIA Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -39383335313438623439373538343266,OUYA Controller,a:b0,b:b2,dpdown:b18,dpleft:b15,dpright:b16,dpup:b17,leftshoulder:b3,leftstick:b9,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b10,righttrigger:b7,rightx:a3,righty:a4,x:b1,y:b19,platform:Android, -4f5559412047616d6520436f6e74726f,OUYA Controller,a:b0,b:b2,dpdown:b18,dpleft:b15,dpright:b6,dpup:b17,leftshoulder:b3,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,x:b1,y:b19,platform:Android, -506572666f726d616e63652044657369,PDP PS3 Rock Candy Controller,a:b1,b:b17,back:h0.2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b4,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b6,righttrigger:b10,rightx:a2,righty:a3,start:b16,x:b0,y:b2,platform:Android, -62653335326261303663356263626339,PlayStation Classic Controller,a:b19,b:b1,back:b17,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b9,lefttrigger:b3,rightshoulder:b10,righttrigger:b20,start:b18,x:b2,y:b0,platform:Android, -536f6e7920496e746572616374697665,PlayStation Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,misc1:b8,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -576972656c65737320436f6e74726f6c,PlayStation Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -61653962353232366130326530363061,Pokken,a:b1,b:b19,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,lefttrigger:b9,rightshoulder:b20,righttrigger:b10,start:b18,x:b0,y:b2,platform:Android, -32666633663735353234363064386132,PS2,a:b23,b:b22,back:b29,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b27,lefttrigger:b25,leftx:a0,lefty:a1,rightshoulder:b28,righttrigger:b26,rightx:a3,righty:a2,start:b30,x:b24,y:b21,platform:Android, -050000004c05000068020000dfff3f00,PS3 Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -536f6e7920504c415953544154494f4e,PS3 Controller,a:b0,b:b1,back:b15,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -61363034663839376638653463633865,PS3 Controller,a:b0,b:b1,back:b15,dpdown:a14,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -66366539656564653432353139356536,PS3 Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -66383132326164626636313737373037,PS3 Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000004c050000c405000000783f00,PS4 Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000004c050000c4050000fffe3f00,PS4 Controller,a:b1,b:b17,back:b15,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b3,leftstick:b4,lefttrigger:+a3,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b6,righttrigger:+a4,rightx:a2,righty:a5,start:b16,x:b0,y:b2,platform:Android, -050000004c050000c4050000fffe3f80,PS4 Controller,a:b1,b:b17,back:b15,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b3,leftstick:b4,lefttrigger:+a2,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b6,righttrigger:+a3,rightx:a4,righty:a5,start:b16,x:b0,y:b2,platform:Android, -050000004c050000c4050000ffff3f00,PS4 Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000004c050000cc090000fffe3f00,PS4 Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000004c050000cc090000ffff3f00,PS4 Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -30303839663330346632363232623138,PS4 Controller,a:b1,b:b17,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b6,righttrigger:a4,rightx:a2,righty:a5,start:b16,x:b0,y:b2,platform:Android, -31326235383662333266633463653332,PS4 Controller,a:b1,b:b16,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b6,righttrigger:a4,rightx:a2,righty:a5,start:b17,x:b0,y:b2,platform:Android, -31373231336561636235613666323035,PS4 Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -31663838336334393132303338353963,PS4 Controller,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -34613139376634626133336530386430,PS4 Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -35643031303033326130316330353564,PS4 Controller,a:b1,b:b17,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b3,leftstick:b4,lefttrigger:+a3,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b6,righttrigger:+a4,rightx:a2,righty:a5,start:b16,x:b0,y:b2,platform:Android, -37626233336235343937333961353732,PS4 Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -37626464343430636562316661643863,PS4 Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -38393161636261653636653532386639,PS4 Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -63313733393535663339656564343962,PS4 Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -63393662363836383439353064663939,PS4 Controller,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -65366465656364636137653363376531,PS4 Controller,a:b1,b:b19,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b15,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b6,righttrigger:b10,rightx:a2,righty:a3,start:b18,x:b0,y:b2,platform:Android, -66613532303965383534396638613230,PS4 Controller,a:b1,b:b19,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b15,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b6,righttrigger:b10,rightx:a2,righty:a5,start:b18,x:b0,y:b2,platform:Android, -050000004c050000e60c0000fffe3f00,PS5 Controller,a:b1,b:b17,back:b15,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b3,leftstick:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b6,righttrigger:a4,rightx:a2,righty:a5,start:b16,x:b0,y:b2,platform:Android, -050000004c050000e60c0000fffe3f80,PS5 Controller,a:b1,b:b17,back:b15,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b3,leftstick:b4,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b6,righttrigger:a3,rightx:a4,righty:a5,start:b16,x:b0,y:b2,platform:Android, -050000004c050000e60c0000ffff3f00,PS5 Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -32346465346533616263386539323932,PS5 Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -32633532643734376632656664383733,PS5 Controller,a:b1,b:b19,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b15,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b6,righttrigger:b10,rightx:a2,righty:a5,start:b18,x:b0,y:b2,platform:Android, -37363764353731323963323639666565,PS5 Controller,a:b1,b:b19,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b15,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b6,righttrigger:b10,rightx:a2,righty:a5,start:b18,x:b0,y:b2,platform:Android, -61303162353165316365336436343139,PS5 Controller,a:b1,b:b19,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b15,lefttrigger:b9,leftx:a0,lefty:a1,misc1:b8,rightshoulder:b20,rightstick:b6,righttrigger:b10,rightx:a2,righty:a5,start:b18,x:b0,y:b2,platform:Android, -64336263393933626535303339616332,Qanba 4RAF,a:b0,b:b1,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b20,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b3,righttrigger:b9,rightx:a2,righty:a3,start:b18,x:b19,y:b2,platform:Android, -36626666353861663864336130363137,Razer Junglecat,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -05000000f8270000bf0b0000ffff3f00,Razer Kishi,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -62653861643333663663383332396665,Razer Kishi,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000003215000005070000ffff3f00,Razer Raiju Mobile,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000003215000007070000ffff3f00,Razer Raiju Mobile,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000003215000000090000bf7f3f00,Razer Serval,a:b0,b:b1,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,x:b2,y:b3,platform:Android, -5a6869587520526574726f2042697420,Retro Bit Saturn Controller,a:b21,b:b22,back:b29,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b25,lefttrigger:b26,rightshoulder:b27,righttrigger:b28,start:b30,x:b23,y:b24,platform:Android, -32417865732031314b6579732047616d,Retro Bit SNES Controller,a:b0,b:b1,back:b15,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b9,rightshoulder:b10,start:b6,x:b2,y:b3,platform:Android, -36313938306539326233393732613361,Retro Bit SNES Controller,a:b0,b:b1,back:b15,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b9,rightshoulder:b10,start:b6,x:b2,y:b3,platform:Android, -526574726f466c616720576972656420,Retro Controller,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b17,rightshoulder:b18,start:b10,x:b2,y:b3,platform:Android, -61343739353764363165343237303336,Retro Controller,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b17,lefttrigger:b18,leftx:a0,lefty:a1,start:b10,x:b2,y:b3,platform:Android, -526574726f696420506f636b65742043,Retroid Pocket,a:b1,b:b0,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,paddle1:b17,paddle2:b18,rightshoulder:b10,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -582d426f7820436f6e74726f6c6c6572,Retroid Pocket,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,paddle1:b17,paddle2:b18,rightshoulder:b10,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -38653130373365613538333235303036,Retroid Pocket 2,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -64363363336633363736393038313463,Retrolink,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,start:b6,platform:Android, -37393234373533633333323633646531,RetroUSB N64 RetroPort,+rightx:b17,+righty:b15,-rightx:b18,-righty:b6,a:b10,b:b9,dpdown:b19,dpleft:b1,dpright:b0,dpup:b2,leftshoulder:b7,lefttrigger:b20,leftx:a0,lefty:a1,rightshoulder:b5,start:b3,platform:Android, -5365616c6965436f6d707574696e6720,RetroUSB N64 RetroPort,+rightx:b17,+righty:b15,-rightx:b18,-righty:b6,a:b10,b:b9,dpdown:b19,dpleft:b1,dpright:b0,dpup:b2,leftshoulder:b7,lefttrigger:b20,leftx:a0,lefty:a1,rightshoulder:b5,start:b3,platform:Android, -526574726f5553422e636f6d20534e45,RetroUSB SNES RetroPort,a:b1,b:b20,back:b19,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b9,rightshoulder:b10,start:b2,x:b0,y:b3,platform:Android, -64643037633038386238303966376137,RetroUSB SNES RetroPort,a:b1,b:b20,back:b19,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b9,rightshoulder:b10,start:b2,x:b0,y:b3,platform:Android, -37656564346533643138636436356230,Rock Candy Switch Controller,a:b1,b:b19,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b3,leftstick:b15,lefttrigger:b9,leftx:a0,lefty:a1,misc1:b7,rightshoulder:b20,rightstick:b6,righttrigger:b10,rightx:a2,righty:a3,start:b18,x:b0,y:b2,platform:Android, -33373336396634316434323337666361,RumblePad 2,a:b22,b:b23,back:b29,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b25,lefttrigger:b27,leftx:a0,lefty:a1,rightshoulder:b26,righttrigger:b28,rightx:a2,righty:a3,start:b30,x:b21,y:b24,platform:Android, -36363537303435333566386638366333,Samsung EIGP20,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -53616d73756e672047616d6520506164,Samsung EIGP20,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -66386565396238363534313863353065,Sanwa PlayOnline Mobile,a:b21,b:b22,back:b23,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,start:b24,platform:Android, -32383165316333383766336338373261,Saturn,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:a4,righttrigger:a5,x:b2,y:b3,platform:Android, -38613865396530353338373763623431,Saturn,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b9,lefttrigger:b10,rightshoulder:b20,righttrigger:b19,start:b17,x:b2,y:b3,platform:Android, -61316232336262373631343137633631,Saturn,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:a4,righttrigger:a5,x:b2,y:b3,platform:Android, -30353835333338613130373363646337,SG H510,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b17,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b18,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b19,y:b2,platform:Android, -66386262366536653765333235343634,SG H510,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,x:b2,y:b3,platform:Android, -66633132393363353531373465633064,SG H510,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftstick:b17,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b18,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b19,y:b2,platform:Android, -62653761636366393366613135366338,SN30 PP,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b3,y:b2,platform:Android, -38376662666661636265313264613039,SNES,a:b0,b:b1,back:b9,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b3,rightshoulder:b20,start:b10,x:b19,y:b2,platform:Android, -5346432f555342205061640000000000,SNES Adapter,a:b0,b:b1,back:b9,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b3,rightshoulder:b20,start:b10,x:b19,y:b2,platform:Android, -5553422047616d657061642000000000,SNES Controller,a:b1,b:b0,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,rightshoulder:b10,start:b6,x:b3,y:b2,platform:Android, -63303964303462366136616266653561,Sony PSP,a:b21,b:b22,back:b27,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b25,leftx:a0,lefty:a1,rightshoulder:b26,start:b28,x:b23,y:b24,platform:Android, -63376637643462343766333462383235,Sony Vita,a:b1,b:b19,back:b17,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,leftx:a0,lefty:a1,rightshoulder:b20,rightx:a3,righty:a4,start:b18,x:b0,y:b2,platform:Android, -476f6f676c65204c4c43205374616469,Stadia Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -5374616469614e3848532d6532633400,Stadia Controller,a:b0,b:b1,back:b15,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -05000000de2800000511000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Android, -05000000de2800000611000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Android, -0500000011010000201400000f7e0f00,SteelSeries Nimbus,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b3,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b20,righttrigger:b10,rightx:a2,righty:a3,x:b19,y:b2,platform:Android, -35306436396437373135383665646464,SteelSeries Nimbus Plus,a:b0,b:b1,leftshoulder:b3,leftstick:b17,lefttrigger:b9,leftx:a0,rightshoulder:b20,rightstick:b18,righttrigger:b10,rightx:a2,x:b19,y:b2,platform:Android, -54475a20436f6e74726f6c6c65720000,TGZ Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -62363434353532386238336663643836,TGZ Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:b17,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:b18,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -37323236633763666465316365313236,THEC64 Joystick,a:b21,b:b22,back:b27,leftshoulder:b25,leftx:a0,lefty:a1,rightshoulder:b26,start:b27,x:b23,y:b24,platform:Android, -38346162326232346533316164363336,THEGamepad,a:b23,b:b22,back:b27,leftshoulder:b25,leftx:a0,lefty:a1,rightshoulder:b26,start:b28,x:b24,y:b21,platform:Android, -050000004f0400000ed00000fffe3f00,ThrustMaster eSwap Pro Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -5477696e20555342204a6f7973746963,Twin Joystick,a:b22,b:b21,back:b28,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b26,leftstick:b30,lefttrigger:b24,leftx:a0,lefty:a1,rightshoulder:b27,rightstick:b31,righttrigger:b25,rightx:a3,righty:a2,start:b29,x:b23,y:b20,platform:Android, -30623739343039643830333266346439,Valve Steam Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,leftx:a0,lefty:a1,paddle1:b24,paddle2:b23,rightshoulder:b10,rightstick:b8,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -31643365666432386133346639383937,Valve Steam Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,leftx:a0,lefty:a1,paddle1:b24,paddle2:b23,rightshoulder:b10,rightstick:b8,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -30386438313564306161393537333663,Wii Classic Adapter,a:b23,b:b22,back:b29,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b27,lefttrigger:b25,leftx:a0,lefty:a1,rightshoulder:b28,righttrigger:b26,rightx:a2,righty:a3,start:b30,x:b24,y:b21,platform:Android, -33333034646336346339646538643633,Wii Classic Adapter,a:b23,b:b22,back:b29,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b27,lefttrigger:b25,leftx:a0,lefty:a1,rightshoulder:b28,righttrigger:b26,rightx:a2,righty:a3,start:b30,x:b24,y:b21,platform:Android, -050000005e0400008e02000000783f00,Xbox 360 Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -30396232393162346330326334636566,Xbox 360 Controller,a:b0,b:b1,back:b4,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -38313038323730383864666463383533,Xbox 360 Controller,a:b0,b:b1,back:b4,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -58626f782033363020576972656c6573,Xbox 360 Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -65353331386662343338643939643636,Xbox 360 Controller,a:b0,b:b1,back:b4,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -65613532386633373963616462363038,Xbox 360 Controller,a:b0,b:b1,back:b4,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -47656e6572696320582d426f78207061,Xbox Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -4d6963726f736f667420582d426f7820,Xbox Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -64633436313965656664373634323364,Xbox Controller,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b19,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000005e04000091020000ff073f00,Xbox One Controller,a:b0,b:b1,back:b4,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Android, -050000005e04000091020000ff073f80,Xbox One Controller,a:b0,b:b1,back:b4,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000005e040000e00200000ffe3f00,Xbox One Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b3,leftstick:b15,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b16,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b17,y:b2,platform:Android, -050000005e040000e00200000ffe3f80,Xbox One Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b3,leftstick:b15,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b16,righttrigger:a5,rightx:a2,righty:a3,start:b10,x:b17,y:b2,platform:Android, -050000005e040000e0020000ffff3f00,Xbox One Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b4,leftshoulder:b3,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b17,y:b2,platform:Android, -050000005e040000e0020000ffff3f80,Xbox One Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b4,leftshoulder:b3,leftstick:b8,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b7,righttrigger:a5,rightx:a2,righty:a3,start:b10,x:b17,y:b2,platform:Android, -050000005e040000fd020000ffff3f00,Xbox One Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -33356661323266333733373865656366,Xbox One Controller,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -34356136633366613530316338376136,Xbox One Controller,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b3,leftstick:b15,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b16,righttrigger:a5,rightx:a3,righty:a4,x:b17,y:b2,platform:Android, -35623965373264386238353433656138,Xbox One Controller,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -36616131643361333337396261666433,Xbox One Controller,a:b0,b:b1,back:b15,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -58626f7820576972656c65737320436f,Xbox One Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000005e040000000b000000783f00,Xbox One Elite 2 Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Android, -050000005e040000000b000000783f80,Xbox One Elite 2 Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000005e040000050b0000ffff3f00,Xbox One Elite 2 Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a6,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000005e040000e002000000783f00,Xbox One S Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000005e040000ea02000000783f00,Xbox One S Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000005e040000fd020000ff7f3f00,Xbox One S Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000005e040000120b000000783f00,Xbox Series Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Android, -050000005e040000120b000000783f80,Xbox Series Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000005e040000130b0000ffff3f00,Xbox Series Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -65633038363832353634653836396239,Xbox Series Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android, -050000001727000044310000ffff3f00,XiaoMi Controller,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,leftstick:b7,lefttrigger:a7,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a6,rightx:a2,righty:a5,start:b6,x:b2,y:b3,platform:Android, -05000000ac0500000100000000006d01,*,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b4,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a5,rightx:a3,righty:a4,x:b2,y:b3,platform:iOS, -05000000ac050000010000004f066d01,*,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b4,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a5,rightx:a3,righty:a4,x:b2,y:b3,platform:iOS, -05000000ac05000001000000cf076d01,*,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,x:b2,y:b3,platform:iOS, -05000000ac05000001000000df076d01,*,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b2,y:b3,platform:iOS, -05000000ac05000001000000ff076d01,*,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b2,y:b3,platform:iOS, -05000000ac0500000200000000006d02,*,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b4,rightshoulder:b5,x:b2,y:b3,platform:iOS, -05000000ac050000020000004f066d02,*,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b4,rightshoulder:b5,x:b2,y:b3,platform:iOS, -050000008a35000003010000ff070000,Backbone One,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b2,y:b3,platform:iOS, -050000008a35000004010000ff070000,Backbone One,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b2,y:b3,platform:iOS, -4d466947616d65706164010000000000,MFi Extended Gamepad,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a5,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:iOS, -4d466947616d65706164020000000000,MFi Gamepad,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,rightshoulder:b5,start:b6,x:b2,y:b3,platform:iOS, -050000007e050000062000000f060000,Nintendo Switch Joy-Con (L),+leftx:h0.2,+lefty:h0.4,-leftx:h0.8,-lefty:h0.1,a:b0,b:b2,leftshoulder:b4,rightshoulder:b5,x:b1,y:b3,platform:iOS, -050000007e050000062000004f060000,Nintendo Switch Joy-Con (L),+leftx:h0.1,+lefty:h0.2,-leftx:h0.4,-lefty:h0.8,dpdown:b2,dpleft:b0,dpright:b3,dpup:b1,leftshoulder:b4,misc1:b6,rightshoulder:b5,platform:iOS, -050000007e05000008200000df070000,Nintendo Switch Joy-Con (L/R),a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b2,y:b3,platform:iOS, -050000007e0500000e200000df070000,Nintendo Switch Joy-Con (L/R),a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:iOS, -050000007e050000072000004f060000,Nintendo Switch Joy-Con (R),+rightx:h0.4,+righty:h0.8,-rightx:h0.1,-righty:h0.2,a:b1,b:b0,guide:b6,leftshoulder:b4,rightshoulder:b5,x:b3,y:b2,platform:iOS, -050000007e05000009200000df870000,Nintendo Switch Pro Controller,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b10,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:iOS, -050000007e05000009200000ff870000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b2,y:b3,platform:iOS, -050000004c050000cc090000df070000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b2,y:b3,platform:iOS, -050000004c050000cc090000df870001,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b2,y:b3,platform:iOS, -050000004c050000cc090000ff070000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b2,y:b3,platform:iOS, -050000004c050000cc090000ff870001,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,touchpad:b11,x:b2,y:b3,platform:iOS, -050000004c050000cc090000ff876d01,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b2,y:b3,platform:iOS, -050000004c050000e60c0000df870000,PS5 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b9,touchpad:b10,x:b2,y:b3,platform:iOS, -050000004c050000e60c0000ff870000,PS5 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,touchpad:b11,x:b2,y:b3,platform:iOS, -05000000ac0500000300000000006d03,Remote,a:b0,b:b2,leftx:a0,lefty:a1,platform:iOS, -05000000ac0500000300000043006d03,Remote,a:b0,b:b2,leftx:a0,lefty:a1,platform:iOS, -05000000de2800000511000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:iOS, -05000000de2800000611000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:iOS, -050000005e040000050b0000df070001,Xbox Elite Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,paddle1:b10,paddle2:b12,paddle3:b11,paddle4:b13,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b2,y:b3,platform:iOS, -050000005e040000050b0000ff070001,Xbox Elite Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,paddle1:b11,paddle2:b13,paddle3:b12,paddle4:b14,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b2,y:b3,platform:iOS, -050000005e040000e0020000df070000,Xbox One Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b2,y:b3,platform:iOS, -050000005e040000e0020000ff070000,Xbox One Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b2,y:b3,platform:iOS, -050000005e040000130b0000df870001,Xbox Series X Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b10,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b2,y:b3,platform:iOS, -050000005e040000130b0000ff870001,Xbox Series X Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b2,y:b3,platform:iOS, - - - -================================================ -File: resources/pop.ogg -================================================ -[Non-text file] - - -================================================ -File: resources/sample.ogv -================================================ -[Non-text file] - - -================================================ -File: resources/test.txt -================================================ -helloworld - - -================================================ -File: resources/test.zip -================================================ -[Non-text file] - - -================================================ -File: resources/tone.ogg -================================================ -[Non-text file] - - -================================================ -File: resources/vk_layer_settings.txt -================================================ - -# VK_LAYER_KHRONOS_validation - -# Fine Grained Locking -# ===================== -# .fine_grained_locking -# Enable fine grained locking for Core Validation, which should improve -# performance in multithreaded applications. This setting allows the -# optimization to be disabled for debugging. -khronos_validation.fine_grained_locking = true - -# Core -# ===================== -# .validate_core -# The main, heavy-duty validation checks. This may be valuable early in the -# development cycle to reduce validation output while correcting -# parameter/object usage errors. -khronos_validation.validate_core = true - -# Image Layout -# ===================== -# .check_image_layout -# Check that the layout of each image subresource is correct whenever it is used -# by a command buffer. These checks are very CPU intensive for some -# applications. -khronos_validation.check_image_layout = true - -# Command Buffer State -# ===================== -# .check_command_buffer -# Check that all Vulkan objects used by a command buffer have not been -# destroyed. These checks can be CPU intensive for some applications. -khronos_validation.check_command_buffer = true - -# Object in Use -# ===================== -# .check_object_in_use -# Check that Vulkan objects are not in use by a command buffer when they are -# destroyed. -khronos_validation.check_object_in_use = true - -# Query -# ===================== -# .check_query -# Checks for commands that use VkQueryPool objects. -khronos_validation.check_query = true - -# Shader -# ===================== -# .check_shaders -# Shader checks. These checks can be CPU intensive during application start up, -# especially if Shader Validation Caching is also disabled. -khronos_validation.check_shaders = true - -# Caching -# ===================== -# .check_shaders_caching -# Enable caching of shader validation results. -khronos_validation.check_shaders_caching = true - -# Handle Wrapping -# ===================== -# .unique_handles -# Handle wrapping checks. Disable this feature if you are exerience crashes when -# creating new extensions or developing new Vulkan objects/structures. -khronos_validation.unique_handles = true - -# Object Lifetime -# ===================== -# .object_lifetime -# Object tracking checks. This may not always be necessary late in a development -# cycle. -khronos_validation.object_lifetime = true - -# Stateless Parameter -# ===================== -# .stateless_param -# Stateless parameter checks. This may not always be necessary late in a -# development cycle. -khronos_validation.stateless_param = true - -# Thread Safety -# ===================== -# .thread_safety -# Thread checks. In order to not degrade performance, it might be best to run -# your program with thread-checking disabled most of the time, enabling it -# occasionally for a quick sanity check or when debugging difficult application -# behaviors. -khronos_validation.thread_safety = true - -# Synchronization -# ===================== -# .validate_sync -# Enable synchronization validation during command buffers recording. This -# feature reports resource access conflicts due to missing or incorrect -# synchronization operations between actions (Draw, Copy, Dispatch, Blit) -# reading or writing the same regions of memory. -khronos_validation.validate_sync = true - -# QueueSubmit Synchronization Validation -# ===================== -# .sync_queue_submit -# Enable synchronization validation between submitted command buffers when -# Synchronization Validation is enabled. This option will increase the -# synchronization performance cost. -khronos_validation.sync_queue_submit = true - -# GPU Base -# ===================== -# .validate_gpu_based -# Setting an option here will enable specialized areas of validation -khronos_validation.validate_gpu_based = GPU_BASED_NONE - -# Redirect Printf messages to stdout -# ===================== -# .printf_to_stdout -# Enable redirection of Debug Printf messages from the debug callback to stdout -#khronos_validation.printf_to_stdout = true - -# Printf verbose -# ===================== -# .printf_verbose -# Set the verbosity of debug printf messages -#khronos_validation.printf_verbose = false - -# Printf buffer size -# ===================== -# .printf_buffer_size -# Set the size in bytes of the buffer used by debug printf -#khronos_validation.printf_buffer_size = 1024 - -# Shader instrumentation -# ===================== -# .gpuav_shader_instrumentation -# Instrument shaders to validate descriptors, descriptor indexing, buffer device -# addresses and ray queries. Warning: will considerably slow down shader -# executions. -#khronos_validation.gpuav_shader_instrumentation = true - -# Descriptors indexing -# ===================== -# .gpuav_descriptor_checks -# Enable descriptors and buffer out of bounds validation when using descriptor -# indexing -khronos_validation.gpuav_descriptor_checks = true - -# Generate warning on out of bounds accesses even if buffer robustness is enabled -# ===================== -# .gpuav_warn_on_robust_oob -# Warn on out of bounds accesses even if robustness is enabled -khronos_validation.gpuav_warn_on_robust_oob = true - -# Out of bounds buffer device addresses -# ===================== -# .gpuav_buffer_address_oob -# Check for -khronos_validation.gpuav_buffer_address_oob = true - -# Maximum number of buffer device addresses in use at one time -# ===================== -# .gpuav_max_buffer_device_addresses - -khronos_validation.gpuav_max_buffer_device_addresses = 10000 - -# RayQuery SPIR-V Instructions -# ===================== -# .gpuav_validate_ray_query -# Enable shader instrumentation on OpRayQueryInitializeKHR -khronos_validation.gpuav_validate_ray_query = true - -# Cache instrumented shaders rather than instrumenting them on every run -# ===================== -# .gpuav_cache_instrumented_shaders -# Enable instrumented shader caching -khronos_validation.gpuav_cache_instrumented_shaders = true - -# Enable instrumenting shaders selectively -# ===================== -# .gpuav_select_instrumented_shaders -# Select which shaders to instrument passing a VkValidationFeaturesEXT struct -# with GPU-AV enabled in the VkShaderModuleCreateInfo pNext -khronos_validation.gpuav_select_instrumented_shaders = false - -# Buffer content validation -# ===================== -# .gpuav_buffers_validation -# Validate buffers containing parameters used in indirect Vulkan commands, or -# used in copy commands -#khronos_validation.gpuav_buffers_validation = true - -# Indirect draws parameters -# ===================== -# .gpuav_indirect_draws_buffers -# Validate buffers containing draw parameters used in indirect draw commands -khronos_validation.gpuav_indirect_draws_buffers = true - -# Indirect dispatches parameters -# ===================== -# .indirect_dispatches -# Validate buffers containing dispatch parameters used in indirect dispatch -# commands -khronos_validation.indirect_dispatches = true - -# Indirect trace rays parameters -# ===================== -# .indirect_trace_rays -# Validate buffers containing ray tracing parameters used in indirect ray -# tracing commands -khronos_validation.indirect_trace_rays = true - -# Buffer copies -# ===================== -# .gpuav_buffer_copies -# Validate copies involving a VkBuffer. Right now only validates copy buffer to -# image. -khronos_validation.gpuav_buffer_copies = true - -# Reserve Descriptor Set Binding Slot -# ===================== -# .gpuav_reserve_binding_slot -# Specifies that the validation layers reserve a descriptor set binding slot for -# their own use. The layer reports a value for -# VkPhysicalDeviceLimits::maxBoundDescriptorSets that is one less than the value -# reported by the device. If the device supports the binding of only one -# descriptor set, the validation layer does not perform GPU-assisted validation. -#khronos_validation.gpuav_reserve_binding_slot = true - -# Linear Memory Allocation Mode -# ===================== -# .gpuav_vma_linear_output -# Use VMA linear memory allocations for GPU-AV output buffers instead of finding -# best place for new allocations among free regions to optimize memory usage. -# Enabling this setting reduces performance cost but disabling this method -# minimizes memory usage. -#khronos_validation.gpuav_vma_linear_output = true - -# Validate instrumented shaders -# ===================== -# .gpuav_debug_validate_instrumented_shaders -# Run spirv-val after doing shader instrumentation -#khronos_validation.gpuav_debug_validate_instrumented_shaders = false - -# Dump instrumented shaders -# ===================== -# .gpuav_debug_dump_instrumented_shaders -# Will dump the instrumented shaders (before and after) to working directory -#khronos_validation.gpuav_debug_dump_instrumented_shaders = false - -# Best Practices -# ===================== -# .validate_best_practices -# Outputs warnings related to common misuse of the API, but which are not -# explicitly prohibited by the specification. -khronos_validation.validate_best_practices = true - -# ARM-specific best practices -# ===================== -# .validate_best_practices_arm -# Outputs warnings for spec-conforming but non-ideal code on ARM GPUs. -khronos_validation.validate_best_practices_arm = false - -# AMD-specific best practices -# ===================== -# .validate_best_practices_amd -# Outputs warnings for spec-conforming but non-ideal code on AMD GPUs. -khronos_validation.validate_best_practices_amd = false - -# IMG-specific best practices -# ===================== -# .validate_best_practices_img -# Outputs warnings for spec-conforming but non-ideal code on Imagination GPUs. -khronos_validation.validate_best_practices_img = false - -# NVIDIA-specific best practices -# ===================== -# .validate_best_practices_nvidia -# Outputs warnings for spec-conforming but non-ideal code on NVIDIA GPUs. -khronos_validation.validate_best_practices_nvidia = false - -# Debug Action -# ===================== -# .debug_action -# Specifies what action is to be taken when a layer reports information -khronos_validation.debug_action = VK_DBG_LAYER_ACTION_LOG_MSG - -# Log Filename -# ===================== -# .log_filename -# Specifies the output filename -khronos_validation.log_filename = stdout - -# Message Severity -# ===================== -# .report_flags -# Comma-delineated list of options specifying the types of messages to be -# reported -khronos_validation.report_flags = error - -# Limit Duplicated Messages -# ===================== -# .enable_message_limit -# Enable limiting of duplicate messages. -khronos_validation.enable_message_limit = false - -# Max Duplicated Messages -# ===================== -# .duplicate_message_limit -# Maximum number of times any single validation message should be reported. -#khronos_validation.duplicate_message_limit = 10 - -# Mute Message VUIDs -# ===================== -# .message_id_filter -# List of VUIDs and VUID identifers which are to be IGNORED by the validation -# layer -khronos_validation.message_id_filter = - -# Display Application Name -# ===================== -# .message_format_display_application_name -# Useful when running multiple instances to know which instance the message is -# from. -khronos_validation.message_format_display_application_name = false - - - - -================================================ -File: tests/audio.lua -================================================ --- love.audio - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------OBJECTS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- RecordingDevice (love.audio.getRecordingDevices) -love.test.audio.RecordingDevice = function(test) - - -- skip recording device on runners, they cant emulate it - if GITHUB_RUNNER then - return test:skipTest('cant emulate recording devices in CI') - end - - -- check devices first - local devices = love.audio.getRecordingDevices() - if #devices == 0 then - return test:skipTest('cant test this works: no recording devices found') - end - - -- check object created and basics - local device = devices[1] - test:assertObject(device) - test:assertMatch({1, 2}, device:getChannelCount(), 'check channel count is 1 or 2') - test:assertNotEquals(nil, device:getName(), 'check has name') - - -- check initial data is empty as we haven't recorded anything yet - test:assertNotNil(device:getBitDepth()) - test:assertEquals(nil, device:getData(), 'check initial data empty') - test:assertEquals(0, device:getSampleCount(), 'check initial sample empty') - test:assertNotNil(device:getSampleRate()) - test:assertFalse(device:isRecording(), 'check not recording') - - -- start recording for a short time - local startrecording = device:start(32000, 4000, 16, 1) - test:waitFrames(10) - test:assertTrue(startrecording, 'check recording started') - test:assertTrue(device:isRecording(), 'check now recording') - test:assertEquals(4000, device:getSampleRate(), 'check sample rate set') - test:assertEquals(16, device:getBitDepth(), 'check bit depth set') - test:assertEquals(1, device:getChannelCount(), 'check channel count set') - local recording = device:stop() - test:waitFrames(10) - - -- after recording - test:assertFalse(device:isRecording(), 'check not recording') - test:assertEquals(nil, device:getData(), 'using stop should clear buffer') - test:assertObject(recording) - -end - - --- Source (love.audio.newSource) -love.test.audio.Source = function(test) - - -- create stereo source - local stereo = love.audio.newSource('resources/click.ogg', 'static') - test:assertObject(stereo) - - -- check stereo props - test:assertEquals(2, stereo:getChannelCount(), 'check stereo src') - test:assertRange(stereo:getDuration("seconds"), 0, 0.1, 'check stereo seconds') - test:assertNotNil(stereo:getFreeBufferCount()) - test:assertEquals('static', stereo:getType(), 'check stereo type') - - -- check cloning a stereo - local clone = stereo:clone() - test:assertEquals(2, clone:getChannelCount(), 'check clone stereo src') - test:assertRange(clone:getDuration("seconds"), 0, 0.1, 'check clone stereo seconds') - test:assertNotNil(clone:getFreeBufferCount()) - test:assertEquals('static', clone:getType(), 'check cloned stereo type') - - -- mess with stereo playing - test:assertFalse(stereo:isPlaying(), 'check not playing') - stereo:setLooping(true) - stereo:play() - test:assertTrue(stereo:isPlaying(), 'check now playing') - test:assertTrue(stereo:isLooping(), 'check now playing') - stereo:pause() - stereo:seek(0.01, 'seconds') - test:assertEquals(0.01, stereo:tell('seconds'), 'check seek/tell') - stereo:stop() - test:assertFalse(stereo:isPlaying(), 'check stopped playing') - - -- check volume limits - stereo:setVolumeLimits(0.1, 0.5) - local min, max = stereo:getVolumeLimits() - test:assertRange(min, 0.1, 0.2, 'check min limit') - test:assertRange(max, 0.5, 0.6, 'check max limit') - - -- check setting volume - stereo:setVolume(1) - test:assertEquals(1, stereo:getVolume(), 'check set volume') - stereo:setVolume(0) - test:assertEquals(0, stereo:getVolume(), 'check set volume') - - -- change some get/set props that can apply to stereo - stereo:setPitch(2) - test:assertEquals(2, stereo:getPitch(), 'check pitch change') - - -- create mono source - local mono = love.audio.newSource('resources/clickmono.ogg', 'stream') - test:assertObject(mono) - test:assertEquals(1, mono:getChannelCount(), 'check mono src') - test:assertEquals(2927, mono:getDuration("samples"), 'check mono seconds') - test:assertEquals('stream', mono:getType(), 'check mono type') - - -- air absorption - test:assertEquals(0, mono:getAirAbsorption(), 'get air absorption') - mono:setAirAbsorption(1) - test:assertEquals(1, mono:getAirAbsorption(), 'set air absorption') - - -- cone - mono:setCone(0, 90*(math.pi/180), 1) - local ia, oa, ov = mono:getCone() - test:assertEquals(0, ia, 'check cone ia') - test:assertEquals(math.floor(9000*(math.pi/180)), math.floor(oa*100), 'check cone oa') - test:assertEquals(1, ov, 'check cone ov') - - -- direction - mono:setDirection(3, 1, -1) - local x, y, z = mono:getDirection() - test:assertEquals(3, x, 'check direction x') - test:assertEquals(1, y, 'check direction y') - test:assertEquals(-1, z, 'check direction z') - - -- relative - mono:setRelative(true) - test:assertTrue(mono:isRelative(), 'check set relative') - - -- position - mono:setPosition(1, 2, 3) - x, y, z = mono:getPosition() - test:assertEquals(x, 1, 'check pos x') - test:assertEquals(y, 2, 'check pos y') - test:assertEquals(z, 3, 'check pos z') - - -- velocity - mono:setVelocity(1, 3, 4) - x, y, z = mono:getVelocity() - test:assertEquals(x, 1, 'check velocity x') - test:assertEquals(y, 3, 'check velocity x') - test:assertEquals(z, 4, 'check velocity x') - - -- rolloff - mono:setRolloff(1) - test:assertEquals(1, mono:getRolloff(), 'check rolloff set') - - -- create queue source - local queue = love.audio.newQueueableSource(44100, 16, 1, 3) - local sdata = love.sound.newSoundData(1024, 44100, 16, 1) - test:assertObject(queue) - local run = queue:queue(sdata) - test:assertTrue(run, 'check queued sound') - queue:stop() - - -- check making a filer - local setfilter = stereo:setFilter({ - type = 'lowpass', - volume = 0.5, - highgain = 0.3 - }) - test:assertTrue(setfilter, 'check filter applied') - local filter = stereo:getFilter() - test:assertEquals('lowpass', filter.type, 'check filter type') - test:assertEquals(0.5, filter.volume, 'check filter volume') - test:assertRange(filter.highgain, 0.3, 0.4, 'check filter highgain') - test:assertEquals(nil, filter.lowgain, 'check filter lowgain') - - -- add an effect - local effsource = love.audio.newSource('resources/click.ogg', 'static') - love.audio.setEffect('testeffect', { - type = 'flanger', - volume = 0.75 - }) - local seteffect, err = effsource:setEffect('testeffect', { - type = 'highpass', - volume = 0.3, - lowgain = 0.1 - }) - - -- both these fail on 12 using stereo or mono, no err - test:assertTrue(seteffect, 'check effect was applied') - local filtersettings = effsource:getEffect('effectthatdoesntexist', {}) - test:assertNotNil(filtersettings) - - love.audio.stop(stereo) - love.audio.stop(mono) - love.audio.stop(effsource) - -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------METHODS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.audio.getActiveEffects -love.test.audio.getActiveEffects = function(test) - -- check we get a value - test:assertNotNil(love.audio.getActiveEffects()) - -- check setting an effect active - love.audio.setEffect('testeffect', { - type = 'chorus', - volume = 0.75 - }) - test:assertEquals(1, #love.audio.getActiveEffects(), 'check 1 effect running') - test:assertEquals('testeffect', love.audio.getActiveEffects()[1], 'check effect details') -end - - --- love.audio.getActiveSourceCount -love.test.audio.getActiveSourceCount = function(test) - -- check we get a value - test:assertNotNil(love.audio.getActiveSourceCount()) - -- check source isn't active by default - local testsource = love.audio.newSource('resources/click.ogg', 'static') - love.audio.stop(testsource) - test:assertEquals(0, love.audio.getActiveSourceCount(), 'check not active') - -- check playing a source marks it as active - love.audio.play(testsource) - test:assertEquals(1, love.audio.getActiveSourceCount(), 'check now active') - love.audio.pause() -end - - --- love.audio.getDistanceModel -love.test.audio.getDistanceModel = function(test) - -- check we get a value - test:assertNotNil(love.audio.getDistanceModel()) - -- check default value from documentation - test:assertEquals('inverseclamped', love.audio.getDistanceModel(), 'check default value') - -- check get correct value after setting - love.audio.setDistanceModel('inverse') - test:assertEquals('inverse', love.audio.getDistanceModel(), 'check setting model') -end - - --- love.audio.getDopplerScale -love.test.audio.getDopplerScale = function(test) - -- check default value - test:assertEquals(1, love.audio.getDopplerScale(), 'check default 1') - -- check correct value after setting to 0 - love.audio.setDopplerScale(0) - test:assertEquals(0, love.audio.getDopplerScale(), 'check setting to 0') - love.audio.setDopplerScale(1) -end - - --- love.audio.getEffect -love.test.audio.getEffect = function(test) - -- check getting a non-existent effect - test:assertEquals(nil, love.audio.getEffect('madeupname'), 'check wrong name') - -- check getting a valid effect - love.audio.setEffect('testeffect', { - type = 'chorus', - volume = 0.75 - }) - test:assertNotNil(love.audio.getEffect('testeffect')) - -- check effect values match creation values - test:assertEquals('chorus', love.audio.getEffect('testeffect').type, 'check effect type') - test:assertEquals(0.75, love.audio.getEffect('testeffect').volume, 'check effect volume') -end - - --- love.audio.getMaxSceneEffects --- @NOTE feel like this is platform specific number so best we can do is a nil? -love.test.audio.getMaxSceneEffects = function(test) - test:assertNotNil(love.audio.getMaxSceneEffects()) -end - - --- love.audio.getMaxSourceEffects --- @NOTE feel like this is platform specific number so best we can do is a nil? -love.test.audio.getMaxSourceEffects = function(test) - test:assertNotNil(love.audio.getMaxSourceEffects()) -end - - --- love.audio.getOrientation --- @NOTE is there an expected default listener pos? -love.test.audio.getOrientation = function(test) - -- checking getting values matches what was set - love.audio.setOrientation(1, 2, 3, 4, 5, 6) - local fx, fy, fz, ux, uy, uz = love.audio.getOrientation() - test:assertEquals(1, fx, 'check fx orientation') - test:assertEquals(2, fy, 'check fy orientation') - test:assertEquals(3, fz, 'check fz orientation') - test:assertEquals(4, ux, 'check ux orientation') - test:assertEquals(5, uy, 'check uy orientation') - test:assertEquals(6, uz, 'check uz orientation') -end - - --- love.audio.getPlaybackDevice -love.test.audio.getPlaybackDevice = function(test) - test:assertNotNil(love.audio.getPlaybackDevice) - test:assertNotNil(love.audio.getPlaybackDevice()) -end - - --- love.audio.getPlaybackDevices -love.test.audio.getPlaybackDevices = function(test) - test:assertNotNil(love.audio.getPlaybackDevices) - test:assertGreaterEqual(0, #love.audio.getPlaybackDevices(), 'check table') -end - - --- love.audio.getPosition --- @NOTE is there an expected default listener pos? -love.test.audio.getPosition = function(test) - -- check getting values matches what was set - love.audio.setPosition(1, 2, 3) - local x, y, z = love.audio.getPosition() - test:assertEquals(1, x, 'check x position') - test:assertEquals(2, y, 'check y position') - test:assertEquals(3, z, 'check z position') -end - - --- love.audio.getRecordingDevices --- @NOTE hardware dependent so best can do is not nil check -love.test.audio.getRecordingDevices = function(test) - test:assertNotNil(love.audio.getRecordingDevices()) -end - - --- love.audio.getVelocity -love.test.audio.getVelocity = function(test) - -- check getting values matches what was set - love.audio.setVelocity(1, 2, 3) - local x, y, z = love.audio.getVelocity() - test:assertEquals(1, x, 'check x velocity') - test:assertEquals(2, y, 'check y velocity') - test:assertEquals(3, z, 'check z velocity') -end - - --- love.audio.getVolume -love.test.audio.getVolume = function(test) - -- check getting values matches what was set - love.audio.setVolume(0.5) - test:assertEquals(0.5, love.audio.getVolume(), 'check matches set') -end - - --- love.audio.isEffectsSupported -love.test.audio.isEffectsSupported = function(test) - test:assertNotNil(love.audio.isEffectsSupported()) -end - - --- love.audio.newQueueableSource --- @NOTE this is just basic nil checking, objs have their own test method -love.test.audio.newQueueableSource = function(test) - test:assertObject(love.audio.newQueueableSource(32, 8, 1, 8)) -end - - --- love.audio.newSource --- @NOTE this is just basic nil checking, objs have their own test method -love.test.audio.newSource = function(test) - test:assertObject(love.audio.newSource('resources/click.ogg', 'static')) - test:assertObject(love.audio.newSource('resources/click.ogg', 'stream')) -end - - --- love.audio.pause -love.test.audio.pause = function(test) - -- check nothing paused (as should be nothing playing) - local nopauses = love.audio.pause() - test:assertEquals(0, #nopauses, 'check nothing paused') - -- check 1 source paused after playing/pausing 1 - local source = love.audio.newSource('resources/click.ogg', 'static') - love.audio.play(source) - local onepause = love.audio.pause() - test:assertEquals(1, #onepause, 'check 1 paused') - love.audio.stop(source) -end - - --- love.audio.play -love.test.audio.play = function(test) - -- check playing source is detected - local source = love.audio.newSource('resources/click.ogg', 'static') - love.audio.play(source) - test:assertTrue(source:isPlaying(), 'check something playing') - love.audio.stop() -end - - --- love.audio.setDistanceModel -love.test.audio.setDistanceModel = function(test) - -- check setting each of the distance models is accepted and val returned - local distancemodel = { - 'none', 'inverse', 'inverseclamped', 'linear', 'linearclamped', - 'exponent', 'exponentclamped' - } - for d=1,#distancemodel do - love.audio.setDistanceModel(distancemodel[d]) - test:assertEquals(distancemodel[d], love.audio.getDistanceModel(), - 'check model set to ' .. distancemodel[d]) - end -end - - --- love.audio.setDopplerScale -love.test.audio.setDopplerScale = function(test) - -- check setting value is returned properly - love.audio.setDopplerScale(0) - test:assertEquals(0, love.audio.getDopplerScale(), 'check set to 0') - love.audio.setDopplerScale(1) - test:assertEquals(1, love.audio.getDopplerScale(), 'check set to 1') -end - - --- love.audio.setEffect -love.test.audio.setEffect = function(test) - -- check effect is set correctly - local effect = love.audio.setEffect('testeffect', { - type = 'chorus', - volume = 0.75 - }) - test:assertTrue(effect, 'check effect created') - -- check values set match - local settings = love.audio.getEffect('testeffect') - test:assertEquals('chorus', settings.type, 'check effect type') - test:assertEquals(0.75, settings.volume, 'check effect volume') -end - - --- love.audio.setMixWithSystem -love.test.audio.setMixWithSystem = function(test) - test:assertNotNil(love.audio.setMixWithSystem(true)) -end - - --- love.audio.setOrientation -love.test.audio.setOrientation = function(test) - -- check setting orientation vals are returned - love.audio.setOrientation(1, 2, 3, 4, 5, 6) - local fx, fy, fz, ux, uy, uz = love.audio.getOrientation() - test:assertEquals(1, fx, 'check fx orientation') - test:assertEquals(2, fy, 'check fy orientation') - test:assertEquals(3, fz, 'check fz orientation') - test:assertEquals(4, ux, 'check ux orientation') - test:assertEquals(5, uy, 'check uy orientation') - test:assertEquals(6, uz, 'check uz orientation') -end - - --- love.audio.setPlaybackDevice -love.test.audio.setPlaybackDevice = function(test) - -- check method - test:assertNotNil(love.audio.setPlaybackDevice) - - -- check blank string name - test:assertTrue(love.audio.setPlaybackDevice(''), 'check blank device is fine') - - -- check invalid name - test:assertFalse(love.audio.setPlaybackDevice('loveFM'), 'check invalid device fails') - - -- check setting already set - test:assertTrue(love.audio.setPlaybackDevice(love.audio.getPlaybackDevice()), 'check existing device is fine') - - -- if other devices to play with lets set a different one - local devices = love.audio.getPlaybackDevices() - if #devices > 1 then - local another = '' - local current = love.audio.getPlaybackDevice() - for a=1,#devices do - if devices[a] ~= current then - another = devices[a] - break - end - end - if another ~= '' then - -- check setting new device - local success4, msg4 = love.audio.setPlaybackDevice(another) - test:assertTrue(success4, 'check setting different device') - -- check resetting to default - local success5, msg5 = love.audio.setPlaybackDevice() - test:assertTrue(success5, 'check resetting') - test:assertEquals(current, love.audio.getPlaybackDevice()) - end - end -end - - --- love.audio.setPosition -love.test.audio.setPosition = function(test) - -- check setting position vals are returned - love.audio.setPosition(1, 2, 3) - local x, y, z = love.audio.getPosition() - test:assertEquals(1, x, 'check x position') - test:assertEquals(2, y, 'check y position') - test:assertEquals(3, z, 'check z position') -end - - --- love.audio.setVelocity -love.test.audio.setVelocity = function(test) - -- check setting velocity vals are returned - love.audio.setVelocity(1, 2, 3) - local x, y, z = love.audio.getVelocity() - test:assertEquals(1, x, 'check x velocity') - test:assertEquals(2, y, 'check y velocity') - test:assertEquals(3, z, 'check z velocity') -end - - --- love.audio.setVolume -love.test.audio.setVolume = function(test) - -- check setting volume works - love.audio.setVolume(0.5) - test:assertEquals(0.5, love.audio.getVolume(), 'check set to 0.5') -end - - --- love.audio.stop -love.test.audio.stop = function(test) - -- check source is playing first - local source = love.audio.newSource('resources/click.ogg', 'static') - love.audio.play(source) - test:assertTrue(source:isPlaying(), 'check is playing') - -- check source is then stopped - love.audio.stop() - test:assertFalse(source:isPlaying(), 'check stopped playing') -end - - - -================================================ -File: tests/data.lua -================================================ --- love.data - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------OBJECTS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- ByteData (love.data.newByteData) -love.test.data.ByteData = function(test) - - -- create new obj - local data = love.data.newByteData('helloworld') - test:assertObject(data) - - -- check properties match expected - test:assertEquals('helloworld', data:getString(), 'check data string') - test:assertEquals(10, data:getSize(), 'check data size') - - -- check cloning the bytedata - local cloneddata = data:clone() - test:assertObject(cloneddata) - test:assertEquals('helloworld', cloneddata:getString(), 'check cloned data') - test:assertEquals(10, cloneddata:getSize(), 'check cloned size') - - -- check pointer access if allowed - if data:getFFIPointer() ~= nil and ffi ~= nil then - local pointer = data:getFFIPointer() - local ptr = ffi.cast('uint8_t*', pointer) - local byte5 = ptr[4] - test:assertEquals('o', byte5) - end - - -- check overwriting the byte data string - data:setString('love!', 5) - test:assertEquals('hellolove!', data:getString(), 'check change string') - -end - - --- CompressedData (love.data.compress) -love.test.data.CompressedData = function(test) - - -- create new compressed data - local cdata = love.data.compress('data', 'zlib', 'helloworld', -1) - test:assertObject(cdata) - test:assertEquals('zlib', cdata:getFormat(), 'check format used') - - -- check properties match expected - test:assertEquals(18, cdata:getSize()) - test:assertEquals('helloworld', love.data.decompress('data', cdata):getString()) - - -- check cloning the data - local clonedcdata = cdata:clone() - test:assertObject(clonedcdata) - test:assertEquals('zlib', clonedcdata:getFormat()) - test:assertEquals(18, clonedcdata:getSize()) - test:assertEquals('helloworld', love.data.decompress('data', clonedcdata):getString()) - -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------METHODS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.data.compress -love.test.data.compress = function(test) - -- here just testing each combo 'works' - in decompress's test method - -- we actually check the compress + decompress give the right value back - local compressions = { - { love.data.compress('string', 'lz4', 'helloworld', -1), 'string'}, - { love.data.compress('string', 'lz4', 'helloworld', 0), 'string'}, - { love.data.compress('string', 'lz4', 'helloworld', 9), 'string'}, - { love.data.compress('string', 'zlib', 'helloworld', -1), 'string'}, - { love.data.compress('string', 'zlib', 'helloworld', 0), 'string'}, - { love.data.compress('string', 'zlib', 'helloworld', 9), 'string'}, - { love.data.compress('string', 'gzip', 'helloworld', -1), 'string'}, - { love.data.compress('string', 'gzip', 'helloworld', 0), 'string'}, - { love.data.compress('string', 'gzip', 'helloworld', 9), 'string'}, - { love.data.compress('string', 'deflate', 'aaaaaa', 1), 'string'}, - { love.data.compress('string', 'deflate', 'heloworld', -1), 'string'}, - { love.data.compress('string', 'deflate', 'heloworld', 0), 'string'}, - { love.data.compress('string', 'deflate', 'heloworld', 9), 'string'}, - { love.data.compress('data', 'lz4', 'helloworld', -1), 'userdata'}, - { love.data.compress('data', 'lz4', 'helloworld', 0), 'userdata'}, - { love.data.compress('data', 'lz4', 'helloworld', 9), 'userdata'}, - { love.data.compress('data', 'zlib', 'helloworld', -1), 'userdata'}, - { love.data.compress('data', 'zlib', 'helloworld', 0), 'userdata'}, - { love.data.compress('data', 'zlib', 'helloworld', 9), 'userdata'}, - { love.data.compress('data', 'gzip', 'helloworld', -1), 'userdata'}, - { love.data.compress('data', 'gzip', 'helloworld', 0), 'userdata'}, - { love.data.compress('data', 'gzip', 'helloworld', 9), 'userdata'}, - { love.data.compress('data', 'deflate', 'heloworld', -1), 'userdata'}, - { love.data.compress('data', 'deflate', 'heloworld', 0), 'userdata'}, - { love.data.compress('data', 'deflate', 'heloworld', 9), 'userdata'}, - } - for c=1,#compressions do - test:assertNotNil(compressions[c][1]) - -- sense check return type and make sure bytedata returns are an object - test:assertEquals(compressions[c][2], type(compressions[c][1]), 'check is userdata') - if compressions[c][2] == 'userdata' then - test:assertNotEquals(nil, compressions[c][1]:type(), 'check has :type()') - end - end -end - - --- love.data.decode -love.test.data.decode = function(test) - -- setup encoded strings - local str1 = love.data.encode('string', 'base64', 'helloworld', 0) - local str2 = love.data.encode('string', 'hex', 'helloworld') - local str3 = love.data.encode('data', 'base64', 'helloworld', 0) - local str4 = love.data.encode('data', 'hex', 'helloworld') - -- check value matches expected when decoded back - test:assertEquals('helloworld', love.data.decode('string', 'base64', str1), 'check string base64 decode') - test:assertEquals('helloworld', love.data.decode('string', 'hex', str2), 'check string hex decode') - test:assertEquals(love.data.newByteData('helloworld'):getString(), love.data.decode('data', 'base64', str3):getString(), 'check data base64 decode') - test:assertEquals(love.data.newByteData('helloworld'):getString(), love.data.decode('data', 'hex', str4):getString(), 'check data hex decode') -end - - --- love.data.decompress -love.test.data.decompress = function(test) - -- setup compressed data for each combination - local str1 = love.data.compress('string', 'lz4', 'helloworld', -1) - local str2 = love.data.compress('string', 'lz4', 'helloworld', 0) - local str3 = love.data.compress('string', 'lz4', 'helloworld', 9) - local str4 = love.data.compress('string', 'zlib', 'helloworld', -1) - local str5 = love.data.compress('string', 'zlib', 'helloworld', 0) - local str6 = love.data.compress('string', 'zlib', 'helloworld', 9) - local str7 = love.data.compress('string', 'gzip', 'helloworld', -1) - local str8 = love.data.compress('string', 'gzip', 'helloworld', 0) - local str9 = love.data.compress('string', 'gzip', 'helloworld', 9) - local str10 = love.data.compress('data', 'lz4', 'helloworld', -1) - local str11 = love.data.compress('data', 'lz4', 'helloworld', 0) - local str12 = love.data.compress('data', 'lz4', 'helloworld', 9) - local str13 = love.data.compress('data', 'zlib', 'helloworld', -1) - local str14 = love.data.compress('data', 'zlib', 'helloworld', 0) - local str15 = love.data.compress('data', 'zlib', 'helloworld', 9) - local str16 = love.data.compress('data', 'gzip', 'helloworld', -1) - local str17 = love.data.compress('data', 'gzip', 'helloworld', 0) - local str18 = love.data.compress('data', 'gzip', 'helloworld', 9) - -- check decompressed value matches whats expected - test:assertEquals('helloworld', love.data.decompress('string', 'lz4', str1), 'check string lz4 decompress') - test:assertEquals('helloworld', love.data.decompress('string', 'lz4', str2), 'check string lz4 decompress') - test:assertEquals('helloworld', love.data.decompress('string', 'lz4', str3), 'check string lz4 decompress') - test:assertEquals('helloworld', love.data.decompress('string', 'zlib', str4), 'check string zlib decompress') - test:assertEquals('helloworld', love.data.decompress('string', 'zlib', str5), 'check string zlib decompress') - test:assertEquals('helloworld', love.data.decompress('string', 'zlib', str6), 'check string zlib decompress') - test:assertEquals('helloworld', love.data.decompress('string', 'gzip', str7), 'check string glib decompress') - test:assertEquals('helloworld', love.data.decompress('string', 'gzip', str8), 'check string glib decompress') - test:assertEquals('helloworld', love.data.decompress('string', 'gzip', str9), 'check string glib decompress') - test:assertEquals(love.data.newByteData('helloworld'):getString(), love.data.decompress('data', 'lz4', str10):getString(), 'check data lz4 decompress') - test:assertEquals(love.data.newByteData('helloworld'):getString(), love.data.decompress('data', 'lz4', str11):getString(), 'check data lz4 decompress') - test:assertEquals(love.data.newByteData('helloworld'):getString(), love.data.decompress('data', 'lz4', str12):getString(), 'check data lz4 decompress') - test:assertEquals(love.data.newByteData('helloworld'):getString(), love.data.decompress('data', 'zlib', str13):getString(), 'check data zlib decompress') - test:assertEquals(love.data.newByteData('helloworld'):getString(), love.data.decompress('data', 'zlib', str14):getString(), 'check data zlib decompress') - test:assertEquals(love.data.newByteData('helloworld'):getString(), love.data.decompress('data', 'zlib', str15):getString(), 'check data zlib decompress') - test:assertEquals(love.data.newByteData('helloworld'):getString(), love.data.decompress('data', 'gzip', str16):getString(), 'check data glib decompress') - test:assertEquals(love.data.newByteData('helloworld'):getString(), love.data.decompress('data', 'gzip', str17):getString(), 'check data glib decompress') - test:assertEquals(love.data.newByteData('helloworld'):getString(), love.data.decompress('data', 'gzip', str18):getString(), 'check data glib decompress') -end - - --- love.data.encode -love.test.data.encode = function(test) - -- here just testing each combo 'works' - in decode's test method - -- we actually check the encode + decode give the right value back - local encodes = { - { love.data.encode('string', 'base64', 'helloworld', 0), 'string'}, - { love.data.encode('string', 'base64', 'helloworld', 2), 'string'}, - { love.data.encode('string', 'hex', 'helloworld'), 'string'}, - { love.data.encode('data', 'base64', 'helloworld', 0), 'userdata'}, - { love.data.encode('data', 'base64', 'helloworld', 2), 'userdata'}, - { love.data.encode('data', 'hex', 'helloworld'), 'userdata'} - } - for e=1,#encodes do - test:assertNotNil(encodes[e][1]) - -- sense check return type and make sure bytedata returns are an object - test:assertEquals(encodes[e][2], type(encodes[e][1]), 'check is usedata') - if encodes[e][2] == 'userdata' then - test:assertNotEquals(nil, encodes[e][1]:type(), 'check has :type()') - end - end - -end - - --- love.data.getPackedSize -love.test.data.getPackedSize = function(test) - local pack1 = love.data.getPackedSize('>xI3b') - local pack2 = love.data.getPackedSize('>I2B') - local pack3 = love.data.getPackedSize('>I4I4I4I4x') - test:assertEquals(5, pack1, 'check pack size 1') - test:assertEquals(3, pack2, 'check pack size 2') - test:assertEquals(17, pack3, 'check pack size 3') -end - - --- love.data.hash -love.test.data.hash = function(test) - -- setup all the different hashing types - local str1 = love.data.hash('string', 'md5', 'helloworld') - local str2 = love.data.hash('string', 'sha1', 'helloworld') - local str3 = love.data.hash('string', 'sha224', 'helloworld') - local str4 = love.data.hash('string', 'sha256', 'helloworld') - local str5 = love.data.hash('string', 'sha384', 'helloworld') - local str6 = love.data.hash('string', 'sha512', 'helloworld') - local data1 = love.data.hash('data', 'md5', 'helloworld') - local data2 = love.data.hash('data', 'sha1', 'helloworld') - local data3 = love.data.hash('data', 'sha224', 'helloworld') - local data4 = love.data.hash('data', 'sha256', 'helloworld') - local data5 = love.data.hash('data', 'sha384', 'helloworld') - local data6 = love.data.hash('data', 'sha512', 'helloworld') - -- check encoded hash value matches what's expected for that algo - -- test container string - test:assertEquals('fc5e038d38a57032085441e7fe7010b0', love.data.encode("string", "hex", str1), 'check string md5 encode') - test:assertEquals('6adfb183a4a2c94a2f92dab5ade762a47889a5a1', love.data.encode("string", "hex", str2), 'check string sha1 encode') - test:assertEquals('b033d770602994efa135c5248af300d81567ad5b59cec4bccbf15bcc', love.data.encode("string", "hex", str3), 'check string sha224 encode') - test:assertEquals('936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af', love.data.encode("string", "hex", str4), 'check string sha256 encode') - test:assertEquals('97982a5b1414b9078103a1c008c4e3526c27b41cdbcf80790560a40f2a9bf2ed4427ab1428789915ed4b3dc07c454bd9', love.data.encode("string", "hex", str5), 'check string sha384 encode') - test:assertEquals('1594244d52f2d8c12b142bb61f47bc2eaf503d6d9ca8480cae9fcf112f66e4967dc5e8fa98285e36db8af1b8ffa8b84cb15e0fbcf836c3deb803c13f37659a60', love.data.encode("string", "hex", str6), 'check string sha512 encode') - -- test container data - test:assertEquals('fc5e038d38a57032085441e7fe7010b0', love.data.encode("string", "hex", data1), 'check data md5 encode') - test:assertEquals('6adfb183a4a2c94a2f92dab5ade762a47889a5a1', love.data.encode("string", "hex", data2), 'check data sha1 encode') - test:assertEquals('b033d770602994efa135c5248af300d81567ad5b59cec4bccbf15bcc', love.data.encode("string", "hex", data3), 'check data sha224 encode') - test:assertEquals('936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af', love.data.encode("string", "hex", data4), 'check data sha256 encode') - test:assertEquals('97982a5b1414b9078103a1c008c4e3526c27b41cdbcf80790560a40f2a9bf2ed4427ab1428789915ed4b3dc07c454bd9', love.data.encode("string", "hex", data5), 'check data sha384 encode') - test:assertEquals('1594244d52f2d8c12b142bb61f47bc2eaf503d6d9ca8480cae9fcf112f66e4967dc5e8fa98285e36db8af1b8ffa8b84cb15e0fbcf836c3deb803c13f37659a60', love.data.encode("string", "hex", data6), 'check data sha512 encode') -end - - --- love.data.newByteData --- @NOTE this is just basic nil checking, objs have their own test method -love.test.data.newByteData = function(test) - test:assertObject(love.data.newByteData('helloworld')) -end - - --- love.data.newDataView --- @NOTE this is just basic nil checking, objs have their own test method -love.test.data.newDataView = function(test) - test:assertObject(love.data.newDataView(love.data.newByteData('helloworld'), 0, 10)) -end - - --- love.data.pack -love.test.data.pack = function(test) - local packed1 = love.data.pack('string', '>I4I4I4I4', 9999, 1000, 1010, 2030) - local packed2 = love.data.pack('data', '>I4I4I4I4', 9999, 1000, 1010, 2030) - local a, b, c, d = love.data.unpack('>I4I4I4I4', packed1) - local e, f, g, h = love.data.unpack('>I4I4I4I4', packed2) - test:assertEquals(9999+9999, a+e, 'check packed 1') - test:assertEquals(1000+1000, b+f, 'check packed 2') - test:assertEquals(1010+1010, c+g, 'check packed 3') - test:assertEquals(2030+2030, d+h, 'check packed 4') -end - - --- love.data.unpack -love.test.data.unpack = function(test) - local packed1 = love.data.pack('string', '>s5s4I3', 'hello', 'love', 100) - local packed2 = love.data.pack('data', '>s5I2', 'world', 20) - local a, b, c = love.data.unpack('>s5s4I3', packed1) - local d, e = love.data.unpack('>s5I2', packed2) - test:assertEquals(a .. ' ' .. d, 'hello world', 'check unpack 1') - test:assertEquals(b, 'love', 'check unpack 2') - test:assertEquals(c - e, 80, 'check unpack 3') -end - - - -================================================ -File: tests/event.lua -================================================ --- love.event - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------METHODS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.event.clear -love.test.event.clear = function(test) - -- push some events first - love.event.push('test', 1, 2, 3) - love.event.push('test', 1, 2, 3) - love.event.push('test', 1, 2, 3) - -- check after calling clear there are no events left - love.event.clear() - local count = 0 - for n, a, b, c, d, e, f in love.event.poll() do - count = count + 1 - end - test:assertEquals(0, count, 'check no events') -end - - --- love.event.poll -love.test.event.poll = function(test) - -- push some events first - love.event.push('test', 1, 2, 3) - love.event.push('test', 1, 2, 3) - love.event.push('test', 1, 2, 3) - -- check poll recieves all events - local count = 0 - for n, a, b, c, d, e, f in love.event.poll() do - count = count + 1 - end - test:assertEquals(3, count, 'check 3 events') -end - - --- love.event.pump --- @NOTE dont think can really test as internally used -love.test.event.pump = function(test) - test:skipTest('used internally') -end - - --- love.event.push -love.test.event.push = function(test) - -- check pushing some different types - love.event.push('add', 1, 2, 3) - love.event.push('ignore', 1, 2, 3) - love.event.push('add', 1, 2, 3) - love.event.push('ignore', 1, 2, 3) - local count = 0 - for n, a, b, c, d, e, f in love.event.poll() do - if n == 'add' then - count = count + a + b + c - end - end - test:assertEquals(12, count, 'check total events') -end - - --- love.event.quit -love.test.event.quit = function(test) - -- setting this overrides the quit hook to prevent actually quitting - love.test.module.fakequit = true - love.event.quit(0) - -- if it failed we'd have quit here - test:assertTrue(true, 'check quit hook called') -end - - --- love.event.wait --- @NOTE not sure best way to test this one -love.test.event.wait = function(test) - test:skipTest('used internally') -end - - - -================================================ -File: tests/filesystem.lua -================================================ --- love.filesystem - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------OBJECTS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- File (love.filesystem.newFile) -love.test.filesystem.File = function(test) - - -- setup a file to play with - local file1 = love.filesystem.openFile('data.txt', 'w') - file1:write('helloworld') - test:assertObject(file1) - file1:close() - - -- test read mode - file1:open('r') - test:assertEquals('r', file1:getMode(), 'check read mode') - local contents, size = file1:read() - test:assertEquals('helloworld', contents) - test:assertEquals(10, size, 'check file read') - test:assertEquals(10, file1:getSize()) - local ok1, err1 = file1:write('hello') - test:assertNotEquals(nil, err1, 'check cant write in read mode') - local iterator = file1:lines() - test:assertNotEquals(nil, iterator, 'check can read lines') - test:assertEquals('data.txt', file1:getFilename(), 'check filename matches') - file1:close() - - -- test write mode - file1:open('w') - test:assertEquals('w', file1:getMode(), 'check write mode') - contents, size = file1:read() - test:assertEquals(nil, contents, 'check cant read file in write mode') - test:assertEquals('string', type(size), 'check err message shown') - local ok2, err2 = file1:write('helloworld') - test:assertTrue(ok2, 'check file write') - test:assertEquals(nil, err2, 'check no err writing') - - -- test open/closing - file1:open('r') - test:assertTrue(file1:isOpen(), 'check file is open') - file1:close() - test:assertFalse(file1:isOpen(), 'check file gets closed') - file1:close() - - -- test buffering and flushing - file1:open('w') - local ok3, err3 = file1:setBuffer('full', 10000) - test:assertTrue(ok3) - test:assertEquals('full', file1:getBuffer()) - file1:write('replacedcontent') - file1:flush() - file1:close() - file1:open('r') - contents, size = file1:read() - test:assertEquals('replacedcontent', contents, 'check buffered content was written') - file1:close() - - -- loop through file data with seek/tell until EOF - file1:open('r') - local counter = 0 - for i=1,100 do - file1:seek(i) - test:assertEquals(i, file1:tell()) - if file1:isEOF() == true then - counter = i - break - end - end - test:assertEquals(counter, 15) - file1:close() - -end - - --- FileData (love.filesystem.newFileData) -love.test.filesystem.FileData = function(test) - - -- create new obj - local fdata = love.filesystem.newFileData('helloworld', 'test.txt') - test:assertObject(fdata) - test:assertEquals('test.txt', fdata:getFilename()) - test:assertEquals('txt', fdata:getExtension()) - - -- check properties match expected - test:assertEquals('helloworld', fdata:getString(), 'check data string') - test:assertEquals(10, fdata:getSize(), 'check data size') - - -- check cloning the bytedata - local clonedfdata = fdata:clone() - test:assertObject(clonedfdata) - test:assertEquals('helloworld', clonedfdata:getString(), 'check cloned data') - test:assertEquals(10, clonedfdata:getSize(), 'check cloned size') - -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------METHODS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.filesystem.append -love.test.filesystem.append = function(test) - -- create a new file to test with - love.filesystem.write('filesystem.append.txt', 'foo') - -- try appending text and check new file contents/size matches - local success, message = love.filesystem.append('filesystem.append.txt', 'bar') - test:assertNotEquals(false, success, 'check success') - test:assertEquals(nil, message, 'check no error msg') - local contents, size = love.filesystem.read('filesystem.append.txt') - test:assertEquals(contents, 'foobar', 'check file contents') - test:assertEquals(size, 6, 'check file size') - -- check appending a specific no. of bytes - love.filesystem.append('filesystem.append.txt', 'foobarfoobarfoo', 6) - contents, size = love.filesystem.read('filesystem.append.txt') - test:assertEquals(contents, 'foobarfoobar', 'check appended contents') - test:assertEquals(size, 12, 'check appended size') - -- cleanup - love.filesystem.remove('filesystem.append.txt') -end - - --- love.filesystem.areSymlinksEnabled --- @NOTE best can do here is just check not nil -love.test.filesystem.areSymlinksEnabled = function(test) - test:assertNotNil(love.filesystem.areSymlinksEnabled()) -end - - --- love.filesystem.createDirectory -love.test.filesystem.createDirectory = function(test) - -- try creating a dir + subdir and check both exist - local success = love.filesystem.createDirectory('foo/bar') - test:assertNotEquals(false, success, 'check success') - test:assertNotEquals(nil, love.filesystem.getInfo('foo', 'directory'), 'check directory created') - test:assertNotEquals(nil, love.filesystem.getInfo('foo/bar', 'directory'), 'check subdirectory created') - -- cleanup - love.filesystem.remove('foo/bar') - love.filesystem.remove('foo') -end - - --- love.filesystem.getAppdataDirectory --- @NOTE i think this is too platform dependent to be tested nicely -love.test.filesystem.getAppdataDirectory = function(test) - test:assertNotNil(love.filesystem.getAppdataDirectory()) -end - - --- love.filesystem.getCRequirePath -love.test.filesystem.getCRequirePath = function(test) - -- check default value from documentation - test:assertEquals('??', love.filesystem.getCRequirePath(), 'check default value') -end - - --- love.filesystem.getDirectoryItems -love.test.filesystem.getDirectoryItems = function(test) - -- create a dir + subdir with 2 files - love.filesystem.createDirectory('foo/bar') - love.filesystem.write('foo/file1.txt', 'file1') - love.filesystem.write('foo/bar/file2.txt', 'file2') - -- check both the file + subdir exist in the item list - local files = love.filesystem.getDirectoryItems('foo') - local hasfile = false - local hasdir = false - for _,v in ipairs(files) do - local info = love.filesystem.getInfo('foo/'..v) - if v == 'bar' and info.type == 'directory' then hasdir = true end - if v == 'file1.txt' and info.type == 'file' then hasfile = true end - end - test:assertTrue(hasfile, 'check file exists') - test:assertTrue(hasdir, 'check directory exists') - -- cleanup - love.filesystem.remove('foo/file1.txt') - love.filesystem.remove('foo/bar/file2.txt') - love.filesystem.remove('foo/bar') - love.filesystem.remove('foo') -end - - --- love.filesystem.getFullCommonPath -love.test.filesystem.getFullCommonPath = function(test) - -- check standard paths - local appsavedir = love.filesystem.getFullCommonPath('appsavedir') - local appdocuments = love.filesystem.getFullCommonPath('appdocuments') - local userhome = love.filesystem.getFullCommonPath('userhome') - local userappdata = love.filesystem.getFullCommonPath('userappdata') - local userdesktop = love.filesystem.getFullCommonPath('userdesktop') - local userdocuments = love.filesystem.getFullCommonPath('userdocuments') - test:assertNotNil(appsavedir) - test:assertNotNil(appdocuments) - test:assertNotNil(userhome) - test:assertNotNil(userappdata) - test:assertNotNil(userdesktop) - test:assertNotNil(userdocuments) - -- check invalid path - local ok = pcall(love.filesystem.getFullCommonPath, 'fakepath') - test:assertFalse(ok, 'check invalid common path') -end - - --- love.filesystem.getIdentity -love.test.filesystem.getIdentity = function(test) - -- check setting identity matches - local original = love.filesystem.getIdentity() - love.filesystem.setIdentity('lover') - test:assertEquals('lover', love.filesystem.getIdentity(), 'check identity matches') - -- put back to original value - love.filesystem.setIdentity(original) -end - - --- love.filesystem.getRealDirectory -love.test.filesystem.getRealDirectory = function(test) - -- make a test dir + file first - love.filesystem.createDirectory('foo') - love.filesystem.write('foo/test.txt', 'test') - -- check save dir matches the real dir we just wrote to - test:assertEquals(love.filesystem.getSaveDirectory(), - love.filesystem.getRealDirectory('foo/test.txt'), 'check directory matches') - -- cleanup - love.filesystem.remove('foo/test.txt') - love.filesystem.remove('foo') -end - - --- love.filesystem.getRequirePath -love.test.filesystem.getRequirePath = function(test) - test:assertEquals('?.lua;?/init.lua', - love.filesystem.getRequirePath(), 'check default value') -end - - --- love.filesystem.getSource --- @NOTE i dont think we can test this cos love calls it first -love.test.filesystem.getSource = function(test) - test:skipTest('used internally') -end - - --- love.filesystem.getSourceBaseDirectory --- @NOTE i think this is too platform dependent to be tested nicely -love.test.filesystem.getSourceBaseDirectory = function(test) - test:assertNotNil(love.filesystem.getSourceBaseDirectory()) -end - - --- love.filesystem.getUserDirectory --- @NOTE i think this is too platform dependent to be tested nicely -love.test.filesystem.getUserDirectory = function(test) - test:assertNotNil(love.filesystem.getUserDirectory()) -end - - --- love.filesystem.getWorkingDirectory --- @NOTE i think this is too platform dependent to be tested nicely -love.test.filesystem.getWorkingDirectory = function(test) - test:assertNotNil(love.filesystem.getWorkingDirectory()) -end - - --- love.filesystem.getSaveDirectory --- @NOTE i think this is too platform dependent to be tested nicely -love.test.filesystem.getSaveDirectory = function(test) - test:assertNotNil(love.filesystem.getSaveDirectory()) -end - - --- love.filesystem.getInfo -love.test.filesystem.getInfo = function(test) - -- create a dir and subdir with a file - love.filesystem.createDirectory('foo/bar') - love.filesystem.write('foo/bar/file2.txt', 'file2') - -- check getinfo returns the correct values - test:assertEquals(nil, love.filesystem.getInfo('foo/bar/file2.txt', 'directory'), 'check not directory') - test:assertNotEquals(nil, love.filesystem.getInfo('foo/bar/file2.txt'), 'check info not nil') - test:assertEquals(love.filesystem.getInfo('foo/bar/file2.txt').size, 5, 'check info size match') - test:assertFalse(love.filesystem.getInfo('foo/bar/file2.txt').readonly, 'check readonly') - -- @TODO test modified timestamp from info.modtime? - -- cleanup - love.filesystem.remove('foo/bar/file2.txt') - love.filesystem.remove('foo/bar') - love.filesystem.remove('foo') -end - - --- love.filesystem.isFused -love.test.filesystem.isFused = function(test) - -- kinda assuming you'd run the testsuite in a non-fused game - test:assertEquals(love.filesystem.isFused(), false, 'check not fused') -end - - --- love.filesystem.lines -love.test.filesystem.lines = function(test) - -- check lines returns the 3 lines expected - love.filesystem.write('file.txt', 'line1\nline2\nline3') - local linenum = 1 - for line in love.filesystem.lines('file.txt') do - test:assertEquals('line' .. tostring(linenum), line, 'check line matches') - -- also check it removes newlines like the docs says it does - test:assertEquals(nil, string.find(line, '\n'), 'check newline removed') - linenum = linenum + 1 - end - -- cleanup - love.filesystem.remove('file.txt') -end - - --- love.filesystem.load -love.test.filesystem.load = function(test) - -- setup some fake lua files - love.filesystem.write('test1.lua', 'function test()\nreturn 1\nend\nreturn test()') - love.filesystem.write('test2.lua', 'function test()\nreturn 1') - - if test:isAtLeastLuaVersion(5.2) or test:isLuaJITEnabled() then - -- check file that doesn't exist - local chunk1, errormsg1 = love.filesystem.load('faker.lua', 'b') - test:assertEquals(nil, chunk1, 'check file doesnt exist') - -- check valid lua file (text load) - local chunk2, errormsg2 = love.filesystem.load('test1.lua', 't') - test:assertEquals(nil, errormsg2, 'check no error message') - test:assertEquals(1, chunk2(), 'check lua file runs') - else - local _, errormsg3 = love.filesystem.load('test1.lua', 'b') - test:assertNotEquals(nil, errormsg3, 'check for an error message') - - local _, errormsg4 = love.filesystem.load('test1.lua', 't') - test:assertNotEquals(nil, errormsg4, 'check for an error message') - end - - -- check valid lua file (any load) - local chunk5, errormsg5 = love.filesystem.load('test1.lua', 'bt') - test:assertEquals(nil, errormsg5, 'check no error message') - test:assertEquals(1, chunk5(), 'check lua file runs') - - -- check invalid lua file - local ok, chunk, err = pcall(love.filesystem.load, 'test2.lua') - test:assertFalse(ok, 'check invalid lua file') - -- cleanup - love.filesystem.remove('test1.lua') - love.filesystem.remove('test2.lua') -end - - --- love.filesystem.mount -love.test.filesystem.mount = function(test) - -- write an example zip to savedir to use - local contents, size = love.filesystem.read('resources/test.zip') -- contains test.txt - love.filesystem.write('test.zip', contents, size) - -- check mounting file and check contents are mounted - local success = love.filesystem.mount('test.zip', 'test') - test:assertTrue(success, 'check success') - test:assertNotEquals(nil, love.filesystem.getInfo('test'), 'check mount not nil') - test:assertEquals('directory', love.filesystem.getInfo('test').type, 'check directory made') - test:assertNotEquals(nil, love.filesystem.getInfo('test/test.txt'), 'check file not nil') - test:assertEquals('file', love.filesystem.getInfo('test/test.txt').type, 'check file type') - -- cleanup - love.filesystem.remove('test/test.txt') - love.filesystem.remove('test') - love.filesystem.remove('test.zip') -end - - --- love.filesystem.mountFullPath -love.test.filesystem.mountFullPath = function(test) - -- mount something in the working directory - local mount = love.filesystem.mountFullPath(love.filesystem.getSource() .. '/tests', 'tests', 'read') - test:assertTrue(mount, 'check can mount') - -- check reading file through mounted path label - local contents, _ = love.filesystem.read('tests/audio.lua') - test:assertNotEquals(nil, contents) - local unmount = love.filesystem.unmountFullPath(love.filesystem.getSource() .. '/tests') - test:assertTrue(unmount, 'reset mount') -end - - --- love.filesystem.unmountFullPath -love.test.filesystem.unmountFullPath = function(test) - -- try unmounting something we never mounted - local unmount1 = love.filesystem.unmountFullPath(love.filesystem.getSource() .. '/faker') - test:assertFalse(unmount1, 'check not mounted to start with') - -- mount something to unmount after - love.filesystem.mountFullPath(love.filesystem.getSource() .. '/tests', 'tests', 'read') - local unmount2 = love.filesystem.unmountFullPath(love.filesystem.getSource() .. '/tests') - test:assertTrue(unmount2, 'check unmounted') -end - - --- love.filesystem.mountCommonPath -love.test.filesystem.mountCommonPath = function(test) - -- check if we can mount all the expected paths - local mount1 = love.filesystem.mountCommonPath('appsavedir', 'appsavedir', 'readwrite') - local mount2 = love.filesystem.mountCommonPath('appdocuments', 'appdocuments', 'readwrite') - local mount3 = love.filesystem.mountCommonPath('userhome', 'userhome', 'readwrite') - local mount4 = love.filesystem.mountCommonPath('userappdata', 'userappdata', 'readwrite') - -- userdesktop isnt valid on linux - if not test:isOS('Linux') then - local mount5 = love.filesystem.mountCommonPath('userdesktop', 'userdesktop', 'readwrite') - test:assertTrue(mount5, 'check mount userdesktop') - end - local mount6 = love.filesystem.mountCommonPath('userdocuments', 'userdocuments', 'readwrite') - local ok = pcall(love.filesystem.mountCommonPath, 'fakepath', 'fake', 'readwrite') - test:assertFalse(mount1, 'check mount appsavedir') -- This is already mounted, we can't do it again. - test:assertTrue(mount2, 'check mount appdocuments') - test:assertTrue(mount3, 'check mount userhome') - test:assertTrue(mount4, 'check mount userappdata') - test:assertTrue(mount6, 'check mount userdocuments') - test:assertFalse(ok, 'check mount invalid common path fails') -end - - --- love.filesystem.unmountCommonPath ---love.test.filesystem.unmountCommonPath = function(test) --- -- check unmounting invalid --- local ok = pcall(love.filesystem.unmountCommonPath, 'fakepath') --- test:assertFalse(ok, 'check unmount invalid common path') --- -- check mounting valid paths --- love.filesystem.mountCommonPath('appsavedir', 'appsavedir', 'read') --- love.filesystem.mountCommonPath('appdocuments', 'appdocuments', 'read') --- love.filesystem.mountCommonPath('userhome', 'userhome', 'read') --- love.filesystem.mountCommonPath('userappdata', 'userappdata', 'read') --- love.filesystem.mountCommonPath('userdesktop', 'userdesktop', 'read') --- love.filesystem.mountCommonPath('userdocuments', 'userdocuments', 'read') --- local unmount1 = love.filesystem.unmountCommonPath('appsavedir') --- local unmount2 = love.filesystem.unmountCommonPath('appdocuments') --- local unmount3 = love.filesystem.unmountCommonPath('userhome') --- local unmount4 = love.filesystem.unmountCommonPath('userappdata') --- local unmount5 = love.filesystem.unmountCommonPath('userdesktop') --- local unmount6 = love.filesystem.unmountCommonPath('userdocuments') --- test:assertTrue(unmount1, 'check unmount appsavedir') --- test:assertTrue(unmount2, 'check unmount appdocuments') --- test:assertTrue(unmount3, 'check unmount userhome') --- test:assertTrue(unmount4, 'check unmount userappdata') --- test:assertTrue(unmount5, 'check unmount userdesktop') --- test:assertTrue(unmount6, 'check unmount userdocuments') --- -- remount or future tests fail --- love.filesystem.mountCommonPath('appsavedir', 'appsavedir', 'readwrite') --- love.filesystem.mountCommonPath('appdocuments', 'appdocuments', 'readwrite') --- love.filesystem.mountCommonPath('userhome', 'userhome', 'readwrite') --- love.filesystem.mountCommonPath('userappdata', 'userappdata', 'readwrite') --- love.filesystem.mountCommonPath('userdesktop', 'userdesktop', 'readwrite') --- love.filesystem.mountCommonPath('userdocuments', 'userdocuments', 'readwrite') ---end - - --- love.filesystem.openFile --- @NOTE this is just basic nil checking, objs have their own test method -love.test.filesystem.openFile = function(test) - test:assertNotNil(love.filesystem.openFile('file2.txt', 'w')) - test:assertNotNil(love.filesystem.openFile('file2.txt', 'r')) - test:assertNotNil(love.filesystem.openFile('file2.txt', 'a')) - test:assertNotNil(love.filesystem.openFile('file2.txt', 'c')) - love.filesystem.remove('file2.txt') -end - - --- love.filesystem.newFileData --- @NOTE this is just basic nil checking, objs have their own test method -love.test.filesystem.newFileData = function(test) - test:assertNotNil(love.filesystem.newFileData('helloworld', 'file1')) -end - - --- love.filesystem.read -love.test.filesystem.read = function(test) - -- check reading a full file - local content, size = love.filesystem.read('resources/test.txt') - test:assertNotEquals(nil, content, 'check not nil') - test:assertEquals('helloworld', content, 'check content match') - test:assertEquals(10, size, 'check size match') - -- check reading partial file - content, size = love.filesystem.read('resources/test.txt', 5) - test:assertNotEquals(nil, content, 'check not nil') - test:assertEquals('hello', content, 'check content match') - test:assertEquals(5, size, 'check size match') -end - - --- love.filesystem.remove -love.test.filesystem.remove = function(test) - -- create a dir + subdir with a file - love.filesystem.createDirectory('foo/bar') - love.filesystem.write('foo/bar/file2.txt', 'helloworld') - -- check removing files + dirs (should fail to remove dir if file inside) - test:assertFalse(love.filesystem.remove('foo'), 'check fail when file inside') - test:assertFalse(love.filesystem.remove('foo/bar'), 'check fail when file inside') - test:assertTrue(love.filesystem.remove('foo/bar/file2.txt'), 'check file removed') - test:assertTrue(love.filesystem.remove('foo/bar'), 'check subdirectory removed') - test:assertTrue(love.filesystem.remove('foo'), 'check directory removed') - -- cleanup not needed here hopefully... -end - - --- love.filesystem.setCRequirePath -love.test.filesystem.setCRequirePath = function(test) - -- check setting path val is returned - love.filesystem.setCRequirePath('/??') - test:assertEquals('/??', love.filesystem.getCRequirePath(), 'check crequirepath value') - love.filesystem.setCRequirePath('??') -end - - --- love.filesystem.setIdentity -love.test.filesystem.setIdentity = function(test) - -- check setting identity val is returned - local original = love.filesystem.getIdentity() - love.filesystem.setIdentity('lover') - test:assertEquals('lover', love.filesystem.getIdentity(), 'check indentity value') - -- return value to original - love.filesystem.setIdentity(original) -end - - --- love.filesystem.setRequirePath -love.test.filesystem.setRequirePath = function(test) - -- check setting path val is returned - love.filesystem.setRequirePath('?.lua;?/start.lua') - test:assertEquals('?.lua;?/start.lua', love.filesystem.getRequirePath(), 'check require path') - -- reset to default - love.filesystem.setRequirePath('?.lua;?/init.lua') -end - - --- love.filesystem.setSource -love.test.filesystem.setSource = function(test) - test:skipTest('used internally') -end - - --- love.filesystem.unmount -love.test.filesystem.unmount = function(test) - -- create a zip file mounted to use - local contents, size = love.filesystem.read('resources/test.zip') -- contains test.txt - love.filesystem.write('test.zip', contents, size) - love.filesystem.mount('test.zip', 'test') - -- check mounted, unmount, then check its unmounted - test:assertNotEquals(nil, love.filesystem.getInfo('test/test.txt'), 'check mount exists') - love.filesystem.unmount('test.zip') - test:assertEquals(nil, love.filesystem.getInfo('test/test.txt'), 'check unmounted') - -- cleanup - love.filesystem.remove('test/test.txt') - love.filesystem.remove('test') - love.filesystem.remove('test.zip') -end - - --- love.filesystem.write -love.test.filesystem.write = function(test) - -- check writing a bunch of files matches whats read back - love.filesystem.write('test1.txt', 'helloworld') - love.filesystem.write('test2.txt', 'helloworld', 10) - love.filesystem.write('test3.txt', 'helloworld', 5) - test:assertEquals('helloworld', love.filesystem.read('test1.txt'), 'check read file') - test:assertEquals('helloworld', love.filesystem.read('test2.txt'), 'check read all') - test:assertEquals('hello', love.filesystem.read('test3.txt'), 'check read partial') - -- cleanup - love.filesystem.remove('test1.txt') - love.filesystem.remove('test2.txt') - love.filesystem.remove('test3.txt') -end - - - -================================================ -File: tests/font.lua -================================================ --- love.font - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------OBJECTS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- GlyphData (love.font.newGlyphData) -love.test.font.GlyphData = function(test) - - -- create obj - local rasterizer = love.font.newRasterizer('resources/font.ttf') - local gdata = love.font.newGlyphData(rasterizer, 97) -- 'a' - test:assertObject(gdata) - - -- check properties match expected - test:assertNotNil(gdata:getString()) - test:assertEquals(128, gdata:getSize(), 'check data size') - test:assertEquals(9, gdata:getAdvance(), 'check advance') - test:assertEquals('la8', gdata:getFormat(), 'check format') - - -- @TODO - --[[ - currently these will return 0 and '' respectively as not implemented - https://github.com/love2d/love/blob/12.0-development/src/modules/font/freetype/TrueTypeRasterizer.cpp#L140-L141 - "basically I haven't decided what to do here yet, because of the more - advanced text shaping that happens in love 12 having a unicode codepoint - associated with a glyph probably doesn't make sense in the first place" - ]]-- - --test:assertEquals(97, gdata:getGlyph(), 'check glyph number') - returns 0 - --test:assertEquals('a', gdata:getGlyphString(), 'check glyph string') - returns '' - - -- check height + width - test:assertEquals(8, gdata:getHeight(), 'check height') - test:assertEquals(8, gdata:getWidth(), 'check width') - - -- check boundary / dimensions - local x, y, w, h = gdata:getBoundingBox() - local dw, dh = gdata:getDimensions() - test:assertEquals(0, x, 'check bbox x') - test:assertEquals(-3, y, 'check bbox y') - test:assertEquals(8, w, 'check bbox w') - test:assertEquals(14, h, 'check bbox h') - test:assertEquals(8, dw, 'check dim width') - test:assertEquals(8, dh, 'check dim height') - - -- check bearing - local bw, bh = gdata:getBearing() - test:assertEquals(0, bw, 'check bearing w') - test:assertEquals(11, bh, 'check bearing h') - -end - - --- Rasterizer (love.font.newRasterizer) -love.test.font.Rasterizer = function(test) - - -- create obj - local rasterizer = love.font.newRasterizer('resources/font.ttf') - test:assertObject(rasterizer) - - -- check advance - test:assertEquals(9, rasterizer:getAdvance(), 'check advance') - - -- check ascent/descent - test:assertEquals(9, rasterizer:getAscent(), 'check ascent') - test:assertEquals(-3, rasterizer:getDescent(), 'check descent') - - -- check glyphcount - test:assertEquals(77, rasterizer:getGlyphCount(), 'check glyph count') - - -- check specific glyphs - test:assertObject(rasterizer:getGlyphData('L')) - test:assertTrue(rasterizer:hasGlyphs('L', 'O', 'V', 'E'), 'check LOVE') - - -- check height + lineheight - test:assertEquals(12, rasterizer:getHeight(), 'check height') - test:assertEquals(15, rasterizer:getLineHeight(), 'check line height') - -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------METHODS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.font.newBMFontRasterizer -love.test.font.newBMFontRasterizer = function(test) - local rasterizer = love.font.newBMFontRasterizer('resources/love.png'); - test:assertObject(rasterizer) -end - - --- love.font.newGlyphData --- @NOTE this is just basic nil checking, objs have their own test method -love.test.font.newGlyphData = function(test) - local img = love.image.newImageData('resources/love.png') - local rasterizer = love.font.newImageRasterizer(img, 'ABC', 0, 1); - local glyphdata = love.font.newGlyphData(rasterizer, 65) - test:assertObject(glyphdata) -end - - --- love.font.newImageRasterizer --- @NOTE this is just basic nil checking, objs have their own test method -love.test.font.newImageRasterizer = function(test) - local img = love.image.newImageData('resources/love.png') - local rasterizer = love.font.newImageRasterizer(img, 'ABC', 0, 1); - test:assertObject(rasterizer) -end - - --- love.font.newRasterizer --- @NOTE this is just basic nil checking, objs have their own test method -love.test.font.newRasterizer = function(test) - test:assertObject(love.font.newRasterizer('resources/font.ttf')) -end - - --- love.font.newTrueTypeRasterizer --- @NOTE this is just basic nil checking, objs have their own test method -love.test.font.newTrueTypeRasterizer = function(test) - test:assertObject(love.font.newTrueTypeRasterizer(12, "normal", 1)) - test:assertObject(love.font.newTrueTypeRasterizer('resources/font.ttf', 8, "normal", 1)) -end - - - -================================================ -File: tests/graphics.lua -================================================ --- love.graphics - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------OBJECTS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- GraphicsBuffer (love.graphics.newBuffer) -love.test.graphics.Buffer = function(test) - - -- setup vertex data and create some buffers - local vertexformat = { - {name="VertexPosition", format="floatvec2", location=0}, - {name="VertexTexCoord", format="floatvec2", location=1}, - {name="VertexColor", format="unorm8vec4", location=2}, - } - local vertexdata = { - {0, 0, 0, 0, 1, 0, 1, 1}, - {10, 0, 1, 0, 0, 1, 1, 1}, - {10, 10, 1, 1, 0, 0, 1, 1}, - {0, 10, 0, 1, 1, 0, 0, 1}, - } - local flatvertexdata = {} - for i, vert in ipairs(vertexdata) do - for j, v in ipairs(vert) do - table.insert(flatvertexdata, v) - end - end - local vertexbuffer1 = love.graphics.newBuffer(vertexformat, 4, {vertex=true, debugname='testvertexbuffer'}) - local vertexbuffer2 = love.graphics.newBuffer(vertexformat, vertexdata, {vertex=true}) - test:assertObject(vertexbuffer1) - test:assertObject(vertexbuffer2) - - -- check buffer properties - test:assertEquals(4, vertexbuffer1:getElementCount(), 'check vertex count 1') - test:assertEquals(4, vertexbuffer2:getElementCount(), 'check vertex count 2') - -- vertex buffers have their elements tightly packed. - test:assertEquals(20, vertexbuffer1:getElementStride(), 'check vertex array stride') - test:assertEquals(20 * 4, vertexbuffer1:getSize(), 'check vertex buffer size') - vertexbuffer1:setArrayData(vertexdata) - vertexbuffer1:setArrayData(flatvertexdata) - vertexbuffer1:clear(8, 8) -- partial clear (the first texcoord) - - -- check buffer types - test:assertTrue(vertexbuffer1:isBufferType('vertex'), 'check is vertex buffer') - test:assertFalse(vertexbuffer1:isBufferType('index'), 'check is not index buffer') - test:assertFalse(vertexbuffer1:isBufferType('texel'), 'check is not texel buffer') - test:assertFalse(vertexbuffer1:isBufferType('shaderstorage'), 'check is not shader storage buffer') - - -- check debug name - test:assertEquals('testvertexbuffer', vertexbuffer1:getDebugName(), 'check buffer debug name') - - -- check buffer format and format properties - local format = vertexbuffer1:getFormat() - test:assertEquals('table', type(format), 'check buffer format is table') - test:assertEquals(#vertexformat, #format, 'check buffer format length') - for i, v in ipairs(vertexformat) do - test:assertEquals(v.name, format[i].name, string.format('check buffer format %d name', i)) - test:assertEquals(v.format, format[i].format, string.format('check buffer format %d format', i)) - test:assertEquals(0, format[i].arraylength, string.format('check buffer format %d array length', i)) - test:assertNotNil(format[i].offset) - end - - -- check index buffer - local indexbuffer = love.graphics.newBuffer('uint16', 128, {index=true}) - test:assertTrue(indexbuffer:isBufferType('index'), 'check is index buffer') - -end - - --- Shader Storage GraphicsBuffer (love.graphics.newBuffer) --- Separated from the above test so we can skip it when they aren't supported. -love.test.graphics.ShaderStorageBuffer = function(test) - if not love.graphics.getSupported().glsl4 then - test:skipTest('GLSL 4 and shader storage buffers are not supported on this system') - return - end - - -- setup buffer - local format = { - { name="1", format="float" }, - { name="2", format="floatmat4x4" }, - { name="3", format="floatvec2" } - } - local buffer = love.graphics.newBuffer(format, 1, {shaderstorage = true}) - test:assertEquals(96, buffer:getElementStride(), 'check shader storage buffer element stride') - - -- set new data - local data = {} - for i = 1, 19 do - data[i] = 0 - end - buffer:setArrayData(data) - -end - - --- Canvas (love.graphics.newCanvas) -love.test.graphics.Canvas = function(test) - - -- create canvas with defaults - local canvas = love.graphics.newCanvas(100, 100, { - type = '2d', - format = 'normal', - readable = true, - msaa = 0, - dpiscale = love.graphics.getDPIScale(), - mipmaps = 'auto', - debugname = 'testcanvas' - }) - test:assertObject(canvas) - test:assertTrue(canvas:isCanvas(), 'check is canvas') - test:assertFalse(canvas:isComputeWritable(), 'check not compute writable') - - -- check dpi - test:assertEquals(love.graphics.getDPIScale(), canvas:getDPIScale(), 'check dpi scale') - - -- check depth - test:assertEquals(1, canvas:getDepth(), 'check depth is 2d') - test:assertEquals(nil, canvas:getDepthSampleMode(), 'check depth sample nil') - - local maxanisotropy = love.graphics.getSystemLimits().anisotropy - - -- check fliter - local min1, mag1, ani1 = canvas:getFilter() - test:assertEquals('nearest', min1, 'check filter def min') - test:assertEquals('nearest', mag1, 'check filter def mag') - test:assertEquals(1, ani1, 'check filter def ani') - canvas:setFilter('linear', 'linear', 2) - local min2, mag2, ani2 = canvas:getFilter() - test:assertEquals('linear', min2, 'check filter changed min') - test:assertEquals('linear', mag2, 'check filter changed mag') - test:assertEquals(math.min(maxanisotropy, 2), ani2, 'check filter changed ani') - - -- check layer - test:assertEquals(1, canvas:getLayerCount(), 'check 1 layer for 2d') - - -- check texture type - test:assertEquals('2d', canvas:getTextureType(), 'check 2d') - - -- check texture wrap - local horiz1, vert1 = canvas:getWrap() - test:assertEquals('clamp', horiz1, 'check def wrap h') - test:assertEquals('clamp', vert1, 'check def wrap v') - canvas:setWrap('repeat', 'repeat') - local horiz2, vert2 = canvas:getWrap() - test:assertEquals('repeat', horiz2, 'check changed wrap h') - test:assertEquals('repeat', vert2, 'check changed wrap v') - - -- check readable - test:assertTrue(canvas:isReadable(), 'check canvas readable') - - -- check msaa - test:assertEquals(1, canvas:getMSAA(), 'check samples match') - - -- check dimensions - local cw, ch = canvas:getDimensions() - test:assertEquals(100, cw, 'check canvas dim w') - test:assertEquals(100, ch, 'check canvas dim h') - test:assertEquals(cw, canvas:getWidth(), 'check canvas w matches dim') - test:assertEquals(ch, canvas:getHeight(), 'check canvas h matches dim') - local pw, ph = canvas:getPixelDimensions() - test:assertEquals(100*love.graphics.getDPIScale(), pw, 'check pixel dim w') - test:assertEquals(100*love.graphics.getDPIScale(), ph, 'check pixel dim h') - test:assertEquals(pw, canvas:getPixelWidth(), 'check pixel w matches dim') - test:assertEquals(ph, canvas:getPixelHeight(), 'check pixel h matches dim') - - -- check mipmaps - local mode, sharpness = canvas:getMipmapFilter() - test:assertEquals('linear', mode, 'check def minmap filter mode') - test:assertEquals(0, sharpness, 'check def minmap filter sharpness') - local name, version, vendor, device = love.graphics.getRendererInfo() - canvas:setMipmapFilter('nearest', 1) - mode, sharpness = canvas:getMipmapFilter() - test:assertEquals('nearest', mode, 'check changed minmap filter mode') - -- @NOTE mipmap sharpness wont work on opengl/metal - if string.match(name, 'OpenGL ES') == nil and string.match(name, 'Metal') == nil then - test:assertEquals(1, sharpness, 'check changed minmap filter sharpness') - end - test:assertGreaterEqual(2, canvas:getMipmapCount()) -- docs say no mipmaps should return 1 - test:assertEquals('auto', canvas:getMipmapMode()) - - -- check debug name - test:assertEquals('testcanvas', canvas:getDebugName()) - - -- check basic rendering - canvas:renderTo(function() - love.graphics.setColor(1, 0, 0) - love.graphics.rectangle('fill', 0, 0, 200, 200) - love.graphics.setColor(1, 1, 1, 1) - end) - local imgdata1 = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata1) - - -- check using canvas in love.graphics.draw() - local xcanvas = love.graphics.newCanvas() - love.graphics.setCanvas(xcanvas) - love.graphics.draw(canvas, 0, 0) - love.graphics.setCanvas() - local imgdata2 = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata2) - - -- check y-down - local shader1 = love.graphics.newShader[[ - vec4 effect(vec4 c, Image tex, vec2 tc, vec2 pc) { - return tc.y > 0.5 ? vec4(1.0, 0.0, 0.0, 1.0) : vec4(0.0, 1.0, 0.0, 1.0); - } - ]] - local shader2 = love.graphics.newShader[[ - vec4 effect(vec4 c, Image tex, vec2 tc, vec2 pc) { - // rounding during quantization from float to unorm8 doesn't seem to be - // totally consistent across devices, lets do it ourselves. - highp vec2 value = pc / love_ScreenSize.xy; - highp vec2 quantized = (floor(255.0 * value + 0.5) + 0.1) / 255.0; - return vec4(quantized, 0.0, 1.0); - } - ]] - local img = love.graphics.newImage(love.image.newImageData(1, 1)) - - love.graphics.push("all") - love.graphics.setCanvas(canvas) - love.graphics.setShader(shader1) - love.graphics.draw(img, 0, 0, 0, canvas:getDimensions()) - love.graphics.pop() - local imgdata3 = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata3) - - love.graphics.push("all") - love.graphics.setCanvas(canvas) - love.graphics.setShader(shader2) - love.graphics.draw(img, 0, 0, 0, canvas:getDimensions()) - love.graphics.pop() - local imgdata4 = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata4) - - - -- check depth samples - local dcanvas = love.graphics.newCanvas(100, 100, { - type = '2d', - format = 'depth16', - readable = true - }) - test:assertEquals(nil, dcanvas:getDepthSampleMode(), 'check depth sample mode nil by def') - dcanvas:setDepthSampleMode('equal') - test:assertEquals('equal', dcanvas:getDepthSampleMode(), 'check depth sample mode set') - - -- check compute writeable (wont work on opengl mac) - if love.graphics.getSupported().glsl4 then - local ccanvas = love.graphics.newCanvas(100, 100, { - type = '2d', - format = 'rgba8', - computewrite = true - }) - test:assertTrue(ccanvas:isComputeWritable()) - end - -end - - --- Font (love.graphics.newFont) -love.test.graphics.Font = function(test) - - -- create obj - local font = love.graphics.newFont('resources/font.ttf', 8) - test:assertObject(font) - - -- check ascent/descent - test:assertEquals(6, font:getAscent(), 'check ascent') - test:assertEquals(-2, font:getDescent(), 'check descent') - - -- check baseline - test:assertEquals(6, font:getBaseline(), 'check baseline') - - -- check dpi - test:assertEquals(1, font:getDPIScale(), 'check dpi') - - -- check filter - test:assertEquals('nearest', font:getFilter(), 'check filter def') - font:setFilter('linear', 'linear') - test:assertEquals('linear', font:getFilter(), 'check filter change') - font:setFilter('nearest', 'nearest') - - -- check height + lineheight - test:assertEquals(8, font:getHeight(), 'check height') - test:assertEquals(1, font:getLineHeight(), 'check line height') - font:setLineHeight(2) - test:assertEquals(2, font:getLineHeight(), 'check changed line height') - font:setLineHeight(1) -- reset for drawing + wrap later - - -- check width + kerning - test:assertEquals(0, font:getKerning('a', 'b'), 'check kerning') - test:assertEquals(24, font:getWidth('test'), 'check data size') - - -- check specific glyphs - test:assertTrue(font:hasGlyphs('test'), 'check data size') - - -- check font wrapping - local width, wrappedtext = font:getWrap('LÖVE is an *awesome* framework you can use to make 2D games in Lua.', 50) - test:assertEquals(48, width, 'check actual wrap width') - test:assertEquals(8, #wrappedtext, 'check wrapped lines') - test:assertEquals('LÖVE is an ', wrappedtext[1], 'check wrapped line') - - -- check drawing font - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.setFont(font) - love.graphics.print('Aa', 0, 5) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) - - -- check font substitution - local fontab = love.graphics.newImageFont('resources/font-letters-ab.png', 'AB') - local fontcd = love.graphics.newImageFont('resources/font-letters-cd.png', 'CD') - fontab:setFallbacks(fontcd) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 0) - love.graphics.setFont(fontab) - love.graphics.print('AB', 0, 0) -- should come from fontab - love.graphics.print('CD', 0, 9) -- should come from fontcd - love.graphics.setCanvas() - local imgdata2 = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata2) - -end - - --- Image (love.graphics.newImage) -love.test.graphics.Image = function(test) - - -- create object - local image = love.graphics.newImage('resources/love.png', { - dpiscale = 1, - mipmaps = true - }) - test:assertObject(image) - test:assertFalse(image:isCanvas(), 'check not canvas') - test:assertFalse(image:isComputeWritable(), 'check not compute writable') - - -- check dpi - test:assertEquals(love.graphics.getDPIScale(), image:getDPIScale(), 'check dpi scale') - - -- check depth - test:assertEquals(1, image:getDepth(), 'check depth is 2d') - test:assertEquals(nil, image:getDepthSampleMode(), 'check depth sample nil') - - local maxanisotropy = love.graphics.getSystemLimits().anisotropy - - -- check filter - local min1, mag1, ani1 = image:getFilter() - test:assertEquals('nearest', min1, 'check filter def min') - test:assertEquals('nearest', mag1, 'check filter def mag') - test:assertEquals(1, ani1, 'check filter def ani') - image:setFilter('linear', 'linear', 2) - local min2, mag2, ani2 = image:getFilter() - test:assertEquals('linear', min2, 'check filter changed min') - test:assertEquals('linear', mag2, 'check filter changed mag') - test:assertEquals(math.min(maxanisotropy, 2), ani2, 'check filter changed ani') - image:setFilter('nearest', 'nearest', 1) - - -- check layers - test:assertEquals(1, image:getLayerCount(), 'check 1 layer for 2d') - - -- check texture type - test:assertEquals('2d', image:getTextureType(), 'check 2d') - - -- check texture wrapping - local horiz1, vert1 = image:getWrap() - test:assertEquals('clamp', horiz1, 'check def wrap h') - test:assertEquals('clamp', vert1, 'check def wrap v') - image:setWrap('repeat', 'repeat') - local horiz2, vert2 = image:getWrap() - test:assertEquals('repeat', horiz2, 'check changed wrap h') - test:assertEquals('repeat', vert2, 'check changed wrap v') - - -- check readable - test:assertTrue(image:isReadable(), 'check canvas readable') - - -- check msaa - test:assertEquals(1, image:getMSAA(), 'check samples match') - - -- check dimensions - local cw, ch = image:getDimensions() - test:assertEquals(64, cw, 'check canvas dim w') - test:assertEquals(64, ch, 'check canvas dim h') - test:assertEquals(cw, image:getWidth(), 'check canvas w matches dim') - test:assertEquals(ch, image:getHeight(), 'check canvas h matches dim') - local pw, ph = image:getPixelDimensions() - test:assertEquals(64*love.graphics.getDPIScale(), pw, 'check pixel dim w') - test:assertEquals(64*love.graphics.getDPIScale(), ph, 'check pixel dim h') - test:assertEquals(pw, image:getPixelWidth(), 'check pixel w matches dim') - test:assertEquals(ph, image:getPixelHeight(), 'check pixel h matches dim') - - -- check mipmaps - local mode, sharpness = image:getMipmapFilter() - test:assertEquals('linear', mode, 'check def minmap filter mode') - test:assertEquals(0, sharpness, 'check def minmap filter sharpness') - local name, version, vendor, device = love.graphics.getRendererInfo() - -- @note mipmap sharpness wont work on opengl/metal - image:setMipmapFilter('nearest', 1) - mode, sharpness = image:getMipmapFilter() - test:assertEquals('nearest', mode, 'check changed minmap filter mode') - if string.match(name, 'OpenGL ES') == nil and string.match(name, 'Metal') == nil then - test:assertEquals(1, sharpness, 'check changed minmap filter sharpness') - end - test:assertGreaterEqual(2, image:getMipmapCount()) -- docs say no mipmaps should return 1? - - -- check image properties - test:assertFalse(image:isCompressed(), 'check not compressed') - test:assertFalse(image:isFormatLinear(), 'check not linear') - local cimage = love.graphics.newImage('resources/love.dxt1') - test:assertObject(cimage) - test:assertTrue(cimage:isCompressed(), 'check is compressed') - - -- check pixel replacement - local rimage = love.image.newImageData('resources/loveinv.png') - image:replacePixels(rimage) - local canvas = love.graphics.newCanvas(64, 64) - love.graphics.setCanvas(canvas) - love.graphics.draw(image, 0, 0) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - local r1, g1, b1 = imgdata:getPixel(25, 25) - test:assertEquals(3, r1+g1+b1, 'check back to white') - test:compareImg(imgdata) - -end - - --- Mesh (love.graphics.newMesh) -love.test.graphics.Mesh = function(test) - - -- create 2d mesh with pretty colors - local image = love.graphics.newImage('resources/love.png') - local vertices = { - { 0, 0, 0, 0, 1, 0, 0 }, - { image:getWidth(), 0, 1, 0, 0, 1, 0 }, - { image:getWidth(), image:getHeight(), 1, 1, 0, 0, 1 }, - { 0, image:getHeight(), 0, 1, 1, 1, 0 }, - } - local mesh1 = love.graphics.newMesh(vertices, 'fan') - test:assertObject(mesh1) - - -- check draw mode - test:assertEquals('fan', mesh1:getDrawMode(), 'check draw mode') - mesh1:setDrawMode('triangles') - test:assertEquals('triangles', mesh1:getDrawMode(), 'check draw mode set') - - -- check draw range - local min1, max1 = mesh1:getDrawRange() - test:assertEquals(nil, min1, 'check draw range not set') - mesh1:setDrawRange(1, 10) - local min2, max2 = mesh1:getDrawRange() - test:assertEquals(1, min2, 'check draw range set min') - test:assertEquals(10, max2, 'check draw range set max') - - -- check texture pointer - test:assertEquals(nil, mesh1:getTexture(), 'check no texture') - mesh1:setTexture(image) - test:assertEquals(image:getHeight(), mesh1:getTexture():getHeight(), 'check texture match w') - test:assertEquals(image:getWidth(), mesh1:getTexture():getWidth(), 'check texture match h') - - -- check vertext count - test:assertEquals(4, mesh1:getVertexCount(), 'check vertex count') - - -- check def vertex format - local format = mesh1:getVertexFormat() - test:assertEquals('floatvec2', format[2].format, 'check def vertex format 2') - test:assertEquals('VertexColor', format[3].name, 'check def vertex format 3') - - -- check vertext attributes - test:assertTrue(mesh1:isAttributeEnabled('VertexPosition'), 'check def attribute VertexPosition') - test:assertTrue(mesh1:isAttributeEnabled('VertexTexCoord'), 'check def attribute VertexTexCoord') - test:assertTrue(mesh1:isAttributeEnabled('VertexColor'), 'check def attribute VertexColor') - mesh1:setAttributeEnabled('VertexPosition', false) - mesh1:setAttributeEnabled('VertexTexCoord', false) - mesh1:setAttributeEnabled('VertexColor', false) - test:assertFalse(mesh1:isAttributeEnabled('VertexPosition'), 'check disable attribute VertexPosition') - test:assertFalse(mesh1:isAttributeEnabled('VertexTexCoord'), 'check disable attribute VertexTexCoord') - test:assertFalse(mesh1:isAttributeEnabled('VertexColor'), 'check disable attribute VertexColor') - - -- check vertex itself - local x1, y1, u1, v1, r1, g1, b1, a1 = mesh1:getVertex(1) - test:assertEquals(0, x1, 'check vertex props x') - test:assertEquals(0, y1, 'check vertex props y') - test:assertEquals(0, u1, 'check vertex props u') - test:assertEquals(0, v1, 'check vertex props v') - test:assertEquals(1, r1, 'check vertex props r') - test:assertEquals(0, g1, 'check vertex props g') - test:assertEquals(0, b1, 'check vertex props b') - test:assertEquals(1, a1, 'check vertex props a') - - -- check setting a specific vertex - mesh1:setVertex(2, image:getWidth(), 0, 1, 0, 0, 1, 1, 1) - local x2, y2, u2, v2, r2, g2, b2, a2 = mesh1:getVertex(2) - test:assertEquals(image:getWidth(), x2, 'check changed vertex props x') - test:assertEquals(0, y2, 'check changed vertex props y') - test:assertEquals(1, u2, 'check changed vertex props u') - test:assertEquals(0, v2, 'check changed vertex props v') - test:assertEquals(0, r2, 'check changed vertex props r') - test:assertEquals(1, g2, 'check changed vertex props g') - test:assertEquals(1, b2, 'check changed vertex props b') - test:assertEquals(1, a2, 'check changed vertex props a') - - -- check setting a specific vertex attribute - local r3, g3, b3, a3 = mesh1:getVertexAttribute(3, 3) - test:assertEquals(1, b3, 'check specific vertex color') - mesh1:setVertexAttribute(4, 3, 1, 0, 1) - local r4, g4, b4, a4 = mesh1:getVertexAttribute(4, 3) - test:assertEquals(0, g4, 'check changed vertex color') - - -- check setting a vertice - mesh1:setVertices(vertices) - local r5, g5, b5, a5 = mesh1:getVertexAttribute(4, 3) - local x6, y6, u6, v6, r6, g6, b6, a6 = mesh1:getVertex(2) - test:assertEquals(1, g5, 'check reset vertex color 1') - test:assertEquals(0, b5, 'check reset vertex color 2') - - -- check setting the vertex map - local vmap1 = mesh1:getVertexMap() - test:assertEquals(nil, vmap1, 'check no map by def') - mesh1:setVertexMap({4, 1, 2, 3}) - local vmap2 = mesh1:getVertexMap() - test:assertEquals(4, #vmap2, 'check set map len') - test:assertEquals(2, vmap2[3], 'check set map val') - - -- check using custom attributes - local mesh2 = love.graphics.newMesh({ - { name = 'VertexPosition', format = 'floatvec2', location = 0}, - { name = 'VertexTexCoord', format = 'floatvec2', location = 1}, - { name = 'VertexColor', format = 'floatvec4', location = 2}, - { name = 'CustomValue1', format = 'floatvec2', location = 3}, - { name = 'CustomValue2', format = 'uint16', location = 4} - }, { - { 0, 0, 0, 0, 1, 0, 0, 1, 2, 1, 1005 }, - { image:getWidth(), 0, 1, 0, 0, 1, 0, 0, 2, 2, 2005 }, - { image:getWidth(), image:getHeight(), 1, 1, 0, 0, 1, 0, 2, 3, 3005 }, - { 0, image:getHeight(), 0, 1, 1, 1, 0, 0, 2, 4, 4005 }, - }, 'fan') - local c1, c2 = mesh2:getVertexAttribute(1, 4) - local c3 = mesh2:getVertexAttribute(1, 5) - test:assertEquals(2, c1, 'check custom attribute val 1') - test:assertEquals(1, c2, 'check custom attribute val 2') - test:assertEquals(1005, c3, 'check custom attribute val 3') - - -- check attaching custom attribute + detaching - mesh1:attachAttribute('CustomValue1', mesh2) - test:assertTrue(mesh1:isAttributeEnabled('CustomValue1'), 'check custom attribute attached') - mesh1:detachAttribute('CustomValue1') - local obj, err = pcall(mesh1.isAttributeEnabled, mesh1, 'CustomValue1') - test:assertNotEquals(nil, err, 'check attribute detached') - mesh1:detachAttribute('VertexPosition') - test:assertTrue(mesh1:isAttributeEnabled('VertexPosition'), 'check cant detach def attribute') - -end - - --- ParticleSystem (love.graphics.newParticleSystem) -love.test.graphics.ParticleSystem = function(test) - - -- create new system - local image = love.graphics.newImage('resources/pixel.png') - local quad1 = love.graphics.newQuad(0, 0, 1, 1, image) - local quad2 = love.graphics.newQuad(0, 0, 1, 1, image) - local psystem = love.graphics.newParticleSystem(image, 1000) - test:assertObject(psystem) - - -- check psystem state properties - psystem:start() - psystem:update(1) - test:assertTrue(psystem:isActive(), 'check active') - test:assertFalse(psystem:isPaused(), 'checked not paused by def') - test:assertFalse(psystem:hasRelativeRotation(), 'check rel rot def') - psystem:pause() - test:assertTrue(psystem:isPaused(), 'check now paused') - test:assertFalse(psystem:isStopped(), 'check not stopped by def') - psystem:stop() - test:assertTrue(psystem:isStopped(), 'check now stopped') - psystem:start() - psystem:reset() - - -- check emitting some particles - -- need to set a lifespan at minimum or none will be counted - local min, max = psystem:getParticleLifetime() - test:assertEquals(0, min, 'check def lifetime min') - test:assertEquals(0, max, 'check def lifetime max') - psystem:setParticleLifetime(1, 2) - psystem:emit(10) - psystem:update(1) - test:assertEquals(10, psystem:getCount(), 'check added particles') - psystem:reset() - test:assertEquals(0, psystem:getCount(), 'check reset') - - -- check setting colors - local colors1 = {psystem:getColors()} - test:assertEquals(1, #colors1, 'check 1 color by def') - psystem:setColors(1, 1, 1, 1, 1, 0, 0, 1) - local colors2 = {psystem:getColors()} - test:assertEquals(2, #colors2, 'check set colors') - test:assertEquals(1, colors2[2][1], 'check set color') - - -- check setting direction - test:assertEquals(0, psystem:getDirection(), 'check def direction') - psystem:setDirection(90 * (math.pi/180)) - test:assertEquals(math.floor(math.pi/2*100), math.floor(psystem:getDirection()*100), 'check set direction') - - -- check emission area options - psystem:setEmissionArea('normal', 100, 50) - psystem:setEmissionArea('ellipse', 100, 50) - psystem:setEmissionArea('borderellipse', 100, 50) - psystem:setEmissionArea('borderrectangle', 100, 50) - psystem:setEmissionArea('none', 100, 50) - psystem:setEmissionArea('uniform', 100, 50) - local dist, dx, dy, angle, rel = psystem:getEmissionArea() - test:assertEquals('uniform', dist, 'check emission area dist') - test:assertEquals(100, dx, 'check emission area dx') - test:assertEquals(50, dy, 'check emission area dy') - test:assertEquals(0, angle, 'check emission area angle') - test:assertFalse(rel, 'check emission area rel') - - -- check emission rate - test:assertEquals(0, psystem:getEmissionRate(), 'check def emission rate') - psystem:setEmissionRate(1) - test:assertEquals(1, psystem:getEmissionRate(), 'check changed emission rate') - - -- check emission lifetime - test:assertEquals(-1, psystem:getEmitterLifetime(), 'check def emitter life') - psystem:setEmitterLifetime(10) - test:assertEquals(10, psystem:getEmitterLifetime(), 'check changed emitter life') - - -- check insert mode - test:assertEquals('top', psystem:getInsertMode(), 'check def insert mode') - psystem:setInsertMode('bottom') - psystem:setInsertMode('random') - test:assertEquals('random', psystem:getInsertMode(), 'check change insert mode') - - -- check linear acceleration - local xmin1, ymin1, xmax1, ymax1 = psystem:getLinearAcceleration() - test:assertEquals(0, xmin1, 'check def lin acceleration xmin') - test:assertEquals(0, ymin1, 'check def lin acceleration ymin') - test:assertEquals(0, xmax1, 'check def lin acceleration xmax') - test:assertEquals(0, ymax1, 'check def lin acceleration ymax') - psystem:setLinearAcceleration(1, 2, 3, 4) - local xmin2, ymin2, xmax2, ymax2 = psystem:getLinearAcceleration() - test:assertEquals(1, xmin2, 'check change lin acceleration xmin') - test:assertEquals(2, ymin2, 'check change lin acceleration ymin') - test:assertEquals(3, xmax2, 'check change lin acceleration xmax') - test:assertEquals(4, ymax2, 'check change lin acceleration ymax') - - -- check linear damping - local min3, max3 = psystem:getLinearDamping() - test:assertEquals(0, min3, 'check def lin damping min') - test:assertEquals(0, max3, 'check def lin damping max') - psystem:setLinearDamping(1, 2) - local min4, max4 = psystem:getLinearDamping() - test:assertEquals(1, min4, 'check change lin damping min') - test:assertEquals(2, max4, 'check change lin damping max') - - -- check offset - local ox1, oy1 = psystem:getOffset() - test:assertEquals(0.5, ox1, 'check def offset x') -- 0.5 cos middle of pixel image which is 1x1 - test:assertEquals(0.5, oy1, 'check def offset y') - psystem:setOffset(0, 10) - local ox2, oy2 = psystem:getOffset() - test:assertEquals(0, ox2, 'check change offset x') - test:assertEquals(10, oy2, 'check change offset y') - - -- check lifetime (we set it earlier) - local min5, max5 = psystem:getParticleLifetime() - test:assertEquals(1, min5, 'check p lifetime min') - test:assertEquals(2, max5, 'check p lifetime max') - - -- check position - local x1, y1 = psystem:getPosition() - test:assertEquals(0, x1, 'check emitter x') - test:assertEquals(0, y1, 'check emitter y') - psystem:setPosition(10, 12) - local x2, y2 = psystem:getPosition() - test:assertEquals(10, x2, 'check set emitter x') - test:assertEquals(12, y2, 'check set emitter y') - - -- check quads - test:assertEquals(0, #psystem:getQuads(), 'check def quads') - psystem:setQuads({quad1}) - psystem:setQuads(quad1, quad2) - test:assertEquals(2, #psystem:getQuads(), 'check set quads') - - -- check radial acceleration - local min6, max6 = psystem:getRadialAcceleration() - test:assertEquals(0, min6, 'check def rad accel min') - test:assertEquals(0, max6, 'check def rad accel max') - psystem:setRadialAcceleration(1, 2) - local min7, max7 = psystem:getRadialAcceleration() - test:assertEquals(1, min7, 'check change rad accel min') - test:assertEquals(2, max7, 'check change rad accel max') - - -- check rotation - local min8, max8 = psystem:getRotation() - test:assertEquals(0, min8, 'check def rot min') - test:assertEquals(0, max8, 'check def rot max') - psystem:setRotation(90 * (math.pi/180), 180 * (math.pi/180)) - local min8, max8 = psystem:getRotation() - test:assertEquals(math.floor(math.pi/2*100), math.floor(min8*100), 'check set rot min') - test:assertEquals(math.floor(math.pi*100), math.floor(max8*100), 'check set rot max') - - -- check variation - test:assertEquals(0, psystem:getSizeVariation(), 'check def variation') - psystem:setSizeVariation(1) - test:assertEquals(1, psystem:getSizeVariation(), 'check change variation') - - -- check sizes - test:assertEquals(1, #{psystem:getSizes()}, 'check def size') - psystem:setSizes(1, 2, 4, 1, 3, 2) - local sizes = {psystem:getSizes()} - test:assertEquals(6, #sizes, 'check set sizes') - test:assertEquals(3, sizes[5], 'check set size') - - -- check speed - local min9, max9 = psystem:getSpeed() - test:assertEquals(0, min9, 'check def speed min') - test:assertEquals(0, max9, 'check def speed max') - psystem:setSpeed(1, 10) - local min10, max10 = psystem:getSpeed() - test:assertEquals(1, min10, 'check change speed min') - test:assertEquals(10, max10, 'check change speed max') - - -- check variation + spin - local variation = psystem:getSpinVariation() - test:assertEquals(0, variation, 'check def spin variation') - psystem:setSpinVariation(1) - test:assertEquals(1, psystem:getSpinVariation(), 'check change spin variation') - psystem:setSpin(1, 2) - local min11, max11 = psystem:getSpin() - test:assertEquals(1, min11, 'check change spin min') - test:assertEquals(2, max11, 'check change spin max') - - -- check spread - test:assertEquals(0, psystem:getSpread(), 'check def spread') - psystem:setSpread(90 * (math.pi/180)) - test:assertEquals(math.floor(math.pi/2*100), math.floor(psystem:getSpread()*100), 'check change spread') - - -- tangential acceleration - local min12, max12 = psystem:getTangentialAcceleration() - test:assertEquals(0, min12, 'check def tan accel min') - test:assertEquals(0, max12, 'check def tan accel max') - psystem:setTangentialAcceleration(1, 2) - local min13, max13 = psystem:getTangentialAcceleration() - test:assertEquals(1, min13, 'check change tan accel min') - test:assertEquals(2, max13, 'check change tan accel max') - - -- check texture - test:assertNotEquals(nil, psystem:getTexture(), 'check texture obj') - test:assertObject(psystem:getTexture()) - psystem:setTexture(love.graphics.newImage('resources/love.png')) - test:assertObject(psystem:getTexture()) - - -- try a graphics test! - -- hard to get exactly because of the variation but we can use some pixel - -- tolerance and volume to try and cover the randomness - local psystem2 = love.graphics.newParticleSystem(image, 5000) - psystem2:setEmissionArea('uniform', 2, 64) - psystem2:setColors(1, 0, 0, 1) - psystem2:setDirection(0 * math.pi/180) - psystem2:setEmitterLifetime(100) - psystem2:setEmissionRate(5000) - local psystem3 = psystem2:clone() - psystem3:setPosition(64, 0) - psystem3:setColors(0, 1, 0, 1) - psystem3:setDirection(180 * (math.pi/180)) - psystem2:start() - psystem3:start() - psystem2:update(1) - psystem3:update(1) - local canvas = love.graphics.newCanvas(64, 64) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.draw(psystem2, 0, 0) - love.graphics.draw(psystem3, 0, 0) - love.graphics.setCanvas() - -- this should result in a bunch of red pixels on the left 2px of the canvas - -- and a bunch of green pixels on the right 2px of the canvas - local imgdata = love.graphics.readbackTexture(canvas) - test.pixel_tolerance = 1 - test:compareImg(imgdata) - -end - - --- Quad (love.graphics.newQuad) -love.test.graphics.Quad = function(test) - - -- create quad obj - local texture = love.graphics.newImage('resources/love.png') - local quad = love.graphics.newQuad(0, 0, 32, 32, texture) - test:assertObject(quad) - - -- check properties - test:assertEquals(1, quad:getLayer(), 'check default layer') - quad:setLayer(2) - test:assertEquals(2, quad:getLayer(), 'check changed layer') - local sw, sh = quad:getTextureDimensions() - test:assertEquals(64, sw, 'check texture w') - test:assertEquals(64, sh, 'check texture h') - - -- check drawing and viewport changes - local canvas = love.graphics.newCanvas(64, 64) - love.graphics.setCanvas(canvas) - love.graphics.draw(texture, quad, 0, 0) - quad:setViewport(32, 32, 32, 32, 64, 64) - love.graphics.draw(texture, quad, 32, 32) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) - -end - - --- Shader (love.graphics.newShader) -love.test.graphics.Shader = function(test) - - -- check valid shader - local pixelcode1 = [[ - uniform Image tex2; - vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) { - vec4 texturecolor = Texel(tex2, texture_coords); - return texturecolor * color; - } - ]] - local vertexcode1 = [[ - vec4 position(mat4 transform_projection, vec4 vertex_position) { - return transform_projection * vertex_position; - } - ]] - local shader1 = love.graphics.newShader(pixelcode1, vertexcode1, {debugname = 'testshader'}) - test:assertObject(shader1) - test:assertEquals('', shader1:getWarnings(), 'check shader valid') - test:assertFalse(shader1:hasUniform('tex1'), 'check invalid uniform') - test:assertTrue(shader1:hasUniform('tex2'), 'check valid uniform') - test:assertEquals('testshader', shader1:getDebugName()) - - -- check invalid shader - local pixelcode2 = [[ - uniform float ww; - vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) { - vec4 texturecolor = Texel(tex, texture_coords); - float unused = ww * 3 * color; - return texturecolor * color; - } - ]] - local res, err = pcall(love.graphics.newShader, pixelcode2, vertexcode1) - test:assertNotEquals(nil, err, 'check shader compile fails') - - -- check using a shader to draw + sending uniforms - -- shader will return a given color if overwrite set to 1, otherwise def. draw - local pixelcode3 = [[ - uniform vec4 col; - uniform float overwrite; - vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) { - vec4 texcol = Texel(tex, texture_coords); - if (overwrite == 1.0) { - return col; - } else { - return texcol * color; - } - } - ]] - local shader3 = love.graphics.newShader(pixelcode3, vertexcode1) - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.push("all") - love.graphics.setCanvas(canvas) - -- set color to yellow - love.graphics.setColor(1, 1, 0, 1) - -- turn shader 'on' and use red to draw - shader3:send('overwrite', 1) - shader3:sendColor('col', {1, 0, 0, 1}) - love.graphics.setShader(shader3) - love.graphics.rectangle('fill', 0, 0, 8, 8) - love.graphics.setShader() - -- turn shader 'off' and draw again - shader3:send('overwrite', 0) - love.graphics.setShader(shader3) - love.graphics.rectangle('fill', 8, 8, 8, 8) - love.graphics.pop() - - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) - - -- test some uncommon paths for shader uniforms - local shader4 = love.graphics.newShader[[ - uniform bool booleans[5]; - vec4 effect(vec4 vcolor, Image tex, vec2 tc, vec2 pc) { - return booleans[3] ? vec4(0, 1, 0, 0) : vec4(1, 0, 0, 0); - } - ]] - - shader4:send("booleans", false, true, true) - - local shader5 = love.graphics.newShader[[ - uniform sampler2D textures[5]; - vec4 effect(vec4 vcolor, Image tex, vec2 tc, vec2 pc) { - return Texel(textures[2], tc) + Texel(textures[3], tc); - } - ]] - - local canvas2 = love.graphics.newCanvas(1, 1) - love.graphics.setCanvas(canvas2) - love.graphics.clear(0, 0.5, 0, 1) - love.graphics.setCanvas() - - shader5:send("textures", canvas2, canvas2, canvas2, canvas2, canvas2) - - local shader6 = love.graphics.newShader[[ - struct Data { - bool boolValue; - float floatValue; - sampler2D tex; - }; - - uniform Data data; - uniform Data dataArray[3]; - - vec4 effect(vec4 vcolor, Image tex, vec2 tc, vec2 pc) { - return (data.boolValue && dataArray[1].boolValue) ? Texel(dataArray[0].tex, tc) : vec4(0.0, 0.0, 0.0, 0.0); - } - ]] - - shader6:send("data.boolValue", true) - shader6:send("dataArray[1].boolValue", true) - shader6:send("dataArray[0].tex", canvas2) - - local shader7 = love.graphics.newShader[[ - uniform vec3 vec3s[3]; - - vec4 effect(vec4 vcolor, Image tex, vec2 tc, vec2 pc) { - return vec4(vec3s[1], 1.0); - } - ]] - - shader7:send("vec3s", {0, 0, 1}, {0, 1, 0}, {1, 0, 0}) - - local canvas3 = love.graphics.newCanvas(16, 16) - love.graphics.push("all") - love.graphics.setCanvas(canvas3) - love.graphics.setShader(shader7) - love.graphics.rectangle("fill", 0, 0, 16, 16) - love.graphics.pop() - local imgdata2 = love.graphics.readbackTexture(canvas3) - test:compareImg(imgdata2) - - if love.graphics.getSupported().glsl3 then - local shader8 = love.graphics.newShader[[ - #pragma language glsl3 - #ifdef GL_ES - precision highp float; - #endif - - varying vec4 VaryingUnused1; - varying mat3 VaryingMatrix; - flat varying ivec4 VaryingInt; - - #ifdef VERTEX - layout(location = 0) in vec4 VertexPosition; - layout(location = 1) in ivec4 IntAttributeUnused; - - void vertexmain() - { - VaryingMatrix = mat3(vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1)); - VaryingInt = ivec4(1, 1, 1, 1); - love_Position = TransformProjectionMatrix * VertexPosition; - } - #endif - - #ifdef PIXEL - out ivec4 outData; - - void pixelmain() - { - outData = ivec4(VaryingMatrix[1][1] > 0.0 ? 1 : 0, 1, VaryingInt.x, 1); - } - #endif - ]] - - local canvas4 = love.graphics.newCanvas(16, 16, {format="rgba8i"}) - love.graphics.push("all") - love.graphics.setBlendMode("none") - love.graphics.setCanvas(canvas4) - love.graphics.setShader(shader8) - love.graphics.rectangle("fill", 0, 0, 16, 16) - love.graphics.pop() - - local intimagedata = love.graphics.readbackTexture(canvas4) - local imgdata3 = love.image.newImageData(16, 16, "rgba8") - for y=0, 15 do - for x=0, 15 do - local ir, ig, ib, ia = intimagedata:getInt8(4 * (y * 16 + x), 4) - imgdata3:setPixel(x, y, ir, ig, ib, ia) - end - end - test:compareImg(imgdata3) - else - test:assertTrue(true, "skip shader IO test") - end -end - - --- SpriteBatch (love.graphics.newSpriteBatch) -love.test.graphics.SpriteBatch = function(test) - - -- create batch - local texture1 = love.graphics.newImage('resources/cubemap.png') - local texture2 = love.graphics.newImage('resources/love.png') - local quad1 = love.graphics.newQuad(32, 12, 1, 1, texture2) -- lovepink - local quad2 = love.graphics.newQuad(32, 32, 1, 1, texture2) -- white - local sbatch = love.graphics.newSpriteBatch(texture1, 5000) - test:assertObject(sbatch) - - -- check initial count - test:assertEquals(0, sbatch:getCount(), 'check batch size') - - -- check buffer size - test:assertEquals(5000, sbatch:getBufferSize(), 'check batch size') - - -- check height/width/texture - test:assertEquals(texture1:getWidth(), sbatch:getTexture():getWidth(), 'check texture match w') - test:assertEquals(texture1:getHeight(), sbatch:getTexture():getHeight(), 'check texture match h') - sbatch:setTexture(texture2) - test:assertEquals(texture2:getWidth(), sbatch:getTexture():getWidth(), 'check texture change w') - test:assertEquals(texture2:getHeight(), sbatch:getTexture():getHeight(), 'check texture change h') - - -- check colors - local r1, g1, b1, a1 = sbatch:getColor() - test:assertEquals(1, r1, 'check initial color r') - test:assertEquals(1, g1, 'check initial color g') - test:assertEquals(1, b1, 'check initial color b') - test:assertEquals(1, a1, 'check initial color a') - sbatch:setColor(1, 0, 0, 1) - local r2, g2, b2, a2 = sbatch:getColor() - test:assertEquals(1, r2, 'check set color r') - test:assertEquals(0, g2, 'check set color g') - test:assertEquals(0, b2, 'check set color b') - test:assertEquals(1, a2, 'check set color a') - - -- check adding sprites - local offset_x = 0 - local offset_y = 0 - local color = 'white' - sbatch:setColor(1, 1, 1, 1) - local sprites = {} - for s=1,4096 do - local spr = sbatch:add(quad1, offset_x, offset_y, 0, 1, 1) - table.insert(sprites, {spr, offset_x, offset_y}) - offset_x = offset_x + 1 - if s % 64 == 0 then - -- alternate row colors - if color == 'white' then - color = 'red' - sbatch:setColor(1, 0, 0, 1) - else - color = 'white' - sbatch:setColor(1, 1, 1, 1) - end - offset_y = offset_y + 1 - offset_x = 0 - end - end - test:assertEquals(4096, sbatch:getCount()) - - -- test drawing and setting - local canvas = love.graphics.newCanvas(64, 64) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.draw(sbatch, 0, 0) - love.graphics.setCanvas() - local imgdata1 = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata1) - - -- use set to change some sprites - for s=1,2048 do - sbatch:set(sprites[s][1], quad2, sprites[s][2], sprites[s][3]+1, 0, 1, 1) - end - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.draw(sbatch, 0, 0) - love.graphics.setCanvas() - local imgdata2 = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata2) - - -- set drawRange and redraw - sbatch:setDrawRange(1025, 2048) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.draw(sbatch, 0, 0) - love.graphics.setCanvas() - local imgdata3 = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata3) - - -- clear and redraw - sbatch:clear() - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.draw(sbatch, 0, 0) - love.graphics.setCanvas() - local imgdata4 = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata4) - - -- array texture sbatch - local texture3 = love.graphics.newArrayImage({ - 'resources/love.png', - 'resources/loveinv.png' - }) - local asbatch = love.graphics.newSpriteBatch(texture3, 4096) - local quad3 = love.graphics.newQuad(32, 52, 1, 1, texture3) -- loveblue - sprites = {} - for s=1,4096 do - local spr = asbatch:addLayer(1, quad3, 0, s, math.floor(s/64), 1, 1) - table.insert(sprites, {spr, s, math.floor(s/64)}) - end - test:assertEquals(4096, asbatch:getCount(), 'check max batch size applies') - for s=1,2048 do - asbatch:setLayer(sprites[s][1], 2, sprites[s][2], sprites[s][3], 0, 1, 1) - end - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.draw(asbatch, 0, 0) - love.graphics.setCanvas() - local imgdata5 = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata5) - -end - - --- Text (love.graphics.newTextBatch) -love.test.graphics.Text = function(test) - - -- setup text object - local font = love.graphics.newFont('resources/font.ttf', 8) - local plaintext = love.graphics.newTextBatch(font, 'test') - test:assertObject(plaintext) - - -- check height/width/dimensions - test:assertEquals(font:getHeight(), plaintext:getFont():getHeight(), 'check font matches') - local tw, th = plaintext:getDimensions() - test:assertEquals(24, tw, 'check initial dim w') - test:assertEquals(8, th, 'check initial dim h') - test:assertEquals(tw, plaintext:getWidth(), 'check initial dim w') - test:assertEquals(th, plaintext:getHeight(), 'check initial dim h') - - -- check changing text effects dimensions - plaintext:add('more text', 100, 0, 0) - test:assertEquals(49, plaintext:getDimensions(), 'check adding text') - plaintext:set('test') - test:assertEquals(24, plaintext:getDimensions(), 'check resetting text') - plaintext:clear() - test:assertEquals(0, plaintext:getDimensions(), 'check clearing text') - - -- check drawing + setting more complex text - local colortext = love.graphics.newTextBatch(font, {{1, 0, 0, 1}, 'test'}) - test:assertObject(colortext) - colortext:setf('LÖVE is an *awesome* framework you can use to make 2D games in Lua', 60, 'right') - colortext:addf({{1, 1, 0}, 'overlap'}, 1000, 'left') - local font2 = love.graphics.newFont('resources/font.ttf', 8) - colortext:setFont(font2) - local canvas = love.graphics.newCanvas(64, 64) - love.graphics.setCanvas(canvas) - love.graphics.draw(colortext, 0, 10) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) - -end - - --- Video (love.graphics.newVideo) -love.test.graphics.Video = function(test) - - -- create video obj - local video = love.graphics.newVideo('resources/sample.ogv') - test:assertObject(video) - - -- check dimensions - local w, h = video:getDimensions() - test:assertEquals(496, w, 'check vid dim w') - test:assertEquals(502, h, 'check vid dim h') - test:assertEquals(w, video:getWidth(), 'check vid width match') - test:assertEquals(h, video:getHeight(), 'check vid height match') - - -- check filters - local min1, mag1, ani1 = video:getFilter() - test:assertEquals('nearest', min1, 'check def filter min') - test:assertEquals('nearest', mag1, 'check def filter mag') - test:assertEquals(1, ani1, 'check def filter ani') - video:setFilter('linear', 'linear', 2) - local min2, mag2, ani2 = video:getFilter() - test:assertEquals('linear', min2, 'check changed filter min') - test:assertEquals('linear', mag2, 'check changed filter mag') - test:assertEquals(2, ani2, 'check changed filter ani') - - -- check video playing - test:assertFalse(video:isPlaying(), 'check paused by default') - test:assertEquals(0, video:tell(), 'check 0:00 by default') - - -- covered by their own obj tests in video but check returns obj - local source = video:getSource() - test:assertObject(source) - local stream = video:getStream() - test:assertObject(stream) - - -- check playing / pausing / seeking states - video:play() - test:waitSeconds(0.25) - video:pause() - -- runners can be a bit funny and just not play anything sometimes - if not GITHUB_RUNNER then - test:assertRange(video:tell(), 0.2, 0.35, 'check video playing for 0.25s') - end - video:seek(0.2) - test:assertEquals(0.2, video:tell(), 'check video seeking') - video:rewind() - test:assertEquals(0, video:tell(), 'check video rewind') - video:setFilter('nearest', 'nearest', 1) - - -- check actuall drawing with the vid - local canvas = love.graphics.newCanvas(500, 500) - love.graphics.setCanvas(canvas) - love.graphics.clear(1, 0, 0, 1) - love.graphics.draw(video, 0, 0) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) - -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------DRAWING------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.graphics.arc -love.test.graphics.arc = function(test) - -- draw some arcs using pi format - local canvas = love.graphics.newCanvas(32, 32) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.arc('line', "pie", 16, 16, 16, 0 * (math.pi/180), 360 * (math.pi/180), 10) - love.graphics.arc('fill', "pie", 16, 16, 16, 270 * (math.pi/180), 45 * (math.pi/180), 10) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.arc('line', "pie", 16, 16, 16, 0 * (math.pi/180), 90 * (math.pi/180), 10) - love.graphics.setColor(1, 1, 0, 1) - love.graphics.arc('line', "pie", 16, 16, 16, 180 * (math.pi/180), 135 * (math.pi/180), 10) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata1 = love.graphics.readbackTexture(canvas) - -- draw some arcs with open format - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.arc('line', "open", 16, 16, 16, 0 * (math.pi/180), 315 * (math.pi/180), 10) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.arc('fill', "open", 16, 16, 16, 0 * (math.pi/180), 180 * (math.pi/180), 10) - love.graphics.setColor(1, 1, 0, 1) - love.graphics.arc('fill', "open", 16, 16, 16, 180 * (math.pi/180), 270 * (math.pi/180), 10) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata2 = love.graphics.readbackTexture(canvas) - -- draw some arcs with closed format - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.arc('line', "closed", 16, 16, 16, 0 * (math.pi/180), 315 * (math.pi/180), 10) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.arc('fill', "closed", 16, 16, 16, 0 * (math.pi/180), 180 * (math.pi/180), 10) - love.graphics.setColor(1, 1, 0, 1) - love.graphics.arc('line', "closed", 16, 16, 16, 180 * (math.pi/180), 90 * (math.pi/180), 10) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata3 = love.graphics.readbackTexture(canvas) - if GITHUB_RUNNER and test:isOS('OS X') then - -- on macosx runners, the arcs are not drawn as accurately at low res - -- there's a couple pixels different in the curve of the arc but as we - -- are at such a low resolution I think that can be expected - -- on real hardware the test passes fine though - test:assertTrue(true, 'skip test') - else - test:compareImg(imgdata1) - test:compareImg(imgdata2) - test:compareImg(imgdata3) - end -end - - --- love.graphics.circle -love.test.graphics.circle = function(test) - -- draw some circles - local canvas = love.graphics.newCanvas(32, 32) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.circle('fill', 16, 16, 16) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.circle('line', 16, 16, 16) - love.graphics.setColor(1, 1, 0, 1) - love.graphics.circle('fill', 16, 16, 8) - love.graphics.setColor(0, 1, 0, 1) - love.graphics.circle('fill', 16, 16, 4) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - - --- love.graphics.clear -love.test.graphics.clear = function(test) - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.clear(1, 1, 0, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.discard -love.test.graphics.discard = function(test) - -- from the docs: "on some desktops this may do nothing" - test:skipTest('cant test this worked') -end - - --- love.graphics.draw -love.test.graphics.draw = function(test) - local canvas1 = love.graphics.newCanvas(32, 32) - local canvas2 = love.graphics.newCanvas(32, 32) - local transform = love.math.newTransform( ) - transform:translate(16, 0) - transform:scale(0.5, 0.5) - love.graphics.setCanvas(canvas1) - love.graphics.clear(0, 0, 0, 1) - -- img, offset - love.graphics.draw(Logo.texture, Logo.img, 0, 0, 0, 1, 1, 16, 16) - love.graphics.setCanvas() - love.graphics.setCanvas(canvas2) - love.graphics.clear(1, 0, 0, 1) - -- canvas, scale, shear, transform obj - love.graphics.draw(canvas1, 0, 0, 0, 1, 1, 0, 0, 2, 2) - love.graphics.draw(canvas1, 0, 16, 0, 0.5, 0.5) - love.graphics.draw(canvas1, 16, 16, 0, 0.5, 0.5) - love.graphics.draw(canvas1, transform) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas2) - test:compareImg(imgdata) -end - - --- love.graphics.drawInstanced -love.test.graphics.drawInstanced = function(test) - local image = love.graphics.newImage('resources/love.png') - local vertices = { - { 0, 0, 0, 0, 1, 0, 0 }, - { image:getWidth(), 0, 1, 0, 0, 1, 0 }, - { image:getWidth(), image:getHeight(), 1, 1, 0, 0, 1 }, - { 0, image:getHeight(), 0, 1, 1, 1, 0 }, - } - local mesh = love.graphics.newMesh(vertices, 'fan') - local canvas = love.graphics.newCanvas(64, 64) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.drawInstanced(mesh, 1000, 0, 0, 0, 1, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - -- need 1 tolerance here just cos of the amount of colors - test.rgba_tolerance = 1 - test:compareImg(imgdata) -end - - --- love.graphics.drawLayer -love.test.graphics.drawLayer = function(test) - local image = love.graphics.newArrayImage({ - 'resources/love.png', 'resources/loveinv.png', - 'resources/love.png', 'resources/loveinv.png' - }) - local canvas = love.graphics.newCanvas(64, 64) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.drawLayer(image, 1, 0, 0, 0, 1, 1) - love.graphics.drawLayer(image, 2, 32, 0, 0, 0.5, 0.5) - love.graphics.drawLayer(image, 4, 0, 32, 0, 0.5, 0.5) - love.graphics.drawLayer(image, 3, 32, 32, 0, 2, 2, 16, 16) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.ellipse -love.test.graphics.ellipse = function(test) - local canvas = love.graphics.newCanvas(32, 32) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.ellipse('fill', 16, 16, 16, 8) - love.graphics.setColor(1, 1, 0, 1) - love.graphics.ellipse('fill', 24, 24, 10, 24) - love.graphics.setColor(1, 0, 1, 1) - love.graphics.ellipse('fill', 16, 0, 8, 16) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.flushBatch -love.test.graphics.flushBatch = function(test) - love.graphics.flushBatch() - local initial = love.graphics.getStats()['drawcalls'] - local canvas = love.graphics.newCanvas(32, 32) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.rectangle('fill', 0, 0, 32, 32) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - love.graphics.flushBatch() - local after = love.graphics.getStats()['drawcalls'] - test:assertEquals(initial+1, after, 'check drawcalls increased') -end - - --- love.graphics.line -love.test.graphics.line = function(test) - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.line(1,1,16,1,16,16,1,16,1,1) - love.graphics.setColor(1, 1, 0, 1) - love.graphics.line({0,0,8,8,16,0,8,8,16,16,8,8,0,16}) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.points -love.test.graphics.points = function(test) - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.push("all") - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.translate(0.5, 0.5) -- draw points at the center of pixels - love.graphics.setColor(1, 0, 0, 1) - love.graphics.points(0,0,15,0,15,15,0,15,0,0) - love.graphics.setColor(1, 1, 0, 1) - love.graphics.points({1,1,7,7,14,1,7,8,14,14,8,8,1,14,8,7}) - love.graphics.pop() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.polygon -love.test.graphics.polygon = function(test) - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.polygon("fill", 1, 1, 4, 5, 8, 10, 16, 2, 7, 3, 5, 16, 16, 16, 1, 8) - love.graphics.setColor(1, 1, 0, 1) - love.graphics.polygon("line", {2, 2, 4, 5, 3, 7, 8, 15, 12, 4, 5, 10}) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.print -love.test.graphics.print = function(test) - love.graphics.setFont(Font) - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.print('love', 0, 3, 0, 1, 1, 0, 0) - love.graphics.setColor(0, 1, 0, 1) - love.graphics.print('ooo', 0, 3, 0, 2, 2, 0, 0) - love.graphics.setColor(0, 0, 1, 1) - love.graphics.print('hello', 0, 3, 90*(math.pi/180), 1, 1, 0, 8) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.printf -love.test.graphics.printf = function(test) - love.graphics.setFont(Font) - local canvas = love.graphics.newCanvas(32, 32) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.printf('love', 0, 0, 8, "left") - love.graphics.setColor(0, 1, 0, 1) - love.graphics.printf('love', 0, 5, 16, "right") - love.graphics.setColor(0, 0, 1, 1) - love.graphics.printf('love', 0, 7, 32, "center") - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.rectangle -love.test.graphics.rectangle = function(test) - -- setup, draw a 16x16 red rectangle with a blue central square - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.rectangle('fill', 0, 0, 16, 16) - love.graphics.setColor(0, 0, 1, 1) - love.graphics.rectangle('fill', 6, 6, 4, 4) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata1 = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata1) - -- clear canvas to do some line testing - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.rectangle('line', 1, 1, 15, 15) -- red border - love.graphics.setColor(0, 0, 1, 1) - love.graphics.rectangle('line', 1, 1, 2, 15) -- 3x16 left aligned blue outline - love.graphics.setColor(0, 1, 0, 1) - love.graphics.rectangle('line', 11, 1, 5, 15) -- 6x16 right aligned green outline - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata2 = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata2) -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- ---------------------------------OBJECT CREATION--------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.graphics.captureScreenshot -love.test.graphics.captureScreenshot = function(test) - love.graphics.captureScreenshot('example-screenshot.png') - test:waitFrames(1) - -- need to wait until end of the frame for the screenshot - test:assertTrue(love.filesystem.exists('example-screenshot.png')) - love.filesystem.remove('example-screenshot.png') - -- test callback version - local cbdata = nil - local prevtextcommand = TextCommand - TextCommand = "Capturing screenshot" - love.graphics.captureScreenshot(function (idata) - test:assertNotEquals(nil, idata, 'check we have image data') - cbdata = idata - end) - test:waitFrames(1) - TextCommand = prevtextcommand - test:assertNotNil(cbdata) - - if test:isOS('iOS', 'Android') then - -- Mobile operating systems don't let us control the window resolution, - -- so we can't compare the reference image properly. - test:assertTrue(true, 'skip test') - else - test:compareImg(cbdata) - end -end - - --- love.graphics.newArrayImage --- @NOTE this is just basic nil checking, objs have their own test method -love.test.graphics.newArrayImage = function(test) - test:assertObject(love.graphics.newArrayImage({ - 'resources/love.png', 'resources/love2.png', 'resources/love3.png' - })) -end - --- love.graphics.newCanvas --- @NOTE this is just basic nil checking, objs have their own test method -love.test.graphics.newCanvas = function(test) - test:assertObject(love.graphics.newCanvas(16, 16, { - type = '2d', - format = 'normal', - readable = true, - msaa = 0, - dpiscale = 1, - mipmaps = 'none' - })) - test:assertObject(love.graphics.newCanvas(1000, 1000)) -end - - --- love.graphics.newCubeImage --- @NOTE this is just basic nil checking, objs have their own test method -love.test.graphics.newCubeImage = function(test) - test:assertObject(love.graphics.newCubeImage('resources/cubemap.png', { - mipmaps = false, - linear = false - })) -end - - --- love.graphics.newFont --- @NOTE this is just basic nil checking, objs have their own test method -love.test.graphics.newFont = function(test) - test:assertObject(love.graphics.newFont('resources/font.ttf')) - test:assertObject(love.graphics.newFont('resources/font.ttf', 8, "normal", 1)) -end - - --- love.graphics.newImage --- @NOTE this is just basic nil checking, objs have their own test method -love.test.graphics.newImage = function(test) - test:assertObject(love.graphics.newImage('resources/love.png', { - mipmaps = false, - linear = false, - dpiscale = 1 - })) -end - - --- love.graphics.newImageFont --- @NOTE this is just basic nil checking, objs have their own test method -love.test.graphics.newImageFont = function(test) - test:assertObject(love.graphics.newImageFont('resources/love.png', 'ABCD', 1)) -end - - --- love.graphics.newMesh --- @NOTE this is just basic nil checking, objs have their own test method -love.test.graphics.newMesh = function(test) - test:assertObject(love.graphics.newMesh({{1, 1, 0, 0, 1, 1, 1, 1}}, 'fan', 'dynamic')) -end - - --- love.graphics.newParticleSystem --- @NOTE this is just basic nil checking, objs have their own test method -love.test.graphics.newParticleSystem = function(test) - local imgdata = love.graphics.newImage('resources/love.png') - test:assertObject(love.graphics.newParticleSystem(imgdata, 1000)) -end - - --- love.graphics.newQuad --- @NOTE this is just basic nil checking, objs have their own test method -love.test.graphics.newQuad = function(test) - local imgdata = love.graphics.newImage('resources/love.png') - test:assertObject(love.graphics.newQuad(0, 0, 16, 16, imgdata)) -end - - --- love.graphics.newShader --- @NOTE this is just basic nil checking, objs have their own test method -love.test.graphics.newShader = function(test) - local pixelcode = [[ - vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) { - vec4 texturecolor = Texel(tex, texture_coords); - return texturecolor * color; - } - ]] - local vertexcode = [[ - vec4 position(mat4 transform_projection, vec4 vertex_position) { - return transform_projection * vertex_position; - } - ]] - test:assertObject(love.graphics.newShader(pixelcode, vertexcode)) -end - - --- love.graphics.newSpriteBatch --- @NOTE this is just basic nil checking, objs have their own test method -love.test.graphics.newSpriteBatch = function(test) - local imgdata = love.graphics.newImage('resources/love.png') - test:assertObject(love.graphics.newSpriteBatch(imgdata, 1000)) -end - - --- love.graphics.newTextBatch --- @NOTE this is just basic nil checking, objs have their own test method -love.test.graphics.newTextBatch = function(test) - local font = love.graphics.newFont('resources/font.ttf') - test:assertObject(love.graphics.newTextBatch(font, 'helloworld')) -end - - --- love.graphics.newTexture --- @NOTE this is just basic nil checking, objs have their own test method -love.test.graphics.newTexture = function(test) - local imgdata = love.image.newImageData('resources/love.png') - test:assertObject(love.graphics.newTexture(imgdata)) -end - - --- love.graphics.newVideo --- @NOTE this is just basic nil checking, objs have their own test method -love.test.graphics.newVideo = function(test) - test:assertObject(love.graphics.newVideo('resources/sample.ogv', { - audio = false, - dpiscale = 1 - })) -end - - --- love.graphics.newVolumeImage --- @NOTE this is just basic nil checking, objs have their own test method -love.test.graphics.newVolumeImage = function(test) - test:assertObject(love.graphics.newVolumeImage({ - 'resources/love.png', 'resources/love2.png', 'resources/love3.png' - }, { - mipmaps = false, - linear = false - })) -end - - --- love.graphics.validateShader -love.test.graphics.validateShader = function(test) - local pixelcode = [[ - vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) { - vec4 texturecolor = Texel(tex, texture_coords); - return texturecolor * color; - } - ]] - local vertexcode = [[ - vec4 position(mat4 transform_projection, vec4 vertex_position) { - return transform_projection * vertex_position; - } - ]] - -- check made up code first - local status, _ = love.graphics.validateShader(true, 'nothing here', 'or here') - test:assertFalse(status, 'check invalid shader code') - -- check real code - status, _ = love.graphics.validateShader(true, pixelcode, vertexcode) - test:assertTrue(status, 'check valid shader code') -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- ----------------------------------GRAPHICS STATE--------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.graphics.getBackgroundColor -love.test.graphics.getBackgroundColor = function(test) - -- check default bg is black - local r, g, b, a = love.graphics.getBackgroundColor() - test:assertEquals(0, r, 'check default background r') - test:assertEquals(0, g, 'check default background g') - test:assertEquals(0, b, 'check default background b') - test:assertEquals(1, a, 'check default background a') - -- check set value returns correctly - love.graphics.setBackgroundColor(1, 1, 1, 0) - r, g, b, a = love.graphics.getBackgroundColor() - test:assertEquals(1, r, 'check updated background r') - test:assertEquals(1, g, 'check updated background g') - test:assertEquals(1, b, 'check updated background b') - test:assertEquals(0, a, 'check updated background a') - love.graphics.setBackgroundColor(0, 0, 0, 1) -- reset -end - - --- love.graphics.getBlendMode -love.test.graphics.getBlendMode = function(test) - -- check default blend mode - local mode, alphamode = love.graphics.getBlendMode() - test:assertEquals('alpha', mode, 'check default blend mode') - test:assertEquals('alphamultiply', alphamode, 'check default alpha blend') - -- check set mode returns correctly - love.graphics.setBlendMode('add', 'premultiplied') - mode, alphamode = love.graphics.getBlendMode() - test:assertEquals('add', mode, 'check changed blend mode') - test:assertEquals('premultiplied', alphamode, 'check changed alpha blend') - love.graphics.setBlendMode('alpha', 'alphamultiply') -- reset -end - - --- love.graphics.getCanvas -love.test.graphics.getCanvas = function(test) - -- by default should be nil if drawing to real screen - test:assertEquals(nil, love.graphics.getCanvas(), 'check no canvas set') - -- should return not nil when we target a canvas - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - test:assertObject(love.graphics.getCanvas()) - love.graphics.setCanvas() -end - - --- love.graphics.getColor -love.test.graphics.getColor = function(test) - -- by default should be white - local r, g, b, a = love.graphics.getColor() - test:assertEquals(1, r, 'check default color r') - test:assertEquals(1, g, 'check default color g') - test:assertEquals(1, b, 'check default color b') - test:assertEquals(1, a, 'check default color a') - -- check set color is returned correctly - love.graphics.setColor(0, 0, 0, 0) - r, g, b, a = love.graphics.getColor() - test:assertEquals(0, r, 'check changed color r') - test:assertEquals(0, g, 'check changed color g') - test:assertEquals(0, b, 'check changed color b') - test:assertEquals(0, a, 'check changed color a') - love.graphics.setColor(1, 1, 1, 1) -- reset -end - - --- love.graphics.getColorMask -love.test.graphics.getColorMask = function(test) - -- by default should all be active - local r, g, b, a = love.graphics.getColorMask() - test:assertTrue(r, 'check default color mask r') - test:assertTrue(g, 'check default color mask g') - test:assertTrue(b, 'check default color mask b') - test:assertTrue(a, 'check default color mask a') - -- check set color mask is returned correctly - love.graphics.setColorMask(false, false, true, false) - r, g, b, a = love.graphics.getColorMask() - test:assertFalse(r, 'check changed color mask r') - test:assertFalse(g, 'check changed color mask g') - test:assertTrue( b, 'check changed color mask b') - test:assertFalse(a, 'check changed color mask a') - love.graphics.setColorMask(true, true, true, true) -- reset -end - - --- love.graphics.getDefaultFilter -love.test.graphics.getDefaultFilter = function(test) - -- we set this already for testsuite so we know what it should be - local min, mag, anisotropy = love.graphics.getDefaultFilter() - test:assertEquals('nearest', min, 'check default filter min') - test:assertEquals('nearest', mag, 'check default filter mag') - test:assertEquals(1, anisotropy, 'check default filter mag') -end - - --- love.graphics.getDepthMode -love.test.graphics.getDepthMode = function(test) - -- by default should be always/write - local comparemode, write = love.graphics.getDepthMode() - test:assertEquals('always', comparemode, 'check default compare depth') - test:assertFalse(write, 'check default depth buffer write') -end - - --- love.graphics.getFont -love.test.graphics.getFont = function(test) - test:assertObject(love.graphics.getFont()) -end - - --- love.graphics.getFrontFaceWinding -love.test.graphics.getFrontFaceWinding = function(test) - -- check default winding - test:assertEquals('ccw', love.graphics.getFrontFaceWinding()) - -- check setting value changes it correctly - love.graphics.setFrontFaceWinding('cw') - test:assertEquals('cw', love.graphics.getFrontFaceWinding()) - love.graphics.setFrontFaceWinding('ccw') -- reset -end - - --- love.graphics.getLineJoin -love.test.graphics.getLineJoin = function(test) - -- check default line join - test:assertEquals('miter', love.graphics.getLineJoin()) - -- check set value returned correctly - love.graphics.setLineJoin('none') - test:assertEquals('none', love.graphics.getLineJoin()) - love.graphics.setLineJoin('miter') -- reset -end - - --- love.graphics.getLineStyle -love.test.graphics.getLineStyle = function(test) - -- we know this should be as testsuite sets it! - test:assertEquals('rough', love.graphics.getLineStyle()) - -- check set value returned correctly - love.graphics.setLineStyle('smooth') - test:assertEquals('smooth', love.graphics.getLineStyle()) - love.graphics.setLineStyle('rough') -- reset -end - - --- love.graphics.getLineWidth -love.test.graphics.getLineWidth = function(test) - -- we know this should be as testsuite sets it! - test:assertEquals(1, love.graphics.getLineWidth()) - -- check set value returned correctly - love.graphics.setLineWidth(10) - test:assertEquals(10, love.graphics.getLineWidth()) - love.graphics.setLineWidth(1) -- reset -end - - --- love.graphics.getMeshCullMode -love.test.graphics.getMeshCullMode = function(test) - -- get default mesh culling - test:assertEquals('none', love.graphics.getMeshCullMode()) - -- check set value returned correctly - love.graphics.setMeshCullMode('front') - test:assertEquals('front', love.graphics.getMeshCullMode()) - love.graphics.setMeshCullMode('back') -- reset -end - - --- love.graphics.getPointSize -love.test.graphics.getPointSize = function(test) - -- get default point size - test:assertEquals(1, love.graphics.getPointSize()) - -- check set value returned correctly - love.graphics.setPointSize(10) - test:assertEquals(10, love.graphics.getPointSize()) - love.graphics.setPointSize(1) -- reset -end - - --- love.graphics.getScissor -love.test.graphics.getScissor = function(test) - -- should be no scissor atm - local x, y, w, h = love.graphics.getScissor() - test:assertEquals(nil, x, 'check no scissor') - test:assertEquals(nil, y, 'check no scissor') - test:assertEquals(nil, w, 'check no scissor') - test:assertEquals(nil, h, 'check no scissor') - -- check set value returned correctly - love.graphics.setScissor(0, 0, 16, 16) - x, y, w, h = love.graphics.getScissor() - test:assertEquals(0, x, 'check scissor set') - test:assertEquals(0, y, 'check scissor set') - test:assertEquals(16, w, 'check scissor set') - test:assertEquals(16, h, 'check scissor set') - love.graphics.setScissor() -- reset -end - - --- love.graphics.getShader -love.test.graphics.getShader = function(test) - -- should be no shader active - test:assertEquals(nil, love.graphics.getShader(), 'check no active shader') -end - - --- love.graphics.getStackDepth -love.test.graphics.getStackDepth = function(test) - -- by default should be none - test:assertEquals(0, love.graphics.getStackDepth(), 'check no transforms in stack') - -- now add 3 - love.graphics.push() - love.graphics.push() - love.graphics.push() - test:assertEquals(3, love.graphics.getStackDepth(), 'check 3 transforms in stack') - -- now remove 2 - love.graphics.pop() - love.graphics.pop() - test:assertEquals(1, love.graphics.getStackDepth(), 'check 1 transforms in stack') - -- now back to 0 - love.graphics.pop() - test:assertEquals(0, love.graphics.getStackDepth(), 'check no transforms in stack') -end - - --- love.graphics.getStencilState -love.test.graphics.getStencilState = function(test) - -- check default vals - local action, comparemode, value = love.graphics.getStencilState( ) - test:assertEquals('keep', action, 'check default stencil action') - test:assertEquals('always', comparemode, 'check default stencil compare') - test:assertEquals(0, value, 'check default stencil value') - -- check set stencil values is returned - love.graphics.setStencilState('replace', 'less', 255) - local action, comparemode, value = love.graphics.getStencilState() - test:assertEquals('replace', action, 'check changed stencil action') - test:assertEquals('less', comparemode, 'check changed stencil compare') - test:assertEquals(255, value, 'check changed stencil value') - love.graphics.setStencilState() -- reset -end - - --- love.graphics.intersectScissor -love.test.graphics.intersectScissor = function(test) - -- make a scissor for the left half, then interset to make the top half - -- then we should be able to fill the canvas with red and only top 4x4 is filled - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.origin() - love.graphics.setScissor(0, 0, 8, 16) - love.graphics.intersectScissor(0, 0, 4, 4) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.rectangle('fill', 0, 0, 16, 16) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setScissor() - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.isActive -love.test.graphics.isActive = function(test) - test:assertTrue(love.graphics.isActive(), 'check graphics is active') -- i mean if you got this far -end - - --- love.graphics.isGammaCorrect -love.test.graphics.isGammaCorrect = function(test) - -- we know the config so know this is false - test:assertNotNil(love.graphics.isGammaCorrect()) -end - - --- love.graphics.isWireframe -love.test.graphics.isWireframe = function(test) - local name, version, vendor, device = love.graphics.getRendererInfo() - if string.match(name, 'OpenGL ES') then - test:skipTest('Wireframe not supported on OpenGL ES') - else - -- check off by default - test:assertFalse(love.graphics.isWireframe(), 'check no wireframe by default') - -- check on when enabled - love.graphics.setWireframe(true) - test:assertTrue(love.graphics.isWireframe(), 'check wireframe is set') - love.graphics.setWireframe(false) -- reset - end -end - - --- love.graphics.reset -love.test.graphics.reset = function(test) - -- reset should reset current canvas and any colors/scissor - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setBackgroundColor(0, 0, 1, 1) - love.graphics.setColor(0, 1, 0, 1) - love.graphics.setCanvas(canvas) - love.graphics.reset() - local r, g, b, a = love.graphics.getBackgroundColor() - test:assertEquals(1, r+g+b+a, 'check background reset') - r, g, b, a = love.graphics.getColor() - test:assertEquals(4, r+g+b+a, 'check color reset') - test:assertEquals(nil, love.graphics.getCanvas(), 'check canvas reset') - love.graphics.setDefaultFilter("nearest", "nearest") - love.graphics.setLineStyle('rough') - love.graphics.setPointSize(1) - love.graphics.setLineWidth(1) -end - - --- love.graphics.setBackgroundColor -love.test.graphics.setBackgroundColor = function(test) - -- check background is set - love.graphics.setBackgroundColor(1, 0, 0, 1) - local r, g, b, a = love.graphics.getBackgroundColor() - test:assertEquals(1, r, 'check set bg r') - test:assertEquals(0, g, 'check set bg g') - test:assertEquals(0, b, 'check set bg b') - test:assertEquals(1, a, 'check set bg a') - love.graphics.setBackgroundColor(0, 0, 0, 1) -end - - --- love.graphics.setBlendMode -love.test.graphics.setBlendMode = function(test) - -- create fully white canvas, then draw diff. pixels through blendmodes - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0.5, 0.5, 0.5, 1) - love.graphics.setBlendMode('add', 'alphamultiply') - love.graphics.setColor(1, 0, 0, 1) - love.graphics.rectangle('fill', 0, 0, 1, 1) - love.graphics.setBlendMode('subtract', 'alphamultiply') - love.graphics.setColor(1, 1, 1, 0.5) - love.graphics.rectangle('fill', 15, 0, 1, 1) - love.graphics.setBlendMode('multiply', 'premultiplied') - love.graphics.setColor(0, 1, 0, 1) - love.graphics.rectangle('fill', 15, 15, 1, 1) - love.graphics.setBlendMode('replace', 'premultiplied') - love.graphics.setColor(0, 0, 1, 0.5) - love.graphics.rectangle('fill', 0, 15, 1, 1) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - love.graphics.setBlendMode('alpha', 'alphamultiply') -- reset - -- need 1rgba tolerance here on some machines - test.rgba_tolerance = 1 - test:compareImg(imgdata) -end - - --- love.graphics.setCanvas -love.test.graphics.setCanvas = function(test) - -- make 2 canvas, set to each, draw one to the other, check output - local canvas1 = love.graphics.newCanvas(16, 16) - local canvas2 = love.graphics.newCanvas(16, 16, {mipmaps = "auto"}) - love.graphics.setCanvas(canvas1) - test:assertEquals(canvas1, love.graphics.getCanvas(), 'check canvas 1 set') - love.graphics.clear(1, 0, 0, 1) - love.graphics.setCanvas(canvas2) - test:assertEquals(canvas2, love.graphics.getCanvas(), 'check canvas 2 set') - love.graphics.clear(0, 0, 0, 1) - love.graphics.draw(canvas1, 0, 0) - love.graphics.setCanvas() - test:assertEquals(nil, love.graphics.getCanvas(), 'check no canvas set') - local imgdata = love.graphics.readbackTexture(canvas2) - test:compareImg(imgdata) - local imgdata2 = love.graphics.readbackTexture(canvas2, 1, 2) -- readback mipmap - test:compareImg(imgdata2) -end - - --- love.graphics.setColor -love.test.graphics.setColor = function(test) - -- set colors, draw rect, check color - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 0, 0, 1) - local r, g, b, a = love.graphics.getColor() - test:assertEquals(1, r, 'check r set') - test:assertEquals(0, g, 'check g set') - test:assertEquals(0, b, 'check b set') - test:assertEquals(1, a, 'check a set') - - love.graphics.rectangle('fill', 0, 0, 16, 1) - love.graphics.setColor(1, 1, 0, 1) - love.graphics.rectangle('fill', 0, 1, 16, 1) - love.graphics.setColor(0, 1, 0, 0.5) - love.graphics.rectangle('fill', 0, 2, 16, 1) - love.graphics.setColor(0, 0, 1, 1) - love.graphics.rectangle('fill', 0, 3, 16, 1) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.setColorMask -love.test.graphics.setColorMask = function(test) - -- set mask, draw stuff, check output pixels - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - -- mask off blue - love.graphics.setColorMask(true, true, false, true) - local r, g, b, a = love.graphics.getColorMask() - test:assertEquals(r, true, 'check r mask') - test:assertEquals(g, true, 'check g mask') - test:assertEquals(b, false, 'check b mask') - test:assertEquals(a, true, 'check a mask') - -- draw "black" which should then turn to yellow - love.graphics.setColor(1, 1, 1, 1) - love.graphics.rectangle('fill', 0, 0, 16, 16) - love.graphics.setColorMask(true, true, true, true) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.setDefaultFilter -love.test.graphics.setDefaultFilter = function(test) - -- check setting filter val works - love.graphics.setDefaultFilter('linear', 'linear', 1) - local min, mag, anisotropy = love.graphics.getDefaultFilter() - test:assertEquals('linear', min, 'check default filter min') - test:assertEquals('linear', mag, 'check default filter mag') - test:assertEquals(1, anisotropy, 'check default filter mag') - love.graphics.setDefaultFilter('nearest', 'nearest', 1) -- reset -end - - --- love.graphics.setDepthMode -love.test.graphics.setDepthMode = function(test) - -- check documented modes are valid - local comparemode, write = love.graphics.getDepthMode() - local modes = { - 'equal', 'notequal', 'less', 'lequal', 'gequal', - 'greater', 'never', 'always' - } - for m=1,#modes do - love.graphics.setDepthMode(modes[m], true) - test:assertEquals(modes[m], love.graphics.getDepthMode(), 'check depth mode ' .. modes[m] .. ' set') - end - love.graphics.setDepthMode(comparemode, write) - -- @TODO better graphics drawing specific test -end - - --- love.graphics.setFont -love.test.graphics.setFont = function(test) - -- set font doesnt return anything so draw with the test font - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setFont(Font) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.print('love', 0, 3) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.setFrontFaceWinding -love.test.graphics.setFrontFaceWinding = function(test) - -- check documented modes are valid - local original = love.graphics.getFrontFaceWinding() - love.graphics.setFrontFaceWinding('cw') - test:assertEquals('cw', love.graphics.getFrontFaceWinding(), 'check ffw cw set') - love.graphics.setFrontFaceWinding('ccw') - test:assertEquals('ccw', love.graphics.getFrontFaceWinding(), 'check ffw ccw set') - love.graphics.setFrontFaceWinding(original) - -- @TODO better graphics drawing specific test - - local shader = love.graphics.newShader[[ -vec4 effect(vec4 c, Image tex, vec2 tc, vec2 pc) { - return gl_FrontFacing ? vec4(0.0, 1.0, 0.0, 1.0) : vec4(1.0, 0.0, 0.0, 1.0); -} - ]] - local dummyimg = love.graphics.newImage(love.image.newImageData(1, 1)) - - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.push("all") - love.graphics.setCanvas(canvas) - love.graphics.setShader(shader) - love.graphics.draw(dummyimg, 0, 0, 0, 16, 16) - love.graphics.pop() - - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.setLineJoin -love.test.graphics.setLineJoin = function(test) - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setFont(Font) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - local line = {0,1,8,1,8,8} - love.graphics.setLineStyle('rough') - love.graphics.setLineWidth(2) - love.graphics.setColor(1, 0, 0) - love.graphics.setLineJoin('bevel') - love.graphics.line(line) - love.graphics.translate(0, 4) - love.graphics.setColor(1, 1, 0) - love.graphics.setLineJoin('none') - love.graphics.line(line) - love.graphics.translate(0, 4) - love.graphics.setColor(0, 0, 1) - love.graphics.setLineJoin('miter') - love.graphics.line(line) - love.graphics.setColor(1, 1, 1) - love.graphics.setLineWidth(1) - love.graphics.origin() - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.setLineStyle -love.test.graphics.setLineStyle = function(test) - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setFont(Font) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 0, 0) - local line = {0,1,16,1} - love.graphics.setLineStyle('rough') - love.graphics.line(line) - love.graphics.translate(0, 4) - love.graphics.setLineStyle('smooth') - love.graphics.line(line) - love.graphics.setLineStyle('rough') - love.graphics.setColor(1, 1, 1) - love.graphics.origin() - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - -- linux runner needs a 1/255 tolerance for the blend between a rough line + bg - if GITHUB_RUNNER and test:isOS('Linux') then - test.rgba_tolerance = 1 - end - test:compareImg(imgdata) -end - - --- love.graphics.setLineWidth -love.test.graphics.setLineWidth = function(test) - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setFont(Font) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - local line = {0,1,8,1,8,8} - love.graphics.setColor(1, 0, 0) - love.graphics.setLineWidth(2) - love.graphics.line(line) - love.graphics.translate(0, 4) - love.graphics.setColor(1, 1, 0) - love.graphics.setLineWidth(3) - love.graphics.line(line) - love.graphics.translate(0, 4) - love.graphics.setColor(0, 0, 1) - love.graphics.setLineWidth(4) - love.graphics.line(line) - love.graphics.setColor(1, 1, 1) - love.graphics.setLineWidth(1) - love.graphics.origin() - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.setMeshCullMode -love.test.graphics.setMeshCullMode = function(test) - -- check documented modes are valid - local original = love.graphics.getMeshCullMode() - local modes = {'back', 'front', 'none'} - for m=1,#modes do - love.graphics.setMeshCullMode(modes[m]) - test:assertEquals(modes[m], love.graphics.getMeshCullMode(), 'check mesh cull mode ' .. modes[m] .. ' was set') - end - love.graphics.setMeshCullMode(original) - -- @TODO better graphics drawing specific test -end - - --- love.graphics.setScissor -love.test.graphics.setScissor = function(test) - -- make a scissor for the left half - -- then we should be able to fill the canvas with red and only left is filled - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.origin() - love.graphics.setScissor(0, 0, 8, 16) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.rectangle('fill', 0, 0, 16, 16) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setScissor() - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.setShader -love.test.graphics.setShader = function(test) - -- make a shader that will only ever draw yellow - local pixelcode = [[ - vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) { - vec4 texturecolor = Texel(tex, texture_coords); - return vec4(1.0,1.0,0.0,1.0); - } - ]] - local vertexcode = [[ - vec4 position(mat4 transform_projection, vec4 vertex_position) { - return transform_projection * vertex_position; - } - ]] - local shader = love.graphics.newShader(pixelcode, vertexcode) - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setShader(shader) - -- draw red rectangle - love.graphics.setColor(1, 0, 0, 1) - love.graphics.rectangle('fill', 0, 0, 16, 16) - love.graphics.setShader() - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.setStencilState -love.test.graphics.setStencilState = function(test) - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas({canvas, stencil=true}) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setStencilState('replace', 'always', 1) - love.graphics.circle('fill', 8, 8, 6) - love.graphics.setStencilState('keep', 'greater', 0) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.rectangle('fill', 0, 0, 16, 16) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setStencilState() - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.setWireframe -love.test.graphics.setWireframe = function(test) - local name, version, vendor, device = love.graphics.getRendererInfo() - if string.match(name, 'OpenGL ES') then - test:skipTest('Wireframe not supported on OpenGL ES') - else - -- check wireframe outlines - love.graphics.setWireframe(true) - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 1, 0, 1) - love.graphics.rectangle('fill', 2, 2, 13, 13) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - love.graphics.setWireframe(false) - local imgdata = love.graphics.readbackTexture(canvas) - -- on macOS runners wireframes are drawn 1px off from the target - if GITHUB_RUNNER and test:isOS('OS X') then - test.pixel_tolerance = 1 - end - test:compareImg(imgdata) - end -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- --------------------------------COORDINATE SYSTEM-------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.graphics.applyTransform -love.test.graphics.applyTransform = function(test) - -- use transform object to translate the drawn rectangle - local transform = love.math.newTransform() - transform:translate(10, 0) - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.applyTransform(transform) - love.graphics.rectangle('fill', 0, 0, 1, 1) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.inverseTransformPoint -love.test.graphics.inverseTransformPoint = function(test) - -- start with 0, 0 - local sx, sy = love.graphics.inverseTransformPoint(0, 0) - test:assertEquals(0, sx, 'check starting x is 0') - test:assertEquals(0, sy, 'check starting y is 0') - -- check translation effects the point - love.graphics.translate(1, 5) - sx, sy = love.graphics.inverseTransformPoint(1, 5) - test:assertEquals(0, sx, 'check transformed x is 0') - test:assertEquals(0, sy, 'check transformed y is 0') - love.graphics.origin() -end - - --- love.graphics.origin -love.test.graphics.origin = function(test) - -- if we do some translations and scaling - -- using .origin() should reset it all and draw the pixel at 0,0 - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.origin() - love.graphics.translate(10, 10) - love.graphics.scale(1, 1) - love.graphics.shear(20, 20) - love.graphics.origin() - love.graphics.setColor(1, 0, 0, 1) - love.graphics.rectangle('fill', 0, 0, 1, 1) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.pop -love.test.graphics.pop = function(test) - -- if we push at the start, and then run a pop - -- it should reset it all and draw the pixel at 0,0 - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.push() - love.graphics.translate(10, 10) - love.graphics.scale(1, 1) - love.graphics.shear(20, 20) - love.graphics.pop() - love.graphics.setColor(1, 0, 0, 1) - love.graphics.rectangle('fill', 0, 0, 1, 1) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.push -love.test.graphics.push = function(test) - -- if we push at the start, do some stuff, then another push - -- 1 pop should only go back 1 push and draw the pixel at 1, 1 - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.push() - love.graphics.scale(1, 1) - love.graphics.shear(20, 20) - love.graphics.push() - love.graphics.translate(1, 1) - love.graphics.pop() - love.graphics.setColor(1, 0, 0, 1) - love.graphics.rectangle('fill', 0, 0, 1, 1) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.pop() - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.replaceTransform -love.test.graphics.replaceTransform = function(test) - -- if use transform object to translate - -- set some normal transforms first which should get overwritten - local transform = love.math.newTransform() - transform:translate(10, 0) - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.scale(2, 2) - love.graphics.translate(10, 10) - love.graphics.replaceTransform(transform) - love.graphics.rectangle('fill', 0, 0, 1, 1) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.rotate -love.test.graphics.rotate = function(test) - -- starting at 0,0, we rotate by 90deg and then draw - -- we can then check the drawn rectangle is rotated - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.translate(4, 0) - love.graphics.rotate(90 * (math.pi/180)) - love.graphics.rectangle('fill', 0, 0, 4, 4) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.scale -love.test.graphics.scale = function(test) - -- starting at 0,0, we scale by 4x and then draw - -- we can then check the drawn rectangle covers the whole canvas - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.scale(4, 4) - love.graphics.rectangle('fill', 0, 0, 4, 4) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --- love.graphics.shear -love.test.graphics.shear = function(test) - -- starting at 0,0, we shear by 2x and then draw - -- we can then check the drawn rectangle has moved over - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.origin() - love.graphics.setColor(1, 0, 0, 1) - love.graphics.shear(2, 0) - love.graphics.rectangle('fill', 0, 0, 4, 4) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata1 = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata1) - -- same again at 0,0, we shear by 2y and then draw - -- we can then check the drawn rectangle has moved down - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.origin() - love.graphics.setColor(1, 0, 0, 1) - love.graphics.shear(0, 2) - love.graphics.rectangle('fill', 0, 0, 4, 4) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata2 = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata2) -end - - --- love.graphics.transformPoint -love.test.graphics.transformPoint = function(test) - -- start with 0, 0 - local sx, sy = love.graphics.transformPoint(0, 0) - test:assertEquals(0, sx, 'check starting x is 0') - test:assertEquals(0, sy, 'check starting y is 0') - -- check translation effects the point - love.graphics.translate(1, 5) - sx, sy = love.graphics.transformPoint(0, 0) - test:assertEquals(1, sx, 'check transformed x is 0') - test:assertEquals(5, sy, 'check transformed y is 10') -end - - --- love.graphics.translate -love.test.graphics.translate = function(test) - -- starting at 0,0, we translate 4 times and draw a pixel at each point - -- we can then check the 4 points are now red - local canvas = love.graphics.newCanvas(16, 16) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, 1) - love.graphics.setColor(1, 0, 0, 1) - love.graphics.translate(5, 0) - love.graphics.rectangle('fill', 0, 0, 1, 1) - love.graphics.translate(0, 5) - love.graphics.rectangle('fill', 0, 0, 1, 1) - love.graphics.translate(-5, 0) - love.graphics.rectangle('fill', 0, 0, 1, 1) - love.graphics.translate(0, -5) - love.graphics.rectangle('fill', 0, 0, 1, 1) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setCanvas() - local imgdata = love.graphics.readbackTexture(canvas) - test:compareImg(imgdata) -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- --------------------------------------WINDOW------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.graphics.getDPIScale --- @NOTE hardware dependent so can't check result -love.test.graphics.getDPIScale = function(test) - test:assertNotNil(love.graphics.getDPIScale()) -end - - --- love.graphics.getDimensions -love.test.graphics.getDimensions = function(test) - -- check graphics dimensions match window dimensions - local gwidth, gheight = love.graphics.getDimensions() - local wwidth, wheight, _ = love.window.getMode() - test:assertEquals(wwidth, gwidth, 'check graphics dimension w matches window w') - test:assertEquals(wheight, gheight, 'check graphics dimension h matches window h') -end - - --- love.graphics.getHeight -love.test.graphics.getHeight = function(test) - -- check graphics height match window height - local wwidth, wheight, _ = love.window.getMode() - test:assertEquals(wheight, love.graphics.getHeight(), 'check graphics h matches window h') -end - - --- love.graphics.getPixelDimensions -love.test.graphics.getPixelDimensions = function(test) - -- check graphics dimensions match window dimensions relative to dpi - local dpi = love.graphics.getDPIScale() - local gwidth, gheight = love.graphics.getPixelDimensions() - local wwidth, wheight, _ = love.window.getMode() - test:assertEquals(wwidth, gwidth/dpi, 'check graphics pixel dpi w matches window w') - test:assertEquals(wheight, gheight/dpi, 'check graphics pixel dpi h matches window h') -end - - --- love.graphics.getPixelHeight -love.test.graphics.getPixelHeight = function(test) - -- check graphics height match window height relative to dpi - local dpi = love.graphics.getDPIScale() - local wwidth, wheight, _ = love.window.getMode() - test:assertEquals(wheight,love.graphics.getPixelHeight()/dpi, 'check graphics pixel dpi h matches window h') -end - - --- love.graphics.getPixelWidth -love.test.graphics.getPixelWidth = function(test) - -- check graphics width match window width relative to dpi - local dpi = love.graphics.getDPIScale() - local wwidth, wheight, _ = love.window.getMode() - test:assertEquals(wwidth, love.graphics.getWidth()/dpi, 'check graphics pixel dpi w matches window w') -end - - --- love.graphics.getWidth -love.test.graphics.getWidth = function(test) - -- check graphics width match window width - local wwidth, wheight, _ = love.window.getMode() - test:assertEquals(wwidth, love.graphics.getWidth(), 'check graphics w matches window w') -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- --------------------------------SYSTEM INFORMATION------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.graphics.getTextureFormats -love.test.graphics.getTextureFormats = function(test) - local formats = { - 'hdr', 'r8i', 'r8ui', 'r16i', 'r16ui', 'r32i', 'r32ui', 'rg8i', 'rg8ui', - 'rg16i', 'rg16ui', 'rg32i', 'rg32ui', 'bgra8', 'r8', 'rgba8i', 'rgba8ui', - 'rgba16i', 'rg8', 'rgba32i', 'rgba32ui', 'rgba8', 'DXT1', 'r16', 'DXT5', - 'rg16', 'BC4s', 'rgba16', 'BC5s', 'r16f', 'BC6hs', 'BC7', 'PVR1rgb2', - 'rg16f', 'PVR1rgba2', 'rgba16f', 'ETC1', 'r32f', 'ETC2rgba', 'rg32f', - 'EACr', 'rgba32f', 'EACrg', 'rgba4', 'ASTC4x4', 'ASTC5x4', 'rgb5a1', - 'ASTC6x5', 'rgb565', 'ASTC8x5', 'ASTC8x6', 'rgb10a2', 'ASTC10x5', - 'rg11b10f', 'ASTC10x8', 'ASTC10x10', 'ASTC12x10', 'ASTC12x12', 'normal', - 'srgba8', 'la8', 'ASTC10x6', 'ASTC8x8', 'ASTC6x6', 'ASTC5x5', 'EACrgs', - 'EACrs', 'ETC2rgba1', 'ETC2rgb', 'PVR1rgba4', 'PVR1rgb4', 'BC6h', - 'BC5', 'BC4', 'DXT3', 'rgba16ui', 'bgra8srgb', - 'depth16', 'depth24', 'depth32f', 'depth24stencil8', 'depth32fstencil8', 'stencil8' - } - local supported = love.graphics.getTextureFormats({ canvas = true }) - test:assertNotNil(supported) - for f=1,#formats do - test:assertNotEquals(nil, supported[formats[f] ], 'expected a key for format: ' .. formats[f]) - end -end - - --- love.graphics.getRendererInfo --- @NOTE hardware dependent so best can do is nil checking -love.test.graphics.getRendererInfo = function(test) - local name, version, vendor, device = love.graphics.getRendererInfo() - test:assertNotNil(name) - test:assertNotNil(version) - test:assertNotNil(vendor) - test:assertNotNil(device) -end - - --- love.graphics.getStats --- @NOTE cant really predict some of these so just nil check for most -love.test.graphics.getStats = function(test) - local stattypes = { - 'drawcalls', 'canvasswitches', 'texturememory', 'shaderswitches', - 'drawcallsbatched', 'textures', 'fonts' - } - local stats = love.graphics.getStats() - for s=1,#stattypes do - test:assertNotEquals(nil, stats[stattypes[s] ], 'expected a key for stat: ' .. stattypes[s]) - end -end - - --- love.graphics.getSupported -love.test.graphics.getSupported = function(test) - -- cant check values as hardware dependent but we can check the keys in the - -- table match what the documentation lists - local gfs = { - 'clampzero', 'lighten', 'glsl3', 'instancing', 'fullnpot', - 'pixelshaderhighp', 'shaderderivatives', 'indirectdraw', - 'copytexturetobuffer', 'multicanvasformats', - 'clampone', 'glsl4' - } - local features = love.graphics.getSupported() - for g=1,#gfs do - test:assertNotEquals(nil, features[gfs[g] ], 'expected a key for graphic feature: ' .. gfs[g]) - end -end - - --- love.graphics.getSystemLimits -love.test.graphics.getSystemLimits = function(test) - -- cant check values as hardware dependent but we can check the keys in the - -- table match what the documentation lists - local glimits = { - 'texelbuffersize', 'shaderstoragebuffersize', 'threadgroupsx', - 'threadgroupsy', 'pointsize', 'texturesize', 'texturelayers', 'volumetexturesize', - 'cubetexturesize', 'anisotropy', 'texturemsaa', 'multicanvas', 'threadgroupsz' - } - local limits = love.graphics.getSystemLimits() - for g=1,#glimits do - test:assertNotEquals(nil, limits[glimits[g] ], 'expected a key for system limit: ' .. glimits[g]) - end -end - - --- love.graphics.getTextureTypes -love.test.graphics.getTextureTypes = function(test) - -- cant check values as hardware dependent but we can check the keys in the - -- table match what the documentation lists - local ttypes = { - '2d', 'array', 'cube', 'volume' - } - local types = love.graphics.getTextureTypes() - for t=1,#ttypes do - test:assertNotEquals(nil, types[ttypes[t] ], 'expected a key for texture type: ' .. ttypes[t]) - end -end - - - -================================================ -File: tests/image.lua -================================================ --- love.image - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------OBJECTS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- CompressedImageData (love.image.newCompressedImageData) -love.test.image.CompressedImageData = function(test) - - -- create obj - local idata = love.image.newCompressedData('resources/love.dxt1') - test:assertObject(idata) - - -- check string + size - test:assertNotEquals(nil, idata:getString(), 'check data string') - test:assertEquals(2744, idata:getSize(), 'check data size') - - -- check img dimensions - local iw, ih = idata:getDimensions() - test:assertEquals(64, iw, 'check image dimension w') - test:assertEquals(64, ih, 'check image dimension h') - test:assertEquals(64, idata:getWidth(), 'check image direct w') - test:assertEquals(64, idata:getHeight(), 'check image direct h') - - -- check format - test:assertEquals('DXT1', idata:getFormat(), 'check image format') - - -- check mipmap count - test:assertEquals(7, idata:getMipmapCount(), 'check mipmap count') - - -- check linear - test:assertFalse(idata:isLinear(), 'check not linear') - idata:setLinear(true) - test:assertTrue(idata:isLinear(), 'check now linear') - -end - - --- ImageData (love.image.newImageData) -love.test.image.ImageData = function(test) - - -- create obj - local idata = love.image.newImageData('resources/love.png') - test:assertObject(idata) - - -- check string + size - test:assertNotEquals(nil, idata:getString(), 'check data string') - test:assertEquals(16384, idata:getSize(), 'check data size') - - -- check img dimensions - local iw, ih = idata:getDimensions() - test:assertEquals(64, iw, 'check image dimension w') - test:assertEquals(64, ih, 'check image dimension h') - test:assertEquals(64, idata:getWidth(), 'check image direct w') - test:assertEquals(64, idata:getHeight(), 'check image direct h') - - -- check format - test:assertEquals('rgba8', idata:getFormat(), 'check image format') - - -- manipulate image data so white heart is black - local mapdata = function(x, y, r, g, b, a) - if r == 1 and g == 1 and b == 1 then - r = 0; g = 0; b = 0 - end - return r, g, b, a - end - idata:mapPixel(mapdata, 0, 0, 64, 64) - local r1, g1, b1 = idata:getPixel(25, 25) - test:assertEquals(0, r1+g1+b1, 'check mapped black') - - -- map some other data into the idata - local idata2 = love.image.newImageData('resources/loveinv.png') - idata:paste(idata2, 0, 0, 0, 0) - r1, g1, b1 = idata:getPixel(25, 25) - test:assertEquals(3, r1+g1+b1, 'check back to white') - - -- set pixels directly - idata:setPixel(25, 25, 1, 0, 0, 1) - local r2, g2, b2 = idata:getPixel(25, 25) - test:assertEquals(1, r2+g2+b2, 'check set to red') - - -- check encoding to an image (png) - idata:encode('png', 'test-encode.png') - local read1 = love.filesystem.openFile('test-encode.png', 'r') - test:assertNotNil(read1) - love.filesystem.remove('test-encode.png') - - -- check encoding to an image (exr) - local edata = love.image.newImageData(100, 100, 'r16f') - edata:encode('exr', 'test-encode.exr') - local read2 = love.filesystem.openFile('test-encode.exr', 'r') - test:assertNotNil(read2) - love.filesystem.remove('test-encode.exr') - - -- check linear - test:assertFalse(idata:isLinear(), 'check not linear') - idata:setLinear(true) - test:assertTrue(idata:isLinear(), 'check now linear') - -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------METHODS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.image.isCompressed --- @NOTE really we need to test each of the files listed here: --- https://love2d.org/wiki/CompressedImageFormat --- also need to be platform dependent (e.g. dxt not suppored on phones) -love.test.image.isCompressed = function(test) - test:assertTrue(love.image.isCompressed('resources/love.dxt1'), - 'check dxt1 valid compressed image') -end - - --- love.image.newCompressedData --- @NOTE this is just basic nil checking, objs have their own test method -love.test.image.newCompressedData = function(test) - test:assertObject(love.image.newCompressedData('resources/love.dxt1')) -end - - --- love.image.newImageData --- @NOTE this is just basic nil checking, objs have their own test method -love.test.image.newImageData = function(test) - test:assertObject(love.image.newImageData('resources/love.png')) - test:assertObject(love.image.newImageData(16, 16, 'rgba8', nil)) -end - - - -================================================ -File: tests/joystick.lua -================================================ --- love.joystick --- @NOTE we can't test this module fully as it's hardware dependent --- however we can test methods do what is expected and can handle certain params - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------METHODS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.joystick.getGamepadMappingString -love.test.joystick.getGamepadMappingString = function(test) - local mapping = love.joystick.getGamepadMappingString('faker') - test:assertEquals(nil, mapping, 'check no mapping for fake gui') -end - - --- love.joystick.getJoystickCount -love.test.joystick.getJoystickCount = function(test) - local count = love.joystick.getJoystickCount() - test:assertGreaterEqual(0, count, 'check number') -end - - --- love.joystick.getJoysticks -love.test.joystick.getJoysticks = function(test) - local joysticks = love.joystick.getJoysticks() - test:assertGreaterEqual(0, #joysticks, 'check is count') -end - - --- love.joystick.loadGamepadMappings -love.test.joystick.loadGamepadMappings = function(test) - local ok, err = pcall(love.joystick.loadGamepadMappings, 'fakefile.txt') - test:assertEquals(false, ok, 'check invalid file') - love.joystick.loadGamepadMappings('resources/mappings.txt') -end - - --- love.joystick.saveGamepadMappings -love.test.joystick.saveGamepadMappings = function(test) - love.joystick.loadGamepadMappings('resources/mappings.txt') - local mapping = love.joystick.saveGamepadMappings() - test:assertGreaterEqual(0, #mapping, 'check something mapped') -end - - --- love.joystick.setGamepadMapping -love.test.joystick.setGamepadMapping = function(test) - local guid = '030000005e040000130b000011050000' - local mappings = { - love.joystick.setGamepadMapping(guid, 'a', 'button', 1, nil), - love.joystick.setGamepadMapping(guid, 'b', 'button', 2, nil), - love.joystick.setGamepadMapping(guid, 'x', 'button', 3, nil), - love.joystick.setGamepadMapping(guid, 'y', 'button', 4, nil), - love.joystick.setGamepadMapping(guid, 'back', 'button', 5, nil), - love.joystick.setGamepadMapping(guid, 'start', 'button', 6, nil), - love.joystick.setGamepadMapping(guid, 'guide', 'button', 7, nil), - love.joystick.setGamepadMapping(guid, 'leftstick', 'button', 8, nil), - love.joystick.setGamepadMapping(guid, 'leftshoulder', 'button', 9, nil), - love.joystick.setGamepadMapping(guid, 'rightstick', 'button', 10, nil), - love.joystick.setGamepadMapping(guid, 'rightshoulder', 'button', 11, nil), - love.joystick.setGamepadMapping(guid, 'dpup', 'button', 12, nil), - love.joystick.setGamepadMapping(guid, 'dpdown', 'button', 13, nil), - love.joystick.setGamepadMapping(guid, 'dpleft', 'button', 14, nil), - love.joystick.setGamepadMapping(guid, 'dpright', 'button', 15, nil), - love.joystick.setGamepadMapping(guid, 'dpup', 'button', 12, 'u'), - love.joystick.setGamepadMapping(guid, 'dpdown', 'button', 13, 'd'), - love.joystick.setGamepadMapping(guid, 'dpleft', 'button', 14, 'l'), - love.joystick.setGamepadMapping(guid, 'dpright', 'button', 15, 'r'), - love.joystick.setGamepadMapping(guid, 'dpup', 'hat', 12, 'lu'), - love.joystick.setGamepadMapping(guid, 'dpdown', 'hat', 13, 'ld'), - love.joystick.setGamepadMapping(guid, 'dpleft', 'hat', 14, 'ru'), - love.joystick.setGamepadMapping(guid, 'dpright', 'hat', 15, 'rd'), - love.joystick.setGamepadMapping(guid, 'leftstick', 'axis', 8, 'c') - } - for m=1,#mappings do - test:assertEquals(true, mappings[m], 'check mapping #' .. tostring(m)) - end -end - - - -================================================ -File: tests/keyboard.lua -================================================ --- love.keyboard --- @NOTE we can't test this module fully as it's hardware dependent --- however we can test methods do what is expected and can handle certain params - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------METHODS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.keyboard.getKeyFromScancode -love.test.keyboard.getKeyFromScancode = function(test) - test:assertEquals('function', type(love.keyboard.getKeyFromScancode)) -end - - --- love.keyboard.getScancodeFromKey -love.test.keyboard.getScancodeFromKey = function(test) - test:assertEquals('function', type(love.keyboard.getScancodeFromKey)) -end - - --- love.keyboard.hasKeyRepeat -love.test.keyboard.hasKeyRepeat = function(test) - local enabled = love.keyboard.hasKeyRepeat() - test:assertNotNil(enabled) -end - - --- love.keyboard.hasScreenKeyboard -love.test.keyboard.hasScreenKeyboard = function(test) - local enabled = love.keyboard.hasScreenKeyboard() - test:assertNotNil(enabled) -end - - --- love.keyboard.hasTextInput -love.test.keyboard.hasTextInput = function(test) - local enabled = love.keyboard.hasTextInput() - test:assertNotNil(enabled) -end - - --- love.keyboard.isDown -love.test.keyboard.isDown = function(test) - local keydown = love.keyboard.isDown('a') - test:assertNotNil(keydown) -end - - --- love.keyboard.isScancodeDown -love.test.keyboard.isScancodeDown = function(test) - local keydown = love.keyboard.isScancodeDown('a') - test:assertNotNil(keydown) -end - - --- love.keyboard.setKeyRepeat -love.test.keyboard.setKeyRepeat = function(test) - love.keyboard.setKeyRepeat(true) - local enabled = love.keyboard.hasKeyRepeat() - test:assertEquals(true, enabled, 'check key repeat set') -end - - --- love.keyboard.isModifierActive -love.test.keyboard.isModifierActive = function(test) - local active1 = love.keyboard.isModifierActive('numlock') - local active2 = love.keyboard.isModifierActive('capslock') - local active3 = love.keyboard.isModifierActive('scrolllock') - local active4 = love.keyboard.isModifierActive('mode') - test:assertNotNil(active1) - test:assertNotNil(active2) - test:assertNotNil(active3) - test:assertNotNil(active4) -end - - --- love.keyboard.setTextInput -love.test.keyboard.setTextInput = function(test) - love.keyboard.setTextInput(false) - test:assertEquals(false, love.keyboard.hasTextInput(), 'check disable text input') -end - - - -================================================ -File: tests/love.lua -================================================ --- love --- tests for the main love hooks + methods, mainly just that they exist - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------METHODS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.getVersion -love.test.love.getVersion = function(test) - local major, minor, revision, codename = love.getVersion() - test:assertGreaterEqual(0, major, 'check major is number') - test:assertGreaterEqual(0, minor, 'check minor is number') - test:assertGreaterEqual(0, revision, 'check revision is number') - test:assertTrue(codename ~= nil, 'check has codename') -end - - --- love.hasDeprecationOutput -love.test.love.hasDeprecationOutput = function(test) - local enabled = love.hasDeprecationOutput() - test:assertEquals(true, enabled, 'check enabled by default') -end - - --- love.isVersionCompatible -love.test.love.isVersionCompatible = function(test) - local major, minor, revision, _ = love.getVersion() - test:assertTrue(love.isVersionCompatible(major, minor, revision), 'check own version') -end - - --- love.setDeprecationOutput -love.test.love.setDeprecationOutput = function(test) - local enabled = love.hasDeprecationOutput() - test:assertEquals(true, enabled, 'check enabled by default') - love.setDeprecationOutput(false) - test:assertEquals(false, love.hasDeprecationOutput(), 'check disable') - love.setDeprecationOutput(true) -end - - --- love.errhand -love.test.love.errhand = function(test) - test:assertTrue(type(love.errhand) == 'function', 'check defined') -end - - --- love.run -love.test.love.run = function(test) - test:assertTrue(type(love.run) == 'function', 'check defined') -end - - - -================================================ -File: tests/math.lua -================================================ --- love.math - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------OBJECTS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- BezierCurve (love.math.newBezierCurve) -love.test.math.BezierCurve = function(test) - - -- create obj - local curve = love.math.newBezierCurve(1, 1, 2, 2, 3, 1) - local px, py = curve:getControlPoint(2) - test:assertObject(curve) - - -- check initial properties - test:assertCoords({2, 2}, {px, py}, 'check point x/y') - test:assertEquals(3, curve:getControlPointCount(), 'check 3 points') - test:assertEquals(2, curve:getDegree(), 'check degree is points-1') - - -- check some values on the curve - test:assertEquals(1, curve:evaluate(0), 'check curve evaluation 0') - test:assertRange(curve:evaluate(0.1), 1.2, 1.3, 'check curve evaluation 0.1') - test:assertRange(curve:evaluate(0.2), 1.4, 1.5, 'check curve evaluation 0.2') - test:assertRange(curve:evaluate(0.5), 2, 2.1, 'check curve evaluation 0.5') - test:assertEquals(3, curve:evaluate(1), 'check curve evaluation 1') - - -- check derivative - local deriv = curve:getDerivative() - test:assertObject(deriv) - test:assertEquals(2, deriv:getControlPointCount(), 'check deriv points') - test:assertRange(deriv:evaluate(0.1), 2, 2.1, 'check deriv evaluation 0.1') - - -- check segment - local segment = curve:getSegment(0, 0.5) - test:assertObject(segment) - test:assertEquals(3, segment:getControlPointCount(), 'check segment points') - test:assertRange(segment:evaluate(0.1), 1, 1.1, 'check segment evaluation 0.1') - - -- mess with control points - curve:removeControlPoint(2) - curve:insertControlPoint(4, 1, -1) - curve:insertControlPoint(5, 3, -1) - curve:insertControlPoint(6, 2, -1) - curve:setControlPoint(2, 3, 2) - test:assertEquals(5, curve:getControlPointCount(), 'check 3 points still') - local px1, py1 = curve:getControlPoint(1) - local px2, py2 = curve:getControlPoint(3) - local px3, py3 = curve:getControlPoint(5) - test:assertCoords({1, 1}, {px1, py1}, 'check modified point 1') - test:assertCoords({5, 3}, {px2, py2}, 'check modified point 1') - test:assertCoords({3, 1}, {px3, py3}, 'check modified point 1') - - -- check render lists - local coords1 = curve:render(5) - local coords2 = curve:renderSegment(0, 0.1, 5) - test:assertEquals(196, #coords1, 'check coords') - test:assertEquals(20, #coords2, 'check segment coords') - - -- check translation values - px, py = curve:getControlPoint(2) - test:assertCoords({3, 2}, {px, py}, 'check pretransform x/y') - curve:rotate(90 * (math.pi/180), 0, 0) - px, py = curve:getControlPoint(2) - test:assertCoords({-2, 3}, {px, py}, 'check rotated x/y') - curve:scale(2, 0, 0) - px, py = curve:getControlPoint(2) - test:assertCoords({-4, 6}, {px, py}, 'check scaled x/y') - curve:translate(5, -5) - px, py = curve:getControlPoint(2) - test:assertCoords({1, 1}, {px, py}, 'check translated x/y') - -end - - --- RandomGenerator (love.math.RandomGenerator) --- @NOTE as this checks random numbers the chances this fails is very unlikely, but not 0... --- if you've managed to proc it congrats! your prize is to rerun the testsuite again -love.test.math.RandomGenerator = function(test) - - -- create object - local rng1 = love.math.newRandomGenerator(3418323524, 20529293) - test:assertObject(rng1) - - -- check set properties - local low, high = rng1:getSeed() - test:assertEquals(3418323524, low, 'check seed low') - test:assertEquals(20529293, high, 'check seed high') - - -- check states - local rng2 = love.math.newRandomGenerator(1448323524, 10329293) - test:assertNotEquals(rng1:random(), rng2:random(), 'check not matching states') - test:assertNotEquals(rng1:randomNormal(), rng2:randomNormal(), 'check not matching states') - - -- check setting state works - rng2:setState(rng1:getState()) - test:assertEquals(rng1:random(), rng2:random(), 'check now matching') - - -- check overwriting seed works, should change output - rng1:setSeed(os.time()) - test:assertNotEquals(rng1:random(), rng2:random(), 'check not matching states') - test:assertNotEquals(rng1:randomNormal(), rng2:randomNormal(), 'check not matching states') - -end - - --- Transform (love.math.Transform) -love.test.math.Transform = function(test) - - -- create obj - local transform = love.math.newTransform(0, 0, 0, 1, 1, 0, 0, 0, 0) - test:assertObject(transform) - - -- set some values and check the matrix and transformPoint values - transform:translate(10, 8) - transform:scale(2, 3) - transform:rotate(90*(math.pi/180)) - transform:shear(1, 2) - local px, py = transform:transformPoint(1, 1) - test:assertCoords({4, 14}, {px, py}, 'check transformation methods') - transform:reset() - px, py = transform:transformPoint(1, 1) - test:assertCoords({1, 1}, {px, py}, 'check reset') - - -- apply a transform to another transform - local transform2 = love.math.newTransform() - transform2:translate(5, 3) - transform:apply(transform2) - px, py = transform:transformPoint(1, 1) - test:assertCoords({6, 4}, {px, py}, 'check apply other transform') - - -- check cloning a transform - local transform3 = transform:clone() - px, py = transform3:transformPoint(1, 1) - test:assertCoords({6, 4}, {px, py}, 'check clone transform') - - -- check inverse and inverseTransform - transform:reset() - transform:translate(-14, 6) - local ipx, ipy = transform:inverseTransformPoint(0, 0) - transform:inverse() - px, py = transform:transformPoint(0, 0) - test:assertCoords({-px, -py}, {ipx, ipy}, 'check inverse points transform') - - -- check matrix manipulation - transform:setTransformation(0, 0, 0, 1, 1, 0, 0, 0, 0) - transform:translate(4, 4) - local m1, m2, m3, m4, m5, m6, m7, m8, - m9, m10, m11, m12, m13, m14, m15, m16 = transform:getMatrix() - test:assertEquals(4, m4, 'check translate matrix x') - test:assertEquals(4, m8, 'check translate matrix y') - transform:setMatrix(m1, m2, m3, 3, m5, m6, m7, 1, m9, m10, m11, m12, m13, m14, m15, m16) - px, py = transform:transformPoint(1, 1) - test:assertCoords({4, 2}, {px, py}, 'check set matrix') - - -- check affine vs non affine - transform:reset() - test:assertTrue(transform:isAffine2DTransform(), 'check affine 1') - transform:translate(4, 3) - test:assertTrue(transform:isAffine2DTransform(), 'check affine 2') - transform:setMatrix(1, 3, 4, 5.5, 1, 4.5, 2, 1, 3.4, 5.1, 4.1, 13, 1, 1, 2, 3) - test:assertFalse(transform:isAffine2DTransform(), 'check not affine') - -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------METHODS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.math.colorFromBytes -love.test.math.colorFromBytes = function(test) - -- check random value - local r1, g1, b1, a1 = love.math.colorFromBytes(51, 51, 51, 51) - test:assertEquals(r1, 0.2, 'check r from bytes') - test:assertEquals(g1, 0.2, 'check g from bytes') - test:assertEquals(b1, 0.2, 'check b from bytes') - test:assertEquals(a1, 0.2, 'check a from bytes') - -- check "max" value - local r2, g2, b2, a2 = love.math.colorFromBytes(255, 255, 255, 255) - test:assertEquals(r2, 1, 'check r from bytes') - test:assertEquals(g2, 1, 'check g from bytes') - test:assertEquals(b2, 1, 'check b from bytes') - test:assertEquals(a2, 1, 'check a from bytes') - -- check "min" value - local r3, g3, b3, a3 = love.math.colorFromBytes(0, 0, 0, 0) - test:assertEquals(r3, 0, 'check r from bytes') - test:assertEquals(g3, 0, 'check g from bytes') - test:assertEquals(b3, 0, 'check b from bytes') - test:assertEquals(a3, 0, 'check a from bytes') -end - - --- love.math.colorToBytes -love.test.math.colorToBytes = function(test) - -- check random value - local r1, g1, b1, a1 = love.math.colorToBytes(0.2, 0.2, 0.2, 0.2) - test:assertEquals(r1, 51, 'check bytes from r') - test:assertEquals(g1, 51, 'check bytes from g') - test:assertEquals(b1, 51, 'check bytes from b') - test:assertEquals(a1, 51, 'check bytes from a') - -- check "max" value - local r2, g2, b2, a2 = love.math.colorToBytes(1, 1, 1, 1) - test:assertEquals(r2, 255, 'check bytes from r') - test:assertEquals(g2, 255, 'check bytes from g') - test:assertEquals(b2, 255, 'check bytes from b') - test:assertEquals(a2, 255, 'check bytes from a') - -- check "min" value - local r3, g3, b3, a3 = love.math.colorToBytes(0, 0, 0, 0) - test:assertEquals(r3, 0, 'check bytes from r') - test:assertEquals(g3, 0, 'check bytes from g') - test:assertEquals(b3, 0, 'check bytes from b') - test:assertEquals(a3, 0, 'check bytes from a') -end - - --- love.math.gammaToLinear --- @NOTE I tried doing the same formula as the source from MathModule.cpp --- but get test failues due to slight differences -love.test.math.gammaToLinear = function(test) - local lr, lg, lb = love.math.gammaToLinear(1, 0.8, 0.02) - --local eg = ((0.8 + 0.055) / 1.055)^2.4 - --local eb = 0.02 / 12.92 - test:assertGreaterEqual(0, lr, 'check gamma r to linear') - test:assertGreaterEqual(0, lg, 'check gamma g to linear') - test:assertGreaterEqual(0, lb, 'check gamma b to linear') -end - - --- love.math.getRandomSeed --- @NOTE whenever i run this high is always 0, is that intended? -love.test.math.getRandomSeed = function(test) - local low, high = love.math.getRandomSeed() - test:assertGreaterEqual(0, low, 'check random seed low') - test:assertGreaterEqual(0, high, 'check random seed high') -end - - --- love.math.getRandomState -love.test.math.getRandomState = function(test) - test:assertNotNil(love.math.getRandomState()) -end - - --- love.math.isConvex -love.test.math.isConvex = function(test) - local isconvex = love.math.isConvex({0, 0, 1, 0, 1, 1, 1, 0, 0, 0}) -- square - local notconvex = love.math.isConvex({1, 2, 2, 4, 3, 4, 2, 1, 3, 1}) -- weird shape - test:assertTrue(isconvex, 'check polygon convex') - test:assertFalse(notconvex, 'check polygon not convex') -end - - --- love.math.linearToGammer --- @NOTE I tried doing the same formula as the source from MathModule.cpp --- but get test failues due to slight differences -love.test.math.linearToGamma = function(test) - local gr, gg, gb = love.math.linearToGamma(1, 0.8, 0.001) - --local eg = 1.055 * (0.8^1/2.4) - 0.055 - --local eb = 0.001 * 12.92 - test:assertGreaterEqual(0, gr, 'check linear r to gamme') - test:assertGreaterEqual(0, gg, 'check linear g to gamme') - test:assertGreaterEqual(0, gb, 'check linear b to gamme') -end - - --- love.math.newBezierCurve --- @NOTE this is just basic nil checking, objs have their own test method -love.test.math.newBezierCurve = function(test) - test:assertObject(love.math.newBezierCurve({0, 0, 0, 1, 1, 1, 2, 1})) -end - - --- love.math.newRandomGenerator --- @NOTE this is just basic nil checking, objs have their own test method -love.test.math.newRandomGenerator = function(test) - test:assertObject(love.math.newRandomGenerator()) -end - - --- love.math.newTransform --- @NOTE this is just basic nil checking, objs have their own test method -love.test.math.newTransform = function(test) - test:assertObject(love.math.newTransform()) -end - - --- love.math.perlinNoise -love.test.math.perlinNoise = function(test) - -- check some noise values - -- output should be consistent if given the same input - local noise1 = love.math.perlinNoise(100) - local noise2 = love.math.perlinNoise(1, 10) - local noise3 = love.math.perlinNoise(1043, 31.123, 999) - local noise4 = love.math.perlinNoise(99.222, 10067, 8, 1843) - test:assertRange(noise1, 0.5, 0.51, 'check noise 1 dimension') - test:assertRange(noise2, 0.5, 0.51, 'check noise 2 dimensions') - test:assertRange(noise3, 0.56, 0.57, 'check noise 3 dimensions') - test:assertRange(noise4, 0.52, 0.53, 'check noise 4 dimensions') -end - - --- love.math.simplexNoise -love.test.math.simplexNoise = function(test) - -- check some noise values - -- output should be consistent if given the same input - local noise1 = love.math.simplexNoise(100) - local noise2 = love.math.simplexNoise(1, 10) - local noise3 = love.math.simplexNoise(1043, 31.123, 999) - local noise4 = love.math.simplexNoise(99.222, 10067, 8, 1843) - -- rounded to avoid floating point issues - test:assertRange(noise1, 0.5, 0.51, 'check noise 1 dimension') - test:assertRange(noise2, 0.47, 0.48, 'check noise 2 dimensions') - test:assertRange(noise3, 0.26, 0.27, 'check noise 3 dimensions') - test:assertRange(noise4, 0.53, 0.54, 'check noise 4 dimensions') -end - - --- love.math.random -love.test.math.random = function(test) - -- check some random ranges - love.math.setRandomSeed(123) - test:assertRange(love.math.random(), 0.37068322251462, 0.37068322251464, "check random algorithm") - test:assertEquals(love.math.random(10), 4, "check single random param") - test:assertEquals(love.math.random(15, 100), 92, "check two random params") -end - - --- love.math.randomNormal -love.test.math.randomNormal = function(test) - love.math.setRandomSeed(1234) - test:assertRange(love.math.randomNormal(1, 2), 1.0813614997253, 1.0813614997255, 'check randomNormal two params') -end - - --- love.math.setRandomSeed --- @NOTE same with getRandomSeed, high is always 0 when I tested it? -love.test.math.setRandomSeed = function(test) - love.math.setRandomSeed(9001) - local low, high = love.math.getRandomSeed() - test:assertEquals(9001, low, 'check seed low set') - test:assertEquals(0, high, 'check seed high set') -end - - --- love.math.setRandomState -love.test.math.setRandomState = function(test) - -- check setting state matches value returned - local rs1 = love.math.getRandomState() - love.math.setRandomState(rs1) - local rs2 = love.math.getRandomState() - test:assertEquals(rs1, rs2, 'check random state set') -end - - --- love.math.triangulate -love.test.math.triangulate = function(test) - local triangles1 = love.math.triangulate({0, 0, 1, 0, 1, 1, 1, 0, 0, 0}) -- square - local triangles2 = love.math.triangulate({1, 2, 2, 4, 3, 4, 2, 1, 3, 1}) -- weird shape - test:assertEquals(3, #triangles1, 'check polygon triangles') - test:assertEquals(3, #triangles2, 'check polygon triangles') -end - - - -================================================ -File: tests/mouse.lua -================================================ --- love.mouse --- @NOTE we can't test this module fully as it's hardware dependent --- however we can test methods do what is expected and can handle certain params - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------METHODS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.mouse.getCursor -love.test.mouse.getCursor = function(test) - local cursor = love.mouse.getCursor() - test:assertEquals(nil, cursor, 'check nil initially') - -- try setting a cursor to check return if supported - if love.mouse.isCursorSupported() then - love.mouse.setCursor(love.mouse.getSystemCursor("hand")) - local newcursor = love.mouse.getCursor() - test:assertObject(newcursor) - love.mouse.setCursor() - end -end - - --- love.mouse.getPosition -love.test.mouse.getPosition = function(test) - love.mouse.setPosition(0, 0) -- cant predict - local x, y = love.mouse.getPosition() - test:assertEquals(0, x, 'check x pos') - test:assertEquals(0, y, 'check y pos') -end - - --- love.mouse.getRelativeMode -love.test.mouse.getRelativeMode = function(test) - local enabled = love.mouse.getRelativeMode() - test:assertEquals(false, enabled, 'check relative mode') - love.mouse.setRelativeMode(true) - test:assertEquals(true, love.mouse.getRelativeMode(), 'check enabling') -end - - --- love.mouse.getSystemCursor -love.test.mouse.getSystemCursor = function(test) - local hand = love.mouse.getSystemCursor('hand') - test:assertObject(hand) - local ok, err = pcall(love.mouse.getSystemCursor, 'love') - test:assertEquals(false, ok, 'check invalid cursor') -end - - --- love.mouse.getX -love.test.mouse.getX = function(test) - love.mouse.setPosition(0, 0) -- cant predict - local x = love.mouse.getX() - test:assertEquals(0, x, 'check x pos') - love.mouse.setX(10) - test:assertEquals(10, love.mouse.getX(), 'check set x') -end - - --- love.mouse.getY -love.test.mouse.getY = function(test) - love.mouse.setPosition(0, 0) -- cant predict - local y = love.mouse.getY() - test:assertEquals(0, y, 'check x pos') - love.mouse.setY(10) - test:assertEquals(10, love.mouse.getY(), 'check set y') -end - - --- love.mouse.isCursorSupported -love.test.mouse.isCursorSupported = function(test) - test:assertNotNil(love.mouse.isCursorSupported()) -end - - --- love.mouse.isDown -love.test.mouse.isDown = function(test) - test:assertNotNil(love.mouse.isDown()) -end - - --- love.mouse.isGrabbed -love.test.mouse.isGrabbed = function(test) - test:assertNotNil(love.mouse.isGrabbed()) -end - - --- love.mouse.isVisible -love.test.mouse.isVisible = function(test) - local visible = love.mouse.isVisible() - test:assertEquals(true, visible, 'check visible default') - love.mouse.setVisible(false) - test:assertEquals(false, love.mouse.isVisible(), 'check invisible') - love.mouse.setVisible(true) -end - - --- love.mouse.newCursor -love.test.mouse.newCursor = function(test) - -- new cursor might fail if not supported - if love.mouse.isCursorSupported() then - local cursor = love.mouse.newCursor('resources/love.png', 0, 0) - test:assertObject(cursor) - else - test:skipTest('cursor not supported on this system') - end -end - - --- love.mouse.setCursor -love.test.mouse.setCursor = function(test) - -- cant set cursor if not supported - if love.mouse.isCursorSupported() then - love.mouse.setCursor() - test:assertEquals(nil, love.mouse.getCursor(), 'check reset') - love.mouse.setCursor(love.mouse.getSystemCursor('hand')) - test:assertObject(love.mouse.getCursor()) - else - test:skipTest('cursor not supported on this system') - end -end - - --- love.mouse.setGrabbed --- @NOTE can fail if you move the mouse a bunch while the test runs -love.test.mouse.setGrabbed = function(test) - test:assertEquals(false, love.mouse.isGrabbed(), 'check not grabbed') - love.mouse.setGrabbed(true) - test:assertEquals(true, love.mouse.isGrabbed(), 'check now grabbed') - love.mouse.setGrabbed(false) -end - - --- love.mouse.setPosition -love.test.mouse.setPosition = function(test) - love.mouse.setPosition(10, 10) - local x, y = love.mouse.getPosition() - test:assertEquals(10, x, 'check x position') - test:assertEquals(10, y, 'check y position') - love.mouse.setPosition(15, 20) - local x2, y2 = love.mouse.getPosition() - test:assertEquals(15, x2, 'check new x position') - test:assertEquals(20, y2, 'check new y position') -end - - --- love.mouse.setRelativeMode -love.test.mouse.setRelativeMode = function(test) - love.mouse.setRelativeMode(true) - local enabled = love.mouse.getRelativeMode() - test:assertEquals(true, enabled, 'check relative mode') - love.mouse.setRelativeMode(false) - test:assertEquals(false, love.mouse.getRelativeMode(), 'check disabling') -end - - --- love.mouse.setVisible -love.test.mouse.setVisible = function(test) - local visible = love.mouse.isVisible() - test:assertEquals(true, visible, 'check visible default') - love.mouse.setVisible(false) - test:assertEquals(false, love.mouse.isVisible(), 'check invisible') - love.mouse.setVisible(true) -end - - --- love.mouse.setX -love.test.mouse.setX = function(test) - love.mouse.setX(30) - local x = love.mouse.getX() - test:assertEquals(30, x, 'check x pos') - love.mouse.setX(10) - test:assertEquals(10, love.mouse.getX(), 'check set x') -end - - --- love.mouse.setY -love.test.mouse.setY = function(test) - love.mouse.setY(12) - local y = love.mouse.getY() - test:assertEquals(12, y, 'check x pos') - love.mouse.setY(10) - test:assertEquals(10, love.mouse.getY(), 'check set y') -end - - - -================================================ -File: tests/physics.lua -================================================ --- love.physics - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -----------------------------------OBJECTS--------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- Body (love.physics.newBody) -love.test.physics.Body = function(test) - - -- create body - local world = love.physics.newWorld(1, 1, true) - local body1 = love.physics.newBody(world, 0, 0, 'static') - local body2 = love.physics.newBody(world, 30, 30, 'dynamic') - love.physics.newRectangleShape(body1, 5, 5, 10, 10) - love.physics.newRectangleShape(body2, 5, 5, 10, 10) - test:assertObject(body1) - - -- check shapes - test:assertEquals(1, #body1:getShapes(), 'check shapes total 1') - test:assertEquals(1, #body2:getShapes(), 'check shapes total 2') - test:assertNotEquals(nil, body1:getShape(), 'check shape 1') - test:assertNotEquals(nil, body2:getShape(), 'check shape 2') - - -- check body active - test:assertTrue(body1:isActive(), 'check active by def') - - -- check body bullet - test:assertFalse(body1:isBullet(), 'check not bullet by def') - body1:setBullet(true) - test:assertTrue(body1:isBullet(), 'check set bullet') - - -- check fixed rotation - test:assertFalse(body1:isFixedRotation(), 'check fix rot def') - body1:setFixedRotation(true) - test:assertTrue(body1:isFixedRotation(), 'check set fix rot') - - -- check sleeping/waking - test:assertTrue(body1:isSleepingAllowed(), 'check sleep def') - body1:setSleepingAllowed(false) - test:assertFalse(body1:isSleepingAllowed(), 'check set sleep') - body1:setSleepingAllowed(true) - world:update(1) - test:assertFalse(body1:isAwake(), 'check fell asleep') - body1:setSleepingAllowed(false) - body1:setType('dynamic') - test:assertTrue(body1:isAwake(), 'check waking up') - - -- check touching - test:assertFalse(body1:isTouching(body2)) - body2:setPosition(5, 5) - world:update(1) - test:assertTrue(body1:isTouching(body2)) - - -- check children lists - test:assertEquals(1, #body1:getContacts(), 'check contact list') - test:assertEquals(0, #body1:getJoints(), 'check joints list') - love.physics.newDistanceJoint(body1, body2, 5, 5, 10, 10, true) - test:assertEquals(1, #body1:getJoints(), 'check joints list') - - -- check local points - local x, y = body1:getLocalCenter() - test:assertRange(x, 5, 6, 'check local center x') - test:assertRange(y, 5, 6, 'check local center y') - local lx, ly = body1:getLocalPoint(10, 10) - test:assertRange(lx, 10, 11, 'check local point x') - test:assertRange(ly, 9, 10, 'check local point y') - local lx1, ly1, lx2, ly2 = body1:getLocalPoints(0, 5, 5, 10) - test:assertRange(lx1, 0, 1, 'check local points x 1') - test:assertRange(ly1, 3, 4, 'check local points y 1') - test:assertRange(lx2, 5, 6, 'check local points x 2') - test:assertRange(ly2, 9, 10, 'check local points y 2') - - -- check world points - local wx, wy = body1:getWorldPoint(10.4, 9) - test:assertRange(wx, 10, 11, 'check world point x') - test:assertRange(wy, 10, 11, 'check world point y') - local wx1, wy1, wx2, wy2 = body1:getWorldPoints(0.4, 4, 5.4, 9) - test:assertRange(wx1, 0, 1, 'check world points x 1') - test:assertRange(wy1, 5, 6, 'check world points y 1') - test:assertRange(wx2, 5, 6, 'check world points x 2') - test:assertRange(wy2, 10, 11, 'check world points y 2') - - -- check angular damping + velocity - test:assertEquals(0, body1:getAngularDamping(), 'check angular damping') - test:assertEquals(0, body1:getAngularVelocity(), 'check angular velocity') - - -- check world props - test:assertObject(body1:getWorld()) - test:assertEquals(2, body1:getWorld():getBodyCount(), 'check world count') - local cx, cy = body1:getWorldCenter() - test:assertRange(cx, 4, 5, 'check world center x') - test:assertRange(cy, 6, 7, 'check world center y') - local vx, vy = body1:getWorldVector(5, 10) - test:assertEquals(5, vx, 'check vector x') - test:assertEquals(10, vy, 'check vector y') - - -- check inertia - test:assertRange(body1:getInertia(), 5, 6, 'check inertia') - - -- check angle - test:assertEquals(0, body1:getAngle(), 'check def angle') - body1:setAngle(90 * (math.pi/180)) - test:assertEquals(math.floor(math.pi/2*100), math.floor(body1:getAngle()*100), 'check set angle') - - -- check gravity scale - test:assertEquals(1, body1:getGravityScale(), 'check def grav') - body1:setGravityScale(2) - test:assertEquals(2, body1:getGravityScale(), 'check change grav') - - -- check damping - test:assertEquals(0, body1:getLinearDamping(), 'check def lin damping') - body1:setLinearDamping(0.1) - test:assertRange(body1:getLinearDamping(), 0, 0.2, 'check change lin damping') - - -- check velocity - local x2, y2 = body1:getLinearVelocity() - test:assertEquals(1, x2, 'check def lin velocity x') - test:assertEquals(1, y2, 'check def lin velocity y') - body1:setLinearVelocity(4, 5) - local x3, y3 = body1:getLinearVelocity() - test:assertEquals(4, x3, 'check change lin velocity x') - test:assertEquals(5, y3, 'check change lin velocity y') - - -- check mass - test:assertRange(body1:getMass(), 0.1, 0.2, 'check def mass') - body1:setMass(10) - test:assertEquals(10, body1:getMass(), 'check change mass') - body1:setMassData(3, 5, 10, 1) - local x4, y4, mass4, inertia4 = body1:getMassData() - test:assertEquals(3, x4, 'check mass data change x') - test:assertEquals(5, y4, 'check mass data change y') - test:assertEquals(10, mass4, 'check mass data change mass') - test:assertRange(inertia4, 340, 341, 'check mass data change inertia') - body1:resetMassData() - local x5, y5, mass5, inertia5 = body1:getMassData() - test:assertRange(x5, 5, 6, 'check mass data reset x') - test:assertRange(y5, 5, 6, 'check mass data reset y') - test:assertRange(mass5, 0.1, 0.2, 'check mass data reset mass') - test:assertRange(inertia5, 5, 6, 'check mass data reset inertia') - test:assertFalse(body1:hasCustomMassData()) - - -- check position - local x6, y6 = body1:getPosition() - test:assertRange(x6, -1, 0, 'check position x') - test:assertRange(y6, 0, 1, 'check position y') - body1:setPosition(10, 4) - local x7, y7 = body1:getPosition() - test:assertEquals(x7, 10, 'check set position x') - test:assertEquals(y7, 4, 'check set position y') - - -- check type - test:assertEquals('dynamic', body1:getType(), 'check type match') - body1:setType('kinematic') - body1:setType('static') - test:assertEquals('static', body1:getType(), 'check type change') - - -- check userdata - test:assertEquals(nil, body1:getUserData(), 'check user data') - body1:setUserData({ love = 'cool' }) - test:assertEquals('cool', body1:getUserData().love, 'check set user data') - - -- check x/y direct - test:assertEquals(10, math.floor(body1:getX()), 'check get x') - test:assertEquals(4, math.floor(body1:getY()), 'check get y') - body1:setX(0) - body1:setY(0) - test:assertEquals(0, body1:getX(), 'check get x') - test:assertEquals(0, body1:getY(), 'check get y') - - -- apply angular impulse - local vel = body2:getAngularVelocity() - test:assertRange(vel, 0, 0, 'check velocity before') - body2:applyAngularImpulse(10) - local vel1 = body2:getAngularVelocity() - test:assertRange(vel1, 5, 6, 'check velocity after 1') - - -- apply standard force - local ang1 = body2:getAngle() - test:assertRange(ang1, 0.1, 0.2, 'check initial angle 1') - body2:applyForce(2, 3) - world:update(2) - local vel2 = body2:getAngularVelocity() - local ang2 = body2:getAngle() - test:assertRange(ang2, -0.1, 0, 'check angle after 2') - test:assertRange(vel2, 1, 2, 'check velocity after 2') - - -- apply linear impulse - body2:applyLinearImpulse(-4, -59) - world:update(1) - local ang3 = body2:getAngle() - local vel3 = body2:getAngularVelocity() - test:assertRange(ang3, -2, -1, 'check angle after 3') - test:assertRange(vel3, 0, 1, 'check velocity after 3') - - -- apply torque - body2:applyTorque(4) - world:update(2) - local ang4 = body2:getAngle() - local vel4 = body2:getAngularVelocity() - test:assertRange(ang4, -1, 0, 'check angle after 4') - test:assertRange(vel4, 0, 1, 'check velocity after 4') - - -- check destroy - test:assertFalse(body1:isDestroyed(), 'check not destroyed') - body1:destroy() - test:assertTrue(body1:isDestroyed(), 'check destroyed') - -end - - --- Contact (love.physics.World:getContacts) -love.test.physics.Contact = function(test) - - -- create a setup so we can access some contact objects - local world = love.physics.newWorld(1, 1, true) - local body1 = love.physics.newBody(world, 0, 0, 'dynamic') - local body2 = love.physics.newBody(world, 10, 10, 'dynamic') - local rectangle1 = love.physics.newRectangleShape(body1, 0, 0, 10, 10) - local rectangle2 = love.physics.newRectangleShape(body2, 0, 0, 10, 10) - rectangle1:setUserData('rec1') - rectangle2:setUserData('rec2') - - -- used to check for collisions + no. of collisions - local collided = false - local pass = 1 - - -- set callback for contact start - world:setCallbacks( - function(shape_a, shape_b, contact) - collided = true - - -- check contact object - test:assertObject(contact) - - -- check child indices - local indexA, indexB = contact:getChildren() - test:assertEquals(1, indexA, 'check child indice a') - test:assertEquals(1, indexB, 'check child indice b') - - -- check shapes match using userdata - local shapeA, shapeB = contact:getShapes() - test:assertEquals(shape_a:getUserData(), shapeA:getUserData(), 'check shape a matches') - test:assertEquals(shape_b:getUserData(), shapeB:getUserData(), 'check shape b matches') - - -- check normal pos - local nx, ny = contact:getNormal() - test:assertEquals(1, nx, 'check normal x') - test:assertEquals(0, ny, 'check normal y') - - -- check actual pos - local px1, py1, px2, py2 = contact:getPositions() - test:assertRange(px1, 5, 6, 'check collide x 1') - test:assertRange(py1, 5, 6, 'check collide y 1') - test:assertRange(px2, 5, 6, 'check collide x 2') - test:assertRange(py2, 5, 6, 'check collide y 2') - - -- check touching - test:assertTrue(contact:isTouching(), 'check touching') - - -- check enabled (we pass through twice to test on/off) - test:assertEquals(pass == 1, contact:isEnabled(), 'check enabled for pass ' .. tostring(pass)) - - -- check friction - test:assertRange(contact:getFriction(), 0.2, 0.3, 'check def friction') - contact:setFriction(0.1) - test:assertRange(contact:getFriction(), 0.1, 0.2, 'check set friction') - contact:resetFriction() - test:assertRange(contact:getFriction(), 0.2, 0.3, 'check reset friction') - - -- check restitution - test:assertEquals(0, contact:getRestitution(), 'check def restitution') - contact:setRestitution(1) - test:assertEquals(1, contact:getRestitution(), 'check set restitution') - contact:resetRestitution() - test:assertEquals(0, contact:getRestitution(), 'check reset restitution') - pass = pass + 1 - - end, function() end, function(shape_a, shape_b, contact) - if pass > 2 then - contact:setEnabled(false) - end - end, function() end - ) - - -- check bodies collided - world:update(1) - test:assertTrue(collided, 'check bodies collided') - - -- update again for enabled check - world:update(1) - test:assertEquals(2, pass, 'check ran twice') - -end - - --- Joint (love.physics.newDistanceJoint) -love.test.physics.Joint = function(test) - - -- make joint - local world = love.physics.newWorld(1, 1, true) - local body1 = love.physics.newBody(world, 10, 10, 'dynamic') - local body2 = love.physics.newBody(world, 20, 20, 'dynamic') - local joint = love.physics.newDistanceJoint(body1, body2, 10, 10, 20, 20, true) - test:assertObject(joint) - - -- check type - test:assertEquals('distance', joint:getType(), 'check joint type') - - -- check not destroyed - test:assertFalse(joint:isDestroyed(), 'check not destroyed') - - - -- check reaction props - world:update(1) - local rx1, ry1 = joint:getReactionForce(1) - test:assertEquals(0, rx1, 'check reaction force x') - test:assertEquals(0, ry1, 'check reaction force y') - local rx2, ry2 = joint:getReactionTorque(1) - test:assertEquals(0, rx2, 'check reaction torque x') - test:assertEquals(nil, ry2, 'check reaction torque y') - - -- check body pointer - local b1, b2 = joint:getBodies() - test:assertEquals(body1:getX(), b1:getX(), 'check body 1') - test:assertEquals(body2:getX(), b2:getX(), 'check body 2') - - -- check joint anchors - local x1, y1, x2, y2 = joint:getAnchors() - test:assertRange(x1, 10, 11, 'check anchor x1') - test:assertRange(y1, 10, 11, 'check anchor y1') - test:assertRange(x2, 20, 21, 'check anchor x2') - test:assertRange(y2, 20, 21, 'check anchor y2') - test:assertTrue(joint:getCollideConnected(), 'check not colliding') - - -- test userdata - test:assertEquals(nil, joint:getUserData(), 'check no data by def') - joint:setUserData('hello') - test:assertEquals('hello', joint:getUserData(), 'check set userdata') - - -- destroy - joint:destroy() - test:assertTrue(joint:isDestroyed(), 'check destroyed') - -end - - --- Shape (love.physics.newCircleShape) --- @NOTE in 12.0 fixtures have been merged into shapes -love.test.physics.Shape = function(test) - - -- create shape - local world = love.physics.newWorld(0, 0, false) - local body1 = love.physics.newBody(world, 0, 0, 'dynamic') - local shape1 = love.physics.newRectangleShape(body1, 5, 5, 10, 10) - test:assertObject(shape1) - - -- check child count - test:assertEquals(1, shape1:getChildCount(), 'check child count') - - -- check radius - test:assertRange(shape1:getRadius(), 0, 0.4, 'check radius') - - -- check type match - test:assertEquals('polygon', shape1:getType(), 'check rectangle type') - - -- check body pointer - test:assertEquals(0, shape1:getBody():getX(), 'check body link') - - -- check category - test:assertEquals(1, shape1:getCategory(), 'check def category') - shape1:setCategory(3, 5, 6) - local categories = {shape1:getCategory()} - test:assertEquals(14, categories[1] + categories[2] + categories[3], 'check set category') - - -- check sensor prop - test:assertFalse(shape1:isSensor(), 'check sensor def') - shape1:setSensor(true) - test:assertTrue(shape1:isSensor(), 'check set sensor') - shape1:setSensor(false) - - -- check not destroyed - test:assertFalse(shape1:isDestroyed(), 'check not destroyed') - - -- check user data - test:assertEquals(nil, shape1:getUserData(), 'check no user data') - shape1:setUserData({ test = 14 }) - test:assertEquals(14, shape1:getUserData().test, 'check user data set') - - -- check bounding box - -- polygons have an additional skin radius to help with collisions - -- so this wont be 0, 0, 10, 10 as you'd think but has an additional 0.3 padding - local topLeftX, topLeftY, bottomRightX, bottomRightY = shape1:computeAABB(0, 0, 0, 1) - local tlx, tly, brx, bry = shape1:getBoundingBox(1) - test:assertEquals(topLeftX, tlx, 'check bbox methods match tlx') - test:assertEquals(topLeftY, tly, 'check bbox methods match tly') - test:assertEquals(bottomRightX, brx, 'check bbox methods match brx') - test:assertEquals(bottomRightY, bry, 'check bbox methods match bry') - test:assertEquals(topLeftX, topLeftY, 'check bbox tl 1') - test:assertRange(topLeftY, -0.3, -0.2, 'check bbox tl 2') - test:assertEquals(bottomRightX, bottomRightY, 'check bbox br 1') - test:assertRange(bottomRightX, 10.3, 10.4, 'check bbox br 2') - - -- check density - test:assertEquals(1, shape1:getDensity(), 'check def density') - shape1:setDensity(5) - test:assertEquals(5, shape1:getDensity(), 'check set density') - - -- check mass - local x1, y1, mass1, inertia1 = shape1:getMassData() - test:assertRange(x1, 5, 5.1, 'check shape mass pos x') - test:assertRange(y1, 5, 5.1, 'check shape mass pos y') - test:assertRange(mass1, 0.5, 0.6, 'check mass at 1 density') - test:assertRange(inertia1, 0, 0.1, 'check intertia at 1 density') - local x2, y2, mass2, inertia2 = shape1:computeMass(1000) - test:assertRange(mass2, 111, 112, 'check mass at 1000 density') - test:assertRange(inertia2, 7407, 7408, 'check intertia at 1000 density') - - -- check friction - test:assertRange(shape1:getFriction(), 0.2, 0.3, 'check def friction') - shape1:setFriction(1) - test:assertEquals(1, shape1:getFriction(), 'check set friction') - - -- check restitution - test:assertEquals(0, shape1:getRestitution(), 'check def restitution') - shape1:setRestitution(0.5) - test:assertRange(shape1:getRestitution(), 0.5, 0.6, 'check set restitution') - - -- check points - local bodyp = love.physics.newBody(world, 0, 0, 'dynamic') - local shape2 = love.physics.newRectangleShape(bodyp, 5, 5, 10, 10) - test:assertTrue(shape2:testPoint(5, 5), 'check point 5,5') - test:assertTrue(shape2:testPoint(10, 10, 0, 15, 15), 'check point 15,15 after translate 10,10') - test:assertFalse(shape2:testPoint(5, 5, 90, 10, 10), 'check point 10,10 after translate 5,5,90') - test:assertFalse(shape2:testPoint(10, 10, 90, 5, 5), 'check point 5,5 after translate 10,10,90') - test:assertFalse(shape2:testPoint(15, 15), 'check point 15,15') - - -- check ray cast - local xn1, yn1, fraction1 = shape2:rayCast(-20, -20, 20, 20, 100, 0, 0, 0, 1) - test:assertNotEquals(nil, xn1, 'check ray 1 x') - test:assertNotEquals(nil, xn1, 'check ray 1 y') - local xn2, yn2, fraction2 = shape2:rayCast(10, 10, -150, -150, 100, 0, 0, 0, 1) - test:assertEquals(nil, xn2, 'check ray 2 x') - test:assertEquals(nil, yn2, 'check ray 2 y') - - -- check filtering - test:assertEquals(nil, shape2:getMask(), 'check no mask') - shape2:setMask(1, 2, 3) - test:assertEquals(3, #{shape2:getMask()}, 'check set mask') - test:assertEquals(0, shape2:getGroupIndex(), 'check no index') - shape2:setGroupIndex(-1) - test:assertEquals(-1, shape2:getGroupIndex(), 'check set index') - local cat, mask, group = shape2:getFilterData() - test:assertEquals(1, cat, 'check filter cat') - test:assertEquals(65528, mask, 'check filter mask') - test:assertEquals(-1, group, 'check filter group') - - -- check destroyed - shape1:destroy() - test:assertTrue(shape1:isDestroyed(), 'check destroyed') - shape2:destroy() - - -- run some collision checks using filters, setup new shapes - local body2 = love.physics.newBody(world, 5, 5, 'dynamic') - local shape3 = love.physics.newRectangleShape(body1, 0, 0, 10, 10) - local shape4 = love.physics.newRectangleShape(body2, 0, 0, 10, 10) - local collisions = 0 - world:setCallbacks( - function() collisions = collisions + 1 end, - function() end, - function() end, - function() end - ) - - -- same positive group do collide - shape3:setGroupIndex(1) - shape4:setGroupIndex(1) - world:update(1) - test:assertEquals(1, collisions, 'check positive group collide') - - -- check negative group dont collide - shape3:setGroupIndex(-1) - shape4:setGroupIndex(-1) - body2:setPosition(20, 20); world:update(1); body2:setPosition(0, 0); world:update(1) - test:assertEquals(1, collisions, 'check negative group collide') - - -- check masks do collide - shape3:setGroupIndex(0) - shape4:setGroupIndex(0) - shape3:setCategory(2) - shape4:setMask(3) - body2:setPosition(20, 20); world:update(1); body2:setPosition(0, 0); world:update(1) - test:assertEquals(2, collisions, 'check mask collide') - - -- check masks not colliding - shape3:setCategory(2) - shape4:setMask(2, 4, 6) - body2:setPosition(20, 20); world:update(1); body2:setPosition(0, 0); world:update(1) - test:assertEquals(2, collisions, 'check mask not collide') - -end - - --- World (love.physics.newWorld) -love.test.physics.World = function(test) - - -- create new world - local world = love.physics.newWorld(0, 0, false) - local body1 = love.physics.newBody(world, 0, 0, 'dynamic') - local rectangle1 = love.physics.newRectangleShape(body1, 0, 0, 10, 10) - test:assertObject(world) - - -- check bodies in world - test:assertEquals(1, #world:getBodies(), 'check 1 body') - test:assertEquals(0, world:getBodies()[1]:getX(), 'check body prop x') - test:assertEquals(0, world:getBodies()[1]:getY(), 'check body prop y') - world:translateOrigin(-10, -10) -- check affects bodies - test:assertRange(world:getBodies()[1]:getX(), 9, 11, 'check body prop change x') - test:assertRange(world:getBodies()[1]:getY(), 9, 11, 'check body prop change y') - test:assertEquals(1, world:getBodyCount(), 'check 1 body count') - - -- check shapes in world - test:assertEquals(1, #world:getShapesInArea(0, 0, 10, 10), 'check shapes in area #1') - test:assertEquals(0, #world:getShapesInArea(20, 20, 30, 30), 'check shapes in area #1') - - -- check world status - test:assertFalse(world:isLocked(), 'check not updating') - test:assertFalse(world:isSleepingAllowed(), 'check no sleep (till brooklyn)') - world:setSleepingAllowed(true) - test:assertTrue(world:isSleepingAllowed(), 'check can sleep') - - -- check world objects - test:assertEquals(0, #world:getJoints(), 'check no joints') - test:assertEquals(0, world:getJointCount(), 'check no joints count') - test:assertEquals(0, world:getGravity(), 'check def gravity') - test:assertEquals(0, #world:getContacts(), 'check no contacts') - test:assertEquals(0, world:getContactCount(), 'check no contact count') - - -- check callbacks are called - local beginContact, endContact, preSolve, postSolve = world:getCallbacks() - test:assertEquals(nil, beginContact, 'check no begin contact callback') - test:assertEquals(nil, endContact, 'check no end contact callback') - test:assertEquals(nil, preSolve, 'check no pre solve callback') - test:assertEquals(nil, postSolve, 'check no post solve callback') - local beginContactCheck = false - local endContactCheck = false - local preSolveCheck = false - local postSolveCheck = false - local collisions = 0 - world:setCallbacks( - function() beginContactCheck = true; collisions = collisions + 1 end, - function() endContactCheck = true end, - function() preSolveCheck = true end, - function() postSolveCheck = true end - ) - - -- setup so we can collide stuff to call the callbacks - local body2 = love.physics.newBody(world, 10, 10, 'dynamic') - local rectangle2 = love.physics.newRectangleShape(body2, 0, 0, 10, 10) - test:assertFalse(beginContactCheck, 'check world didnt update after adding body') - world:update(1) - test:assertTrue(beginContactCheck, 'check contact start') - test:assertTrue(preSolveCheck, 'check pre solve') - test:assertTrue(postSolveCheck, 'check post solve') - body2:setPosition(100, 100) - world:update(1) - test:assertTrue(endContactCheck, 'check contact end') - - -- check point checking - local shapes = 0 - world:queryShapesInArea(0, 0, 10, 10, function(x) - shapes = shapes + 1 - end) - test:assertEquals(1, shapes, 'check shapes in area') - - -- check raycast - world:rayCast(0, 0, 200, 200, function(x) - shapes = shapes + 1 - return 1 - end) - test:assertEquals(3, shapes, 'check shapes in raycast') - test:assertEquals(world:rayCastClosest(0, 0, 200, 200), rectangle1, 'check closest raycast') - test:assertNotEquals(nil, world:rayCastAny(0, 0, 200, 200), 'check any raycast') - - -- change collision logic - test:assertEquals(nil, world:getContactFilter(), 'check def filter') - world:update(1) - world:setContactFilter(function(s1, s2) - return false -- nothing collides - end) - body2:setPosition(10, 10) - world:update(1) - test:assertEquals(1, collisions, 'check collision logic change') - - -- check gravity - world:setGravity(1, 1) - test:assertEquals(1, world:getGravity(), 'check grav change') - - -- check destruction - test:assertFalse(world:isDestroyed(), 'check not destroyed') - world:destroy() - test:assertTrue(world:isDestroyed(), 'check world destroyed') - -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------METHODS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.physics.getDistance -love.test.physics.getDistance = function(test) - -- setup two fixtues to check - local world = love.physics.newWorld(0, 0, false) - local body = love.physics.newBody(world, 10, 10, 'static') - local shape1 = love.physics.newEdgeShape(body, 0, 0, 5, 5) - local shape2 = love.physics.newEdgeShape(body, 10, 10, 15, 15) - -- check distance between them - test:assertRange(love.physics.getDistance(shape1, shape2), 6, 7, 'check distance matches') -end - - --- love.physics.getMeter -love.test.physics.getMeter = function(test) - -- check value set is returned - love.physics.setMeter(30) - test:assertEquals(30, love.physics.getMeter(), 'check meter matches') -end - - --- love.physics.newBody --- @NOTE this is just basic nil checking, objs have their own test method -love.test.physics.newBody = function(test) - local world = love.physics.newWorld(1, 1, true) - local body = love.physics.newBody(world, 10, 10, 'static') - test:assertObject(body) -end - - --- love.physics.newChainShape --- @NOTE this is just basic nil checking, objs have their own test method -love.test.physics.newChainShape = function(test) - local world = love.physics.newWorld(1, 1, true) - local body = love.physics.newBody(world, 10, 10, 'static') - test:assertObject(love.physics.newChainShape(body, true, 0, 0, 1, 0, 1, 1, 0, 1)) -end - - --- love.physics.newCircleShape --- @NOTE this is just basic nil checking, objs have their own test method -love.test.physics.newCircleShape = function(test) - local world = love.physics.newWorld(1, 1, true) - local body = love.physics.newBody(world, 10, 10, 'static') - test:assertObject(love.physics.newCircleShape(body, 10)) -end - - --- love.physics.newDistanceJoint --- @NOTE this is just basic nil checking, objs have their own test method -love.test.physics.newDistanceJoint = function(test) - local world = love.physics.newWorld(1, 1, true) - local body1 = love.physics.newBody(world, 10, 10, 'static') - local body2 = love.physics.newBody(world, 20, 20, 'static') - local obj = love.physics.newDistanceJoint(body1, body2, 10, 10, 20, 20, true) - test:assertObject(obj) -end - - --- love.physics.newEdgeShape --- @NOTE this is just basic nil checking, objs have their own test method -love.test.physics.newEdgeShape = function(test) - local world = love.physics.newWorld(1, 1, true) - local body = love.physics.newBody(world, 10, 10, 'static') - local obj = love.physics.newEdgeShape(body, 0, 0, 10, 10) - test:assertObject(obj) -end - - --- love.physics.newFrictionJoint --- @NOTE this is just basic nil checking, objs have their own test method -love.test.physics.newFrictionJoint = function(test) - local world = love.physics.newWorld(1, 1, true) - local body1 = love.physics.newBody(world, 10, 10, 'static') - local body2 = love.physics.newBody(world, 20, 20, 'static') - local obj = love.physics.newFrictionJoint(body1, body2, 15, 15, true) - test:assertObject(obj) -end - - --- love.physics.newGearJoint --- @NOTE this is just basic nil checking, objs have their own test method -love.test.physics.newGearJoint = function(test) - local world = love.physics.newWorld(1, 1, true) - local body1 = love.physics.newBody(world, 10, 10, 'dynamic') - local body2 = love.physics.newBody(world, 20, 20, 'dynamic') - local body3 = love.physics.newBody(world, 30, 30, 'dynamic') - local body4 = love.physics.newBody(world, 40, 40, 'dynamic') - local joint1 = love.physics.newPrismaticJoint(body1, body2, 10, 10, 20, 20, true) - local joint2 = love.physics.newPrismaticJoint(body3, body4, 30, 30, 40, 40, true) - local obj = love.physics.newGearJoint(joint1, joint2, 1, true) - test:assertObject(obj) -end - - --- love.physics.newMotorJoint --- @NOTE this is just basic nil checking, objs have their own test method -love.test.physics.newMotorJoint = function(test) - local world = love.physics.newWorld(1, 1, true) - local body1 = love.physics.newBody(world, 10, 10, 'static') - local body2 = love.physics.newBody(world, 20, 20, 'static') - local obj = love.physics.newMotorJoint(body1, body2, 1) - test:assertObject(obj) -end - - --- love.physics.newMouseJoint --- @NOTE this is just basic nil checking, objs have their own test method -love.test.physics.newMouseJoint = function(test) - local world = love.physics.newWorld(1, 1, true) - local body = love.physics.newBody(world, 10, 10, 'static') - local obj = love.physics.newMouseJoint(body, 10, 10) - test:assertObject(obj) -end - - --- love.physics.newPolygonShape --- @NOTE this is just basic nil checking, objs have their own test method -love.test.physics.newPolygonShape = function(test) - local world = love.physics.newWorld(1, 1, true) - local body = love.physics.newBody(world, 10, 10, 'static') - local obj = love.physics.newPolygonShape(body, {0, 0, 2, 3, 2, 1, 3, 1, 5, 1}) - test:assertObject(obj) -end - - --- love.physics.newPrismaticJoint --- @NOTE this is just basic nil checking, objs have their own test method -love.test.physics.newPrismaticJoint = function(test) - local world = love.physics.newWorld(1, 1, true) - local body1 = love.physics.newBody(world, 10, 10, 'static') - local body2 = love.physics.newBody(world, 20, 20, 'static') - local obj = love.physics.newPrismaticJoint(body1, body2, 10, 10, 20, 20, true) - test:assertObject(obj) -end - - --- love.physics.newPulleyJoint --- @NOTE this is just basic nil checking, objs have their own test method -love.test.physics.newPulleyJoint = function(test) - local world = love.physics.newWorld(1, 1, true) - local body1 = love.physics.newBody(world, 10, 10, 'static') - local body2 = love.physics.newBody(world, 20, 20, 'static') - local obj = love.physics.newPulleyJoint(body1, body2, 10, 10, 20, 20, 15, 15, 25, 25, 1, true) - test:assertObject(obj) -end - - --- love.physics.newRectangleShape --- @NOTE this is just basic nil checking, objs have their own test method -love.test.physics.newRectangleShape = function(test) - local world = love.physics.newWorld(1, 1, true) - local body = love.physics.newBody(world, 10, 10, 'static') - local shape1 = love.physics.newRectangleShape(body, 10, 20) - local shape2 = love.physics.newRectangleShape(body, 10, 10, 40, 30, 10) - test:assertObject(shape1) - test:assertObject(shape2) -end - - --- love.physics.newRevoluteJoint --- @NOTE this is just basic nil checking, objs have their own test method -love.test.physics.newRevoluteJoint = function(test) - local world = love.physics.newWorld(1, 1, true) - local body1 = love.physics.newBody(world, 10, 10, 'static') - local body2 = love.physics.newBody(world, 20, 20, 'static') - local obj = love.physics.newRevoluteJoint(body1, body2, 10, 10, true) - test:assertObject(obj) -end - - --- love.physics.newRopeJoint --- @NOTE this is just basic nil checking, objs have their own test method -love.test.physics.newRopeJoint = function(test) - local world = love.physics.newWorld(1, 1, true) - local body1 = love.physics.newBody(world, 10, 10, 'static') - local body2 = love.physics.newBody(world, 20, 20, 'static') - local obj = love.physics.newRopeJoint(body1, body2, 10, 10, 20, 20, 50, true) - test:assertObject(obj) -end - - --- love.physics.newWeldJoint --- @NOTE this is just basic nil checking, objs have their own test method -love.test.physics.newWeldJoint = function(test) - local world = love.physics.newWorld(1, 1, true) - local body1 = love.physics.newBody(world, 10, 10, 'static') - local body2 = love.physics.newBody(world, 20, 20, 'static') - local obj = love.physics.newWeldJoint(body1, body2, 10, 10, true) - test:assertObject(obj) -end - - --- love.physics.newWheelJoint --- @NOTE this is just basic nil checking, objs have their own test method -love.test.physics.newWheelJoint = function(test) - local world = love.physics.newWorld(1, 1, true) - local body1 = love.physics.newBody(world, 10, 10, 'static') - local body2 = love.physics.newBody(world, 20, 20, 'static') - local obj = love.physics.newWheelJoint(body1, body2, 10, 10, 5, 5, true) - test:assertObject(obj) -end - - --- love.physics.newWorld --- @NOTE this is just basic nil checking, objs have their own test method -love.test.physics.newWorld = function(test) - local world = love.physics.newWorld(1, 1, true) - test:assertObject(world) -end - - --- love.physics.setMeter -love.test.physics.setMeter = function(test) - -- set initial meter - local world = love.physics.newWorld(1, 1, true) - love.physics.setMeter(30) - local body = love.physics.newBody(world, 300, 300, "dynamic") - -- check changing meter changes pos value relatively - love.physics.setMeter(10) - local x, y = body:getPosition() - test:assertEquals(100, x, 'check pos x') - test:assertEquals(100, y, 'check pos y') -end - - - -================================================ -File: tests/sensor.lua -================================================ --- love.sensor --- @NOTE we can't test this module fully as it's hardware dependent --- however we can test methods do what is expected and can handle certain params - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------HELPERS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - -local function testIsEnabled(test, sensorType) - love.sensor.setEnabled(sensorType, true) - test:assertTrue(love.sensor.isEnabled(sensorType), 'check ' .. sensorType .. ' enabled') - love.sensor.setEnabled(sensorType, false) - test:assertFalse(love.sensor.isEnabled(sensorType), 'check ' .. sensorType .. ' disabled') -end - - -local function testGetName(test, sensorType) - love.sensor.setEnabled(sensorType, true) - local ok, name = pcall(love.sensor.getName, sensorType) - test:assertTrue(ok, 'check sensor.getName("' .. sensorType .. '") success') - test:assertEquals(type(name), 'string', 'check sensor.getName("' .. sensorType .. '") return value type') - - love.sensor.setEnabled(sensorType, false) - ok, name = pcall(love.sensor.getName, sensorType) - test:assertFalse(ok, 'check sensor.getName("' .. sensorType .. '") errors when disabled') -end - - -local function testGetData(test, sensorType) - love.sensor.setEnabled(sensorType, true) - local ok, x, y, z = pcall(love.sensor.getData, sensorType) - test:assertTrue(ok, 'check sensor.getData("' .. sensorType .. '") success') - if ok then - test:assertNotNil(x) - test:assertNotNil(y) - test:assertNotNil(z) - end - - love.sensor.setEnabled(sensorType, false) - ok, x, y, z = pcall(love.sensor.getData, sensorType) - test:assertFalse(ok, 'check sensor.getData("' .. sensorType .. '") errors when disabled') -end - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------METHODS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.sensor.hasSensor -love.test.sensor.hasSensor = function(test) - -- but we can make sure that the SensorTypes can be passed - local accelerometer = love.sensor.hasSensor('accelerometer') - local gyroscope = love.sensor.hasSensor('gyroscope') - test:assertNotNil(accelerometer) - test:assertNotNil(gyroscope) -end - - --- love.sensor.isEnabled and love.sensor.setEnabled -love.test.sensor.isEnabled = function(test) - local accelerometer = love.sensor.hasSensor('accelerometer') - local gyroscope = love.sensor.hasSensor('gyroscope') - - if accelerometer or gyroscope then - if accelerometer then testIsEnabled(test, 'accelerometer') end - if gyroscope then testIsEnabled(test, 'gyroscope') end - else - test:skipTest('neither accelerometer nor gyroscope are supported in this system') - end -end - - --- love.sensor.getName -love.test.sensor.getName = function(test) - local accelerometer = love.sensor.hasSensor('accelerometer') - local gyroscope = love.sensor.hasSensor('gyroscope') - - if accelerometer or gyroscope then - if accelerometer then testGetName(test, 'accelerometer') end - if gyroscope then testGetName(test, 'gyroscope') end - else - test:skipTest('neither accelerometer nor gyroscope are supported in this system') - end -end - - --- love.sensor.getData -love.test.sensor.getData = function(test) - local accelerometer = love.sensor.hasSensor('accelerometer') - local gyroscope = love.sensor.hasSensor('gyroscope') - - if accelerometer or gyroscope then - if accelerometer then testGetData(test, 'accelerometer') end - if gyroscope then testGetData(test, 'gyroscope') end - else - test:skipTest('neither accelerometer nor gyroscope are supported in this system') - end -end - - - -================================================ -File: tests/sound.lua -================================================ --- love.sound - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------OBJECTS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- Decoder (love.sound.newDecoder) -love.test.sound.Decoder = function(test) - - -- create obj - local decoder = love.sound.newDecoder('resources/click.ogg') - test:assertObject(decoder) - - -- check bit depth - test:assertMatch({8, 16}, decoder:getBitDepth(), 'check bit depth') - - -- check channel count - test:assertMatch({1, 2}, decoder:getChannelCount(), 'check channel count') - - -- check duration - test:assertRange(decoder:getDuration(), 0.06, 0.07, 'check duration') - - -- check sample rate - test:assertEquals(44100, decoder:getSampleRate(), 'check sample rate') - - -- check makes sound data (test in method below) - test:assertObject(decoder:decode()) - - -- check cloning sound - local clone = decoder:clone() - test:assertMatch({8, 16}, clone:getBitDepth(), 'check cloned bit depth') - test:assertMatch({1, 2}, clone:getChannelCount(), 'check cloned channel count') - test:assertRange(clone:getDuration(), 0.06, 0.07, 'check cloned duration') - test:assertEquals(44100, clone:getSampleRate(), 'check cloned sample rate') - -end - - --- SoundData (love.sound.newSoundData) -love.test.sound.SoundData = function(test) - - -- create obj - local sdata = love.sound.newSoundData('resources/click.ogg') - test:assertObject(sdata) - - -- check data size + string - test:assertEquals(11708, sdata:getSize(), 'check size') - test:assertNotNil(sdata:getString()) - - -- check bit depth - test:assertMatch({8, 16}, sdata:getBitDepth(), 'check bit depth') - - -- check channel count - test:assertMatch({1, 2}, sdata:getChannelCount(), 'check channel count') - - -- check duration - test:assertRange(sdata:getDuration(), 0.06, 0.07, 'check duration') - - -- check samples - test:assertEquals(44100, sdata:getSampleRate(), 'check sample rate') - test:assertEquals(2927, sdata:getSampleCount(), 'check sample count') - - -- check cloning - local clone = sdata:clone() - test:assertEquals(11708, clone:getSize(), 'check clone size') - test:assertNotNil(clone:getString()) - test:assertMatch({8, 16}, clone:getBitDepth(), 'check clone bit depth') - test:assertMatch({1, 2}, clone:getChannelCount(), 'check clone channel count') - test:assertRange(clone:getDuration(), 0.06, 0.07, 'check clone duration') - test:assertEquals(44100, clone:getSampleRate(), 'check clone sample rate') - test:assertEquals(2927, clone:getSampleCount(), 'check clone sample count') - - -- check sample setting - test:assertRange(sdata:getSample(0.001), -0.1, 0, 'check sample 1') - test:assertRange(sdata:getSample(0.005), -0.1, 0, 'check sample 1') - sdata:setSample(0.002, 1) - test:assertEquals(1, sdata:getSample(0.002), 'check setting sample manually') - - -- check copying from another sound - local copy1 = love.sound.newSoundData('resources/tone.ogg') - local copy2 = love.sound.newSoundData('resources/pop.ogg') - local before = copy2:getSample(0.02) - copy2:copyFrom(copy1, 0.01, 1, 0.02) - test:assertNotEquals(before, copy2:getSample(0.02), 'check changed') - - -- check slicing - local count = math.floor(copy1:getSampleCount()/2) - local slice = copy1:slice(0, count) - test:assertEquals(count, slice:getSampleCount(), 'check slice length') - -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------METHODS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - - --- love.sound.newDecoder --- @NOTE this is just basic nil checking, objs have their own test method -love.test.sound.newDecoder = function(test) - test:assertObject(love.sound.newDecoder('resources/click.ogg')) -end - - --- love.sound.newSoundData --- @NOTE this is just basic nil checking, objs have their own test method -love.test.sound.newSoundData = function(test) - test:assertObject(love.sound.newSoundData('resources/click.ogg')) - test:assertObject(love.sound.newSoundData(math.floor((1/32)*44100), 44100, 16, 1)) -end - - - -================================================ -File: tests/system.lua -================================================ --- love.system - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -----------------------------------METHODS--------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.system.getClipboardText -love.test.system.getClipboardText = function(test) - -- ignore if not using window - if love.test.windowmode == false then - return test:skipTest('clipboard only available in window mode') - end - -- check clipboard value is set - love.system.setClipboardText('helloworld') - test:assertEquals('helloworld', love.system.getClipboardText(), 'check clipboard match') -end - - --- love.system.getOS -love.test.system.getOS = function(test) - -- check os is in documented values - local os = love.system.getOS() - local options = {'OS X', 'Windows', 'Linux', 'Android', 'iOS'} - test:assertMatch(options, os, 'check value matches') -end - - --- love.system.getPreferredLocales -love.test.system.getPreferredLocales = function(test) - local locale = love.system.getPreferredLocales() - test:assertNotNil(locale) - test:assertEquals('table', type(locale), 'check returns table') -end - - --- love.system.getPowerInfo -love.test.system.getPowerInfo = function(test) - -- check battery state is one of the documented states - local state, percent, seconds = love.system.getPowerInfo() - local states = {'unknown', 'battery', 'nobattery', 'charging', 'charged'} - test:assertMatch(states, state, 'check value matches') - -- if percent/seconds check within expected range - if percent ~= nil then - test:assertRange(percent, 0, 100, 'check battery percent within range') - end - if seconds ~= nil then - test:assertNotNil(seconds) - end -end - - --- love.system.getProcessorCount -love.test.system.getProcessorCount = function(test) - test:assertNotNil(love.system.getProcessorCount()) -- youd hope right -end - - --- love.system.hasBackgroundMusic -love.test.system.hasBackgroundMusic = function(test) - test:assertNotNil(love.system.hasBackgroundMusic()) -end - - --- love.system.openURL -love.test.system.openURL = function(test) - test:skipTest('cant test this worked') - --test:assertNotEquals(nil, love.system.openURL('https://love2d.org'), 'check open URL') -end - - --- love.system.getClipboardText -love.test.system.setClipboardText = function(test) - -- ignore if not using window - if love.test.windowmode == false then - return test:skipTest('clipboard only available in window mode') - end - -- check value returned is what was set - love.system.setClipboardText('helloworld') - test:assertEquals('helloworld', love.system.getClipboardText(), 'check set text') -end - - --- love.system.vibrate --- @NOTE cant really test this -love.test.system.vibrate = function(test) - test:skipTest('cant test this worked') -end - - - -================================================ -File: tests/thread.lua -================================================ --- love.thread - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -----------------------------------OBJECTS--------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- Channel (love.thread.newChannel) -love.test.thread.Channel = function(test) - - -- create channel - local channel = love.thread.getChannel('test') - test:assertObject(channel) - - -- setup thread to use - local threadcode1 = [[ - require("love.timer") - love.timer.sleep(0.1) - love.thread.getChannel('test'):push('hello world') - love.timer.sleep(0.1) - love.thread.getChannel('test'):push('me again') - ]] - local thread1 = love.thread.newThread(threadcode1) - thread1:start() - - -- check message sent from thread to channel - local msg1 = channel:demand() - test:assertEquals('hello world', msg1, 'check 1st message was sent') - thread1:wait() - test:assertEquals(1, channel:getCount(), 'check still another message') - test:assertEquals('me again', channel:peek(), 'check 2nd message pending') - local msg2 = channel:pop() - test:assertEquals('me again', msg2, 'check 2nd message was sent') - channel:clear() - - -- setup another thread for some ping pong - local threadcode2 = [[ - local function setChannel(channel, value) - channel:clear() - return channel:push(value) - end - local channel = love.thread.getChannel('test') - local waiting = true - local sent = nil - while waiting == true do - if sent == nil then - sent = channel:performAtomic(setChannel, 'ping') - end - if channel:hasRead(sent) then - local msg = channel:demand() - if msg == 'pong' then - channel:push(msg) - waiting = false - end - end - end - ]] - - -- first we run a thread that will send 1 ping - local thread2 = love.thread.newThread(threadcode2) - thread2:start() - - -- we wait for that ping to be sent and then send a pong back - local msg3 = channel:demand() - test:assertEquals('ping', msg3, 'check message recieved 1') - - -- thread should be waiting for us, and checking is the ping was read - channel:supply('pong', 1) - - -- if it was then it should send back our pong and thread should die - thread2:wait() - local msg4 = channel:pop() - test:assertEquals('pong', msg4, 'check message recieved 2') - test:assertEquals(0, channel:getCount()) - -end - - --- Thread (love.thread.newThread) -love.test.thread.Thread = function(test) - - -- create thread - local threadcode = [[ - local b = 0 - for a=1,100000 do - b = b + a - end - ]] - local thread = love.thread.newThread(threadcode) - test:assertObject(thread) - - -- check thread runs - thread:start() - test:assertTrue(thread:isRunning(), 'check started') - thread:wait() - test:assertFalse(thread:isRunning(), 'check finished') - test:assertEquals(nil, thread:getError(), 'check no errors') - - -- check an invalid thread - local badthreadcode = 'local b = 0\nreturn b + "string" .. 10' - local badthread = love.thread.newThread(badthreadcode) - badthread:start() - badthread:wait() - test:assertNotNil(badthread:getError()) - -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -----------------------------------METHODS--------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.thread.getChannel --- @NOTE this is just basic nil checking, objs have their own test method -love.test.thread.getChannel = function(test) - test:assertObject(love.thread.getChannel('test')) -end - - --- love.thread.newChannel --- @NOTE this is just basic nil checking, objs have their own test method -love.test.thread.newChannel = function(test) - test:assertObject(love.thread.newChannel()) -end - - --- love.thread.newThread --- @NOTE this is just basic nil checking, objs have their own test method -love.test.thread.newThread = function(test) - test:assertObject(love.thread.newThread('classes/TestSuite.lua')) -end - - - -================================================ -File: tests/timer.lua -================================================ --- love.timer - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -----------------------------------METHODS--------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.timer.getAverageDelta --- @NOTE not sure if you could reliably get a specific delta? -love.test.timer.getAverageDelta = function(test) - test:assertNotNil(love.timer.getAverageDelta()) -end - --- love.timer.getDelta --- @NOTE not sure if you could reliably get a specific delta? -love.test.timer.getDelta = function(test) - test:assertNotNil(love.timer.getDelta()) -end - - --- love.timer.getFPS --- @NOTE not sure if you could reliably get a specific FPS? -love.test.timer.getFPS = function(test) - test:assertNotNil(love.timer.getFPS()) -end - - --- love.timer.getTime -love.test.timer.getTime = function(test) - local starttime = love.timer.getTime() - love.timer.sleep(0.1) - local endtime = love.timer.getTime() - starttime - test:assertRange(endtime, 0.05, 1, 'check 0.1s passes') -end - - --- love.timer.sleep -love.test.timer.sleep = function(test) - local starttime = love.timer.getTime() - love.timer.sleep(0.1) - test:assertRange(love.timer.getTime() - starttime, 0.05, 1, 'check 0.1s passes') -end - - --- love.timer.step --- @NOTE not sure if you could reliably get a specific step val? -love.test.timer.step = function(test) - test:assertNotNil(love.timer.step()) -end - - - -================================================ -File: tests/touch.lua -================================================ --- love.touch --- @NOTE we can't test this module fully as it's hardware dependent --- however we can test methods do what is expected and can handle certain params - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -------------------------------------METHODS------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.touch.getPosition --- @TODO is there a way to fake the touchid pointer? -love.test.touch.getPosition = function(test) - test:assertNotNil(love.touch.getPosition) - test:assertEquals('function', type(love.touch.getPosition)) -end - - --- love.touch.getPressure --- @TODO is there a way to fake the touchid pointer? -love.test.touch.getPressure = function(test) - test:assertNotNil(love.touch.getPressure) - test:assertEquals('function', type(love.touch.getPressure)) -end - - --- love.touch.getTouches -love.test.touch.getTouches = function(test) - test:assertEquals('function', type(love.touch.getTouches)) -end - - - -================================================ -File: tests/video.lua -================================================ --- love.video - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -----------------------------------OBJECTS--------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- VideoStream (love.thread.newVideoStream) -love.test.video.VideoStream = function(test) - - -- create obj - local video = love.video.newVideoStream('resources/sample.ogv') - test:assertObject(video) - - -- check def properties - test:assertEquals('resources/sample.ogv', video:getFilename(), 'check filename') - test:assertFalse(video:isPlaying(), 'check not playing by def') - - -- check playing and pausing states - video:play() - test:assertTrue(video:isPlaying(), 'check now playing') - video:seek(0.3) - test:assertRange(video:tell(), 0.3, 0.4, 'check seek/tell') - video:rewind() - test:assertRange(video:tell(), 0, 0.1, 'check rewind') - video:pause() - test:assertFalse(video:isPlaying(), 'check paused') - -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -----------------------------------METHODS--------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.video.newVideoStream --- @NOTE this is just basic nil checking, objs have their own test method -love.test.video.newVideoStream = function(test) - test:assertObject(love.video.newVideoStream('resources/sample.ogv')) -end - - - -================================================ -File: tests/window.lua -================================================ --- love.window - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -----------------------------------METHODS--------------------------------------- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - - --- love.window.focus -love.test.window.focus = function(test) - -- cant test as doesnt return anything - test:assertEquals('function', type(love.window.focus), 'check method exists') -end - - --- love.window.fromPixels -love.test.window.fromPixels = function(test) - -- check dpi/pixel ratio as expected - local dpi = love.window.getDPIScale() - local pixels = love.window.fromPixels(100) - test:assertEquals(100/dpi, pixels, 'check dpi ratio') -end - - --- love.window.getDPIScale --- @NOTE dependent on hardware so best can do is not nil -love.test.window.getDPIScale = function(test) - test:assertNotNil(test) -end - - --- love.window.getDesktopDimensions --- @NOTE dependent on hardware so best can do is not nil -love.test.window.getDesktopDimensions = function(test) - local w, h = love.window.getDesktopDimensions() - test:assertNotNil(w) - test:assertNotNil(h) -end - - --- love.window.getDisplayCount --- @NOTE cant wait for the test suite to be run headless and fail here -love.test.window.getDisplayCount = function(test) - test:assertGreaterEqual(1, love.window.getDisplayCount(), 'check 1 display') -end - - --- love.window.getDisplayName --- @NOTE dependent on hardware so best can do is not nil -love.test.window.getDisplayName = function(test) - test:assertNotNil(love.window.getDisplayName(1)) -end - - --- love.window.getDisplayOrientation --- @NOTE dependent on hardware so best can do is not nil -love.test.window.getDisplayOrientation = function(test) - test:assertNotNil(love.window.getDisplayOrientation(1)) -end - - --- love.window.getFullscreen -love.test.window.getFullscreen = function(test) - if GITHUB_RUNNER and test:isOS('Linux') then - return test:skipTest("xvfb on Linux doesn't support fullscreen") - end - - -- check not fullscreen to start - test:assertFalse(love.window.getFullscreen(), 'check not fullscreen') - love.window.setFullscreen(true) - -- check now fullscreen - test:assertTrue(love.window.getFullscreen(), 'check now fullscreen') - love.window.setFullscreen(false) -- reset -end - - --- love.window.getFullscreenModes --- @NOTE dependent on hardware so best can do is not nil -love.test.window.getFullscreenModes = function(test) - test:assertNotNil(love.window.getFullscreenModes(1)) -end - - --- love.window.getIcon -love.test.window.getIcon = function(test) - -- check icon nil by default if not set - test:assertEquals(nil, love.window.getIcon(), 'check nil by default') - local icon = love.image.newImageData('resources/love.png') - -- check getting icon not nil after setting - love.window.setIcon(icon) - test:assertNotNil(love.window.getIcon()) -end - - --- love.window.getMode --- @NOTE could prob add more checks on the flags here based on conf.lua -love.test.window.getMode = function(test) - local w, h, flags = love.window.getMode() - test:assertEquals(360, w, 'check w') - test:assertEquals(240, h, 'check h') - test:assertFalse(flags["fullscreen"], 'check fullscreen') -end - - --- love.window.getPosition --- @NOTE anything we could check display index agaisn't in getPosition return? -love.test.window.getPosition = function(test) - love.window.setPosition(100, 100, 1) - local x, y, _ = love.window.getPosition() - test:assertEquals(100, x, 'check position x') - test:assertEquals(100, y, 'check position y') -end - - --- love.window.getSafeArea --- @NOTE dependent on hardware so best can do is not nil -love.test.window.getSafeArea = function(test) - local x, y, w, h = love.window.getSafeArea() - test:assertNotNil(x) - test:assertNotNil(y) - test:assertNotNil(w) - test:assertNotNil(h) -end - - --- love.window.getTitle -love.test.window.getTitle = function(test) - -- check title returned is what was set - love.window.setTitle('love.testing') - test:assertEquals('love.testing', love.window.getTitle(), 'check title match') - love.window.setTitle('love.test') -end - - --- love.window.getVSync -love.test.window.getVSync = function(test) - test:assertNotNil(love.window.getVSync()) -end - - --- love.window.hasFocus --- @NOTE cant really test as cant force focus -love.test.window.hasFocus = function(test) - test:assertNotNil(love.window.hasFocus()) -end - - --- love.window.hasMouseFocus --- @NOTE cant really test as cant force focus -love.test.window.hasMouseFocus = function(test) - test:assertNotNil(love.window.hasMouseFocus()) -end - - --- love.window.isDisplaySleepEnabled -love.test.window.isDisplaySleepEnabled = function(test) - test:assertNotNil(love.window.isDisplaySleepEnabled()) - -- check disabled - love.window.setDisplaySleepEnabled(false) - test:assertFalse(love.window.isDisplaySleepEnabled(), 'check sleep disabled') - -- check enabled - love.window.setDisplaySleepEnabled(true) - test:assertTrue(love.window.isDisplaySleepEnabled(), 'check sleep enabled') -end - - --- love.window.isMaximized -love.test.window.isMaximized = function(test) - if GITHUB_RUNNER and test:isOS('Linux') then - return test:skipTest("xvfb on Linux doesn't support window maximization") - end - - test:assertFalse(love.window.isMaximized(), 'check window not maximized') - love.window.maximize() - test:waitFrames(10) - -- on MACOS maximize wont get recognised immedietely so wait a few frames - test:assertTrue(love.window.isMaximized(), 'check window now maximized') - love.window.restore() -end - - --- love.window.isMinimized -love.test.window.isMinimized = function(test) - if GITHUB_RUNNER and test:isOS('Linux') then - return test:skipTest("xvfb on Linux doesn't support window minimization") - end - - -- check not minimized to start - test:assertFalse(love.window.isMinimized(), 'check window not minimized') - -- try to minimize - love.window.minimize() - test:waitFrames(10) - -- on linux minimize won't get recognized immediately, so wait a few frames - test:assertTrue(love.window.isMinimized(), 'check window minimized') - love.window.restore() -end - - --- love.window.isOccluded -love.test.window.isOccluded = function(test) - love.window.focus() - test:assertFalse(love.window.isOccluded(), 'check window not occluded') -end - - --- love.window.isOpen -love.test.window.isOpen = function(test) - -- check open initially - test:assertTrue(love.window.isOpen(), 'check window open') - -- we check closing in test.window.close -end - - --- love.window.isVisible -love.test.window.isVisible = function(test) - -- check visible initially - test:assertTrue(love.window.isVisible(), 'check window visible') -end - - --- love.window.maximize -love.test.window.maximize = function(test) - if GITHUB_RUNNER and test:isOS('Linux') then - return test:skipTest("xvfb on Linux doesn't support window maximization") - end - - test:assertFalse(love.window.isMaximized(), 'check window not maximized') - -- check maximizing is set - love.window.maximize() - test:waitFrames(10) - -- on macos we need to wait a few frames - test:assertTrue(love.window.isMaximized(), 'check window maximized') - love.window.restore() -end - - --- love.window.minimize -love.test.window.minimize = function(test) - if GITHUB_RUNNER and test:isOS('Linux') then - return test:skipTest("xvfb on Linux doesn't support window minimization") - end - - test:assertFalse(love.window.isMinimized(), 'check window not minimized') - -- check minimizing is set - love.window.minimize() - test:waitFrames(10) - -- on linux we need to wait a few frames - test:assertTrue(love.window.isMinimized(), 'check window maximized') - love.window.restore() -end - - --- love.window.requestAttention -love.test.window.requestAttention = function(test) - test:skipTest('cant test this worked') -end - - --- love.window.restore -love.test.window.restore = function(test) - if GITHUB_RUNNER and test:isOS('Linux') then - return test:skipTest("xvfb on Linux doesn't support window minimization") - end - - -- check minimized to start - love.window.minimize() - test:waitFrames(10) - love.window.restore() - test:waitFrames(10) - -- check restoring the state of the window - test:assertFalse(love.window.isMinimized(), 'check window restored') -end - - --- love.window.setDisplaySleepEnabled -love.test.window.setDisplaySleepEnabled = function(test) - -- check disabling sleep - love.window.setDisplaySleepEnabled(false) - test:assertFalse(love.window.isDisplaySleepEnabled(), 'check sleep disabled') - -- check setting it back to enabled - love.window.setDisplaySleepEnabled(true) - test:assertTrue(love.window.isDisplaySleepEnabled(), 'check sleep enabled') -end - - --- love.window.setFullscreen -love.test.window.setFullscreen = function(test) - if GITHUB_RUNNER and test:isOS('Linux') then - return test:skipTest("xvfb on Linux doesn't support fullscreen") - end - - -- check fullscreen is set - love.window.setFullscreen(true) - test:assertTrue(love.window.getFullscreen(), 'check fullscreen') - -- check setting back to normal - love.window.setFullscreen(false) - test:assertFalse(love.window.getFullscreen(), 'check not fullscreen') -end - - --- love.window.setIcon --- @NOTE could check the image data itself? -love.test.window.setIcon = function(test) - -- check setting an icon returns the val - local icon = love.image.newImageData('resources/love.png') - love.window.setIcon(icon) - test:assertNotEquals(nil, love.window.getIcon(), 'check icon not nil') -end - - --- love.window.setMode --- @NOTE same as getMode could be checking more flag properties -love.test.window.setMode = function(test) - -- set window mode - love.window.setMode(512, 512, { - fullscreen = false, - resizable = false - }) - -- check what we set is returned - local width, height, flags = love.window.getMode() - test:assertEquals(512, width, 'check window w match') - test:assertEquals(512, height, 'check window h match') - test:assertFalse(flags["fullscreen"], 'check window not fullscreen') - test:assertFalse(flags["resizable"], 'check window not resizeable') - love.window.setMode(360, 240, { - fullscreen = false, - resizable = true - }) -end - --- love.window.setPosition -love.test.window.setPosition = function(test) - -- check position is returned - love.window.setPosition(100, 100, 1) - test:waitFrames(10) - local x, y, _ = love.window.getPosition() - test:assertEquals(100, x, 'check position x') - test:assertEquals(100, y, 'check position y') -end - - --- love.window.setTitle -love.test.window.setTitle = function(test) - -- check setting title val is returned - love.window.setTitle('love.testing') - test:assertEquals('love.testing', love.window.getTitle(), 'check title matches') - love.window.setTitle('love.test') -end - - --- love.window.setVSync -love.test.window.setVSync = function(test) - love.window.setVSync(0) - test:assertNotNil(love.window.getVSync()) -end - - --- love.window.showMessageBox --- @NOTE if running headless would need to skip anyway cos can't press it -love.test.window.showMessageBox = function(test) - test:skipTest('cant test this worked') -end - - --- love.window.toPixels -love.test.window.toPixels = function(test) - -- check dpi/pixel ratio is as expected - local dpi = love.window.getDPIScale() - local pixels = love.window.toPixels(50) - test:assertEquals(50*dpi, pixels, 'check dpi ratio') -end - - --- love.window.updateMode -love.test.window.updateMode = function(test) - -- set initial mode - love.window.setMode(512, 512, { - fullscreen = false, - resizable = false - }) - -- update mode with some props but not others - love.window.updateMode(360, 240, nil) - -- check only changed values changed - local width, height, flags = love.window.getMode() - test:assertEquals(360, width, 'check window w match') - test:assertEquals(240, height, 'check window h match') - test:assertFalse(flags["fullscreen"], 'check window not fullscreen') - test:assertFalse(flags["resizable"], 'check window not resizeable') - love.window.setMode(360, 240, { -- reset - fullscreen = false, - resizable = true - }) - - -- test different combinations of the backbuffer depth/stencil buffer. - test:waitFrames(1) - love.window.updateMode(360, 240, {depth = false, stencil = false}) - test:waitFrames(1) - love.window.updateMode(360, 240, {depth = true, stencil = true}) - test:waitFrames(1) - love.window.updateMode(360, 240, {depth = true, stencil = false}) - test:waitFrames(1) - love.window.updateMode(360, 240, {depth = false, stencil = true}) -end - - diff --git a/project/love2d-api/modules/audio.md b/project/love2d-api/modules/audio.md deleted file mode 100644 index cc51084b..00000000 --- a/project/love2d-api/modules/audio.md +++ /dev/null @@ -1,40 +0,0 @@ -# `love.audio` Module API Mapping - -This document maps the functions available in the `love.audio` module of Love2D to their proposed equivalents in the Night Engine. This entire module is **Out of Scope** for the initial prototype. - -| Love2D Function (`love.audio.`) | Night Engine API (`Night.Audio.`) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|---------------------------------|-----------------------------------|---------------------------|--------------------------|------| -| `love.audio.getActiveSourceCount()` | `Night.Audio.GetActiveSourceCount()` | `public static int GetActiveSourceCount()` | Out of Scope | [ ] | -| `love.audio.getDistanceModel()` | `Night.Audio.GetDistanceModel()` | `public static Night.DistanceModel GetDistanceModel()`
`DistanceModel` enum. | Out of Scope | [ ] | -| `love.audio.getDopplerScale()` | `Night.Audio.GetDopplerScale()` | `public static double GetDopplerScale()` | Out of Scope | [ ] | -| `love.audio.getEffect(name)` | `Night.Audio.GetEffect(string name)` | `public static Night.AudioEffect? GetEffect(string name)`
`AudioEffect` would be a base class for effects. | Out of Scope | [ ] | -| `love.audio.getOrientation()` | `Night.Audio.GetListenerOrientation()` | `public static (float fx, float fy, float fz, float ux, float uy, float uz) GetListenerOrientation()` | Out of Scope | [ ] | -| `love.audio.getPosition()` | `Night.Audio.GetListenerPosition()` | `public static (float x, float y, float z) GetListenerPosition()` | Out of Scope | [ ] | -| `love.audio.getRecordingDevices()` | `Night.Audio.GetRecordingDevices()` | `public static Night.RecordingDevice[] GetRecordingDevices()` | Out of Scope | [ ] | -| `love.audio.getSourceCount()` | `Night.Audio.GetTotalSourceCount()` | `public static int GetTotalSourceCount()` | Out of Scope | [ ] | -| `love.audio.getVelocity()` | `Night.Audio.GetListenerVelocity()` | `public static (float x, float y, float z) GetListenerVelocity()` | Out of Scope | [ ] | -| `love.audio.getVolume()` | `Night.Audio.GetMasterVolume()` | `public static float GetMasterVolume()` | Out of Scope | [ ] | -| `love.audio.isEffectsSupported()` | `Night.Audio.IsEffectsSupported()` | `public static bool IsEffectsSupported()` | Out of Scope | [ ] | -| `love.audio.newSource(filename, type)` or `love.audio.newSource(decoder, type)` | `Night.Audio.NewSource(string filePath, Night.SourceType type = Static)` or `Night.Audio.NewSource(Night.Decoder decoder, Night.SourceType type = Stream)` | `public static Night.Source NewSource(...)`
`SourceType` enum: `Static`, `Stream`. `Decoder` for custom audio formats. | Out of Scope | [ ] | -| `love.audio.pause(source)` or `love.audio.pause()` | `Night.Audio.Pause(Night.Source? source = null)` | `public static void Pause(Night.Source? source = null)`
Pauses specific source or all. | Out of Scope | [ ] | -| `love.audio.play(source)` | `Night.Audio.Play(Night.Source source)` | `public static void Play(Night.Source source)` | Out of Scope | [ ] | -| `love.audio.resume(source)` or `love.audio.resume()` | `Night.Audio.Resume(Night.Source? source = null)` | `public static void Resume(Night.Source? source = null)` | Out of Scope | [ ] | -| `love.audio.setDistanceModel(model)` | `Night.Audio.SetDistanceModel(Night.DistanceModel model)` | `public static void SetDistanceModel(Night.DistanceModel model)` | Out of Scope | [ ] | -| `love.audio.setDopplerScale(scale)` | `Night.Audio.SetDopplerScale(double scale)` | `public static void SetDopplerScale(double scale)` | Out of Scope | [ ] | -| `love.audio.setEffect(name, settings)` | `Night.Audio.SetEffect(string name, Night.AudioEffectSettings settings)` | `public static bool SetEffect(string name, Night.AudioEffectSettings settings)` | Out of Scope | [ ] | -| `love.audio.setMixWithSystem(mix)` | `Night.Audio.SetMixWithSystem(bool mix)` | `public static void SetMixWithSystem(bool mix)` | Out of Scope | [ ] | -| `love.audio.setOrientation(fx, fy, fz, ux, uy, uz)` | `Night.Audio.SetListenerOrientation(...)` | `public static void SetListenerOrientation(float forwardX, ...)` | Out of Scope | [ ] | -| `love.audio.setPosition(x, y, z)` | `Night.Audio.SetListenerPosition(float x, float y, float z)` | `public static void SetListenerPosition(float x, float y, float z)` | Out of Scope | [ ] | -| `love.audio.setRecordingDevice(name)` | `Night.Audio.SetRecordingDevice(string name)` | `public static void SetRecordingDevice(string name)` | Out of Scope | [ ] | -| `love.audio.setVelocity(x, y, z)` | `Night.Audio.SetListenerVelocity(float x, float y, float z)` | `public static void SetListenerVelocity(float x, float y, float z)` | Out of Scope | [ ] | -| `love.audio.setVolume(volume)` | `Night.Audio.SetMasterVolume(float volume)` | `public static void SetMasterVolume(float volume)` | Out of Scope | [ ] | -| `love.audio.stop(source)` or `love.audio.stop()` | `Night.Audio.Stop(Night.Source? source = null)` | `public static void Stop(Night.Source? source = null)` | Out of Scope | [ ] | - -**Night Engine Specific Types:** -* `Night.Source`: Represents an audio source (sound effect or music). Would have methods like `Play()`, `Pause()`, `Stop()`, `SetVolume()`, `Seek()`, `IsPlaying()`, etc. -* `Night.SourceType`: Enum (`Static`, `Stream`). -* `Night.Decoder`: Represents a custom audio decoder. -* `Night.DistanceModel`: Enum for 3D audio distance attenuation (e.g., `None`, `Inverse`, `Linear`). -* `Night.AudioEffect`: Base class for audio effects (e.g., reverb, echo). -* `Night.AudioEffectSettings`: Base class for effect-specific settings. -* `Night.RecordingDevice`: Represents an audio recording device. diff --git a/project/love2d-api/modules/data.md b/project/love2d-api/modules/data.md deleted file mode 100644 index 10d323c9..00000000 --- a/project/love2d-api/modules/data.md +++ /dev/null @@ -1,22 +0,0 @@ -# `love.data` Module API Mapping - -This document maps the functions available in the `love.data` module of Love2D to their proposed equivalents in the Night Engine. This entire module is **Out of Scope** for the initial prototype. .NET provides extensive built-in support for these operations in namespaces like `System.IO.Compression`, `System.Security.Cryptography`, and `System.Text`. - -| Love2D Function (`love.data.`) | Night Engine API (`Night.Data` or `System` namespaces) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|--------------------------------|--------------------------------------------------------|---------------------------|--------------------------|------| -| `love.data.compress(container, format, rawstring, level)` | `Night.Data.Compress(Night.DataContainerType container, Night.CompressionFormat format, byte[] data, int? level = null)` | `public static byte[] Compress(...)`
Uses `System.IO.Compression`. | Out of Scope | [ ] | -| `love.data.decompress(container, format, compressedstring)` | `Night.Data.Decompress(Night.DataContainerType container, Night.CompressionFormat format, byte[] compressedData)` | `public static byte[] Decompress(...)` | Out of Scope | [ ] | -| `love.data.decode(container, format, encodedstring)` | `Night.Data.Decode(Night.DataContainerType container, Night.EncodingFormat format, string encodedString)` | `public static byte[] Decode(...)`
e.g., Base64, Hex. Uses `System.Convert`. | Out of Scope | [ ] | -| `love.data.encode(container, format, rawstring, linelength)` | `Night.Data.Encode(Night.DataContainerType container, Night.EncodingFormat format, byte[] data, int? lineLength = null)` | `public static string Encode(...)` | Out of Scope | [ ] | -| `love.data.getPackedSize(format)` | `Night.Data.GetPackedSize(string packFormat)` | `public static int GetPackedSize(string packFormat)`
For binary packing. | Out of Scope | [ ] | -| `love.data.hash(hashfunction, string_or_Data)` | `Night.Data.Hash(Night.HashFunction function, byte[] data)` or `Night.Data.Hash(Night.HashFunction function, string data)` | `public static string Hash(...)`
Uses `System.Security.Cryptography`. | Out of Scope | [ ] | -| `love.data.newDataView(data, offset, size)` | `Night.Data.NewDataView(byte[] data, int offset = 0, int? size = null)` | `public static Night.DataView NewDataView(...)`
Similar to `System.Memory` or `ArraySegment`. | Out of Scope | [ ] | -| `love.data.pack(format, values...)` | `Night.Data.Pack(string packFormat, params object[] values)` | `public static byte[] Pack(...)`
Binary packing. | Out of Scope | [ ] | -| `love.data.unpack(format, datastring)` | `Night.Data.Unpack(string packFormat, byte[] packedData)` | `public static object[] Unpack(...)` | Out of Scope | [ ] | - -**Night Engine Specific Types (if module were implemented):** -* `Night.DataContainerType`: Enum (e.g., `String`, `Data`). (Love2D distinction, less relevant for C# byte arrays). -* `Night.CompressionFormat`: Enum (e.g., `Gzip`, `Zlib`, `Lz4`). -* `Night.EncodingFormat`: Enum (e.g., `Base64`, `Hex`). -* `Night.HashFunction`: Enum (e.g., `Md5`, `Sha1`, `Sha256`). -* `Night.DataView`: Wrapper for a segment of byte array, similar to `System.Memory`. diff --git a/project/love2d-api/modules/event.md b/project/love2d-api/modules/event.md deleted file mode 100644 index a3dba4f8..00000000 --- a/project/love2d-api/modules/event.md +++ /dev/null @@ -1,16 +0,0 @@ -# `love.event` Module API Mapping - -This document maps the functions available in the `love.event` module of Love2D to their proposed equivalents in the Night Engine. In Night Engine, event handling is primarily managed by the engine invoking specific callback methods on the user's game class (e.g., `MyGame.KeyPressed`). Direct manipulation of an event queue by the user is **Out of Scope** for the initial prototype. - -| Love2D Function (`love.event.`) | Night Engine API (`Night.Event` or Engine Internals) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|---------------------------------|------------------------------------------------------|---------------------------|--------------------------|------| -| `love.event.clear()` | `Night.Event.ClearQueue()` (Engine internal or not exposed) | `internal static void ClearQueue()`
Clears pending events. Engine might do this per frame. | Out of Scope | [ ] | -| `love.event.poll()` | `Night.Event.Poll()` (Engine internal) | `internal static Night.EventData? Poll()`
Returns next event if any. Engine uses this in its loop. | Out of Scope | [ ] | -| `love.event.pump()` | `Night.Event.PumpEvents()` (Engine internal) | `internal static void PumpEvents()`
Processes OS events into LÖVE events. Engine does this. | Out of Scope | [ ] | -| `love.event.push(e, ...)` | `Night.Event.PushCustomEvent(string eventName, params object[] args)` | `public static void PushCustomEvent(string eventName, params object[] args)`
Allows user to push custom events. Would require a `MyGame.CustomEvent(name, args)` callback. | Out of Scope | [ ] | -| `love.event.quit(exitstatus)` | `Night.Engine.RequestQuit(int exitStatus = 0)` | `public static void RequestQuit(int exitStatus = 0)`
Pushes a quit event. | In Scope (as `Night.Engine.RequestQuit`) | [ ] | -| `love.event.wait()` | `Night.Event.Wait()` (Engine internal or not exposed) | `internal static Night.EventData Wait()`
Waits for next event. Not typical for game loops. | Out of Scope | [ ] | - -**Night Engine Specific Types (if module were implemented for custom events):** -* `Night.EventData`: A base class or struct for event information, potentially with derived types for specific events if not handled by direct callbacks. -* Custom event callbacks in `MyGame` like `MyGame.OnCustomEvent(string name, object[] args)`. diff --git a/project/love2d-api/modules/filesystem.md b/project/love2d-api/modules/filesystem.md deleted file mode 100644 index 5e23265a..00000000 --- a/project/love2d-api/modules/filesystem.md +++ /dev/null @@ -1,47 +0,0 @@ -# `love.filesystem` Module API Mapping - -This document maps the functions available in the `love.filesystem` module of Love2D to their proposed equivalents in the Night Engine. Most functions in this module are **Out of Scope** for the initial prototype. - -| Love2D Function (`love.filesystem.`) | Night Engine API (`Night.Filesystem.`) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|--------------------------------------|----------------------------------------|---------------------------|--------------------------|------| -| `love.filesystem.append(name, data, size)` | `Night.Filesystem.Append(string path, byte[] data, int? size = null)` or `Night.Filesystem.AppendText(string path, string content)` | `public static bool Append(string path, byte[] data, int? size = null)`
`public static bool AppendText(string path, string content)` | Out of Scope | [ ] | -| `love.filesystem.areSymlinksEnabled()` | `Night.Filesystem.AreSymlinksEnabled()` | `public static bool AreSymlinksEnabled()` | Out of Scope | [ ] | -| `love.filesystem.createDirectory(name)` | `Night.Filesystem.CreateDirectory(string path)` | `public static bool CreateDirectory(string path)` | Out of Scope | [ ] | -| `love.filesystem.getAppdataDirectory()` | `Night.Filesystem.GetAppDataDirectory()` | `public static string GetAppDataDirectory()` | Out of Scope | [ ] | -| `love.filesystem.getDirectoryItems(name)` | `Night.Filesystem.GetDirectoryItems(string path)` | `public static string[] GetDirectoryItems(string path)` | Out of Scope | [ ] | -| `love.filesystem.getExecutablePath()` | `Night.Filesystem.GetExecutablePath()` | `public static string GetExecutablePath()` | Out of Scope | [ ] | -| `love.filesystem.getIdentity()` | `Night.Filesystem.GetIdentity()` | `public static string GetIdentity()`
Gets the save directory identity. | Out of Scope | [ ] | -| `love.filesystem.getLastModified(name)` | `Night.Filesystem.GetInfo(string path).ModTime` | `public static DateTime GetLastModifiedTime(string path)` (or long timestamp) | Superseded by GetInfo | [x] | -| `love.filesystem.getRealDirectory(name)` | `Night.Filesystem.GetRealDirectory(string path)` | `public static string GetRealDirectory(string path)`
Resolves symlinks. | Out of Scope | [ ] | -| `love.filesystem.getSaveDirectory()` | `Night.Filesystem.GetSaveDirectory()` | `public static string GetSaveDirectory()` | Out of Scope | [ ] | -| `love.filesystem.getSize(name)` | `Night.Filesystem.GetInfo(string path).Size` | `public static long GetFileSize(string path)` | Superseded by GetInfo | [x] | -| `love.filesystem.getSource()` | `Night.Filesystem.GetSourcePath()` | `public static string GetSourcePath()`
Path to the game's source (.love file or directory). | Out of Scope | [ ] | -| `love.filesystem.getSourceBaseDirectory()` | `Night.Filesystem.GetSourceBaseDirectory()` | `public static string GetSourceBaseDirectory()` | Out of Scope | [ ] | -| `love.filesystem.getUserDirectory()` | `Night.Filesystem.GetUserDirectory()` | `public static string GetUserDirectory()` | Out of Scope | [ ] | -| `love.filesystem.getWorkingDirectory()` | `Night.Filesystem.GetWorkingDirectory()` | `public static string GetWorkingDirectory()` | Out of Scope | [ ] | -| `love.filesystem.isFused()` | `Night.Filesystem.IsFused()` | `public static bool IsFused()`
True if game is a .love file and merged with interpreter. | Out of Scope | [ ] | -| `love.filesystem.isDirectory(name)` | `Night.Filesystem.GetInfo(string path).Type == Night.FileType.Directory` | `public static bool IsDirectory(string path)` | Superseded by GetInfo | [x] | -| `love.filesystem.isFile(name)` | `Night.Filesystem.GetInfo(string path).Type == Night.FileType.File` | `public static bool IsFile(string path)` | Superseded by GetInfo | [x] | -| `love.filesystem.isSymlink(name)` | `Night.Filesystem.GetInfo(string path).Type == Night.FileType.Symlink` | `public static bool IsSymlink(string path)` | Superseded by GetInfo | [x] | -| `love.filesystem.lines(name)` | `Night.Filesystem.ReadLines(string path)` | `public static IEnumerable ReadLines(string path)` | Out of Scope | [ ] | -| `love.filesystem.load(name)` | `Night.Filesystem.LoadLuaScript(string path)` | `public static Action LoadLuaScript(string path)`
Loads and runs a Lua file. Night Engine might not support this directly. | Out of Scope | [ ] | -| `love.filesystem.mount(archive, mountpoint, appendToPath)` | `Night.Filesystem.Mount(string archivePath, string mountPoint, bool appendToSearchPath = false)` | `public static bool Mount(...)` | Out of Scope | [ ] | -| `love.filesystem.newFile(filename, mode)` | `Night.Filesystem.NewFileStream(string path, Night.FileMode mode = Read)` | `public static Night.FileStream NewFileStream(...)`
`FileMode` enum: `Read`, `Write`, `Append`. `FileStream` would be a custom stream wrapper. | Out of Scope | [ ] | -| `love.filesystem.newFileData(contents, name, decoder)` | `Night.Filesystem.NewFileData(byte[] content, string name, Night.FileDecoder decoder = Raw)` | `public static Night.FileData NewFileData(...)`
`FileDecoder` enum: `Raw`, `Base64`. `FileData` is an in-memory file. | Out of Scope | [ ] | -| `love.filesystem.read(name, size)` | `Night.Filesystem.ReadBytes(string path, int? count = null)` or `Night.Filesystem.ReadText(string path)` | `public static byte[]? ReadBytes(string path, int? count = null)`
`public static string? ReadText(string path)` | Out of Scope | [ ] | -| `love.filesystem.remove(name)` | `Night.Filesystem.Remove(string path)` | `public static bool Remove(string path)`
Removes file or empty directory. | Out of Scope | [ ] | -| `love.filesystem.setIdentity(name, appendToPath)` | `Night.Filesystem.SetIdentity(string identity, bool appendToPath = false)` | `public static void SetIdentity(...)` | Out of Scope | [ ] | -| `love.filesystem.setSymlinksEnabled(enable)` | `Night.Filesystem.SetSymlinksEnabled(bool enable)` | `public static void SetSymlinksEnabled(bool enable)` | Out of Scope | [ ] | -| `love.filesystem.setSource(path)` | `Night.Filesystem.SetSource(string path)` | `public static void SetSource(string path)` | Out of Scope | [ ] | -| `love.filesystem.unmount(archive)` | `Night.Filesystem.Unmount(string archivePath)` | `public static bool Unmount(string archivePath)` | Out of Scope | [ ] | -| `love.filesystem.write(name, data, size)` | `Night.Filesystem.WriteBytes(string path, byte[] data, int? size = null)` or `Night.Filesystem.WriteText(string path, string content)` | `public static bool WriteBytes(...)`
`public static bool WriteText(...)` | Out of Scope | [ ] | - -| `love.filesystem.getInfo(path, filtertype, info)` | `Night.Filesystem.GetInfo(string path, Night.FileType? filterType = null, Night.FileSystemInfo? existingInfo = null)` | `public static Night.FileSystemInfo? GetInfo(string path, Night.FileType? filterType = null)`
`public static Night.FileSystemInfo? GetInfo(string path, Night.FileSystemInfo info)`
`public static Night.FileSystemInfo? GetInfo(string path, Night.FileType filterType, Night.FileSystemInfo info)` | In Scope | [x] | - -**Night Engine Specific Types:** -* `Night.FileMode`: Enum (`Read`, `Write`, `Append`). -* `Night.FileStream`: Custom stream wrapper for file operations. -* `Night.FileData`: Represents an in-memory file. -* `Night.FileDecoder`: Enum (`Raw`, `Base64`). -* `Night.FileType`: Enum (`File`, `Directory`, `Symlink`, `Other`, `None`). -* `Night.FileSystemInfo`: Class (Properties: `Type`, `Size`, `ModTime`). diff --git a/project/love2d-api/modules/font.md b/project/love2d-api/modules/font.md deleted file mode 100644 index 82e07454..00000000 --- a/project/love2d-api/modules/font.md +++ /dev/null @@ -1,22 +0,0 @@ -# `love.font` Module API Mapping - -This document maps the functions available in the `love.font` module of Love2D to their proposed equivalents in the Night Engine. This entire module is **Out of Scope** for the initial prototype. The primary way to get a font object in Night Engine would be `Night.Graphics.NewFont()`. - -| Love2D Function (`love.font.`) | Night Engine API (`Night.Font` methods or `Night.Graphics`) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|--------------------------------|-------------------------------------------------------------|---------------------------|--------------------------|------| -| `love.font.newRasterizer(filename, size)` or `love.font.newRasterizer(filedata, size)` or `love.font.newRasterizer(size)` | `Night.Graphics.NewFontRasterizer(string filePath, int size)` etc. | `public static Night.FontRasterizer NewFontRasterizer(...)`
Creates a font rasterizer. `Night.Font` would likely encapsulate this. | Out of Scope | [ ] | -| `love.font.newGlyphData(rasterizer, glyph)` | `(Night.FontRasterizer).NewGlyphData(char glyph)` or `(Night.FontRasterizer).NewGlyphData(uint glyph)` | `public Night.GlyphData NewGlyphData(char glyph)` (method on `FontRasterizer` or `Font`) | Out of Scope | [ ] | - -**Related functionality in Night Engine (on `Night.Font` objects):** -* Getting font height: `myFont.GetHeight()` -* Getting ascent/descent: `myFont.GetAscent()`, `myFont.GetDescent()` -* Getting baseline: `myFont.GetBaseline()` -* Getting line height: `myFont.GetLineHeight()` -* Getting text width: `myFont.GetWidth(string text)` -* Wrapping text: `myFont.Wrap(string text, float wrapLimit)` -* Setting fallback fonts: `myFont.SetFallback(Night.Font fallback1, ...)` - -**Night Engine Specific Types:** -* `Night.Font`: Represents a loaded font. Created via `Night.Graphics.NewFont()`. Would have methods for metrics and properties. -* `Night.FontRasterizer`: Internal or advanced type for rasterizing glyphs. -* `Night.GlyphData`: Represents rasterized data for a single glyph. diff --git a/project/love2d-api/modules/graphics.md b/project/love2d-api/modules/graphics.md deleted file mode 100644 index 6bc2efee..00000000 --- a/project/love2d-api/modules/graphics.md +++ /dev/null @@ -1,110 +0,0 @@ -# `love.graphics` Module API Mapping - -This document maps the functions available in the `love.graphics` module of Love2D to their proposed equivalents in the Night Engine. - -| Love2D Function (`love.graphics.`) | Night Engine API (`Night.Graphics.`) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|------------------------------------|--------------------------------------|---------------------------|--------------------------|------| -| `love.graphics.arc(mode, arcType, x, y, radius, angle1, angle2, segments)` | `Night.Graphics.DrawArc(Night.DrawMode mode, Night.ArcType arcType, float x, float y, float radius, float angle1, float angle2, int? segments = null)` | `public static void DrawArc(Night.DrawMode mode, Night.ArcType arcType, float x, float y, float radius, float angle1, float angle2, int? segments = null)`
`DrawMode` enum: `Fill`, `Line`. `ArcType` enum: `Open`, `Closed`, `Pie`. Segments auto-calculated if null. | Out of Scope | [ ] | -| `love.graphics.circle(mode, x, y, radius, segments)` | `Night.Graphics.DrawCircle(Night.DrawMode mode, float x, float y, float radius, int? segments = null)` | `public static void DrawCircle(Night.DrawMode mode, float x, float y, float radius, int? segments = null)` | Out of Scope | [ ] | -| `love.graphics.clear(r, g, b, a)` or `love.graphics.clear(color)` | `Night.Graphics.Clear(Night.Color color)` or `Night.Graphics.Clear(byte r, byte g, byte b, byte a = 255)` | `public static void Clear(Night.Color color)`
`public static void Clear(byte r, byte g, byte b, byte a = 255)` | In Scope | [ ] | -| `love.graphics.discard(discardColor, discardStencil)` | `Night.Graphics.Discard(bool discardColor = true, bool discardStencil = true)` | `public static void Discard(bool discardColor = true, bool discardStencil = true)`
Discards render target contents. | Out of Scope | [ ] | -| `love.graphics.draw(drawable, x, y, r, sx, sy, ox, oy, kx, ky)` | `Night.Graphics.Draw(Night.IDrawable drawable, float x, float y, float rotation = 0, float scaleX = 1, float scaleY = 1, float offsetX = 0, float offsetY = 0, float shearX = 0, float shearY = 0)` | `public static void Draw(Night.IDrawable drawable, float x, float y, float rotation = 0, float scaleX = 1, float scaleY = 1, float offsetX = 0, float offsetY = 0, float shearX = 0, float shearY = 0)`
`IDrawable` could be `Sprite`, `Text`, `Shape`, etc. | In Scope (for Sprites) | [ ] | -| `love.graphics.draw(texture, quad, x, y, r, sx, sy, ox, oy, kx, ky)` | `Night.Graphics.Draw(Night.Texture texture, Night.Quad quad, float x, float y, float rotation = 0, float scaleX = 1, float scaleY = 1, float offsetX = 0, float offsetY = 0, float shearX = 0, float shearY = 0)` | `public static void Draw(Night.Texture texture, Night.Quad quad, ...)`
For drawing parts of a texture. | In Scope (for Sprites with Quads) | [ ] | -| `love.graphics.ellipse(mode, x, y, radiusx, radiusy, segments)` | `Night.Graphics.DrawEllipse(Night.DrawMode mode, float x, float y, float radiusX, float radiusY, int? segments = null)` | `public static void DrawEllipse(Night.DrawMode mode, float x, float y, float radiusX, float radiusY, int? segments = null)` | Out of Scope | [ ] | -| `love.graphics.getBackgroundColor()` | `Night.Graphics.GetBackgroundColor()` | `public static Night.Color GetBackgroundColor()` | In Scope | [ ] | -| `love.graphics.getBlendMode()` | `Night.Graphics.GetBlendMode()` | `public static (Night.BlendMode mode, Night.BlendAlphaMode alphaMode) GetBlendMode()` | Out of Scope | [ ] | -| `love.graphics.getCanvas()` | `Night.Graphics.GetRenderTarget()` | `public static Night.IRenderTarget GetRenderTarget()`
Returns current render target (Canvas or screen). | Out of Scope | [ ] | -| `love.graphics.getCanvasFormats()` | `Night.Graphics.GetSupportedRenderTargetFormats()` | `public static Night.RenderTargetFormat[] GetSupportedRenderTargetFormats()` | Out of Scope | [ ] | -| `love.graphics.getColor()` | `Night.Graphics.GetColor()` | `public static Night.Color GetColor()` | In Scope | [ ] | -| `love.graphics.getColorMask()` | `Night.Graphics.GetColorMask()` | `public static (bool r, bool g, bool b, bool a) GetColorMask()` | Out of Scope | [ ] | -| `love.graphics.getDefaultFilter()` | `Night.Graphics.GetDefaultFilter()` | `public static Night.FilterMode GetDefaultFilter()`
`FilterMode` enum: `Linear`, `Nearest`. | In Scope | [ ] | -| `love.graphics.getDepthMode()` | `Night.Graphics.GetDepthMode()` | `public static (Night.CompareMode? mode, bool write) GetDepthMode()` | Out of Scope | [ ] | -| `love.graphics.getDimensions()` | `Night.Graphics.GetDimensions()` | `public static (int width, int height) GetDimensions()`
Gets dimensions of current render target (screen or canvas). | In Scope | [ ] | -| `love.graphics.getFont()` | `Night.Graphics.GetFont()` | `public static Night.Font GetFont()` | Out of Scope | [ ] | -| `love.graphics.getHeight()` | `Night.Graphics.GetHeight()` | `public static int GetHeight()`
Height of current render target. | In Scope | [ ] | -| `love.graphics.getLineWidth()` | `Night.Graphics.GetLineWidth()` | `public static float GetLineWidth()` | Out of Scope | [ ] | -| `love.graphics.getLineStyle()` | `Night.Graphics.GetLineStyle()` | `public static Night.LineStyle GetLineStyle()`
`LineStyle` enum: `Smooth`, `Rough`. | Out of Scope | [ ] | -| `love.graphics.getLineJoin()` | `Night.Graphics.GetLineJoin()` | `public static Night.LineJoin GetLineJoin()`
`LineJoin` enum: `None`, `Miter`, `Bevel`. | Out of Scope | [ ] | -| `love.graphics.getPointSize()` | `Night.Graphics.GetPointSize()` | `public static float GetPointSize()` | Out of Scope | [ ] | -| `love.graphics.getRendererInfo()` | `Night.Graphics.GetRendererInfo()` | `public static Night.RendererInfo GetRendererInfo()`
`RendererInfo` class: `Name`, `Version`, `Vendor`, `Device`. | In Scope | [ ] | -| `love.graphics.getScissor()` | `Night.Graphics.GetScissor()` | `public static Night.Rectangle? GetScissor()` | Out of Scope | [ ] | -| `love.graphics.getShader()` | `Night.Graphics.GetShader()` | `public static Night.Shader GetShader()` | Out of Scope | [ ] | -| `love.graphics.getStats()` | `Night.Graphics.GetStats()` | `public static Night.GraphicsStats GetStats()`
`GraphicsStats` class: `DrawCalls`, `CanvasSwitches`, `ShaderSwitches`, etc. | In Scope (Basic stats) | [ ] | -| `love.graphics.getStencilTest()` | `Night.Graphics.GetStencilTest()` | `public static (Night.CompareMode? mode, int value) GetStencilTest()` | Out of Scope | [ ] | -| `love.graphics.getWidth()` | `Night.Graphics.GetWidth()` | `public static int GetWidth()`
Width of current render target. | In Scope | [ ] | -| `love.graphics.intersectScissor(x, y, width, height)` | `Night.Graphics.IntersectScissor(int x, int y, int width, int height)` | `public static void IntersectScissor(int x, int y, int width, int height)` | Out of Scope | [ ] | -| `love.graphics.isWireframe()` | `Night.Graphics.IsWireframe()` | `public static bool IsWireframe()` | Out of Scope | [ ] | -| `love.graphics.line(x1, y1, x2, y2, ...)` or `love.graphics.line(points)` | `Night.Graphics.DrawLine(params float[] points)` or `Night.Graphics.DrawLine(Night.PointF[] points)` | `public static void DrawLine(params float[] points)`
`public static void DrawLine(Night.PointF[] points)` | Out of Scope | [ ] | -| `love.graphics.newCanvas(width, height, format, msaa)` | `Night.Graphics.NewRenderTarget(int width, int height, Night.RenderTargetFormat format = Default, int msaa = 0)` | `public static Night.IRenderTarget NewRenderTarget(...)` | Out of Scope | [ ] | -| `love.graphics.newFont(filename, size)` or `love.graphics.newFont(size)` | `Night.Graphics.NewFont(string filePath, int size)` or `Night.Graphics.NewFont(int size)` | `public static Night.Font NewFont(...)`
Uses default font if no path. | Out of Scope | [ ] | -| `love.graphics.newImage(filename)` | `Night.Graphics.NewImage(string filePath)` | `public static Night.Sprite NewImage(string filePath)`
PRD refers to `Sprite` as return type. | In Scope | [ ] | -| `love.graphics.newImageFont(filename, glyphs, extraspacing)` | `Night.Graphics.NewImageFont(string filePath, string glyphs, int extraSpacing = 0)` | `public static Night.Font NewImageFont(...)` | Out of Scope | [ ] | -| `love.graphics.newQuad(x, y, width, height, sw, sh)` | `Night.Graphics.NewQuad(float x, float y, float width, float height, float sourceWidth, float sourceHeight)` | `public static Night.Quad NewQuad(...)` | In Scope | [ ] | -| `love.graphics.newShader(pixelcode, vertexcode)` | `Night.Graphics.NewShader(string pixelShaderCode, string vertexShaderCode = null)` | `public static Night.Shader NewShader(...)` | Out of Scope | [ ] | -| `love.graphics.newSpriteBatch(texture, size, usagehint)` | `Night.Graphics.NewSpriteBatch(Night.Texture texture, int size, Night.UsageHint hint = Dynamic)` | `public static Night.SpriteBatch NewSpriteBatch(...)` | Out of Scope | [ ] | -| `love.graphics.newText(font, textparts)` | `Night.Graphics.NewText(Night.Font font, params (string text, Night.Color? color)[] textParts)` | `public static Night.Text NewText(...)` | Out of Scope | [ ] | -| `love.graphics.newVideo(filename, options)` | `Night.Graphics.NewVideo(string filePath, Night.VideoOptions? options = null)` | `public static Night.Video NewVideo(...)` | Out of Scope | [ ] | -| `love.graphics.origin()` | `Night.Graphics.ResetTransform()` | `public static void ResetTransform()`
Resets current transformation to identity. | In Scope | [ ] | -| `love.graphics.points(coords, colors)` | `Night.Graphics.DrawPoints(Night.PointF[] positions, Night.Color[]? colors = null)` | `public static void DrawPoints(...)` | Out of Scope | [ ] | -| `love.graphics.polygon(mode, vertices)` | `Night.Graphics.DrawPolygon(Night.DrawMode mode, params Night.PointF[] vertices)` | `public static void DrawPolygon(...)` | Out of Scope | [ ] | -| `love.graphics.pop()` | `Night.Graphics.PopTransform()` | `public static void PopTransform()` | In Scope | [ ] | -| `love.graphics.present()` | `Night.Graphics.Present()` | `public static void Present()`
Called by engine after `MyGame.Draw()`. | In Scope | [ ] | -| `love.graphics.print(text, x, y, r, sx, sy, ox, oy, kx, ky)` | `Night.Graphics.Print(string text, float x, float y, float rotation = 0, ...)` | `public static void Print(string text, float x, float y, ...)`
Uses current font. | Out of Scope | [ ] | -| `love.graphics.printf(text, x, y, limit, align, r, sx, sy, ox, oy, kx, ky)` | `Night.Graphics.PrintF(string text, float x, float y, float wrapLimit, Night.TextAlign align = Left, ...)` | `public static void PrintF(...)` | Out of Scope | [ ] | -| `love.graphics.push(stacktype)` | `Night.Graphics.PushTransform(Night.StackType type = All)` | `public static void PushTransform(Night.StackType type = Night.StackType.All)`
`StackType` enum: `All`, `Transform`. | In Scope | [ ] | -| `love.graphics.rectangle(mode, x, y, width, height, rx, ry, segments)` | `Night.Graphics.DrawRectangle(Night.DrawMode mode, float x, float y, float width, float height, float cornerRadiusX = 0, float cornerRadiusY = 0, int? segments = null)` | `public static void DrawRectangle(...)` | Out of Scope | [ ] | -| `love.graphics.reset()` | `Night.Graphics.ResetState()` | `public static void ResetState()`
Resets all graphics state (color, blend mode, etc.) | In Scope | [ ] | -| `love.graphics.rotate(angle)` | `Night.Graphics.Rotate(float angleInRadians)` | `public static void Rotate(float angleInRadians)` | In Scope | [ ] | -| `love.graphics.scale(sx, sy)` | `Night.Graphics.Scale(float scaleX, float scaleY)` | `public static void Scale(float scaleX, float scaleY)` | In Scope | [ ] | -| `love.graphics.shear(kx, ky)` | `Night.Graphics.Shear(float shearX, float shearY)` | `public static void Shear(float shearX, float shearY)` | In Scope | [ ] | -| `love.graphics.setBackgroundColor(r, g, b, a)` or `love.graphics.setBackgroundColor(color)` | `Night.Graphics.SetBackgroundColor(Night.Color color)` or `Night.Graphics.SetBackgroundColor(byte r, byte g, byte b, byte a = 255)` | `public static void SetBackgroundColor(...)` | In Scope | [ ] | -| `love.graphics.setBlendMode(mode, alphamode)` | `Night.Graphics.SetBlendMode(Night.BlendMode mode, Night.BlendAlphaMode alphaMode = Multiply)` | `public static void SetBlendMode(...)` | Out of Scope | [ ] | -| `love.graphics.setCanvas(canvas)` or `love.graphics.setCanvas()` | `Night.Graphics.SetRenderTarget(Night.IRenderTarget? target = null)` | `public static void SetRenderTarget(Night.IRenderTarget? target = null)`
`null` sets to screen. | Out of Scope | [ ] | -| `love.graphics.setColor(r, g, b, a)` or `love.graphics.setColor(color)` | `Night.Graphics.SetColor(Night.Color color)` or `Night.Graphics.SetColor(byte r, byte g, byte b, byte a = 255)` | `public static void SetColor(...)` | In Scope | [ ] | -| `love.graphics.setColorMask(r, g, b, a)` | `Night.Graphics.SetColorMask(bool r, bool g, bool b, bool a)` | `public static void SetColorMask(bool r, bool g, bool b, bool a)` | Out of Scope | [ ] | -| `love.graphics.setDefaultFilter(min, mag, anisotropy)` | `Night.Graphics.SetDefaultFilter(Night.FilterMode min, Night.FilterMode? mag = null, float anisotropy = 1.0f)` | `public static void SetDefaultFilter(...)`
`mag` defaults to `min` if null. | In Scope | [ ] | -| `love.graphics.setDepthMode(mode, write)` | `Night.Graphics.SetDepthMode(Night.CompareMode? mode, bool write)` | `public static void SetDepthMode(Night.CompareMode? mode, bool write)` | Out of Scope | [ ] | -| `love.graphics.setFont(font)` | `Night.Graphics.SetFont(Night.Font font)` | `public static void SetFont(Night.Font font)` | Out of Scope | [ ] | -| `love.graphics.setLineWidth(width)` | `Night.Graphics.SetLineWidth(float width)` | `public static void SetLineWidth(float width)` | Out of Scope | [ ] | -| `love.graphics.setLineStyle(style)` | `Night.Graphics.SetLineStyle(Night.LineStyle style)` | `public static void SetLineStyle(Night.LineStyle style)` | Out of Scope | [ ] | -| `love.graphics.setLineJoin(join)` | `Night.Graphics.SetLineJoin(Night.LineJoin join)` | `public static void SetLineJoin(Night.LineJoin join)` | Out of Scope | [ ] | -| `love.graphics.setPointSize(size)` | `Night.Graphics.SetPointSize(float size)` | `public static void SetPointSize(float size)` | Out of Scope | [ ] | -| `love.graphics.setScissor(x, y, width, height)` or `love.graphics.setScissor()` | `Night.Graphics.SetScissor(int? x, int? y, int? width, int? height)` or `Night.Graphics.SetScissor(Night.Rectangle? rect)` | `public static void SetScissor(Night.Rectangle? rect)`
`null` disables scissor. | Out of Scope | [ ] | -| `love.graphics.setShader(shader)` or `love.graphics.setShader()` | `Night.Graphics.SetShader(Night.Shader? shader = null)` | `public static void SetShader(Night.Shader? shader = null)` | Out of Scope | [ ] | -| `love.graphics.setStencilTest(comparemode, comparevalue)` or `love.graphics.setStencilTest()` | `Night.Graphics.SetStencilTest(Night.CompareMode? mode = null, int value = 0)` | `public static void SetStencilTest(Night.CompareMode? mode = null, int value = 0)` | Out of Scope | [ ] | -| `love.graphics.setWireframe(enable)` | `Night.Graphics.SetWireframe(bool enable)` | `public static void SetWireframe(bool enable)` | Out of Scope | [ ] | -| `love.graphics.stencil(stencilfunction, action, value, keepvalues)` | `Night.Graphics.Stencil(Action stencilFunction, Night.StencilAction action = Replace, int value = 1, bool keepValues = false)` | `public static void Stencil(...)`
Complex. | Out of Scope | [ ] | -| `love.graphics.translate(dx, dy)` | `Night.Graphics.Translate(float deltaX, float deltaY)` | `public static void Translate(float deltaX, float deltaY)` | In Scope | [ ] | -| `love.graphics.transformPoint(worldX, worldY)` | `Night.Graphics.TransformPoint(float worldX, float worldY)` | `public static (float screenX, float screenY) TransformPoint(float worldX, float worldY)` | In Scope | [ ] | -| `love.graphics.inverseTransformPoint(screenX, screenY)` | `Night.Graphics.InverseTransformPoint(float screenX, float screenY)` | `public static (float worldX, float worldY) InverseTransformPoint(float screenX, float screenY)` | In Scope | [ ] | - -**Night Engine Specific Types:** -* `Night.DrawMode`: Enum (`Fill`, `Line`). -* `Night.ArcType`: Enum (`Open`, `Closed`, `Pie`). -* `Night.IDrawable`: Interface for drawable objects (Sprite, Text, etc.). -* `Night.Texture`: Represents a texture (likely part of `Night.Image` or `Night.Sprite`). -* `Night.Quad`: Represents a portion of a texture. -* `Night.Color`: Struct/class for color (RGBA). -* `Night.BlendMode`: Enum for blending (e.g., `Alpha`, `Add`, `Subtract`, `Multiply`). -* `Night.BlendAlphaMode`: Enum for alpha blending (e.g., `Multiply`, `PreMultiplied`). -* `Night.IRenderTarget`: Interface for render targets (Canvas or screen). -* `Night.RenderTargetFormat`: Enum for pixel formats of render targets. -* `Night.FilterMode`: Enum (`Linear`, `Nearest`). -* `Night.CompareMode`: Enum for depth/stencil tests (e.g., `Less`, `Equal`, `Greater`, `Always`). -* `Night.Font`: Represents a font. -* `Night.LineStyle`: Enum (`Smooth`, `Rough`). -* `Night.LineJoin`: Enum (`None`, `Miter`, `Bevel`). -* `Night.RendererInfo`: Class with properties like `Name`, `Version`, `Vendor`, `Device`. -* `Night.Rectangle`: Struct/class for a rectangle (X, Y, Width, Height). -* `Night.Shader`: Represents a shader program. -* `Night.GraphicsStats`: Class for graphics statistics. -* `Night.PointF`: Struct for a 2D point with float coordinates. -* `Night.Sprite`: Represents an image that can be drawn. (Corresponds to Love2D Image) -* `Night.SpriteBatch`: For optimized drawing of many sprites from the same texture. -* `Night.Text`: Represents renderable text. -* `Night.Video`: Represents a video that can be drawn. -* `Night.VideoOptions`: Options for video loading. -* `Night.StackType`: Enum (`All`, `Transform`). -* `Night.TextAlign`: Enum (`Left`, `Center`, `Right`, `Justify`). -* `Night.StencilAction`: Enum for stencil operations (e.g., `Keep`, `Replace`, `Increment`). -* `Night.UsageHint`: Enum for SpriteBatch (`Static`, `Dynamic`, `Stream`). diff --git a/project/love2d-api/modules/image.md b/project/love2d-api/modules/image.md deleted file mode 100644 index f06f1885..00000000 --- a/project/love2d-api/modules/image.md +++ /dev/null @@ -1,22 +0,0 @@ -# `love.image` Module API Mapping - -This document maps the functions available in the `love.image` module of Love2D to their proposed equivalents in the Night Engine. The functionality of this module is often integrated into `Night.Sprite` or `Night.Texture` objects, or handled during image loading. Most direct `love.image` functions are **Out of Scope** for the initial prototype as standalone static methods. - -| Love2D Function (`love.image.`) | Night Engine API (`Night.Image` or `Texture`/`Sprite` methods) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|---------------------------------|----------------------------------------------------------------|---------------------------|--------------------------|------| -| `love.image.newImageData(width, height, format, data)` | `Night.Image.NewImageData(int width, int height, Night.PixelFormat format = RGBA8, byte[]? data = null)` | `public static Night.ImageData NewImageData(...)`
Creates raw image data. `Night.ImageData` would be a class/struct. | Out of Scope | [ ] | -| `love.image.isCompressed(filename)` or `love.image.isCompressed(filedata)` | `Night.Image.IsCompressed(string filePath)` or `Night.Image.IsCompressed(Night.FileData fileData)` | `public static bool IsCompressed(...)`
Checks if an image file is a compressed format LÖVE can load. | Out of Scope | [ ] | -| `love.image.newCompressedData(filename)` | `Night.Image.NewCompressedData(string filePath)` | `public static Night.CompressedImageData NewCompressedData(string filePath)`
Loads a compressed image file (e.g. DDS, KTX) into a special data object. | Out of Scope | [ ] | - -**Related functionality in Night Engine (on `Sprite` or `Texture` or `ImageData` objects):** -* Getting dimensions: `mySprite.GetWidth()`, `mySprite.GetHeight()` -* Getting format: `myImageData.GetFormat()` -* Manipulating pixel data: `myImageData.GetPixel(x,y)`, `myImageData.SetPixel(x,y,color)` (Likely Out of Scope for prototype) -* Encoding/Decoding: Functionality to save an `ImageData` to a file (e.g., `myImageData.Encode("png", "filename.png")`) is Out of Scope. - -**Night Engine Specific Types:** -* `Night.ImageData`: Represents raw, uncompressed image data. Could have methods like `GetWidth()`, `GetHeight()`, `GetPixel()`, `SetPixel()`. -* `Night.PixelFormat`: Enum for pixel formats (e.g., `RGBA8`, `RGB8`, `Luminance8`). -* `Night.FileData`: Represents file data in memory (from `Night.Filesystem`). -* `Night.CompressedImageData`: Represents compressed image data. -* `Night.Sprite`: The primary object for loaded images, returned by `Night.Graphics.NewImage()`. It would internally manage texture data. diff --git a/project/love2d-api/modules/joystick.md b/project/love2d-api/modules/joystick.md deleted file mode 100644 index 16ea81e7..00000000 --- a/project/love2d-api/modules/joystick.md +++ /dev/null @@ -1,35 +0,0 @@ -# `love.joystick` Module API Mapping - -This document maps the functions available in the `love.joystick` module of Love2D to their proposed equivalents in the Night Engine. This entire module is **Out of Scope** for the initial prototype. Joystick event callbacks are noted in the `love` module mapping. - -| Love2D Function (`love.joystick.`) | Night Engine API (`Night.Joystick` or `Joystick` instance methods) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|------------------------------------|--------------------------------------------------------------------|---------------------------|--------------------------|------| -| `love.joystick.getJoysticks()` | `Night.Joystick.GetJoysticks()` | `public static Night.Joystick[] GetJoysticks()`
Returns all connected joysticks. | Out of Scope | [ ] | -| `love.joystick.getJoystickCount()` | `Night.Joystick.GetJoystickCount()` | `public static int GetJoystickCount()` | Out of Scope | [ ] | -| `love.joystick.loadGamepadMappings(filename)` or `love.joystick.loadGamepadMappings(string)` | `Night.Joystick.LoadGamepadMappings(string pathOrString)` | `public static bool LoadGamepadMappings(string pathOrString)` | Out of Scope | [ ] | -| `love.joystick.saveGamepadMappings(joystick)` | `(Night.Joystick).SaveGamepadMappings()` | `public string SaveGamepadMappings()` (Method on `Joystick` instance) | Out of Scope | [ ] | -| `love.joystick.setGamepadMapping(guid, buttonOrAxis, inputtype, inputindex, hatdirection)` | `Night.Joystick.SetGamepadMapping(string guid, ...)` | Complex mapping function. | Out of Scope | [ ] | - -**Functionality on `Night.Joystick` instances (if implemented):** -* `joystick.isConnected()` -* `joystick.getName()` -* `joystick.getID()` (instance ID) -* `joystick.getGUID()` -* `joystick.getAxisCount()` -* `joystick.getButtonCount()` -* `joystick.getHatCount()` -* `joystick.getAxis(axisindex)` -* `joystick.getAxes()` -* `joystick.isDown(buttonindex, ...)` -* `joystick.getHat(hatindex)` -* `joystick.isGamepad()` -* `joystick.getGamepadAxis(axis)` -* `joystick.isGamepadDown(button)` -* `joystick.setVibration(left, right, duration)` -* `joystick.hasVibration()` - -**Night Engine Specific Types:** -* `Night.Joystick`: Represents a joystick/gamepad device. -* `Night.GamepadAxis`: Enum for standard gamepad axes (e.g., `LeftX`, `LeftY`, `RightX`, `RightY`, `TriggerLeft`, `TriggerRight`). -* `Night.GamepadButton`: Enum for standard gamepad buttons (e.g., `A`, `B`, `X`, `Y`, `Start`, `Select`, `DPadUp`). -* `Night.HatDirection`: Enum for hat switch directions (e.g., `Centered`, `Up`, `Down`, `Left`, `Right`, `UpLeft`). diff --git a/project/love2d-api/modules/keyboard.md b/project/love2d-api/modules/keyboard.md deleted file mode 100644 index 4684f79d..00000000 --- a/project/love2d-api/modules/keyboard.md +++ /dev/null @@ -1,19 +0,0 @@ -# `love.keyboard` Module API Mapping - -This document maps the functions available in the `love.keyboard` module of Love2D to their proposed equivalents in the Night Engine. - -| Love2D Function (`love.keyboard.`) | Night Engine API (`Night.Keyboard.`) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|------------------------------------|--------------------------------------|---------------------------|--------------------------|------| -| `love.keyboard.isDown(key)` | `Night.Keyboard.IsDown(Night.KeyCode key)` | `public static bool IsDown(Night.KeyCode key)`
Checks if specific keys are held down. `Night.KeyCode` enum will map to SDL scancodes. | In Scope | [ ] | -| `love.keyboard.isScancodeDown(scancode)` | `Night.Keyboard.IsScancodeDown(Night.Scancode scancode)` | `public static bool IsScancodeDown(Night.Scancode scancode)`
`Night.Scancode` would be an enum closely matching SDL scancodes. May be internal or less used if `KeyCode` is preferred. | In Scope (Lower priority than `IsDown`) | [ ] | -| `love.keyboard.getKeyFromScancode(scancode)` | `Night.Keyboard.GetKeyFromScancode(Night.Scancode scancode)` | `public static Night.KeyCode GetKeyFromScancode(Night.Scancode scancode)` | In Scope (Helper for input mapping) | [ ] | -| `love.keyboard.getScancodeFromKey(key)` | `Night.Keyboard.GetScancodeFromKey(Night.KeyCode key)` | `public static Night.Scancode GetScancodeFromKey(Night.KeyCode key)` | In Scope (Helper for input mapping) | [ ] | -| `love.keyboard.setKeyRepeat(enable)` | `Night.Keyboard.SetKeyRepeatEnabled(bool enabled)` | `public static void SetKeyRepeatEnabled(bool enabled)`
Enables or disables key repeat for `love.keypressed`. SDL handles this by default; this might control if `isRepeat` is true in `MyGame.KeyPressed`. | In Scope (Verify SDL behavior) | [ ] | -| `love.keyboard.hasKeyRepeat()` | `Night.Keyboard.HasKeyRepeat()` | `public static bool HasKeyRepeat()`
Checks if key repeat is enabled. | In Scope (Verify SDL behavior) | [ ] | -| `love.keyboard.setTextInput(enable, x, y, w, h)` | `Night.Keyboard.SetTextInputRect(bool enable, Night.Rectangle? rect = null)` | `public static void SetTextInputRect(bool enable, Night.Rectangle? rect = null)`
For on-screen keyboards on touch devices. `rect` defines text input area. | Out of Scope | [ ] | -| `love.keyboard.hasScreenKeyboard()` | `Night.Keyboard.HasScreenKeyboard()` | `public static bool HasScreenKeyboard()` | Out of Scope | [ ] | - -**Night Engine Specific Types:** -* `Night.KeyCode`: Enum representing keyboard keys (e.g., `A`, `Space`, `Return`). This will be mapped to SDL scancodes. -* `Night.Scancode`: Enum representing platform-independent physical key codes (e.g., `SDL_SCANCODE_A`). -* `Night.Rectangle`: Struct/class for a rectangle (X, Y, Width, Height). diff --git a/project/love2d-api/modules/love.md b/project/love2d-api/modules/love.md deleted file mode 100644 index 1635707b..00000000 --- a/project/love2d-api/modules/love.md +++ /dev/null @@ -1,39 +0,0 @@ -# `love` Module API Mapping - -This document maps the functions available in the base `love` module of Love2D to their proposed equivalents in the Night Engine. - -| Love2D Function (`love.`) | Night Engine API (`Night.`) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|---------------------------|-----------------------------|---------------------------|--------------------------|------| -| `love.getVersion()` | `Night.Engine.GetVersion()` | `public static string GetVersion()`
Returns a string like "Major.Minor.Revision Codename". | In Scope | [ ] | -| `love.setDeprecationOutput(boolean enabled)` | `Night.Engine.SetDeprecationOutput(bool enabled)` | `public static void SetDeprecationOutput(bool enabled)`
Controls whether Love2D's deprecation warnings are output. May or may not be relevant for Night. | Out of Scope (Low Priority) | [ ] | -| `love.run()` | `Night.Engine.Run()` or `Night.Engine.Run(IGame gameInstance)` | `public static void Run() where T : IGame, new()`
`public static void Run(IGame gameInstance)`
This is the main entry point that starts the game loop. The user provides a game class/instance. | In Scope | [x] | -| `love.load(arg)` | `MyGame.Load(string[] args)` | Implemented by the user in their game class: `void Load(string[] args);`
Called once at the beginning. `arg` in Love2D contains command-line arguments. | In Scope | [x] | -| `love.update(dt)` | `MyGame.Update(double deltaTime)` | Implemented by the user: `void Update(double deltaTime);`
Called every frame. | In Scope | [x] | -| `love.draw()` | `MyGame.Draw()` | Implemented by the user: `void Draw();`
Called every frame after update. | In Scope | [x] | -| `love.quit()` | `MyGame.Quit()` or `Night.Engine.Quit()` | User implementation: `bool Quit();` (return true to allow quit)
Engine initiated: `Night.Engine.RequestQuit()` or similar. Love2D `love.quit` can also be an event. | In Scope (Basic window close event handling) | [ ] | -| `love.focus(f)` | `MyGame.FocusChanged(bool hasFocus)` | User implementation: `void FocusChanged(bool hasFocus);` | In Scope | [ ] | -| `love.mousefocus(f)` | `MyGame.MouseFocusChanged(bool hasFocus)` | User implementation: `void MouseFocusChanged(bool hasFocus);` | Out of Scope (Covered by general focus) | [ ] | -| `love.visible(v)` | `MyGame.VisibilityChanged(bool isVisible)` | User implementation: `void VisibilityChanged(bool isVisible);` | In Scope | [ ] | -| `love.keypressed(key, scancode, isrepeat)` | `MyGame.KeyPressed(Night.KeyCode key, string scancode, bool isRepeat)` | User implementation: `void KeyPressed(Night.KeyCode key, /* SDL_Scancode scancode, */ bool isRepeat);`
`scancode` might be abstracted away or be an internal SDL detail. | In Scope | [ ] | -| `love.keyreleased(key, scancode)` | `MyGame.KeyReleased(Night.KeyCode key, string scancode)` | User implementation: `void KeyReleased(Night.KeyCode key /*, SDL_Scancode scancode */);` | In Scope | [ ] | -| `love.textinput(text)` | `MyGame.TextInput(string text)` | User implementation: `void TextInput(string text);` | In Scope (but low priority for prototype) | [ ] | -| `love.mousepressed(x, y, button, istouch, presses)` | `MyGame.MousePressed(int x, int y, Night.MouseButton button, bool isTouch, int presses)` | User implementation: `void MousePressed(int x, int y, Night.MouseButton button, int presses);`
`isTouch` might be handled separately if touch events are distinct. | In Scope | [ ] | -| `love.mousereleased(x, y, button, istouch)` | `MyGame.MouseReleased(int x, int y, Night.MouseButton button, bool isTouch)` | User implementation: `void MouseReleased(int x, int y, Night.MouseButton button);` | In Scope | [ ] | -| `love.mousemoved(x, y, dx, dy, istouch)` | `MyGame.MouseMoved(int x, int y, int deltaX, int deltaY, bool isTouch)` | User implementation: `void MouseMoved(int x, int y, int deltaX, int deltaY);` | In Scope | [ ] | -| `love.wheelmoved(x, y)` | `MyGame.MouseWheelMoved(int deltaX, int deltaY)` | User implementation: `void MouseWheelMoved(int deltaX, int deltaY);` | In Scope (Basic support) | [ ] | -| `love.joystickpressed(joystick, button)` | `MyGame.JoystickPressed(Night.Joystick joystick, int button)` | User implementation: `void JoystickPressed(Night.Joystick joystick, int button);` | Out of Scope | [ ] | -| `love.joystickreleased(joystick, button)` | `MyGame.JoystickReleased(Night.Joystick joystick, int button)` | User implementation: `void JoystickReleased(Night.Joystick joystick, int button);` | Out of Scope | [ ] | -| `love.joystickaxis(joystick, axis, value)` | `MyGame.JoystickAxisMoved(Night.Joystick joystick, int axis, float value)` | User implementation: `void JoystickAxisMoved(Night.Joystick joystick, int axis, float value);` | Out of Scope | [ ] | -| `love.joystickhat(joystick, hat, direction)` | `MyGame.JoystickHatMoved(Night.Joystick joystick, int hat, Night.HatDirection direction)` | User implementation: `void JoystickHatMoved(Night.Joystick joystick, int hat, Night.HatDirection direction);` | Out of Scope | [ ] | -| `love.joystickadded(joystick)` | `MyGame.JoystickAdded(Night.Joystick joystick)` | User implementation: `void JoystickAdded(Night.Joystick joystick);` | Out of Scope | [ ] | -| `love.joystickremoved(joystick)` | `MyGame.JoystickRemoved(Night.Joystick joystick)` | User implementation: `void JoystickRemoved(Night.Joystick joystick);` | Out of Scope | [ ] | -| `love.touchpressed(id, x, y, dx, dy, pressure)` | `MyGame.TouchPressed(long id, float x, float y, float deltaX, float deltaY, float pressure)` | User implementation: `void TouchPressed(long id, float x, float y, float deltaX, float deltaY, float pressure);` | Out of Scope | [ ] | -| `love.touchreleased(id, x, y, dx, dy, pressure)` | `MyGame.TouchReleased(long id, float x, float y, float deltaX, float deltaY, float pressure)` | User implementation: `void TouchReleased(long id, float x, float y, float deltaX, float deltaY, float pressure);` | Out of Scope | [ ] | -| `love.touchmoved(id, x, y, dx, dy, pressure)` | `MyGame.TouchMoved(long id, float x, float y, float deltaX, float deltaY, float pressure)` | User implementation: `void TouchMoved(long id, float x, float y, float deltaX, float deltaY, float pressure);` | Out of Scope | [ ] | -| `love.lowmemory()` | `MyGame.LowMemory()` | User implementation: `void LowMemory();` | Out of Scope | [ ] | -| `love.threaderror(thread, errorstr)` | `MyGame.ThreadError(Night.Thread thread, string error)` | User implementation: `void ThreadError(Night.Thread thread, string error);` | Out of Scope | [ ] | -| `love.directorydropped(path)` | `MyGame.DirectoryDropped(string path)` | User implementation: `void DirectoryDropped(string path);` | Out of Scope | [ ] | -| `love.filedropped(file)` | `MyGame.FileDropped(Night.File file)` | User implementation: `void FileDropped(Night.File file);`
`Night.File` would be a wrapper for file data. | Out of Scope | [ ] | -| `love.resize(w, h)` | `MyGame.WindowResized(int width, int height)` | User implementation: `void WindowResized(int width, int height);` | In Scope | [ ] | - -*Note: Many `love` module functions are event callbacks. In Night Engine, these will be methods the user implements in their game class, which are then called by `Night.Engine`.* diff --git a/project/love2d-api/modules/math.md b/project/love2d-api/modules/math.md deleted file mode 100644 index 0ae2302a..00000000 --- a/project/love2d-api/modules/math.md +++ /dev/null @@ -1,27 +0,0 @@ -# `love.math` Module API Mapping - -This document maps the functions available in the `love.math` module of Love2D to their proposed equivalents in the Night Engine. Most of this functionality can be achieved using `System.Math` and `System.Random` in C#. A dedicated `Night.Math` module is **Out of Scope** for the initial prototype, but specific advanced functions might be added later. - -| Love2D Function (`love.math.`) | Night Engine API (`Night.Math` or `System`) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|--------------------------------|---------------------------------------------|---------------------------|--------------------------|------| -| `love.math.triangulate(polygon)` | `Night.Math.Triangulate(Night.PointF[] polygon)` | `public static int[] Triangulate(Night.PointF[] polygon)`
Returns indices for triangles. | Out of Scope | [ ] | -| `love.math.isConvex(polygon)` | `Night.Math.IsConvex(Night.PointF[] polygon)` | `public static bool IsConvex(Night.PointF[] polygon)` | Out of Scope | [ ] | -| `love.math.getAngle(x1, y1, x2, y2)` | `Night.Math.GetAngle(float x1, float y1, float x2, float y2)` | `public static double GetAngle(float x1, float y1, float x2, float y2)`
Similar to `Math.Atan2(y2 - y1, x2 - x1)`. | Out of Scope (Use `System.Math`) | [ ] | -| `love.math.noise(x, y, z, w)` | `Night.Math.Noise(double x, double? y = null, double? z = null, double? w = null)` | `public static double Noise(...)`
Simplex noise. | Out of Scope | [ ] | -| `love.math.random()` | `(new System.Random()).NextDouble()` or `Night.Math.Random()` | `public static double Random()`
Returns [0, 1). | Out of Scope (Use `System.Random`) | [ ] | -| `love.math.random(max)` | `(new System.Random()).Next(max + 1)` or `Night.Math.Random(int max)` | `public static int Random(int max)`
Returns [0, max]. Or `Next(1, max + 1)` for [1, max]. Love2D is [1,max] for integer. | Out of Scope (Use `System.Random`) | [ ] | -| `love.math.random(min, max)` | `(new System.Random()).Next(min, max + 1)` or `Night.Math.Random(int min, int max)` | `public static int Random(int min, int max)`
Returns [min, max]. | Out of Scope (Use `System.Random`) | [ ] | -| `love.math.randomNormal(stddev, mean)` | `Night.Math.RandomNormal(double stdDev = 1.0, double mean = 0.0)` | `public static double RandomNormal(...)`
Normally distributed random number. | Out of Scope | [ ] | -| `love.math.setRandomSeed(seed)` | `Night.Math.SetRandomSeed(int seed)` or `new System.Random(seed)` | `public static void SetRandomSeed(int seed)`
For a global `Night.Math` random generator. | Out of Scope (Use `System.Random` instance) | [ ] | -| `love.math.getRandomSeed()` | `Night.Math.GetRandomSeed()` | `public static (int seed, int? highSeed) GetRandomSeed()` | Out of Scope | [ ] | -| `love.math.getRandomState()` | `Night.Math.GetRandomState()` | `public static string GetRandomState()` | Out of Scope | [ ] | -| `love.math.setRandomState(state)`| `Night.Math.SetRandomState(string state)` | `public static void SetRandomState(string state)` | Out of Scope | [ ] | -| `love.math.newBezierCurve(points)` | `Night.Math.NewBezierCurve(Night.PointF[] controlPoints)` | `public static Night.BezierCurve NewBezierCurve(...)` | Out of Scope | [ ] | -| `love.math.newRandomGenerator()` | `Night.Math.NewRandomGenerator()` | `public static System.Random NewRandomGenerator()` or a custom `Night.RandomGenerator` class. | Out of Scope | [ ] | -| `love.math.gammaToLinear(c)` | `Night.Math.GammaToLinear(double colorComponent)` | `public static double GammaToLinear(double colorComponent)` | Out of Scope | [ ] | -| `love.math.linearToGamma(c)` | `Night.Math.LinearToGamma(double colorComponent)` | `public static double LinearToGamma(double colorComponent)` | Out of Scope | [ ] | - -**Night Engine Specific Types (if module were implemented):** -* `Night.PointF`: Struct for a 2D point with float coordinates. -* `Night.BezierCurve`: Class representing a Bezier curve, with methods like `Evaluate(t)`, `GetDerivative(t)`. -* `Night.RandomGenerator`: A class that might encapsulate `System.Random` or a custom PRNG, potentially with Love2D-compatible state management. diff --git a/project/love2d-api/modules/mouse.md b/project/love2d-api/modules/mouse.md deleted file mode 100644 index 3ca3b1e5..00000000 --- a/project/love2d-api/modules/mouse.md +++ /dev/null @@ -1,29 +0,0 @@ -# `love.mouse` Module API Mapping - -This document maps the functions available in the `love.mouse` module of Love2D to their proposed equivalents in the Night Engine. - -| Love2D Function (`love.mouse.`) | Night Engine API (`Night.Mouse.`) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|---------------------------------|-----------------------------------|---------------------------|--------------------------|------| -| `love.mouse.getX()` | `Night.Mouse.GetX()` | `public static int GetX()` | In Scope | [ ] | -| `love.mouse.getY()` | `Night.Mouse.GetY()` | `public static int GetY()` | In Scope | [ ] | -| `love.mouse.getPosition()` | `Night.Mouse.GetPosition()` | `public static (int x, int y) GetPosition()` | In Scope | [ ] | -| `love.mouse.isDown(button)` | `Night.Mouse.IsDown(Night.MouseButton button)` | `public static bool IsDown(Night.MouseButton button)`
`Night.MouseButton` enum: `Left`, `Right`, `Middle`, `X1`, `X2`, etc. | In Scope | [ ] | -| `love.mouse.isVisible()` | `Night.Mouse.IsVisible()` | `public static bool IsVisible()` | In Scope | [ ] | -| `love.mouse.setX(x)` | `Night.Mouse.SetX(int x)` | `public static void SetX(int x)`
Warps mouse cursor. | In Scope (Low priority) | [ ] | -| `love.mouse.setY(y)` | `Night.Mouse.SetY(int y)` | `public static void SetY(int y)`
Warps mouse cursor. | In Scope (Low priority) | [ ] | -| `love.mouse.setPosition(x,y)` | `Night.Mouse.SetPosition(int x, int y)` | `public static void SetPosition(int x, int y)`
Warps mouse cursor. | In Scope (Low priority) | [ ] | -| `love.mouse.setVisible(visible)`| `Night.Mouse.SetVisible(bool visible)` | `public static void SetVisible(bool visible)` | In Scope | [ ] | -| `love.mouse.setGrabbed(grab)` | `Night.Mouse.SetGrabbed(bool grabbed)` | `public static void SetGrabbed(bool grabbed)`
Confines cursor to window. | In Scope (Low priority) | [ ] | -| `love.mouse.isGrabbed()` | `Night.Mouse.IsGrabbed()` | `public static bool IsGrabbed()` | In Scope (Low priority) | [ ] | -| `love.mouse.getRelativeMode()` | `Night.Mouse.GetRelativeMode()` | `public static bool GetRelativeMode()` | In Scope (Low priority, for FPS-style input) | [ ] | -| `love.mouse.setRelativeMode(enable)` | `Night.Mouse.SetRelativeMode(bool enable)` | `public static void SetRelativeMode(bool enable)` | In Scope (Low priority) | [ ] | -| `love.mouse.getCursor()` | `Night.Mouse.GetCursor()` | `public static Night.Cursor GetCursor()`
`Night.Cursor` would be a custom cursor object. | Out of Scope | [ ] | -| `love.mouse.setCursor(cursor)` | `Night.Mouse.SetCursor(Night.Cursor? cursor = null)` | `public static void SetCursor(Night.Cursor? cursor = null)`
`null` for default system cursor. | Out of Scope | [ ] | -| `love.mouse.newCursor(imagedata, hotx, hoty)` | `Night.Mouse.NewCursor(Night.ImageData imageData, int hotSpotX, int hotSpotY)` | `public static Night.Cursor NewCursor(...)` | Out of Scope | [ ] | -| `love.mouse.getSystemCursor(ctype)` | `Night.Mouse.GetSystemCursor(Night.SystemCursorType type)` | `public static Night.Cursor GetSystemCursor(Night.SystemCursorType type)`
`SystemCursorType` enum: `Arrow`, `IBeam`, `Crosshair`, etc. | Out of Scope | [ ] | - -**Night Engine Specific Types:** -* `Night.MouseButton`: Enum representing mouse buttons (e.g., `Left`, `Right`, `Middle`, `X1`, `X2`). -* `Night.Cursor`: Represents a mouse cursor (custom or system). -* `Night.ImageData`: Wrapper for image data, likely from `Night.Image` module. -* `Night.SystemCursorType`: Enum for standard system cursors. diff --git a/project/love2d-api/modules/sound.md b/project/love2d-api/modules/sound.md deleted file mode 100644 index b5a6cb38..00000000 --- a/project/love2d-api/modules/sound.md +++ /dev/null @@ -1,31 +0,0 @@ -# `love.sound` Module API Mapping - -This document maps the functions available in the `love.sound` module of Love2D to their proposed equivalents in the Night Engine. This module is primarily for decoding sound data, which would be handled internally by `Night.Audio.NewSource` or `Night.Source` objects if the audio module were implemented. This entire module is **Out of Scope** for the initial prototype. - -| Love2D Function (`love.sound.`) | Night Engine API (`Night.Sound` or `Night.Audio` internals) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|---------------------------------|-------------------------------------------------------------|---------------------------|--------------------------|------| -| `love.sound.newDecoder(filedata, bufferSize)` | `Night.Audio.NewDecoder(Night.FileData fileData, int bufferSize = 4096)` | `public static Night.Decoder NewDecoder(...)`
Creates a sound decoder. | Out of Scope | [ ] | -| `love.sound.newSoundData(samples, sampleRate, bitDepth, channels)` | `Night.Audio.NewSoundData(int samples, int sampleRate, int bitDepth, int channels)` or `Night.Audio.NewSoundData(byte[] rawPcmData, ...)` | `public static Night.SoundData NewSoundData(...)`
Creates raw sound data. | Out of Scope | [ ] | - -**Functionality on `Night.Decoder` instances (if implemented):** -* `decoder.GetBitDepth()` -* `decoder.GetChannelCount()` -* `decoder.GetDuration()` -* `decoder.GetSampleRate()` -* `decoder.Decode()` (returns a chunk of SoundData) -* `decoder.Seek(offset)` - -**Functionality on `Night.SoundData` instances (if implemented):** -* `soundData.GetBitDepth()` -* `soundData.GetChannelCount()` -* `soundData.GetDuration()` -* `soundData.GetSampleCount()` -* `soundData.GetSampleRate()` -* `soundData.GetSample(index)` -* `soundData.SetSample(index, value)` -* `soundData.Clone()` - -**Night Engine Specific Types (if module were implemented):** -* `Night.Decoder`: Represents an object that can decode audio from a stream or file data. -* `Night.SoundData`: Represents raw PCM audio data in memory. -* `Night.FileData`: Represents file data in memory (from `Night.Filesystem`). diff --git a/project/love2d-api/modules/system.md b/project/love2d-api/modules/system.md deleted file mode 100644 index f231513d..00000000 --- a/project/love2d-api/modules/system.md +++ /dev/null @@ -1,17 +0,0 @@ -# `love.system` Module API Mapping - -This document maps the functions available in the `love.system` module of Love2D to their proposed equivalents in the Night Engine. Most functions in this module are **Out of Scope** for the initial prototype. Standard .NET `System.Environment` or `System.Runtime.InteropServices.RuntimeInformation` can provide some of this. - -| Love2D Function (`love.system.`) | Night Engine API (`Night.System` or `System` namespace) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|----------------------------------|---------------------------------------------------------|---------------------------|--------------------------|------| -| `love.system.getOS()` | `Night.System.GetOSName()` or `System.Runtime.InteropServices.RuntimeInformation.OSDescription` | `public static string GetOSName()` | Out of Scope | [ ] | -| `love.system.getProcessorCount()`| `System.Environment.ProcessorCount` | `public static int GetProcessorCount()` (via `System.Environment`) | Out of Scope | [ ] | -| `love.system.getPowerInfo()` | `Night.System.GetPowerInfo()` | `public static Night.PowerInfo GetPowerInfo()`
`PowerInfo` class: `State` (enum), `SecondsLeft` (nullable int), `Percent` (nullable int). | Out of Scope | [ ] | -| `love.system.getClipboardText()` | `Night.System.GetClipboardText()` | `public static string GetClipboardText()`
Would need platform-specific implementation or a library. | Out of Scope | [ ] | -| `love.system.setClipboardText(text)` | `Night.System.SetClipboardText(string text)` | `public static void SetClipboardText(string text)` | Out of Scope | [ ] | -| `love.system.openURL(url)` | `Night.System.OpenURL(string url)` or `System.Diagnostics.Process.Start()` | `public static bool OpenURL(string url)`
`Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });` | Out of Scope | [ ] | -| `love.system.vibrate(seconds)` | `Night.System.Vibrate(double seconds)` | `public static void Vibrate(double seconds)`
For mobile devices. | Out of Scope | [ ] | -| `love.system.getPreferredLocales()` | `Night.System.GetPreferredLocales()` | `public static string[] GetPreferredLocales()`
From `System.Globalization.CultureInfo.CurrentUICulture` etc. | Out of Scope | [ ] | - -**Night Engine Specific Types (if module were implemented):** -* `Night.PowerInfo`: Class/Struct with properties `State` (enum: `NoBattery`, `Charging`, `Charged`, `Draining`), `SecondsLeft` (nullable int), `Percent` (nullable int). diff --git a/project/love2d-api/modules/thread.md b/project/love2d-api/modules/thread.md deleted file mode 100644 index 031d29d7..00000000 --- a/project/love2d-api/modules/thread.md +++ /dev/null @@ -1,29 +0,0 @@ -# `love.thread` Module API Mapping - -This document maps the functions available in the `love.thread` module of Love2D to their proposed equivalents in the Night Engine. This entire module is **Out of Scope** for the initial prototype, as .NET provides comprehensive threading capabilities via `System.Threading`. - -| Love2D Function (`love.thread.`) | Night Engine API (`Night.Thread` or `System.Threading`) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|----------------------------------|---------------------------------------------------------|---------------------------|--------------------------|------| -| `love.thread.newThread(filename)` or `love.thread.newThread(codestring)` | `Night.Thread.NewThread(string luaScriptPathOrCode)` or `new System.Threading.Thread(...)` | `public static Night.Thread NewThread(string luaScriptPathOrCode)`
Love2D threads run Lua code. Night Engine would use C# delegates/lambdas with `System.Threading.Thread` or `Task`. | Out of Scope | [ ] | -| `love.thread.getChannel(name)` | `Night.Thread.GetChannel(string name)` | `public static Night.Channel GetChannel(string name)`
Channels are for inter-thread communication. | Out of Scope | [ ] | -| `love.thread.newChannel()` | `Night.Thread.NewChannel()` | `public static Night.Channel NewChannel()` | Out of Scope | [ ] | - -**Functionality on `Night.Thread` instances (if implemented, wrapping `System.Threading.Thread`):** -* `thread.Start()` -* `thread.Wait()` -* `thread.IsRunning()` -* `thread.GetError()` - -**Functionality on `Night.Channel` instances (if implemented, similar to `System.Threading.Channels.Channel`):** -* `channel.Push(T value)` -* `channel.Pop()` (non-blocking, returns nullable T) -* `channel.Demand()` (blocking, returns T) -* `channel.Peek()` -* `channel.GetCount()` -* `channel.HasRead(id)` -* `channel.Clear()` -* `channel.PerformAtomic(Func operation)` - -**Night Engine Specific Types (if module were implemented):** -* `Night.Thread`: A wrapper around `System.Threading.Thread` or `Task`, potentially with easier error handling or specific Love2D-like behaviors if Lua interop were a goal. -* `Night.Channel`: A thread-safe communication channel, similar to `System.Threading.Channels.Channel`. diff --git a/project/love2d-api/modules/timer.md b/project/love2d-api/modules/timer.md deleted file mode 100644 index a946a92c..00000000 --- a/project/love2d-api/modules/timer.md +++ /dev/null @@ -1,12 +0,0 @@ -# `love.timer` Module API Mapping - -This document maps the functions available in the `love.timer` module of Love2D to their proposed equivalents in the Night Engine. Most functions in this module are **Out of Scope** for the initial prototype. - -| Love2D Function (`love.timer.`) | Night Engine API (`Night.Timer.`) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|---------------------------------|-----------------------------------|---------------------------|--------------------------|------| -| `love.timer.getDelta()` | `Night.Timer.GetDelta()` | `public static double GetDelta()`
Time since last frame. This is already provided to `MyGame.Update(deltaTime)`. This function would provide it on demand. | Out of Scope (Covered by `Update`'s `deltaTime`) | [ ] | -| `love.timer.getFPS()` | `Night.Timer.GetFPS()` | `public static int GetFPS()`
Current frames per second. | In Scope (Useful for debugging/display) | [ ] | -| `love.timer.getAverageDelta()` | `Night.Timer.GetAverageDelta()` | `public static double GetAverageDeltaTime()`
Average delta time over the last second. | Out of Scope | [ ] | -| `love.timer.getTime()` | `Night.Timer.GetTime()` | `public static double GetTime()`
Time since the game started, in seconds. | In Scope (Useful utility) | [ ] | -| `love.timer.sleep(s)` | `Night.Timer.Sleep(double seconds)` | `public static void Sleep(double seconds)`
Pauses execution. | Out of Scope (Generally not recommended in game loops) | [ ] | -| `love.timer.step()` | `Night.Timer.Step()` | `public static double Step()`
Measures time between calls. Used internally by Love2D's default `love.run`. Night Engine will have its own internal timing. | Out of Scope (Engine internal) | [ ] | diff --git a/project/love2d-api/modules/touch.md b/project/love2d-api/modules/touch.md deleted file mode 100644 index 548bb0bb..00000000 --- a/project/love2d-api/modules/touch.md +++ /dev/null @@ -1,12 +0,0 @@ -# `love.touch` Module API Mapping - -This document maps the functions available in the `love.touch` module of Love2D to their proposed equivalents in the Night Engine. This entire module is **Out of Scope** for the initial prototype. Touch event callbacks are noted in the `love` module mapping. - -| Love2D Function (`love.touch.`) | Night Engine API (`Night.Touch.`) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|---------------------------------|-----------------------------------|---------------------------|--------------------------|------| -| `love.touch.getPosition(id)` | `Night.Touch.GetPosition(long touchId)` | `public static (float x, float y) GetPosition(long touchId)` | Out of Scope | [ ] | -| `love.touch.getPressure(id)` | `Night.Touch.GetPressure(long touchId)` | `public static float GetPressure(long touchId)` | Out of Scope | [ ] | -| `love.touch.getTouches()` | `Night.Touch.GetActiveTouches()` | `public static long[] GetActiveTouches()`
Returns IDs of currently active touches. | Out of Scope | [ ] | - -**Night Engine Specific Types (if module were implemented):** -* Touch events in `MyGame` would pass a `Night.TouchEventArgs` object containing `Id`, `X`, `Y`, `DeltaX`, `DeltaY`, `Pressure`. diff --git a/project/love2d-api/modules/video.md b/project/love2d-api/modules/video.md deleted file mode 100644 index 8eb961f8..00000000 --- a/project/love2d-api/modules/video.md +++ /dev/null @@ -1,26 +0,0 @@ -# `love.video` Module API Mapping - -This document maps the functions available in the `love.video` module of Love2D to their proposed equivalents in the Night Engine. This entire module is **Out of Scope** for the initial prototype. The primary way to get a video object in Night Engine would be `Night.Graphics.NewVideo()`. - -| Love2D Function (`love.video.`) | Night Engine API (`Night.Video` methods or `Night.Graphics`) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|---------------------------------|--------------------------------------------------------------|---------------------------|--------------------------|------| -| `love.video.newVideoStream(filename)` | `Night.Graphics.NewVideo(string filePath, Night.VideoOptions? options = null)` | `public static Night.Video NewVideo(...)`
This is the main entry point. `VideoStream` in Love2D is just `Video`. | Out of Scope | [ ] | - -**Functionality on `Night.Video` instances (if implemented):** -* `video.Play()` -* `video.Pause()` -* `video.Seek(offset)` -* `video.Tell()` (get current playback time) -* `video.GetSource()` (audio source associated with video) -* `video.IsPlaying()` -* `video.SetSync(audioSource)` -* `video.GetWidth()`, `video.GetHeight()` (as an `IDrawable`) -* `video.GetFilename()` -* `video.GetFilter()` -* `video.SetFilter(min, mag)` - -**Night Engine Specific Types (if module were implemented):** -* `Night.Video`: Represents a video object. It would be an `IDrawable` and might internally manage a `Night.Source` for audio. Created via `Night.Graphics.NewVideo()`. -* `Night.VideoOptions`: Class for video loading options (e.g., `EnableAudio`). -* `Night.Source`: Audio source from `Night.Audio` module. -* `Night.FilterMode`: Enum (`Linear`, `Nearest`). diff --git a/project/love2d-api/modules/window.md b/project/love2d-api/modules/window.md deleted file mode 100644 index 9162c73b..00000000 --- a/project/love2d-api/modules/window.md +++ /dev/null @@ -1,47 +0,0 @@ -# `love.window` Module API Mapping - -This document maps the functions available in the `love.window` module of Love2D to their proposed equivalents in the Night Engine. - -| Love2D Function (`love.window.`) | Night Engine API (`Night.Window.`) | Notes / C# Signature Idea | Status (Prototype Scope) | Done | -|----------------------------------|------------------------------------|---------------------------|--------------------------|------| -| `love.window.close()` | `Night.Window.Close()` | `public static void Close()`
Requests to close the window. The `MyGame.Quit()` callback will be invoked. | In Scope | [x] | -| `love.window.displaySleepEnabled()` | `Night.Window.IsDisplaySleepEnabled()` | `public static bool IsDisplaySleepEnabled()` | Out of Scope | [ ] | -| `love.window.fromPixels(px_x, px_y)` | `Night.Window.FromPixels(double pixelX, double pixelY)` | `public static (double x, double y) FromPixels(double pixelX, double pixelY)`
Converts pixel coordinates to density-independent units. | In Scope (if high DPI is handled) | [ ] | -| `love.window.getDesktopDimensions(displayindex)` | `Night.Window.GetDesktopDimensions(int displayIndex = 0)` | `public static (int width, int height) GetDesktopDimensions(int displayIndex = 0)` | In Scope (for default display) | [ ] | -| `love.window.getDimensions()` | `Night.Window.GetDimensions()` | `public static (int width, int height) GetDimensions()` | In Scope | [ ] | -| `love.window.getDisplayCount()` | `Night.Window.GetDisplayCount()` | `public static int GetDisplayCount()` | In Scope (for default display awareness) | [ ] | -| `love.window.getDisplayName(displayindex)` | `Night.Window.GetDisplayName(int displayIndex = 0)` | `public static string GetDisplayName(int displayIndex = 0)` | Out of Scope | [ ] | -| `love.window.getFullscreen()` | `Night.Window.IsFullscreen()` | `public static bool IsFullscreen()`
Returns true if fullscreen. Also need `Night.Window.GetFullscreenMode()` for type. | In Scope | [ ] | -| `love.window.getFullscreenModes(displayindex)` | `Night.Window.GetFullscreenModes(int displayIndex = 0)` | `public static Night.FullscreenMode[] GetFullscreenModes(int displayIndex = 0)`
`FullscreenMode` struct/class: `int Width, int Height, int RefreshRate`. | In Scope (for setting fullscreen) | [ ] | -| `love.window.getIcon()` | `Night.Window.GetIcon()` | `public static Night.ImageData? GetIcon()` | In Scope | [x] | -| `love.window.getMode()` | `Night.Window.GetMode()` | `public static (int width, int height, Night.WindowFlags flags) GetMode()`
`WindowFlags` would be a struct/class. | In Scope | [ ] | -| `love.window.getPixelDimensions()` | `Night.Window.GetPixelDimensions()` | `public static (int pixelWidth, int pixelHeight) GetPixelDimensions()` | In Scope (if high DPI is handled) | [ ] | -| `love.window.getPixelScale()` | `Night.Window.GetPixelScale()` | `public static double GetPixelScale()` | In Scope (if high DPI is handled) | [ ] | -| `love.window.getPosition()` | `Night.Window.GetPosition()` | `public static (int x, int y, int displayIndex) GetPosition()` | In Scope | [ ] | -| `love.window.getTitle()` | `Night.Window.GetTitle()` | `public static string GetTitle()` | In Scope | [ ] | -| `love.window.hasFocus()` | `Night.Window.HasFocus()` | `public static bool HasFocus()` | In Scope | [ ] | -| `love.window.hasMouseFocus()` | `Night.Window.HasMouseFocus()` | `public static bool HasMouseFocus()` | In Scope | [ ] | -| `love.window.isMaximized()` | `Night.Window.IsMaximized()` | `public static bool IsMaximized()` | In Scope | [ ] | -| `love.window.isMinimized()` | `Night.Window.IsMinimized()` | `public static bool IsMinimized()` | In Scope | [ ] | -| `love.window.isOpen()` | `Night.Window.IsOpen()` | `public static bool IsOpen()`
Checks if the window is open and the game should continue running. | In Scope | [x] | -| `love.window.isVisible()` | `Night.Window.IsVisible()` | `public static bool IsVisible()` | In Scope | [ ] | -| `love.window.maximize()` | `Night.Window.Maximize()` | `public static void Maximize()` | In Scope | [ ] | -| `love.window.minimize()` | `Night.Window.Minimize()` | `public static void Minimize()` | In Scope | [ ] | -| `love.window.requestAttention(continuous)` | `Night.Window.RequestAttention(bool continuous = false)` | `public static void RequestAttention(bool continuous = false)` | Out of Scope | [ ] | -| `love.window.restore()` | `Night.Window.Restore()` | `public static void Restore()`
Restores after minimize/maximize. | In Scope | [ ] | -| `love.window.setDisplaySleepEnabled(enable)` | `Night.Window.SetDisplaySleepEnabled(bool enable)` | `public static void SetDisplaySleepEnabled(bool enable)` | Out of Scope | [ ] | -| `love.window.setFullscreen(fullscreen, fstype)` | `Night.Window.SetFullscreen(bool fullscreen, Night.FullscreenType type = Night.FullscreenType.Desktop)` | `public static bool SetFullscreen(bool fullscreen, Night.FullscreenType type = Night.FullscreenType.Desktop)`
`FullscreenType` enum: `Desktop`, `Exclusive`. Returns success. | In Scope | [ ] | -| `love.window.setIcon(imagedata)` | `Night.Window.SetIcon(string imagePath)` | `public static bool SetIcon(string imagePath)` | In Scope | [x] | -| `love.window.setMode(width, height, flags)` | `Night.Window.SetMode(int width, int height, Night.WindowFlags? flags = null)` | `public static bool SetMode(int width, int height, Night.WindowFlags? flags = null)`
`flags` could include: `Fullscreen`, `Resizable`, `Borderless`, `VSync`, `MinMSAA`, `DepthBits`, `StencilBits`. Returns success. | In Scope | [x] | -| `love.window.setPosition(x, y, displayindex)` | `Night.Window.SetPosition(int x, int y, int displayIndex = -1)` | `public static void SetPosition(int x, int y, int displayIndex = -1)`
`displayIndex = -1` could mean current or primary. | In Scope | [ ] | -| `love.window.setTitle(title)` | `Night.Window.SetTitle(string title)` | `public static void SetTitle(string title)` | In Scope | [x] | -| `love.window.toPixels(x, y)` | `Night.Window.ToPixels(double x, double y)` | `public static (double pixelX, double pixelY) ToPixels(double x, double y)`
Converts density-independent units to pixel coordinates. | In Scope (if high DPI is handled) | [ ] | -| `love.window.updateMode(width, height, flags)` | `Night.Window.UpdateMode(int width, int height, Night.WindowFlags? flags = null)` | `public static bool UpdateMode(int width, int height, Night.WindowFlags? flags = null)`
Similar to `SetMode` but for an existing window. | In Scope | [ ] | -| `love.window.showMessageBox(title, message, type, attachtowindow)` | `Night.Window.ShowMessageBox(string title, string message, Night.MessageBoxType type = Night.MessageBoxType.Info, bool attachToWindow = true)` | `public static void ShowMessageBox(string title, string message, Night.MessageBoxType type = Night.MessageBoxType.Info, bool attachToWindow = true)`
`MessageBoxType` enum: `Info`, `Warning`, `Error`. | Out of Scope (Low priority) | [ ] | - -**Night Engine Specific Types:** -* `Night.WindowFlags`: A struct or class that might contain boolean properties like `Fullscreen`, `Resizable`, `Borderless`, `VSync`, and potentially integer values for `MinMSAA`, `DepthBits`, `StencilBits`. -* `Night.FullscreenType`: Enum (`Desktop`, `Exclusive`). -* `Night.FullscreenMode`: Struct/class (`int Width, int Height, int RefreshRate`). -* `Night.ImageData`: Wrapper for image data, likely from `Night.Image` module. -* `Night.MessageBoxType`: Enum (`Info`, `Warning`, `Error`). diff --git a/project/macos-manual-testing.md b/project/macos-manual-testing.md new file mode 100644 index 00000000..6e214a8a --- /dev/null +++ b/project/macos-manual-testing.md @@ -0,0 +1,195 @@ +# macOS Manual Testing Constraints and Solutions + +## Overview + +This document explains the constraints and solutions for running manual tests on macOS, including the architectural reasons behind these limitations and practical workarounds for developers. + +## Problem Summary + +Manual tests that require graphics display fail when run through `dotnet test` on macOS, even with proper system permissions. This is due to fundamental differences between how xUnit test runners and regular applications access the macOS graphics subsystem. + +## Root Cause Analysis + +### The Core Issue + +The problem is **not** with SDL3, permissions, or our test code, but with the **execution context** differences: + +- **`dotnet test` (xUnit)**: Runs in a restricted test host process that cannot access macOS graphics +- **`dotnet run` (Game)**: Runs as a proper macOS application with full graphics access + +### Technical Details + +1. **xUnit Test Host Process**: Limited sandbox environment without proper macOS entitlements +2. **AppHost vs Library Context**: Test projects with `OutputType=Library` run in `dotnet` process context, while `OutputType=Exe` projects get their own appHost context +3. **macOS Graphics Access**: Requires proper app entitlements and process context that xUnit test host doesn't provide + +## Solutions Implemented + +### 1. Fixed Test Project Configuration + +**Problem**: Multiple entry point conflicts preventing test compilation. + +**Solution**: Changed test project to executable format while maintaining xUnit compatibility: + +```xml + + + Exe + Program + +``` + +**Benefits**: +- Resolves compilation errors +- Enables appHost context for better macOS compatibility +- Maintains full xUnit test runner compatibility + +### 2. Enhanced SDL Initialization + +**Problem**: SDL3 initialization failures on macOS in different environments. + +**Solution**: Added intelligent fallback logic in `Framework.cs`: + +```csharp +// Special handling for macOS headed mode when video init fails +if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !isHeadlessEnv) +{ + Logger.Info("macOS headed mode video init failed. Attempting workarounds..."); + + // Clean up and try with different configuration + SDL.Quit(); + _ = SDL.SetHint(SDL.Hints.VideoDriver, string.Empty); + _ = SDL.SetHint(SDL.Hints.MacBackgroundApp, "0"); + + // Retry with auto-detection + if (!SDL.Init(initializedSubsystemsFlags)) + { + // Provide helpful error guidance + Logger.Error("macOS manual testing requires:"); + Logger.Error("1. Screen Recording permission for your terminal/IDE"); + Logger.Error("2. Running from a GUI application with proper entitlements"); + Logger.Error("3. Consider running: SDL_VIDEODRIVER=dummy mise man-test (headless)"); + return; + } +} +``` + +**Benefits**: +- Graceful handling of macOS video initialization failures +- Clear error messages with actionable guidance +- Automatic fallback attempts for different configurations + +## Current Status + +### ✅ What Works +- **Automated tests**: All 79 automated tests pass on macOS +- **Headless manual tests**: `SDL_VIDEODRIVER=dummy mise man-test` works for logic testing +- **Game execution**: `mise game` shows real graphics window perfectly +- **Test compilation**: No more multiple entry point errors + +### ❌ Current Constraint +- **xUnit manual tests with graphics**: Cannot show real windows due to xUnit test host limitations + +## Workarounds for Manual Testing + +### Option 1: Use SampleGame for Manual Testing ⭐ (Recommended) + +Since `mise game` works perfectly with graphics, modify your game to run manual tests: + +```bash +# Shows real graphics window - perfect for manual testing +mise game +``` + +**Benefits**: +- Full graphics display capability +- Real user interaction +- Same execution context as production +- No xUnit limitations + +### Option 2: Headless Manual Testing + +For logic verification without visual confirmation: + +```bash +# Tests the logic flow without graphics +SDL_VIDEODRIVER=dummy mise man-test +``` + +**Benefits**: +- Validates test logic and flow +- Works in CI environments +- Fast execution +- No graphics dependencies + +### Option 3: Create Standalone Manual Test Runner + +Create a dedicated manual test application: + +```csharp +// ManualTestRunner/Program.cs +using Night; +using NightTest.Groups.Graphics; + +var test = new GraphicsClearTest(); +Framework.Run(test); +``` + +```bash +# Run specific manual tests directly +dotnet run --project ManualTestRunner -- Graphics.Clear +``` + +**Benefits**: +- Full control over execution environment +- Direct SDL access like SampleGame +- Can be integrated into development workflow + +## Development Recommendations + +### For CI/CD Pipelines +- Use automated tests only: `mise test` +- Include headless manual tests for logic validation: `SDL_VIDEODRIVER=dummy dotnet test --filter TestType=Manual` + +### For Local Development +- **Quick graphics verification**: Use `mise game` with test modifications +- **Logic testing**: Use headless manual tests with dummy driver +- **Production validation**: Create standalone test runner for critical manual tests + +### For Manual Test Design +1. **Design for headless**: Ensure manual tests can validate logic without requiring visual confirmation +2. **Clear success criteria**: Make test outcomes programmatically verifiable when possible +3. **Fallback modes**: Provide alternative validation methods for CI environments + +## Technical Notes + +### macOS Permissions Required +Even with these solutions, running any graphics application on macOS requires: + +1. **Screen Recording permission** for your terminal/IDE in System Preferences → Security & Privacy +2. **Proper app entitlements** (automatically handled by `dotnet run` but not `dotnet test`) +3. **GUI application context** (not available in xUnit test host) + +### Alternative Approaches Considered + +1. **Disable top-level statements**: Not applicable - this is about execution context, not compilation +2. **Change project SDK**: Would break xUnit integration +3. **Custom test runners**: Adds complexity without solving the fundamental issue +4. **Mock graphics subsystem**: Defeats the purpose of manual testing + +## Conclusion + +The macOS manual testing constraint is an **architectural limitation** of xUnit test runners, not a bug in our code. The implemented solutions provide: + +1. **Robust automated testing** that works across all platforms +2. **Flexible manual testing options** for different development scenarios +3. **Clear guidance** for developers encountering this constraint + +This approach maintains the benefits of both xUnit integration for automated tests and native application capabilities for manual graphics testing. + +## Related Files + +- `tests/NightTest.csproj` - Test project configuration +- `tests/Program.cs` - Test executable entry point +- `src/Night/Framework.cs` - SDL initialization with macOS handling +- `mise.toml` - Task definitions for different testing modes diff --git a/project/operational-guidelines.md b/project/operational-guidelines.md deleted file mode 100644 index c94d323b..00000000 --- a/project/operational-guidelines.md +++ /dev/null @@ -1,112 +0,0 @@ -# Operational Guidelines - -The "Night" engine project will ALWAYS adhere to the **Google C# Style Guide**. Key aspects of this guide, supplemented by project-specific interpretations, are outlined below. - -- **Formatting & Style:** - - **Indentation:** 2 spaces, no tabs. - - **Column Limit:** 100 characters. - - **Whitespace, Braces, Line Wrapping:** Adhere to the detailed rules in the Google C# Style Guide. This includes rules like no line break before an opening brace, and braces used even when optional. - - **Tooling:** - - `dotnet format` will be used to help enforce formatting rules. - - An `.editorconfig` file will be added to the project root, configured to align with the Google C# Style Guide's formatting and style rules (e.g., indentation, column limit, using directives order). - - Format the `using` directives with specific spacing. Place all System.* directives first, followed by a blank line. Then, group other using directives (like third-party libraries or project-specific namespaces) logically, and insert a blank line between each distinct group. For example, list System usings, then a blank line, then Night usings, then a blank line, then SDL3 usings, rather than listing them all contiguously. - -`using` directives should NEVER have any comments associated with them or on the same line -- **Linting:** - - **Tooling:** Utilize Roslyn Analyzers provided with the .NET SDK. - - The `.editorconfig` file will be configured to enable and set the severity of analyzer rules to align with the principles of the Google C# Style Guide. This includes rules related to naming, organization, and other code quality aspects. -- **Naming Conventions:** - - **General Rules Summary:** - - Names of classes, methods, enumerations, public fields, public properties, namespaces: `PascalCase`. - - Names of local variables, parameters: `camelCase`. - - Names of private, protected, internal, and protected internal fields and properties: `_camelCase` (e.g., `_privateField`). - - Naming convention is unaffected by modifiers such as `const`, `static`, `readonly`, etc.. - - For casing, a “word” is anything written without internal spaces, including acronyms (e.g., `MyRpc` not `MyRPC`). - - Names of interfaces start with `I` (e.g., `IInterface`). - - Filenames and directory names are `PascalCase` (e.g., `MyFile.cs`). - - **Project Specific (API Design):** When naming public API elements for "Night" intended to mirror Love2D functions (e.g., `love.window.setTitle`), use the `PascalCase` version adhering to the above rules (e.g., `Night.Window.SetTitle(...)`). -- **Code Organization:** - - **Modifier Order:** `public protected internal private new abstract virtual override sealed static readonly extern unsafe volatile async`. - - **Namespace `using` Declarations:** Place at the top of the file, before any namespace declarations. Order alphabetically, with `System` imports always first. - - **Class Member Ordering:** Follow the prescribed order: Nested types, static/const/readonly fields, instance fields/properties, constructors/finalizers, methods. Within each group, elements are ordered by access: Public, Internal, Protected internal, Protected, Private. -- **Key Principles (Project-Specific additions and emphasis):** - - **API Design (Night Engine):** Strive for an API design that is idiomatic to C# while closely mirroring the spirit, structure, and ease of use of the Love2D API for the features being implemented. - - **Clarity over Premature Optimization:** For the prototype, prioritize clear, understandable, and maintainable code. - - **Scope Adherence:** Focus strictly on implementing the agreed-upon features (0-4) for this prototype. -- **Testing (if applicable for prototype):** - - **Primary Integration Test:** The `Night.SampleGame` project will serve as the main method for testing the integration and functionality of the `Night.Framework`/`Night.Engine` features. Write the necessary code to test out new functionality in the SampleGame project to allow the user to manually verify. The `Program.cs` file in the SampleGame project is the best place to put this code. - - **Unit Tests (Optional):** Consider adding basic unit tests for any complex internal helper functions or critical non-P/Invoke logic within `Night.Framework`. - - **Manual Verification:** Manual testing of the sample game against the defined user actions and outcomes for each feature in the PRD will be essential. - -## Mapping Native SDL3 Functions to SDL3-CS (C#) Bindings - -When working with the `lib/SDL3-CS` C# wrapper for SDL3, it's often necessary to find the C# equivalent of a native SDL3 C function, enum, or struct. This section provides guidance on that process. The `lib/SDL3-CS` bindings are located in the `lib/SDL3-CS/SDL3-CS/` directory. - -**1. Naming Conventions:** - -* **Functions:** Native SDL3 functions (e.g., `SDL_CreateWindow`, `SDL_PollEvent`) are generally mapped to C# methods within the static `SDL` class using PascalCase. The `SDL_` prefix is removed, and the rest of the name is converted to PascalCase. - * `SDL_CreateWindow` becomes `SDL.CreateWindow()` - * `SDL_PollEvent` becomes `SDL.PollEvent()` -* **Enums and Structs:** Native SDL3 enums and structs (e.g., `SDL_WindowFlags`, `SDL_Event`, `SDL_Keycode`) are typically mapped to C# enums or structs within the `SDL` static class (or directly in the `SDL3` namespace if they are complex types used by the static class members), also using PascalCase. - * `SDL_WindowFlags` becomes `SDL.WindowFlags` (enum) - * `SDL_Event` becomes `SDL.Event` (struct) - * `SDL_Keycode` becomes `SDL.Keycode` (enum) -* **Constants:** Native SDL3 `#define` constants (e.g., `SDL_INIT_VIDEO`) are usually mapped to enum members or `public const int` fields within the relevant C# enum or static class. - * `SDL_INIT_VIDEO` becomes `SDL.InitFlags.Video` - -**2. File Structure of `lib/SDL3-CS/SDL3-CS/SDL/`:** - -The C# source files for the core SDL3 bindings are primarily located under `lib/SDL3-CS/SDL3-CS/SDL/`. This directory is further organized into subdirectories that often mirror SDL3's own categorization of its API (e.g., `Basics`, `Video`, `Audio`, `Input Events`, `GPU`). - -* **P/Invoke Declarations:** The actual `[LibraryImport]` or `[DllImport]` attributes for native functions are often found in files named `PInvoke.cs` within the relevant subdirectory (e.g., [`lib/SDL3-CS/SDL3-CS/SDL/Basics/init/PInvoke.cs`](lib/SDL3-CS/SDL3-CS/SDL/Basics/init/PInvoke.cs:1) for `SDL_Init`, [`lib/SDL3-CS/SDL3-CS/SDL/Video/video/PInvoke.cs`](lib/SDL3-CS/SDL3-CS/SDL/Video/video/PInvoke.cs:1) for windowing functions). -* **Enum and Struct Definitions:** These are typically in their own dedicated `.cs` files, named after the type (e.g., [`lib/SDL3-CS/SDL3-CS/SDL/Video/video/WindowFlags.cs`](lib/SDL3-CS/SDL3-CS/SDL/Video/video/WindowFlags.cs:1), [`lib/SDL3-CS/SDL3-CS/SDL/Input Events/events/Event.cs`](lib/SDL3-CS/SDL3-CS/SDL/Input%20Events/events/Event.cs:1), [`lib/SDL3-CS/SDL3-CS/SDL/Input Events/keycode/Keycode.cs`](lib/SDL3-CS/SDL3-CS/SDL/Input%20Events/keycode/Keycode.cs:1)). -* **Partial Class `SDL`:** The main C# static class `SDL` (in the `SDL3` namespace) is defined as a `partial class`. This means its members (P/Invoke methods, nested enums/structs, helper functions) are spread across multiple files within these subdirectories but are all part of the single `SDL3.SDL` static class from the perspective of an API consumer. - -**3. Strategy for Finding C# Equivalents:** - -* **Identify the Native SDL3 Element:** Start with the name of the native C function, enum, struct, or constant you need (e.g., `SDL_GetWindowFlags`, `SDL_EventType`, `SDL_SCANCODE_A`). -* **Apply C# Naming Conventions:** - * Remove `SDL_` prefix. - * Convert to `PascalCase` (e.g., `GetWindowFlags`, `EventType`, `ScancodeA`). Note that for constants like scancodes, the C# enum member might be simpler (e.g. `SDL.Scancode.A`). -* **Determine the SDL Subsystem:** Understand which part of SDL the function belongs to (e.g., Video, Events, Keyboard, Mouse). This will guide you to the likely subdirectory in `lib/SDL3-CS/SDL3-CS/SDL/`. - * Example: `SDL_CreateWindow` is a Video function. Look in `lib/SDL3-CS/SDL3-CS/SDL/Video/video/`. - * Example: `SDL_PollEvent` is an Event function. Look in `lib/SDL3-CS/SDL3-CS/SDL/Input Events/events/`. -* **Search within the Subsystem Directory:** - * For functions, check `PInvoke.cs` files first. - * For enums/structs, look for a `.cs` file matching the PascalCase name (e.g., `EventType.cs`, `WindowFlags.cs`). - * The C# equivalent will typically be a static method or nested type of `SDL3.SDL` (e.g., `SDL.CreateWindow()`, `SDL.EventType`, `SDL.WindowFlags`). -* **Use Code Search:** If the location isn't immediately obvious: - * Use your IDE's search functionality (or a command-line tool like `grep` or `rg`) within the `lib/SDL3-CS/SDL3-CS/SDL/` directory. - * Search for the PascalCase C# name (e.g., `CreateWindow`). - * Search for the native C name (e.g., `SDL_CreateWindow`) as it often appears in comments or `EntryPoint` attributes of P/Invoke declarations (e.g., `[LibraryImport(SDLLibrary, EntryPoint = "SDL_CreateWindow")]`). -* **Consult SDL3 Wiki & SDL3-CS Examples:** - * The official [SDL Wiki](https://wiki.libsdl.org/SDL3/FrontPage) provides documentation for the native SDL3 API. Understanding the native function's purpose and parameters helps. - * The `SDL3-CS` repository includes examples in `lib/SDL3-CS/SDL3-CS.Examples/` which demonstrate common usage patterns. - -**4. Key C# Idioms and Marshalling in SDL3-CS:** - -Be aware of common C# idioms used in the bindings: - -* **Return Values for Success/Failure:** Many SDL C functions return `0` for success and a negative value for error. In SDL3-CS, these are often converted to `bool`, where `true` indicates success and `false` indicates failure. Use `SDL.GetError()` to get detailed error information. (e.g., `SDL.Init()` returns `bool`). -* **String Marshalling:** - * `const char*` input parameters in C are often marshalled as `string` in C#. - * `char*` (for output strings from SDL) or `const char*` return values from SDL might be marshalled as `string`, or sometimes as `IntPtr` requiring manual marshalling (e.g., using `Marshal.PtrToStringUTF8()` or `SDL.PtrToStringUTF8()` if available). SDL3-CS aims for direct `string` usage where idiomatic. -* **Pointer Parameters (`*`, `**`):** - * Pointers to simple types or structs passed by value to C functions might become `ref` or `out` parameters in C# for structs, or direct value types (`int`, `float`). - * `SDL_Event*` in C (like in `SDL_PollEvent(SDL_Event* event)`) becomes `out SDL.Event e` or `ref SDL.Event e` in C#. - * Opaque pointers (handles like `SDL_Window*`, `SDL_Renderer*`) are typically represented as `IntPtr` in C# or wrapped in dedicated C# classes/structs if the binding provides higher-level abstractions. SDL3-CS often uses `IntPtr` for these handles. -* **Enums:** C enums are mapped to C# enums, often with the `[Flags]` attribute if they are bitmasks. -* **Callbacks:** C function pointers for callbacks are mapped to C# delegates. -* **Helper Methods:** The `SDL` static class in SDL3-CS includes various helper methods for marshalling and pointer manipulation (e.g., `SDL.PointerToStructure()`, `SDL.StructureToPointer()`, `SDL.StringToPointer()`). These can be useful if you need to interact with more complex native patterns not fully abstracted by a direct C# method. - -* **Troubleshooting SDL Extension Libraries (e.g., SDL3_image, SDL3_ttf) with SDL3-CS:** - * SDL extension libraries (like `SDL3_image` for image loading or `SDL3_ttf` for font rendering) provide specialized functionality on top of the core SDL3 library. While `SDL3-CS` provides bindings for these, their interaction with core SDL3 features (like the properties system for `SDL_Texture`) might not always be straightforward or fully documented externally. - * **Problem Identification:** If a function from an SDL extension library (e.g., `SDL3.Image.LoadTexture()`) returns an SDL object (like an `SDL_Texture`), but subsequent attempts to use standard SDL3 mechanisms on that object (e.g., `SDL.GetTextureProperties()` to get dimensions) fail or don't yield expected results, it might indicate that the extension library handles or exposes information differently. - * **Investigation Strategy:** - 1. **Consult Official SDL Wiki:** First, check the official SDL Wiki (or the specific extension library's documentation, if available and linked) for guidance on the function in question and how it interacts with core SDL types. However, be aware that C# binding specifics might not be covered. - 2. **Examine SDL3-CS Bindings Directly:** If official documentation is insufficient or doesn't clarify the C# binding behavior, the most reliable source of truth is the `SDL3-CS` library's source code itself (located in `lib/SDL3-CS/SDL3-CS/`). - * Look for the C# wrapper function corresponding to the native SDL extension library function you're using (e.g., in `lib/SDL3-CS/SDL3-CS/Image/PInvoke.cs` for `SDL3_image` functions). - * See if the extension library offers alternative C# functions within its own namespace (e.g., `SDL3.Image.Load()` to load to an `SDL_Surface` first, from which dimensions can be reliably obtained before converting to an `SDL_Texture`). - * Check how the C# structs for relevant types (e.g., `SDL.Surface` in `lib/SDL3-CS/SDL3-CS/SDL/Video/surface/Surface.cs`) are defined to understand how to access their members (like `Width`, `Height`) after marshalling an `IntPtr`. - 3. **Consider Intermediate Steps:** Sometimes, an extension library might require or work more reliably with an intermediate step. For example, instead of directly loading an image to an `SDL_Texture`, loading it to an `SDL_Surface` first (using a function from the image extension library), then getting information from the `SDL_Surface` (which is a well-defined core SDL structure), and finally creating the `SDL_Texture` from the `SDL_Surface` using a core SDL function (e.g., `SDL.CreateTextureFromSurface()`) can be a more robust approach. Remember to manage the lifecycle of intermediate objects (like freeing the `SDL_Surface` after the texture is created). - 4. **Error Checking:** Always check return values from SDL functions. For functions from extension libraries, use the standard `SDL.GetError()` to retrieve error messages, as specific `Extension.GetError()` functions may not exist or be necessary. -By understanding these conventions and the structure of the `lib/SDL3-CS` library, an AI (or human developer) can more effectively locate and utilize the C# equivalents of native SDL3 functionalities. diff --git a/project/paper/main.pdf b/project/paper/main.pdf new file mode 100644 index 00000000..076a8145 Binary files /dev/null and b/project/paper/main.pdf differ diff --git a/project/paper/main.typ b/project/paper/main.typ new file mode 100644 index 00000000..80c36f30 --- /dev/null +++ b/project/paper/main.typ @@ -0,0 +1,136 @@ +#import "@preview/rubber-article:0.4.2": * + +#show: article.with( + lang: "en", + header-display: true, + header-title: "Night Engine: A C# Game Development Engine", + eq-numbering: "(1.1)", + eq-chapterwise: true, + margins: 1.25in, + cols: none, +) + +#maketitle(title: "Night Engine: A C# Game Development Engine", authors: ("Danny Solivan",), date: datetime + .today() + .display("[day]. [month repr:long] [year]")) + +#block(width: 100%)[ + *Abstract* + + This paper presents Night Engine, a "batteries\-included" C\# game development framework built upon the SDL3 library. The project addresses the need for a streamlined and productive development workflow for C\# developers by providing a high-level, Love2D\-inspired API. The core architecture is two-tiered, consisting of `Night.Framework`, a foundational wrapper around SDL3, and the planned `Night.Engine`, a more opinionated system for common game patterns. Key features of the initial implementation include declarative window management, simplified input polling, 2D sprite-based graphics rendering, and a structured game loop. This work serves as a case study in API design, demonstrating how a low-level, high-performance C++ library can be wrapped to create an idiomatic and developer-friendly C\# experience. The viability of this approach is evaluated through the implementation of a sample game and analysis of the API's expressiveness. +] + +// ============== Main Body ============== + += Introduction +In the landscape of C\# game development, developers are often presented with a choice between monolithic, feature-rich engines like Unity and Godot, or the direct, verbose use of low-level C++ libraries via C\# bindings. While powerful, large engines impose significant architectural opinions and overhead. Conversely, direct library interaction, for instance with SDL3, offers maximum control at the cost of productivity and requires boilerplate for fundamental tasks. This creates a gap for lightweight, "batteries\-included" frameworks that offer high-level abstractions without sacrificing performance or flexibility. + +This project, Night Engine, aims to fill that niche. It is inspired by the design philosophy of LÖVE, a popular framework for the Lua language, which provides a simple, module\-based API for 2D game development. Night Engine adapts this philosophy to the C\# ecosystem, leveraging the modern capabilities of `.NET 9` and the cross-platform power of SDL3. + +The primary goals of this project were to: +- Design and implement a foundational C\# framework (`Night.Framework`) that provides a simple, productive, and Love2D\-inspired API over SDL3. +- Establish a robust architectural base for a future, higher-level, and more opinionated game engine (`Night.Engine`) that will include systems like an ECS and scene management. +- Validate the API design and framework capabilities through the creation of a sample platformer game, demonstrating its ease of use and feature set. + +This paper is structured as follows. Section 2 discusses the theoretical foundations and compares Night Engine to related work in the field. Section 3 details the system's architecture and justifies key design decisions. Section 4 covers notable implementation challenges and their solutions. Section 5 presents an evaluation of the framework. Finally, Section 6 concludes and outlines directions for future work. + += Theoretical Foundations & Related Work +The design of Night Engine is grounded in principles of API design, focusing on creating a high-level abstraction over a low-level system. This involves trade-offs between expressiveness, performance, and control. + +The primary influence is *LÖVE*, a 2D game framework for Lua. LÖVE's success stems from its simple, module\-centric API (e.g., `love.graphics`, `love.keyboard`) that exposes core functionalities in an easy-to-use manner. Night Engine seeks to replicate this developer experience in a statically\-typed C\# context, which introduces challenges and opportunities in API design, such as the use of static classes to mimic Lua's global tables. + +Night Engine also exists within the ecosystem of C\# game frameworks. It can be compared to: +- *MonoGame* and *FNA*: These frameworks are implementations of the Microsoft `XNA 4.0` API. While mature and powerful, their API design is rooted in a paradigm from over a decade ago. Night Engine differentiates itself by building on the modern SDL3 and adopting a different API philosophy. +- *`Raylib-cs`*: A C\# binding for Raylib, another excellent C-based game framework. The key difference lies in the level of abstraction; `Raylib-cs` is a more direct binding, whereas Night Engine provides a more curated and idiomatic C\# layer. +- *Direct `SDL3-CS` usage*: The `SDL3-CS` library provides the raw C\# bindings for SDL3. Night Engine's value proposition is the abstraction layer it builds on top of these bindings, shielding the developer from pointer arithmetic and verbose SDL function calls. + +// TODO: Are there other C\# frameworks that attempt a similar "minimalist wrapper" approach that should be cited here? For instance, frameworks focused on specific genres like roguelikes (e.g., SadConsole)? + += System Architecture & Design +The architecture of Night Engine is intentionally layered to separate concerns and provide a stable foundation for future growth. It is composed of two primary libraries: `Night.Framework` and the prospective `Night.Engine`. + +#figure( + rect(width: 80%, height: 30%, stroke: black)[ + #align(center)[ + Application (`Night.SampleGame`)\ + (Implements `IGame` interface)\ + ↓\ + `Night.Engine` (Future)\ + (ECS, Scene Graph, Physics)\ + ↓\ + `Night.Framework` (Love2D-style API)\ + (`Night.Graphics`, `Night.Window`, `Night.Input`)\ + ↓\ + `SDL3-CS` (C\# Bindings)\ + ↓\ + `SDL3` (Native C++ Library) + ] + ], + caption: [The layered architecture of the Night Engine ecosystem.], +) + +The choice of C\# 13 and `.NET 9` was motivated by the desire to use a modern, performant, and type-safe language with a rich ecosystem. SDL3 was chosen as the underlying backend due to its industry-standard status, cross-platform capabilities, and modern features compared to its predecessor. + +== Component A: Night.Framework +The core of the current implementation is `Night.Framework`. It exposes functionality through a series of static classes within the `Night` namespace (e.g., `Night.Graphics`, `Night.Window`, `Night.Mouse`). This design directly emulates the module-based API of LÖVE and provides a simple, accessible surface area for developers. All interactions with SDL3 are encapsulated within this layer. + +== Component B: The Game Loop +The framework defines a structured game loop managed by `Night.Framework.Run()`. Developers do not write their own loop but instead implement the `Night.IGame` interface, which provides callbacks for key stages: +- `Load()`: Called once at the start to load assets. +- `Update(deltaTime)`: Called each frame for game logic. +- `Draw()`: Called each frame for rendering. +- `KeyPressed(key)`: An event-based callback for input. + +This inversion of control simplifies game creation and ensures a consistent execution model, including details like delta time calculation. + +// TODO: Should this section include a more detailed diagram of the call flow from the user's `IGame.Update` and `IGame.Draw` implementations through `FrameworkLoop.cs` to the underlying SDL3 calls? + += Implementation Challenges & Solutions +Translating the design into a functional framework presented several technical challenges. + +== Challenge 1: Idiomatic API Translation +A significant challenge was translating LÖVE's dynamic, table-based Lua API into idiomatic C\#. The decision was made to use static classes to provide a global, module\-like access pattern (e.g., `Night.Graphics.Draw(etc.)`). This avoids the need for singleton instances while providing a familiar structure for those coming from LÖVE. For callbacks, C\# events and interfaces (`IGame`) provide a type-safe alternative to Lua's function-based approach. + +== Challenge 2: Native Library Management +A common problem in `.NET` projects that rely on native code is ensuring the unmanaged binaries (e.g., `SDL3.dll`) are available at runtime. The solution involved two parts: +1. A Python script (`scripts/sync_sdl3.py`) to fetch the correct pre-built SDL3 binaries for different platforms and place them in a known location (`lib/SDL3-Prebuilt/`). +2. Custom MSBuild targets in the `Night.SampleGame.csproj` file to automatically copy the required native libraries from the `lib` directory to the application's output directory during the build process. + +// TODO: What was the most difficult bug encountered during the implementation of the graphics renderer or input system? For example, was there an issue with texture management, coordinate systems, or event polling logic? Describe it here. + += Evaluation & Results +To be a successful project, Night Engine must be evaluated against its primary goals: providing a usable, productive API for C\# game development. + +Our evaluation is currently qualitative, based on the implementation of `Night.SampleGame`. The API's ability to support a simple platformer with player movement, sprite rendering, and input handling demonstrates its functional completeness for the `v0.1.0` feature set. + +For quantitative analysis, future work will focus on benchmarking. We propose the following metrics: +- *Performance*: Measure the overhead of the framework by comparing the framerate of drawing N sprites using `Night.Graphics` versus raw `SDL3-CS` calls. +- *API Expressiveness*: Compare the lines of code required to perform common tasks (e.g., opening a window and drawing a sprite) in Night Engine versus other frameworks. + +#figure( + table( + columns: (1fr, 1fr, 1fr), + align: (center, center, center), + [*Task*], [*Night Engine (LoC)*], [*Raw `SDL3-CS` (LoC)*], + [Open 800x600 Window], [2], [~10-15], + [Load & Draw Sprite], [2], [~15-20], + ), + caption: [A hypothetical comparison of lines-of-code (LoC) for common tasks, illustrating the abstraction benefit.], +) + +// TODO: What are the most meaningful benchmarks to run? Raw sprite drawing throughput? Input latency? Game loop overhead with N entities? Define the specific experiments to be conducted. + += Conclusion & Future Work +This paper has presented Night Engine, a C\# game development framework designed to provide a LÖVE\-inspired API over SDL3. The project successfully implemented the core `v0.1.0` feature set for `Night.Framework`, including windowing, input, and graphics, and validated its design through a sample application. The primary goal of creating a productive, high-level abstraction over a powerful low-level library was met. + +The main limitation of the current work is its scope. The framework currently only covers a fraction of the LÖVE API and does not yet include the higher-level `Night.Engine` components. + +Future work will proceed in two main directions, as outlined in the project's product requirements document: +1. Expanding `Night.Framework`: Implementing further modules to achieve greater parity with the LÖVE API, including audio (`Night.Audio`), fonts (`Night.Font`), and joystick support (`Night.Joystick`). +2. Developing `Night.Engine`: Building the higher-level, opinionated engine on top of the framework. This will include an Entity Component System (ECS), a scene graph, and more advanced asset management. +3. Tooling and Performance: Investigating tooling, such as Dear ImGui integration for debug consoles, and performance enhancements, potentially including a migration from SDL_Renderer to the more powerful SDL_GPU backend. + += References +// TODO: Add citations for influential works. +// e.g., The LÖVE documentation, the SDL3 wiki, books on API design or game engine architecture. +// #bibliography("references.bib") diff --git a/project/love2d-api/roadmap.md b/project/roadmap.md similarity index 68% rename from project/love2d-api/roadmap.md rename to project/roadmap.md index 21d7a047..ab132236 100644 --- a/project/love2d-api/roadmap.md +++ b/project/roadmap.md @@ -6,89 +6,78 @@ Most functions list the Love2D equivalent module/function/callback implementatio ### Project -- [ ] `docfx` generation onto GitHub pages -- [ ] Tests -- [ ] Logo and icon -- [ ] CI +- [x] `docfx` generation onto GitHub pages +- [x] Testing framework +- [x] Implement tests +- [ ] Logo and icon - nightO)engine +- [x] CI +- [x] Logging system ### Modules - [ ] `love.filesystem`: Provides an interface to the user's filesystem. -- [ ] `love.graphics`: Drawing of shapes and images, management of screen geometry. -- [ ] `love.image`: Provides an interface to decode encoded image data. +- [ ] `love.graphics`: Drawing of shapes and images, management of screen geometry. Partial completion. +- [x] `love.joystick`: Provides an interface to connected joysticks. - [ ] `love.keyboard`: Provides an interface to the user's keyboard. - [ ] `love.mouse`: Provides an interface to the user's mouse. -- [ ] `love.timer`: Provides high-resolution timing functionality. +- [x] `love.system`: Provides access to information about the user's system. +- [x] `love.timer`: Provides high-resolution timing functionality. - [ ] `love.window`: Provides an interface for the program's window. ### Callbacks - General -- [ ] `love.draw`: Callback function used to draw on the screen every frame. -- [ ] `love.load`: This function is called exactly once at the beginning of the game. -- [ ] `love.run`: The main callback function, containing the main loop. A sensible default is used when left out. -- [ ] `love.update`: Callback function used to update the state of the game every frame. +- [x] `love.draw`: Callback function used to draw on the screen every frame. +- [x] `love.load`: This function is called exactly once at the beginning of the game. +- [x] `love.run`: The main callback function, containing the main loop. A sensible default is used when left out. +- [x] `love.update`: Callback function used to update the state of the game every frame. ### Callbacks - Keyboard -- [ ] `love.keypressed`: Callback function triggered when a key is pressed. -- [ ] `love.keyreleased`: Callback function triggered when a keyboard key is released. +- [x] `love.keypressed`: Callback function triggered when a key is pressed. +- [x] `love.keyreleased`: Callback function triggered when a keyboard key is released. ### Callbacks - Mouse -- [ ] `love.mousepressed`: Callback function triggered when a mouse button is pressed. -- [ ] `love.mousereleased`: Callback function triggered when a mouse button is released. +- [x] `love.mousepressed`: Callback function triggered when a mouse button is pressed. +- [x] `love.mousereleased`: Callback function triggered when a mouse button is released. ### Callbacks - General -- [ ] `love.errhand`: The error handler, used to display error messages. (Note: `love.errorhandler` is also listed for 11.0, likely an alias or the preferred name) -- [ ] `love.errorhandler`: The error handler, used to display error messages. - -### Types - -- [ ] `Data`: The superclass of all data. -- [ ] `Object`: The superclass of all LÖVE types. -- [ ] `Variant`: The types supported by love.thread and love.event. +- [x] `love.errorhandler`: The error handler, used to display error messages. +- [x] `love.joystickpressed`: Callback function triggered when a joystick button is pressed. +- [x] `love.joystickreleased`: Callback function triggered when a joystick button is released. +- [x] `love.gamepadaxis`: Called when a Joystick's virtual gamepad axis is moved. +- [x] `love.gamepadpressed`: Called when a Joystick's virtual gamepad button is pressed. +- [x] `love.gamepadreleased`: Called when a Joystick's virtual gamepad button is released. +- [x] `love.joystickadded`: Called when a Joystick is connected. +- [x] `love.joystickaxis`: Called when a joystick axis moves. +- [x] `love.joystickhat`: Called when a joystick hat direction changes. +- [x] `love.joystickremoved`: Called when a Joystick is disconnected. ### General -- [ ] Config Files: Game configuration settings. - -## Version 0.2.0 - -### Modules - -- [ ] `love.joystick`: Provides an interface to connected joysticks. - -### Callbacks - Joystick - -- [ ] `love.joystickpressed`: Callback function triggered when a joystick button is pressed. -- [ ] `love.joystickreleased`: Callback function triggered when a joystick button is released. -- [ ] `love.gamepadaxis`: Called when a Joystick's virtual gamepad axis is moved. -- [ ] `love.gamepadpressed`: Called when a Joystick's virtual gamepad button is pressed. -- [ ] `love.gamepadreleased`: Called when a Joystick's virtual gamepad button is released. -- [ ] `love.joystickadded`: Called when a Joystick is connected. -- [ ] `love.joystickaxis`: Called when a joystick axis moves. -- [ ] `love.joystickhat`: Called when a joystick hat direction changes. -- [ ] `love.joystickremoved`: Called when a Joystick is disconnected. +- [x] Config Files: Game configuration settings. -## Version 0.3.0 +## Version 0.2.X ### Modules -- [ ] `love.audio`: Provides an audio interface for playback/recording sound. - [ ] `love.event`: Manages events, like keypresses. -- [ ] `love.sound`: This module is responsible for decoding sound files. -- [ ] `love.system`: Provides access to information about the user's system. +- [ ] `love.image`: Provides an interface to decode encoded image data. +- [ ] `love.graphics`: Drawing of shapes and images, management of screen geometry. Potential completion. -## Version 0.4.0 +## Version 0.3.X ### Project - [ ] Aseprite support +- [ ] NuGet Package ### Modules +- [ ] `love.audio`: Provides an audio interface for playback/recording sound. - [ ] `love.font`: Allows you to work with fonts. +- [ ] `love.sound`: This module is responsible for decoding sound files. ### Callbacks - General @@ -98,7 +87,7 @@ Most functions list the Love2D equivalent module/function/callback implementatio - [ ] `love.focus`: Callback function triggered when window receives or loses focus. -## Version 0.5.0 +## Version 0.4.X ### Modules @@ -120,7 +109,7 @@ Most functions list the Love2D equivalent module/function/callback implementatio - [ ] `love.textinput`: Called when text has been entered by the user. -## Version 0.6.0 +## Version 0.5.X ### Functions @@ -134,7 +123,7 @@ Most functions list the Love2D equivalent module/function/callback implementatio - [ ] `love.mousemoved`: Callback function triggered when the mouse is moved. -## Version 0.7.0 +## Version 0.6.X ### Modules @@ -151,7 +140,6 @@ Most functions list the Love2D equivalent module/function/callback implementatio ### Callbacks - Window - [ ] `love.directorydropped`: Callback function triggered when a directory is dragged and dropped onto the window. - - [ ] `love.filedropped`: Callback function triggered when a file is dragged and dropped onto the window. ### Callbacks - Keyboard @@ -162,7 +150,7 @@ Most functions list the Love2D equivalent module/function/callback implementatio - [ ] `love.wheelmoved`: Callback function triggered when the mouse wheel is moved. -## Version 0.8 +## Version 0.7.X ### Modules @@ -174,6 +162,7 @@ Most functions list the Love2D equivalent module/function/callback implementatio - [ ] `love.setDeprecationOutput`: Sets whether LÖVE displays warnings when using deprecated functionality. ## Version Horizon (Future) + Mostly related to mobile and touchscreen. ### Modules @@ -191,4 +180,5 @@ Mostly related to mobile and touchscreen. - [ ] `love.touchreleased`: Callback function triggered when the touch screen stops being touched. ## Version Horizon (Far Future) + Networking implementation including rollback. diff --git a/project/testing-guidelines.md b/project/testing-guidelines.md new file mode 100644 index 00000000..76b39060 --- /dev/null +++ b/project/testing-guidelines.md @@ -0,0 +1,531 @@ +# Night Engine Testing Guidelines + +This document provides guidelines for writing tests for the Night Engine, specifically for the `Night.Framework` and future `Night.Engine` components. Our testing framework is built using xUnit and a custom set of base classes. There are three primary types of test cases: `GameTestCase` for automated tests running within the game environment, `ManualTestCase` for tests requiring user validation within the game environment, and `ModTestCase` for module-level tests that run in isolation without a full game instance. + +## Table of Contents + +- [Night Engine Testing Guidelines](#night-engine-testing-guidelines) + - [Table of Contents](#table-of-contents) + - [1. Overview](#1-overview) + - [2. Core Concepts](#2-core-concepts) + - [Test Groups](#test-groups) + - [Test Cases](#test-cases) + - [`GameTestCase`](#gametestcase) + - [`ManualTestCase`](#manualtestcase) + - [`ModTestCase`](#modtestcase) + - [Automated vs. Manual Tests](#automated-vs-manual-tests) + - [3. Directory Structure](#3-directory-structure) + - [4. Naming Conventions](#4-naming-conventions) + - [Files](#files) + - [Classes](#classes) + - [Methods in Test Groups](#methods-in-test-groups) + - [Test Case `Name` Property](#test-case-name-property) + - [5. Creating Tests: Step-by-Step](#5-creating-tests-step-by-step) + - [Step 1: Identify the Module and Feature](#step-1-identify-the-module-and-feature) + - [Step 2: Choose the Appropriate Test Case Type](#step-2-choose-the-appropriate-test-case-type) + - [Step 3: Create a Test Case File and Class](#step-3-create-a-test-case-file-and-class) + - [`GameTestCase` Example](#gametestcase-example) + - [`ManualTestCase` Example](#manualtestcase-example) + - [`ModTestCase` Example](#modtestcase-example) + - [Step 4: Implement Test Logic](#step-4-implement-test-logic) + - [For `GameTestCase` and `ManualTestCase` (`IGame` Methods)](#for-gametestcase-and-manualtestcase-igame-methods) + - [For `ModTestCase`](#for-modtestcase) + - [Step 5: Manage Test Lifecycle and Status (`GameTestCase`/`ManualTestCase`)](#step-5-manage-test-lifecycle-and-status-gametestcasemanualtestcase) + - [Step 6: Create or Update the Test Group](#step-6-create-or-update-the-test-group) + - [6. Key Base Classes and Interfaces](#6-key-base-classes-and-interfaces) + - [7. Best Practices](#7-best-practices) + +## 1. Overview + +The primary goal of our testing strategy is to ensure the reliability and correctness of the `Night Engine`'s features. +Tests are organized into `TestGroup` classes, which are discovered and run by xUnit. +There are three main types of test cases: + +- **`GameTestCase`**: These tests implement [`Night.IGame`](../src/Night/IGame.cs) and run within the engine's main loop, allowing for automated testing of features that require a game context (e.g., graphics, input, windowing). +- **`ManualTestCase`**: A specialization of `GameTestCase`, these also run as `IGame` instances but are designed for scenarios requiring manual user confirmation (e.g., visual verification of rendering). +- **`ModTestCase`**: These tests are designed for testing individual functions, classes, or modules in isolation, much like traditional unit tests. They do not run as `IGame` instances and are suitable for testing logic that doesn't require a game window or full engine loop. + +## 2. Core Concepts + +### Test Groups + +- **Purpose:** To group related test cases for a specific module or major feature (e.g., `Filesystem`, `Graphics`, `Configuration`). +- **Implementation:** Create a C# class that inherits from [`NightTest.Core.TestGroup`](../tests/Core/TestGroup.cs). +- **Location:** `tests/Groups/[ModuleName]/[ModuleName]Group.cs`. +- **Example:** [`ConfigurationGroup.cs`](../tests/Groups/Configuration/ConfigurationGroup.cs) + +### Test Cases + +#### `GameTestCase` + +- **Purpose:** An individual automated test scenario that verifies specific functionality requiring the engine's runtime environment (e.g., interacting with the game window, graphics, or input systems). +- **Implementation:** Create a C# class that: + - Implements [`Night.IGame`](../src/Night/IGame.cs). + - Inherits from [`NightTest.Core.GameTestCase`](../tests/Core/GameTestCase.cs). +- **Logic:** Test assertions and status updates (`CurrentStatus`, `Details`) are typically handled within the `Update(double deltaTime)` method. The test concludes by calling `EndTest()`. +- **Location:** Typically in `tests/Groups/[ModuleName]/[FeatureName]Tests.cs`. +- **`Type` Property:** `TestType.Automated`. + +#### `ManualTestCase` + +- **Purpose:** An individual test scenario that requires user interaction or visual confirmation to determine pass/fail status. These tests also run within the engine's runtime environment. +- **Implementation:** Create a C# class that: + - Implements [`Night.IGame`](../src/Night/IGame.cs). + - Inherits from [`NightTest.Core.ManualTestCase`](../tests/Core/ManualTestCase.cs) (which itself inherits from `GameTestCase`). +- **Logic:** Uses the `RequestManualConfirmation("Prompt message")` method, typically within `Update(double deltaTime)`, to prompt the user. The base `ManualTestCase` handles UI for pass/fail and timeout. +- **Location:** Typically in `tests/Groups/[ModuleName]/[FeatureName]Tests.cs`. +- **`Type` Property:** `TestType.Manual`. +- **Example:** [`GraphicsClearColorTest`](../tests/Groups/Graphics/GraphicsClearTest.cs) + +#### `ModTestCase` + +- **Purpose:** An individual automated test scenario for testing specific functions, methods, or classes in isolation, without needing a full game loop or window. Ideal for unit-testing modules. +- **Implementation:** Create a C# class that: + - Inherits from [`NightTest.Core.ModTestCase`](../tests/Core/ModTestCase.cs). +- **Logic:** Test logic and assertions (using xUnit's `Assert` class) are placed within the overridden `Run()` method. A `SuccessMessage` property defines the message upon successful completion without exceptions. +- **Location:** Typically in `tests/Groups/[ModuleName]/[FeatureName]Tests.cs` or `[SpecificTestName]Test.cs`. +- **`Type` Property:** `TestType.Automated` (as set by the underlying `BaseTestCase`). +- **Example:** [`ConfigurationGameConfig_GetSet`](../tests/Groups/Configuration/GameConfigTest.cs) (from `GameConfigTest.cs`) + +### Automated vs. Manual Tests + +- **Automated Tests:** + - These tests run and determine their pass/fail status programmatically. + - Includes all tests inheriting from `GameTestCase` (that are not `ManualTestCase`) and all tests inheriting from `ModTestCase`. + - Marked with `[Trait("TestType", "Automated")]` in the `TestGroup`. +- **Manual Tests:** + - These tests require human intervention to verify the outcome. + - Implemented by inheriting from `ManualTestCase`. + - Marked with `[Trait("TestType", "Manual")]` in the `TestGroup`. + +## 3. Directory Structure + +```plaintext +tests/ +├── Core/ # Base classes, interfaces, and core testing utilities +│ ├── BaseTestCase.cs +│ ├── GameTestCase.cs +│ ├── ManualTestCase.cs +│ ├── ModTestCase.cs +│ ├── ITestCase.cs +│ ├── TestGroup.cs +│ ├── TestTypes.cs # Contains TestStatus, TestType enums +│ └── ... +└── Groups/ # Contains groups of tests, organized by engine module + └── [ModuleName]/ # e.g., Filesystem, Graphics, Configuration, Window + ├── [ModuleName]Group.cs # xUnit test class for the group + ├── [Feature1]Tests.cs # Contains GameTestCase/ManualTestCase/ModTestCase classes for Feature1 + └── [Feature2]Tests.cs # Contains GameTestCase/ManualTestCase/ModTestCase classes for Feature2 +``` + +## 4. Naming Conventions + +Consistency in naming is crucial for maintainability. + +### Files + +- **Test Group Files:** `[ModuleName]Group.cs` (e.g., `FilesystemGroup.cs`, `ConfigurationGroup.cs`) +- **Test Case Files:** `[FeatureName]Tests.cs` or `[SpecificTestName]Test.cs`. Generally, group related test cases for a sub-feature into one file. (e.g., `LinesTests.cs`, `GraphicsClearTest.cs`, `GameConfigTest.cs`). + +### Classes + +- **Test Group Classes:** `[ModuleName]Group` (e.g., `FilesystemGroup`, `ConfigurationGroup`). + - Example: `public class FilesystemGroup : TestGroup` +- **`GameTestCase` / `ManualTestCase` Classes:** `[ModuleName][FeatureName]_[SpecificBehavior]Test` or a descriptive name if it covers a broader feature. + - Example: `public class FilesystemLines_ReadStandardFileTest : GameTestCase` + - Example: `public class GraphicsClearColorTest : ManualTestCase` (as in [`GraphicsClearTest.cs`](../tests/Groups/Graphics/GraphicsClearTest.cs)) +- **`ModTestCase` Classes:** `[ModuleName][ClassName]_[MethodOrBehavior]Test` or `[ModuleName][FeatureName]_[SpecificBehavior]Test`. + - Example: `public class ConfigurationGameConfig_GetSet : ModTestCase` (as in [`GameConfigTest.cs`](../tests/Groups/Configuration/GameConfigTest.cs)) + +### Methods in Test Groups + +- Related `GameTestCase` instances (automated tests requiring the game environment) should be grouped into a single `[Fact]` method. +- `ManualTestCase` instances can either be run by a dedicated `[Fact]` method or grouped similarly if appropriate. +- For `ModTestCase` instances, related tests (e.g., all tests for a specific function or a closely related set of behaviors within a module) should also be grouped into a single `[Fact]` method. +- **Method Name for consolidated `GameTestCase` group:** `Run_[ModuleName][FeatureOrConcept]_GameTests()` (e.g., `public void Run_Timer_GameTests()`). + - This method will contain multiple calls to `this.Run_GameTestCase(new ...Test());`. +- **Method Name for individual `ManualTestCase`:** `Run_[TestCaseClassName]()` (e.g., `public void Run_GraphicsClearColorTest()`). If grouped, follow a similar pattern to `GameTestCase` or `ModTestCase` consolidated groups. +- **Method Name for consolidated `ModTestCase` group:** `Run_[ModuleName][FeatureOrFunction]_ModTests()` (e.g., `public void Run_ConfigurationManager_ModTests()`). + - This method will then contain multiple calls to `this.Run_ModTestCase(new ...Test());` for each individual test case class belonging to that group. + +### Test Case `Name` Property + +- This property is part of the `ITestCase` interface (implemented by `BaseTestCase`) and is used for logging and identification. +- **Format:** `"[ModuleName].[FeatureNameOrClass].[SpecificBehaviorOrConcept]"` + - `GameTestCase`/`ManualTestCase` Example: `public override string Name => "Graphics.Clear";` (from [`GraphicsClearTest.cs`](../tests/Groups/Graphics/GraphicsClearTest.cs)) + - `ModTestCase` Example: `public override string Name => "Configuration.GameConfig";` (from [`GameConfigTest.cs`](../tests/Groups/Configuration/GameConfigTest.cs)) + +## 5. Creating Tests: Step-by-Step + +### Step 1: Identify the Module and Feature + +Determine which part of the `Night Engine` you are testing. This will dictate the directory and naming. Refer to [`project/PRD.md`](../project/PRD.md) for module names (e.g., `Night.Window`, `Night.Graphics`, `Night.Filesystem`, `Night.Configuration`). + +### Step 2: Choose the Appropriate Test Case Type + +- **`GameTestCase`**: For automated tests that need the game loop, graphics context, input handling, or other engine systems that require an active `IGame` instance. +- **`ManualTestCase`**: For tests requiring visual confirmation or specific user interactions within the game window. +- **`ModTestCase`**: For testing specific C# classes, methods, or logic in isolation, where a game window or the full engine loop is unnecessary (similar to traditional unit tests). + +### Step 3: Create a Test Case File and Class + +1. Navigate to `tests/Groups/[ModuleName]/`. +2. Create a new C# file, e.g., `[FeatureName]Tests.cs` or `[SpecificTestName]Test.cs`. +3. Inside this file, define your test case class, inheriting from the chosen base class. + +#### `GameTestCase` Example + +```csharp +// In tests/Groups/MyModule/MyFeatureGameTests.cs +using NightTest.Core; +using Night; // For IGame, engine APIs + +namespace NightTest.Groups.MyModule +{ + public class MyModuleMyFeature_ExpectedBehaviorGameTest : GameTestCase + { + public override string Name => "MyModule.MyFeature.ExpectedBehaviorGame"; + public override string Description => "Tests that MyFeature exhibits expected behavior within the game loop."; + // Type defaults to TestType.Automated via BaseTestCase + + protected override void Load() + { + base.Load(); // Important for base class setup + // Your test setup logic here (e.g., initialize variables, load assets) + this.Details = "Setting up MyFeature test..."; + } + + protected override void Update(double deltaTime) + { + if (this.IsDone) return; + + // Your test assertion logic here + bool conditionMet = Night.MyModule.MyFeature.DoSomething(); // Example call + if (conditionMet) + { + this.CurrentStatus = TestStatus.Passed; + this.Details = "MyFeature.DoSomething() returned true as expected."; + } + else + { + this.CurrentStatus = TestStatus.Failed; + this.Details = "MyFeature.DoSomething() returned false, expected true."; + } + this.EndTest(); // Call when test logic is complete + } + + protected override void Draw() + { + // Optional: drawing for debugging or if the test involves visuals + // Night.Graphics.Clear(Color.Black); + // Night.Graphics.DrawString("Testing MyFeature...", 10, 10); + } + } +} +``` + +#### `ManualTestCase` Example + +(See [`GraphicsClearTest.cs`](../tests/Groups/Graphics/GraphicsClearTest.cs) for a concrete example.) + +```csharp +// In tests/Groups/MyModule/MyVisualFeatureTests.cs +using NightTest.Core; +using Night; // For IGame, engine APIs + +namespace NightTest.Groups.MyModule +{ + public class MyModuleMyVisualFeature_UserConfirmationTest : ManualTestCase + { + public override string Name => "MyModule.MyVisualFeature.UserConfirmation"; + public override string Description => "User must visually confirm that MyVisualFeature renders correctly."; + // Type is TestType.Manual via ManualTestCase + + protected override void Load() + { + base.Load(); + // Setup for visual output + this.Details = "Test running, observe the visual output."; + } + + protected override void Update(double deltaTime) + { + if (this.IsDone) return; + + // Logic to trigger the visual effect if needed + + // Request confirmation after a short delay or when ready + // ManualTestPromptDelayMilliseconds can be used from base class + if (this.TestStopwatch.ElapsedMilliseconds > this.ManualTestPromptDelayMilliseconds) + { + this.RequestManualConfirmation("Does the screen display [expected visual outcome] correctly?"); + } + } + + protected override void Draw() + { + // Your drawing logic here + // Night.Graphics.Clear(Color.CornflowerBlue); + // Night.MyModule.MyVisualFeature.Render(); + } + } +} +``` + +#### `ModTestCase` Example + +(See [`ConfigurationGameConfig_GetSet`](../tests/Groups/Configuration/GameConfigTest.cs) from `GameConfigTest.cs` for a concrete example.) + +```csharp +// In tests/Groups/MyModule/MyLogicTests.cs +using NightTest.Core; +using Night; // For engine classes being tested, e.g., Night.Configuration.GameConfig +using Xunit; // For Assertions + +namespace NightTest.Groups.MyModule +{ + public class MyModuleMyLogic_SpecificFunctionTest : ModTestCase + { + public override string Name => "MyModule.MyLogic.SpecificFunction"; + public override string Description => "Tests the SpecificFunction of MyLogic class."; + public override string SuccessMessage => "MyLogic.SpecificFunction tested successfully."; + // Type defaults to TestType.Automated via BaseTestCase + + public override void Run() + { + // Arrange + var myObject = new Night.MyModule.MyClassToTest(); + var input = "someInput"; + var expectedOutput = "expectedResult"; + + // Act + var actualResult = myObject.SpecificFunction(input); + + // Assert + Assert.Equal(expectedOutput, actualResult); + // Add more assertions as needed + } + } +} +``` + +### Step 4: Implement Test Logic + +#### For `GameTestCase` and `ManualTestCase` ([`IGame`](../src/Night/IGame.cs) Methods) + +Override methods from `GameTestCase` or `ManualTestCase` as needed. + +- **`Load()`**: + - Called once when the test case starts. Use for one-time setup. + - **Always call `base.Load();`** if you override it. + - If setup fails, set `this.CurrentStatus = TestStatus.Failed;`, provide `this.Details`, and call `this.EndTest();`. +- **`Update(double deltaTime)`**: + - Called every frame. Main test logic and assertions reside here. + - Check `if (this.IsDone) return;` at the beginning. + - For automated `GameTestCase`: Perform actions, check conditions, set `this.CurrentStatus` and `this.Details`, then call `this.EndTest();`. + - For `ManualTestCase`: Call `this.RequestManualConfirmation("Your question");` when ready for user input. The base class handles `EndTest()`. +- **`Draw()`**: + - Called every frame, after `Update()`. Use for rendering required by the test, especially for `ManualTestCase`. + - For `ManualTestCase`, UI buttons and `Night.Graphics.Present()` are handled by the base class after your `Draw()` logic. +- **Input Handling (`KeyPressed`, `MousePressed`, etc.)**: + - Override if your test needs to react to input. + - If overriding in a `ManualTestCase`, ensure you call the `base` method (e.g., `base.KeyPressed(...)`) or replicate necessary base logic for UI interaction. + +#### For `ModTestCase` + +- **`Run()`**: + - This is the primary method where all test logic is implemented. + - Follow the Arrange-Act-Assert pattern. + - Use xUnit's `Assert` static class methods (e.g., `Assert.Equal()`, `Assert.True()`, `Assert.Throws()`) for verifications. + - The test passes if `Run()` completes without throwing an unhandled exception and all assertions pass. It fails if an assertion fails (which throws an exception) or any other unhandled exception occurs. +- **`SuccessMessage` (property)**: + - Define a string that will be used as the `Details` if the test's `Run()` method completes successfully. +- **`Name` and `Description` (properties)**: + - Provide clear and concise information about the test. + +### Step 5: Manage Test Lifecycle and Status (`GameTestCase`/`ManualTestCase`) + +This section primarily applies to `GameTestCase` and `ManualTestCase`. `ModTestCase` lifecycle is simpler and managed more directly by its `Run()` method and xUnit assertions. + +- **`this.CurrentStatus`**: Set to `TestStatus.Passed` or `TestStatus.Failed`. Defaults to `TestStatus.NotRun`. +- **`this.Details`**: Provide a clear string explaining the test outcome or failure reason. +- **`this.EndTest()`**: Call this method when the test logic is complete to stop the test, close the window (if applicable), and record results. +- **Helper methods in `GameTestCase`**: + - `CheckCompletionAfterDuration(...)`: End test after a certain time. + - `CheckCompletionAfterFrames(...)`: End test after a certain number of frames. +- **Error Handling**: Wrap potentially problematic code in `try-catch` blocks. On exception, set status and details, then call `EndTest()`. + + ```csharp + catch (System.Exception e) + { + this.RecordFailure($"An unexpected error occurred: {e.GetType().Name} - {e.Message}", e); + // EndTest() is called by RecordFailure if the test is not already done. + } + ``` + +- **Cleanup**: Clean up resources (e.g., temporary files) in a `finally` block or ensure cleanup before `EndTest()`. + +### Step 6: Create or Update the Test Group + +1. Open or create `tests/Groups/[ModuleName]/[ModuleName]Group.cs`. +2. Ensure it inherits from `NightTest.Core.TestGroup` and has a constructor accepting `ITestOutputHelper`. + + ```csharp + // In tests/Groups/MyModule/MyModuleGroup.cs + using Xunit; + using Xunit.Abstractions; + using NightTest.Core; + // Potentially: using NightTest.Groups.MyModule; // If test cases are in the same namespace + + namespace NightTest.Groups.MyModule + { + [Collection("SequentialTests")] // Important for tests that interact with the game window + public class MyModuleGroup : TestGroup + { + public MyModuleGroup(ITestOutputHelper outputHelper) + : base(outputHelper) + { + } + + // Add [Fact] methods for each test case here + } + } + ``` + +3. Add a new xUnit `[Fact]` method for each `GameTestCase`/`ManualTestCase`, or for each group of related `ModTestCase` instances: + + - **For a consolidated group of `GameTestCase` instances:** + + ```csharp + [Fact] + [Trait("TestType", "Automated")] // GameTestCases are automated + public void Run_MyModule_GameTests() // Consolidated method name for GameTestCases + { + this.Run_GameTestCase(new MyModuleMyFeature_ExpectedBehaviorGameTest()); + this.Run_GameTestCase(new MyModuleAnotherFeature_SomeConditionGameTest()); + // ... and so on for all GameTestCases related to MyModule + } + ``` + + - **For an individual `ManualTestCase` (or a consolidated group):** + `ManualTestCase` instances can still be run individually: + + ```csharp + [Fact] + [Trait("TestType", "Manual")] + public void Run_MyModuleMyVisualFeature_UserConfirmationTest() // Matches test case class name + { + this.Run_GameTestCase(new MyModuleMyVisualFeature_UserConfirmationTest()); // Note: Run_GameTestCase is used for ManualTestCase as well + } + ``` + + Alternatively, if multiple `ManualTestCase`s for a module are grouped, follow a similar consolidated structure as `GameTestCase` or `ModTestCase` groups, using `[Trait("TestType", "Manual")]`. + + - **For a group of `ModTestCase` instances:** + The `TestGroup` class needs a method to execute individual `ModTestCase` instances. This method (`Run_ModTestCase(ModTestCase testCase)`) is responsible for handling the execution, logging, and status recording of a single `ModTestCase`. + + ```csharp + // This method should exist in your NightTest.Core.TestGroup class + protected void Run_ModTestCase(ModTestCase testCase) + { + _outputHelper.WriteLine($"--- Starting Mod Test: {testCase.Name} ---"); + _outputHelper.WriteLine($"Description: {testCase.Description}"); + testCase.PrepareForRun(); // Sets up stopwatch etc. + try + { + testCase.Run(); // Execute the core test logic and assertions + testCase.RecordSuccess(testCase.SuccessMessage); // If Run() completes, it's a pass + _outputHelper.WriteLine($"Mod Test {testCase.Name}: PASSED. Details: {testCase.Details}"); + } + catch (System.Exception ex) + { + // Assertions in xUnit throw exceptions on failure. Other exceptions also indicate failure. + testCase.RecordFailure($"Test failed: {ex.Message}", ex); + _outputHelper.WriteLine($"Mod Test {testCase.Name}: FAILED. Details: {testCase.Details}\n{ex.StackTrace}"); + throw; // Re-throw to ensure xUnit marks the test as failed + } + finally + { + testCase.FinalizeRun(); // Stops stopwatch + _outputHelper.WriteLine($"--- Finished Mod Test: {testCase.Name} --- Duration: {testCase.TestStopwatch.ElapsedMilliseconds}ms"); + } + Assert.Equal(TestStatus.Passed, testCase.CurrentStatus); // Final assertion on the status + } + ``` + + Then, in your specific `[ModuleName]Group.cs`, create a single `[Fact]` method to run all related `ModTestCase`s: + + ```csharp + [Fact] + [Trait("TestType", "Automated")] // ModTests are inherently automated + public void Run_MyModuleMyLogic_ModTests() // Consolidated method name + { + this.Run_ModTestCase(new MyModuleMyLogic_SpecificFunctionTest()); + this.Run_ModTestCase(new MyModuleMyLogic_AnotherFunctionTest()); + this.Run_ModTestCase(new MyModuleMyLogic_EdgeCaseTest()); + // ... and so on for all ModTestCases related to MyModuleMyLogic + } + ``` + + - The `[Trait("TestType", ...)]` attribute helps in filtering tests. + - For `ModTestCase` groups, instantiate each test case class and pass it to `this.Run_ModTestCase()` within the consolidated `[Fact]` method. + +## 6. Key Base Classes and Interfaces + +- **[`Night.IGame`](../src/Night/IGame.cs)**: Interface from the core engine that `GameTestCase` (and thus `ManualTestCase`) implement to hook into the game loop (`Load`, `Update`, `Draw`, input methods). +- **[`NightTest.Core.ITestCase`](../tests/Core/ITestCase.cs)**: Defines properties for test identification (`Name`, `Type`, `Description`, `CurrentStatus`, `Details`, `TestStopwatch`). Implemented by `BaseTestCase`. +- **[`NightTest.Core.BaseTestCase`](../tests/Core/BaseTestCase.cs)**: Common abstract base class for all test cases. Provides shared properties from `ITestCase` and basic methods like `RecordFailure`, `RecordSuccess`. `Type` defaults to `TestType.Automated`. +- **[`NightTest.Core.GameTestCase`](../tests/Core/GameTestCase.cs)**: + - Inherits from `BaseTestCase` and implements `IGame`. + - Base class for automated tests that run within the game loop. + - Provides common game loop integration, frame counting, `EndTest()`, and helper methods like `CheckCompletionAfterDuration()`, `CheckCompletionAfterFrames()`. +- **[`NightTest.Core.ManualTestCase`](../tests/Core/ManualTestCase.cs)**: + - Inherits from `GameTestCase`. + - Overrides `Type` to `TestType.Manual`. + - Provides UI for manual pass/fail confirmation (buttons, ESC key), timeout logic, and the `RequestManualConfirmation()` method. +- **[`NightTest.Core.ModTestCase`](../tests/Core/ModTestCase.cs)**: + - Inherits from `BaseTestCase`. + - Base class for automated tests that run in isolation (unit/module tests). + - Requires implementation of an abstract `Run()` method for test logic and an abstract `SuccessMessage` property. + - Includes `PrepareForRun()` and `FinalizeRun()` methods called by the test group. +- **[`NightTest.Core.TestGroup`](../tests/Core/TestGroup.cs)**: + - Base class for xUnit test classes (the classes containing `[Fact]` methods). + - Takes `ITestOutputHelper` for logging. + - Provides `Run_GameTestCase(GameTestCase testCase)` to execute `IGame`-based tests. + - Provides `Run_ModTestCase(ModTestCase testCase)` to execute individual `ModTestCase` instances. `[Fact]` methods in the `TestGroup` will call this multiple times if consolidating several `ModTestCase`s. +- **[`NightTest.Core.TestType`](../tests/Core/TestTypes.cs)**: Enum (`Automated`, `Manual`). +- **[`NightTest.Core.TestStatus`](../tests/Core/TestTypes.cs)**: Enum (`NotRun`, `Passed`, `Failed`, `Skipped`). + +## 7. Best Practices + +- **Choose the Right Test Type:** Carefully consider if your test needs the game environment (`GameTestCase`/`ManualTestCase`) or can be done in isolation (`ModTestCase`). Prefer `ModTestCase` for unit-level logic testing for speed and simplicity. +- **Clear Naming and Descriptions:** Ensure `ITestCase.Name` and `ITestCase.Description` clearly state what the test is verifying. For `GameTestCase`/`ManualTestCase`, `Details` property should provide context on pass/fail. For `ModTestCase`, `SuccessMessage` is key. +- **Focused Tests:** Each test case should verify a single, specific piece of functionality or behavior. +- **Idempotency (for automated tests):** Automated tests (`GameTestCase`, `ModTestCase`) should produce the same result every time they are run, assuming no code changes. Avoid dependencies on external state not controlled by the test's setup (`Load()` or `Run()`). +- **Resource Management:** Clean up any resources created during a test (e.g., temporary files), typically in a `finally` block or before `EndTest()`. +- **Readability:** Write clean, understandable test code. Comments should explain *why* something is being done if it's not obvious. +- **Use `[Collection("SequentialTests")]`:** For `TestGroup` classes that contain `GameTestCase` or `ManualTestCase` instances, especially those involving UI or window manipulation, to ensure tests run one after another and don't interfere. This may not be strictly necessary for groups containing only `ModTestCase` instances if they are fully isolated. +- **Test Both Success and Failure Cases:** For a given feature, consider creating tests for expected successful outcomes and expected failure conditions (e.g., invalid input, file not found, exceptions). +- **Keep Manual Tests for Visuals/Interaction:** Reserve `ManualTestCase` for scenarios that genuinely require human observation (e.g., "is this color correct?", "does this animation look right?") or complex interactions not easily automated. +- **Assertions in `ModTestCase`:** Use xUnit's `Assert` methods directly within the `Run()` method of your `ModTestCase`. +- **`TestGroup` Responsibility:** The `TestGroup`'s `Run_GameTestCase` and `Run_ModTestCase` methods are responsible for the overall execution flow, logging, and final assertion on the test case's `CurrentStatus`. +- **ALWAYS** write XML summaries for public API or use inheritdoc where appropriate. +- Order tests correctly: + 1. Fields + 2. Constructors + 3. Finalizers + 4. Delegates + 5. Events + 6. Enums + 7. Interfaces + 8. Properties + 9. Indexers + 10. Methods + 1. Public + 2. Internal + 3. Protected + 4. Private + 11. Structs + 12. Classes (Nested) +- Static members should appear before instance members. +- **Running Tests**: Test must be run using this command while doing development (except for manual tests): `SDL_VIDEODRIVER=dummy dotnet test --filter TestType=Automated` instead of `dotnet test`. This is because headless testing is the preferred method. diff --git a/project/testing-plan.md b/project/testing-plan.md deleted file mode 100644 index f94e0efb..00000000 --- a/project/testing-plan.md +++ /dev/null @@ -1,116 +0,0 @@ -# Night.Engine Testing Plan - -## 1. Introduction and Philosophy - -This document outlines the testing strategy for the `Night.Engine` project, specifically focusing on unit testing its constituent modules. The primary goal is to ensure the reliability and correctness of individual components within the engine. - -The testing philosophy draws inspiration from Love2D's module-based testing approach, aiming to test each logical module of `Night.Engine` as independently as possible. We will use xUnit as the primary testing framework for C#. - -This plan adheres to the standards and guidelines set forth in the `operational-guidelines.md` and aligns with the project goals detailed in `PRD.md`. - -## 2. Testing Framework - -- **Framework:** xUnit.net -- **Assertion Library:** xUnit's built-in assertions. - -## 3. Test Project Structure - -A dedicated test project will be created for `Night.Engine` tests: - -- **Project Name:** `Night.Tests` -- **Location:** `tests/Night.Tests/` (This is a recommendation; final location to be decided based on solution structure). -- **Dependencies:** This project will reference the `Night` project. - -## 4. Naming Conventions - -Consistency in naming is crucial for maintainability and readability of tests. - -- **Test Classes:** - - Named after the class or module being tested, suffixed with `Tests`. - - Example: `GraphicsTests.cs` for testing `Night.Graphics`, `WindowTests.cs` for `Night.Window`. -- **Test Methods:** - - Follow the pattern: `[MethodUnderTest]_[ScenarioOrCondition]_[ExpectedBehavior]` - - `[MethodUnderTest]`: The name of the method being tested. - - `[ScenarioOrCondition]`: A brief description of the specific test case or input conditions. - - `[ExpectedBehavior]`: The expected outcome or state. - - Example: `SetMode_ValidResolution_ReturnsTrue`, `IsOpen_WhenWindowIsActive_ReturnsTrue`, `Draw_NullSprite_ThrowsArgumentNullException`. - -## 5. Scope of Testing - Night.Engine Modules - -The following modules and components within `Night.Engine` are the primary targets for unit testing. Given that `Night.Engine` (specifically `Night.Framework`) largely consists of static classes providing a Love2D-like API over SDL3, tests will focus on the C# logic, parameter validation, and correct invocation patterns where feasible. - -### 5.1. `Night.Framework` Modules - -These modules are typically static classes in the `Night` namespace. - -- **`Night.Window` (`Window.cs`)** - - Test methods like `SetMode`, `SetTitle`, `IsOpen`, `Close`. - - Focus on parameter validation (e.g., null title, invalid dimensions for `SetMode` if applicable before SDL call). - - Testing the actual window manipulation might be difficult in pure unit tests and leans towards integration testing (covered by `Night.SampleGame`). We can, however, test the C# logic paths before SDL calls. -- **`Night.Graphics` (`Graphics.cs`)** - - Test methods like `NewImage`, `Draw`, `Clear`, `Present`. - - Parameter validation (e.g., null paths for `NewImage`, null sprites for `Draw`). - - Testing actual rendering output is an integration concern. Unit tests should focus on the C# logic (e.g., does `Draw` handle transform parameters correctly before passing to SDL?). -- **`Night.Keyboard` (`Keyboard.cs`)** - - Test methods like `IsDown`. - - Focus on validating input parameters (e.g., specific `KeyCode` values). - - Directly testing key states requires OS-level interaction, which is beyond typical unit test scope. Tests might focus on internal logic if any exists separate from direct SDL calls. -- **`Night.Mouse` (`Mouse.cs`)** - - Test methods like `IsDown`, `GetPosition`. - - Similar to Keyboard, parameter validation for `MouseButton`. - - Testing actual mouse states/positions is an integration concern. -- **`Night.SDL` (`SDL.cs`)** - - This module directly wraps SDL3 P/Invoke calls. - - Unit testing P/Invoke wrappers is complex and often provides limited value compared to the effort. - - Focus should be on any C# helper methods within this class that do not directly call SDL or that perform significant logic before/after an SDL call. - - Most testing for `SDL.cs` functionality will be indirect, through the higher-level framework modules and `Night.SampleGame`. -- **`FrameworkLoop.cs`** - - Testing the main game loop (`Run` method) in isolation is challenging. - - Focus on unit testing helper methods or individual components within the loop's logic if they can be isolated (e.g., delta time calculation if it's a separate utility). - - The overall loop functionality is best tested via `Night.SampleGame`. -- **`Types.cs`** - - Contains data structures (e.g., `Color`, `Rectangle`, `Sprite`) and interfaces (`IGame`). - - Test constructors and any methods on these types if they contain logic (e.g., `Color.ToSDLColor`, methods on `Rectangle`). - - Interfaces themselves are not tested directly but are implemented by mocks or test classes. - -### 5.2. `Night.Engine` (Future High-Level Systems) - -As `Night.Engine` evolves with higher-level systems (ECS, Scene Management, etc.), dedicated test classes will be created for each new component, following the same principles. - -## 6. Test Case Design - -- **Positive Tests:** Verify that methods work correctly with valid inputs. -- **Negative Tests (Error Handling):** - - Verify behavior with invalid inputs (e.g., null arguments, out-of-range values). - - Ensure appropriate exceptions are thrown as per API contracts (e.g., `ArgumentNullException`, `ArgumentOutOfRangeException`). -- **Edge Cases:** Test boundary conditions and less common scenarios. -- **Idempotency:** For methods that should be idempotent, verify this behavior. - -## 7. Dealing with SDL Dependencies - -Directly testing code that relies heavily on SDL3 native calls can be difficult in unit tests, as it often requires an initialized SDL environment and may involve external system state (like window handles or graphics contexts). - -- **Focus on C# Logic:** Prioritize testing the C# logic that wraps or precedes SDL calls. This includes parameter validation, state management within the C# layer, and correct translation of parameters for SDL functions. -- **Abstraction (If Necessary):** For more complex C# logic interacting with SDL, consider if introducing a thin abstraction over direct SDL calls (internal to the module) could facilitate testing. This should be weighed against added complexity. -- **Integration Tests as a Complement:** Acknowledge that `Night.SampleGame` serves as the primary integration test suite where the full interaction with SDL is validated. Unit tests are meant to catch issues at a more granular C# level. -- **Mocking/Faking SDL (Use with Caution):** - - Creating mocks or fakes for SDL functions is possible but can be very time-consuming and complex to maintain. - - This approach should generally be avoided unless a critical piece of C# logic cannot be tested otherwise and its correctness is paramount. - - If used, these fakes would need to simulate SDL behavior, which can be error-prone. - -## 8. Running Tests - -- Tests can be run using the `dotnet test` command from the command line in the solution root or the test project directory. -- Test runners integrated into IDEs (like Visual Studio, VS Code, Rider) can also be used. - -## 9. Test Maintenance - -- Tests should be kept up-to-date with code changes. -- Refactor tests along with production code to maintain clarity and relevance. -- Remove or update tests for deprecated or changed functionality. - -## 10. Continuous Integration (Future) - -Once a CI/CD pipeline is established, automated execution of these unit tests will be a key component to ensure code quality with every change. - -This testing plan provides a foundation for building a robust suite of unit tests for `Night.Engine`. It will evolve as the engine grows and new features are added. diff --git a/scripts/env.py b/scripts/env.py new file mode 100644 index 00000000..97b0c2b9 --- /dev/null +++ b/scripts/env.py @@ -0,0 +1,58 @@ +import platform +import subprocess +import sys + +# The required version of the .NET SDK. +REQUIRED_DOTNET_VERSION = "9.0" + + +def get_dotnet_version(): + """Gets the version of the .NET SDK.""" + try: + # The 'dotnet --version' command outputs the version of the SDK. + result = subprocess.run( + ["dotnet", "--version"], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + except FileNotFoundError: + return None + except subprocess.CalledProcessError as e: + print(f"Error checking dotnet version: {e}", file=sys.stderr) + return None + + +def main(): + """Verifies the dotnet environment.""" + installed_version = get_dotnet_version() + + if installed_version is None: + print("Error: dotnet CLI not found.", file=sys.stderr) + print( + "Please install the .NET SDK and ensure 'dotnet' is in your PATH.", + file=sys.stderr, + ) + sys.exit(1) + + # We are only interested in the major and minor version numbers. + # A version string might be '9.0.100-preview.5.24307.3'. + # We want to check if it starts with '9.0'. + if not installed_version.startswith(REQUIRED_DOTNET_VERSION): + print( + f"Error: Invalid dotnet version.", + file=sys.stderr, + ) + print( + f"Expected: {REQUIRED_DOTNET_VERSION}.*, Found: {installed_version}", + file=sys.stderr, + ) + sys.exit(1) + + print(f"Dotnet version check passed ({installed_version}).") + sys.exit(0) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/update_api_doc.py b/scripts/update_api_doc.py index 7798f86e..679f98c1 100644 --- a/scripts/update_api_doc.py +++ b/scripts/update_api_doc.py @@ -200,7 +200,7 @@ def generate_markdown(all_module_data, output_file): def main(): framework_dir = os.path.join("src", "Night") - output_md_file = os.path.join("docs", "API.md") + output_md_file = os.path.join("project", "API.md") all_module_data = defaultdict(lambda: {"functions": {}, "enums": [], "types": []}) diff --git a/src/Night/CLI.cs b/src/Night/CLI.cs new file mode 100644 index 00000000..5ecd4725 --- /dev/null +++ b/src/Night/CLI.cs @@ -0,0 +1,209 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Generic; +using System.Globalization; // Added for DateTime parsing if needed for session log, though path generation is in Program.cs +using System.IO; +using System.Linq; + +using Night; + +namespace Night +{ + /// + /// Handles command-line argument parsing for the Night Engine. + /// + public class CLI + { + private readonly List remainingArgs = new(); + private bool isSilentMode = false; + private LogLevel? parsedLogLevel = null; + private bool isDebugMode = false; + private bool enableSessionLog = false; + + /// + /// Initializes a new instance of the class. + /// Parses the provided command-line arguments. + /// + /// The command-line arguments passed to the application. + public CLI(string[] args) + { + if (args == null) + { + return; + } + + for (int i = 0; i < args.Length; i++) + { + string arg = args[i]; + + if (string.Equals(arg, "-s", StringComparison.OrdinalIgnoreCase) || + string.Equals(arg, "--silent", StringComparison.OrdinalIgnoreCase)) + { + this.isSilentMode = true; + } + else if (string.Equals(arg, "--log-level", StringComparison.OrdinalIgnoreCase)) + { + if (i + 1 < args.Length) + { + i++; // Consume the level value + string levelString = args[i]; + if (Enum.TryParse(levelString, true, out LogLevel parsedLevel)) + { + this.parsedLogLevel = parsedLevel; + } + else + { + // Invalid log level, add --log-level and its value back to remaining to be handled as an error or ignored by Program.cs + this.remainingArgs.Add(arg); + this.remainingArgs.Add(levelString); + } + } + else + { + // Missing log level value, add --log-level back + this.remainingArgs.Add(arg); + } + } + else if (string.Equals(arg, "--debug", StringComparison.OrdinalIgnoreCase)) + { + this.isDebugMode = true; + } + else if (string.Equals(arg, "--session-log", StringComparison.OrdinalIgnoreCase)) + { + this.enableSessionLog = true; + } + else + { + this.remainingArgs.Add(arg); + } + } + } + + /// + /// Gets a value indicating whether silent mode was requested via command-line arguments. + /// Silent mode typically suppresses startup console messages. + /// + public bool IsSilentMode => this.isSilentMode; + + /// + /// Gets the log level parsed from the command-line arguments, if provided and valid. + /// + public LogLevel? ParsedLogLevel => this.parsedLogLevel; + + /// + /// Gets a value indicating whether debug mode was requested via command-line arguments. + /// + public bool IsDebugMode => this.isDebugMode; + + /// + /// Gets a value indicating whether session logging was requested via command-line arguments. + /// + public bool EnableSessionLog => this.enableSessionLog; + + /// + /// Gets the list of arguments that were not processed as specific CLI flags by this parser. + /// + public IReadOnlyList RemainingArgs => this.remainingArgs.AsReadOnly(); + + /// + /// Applies logging and other settings based on the parsed command-line arguments. + /// + public void ApplySettings() + { + // Apply settings based on parsed CLI arguments + if (this.ParsedLogLevel.HasValue) + { + LogManager.MinLevel = this.ParsedLogLevel.Value; + if (!this.IsSilentMode) + { + Console.WriteLine($"[Night.Engine.CLI] Log level set to: {this.ParsedLogLevel.Value}"); + } + } + + if (this.IsDebugMode) + { + LogManager.MinLevel = LogLevel.Debug; // Ensure debug level if --debug is set + LogManager.EnableSystemConsoleSink(true); + if (!this.IsSilentMode) + { + Console.WriteLine("[Night.Engine.CLI] Debug mode enabled: Log level set to Debug, console sink enabled."); + } + } + + if (this.EnableSessionLog) + { + try + { + string baseDirectory = AppContext.BaseDirectory ?? "."; + string sessionDirPath = Path.Combine(baseDirectory, "session"); + _ = Directory.CreateDirectory(sessionDirPath); + + string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture); + string logFileName = $"session_log_{timestamp}.log"; + string logFilePath = Path.Combine(sessionDirPath, logFileName); + + LogManager.ConfigureFileSink(logFilePath, LogLevel.Trace); // FileSink itself will capture all from Trace, LogManager.MinLevel filters what's sent + if (!this.IsSilentMode) + { + Console.WriteLine($"[Night.Engine.CLI] Session log enabled. Logging to: {logFilePath}"); + } + } + catch (Exception ex) + { + if (!this.IsSilentMode) + { + Console.WriteLine($"[Night.Engine.CLI] Error enabling session log: {ex.Message}"); + } + } + } + + // Handle any remaining arguments that were not processed by this parser + if (this.RemainingArgs.Any()) + { + if (!this.IsSilentMode) + { + Console.WriteLine($"[Night.Engine.CLI] Warning: Unprocessed or invalid arguments found: {string.Join(" ", this.RemainingArgs)}"); + if (this.RemainingArgs.Contains("--log-level", StringComparer.OrdinalIgnoreCase)) + { + bool valueMissing = true; + int logLevelIndex = this.RemainingArgs.ToList().FindIndex(x => x.Equals("--log-level", StringComparison.OrdinalIgnoreCase)); + if (logLevelIndex != -1 && logLevelIndex + 1 < this.RemainingArgs.Count) + { + if (!this.RemainingArgs[logLevelIndex + 1].StartsWith("--")) + { + Console.WriteLine($"[Night.Engine.CLI] Warning: The value '{this.RemainingArgs[logLevelIndex + 1]}' provided for --log-level is invalid. Using current default."); + valueMissing = false; + } + } + + if (valueMissing) + { + Console.WriteLine("[Night.Engine.CLI] Warning: --log-level option requires a valid level argument (Trace, Debug, Information, Warning, Error, Fatal)."); + } + } + } + } + } + } +} diff --git a/src/Night/Configuration/ConfigurationManager.cs b/src/Night/Configuration/ConfigurationManager.cs index 9c9d7280..16cef1a1 100644 --- a/src/Night/Configuration/ConfigurationManager.cs +++ b/src/Night/Configuration/ConfigurationManager.cs @@ -25,6 +25,7 @@ using System.Text.Json; using Night; +using Night.Log; namespace Night { @@ -33,6 +34,7 @@ namespace Night /// public static class ConfigurationManager { + private static readonly ILogger Logger = LogManager.GetLogger("Night.Configuration.ConfigurationManager"); private static readonly string ConfigFileName = "config.json"; private static GameConfig currentConfig = new GameConfig(); private static bool isLoaded = false; @@ -83,28 +85,28 @@ public static void LoadConfig(string? gameDirectory = null) } else { - Console.WriteLine($"Warning: Could not parse '{ConfigFileName}' from '{configFilePath}'. Using default configuration."); + Logger.Warn($"Could not parse '{ConfigFileName}' from '{configFilePath}'. Using default configuration."); } } else { - Console.WriteLine($"Warning: '{ConfigFileName}' found at '{configFilePath}' is empty. Using default configuration."); + Logger.Warn($"'{ConfigFileName}' found at '{configFilePath}' is empty. Using default configuration."); } } catch (JsonException jsonEx) { - Console.WriteLine($"Error deserializing '{ConfigFileName}' from '{configFilePath}': {jsonEx.Message}. Using default configuration."); + Logger.Error($"Error deserializing '{ConfigFileName}' from '{configFilePath}'. Using default configuration.", jsonEx); } // Catch-all for other potential issues catch (Exception ex) { - Console.WriteLine($"Night.ConfigurationManager: Error loading or deserializing config.json: {ex.Message}. Using default configuration."); + Logger.Error($"Error loading or deserializing config.json. Using default configuration.", ex); } } else { - Console.WriteLine($"Info: '{ConfigFileName}' not found at '{configFilePath}'. Using default configuration."); + Logger.Info($"'{ConfigFileName}' not found at '{configFilePath}'. Using default configuration."); } isLoaded = true; diff --git a/src/Night/Configuration/GameConfig.cs b/src/Night/Configuration/GameConfig.cs index 78a00c35..3bbe0351 100644 --- a/src/Night/Configuration/GameConfig.cs +++ b/src/Night/Configuration/GameConfig.cs @@ -42,10 +42,10 @@ public class GameConfig public bool AppendIdentity { get; set; } = false; /// - /// Gets or sets the LÖVE version this game targets. Currently informational. + /// Gets or sets the Night Engine version this game targets. /// [JsonPropertyName("version")] - public string Version { get; set; } = "11.4"; // Default to LÖVE 11.4 + public string Version { get; set; } = VersionInfo.GetVersion(); /// /// Gets or sets a value indicating whether a console window should be attached (Windows only, currently placeholder). @@ -60,7 +60,7 @@ public class GameConfig public bool AccelerometerJoystick { get; set; } = true; /// - /// Gets or sets a value indicating whether to request external storage access (Android only, currently placeholder). + /// Gets or sets a value indicating whether to request external storage access (Android only). /// [JsonPropertyName("externalstorage")] public bool ExternalStorage { get; set; } = false; @@ -89,8 +89,4 @@ public class GameConfig [JsonPropertyName("modules")] public ModulesConfig Modules { get; set; } = new ModulesConfig(); } - - // NOTE: The definitions for AudioConfig, WindowConfig, and ModulesConfig - // have been moved to their own files: AudioConfig.cs, WindowConfig.cs, and ModulesConfig.cs - // This is to resolve SA1402: File may only contain a single type. } diff --git a/src/Night/Configuration/ModulesConfig.cs b/src/Night/Configuration/ModulesConfig.cs index 021e8fa4..abb5ac93 100644 --- a/src/Night/Configuration/ModulesConfig.cs +++ b/src/Night/Configuration/ModulesConfig.cs @@ -25,7 +25,7 @@ namespace Night { /// - /// Configuration for enabling/disabling engine modules (similar to t.modules in LÖVE's conf.lua). + /// Configuration for enabling/disabling engine modules. /// public class ModulesConfig { @@ -95,7 +95,7 @@ public class ModulesConfig /// Gets or sets a value indicating whether the Window module is enabled. [JsonPropertyName("window")] - public bool WindowModule { get; set; } = true; // Renamed to avoid conflict with Night.Window namespace + public bool WindowModule { get; set; } = true; /// Gets or sets a value indicating whether the Thread module is enabled. [JsonPropertyName("thread")] diff --git a/src/Night/Configuration/WindowConfig.cs b/src/Night/Configuration/WindowConfig.cs index 0dedcee6..f547ddd9 100644 --- a/src/Night/Configuration/WindowConfig.cs +++ b/src/Night/Configuration/WindowConfig.cs @@ -33,7 +33,7 @@ public class WindowConfig /// Gets or sets the window title. /// [JsonPropertyName("title")] - public string? Title { get; set; } = "Night Game"; // Default title + public string? Title { get; set; } = "Night Game"; /// /// Gets or sets the path to the window icon file. Relative to the game's root directory. @@ -69,13 +69,13 @@ public class WindowConfig /// Gets or sets the minimum window width. /// [JsonPropertyName("minwidth")] - public int MinWidth { get; set; } = 1; // LÖVE default + public int MinWidth { get; set; } = 1; /// /// Gets or sets the minimum window height. /// [JsonPropertyName("minheight")] - public int MinHeight { get; set; } = 1; // LÖVE default + public int MinHeight { get; set; } = 1; /// /// Gets or sets a value indicating whether the window is resizable. @@ -99,19 +99,19 @@ public class WindowConfig /// Gets or sets the type of fullscreen mode. Expected values: "desktop" or "exclusive". /// [JsonPropertyName("fullscreentype")] - public string FullscreenType { get; set; } = "desktop"; // LÖVE default + public string FullscreenType { get; set; } = "desktop"; /// /// Gets or sets a value indicating whether VSync is enabled. /// [JsonPropertyName("vsync")] - public bool VSync { get; set; } = true; // LÖVE default + public bool VSync { get; set; } = false; // TODO: Fix needed as true currently breaks the refresh /// /// Gets or sets a value indicating whether to enable high-DPI mode if available. /// [JsonPropertyName("highdpi")] - public bool HighDPI { get; set; } = false; // LÖVE default + public bool HighDPI { get; set; } = false; /// /// Gets or sets the multisample anti-aliasing (MSAA) level. @@ -135,7 +135,7 @@ public class WindowConfig /// Gets or sets the 1-indexed display number to use for the window. /// [JsonPropertyName("display")] - public int Display { get; set; } = 1; // 1-indexed + public int Display { get; set; } = 1; /// /// Gets or sets a value indicating whether to use DPI scaling. diff --git a/src/Night/Engine/.gitkeep b/src/Night/Engine/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/Night/Filesystem/BufferMode.cs b/src/Night/Filesystem/BufferMode.cs new file mode 100644 index 00000000..e1df8897 --- /dev/null +++ b/src/Night/Filesystem/BufferMode.cs @@ -0,0 +1,45 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +namespace Night +{ + /// + /// Specifies how a file's buffer is flushed. + /// + public enum BufferMode + { + /// + /// No buffering. Data is written as soon as possible. + /// + None, + + /// + /// Line buffering. Data is written when a newline character is output, or when the buffer is full. + /// + Line, + + /// + /// Full buffering. Data is written only when the buffer is full. + /// + Full, + } +} diff --git a/src/Night/Filesystem/DroppedFile.cs b/src/Night/Filesystem/DroppedFile.cs new file mode 100644 index 00000000..ba68b4fb --- /dev/null +++ b/src/Night/Filesystem/DroppedFile.cs @@ -0,0 +1,47 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +namespace Night; + +/// +/// Represents a file that has been dropped onto the application window. +/// +/// +/// This object is created by the framework when a file drop event occurs. It provides the absolute path +/// to the dropped file, which can then be used with other functions. +/// +public class DroppedFile +{ + /// + /// Initializes a new instance of the class. + /// + /// The absolute path of the dropped file. + internal DroppedFile(string path) + { + this.Path = path; + } + + /// + /// Gets the absolute path of the dropped file. + /// + public string Path { get; } +} diff --git a/src/Night/Filesystem/FileData.cs b/src/Night/Filesystem/FileData.cs new file mode 100644 index 00000000..5af96257 --- /dev/null +++ b/src/Night/Filesystem/FileData.cs @@ -0,0 +1,98 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; +using System.Text; + +namespace Night; + +/// +/// Data representing the contents of a file. It can be created from a string or byte array +/// and is used to pass around file contents in memory. This is analogous to Love2D's +/// love.filesystem.FileData. +/// +public class FileData +{ + private readonly byte[] data; + private readonly string filenameHint; + + /// + /// Initializes a new instance of the class from a byte array. + /// + /// The byte array containing the file data. + /// A hint for the original filename, used for context (e.g., determining file extension). + /// Thrown if data is null. + public FileData(byte[] data, string filenameHint = "data") + { + this.data = data ?? throw new ArgumentNullException(nameof(data)); + this.filenameHint = filenameHint; + } + + /// + /// Initializes a new instance of the class from a string. + /// The string will be encoded using UTF-8. + /// + /// The string content of the file. + /// A hint for the original filename, used for context (e.g., determining file extension). + /// Thrown if content is null. + public FileData(string content, string filenameHint = "data.txt") + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + this.data = Encoding.UTF8.GetBytes(content); + this.filenameHint = filenameHint; + } + + /// + /// Gets the contents of the FileData as a byte array. + /// + /// A new byte array containing the file's data. + public byte[] GetBytes() => (byte[])this.data.Clone(); + + /// + /// Gets the contents of the FileData as a string, decoded using UTF-8. + /// + /// The file's data as a string. + public string GetString() => Encoding.UTF8.GetString(this.data); + + /// + /// Gets the size of the FileData in bytes. + /// + /// The size of the data in bytes. + public long GetSize() => this.data.Length; + + /// + /// Gets the filename hint associated with this FileData. + /// + /// The filename hint. + public string GetFilenameHint() => this.filenameHint; + + /// + /// Gets the file extension from the filename hint. + /// + /// The file extension (including the period), or an empty string if there is no extension. + public string GetExtension() => Path.GetExtension(this.filenameHint); +} diff --git a/src/Night/Filesystem/FileMode.cs b/src/Night/Filesystem/FileMode.cs index 0fbef957..78b057bb 100644 --- a/src/Night/Filesystem/FileMode.cs +++ b/src/Night/Filesystem/FileMode.cs @@ -43,30 +43,5 @@ public enum FileMode /// Open a file for append. /// Append, - - /// - /// Do not open a file (represents a closed file.) - /// - Close, - - /// - /// Open a file for write. - /// - W = Write, - - /// - /// Open a file for read. - /// - R = Read, - - /// - /// Open a file for append. - /// - A = Append, - - /// - /// Do not open a file (represents a closed file.) - /// - C = Close, } } diff --git a/src/Night/Filesystem/Filesystem.Append.cs b/src/Night/Filesystem/Filesystem.Append.cs new file mode 100644 index 00000000..37ab2236 --- /dev/null +++ b/src/Night/Filesystem/Filesystem.Append.cs @@ -0,0 +1,148 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; +using System.Security; +using System.Text; + +using IOFileMode = System.IO.FileMode; + +namespace Night +{ + /// + /// Provides an interface to the user's filesystem. + /// + public static partial class Filesystem + { + /// + /// Appends string data to a file in the save directory. If the file does not exist, it will be created. + /// + /// The relative path of the file within the save directory. + /// The string data to append. The string will be UTF-8 encoded. + /// The number of bytes of the encoded string to append. If null, the entire string is appended. + /// A tuple indicating success and providing an error message on failure. + public static (bool Success, string? ErrorMessage) Append(string filepath, string data, long? size = null) + { + if (data == null) + { + return (false, "Data to append cannot be null."); + } + + byte[] encodedData = Encoding.UTF8.GetBytes(data); + return Append(filepath, encodedData, size); + } + + /// + /// Appends byte data to a file in the save directory. If the file does not exist, it will be created. + /// + /// The relative path of the file within the save directory. + /// The byte array data to append. + /// The number of bytes to append. If null, the entire array is appended. + /// A tuple indicating success and providing an error message on failure. + public static (bool Success, string? ErrorMessage) Append(string filepath, byte[] data, long? size = null) + { + if (string.IsNullOrEmpty(filepath)) + { + return (false, "Filepath cannot be null or empty."); + } + + if (data == null) + { + return (false, "Data to append cannot be null."); + } + + try + { + string fullPath = GetFullPathInSaveDirectory(filepath); + + // Ensure parent directory exists + string? directoryPath = Path.GetDirectoryName(fullPath); + if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath)) + { + _ = Directory.CreateDirectory(directoryPath); + } + + long bytesToWrite = data.Length; + if (size.HasValue) + { + if (size.Value < 0) + { + return (true, null); // LÖVE does nothing for negative size, so we succeed with no action. + } + + bytesToWrite = Math.Min(size.Value, data.Length); + } + + if (bytesToWrite == 0) + { + // Ensure file exists even if writing 0 bytes, consistent with append mode creating a file. + if (!File.Exists(fullPath)) + { + File.Create(fullPath).Dispose(); + } + + return (true, null); + } + + using (var stream = new FileStream(fullPath, IOFileMode.Append, FileAccess.Write, FileShare.None)) + { + int bytesToWriteInChunk = (int)Math.Min(bytesToWrite, int.MaxValue); + if (bytesToWrite > int.MaxValue) + { + Logger.Warn($"Requested append size ({bytesToWrite} bytes) for '{filepath}' exceeds int.MaxValue. Appending in chunks."); + long totalBytesWritten = 0; + while (totalBytesWritten < bytesToWrite) + { + int currentChunkSize = (int)Math.Min(bytesToWrite - totalBytesWritten, int.MaxValue); + stream.Write(data, (int)totalBytesWritten, currentChunkSize); + totalBytesWritten += currentChunkSize; + } + } + else + { + stream.Write(data, 0, bytesToWriteInChunk); + } + } + + return (true, null); + } + catch (Exception ex) when ( + ex is ArgumentException || + ex is PathTooLongException || + ex is DirectoryNotFoundException || + ex is IOException || + ex is UnauthorizedAccessException || + ex is SecurityException || + ex is NotSupportedException) + { + Logger.Error($"Failed to append to file '{filepath}'. Reason: {ex.Message}", ex); + return (false, ex.Message); + } + catch (Exception ex) + { + Logger.Error($"An unexpected error occurred while appending to '{filepath}'.", ex); + return (false, $"An unexpected error occurred: {ex.Message}"); + } + } + } +} diff --git a/src/Night/Filesystem/Filesystem.Directory.cs b/src/Night/Filesystem/Filesystem.Directory.cs new file mode 100644 index 00000000..a333dc59 --- /dev/null +++ b/src/Night/Filesystem/Filesystem.Directory.cs @@ -0,0 +1,87 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Night +{ + /// + /// Provides an interface to the user's filesystem. + /// + public static partial class Filesystem + { + /// + /// Gets a list of all files and subdirectories in a given path. + /// + /// + /// The path is resolved by checking the save directory first, then the source directory. + /// If the given relative path exists in both the save and source directories, their contents are merged. + /// If a file or directory with the same name exists in both, the one from the save directory takes precedence. + /// + /// The relative path of the directory to list items from. + /// An enumerable collection of file and directory names relative to the given path. + public static IEnumerable GetDirectoryItems(string path) + { + var items = new HashSet(); + + // 1. Check Save Directory + string savePath = Path.Combine(GetSaveDirectory(), path); + if (Directory.Exists(savePath)) + { + try + { + foreach (var entry in Directory.EnumerateFileSystemEntries(savePath)) + { + _ = items.Add(Path.GetFileName(entry)); + } + } + catch (Exception e) + { + Logger.Error($"Failed to enumerate items in save directory path '{savePath}': {e.Message}", e); + } + } + + // 2. Check Source Directory + string sourcePath = Path.Combine(GetSource(), path); + if (Directory.Exists(sourcePath)) + { + try + { + foreach (var entry in Directory.EnumerateFileSystemEntries(sourcePath)) + { + // Add only if not already present from the save directory + _ = items.Add(Path.GetFileName(entry)); + } + } + catch (Exception e) + { + Logger.Error($"Failed to enumerate items in source directory path '{sourcePath}': {e.Message}", e); + } + } + + return items.OrderBy(s => s, StringComparer.Ordinal); + } + } +} diff --git a/src/Night/Filesystem/Filesystem.NewFileData.cs b/src/Night/Filesystem/Filesystem.NewFileData.cs new file mode 100644 index 00000000..240c5c05 --- /dev/null +++ b/src/Night/Filesystem/Filesystem.NewFileData.cs @@ -0,0 +1,51 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +namespace Night; + +/// +/// Provides an interface to the user's filesystem, mirroring Love2D's `love.filesystem` module. +/// +public static partial class Filesystem +{ + /// + /// Creates a new object from a byte array. + /// + /// The byte array containing the file data. + /// The name to use as the filename hint. + /// A new object. + public static FileData NewFileData(byte[] data, string name) + { + return new FileData(data, name); + } + + /// + /// Creates a new object from a string. + /// + /// The string content. + /// The name to use as the filename hint. + /// A new object. + public static FileData NewFileData(string content, string name) + { + return new FileData(content, name); + } +} diff --git a/src/Night/Filesystem/Filesystem.Read.cs b/src/Night/Filesystem/Filesystem.Read.cs new file mode 100644 index 00000000..61154c6b --- /dev/null +++ b/src/Night/Filesystem/Filesystem.Read.cs @@ -0,0 +1,210 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Security; +using System.Text; + +using Night.Log; + +namespace Night +{ + /// + /// Provides an interface to the user's filesystem. + /// + public static partial class Filesystem + { + /// + /// Returns an iterator function that iterates over all the lines in a file. + /// + /// The name (and path) of the file. + /// An enumerable collection of strings, where each string is a line in the file. + /// Thrown if filePath is null. + /// Thrown if filePath is empty. + /// Thrown if the file specified in filePath was not found. + /// Thrown if an I/O error occurs. + /// Thrown if the caller does not have the required permission, or path specified a directory, or the caller does not have read access. + public static IEnumerable Lines(string filePath) + { + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + if (string.IsNullOrEmpty(filePath)) + { + throw new ArgumentException("File path cannot be empty.", nameof(filePath)); + } + + return File.ReadLines(filePath); + } + + /// + /// Reads the contents of a file into a string. + /// + /// The name (and path) of the file. + /// How many bytes to read. If null, reads the entire file. + /// If the requested size exceeds practical limits (e.g., max string size), + /// reading may be capped, and the returned bytesRead will reflect the actual amount. + /// + /// A tuple containing: + /// - contents: The file contents as a string. Null if an error occurs. + /// - bytesRead: How many bytes were read. Null if an error occurs before reading attempt or on critical failure. + /// - errorMsg: An error message if reading fails, otherwise null. + /// + /// + /// This method mimics LÖVE's love.filesystem.read(name, size), defaulting to string content. + /// Content is UTF-8 decoded. + /// + public static (string? Contents, long? BytesRead, string? ErrorMsg) Read(string name, long? sizeToRead = null) + { + var result = Read(ContainerType.String, name, sizeToRead); + if (result.ErrorMsg != null) + { + // Ensure contents is null if there's an error message, bytesRead might be 0 or null depending on when error occurred. + return (null, result.BytesRead, result.ErrorMsg); + } + + return ((string?)result.Contents, result.BytesRead, result.ErrorMsg); + } + + /// + /// Reads the contents of a file. + /// + /// What type to return the file's contents as (string or raw data). + /// The name (and path) of the file. + /// How many bytes to read. If null, reads the entire file. + /// If the requested size exceeds practical limits (e.g., max array/string size), + /// reading may be capped, and the returned bytesRead will reflect the actual amount. + /// + /// A tuple containing: + /// - contents: The file contents as an object (string or byte[]). Null if an error occurs. + /// - bytesRead: How many bytes were read. Null if an error occurs before reading attempt or on critical failure. + /// - errorMsg: An error message if reading fails, otherwise null. + /// + /// + /// This method mimics LÖVE's love.filesystem.read(container, name, size). + /// When is , contents will be byte[]. + /// When is , contents will be a string (UTF-8 decoded). + /// Reading is capped at int.MaxValue bytes due to .NET array/string limitations. + /// + public static (object? Contents, long? BytesRead, string? ErrorMsg) Read(ContainerType container, string name, long? sizeToRead = null) + { + if (string.IsNullOrEmpty(name)) + { + return (null, null, "File name cannot be null or empty."); + } + + if (sizeToRead.HasValue && sizeToRead.Value < 0) + { + // LÖVE's behavior for negative size is not explicitly defined for read, + // but typically means read all or error. Let's treat as an error or invalid argument. + // For consistency with Append, we could return 0 bytes read, but an error seems more appropriate for read. + return (null, 0, "Size to read cannot be negative."); + } + + try + { + if (!File.Exists(name)) + { + return (null, null, "File not found."); + } + + using (var stream = new FileStream(name, global::System.IO.FileMode.Open, FileAccess.Read, FileShare.Read)) + { + long fileLength = stream.Length; + long actualBytesToRead; + + if (sizeToRead.HasValue) + { + actualBytesToRead = Math.Min(sizeToRead.Value, fileLength); + } + else + { + actualBytesToRead = fileLength; + } + + // Cap reading at int.MaxValue due to .NET array/string limitations + if (actualBytesToRead > int.MaxValue) + { + Logger.Warn($"Requested read size ({actualBytesToRead} bytes) for '{name}' exceeds int.MaxValue. Capping read at {int.MaxValue} bytes."); + actualBytesToRead = int.MaxValue; + } + + if (actualBytesToRead == 0) + { + return (container == ContainerType.String ? string.Empty : Array.Empty(), 0, null); + } + + byte[] buffer = new byte[(int)actualBytesToRead]; + int bytesActuallyReadFromStream = stream.Read(buffer, 0, (int)actualBytesToRead); + + if (bytesActuallyReadFromStream < actualBytesToRead) + { + // This might happen if the file is modified concurrently, or other rare FS issues. + // Adjust buffer if fewer bytes were read than expected. + Array.Resize(ref buffer, bytesActuallyReadFromStream); + Logger.Warn($"Read fewer bytes ({bytesActuallyReadFromStream}) than expected ({actualBytesToRead}) for file '{name}'."); + } + + object resultContents; + if (container == ContainerType.String) + { + resultContents = global::System.Text.Encoding.UTF8.GetString(buffer); + } + else + { + resultContents = buffer; + } + + return (resultContents, bytesActuallyReadFromStream, null); + } + } + catch (FileNotFoundException) + { + return (null, null, "File not found."); + } + catch (UnauthorizedAccessException ex) + { + Logger.Error($"Unauthorized access trying to read file '{name}'.", ex); + return (null, null, "Unauthorized access."); + } + catch (SecurityException ex) + { + Logger.Error($"Security error trying to read file '{name}'.", ex); + return (null, null, "Security error."); + } + catch (IOException ex) + { + Logger.Error($"IO error trying to read file '{name}'.", ex); + return (null, null, $"IO error: {ex.Message}"); + } + catch (Exception ex) + { + Logger.Error($"Unexpected error trying to read file '{name}'.", ex); + return (null, null, $"An unexpected error occurred: {ex.Message}"); + } + } + } +} diff --git a/src/Night/Filesystem/Filesystem.Remove.cs b/src/Night/Filesystem/Filesystem.Remove.cs new file mode 100644 index 00000000..fa20ad9b --- /dev/null +++ b/src/Night/Filesystem/Filesystem.Remove.cs @@ -0,0 +1,106 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; +using System.Security; + +namespace Night +{ + /// + /// Provides an interface to the user's filesystem. + /// + public static partial class Filesystem + { + /// + /// Removes a file or an empty directory from the save directory. + /// + /// The path of the file or directory to remove, relative to the save directory. + /// true if the file or directory was successfully removed, false otherwise. + /// + /// This operation is restricted to the game's save directory. Attempting to remove files + /// or directories outside of this location will fail. This method will also fail if + /// attempting to remove a directory that is not empty. + /// + public static bool Remove(string filepath) + { + if (string.IsNullOrWhiteSpace(filepath)) + { + Logger.Warn("Remove failed: filepath cannot be null or empty."); + return false; + } + + try + { + string saveDir = GetSaveDirectory(); + string fullPath = Path.GetFullPath(Path.Combine(saveDir, filepath)); + + // Security check: Ensure the resolved path is within the save directory. + if (!fullPath.StartsWith(saveDir, StringComparison.Ordinal)) + { + Logger.Error($"Remove failed: Cannot remove '{filepath}' as it is outside the save directory."); + return false; + } + + if (File.Exists(fullPath)) + { + File.Delete(fullPath); + Logger.Info($"Successfully removed file: '{filepath}'"); + return true; + } + + if (Directory.Exists(fullPath)) + { + // Directory.Delete(path) throws an IOException if the directory is not empty. + Directory.Delete(fullPath); + Logger.Info($"Successfully removed empty directory: '{filepath}'"); + return true; + } + + // Path does not exist + Logger.Warn($"Remove failed: File or directory not found at '{filepath}'."); + return false; + } + catch (IOException ex) + { + // This can happen if the directory is not empty, or file is in use. + Logger.Error($"Remove failed for '{filepath}'. IO Error: {ex.Message}", ex); + return false; + } + catch (UnauthorizedAccessException ex) + { + Logger.Error($"Remove failed for '{filepath}'. Insufficient permissions. Error: {ex.Message}", ex); + return false; + } + catch (SecurityException ex) + { + Logger.Error($"Remove failed for '{filepath}'. Security error. Error: {ex.Message}", ex); + return false; + } + catch (Exception ex) + { + Logger.Error($"An unexpected error occurred while trying to remove '{filepath}'. Error: {ex.Message}", ex); + return false; + } + } + } +} diff --git a/src/Night/Filesystem/Filesystem.Write.cs b/src/Night/Filesystem/Filesystem.Write.cs new file mode 100644 index 00000000..e4fce217 --- /dev/null +++ b/src/Night/Filesystem/Filesystem.Write.cs @@ -0,0 +1,218 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; + +using Night.Log; + +namespace Night +{ + /// + /// Provides an interface to the user's filesystem. + /// + public static partial class Filesystem + { + // Logger is already defined in another part of this partial class. + + /// + /// Writes data to a file. If the file already exists, it will be completely replaced. + /// + /// The name (and path) of the file. + /// The string data to write to the file. The string will be UTF-8 encoded. + /// How many bytes of the encoded string to write. + /// If null, the entire encoded string is written. + /// If the requested size exceeds practical limits (e.g., max underlying stream write size), + /// writing may be capped, and the operation will proceed with the capped amount. + /// + /// A tuple containing: + /// - Success: True if the operation was successful, false otherwise. + /// - ErrorMessage: An error message if the operation was unsuccessful, otherwise null. + /// + public static (bool Success, string? ErrorMessage) Write(string name, string data, long? size = null) + { + if (data == null) + { + return (false, "Data to write cannot be null."); + } + + byte[] encodedData = Encoding.UTF8.GetBytes(data); + return Write(name, encodedData, size); + } + + /// + /// Writes data to a file. If the file already exists, it will be completely replaced. + /// + /// The name (and path) of the file. + /// The byte array data to write to the file. + /// How many bytes from the data array to write. + /// If null, the entire data array is written. + /// If the requested size exceeds practical limits (e.g., max underlying stream write size), + /// writing may be capped, and the operation will proceed with the capped amount. + /// + /// A tuple containing: + /// - Success: True if the operation was successful, false otherwise. + /// - ErrorMessage: An error message if the operation was unsuccessful, otherwise null. + /// + public static (bool Success, string? ErrorMessage) Write(string name, byte[] data, long? size = null) + { + if (string.IsNullOrEmpty(name)) + { + return (false, "File name cannot be null or empty."); + } + + if (data == null) + { + return (false, "Data to write cannot be null."); + } + + if (size.HasValue && size.Value < 0) + { + return (false, "Size to write cannot be negative."); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + // Check if the path starts with a pattern like "X:\" or "X:/" which indicates a Windows drive letter. + // This is not a standard absolute path format on Unix. + if (name.Length >= 2 && char.IsLetter(name[0]) && name[1] == ':') + { + // Further check if it's followed by a directory separator, making it "X:\..." or "X:/..." + if (name.Length >= 3 && (name[2] == Path.DirectorySeparatorChar || name[2] == Path.AltDirectorySeparatorChar)) + { + Logger?.Info($"Path '{name}' on Unix-like system resembles a Windows drive path. Simulating unmapped drive behavior."); + throw new DirectoryNotFoundException($"Could not find a part of the path '{name}'. (Simulated unmapped drive on Unix for Windows-style path)"); + } + } + } + + try + { + // Determine actual number of bytes to write + long bytesToWrite = data.Length; + if (size.HasValue) + { + bytesToWrite = Math.Min(size.Value, data.Length); + } + + // Ensure parent directory exists + string? directoryPath = Path.GetDirectoryName(name); + if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath)) + { + _ = Directory.CreateDirectory(directoryPath); + } + + using (var stream = new FileStream(name, global::System.IO.FileMode.Create, global::System.IO.FileAccess.Write, global::System.IO.FileShare.None)) + { + if (bytesToWrite == 0) + { + // Ensure the file is created (and truncated if it existed) even if writing 0 bytes. + // FileStream with FileMode.Create already handles this. + return (true, null); + } + + // Cap writing at int.MaxValue due to .NET Stream.Write limitations with byte[] + // FileStream itself might handle larger writes internally if underlying OS supports it, + // but the byte[] overload of Write takes an int count. + // For very large data, consider writing in chunks if this cap is an issue. + int actualBytesToWriteInChunk = (int)Math.Min(bytesToWrite, int.MaxValue); + + if (bytesToWrite > int.MaxValue) + { + Logger.Warn($"Requested write size ({bytesToWrite} bytes) for '{name}' exceeds int.MaxValue. Writing in chunks capped at {int.MaxValue} bytes per chunk."); + + long totalBytesWritten = 0; + while (totalBytesWritten < bytesToWrite) + { + int bytesInCurrentChunk = (int)Math.Min(bytesToWrite - totalBytesWritten, int.MaxValue); + stream.Write(data, (int)totalBytesWritten, bytesInCurrentChunk); + totalBytesWritten += bytesInCurrentChunk; + } + } + else + { + stream.Write(data, 0, actualBytesToWriteInChunk); + } + } + + return (true, null); + } + catch (ArgumentException ex) + { + Logger.Error($"Argument error while trying to write to file '{name}'.", ex); + return (false, $"Argument error: {ex.Message}"); + } + catch (PathTooLongException ex) + { + Logger.Error($"Path too long for file '{name}'.", ex); + return (false, "The specified path, file name, or both exceed the system-defined maximum length."); + } + catch (DirectoryNotFoundException ex) + { + Logger.Error($"Directory not found for file '{name}'.", ex); + return (false, "The specified path is invalid (for example, it is on an unmapped drive)."); + } + catch (IOException ex) + { + // HResult for ERROR_FILENAME_EXCED_RANGE (path too long / invalid filename component) is 0x800700CE. + // This can be thrown by Directory.CreateDirectory if a segment of the path is too long, + // before FileStream itself might throw a PathTooLongException. + // We want to provide a consistent error message for path length issues. + const int ERROR_FILENAME_EXCED_RANGE = unchecked((int)0x800700CE); + + if (ex.HResult == ERROR_FILENAME_EXCED_RANGE) + { + Logger.Error($"Path too long (caught as IOException with HResult 0x{ex.HResult:X8}) for file '{name}'. Original Message: {ex.Message}", ex); + + // Return the same message as for PathTooLongException for consistency + return (false, "The specified path, file name, or both exceed the system-defined maximum length."); + } + + Logger.Error($"IO error trying to write to file '{name}'.", ex); + return (false, $"IO error: {ex.Message}"); + } + catch (UnauthorizedAccessException ex) + { + Logger.Error($"Unauthorized access trying to write to file '{name}'.", ex); + return (false, "Unauthorized access. Check file permissions or if the path is a directory."); + } + catch (SecurityException ex) + { + Logger.Error($"Security error trying to write to file '{name}'.", ex); + return (false, "A security error occurred."); + } + catch (NotSupportedException ex) + { + Logger.Error($"Operation not supported for file '{name}'.", ex); + return (false, $"Operation not supported: {ex.Message}"); + } + catch (Exception ex) + { + Logger.Error($"Unexpected error trying to write to file '{name}'.", ex); + return (false, $"An unexpected error occurred: {ex.Message}"); + } + } + } +} diff --git a/src/Night/Filesystem/Filesystem.cs b/src/Night/Filesystem/Filesystem.cs index 4571a179..2956e112 100644 --- a/src/Night/Filesystem/Filesystem.cs +++ b/src/Night/Filesystem/Filesystem.cs @@ -21,16 +21,232 @@ // using System; +using System.Collections.Generic; using System.IO; +using System.Security; +using System.Text; + +using Night.Log; namespace Night { /// - /// Provides basic file system operations. - /// Inspired by Love2D's love.filesystem module. + /// Provides an interface to the user's filesystem. /// - public static class Filesystem + public static partial class Filesystem { + private static readonly ILogger Logger = LogManager.GetLogger("Night.Filesystem.Filesystem"); + private static readonly object GameIdentityLock = new object(); + private static string gameIdentity = "NightDefault"; + private static string? saveDirectoryCache; + private static string? sourceDirectoryCache; + + /// + /// Specifies the type to return file contents as when reading. + /// + public enum ContainerType + { + /// + /// Read content as a string. + /// + String, + + /// + /// Read content as raw byte data. + /// + Data, + } + + /// + /// Sets the identity of the game. This is used to determine the save directory. + /// + /// The name to use for the game's identity. + /// If null or empty, the identity will be reset to "NightDefault". + /// Invalid path characters will be replaced with underscores. + public static void SetIdentity(string? identityName) + { + lock (GameIdentityLock) + { + if (string.IsNullOrWhiteSpace(identityName)) + { + gameIdentity = "NightDefault"; + Logger.Info("Game identity reset to default: NightDefault."); + } + else + { + string sanitizedName = identityName; + char[] invalidChars = Path.GetInvalidFileNameChars(); // Identity is used as a directory name + foreach (char c in invalidChars) + { + if (sanitizedName.Contains(c)) + { + sanitizedName = sanitizedName.Replace(c, '_'); + } + } + + if (sanitizedName != identityName) + { + Logger.Warn($"Game identity '{identityName}' contained invalid characters and was sanitized to '{sanitizedName}'."); + } + + gameIdentity = sanitizedName; + } + + // Invalidate cached save directory path as it depends on identity + saveDirectoryCache = null; + Logger.Info($"Game identity set to: {gameIdentity}"); + } + } + + /// + /// Gets the current identity of the game. + /// + /// The current game identity. + public static string GetIdentity() + { + lock (GameIdentityLock) + { + return gameIdentity; + } + } + + /// + /// Gets the full path to the directory where the game can save files. + /// The directory is created if it doesn't exist. + /// + /// + /// The path depends on the operating system and the game's identity (set by ): + /// + /// Windows%APPDATA%\Night\[Identity]\ + /// macOS~/Library/Application Support/Night/[Identity]/ + /// Linux$XDG_DATA_HOME/night/[Identity]/ or ~/.local/share/night/[Identity]/ + /// + /// + /// The absolute path to the save directory. + /// Thrown if the save directory could not be created or accessed. + /// Thrown if permissions are insufficient to create the save directory. + public static string GetSaveDirectory() + { + lock (GameIdentityLock) + { + if (saveDirectoryCache != null) + { + return saveDirectoryCache; + } + + string basePath; + string nightFolderName = "Night"; // For Windows and macOS + + if (OperatingSystem.IsWindows()) + { + basePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + } + else if (OperatingSystem.IsMacOS()) + { + basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support"); + } + else if (OperatingSystem.IsLinux()) + { + nightFolderName = "night"; // Lowercase for Linux as per Love2D convention + basePath = Environment.GetEnvironmentVariable("XDG_DATA_HOME") ?? string.Empty; + if (string.IsNullOrEmpty(basePath) || !Directory.Exists(basePath)) + { + basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share"); + } + } + else + { + // Fallback for other OSes, though less specific. + // Consider throwing UnsupportedPlatformException if strict adherence to defined platforms is required. + Logger.Warn($"Unsupported OS detected for save directory. Falling back to ApplicationData folder with 'Night' subfolder."); + basePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + if (string.IsNullOrEmpty(basePath)) + { + basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".NightFallbackData"); + Logger.Warn($"ApplicationData folder not found. Using fallback: {basePath}"); + } + } + + string savePath = Path.Combine(basePath, nightFolderName, gameIdentity); + + try + { + if (!Directory.Exists(savePath)) + { + _ = Directory.CreateDirectory(savePath); + Logger.Info($"Created save directory: {savePath}"); + } + } + catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException || ex is SecurityException) + { + Logger.Error($"Failed to create or access save directory '{savePath}'. Error: {ex.Message}", ex); + throw; // Re-throw critical exceptions + } + catch (Exception ex) + { + Logger.Error($"An unexpected error occurred while creating save directory '{savePath}'. Error: {ex.Message}", ex); + + // Depending on policy, might throw a more generic exception or a custom one. + // For now, re-throw to indicate failure. + throw new IOException($"Could not ensure save directory exists at '{savePath}'.", ex); + } + + saveDirectoryCache = savePath; + return savePath; + } + } + + /// + /// Gets the full path to the application's source directory (usually the directory containing the executable). + /// + /// The absolute path to the source directory. + public static string GetSource() + { + if (sourceDirectoryCache == null) + { + sourceDirectoryCache = AppContext.BaseDirectory ?? Directory.GetCurrentDirectory(); + } + + return sourceDirectoryCache; + } + + /// + /// Gets the full path to the directory containing the application's source directory. + /// + /// The absolute path to the base directory of the source, or null if the source is a root directory. + public static string? GetSourceBaseDirectory() + { + return Path.GetDirectoryName(GetSource()); + } + + /// + /// Gets the full path to the current user's home directory. + /// + /// The absolute path to the user's home directory. + public static string GetUserDirectory() + { + return Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + } + + /// + /// Gets the current working directory of the application. + /// + /// The absolute path to the current working directory. + public static string GetWorkingDirectory() + { + return Directory.GetCurrentDirectory(); + } + + /// + /// Gets whether the game is in "fused mode". + /// In Night, this concept is not directly applicable as with .love files, so it always returns false. + /// + /// false. + public static bool IsFused() + { + return false; + } + /// /// Gets information about the specified file or directory. /// @@ -83,8 +299,9 @@ public static class Filesystem return null; } } - catch (Exception) + catch (Exception ex) { + Logger.Error($"Error getting file info for path '{path}'.", ex); return null; } @@ -168,5 +385,173 @@ public static string ReadText(string path) { return File.ReadAllText(path); } + + /// + /// Creates a directory. + /// + /// The path of the directory to create. + /// True if the directory was created, false if it already existed or an error occurred. + /// Thrown if path is null. + /// Thrown if path is empty. + public static bool CreateDirectory(string path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException("Path cannot be empty or consist only of whitespace.", nameof(path)); + } + + if (Directory.Exists(path)) + { + return false; + } + + try + { + _ = Directory.CreateDirectory(path); // Creates all directories in the specified path, if they don't already exist. + return true; + } + catch (Exception ex) + { + Logger.Error($"Error creating directory '{path}'.", ex); + return false; + } + } + + /// + /// Returns the application data directory. + /// The directory is created if it doesn't exist. + /// + /// The full path to the application data directory. + public static string GetAppdataDirectory() + { + // This method's behavior might need to be re-evaluated in light of GetSaveDirectory(). + // For now, it retains its original logic but uses the locked GetIdentity(). + string currentIdentity = GetIdentity(); // Use the thread-safe getter + + string basePath; + if (OperatingSystem.IsWindows()) + { + basePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + } + else if (OperatingSystem.IsMacOS()) + { + basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support"); + } + else if (OperatingSystem.IsLinux()) + { + basePath = Environment.GetEnvironmentVariable("XDG_DATA_HOME") ?? string.Empty; + if (string.IsNullOrEmpty(basePath) || !Directory.Exists(basePath)) + { + basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share"); + } + } + else + { + basePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + if (string.IsNullOrEmpty(basePath)) + { + basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".NightFallbackData"); // Ensure distinct from GetSaveDirectory fallback + } + } + + // Original GetAppdataDirectory combines directly with identity, e.g., %APPDATA%\MyGame + // GetSaveDirectory combines with "Night" then identity, e.g., %APPDATA%\Night\MyGame + // This distinction is maintained here. + string appDataPath = Path.Combine(basePath, currentIdentity); + + try + { + if (!Directory.Exists(appDataPath)) + { + _ = Directory.CreateDirectory(appDataPath); + Logger.Info($"Created appdata directory (legacy GetAppdataDirectory call): {appDataPath}"); + } + } + catch (Exception ex) + { + Logger.Warn($"Could not create appdata directory '{appDataPath}' (legacy GetAppdataDirectory call): {ex.Message}"); + + // Depending on requirements, this might throw or return a non-guaranteed path. + // For now, it returns the path even if creation failed, consistent with original. + } + + return appDataPath; + } + + /// + /// Creates a new File object. It needs to be opened before it can be accessed. + /// + /// The filename of the file. + /// The new NightFile object. + /// Thrown if filename is null or empty. + public static NightFile NewFile(string filename) + { + // Note: LÖVE's love.filesystem.newFile(filename) does not error at this stage + // for invalid filenames, deferring errors to File:open. + // Our NightFile constructor will throw ArgumentNullException if filename is null/empty, + // which is a reasonable basic validation. + return new NightFile(filename); + } + + /// + /// Creates a File object and opens it for reading, writing, or appending. + /// + /// The filename of the file. + /// The mode to open the file in. + /// A tuple containing the new NightFile object (or null if an error occurred) and an error string if an error occurred. + public static (NightFile? File, string? ErrorStr) NewFile(string filename, FileMode mode) + { + try + { + var file = new NightFile(filename); + (bool success, string? error) = file.Open(mode); + if (success) + { + return (file, null); + } + else + { + // Ensure the file object is disposed if open failed, though NightFile's Open should handle internal state. + // If Open fails, the FileStream might not be created, or if created and failed, it should be handled there. + // For safety, we could call Dispose, but it might be redundant if Open cleans up. + // LÖVE returns nil for the file object on error. + return (null, error); + } + } + catch (ArgumentNullException ex) + { + return (null, ex.Message); + } + catch (Exception ex) + { + Logger.Error($"Unexpected error in Filesystem.NewFile('{filename}', '{mode}'): {ex.Message}", ex); + return (null, $"An unexpected error occurred: {ex.Message}"); + } + } + + /// + /// Resolves a relative path to a full path within the save directory. + /// + /// The relative path to resolve. + /// The full, absolute path inside the save directory. + /// Thrown if the relative path attempts to escape the save directory. + private static string GetFullPathInSaveDirectory(string relativePath) + { + string saveDir = GetSaveDirectory(); + string fullPath = Path.GetFullPath(Path.Combine(saveDir, relativePath)); + + // Security check: Ensure the resolved path is still within the save directory. + if (!fullPath.StartsWith(saveDir, StringComparison.Ordinal)) + { + throw new ArgumentException("Path cannot escape the save directory.", nameof(relativePath)); + } + + return fullPath; + } } } diff --git a/src/Night/Filesystem/NightFile.cs b/src/Night/Filesystem/NightFile.cs new file mode 100644 index 00000000..2d92a8d0 --- /dev/null +++ b/src/Night/Filesystem/NightFile.cs @@ -0,0 +1,388 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; +using System.Text; + +using SysIO = System.IO; + +namespace Night +{ + /// + /// Represents a file in the Night framework, providing methods for file operations. + /// This class is analogous to LÖVE's File object. + /// + public class NightFile : IDisposable + { + private readonly string filename; + private FileStream? fileStream; + private Night.FileMode? currentMode; + private bool disposed = false; + + /// + /// Initializes a new instance of the class. + /// The file is not opened by this constructor. It must be opened with or . + /// + /// The name (and path) of the file. + /// Thrown if filename is null or empty. + public NightFile(string filename) + { + if (string.IsNullOrEmpty(filename)) + { + throw new ArgumentNullException(nameof(filename), "Filename cannot be null or empty."); + } + + this.filename = filename; + } + + /// + /// Finalizes an instance of the class. + /// + ~NightFile() + { + this.Dispose(false); + } + + /// + /// Gets the filename of the file. + /// + public string Filename => this.filename; + + /// + /// Gets a value indicating whether the file is currently open. + /// + public bool IsOpen => this.fileStream != null && this.CanRead_Workaround(); // CanRead can throw if closed + + /// + /// Opens the file in the specified mode. + /// + /// The mode to open the file in. + /// A tuple containing a boolean indicating success and an error message if an error occurred. + public (bool Success, string? Error) Open(Night.FileMode mode) + { + if (this.disposed) + { + return (false, "Cannot open a disposed file."); + } + + if (this.IsOpen) + { + return (false, "File is already open."); + } + + try + { + SysIO.FileMode sysFileMode; + SysIO.FileAccess sysFileAccess; + + switch (mode) + { + case Night.FileMode.Read: + sysFileMode = SysIO.FileMode.Open; + sysFileAccess = SysIO.FileAccess.Read; + break; + case Night.FileMode.Write: + sysFileMode = SysIO.FileMode.Create; // Creates a new file or overwrites an existing file. + sysFileAccess = SysIO.FileAccess.Write; + break; + case Night.FileMode.Append: + sysFileMode = SysIO.FileMode.Append; // Opens the file if it exists and seeks to the end, or creates a new file. + sysFileAccess = SysIO.FileAccess.Write; + break; + default: + return (false, "Invalid file mode specified."); + } + + this.fileStream = new SysIO.FileStream(this.filename, sysFileMode, sysFileAccess); + this.currentMode = mode; + return (true, null); + } + catch (Exception ex) + { + return (false, ex.Message); + } + } + + /// + /// Opens the file in the specified mode string (LÖVE-style). + /// + /// The mode string ("r", "w", "a", "rb", "wb", "ab"). + /// A tuple containing a boolean indicating success and an error message if an error occurred. + public (bool Success, string? Error) Open(string modeString) + { + FileMode? mode; + switch (modeString) + { + case "r": + case "rb": // Binary distinction is handled by read methods in .NET + mode = Night.FileMode.Read; + break; + case "w": + case "wb": + mode = Night.FileMode.Write; + break; + case "a": + case "ab": + mode = Night.FileMode.Append; + break; + default: + return (false, $"Invalid file mode string: {modeString}"); + } + + return this.Open(mode.Value); + } + + /// + /// Reads the entire content of the file as a string (UTF-8 encoded). + /// + /// A tuple containing the file content as a string and an error message if an error occurred. + public (string? Data, string? Error) Read() + { + if (!this.IsOpen || this.currentMode != Night.FileMode.Read) + { + return (null, "File is not open for reading."); + } + + if (this.fileStream == null || !this.fileStream.CanRead) + { + return (null, "File stream cannot be read."); + } + + try + { + // LÖVE's file:read() reads from current position to end if no size specified. + // Ensure stream is at the beginning if it's a fresh read, or respect current position. + // For simplicity here, we read from current position. + // If a full re-read is desired after partial reads, Seek(0, SeekOrigin.Begin) would be needed. + using (var reader = new StreamReader(this.fileStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: -1, leaveOpen: true)) + { + string content = reader.ReadToEnd(); + return (content, null); + } + } + catch (Exception ex) + { + return (null, ex.Message); + } + } + + /// + /// Reads a specified number of bytes from the file. + /// + /// The number of bytes to read. + /// A tuple containing the byte array and an error message if an error occurred. + public (byte[]? Data, string? Error) ReadBytes(long bytesToRead) + { + if (!this.IsOpen || this.currentMode != Night.FileMode.Read) + { + return (null, "File is not open for reading."); + } + + if (this.fileStream == null || !this.fileStream.CanRead) + { + return (null, "File stream cannot be read."); + } + + if (bytesToRead <= 0) + { + return (Array.Empty(), null); // LÖVE returns empty string for 0 or negative + } + + try + { + // Determine how many bytes can actually be read (up to bytesToRead or end of stream) + long remainingBytes = this.fileStream.Length - this.fileStream.Position; + int actualBytesToRead = (int)Math.Min(bytesToRead, remainingBytes); + if (actualBytesToRead <= 0) + { + return (Array.Empty(), null); + } + + byte[] buffer = new byte[actualBytesToRead]; + int bytesRead = this.fileStream.Read(buffer, 0, actualBytesToRead); + + // If bytesRead is less than actualBytesToRead, it means end of stream was reached earlier than expected. + // This is fine, just return what was read. If bytesRead is 0, it means we are at EOF. + if (bytesRead < actualBytesToRead) + { + Array.Resize(ref buffer, bytesRead); + } + + return (buffer, null); + } + catch (Exception ex) + { + return (null, ex.Message); + } + } + + /// + /// Reads all remaining bytes from the current position in the file. + /// + /// A tuple containing the byte array and an error message if an error occurred. + public (byte[]? Data, string? Error) ReadBytes() + { + if (!this.IsOpen || this.currentMode != Night.FileMode.Read) + { + return (null, "File is not open for reading."); + } + + if (this.fileStream == null || !this.fileStream.CanRead) + { + return (null, "File stream cannot be read."); + } + + try + { + long remainingBytes = this.fileStream.Length - this.fileStream.Position; + if (remainingBytes <= 0) + { + return (Array.Empty(), null); + } + + // Ensure remainingBytes fits into an int for the byte array size + if (remainingBytes > int.MaxValue) + { + return (null, "Cannot read remaining bytes: file size exceeds maximum array length."); + } + + byte[] buffer = new byte[(int)remainingBytes]; + int offset = 0; + int count = (int)remainingBytes; + int bytesReadTotal = 0; + + while (count > 0) + { + int bytesRead = this.fileStream.Read(buffer, offset, count); + if (bytesRead == 0) + { + break; // End of file reached + } + + bytesReadTotal += bytesRead; + offset += bytesRead; + count -= bytesRead; + } + + if (bytesReadTotal < (int)remainingBytes) + { + Array.Resize(ref buffer, bytesReadTotal); + } + + return (buffer, null); + } + catch (Exception ex) + { + return (null, ex.Message); + } + } + + /// + /// Closes the file. + /// + /// A tuple containing a boolean indicating success and an error message if an error occurred. + public (bool Success, string? Error) Close() + { + if (this.disposed) + { + // LÖVE allows closing a closed file without error. + // However, if it's disposed, it's a more terminal state. + // For consistency with LÖVE, perhaps just return true if already closed/disposed. + // Let's stick to returning true if not open. + return (true, null); + } + + if (!this.IsOpen) + { + return (true, null); // Already closed or never opened + } + + string? errorMessage = null; + bool success = true; + try + { + // If IsOpen is true, fileStream should not be null due to the IsOpen check. + this.fileStream!.Flush(); // Explicitly flush before closing. + this.fileStream!.Close(); // Close also disposes the FileStream. + } + catch (Exception ex) + { + errorMessage = ex.Message; + success = false; + } + finally + { + this.fileStream = null; + this.currentMode = null; + } + + return (success, errorMessage); + } + + /// + /// Releases all resources used by the object. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + // Dispose managed state (managed objects). + if (this.fileStream != null) + { + this.fileStream.Dispose(); + this.fileStream = null; + } + } + + // Free unmanaged resources (unmanaged objects) and override a finalizer below. + // Set large fields to null. + this.disposed = true; + } + } + + // Workaround for FileStream.CanRead throwing ObjectDisposedException + private bool CanRead_Workaround() + { + try + { + return this.fileStream?.CanRead ?? false; + } + catch (ObjectDisposedException) + { + return false; + } + } + } +} diff --git a/src/Night/Framework.cs b/src/Night/Framework.cs deleted file mode 100644 index a2a9c372..00000000 --- a/src/Night/Framework.cs +++ /dev/null @@ -1,646 +0,0 @@ -// -// zlib license -// -// Copyright (c) 2025 Danny Solivan, Night Circle -// -// This software is provided 'as-is', without any express or implied -// warranty. In no event will the authors be held liable for any damages -// arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose, -// including commercial applications, and to alter it and redistribute it -// freely, subject to the following restrictions: -// -// 1. The origin of this software must not be misrepresented; you must not -// claim that you wrote the original software. If you use this software -// in a product, an acknowledgment in the product documentation would be -// appreciated but is not required. -// 2. Altered source versions must be plainly marked as such, and must not be -// misrepresented as being the original software. -// 3. This notice may not be removed or altered from any source distribution. -// - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; - -using Night; - -using SDL3; - -namespace Night -{ - /// - /// Manages the main game loop and coordination of game states. - /// Provides the main entry point to run a game. - /// - public static class Framework - { - private const int MaxDeltaHistorySamples = 60; // Store up to 1 second of deltas at 60fps - - private static bool isSdlInitialized = false; - private static SDL.InitFlags initializedSubsystems = 0; - - private static int frameCount = 0; - private static double fpsTimeAccumulator = 0.0; - private static List deltaHistory = new List(); - - private static bool inErrorState = false; - - /// - /// Gets a value indicating whether a flag indicating whether the core SDL systems, particularly for input, - /// have been successfully initialized by this Framework's Run method. - /// - public static bool IsInputInitialized { get; internal set; } = false; - - /// - /// Runs the game instance. - /// The game loop will internally call Load, Update, and Draw methods - /// on the provided game logic. - /// This method will initialize and shut down required SDL subsystems. - /// - /// The game interface to run. Must implement . - public static void Run(IGame game) - { - if (game == null) - { - Console.WriteLine("Night.Framework.Run: gameLogic cannot be null."); - return; - } - - ConfigurationManager.LoadConfig(); - var windowConfig = ConfigurationManager.CurrentConfig.Window; - - string nightVersionString = VersionInfo.GetVersion(); - string sdlVersionString = NightSDL.GetVersion(); - Console.WriteLine($"Night Engine: v{nightVersionString}"); - Console.WriteLine($"SDL: v{sdlVersionString}"); - Console.WriteLine(GetFormattedPlatformString()); - Console.WriteLine($"Framework: {RuntimeInformation.FrameworkDescription}"); - - try - { - initializedSubsystems = SDL.InitFlags.Video | SDL.InitFlags.Events; - if (!SDL.Init(initializedSubsystems)) - { - Console.WriteLine($"Night.Framework.Run: SDL_Init failed: {SDL.GetError()}"); - return; - } - - isSdlInitialized = true; - IsInputInitialized = (initializedSubsystems & SDL.InitFlags.Events) == SDL.InitFlags.Events; - - // Setup initial window based on configuration BEFORE game.Load() - SDL.WindowFlags sdlFlags = (SDL.WindowFlags)0; - if (windowConfig.Resizable) - { - sdlFlags |= SDL.WindowFlags.Resizable; - } - - if (windowConfig.Borderless) - { - sdlFlags |= SDL.WindowFlags.Borderless; - } - - if (windowConfig.HighDPI) - { - sdlFlags |= SDL.WindowFlags.HighPixelDensity; - } - - bool modeSet = Window.SetMode(windowConfig.Width, windowConfig.Height, sdlFlags); - if (!modeSet) - { - Console.WriteLine($"Night.Framework.Run: Failed to set initial window mode from configuration: {SDL.GetError()}"); - CleanUpSDL(); - return; - } - - Window.SetTitle(windowConfig.Title ?? "Night Game"); - - if (windowConfig.Fullscreen) - { - FullscreenType fsType = windowConfig.FullscreenType.ToLowerInvariant() == "exclusive" - ? FullscreenType.Exclusive - : FullscreenType.Desktop; - if (!Window.SetFullscreen(true, fsType)) - { - Console.WriteLine($"Night.Framework.Run: Failed to set initial fullscreen mode from configuration: {SDL.GetError()}"); - } - } - - if (Window.RendererPtr != nint.Zero) - { - if (!SDL.SetRenderVSync(Window.RendererPtr, windowConfig.VSync ? 1 : 0)) - { - Console.WriteLine($"Night.Framework.Run: Failed to set initial VSync mode from configuration: {SDL.GetError()}"); - } - } - - if (windowConfig.X.HasValue && windowConfig.Y.HasValue && Window.Handle != nint.Zero) - { - _ = SDL.SetWindowPosition(Window.Handle, windowConfig.X.Value, windowConfig.Y.Value); - } - - // Set window icon if specified in config - if (!string.IsNullOrEmpty(windowConfig.IconPath) && Window.Handle != nint.Zero) - { - // Assuming IconPath is relative to the game's executable directory or a common assets folder. - // AppContext.BaseDirectory should give the directory where the .exe is. - // If your assets are in a subdirectory like "assets", you might need: - // string iconFullPath = System.IO.Path.Combine(AppContext.BaseDirectory, "assets", windowConfig.IconPath); - // For now, let's assume IconPath can be resolved directly or is absolute. - // A more robust solution would involve the Filesystem module to resolve paths. - string iconFullPath = windowConfig.IconPath; - if (!Path.IsPathRooted(iconFullPath)) - { - iconFullPath = Path.Combine(AppContext.BaseDirectory, iconFullPath); - } - - if (!Window.SetIcon(iconFullPath)) - { - Console.WriteLine($"Night.Framework.Run: Failed to set window icon from configuration: '{iconFullPath}'. Check path and image format."); - } - } - - // End of initial window setup - try - { - // game.Load() can now use Graphics.NewImage(), and can also call Window.SetMode again to override. - game.Load(); - } - catch (Exception e) - { - HandleGameException(e, game); - if (inErrorState) - { - CleanUpSDLAndWindow(); - return; - } - } - - // After game.Load(), check if window is still open. - // If game.Load() called Window.Close() or failed to maintain a window, we should not continue. - if (!Window.IsOpen()) - { - Console.WriteLine("Night.Framework.Run: Window is not open after game.Load(). Exiting."); - - // Ensure cleanup if window was closed by game.Load() - CleanUpSDLAndWindow(); - return; - } - - // If game.Load() *did* change window settings (e.g. VSync via a new SetMode call), - // we don't re-apply config VSync here unless we have a way to know it wasn't touched by game. - // The current Window.SetMode creates a new renderer, so VSync would be reset anyway if game called SetMode. - // So, if game called SetMode, it's responsible for its own VSync if it differs from config default for new renderer. - // If game didn't call SetMode, our initial VSync setting stands. - Night.Timer.Initialize(); - - frameCount = 0; - fpsTimeAccumulator = 0.0; - deltaHistory.Clear(); - - // Main game loop - while (Window.IsOpen() && !inErrorState) - { - // Calculate DeltaTime by calling Night.Timer.Step() - double deltaTime = Night.Timer.Step(); - - // FPS Calculation - frameCount++; - fpsTimeAccumulator += deltaTime; - if (fpsTimeAccumulator >= 1.0) - { - Night.Timer.CurrentFPS = frameCount; - frameCount = 0; - - // Subtract 1 second, keep remainder for accuracy - fpsTimeAccumulator -= 1.0; - } - - // Average Delta Calculation - deltaHistory.Add(deltaTime); - if (deltaHistory.Count > MaxDeltaHistorySamples) - { - // Keep the list size bounded - deltaHistory.RemoveAt(0); - } - - if (deltaHistory.Count > 0) - { - Night.Timer.CurrentAverageDelta = deltaHistory.Average(); - } - - // Event Processing - while (SDL.PollEvent(out SDL.Event e) && !inErrorState) - { - var eventType = (SDL.EventType)e.Type; - - if (eventType == SDL.EventType.Quit) - { - Window.Close(); - } - else if (eventType == SDL.EventType.KeyDown) - { - try - { - // TODO: Rename these to match love2d - game.KeyPressed( - (KeySymbol)e.Key.Key, - (KeyCode)e.Key.Scancode, - e.Key.Repeat); - } - catch (Exception exUser) - { - HandleGameException(exUser, game); - } - } - else if (eventType == SDL.EventType.KeyUp) - { - try - { - game.KeyReleased( - (KeySymbol)e.Key.Key, - (KeyCode)e.Key.Scancode); - } - catch (Exception exUser) - { - HandleGameException(exUser, game); - } - } - else if (eventType == SDL.EventType.MouseButtonDown) - { - try - { - game.MousePressed( - (int)e.Button.X, - (int)e.Button.Y, - (MouseButton)e.Button.Button, - /* istouch */ e.Button.Which == SDL.TouchMouseID, - e.Button.Clicks); - } - catch (Exception exUser) - { - HandleGameException(exUser, game); - } - } - else if (eventType == SDL.EventType.MouseButtonUp) - { - try - { - game.MouseReleased( - (int)e.Button.X, - (int)e.Button.Y, - (MouseButton)e.Button.Button, - /* istouch */ e.Button.Which == SDL.TouchMouseID, - e.Button.Clicks); - } - catch (Exception exUser) - { - HandleGameException(exUser, game); - } - } - - // TODO: Add other event handling (mouse, etc.) as per future tasks. - } - - // Check if error occurred during event processing - if (inErrorState) - { - // Error handler (Default or custom) should have run. - // Default handler enters its own loop or prepares for exit. - // If it was a custom handler, it might have cleared _inErrorState or decided to continue. - // If _inErrorState is still true, we break the main loop. - break; - } - - // Update, do not update if an error has occurred and is being handled - if (!inErrorState) - { - try - { - game.Update((float)deltaTime); - } - catch (Exception exUser) - { - HandleGameException(exUser, game); - if (inErrorState) - { - break; // Exit main loop if error sets state - } - } - } - - // Draw, do not draw if an error has occurred and is being handled - if (!inErrorState) - { - try - { - // Graphics.BeginFrame() / Clear etc. should be called by game.Draw() or a higher level abstraction. - // For now, FrameworkLoop does not manage the render target clearing directly. - // It's assumed game.Draw() handles everything from clear to present. - game.Draw(); - - // Present the drawn frame to the screen - Night.Graphics.Present(); - } - catch (Exception exUser) - { - HandleGameException(exUser, game); - - // If Draw fails, we typically still want to try and finish the frame/loop iteration - // unless _inErrorState is set by the handler to signal a desire to stop. - if (inErrorState) - { - break; - } - } - } - } - } - catch (Exception ex) - { - // This is for errors within Framework.Run itself, not game code. - Console.WriteLine($"Night.Framework.Run: An UNEXPECTED FRAMEWORK error occurred: {ex.ToString()}"); - - // Attempt to call default error handler for framework errors too, but without game instance. - HandleGameException(ex, null); - } - finally - { - // TODO: Call gameLogic.Quit() if it's added to IGame. - CleanUpSDLAndWindow(); - } - } - - private static void HandleGameException(Exception e, IGame? gameInstance) - { - inErrorState = true; // Signal that we are now in an error state. - - var customHandler = Night.Error.GetHandler(); - if (customHandler != null) - { - try - { - customHandler(e); - - // If custom handler returns, we assume it handled the error - // and the game might want to continue or has already quit. - // For now, we'll still close the window to be safe, unless custom handler re-opens it. - // This behavior might need refinement. - if (Window.IsOpen()) - { - Window.Close(); - } - } - catch (Exception exHandler) - { - // Error in the custom error handler itself! - Console.WriteLine($"Night.Framework.Run: CRITICAL: Exception in custom error handler: {exHandler.ToString()}"); - - // Fallback to a very minimal default behavior - Console.WriteLine($"Night.Framework.Run: Original game error: {e.ToString()}"); - if (Window.IsOpen()) - { - Window.Close(); // Ensure shutdown - } - } - } - else - { - DefaultErrorHandler(e, gameInstance); - } - } - - private static void DefaultErrorHandler(Exception e, IGame? gameInstance) - { - Console.Error.WriteLine("--- Night Engine: Default Error Handler ---"); - Console.Error.WriteLine($"An error occurred in the game: {e.GetType().Name}"); - Console.Error.WriteLine($"Message: {e.Message}"); - Console.Error.WriteLine("Stack Trace:"); - Console.Error.WriteLine(e.StackTrace); - Console.Error.WriteLine("-------------------------------------------"); - - bool canDrawError = false; - try - { - // Assuming Graphics.RendererPtr is a good check for active graphics - if (!Window.IsOpen() || (Window.RendererPtr == nint.Zero)) - { - Console.WriteLine("Night.Framework.Run (DefaultErrorHandler): Window or Graphics not initialized. Attempting to set mode..."); - - // Attempt to set a basic window mode if not already open. - // Use a default size. WindowFlags can be minimal or Resizable. - if (Window.SetMode(800, 600, SDL.WindowFlags.Resizable)) - { - Console.WriteLine("Night.Framework.Run (DefaultErrorHandler): Window mode set to 800x600."); - canDrawError = Window.RendererPtr != nint.Zero; - } - else - { - Console.WriteLine($"Night.Framework.Run (DefaultErrorHandler): Failed to set window mode. SDL Error: {SDL.GetError()}"); - } - } - else - { - canDrawError = true; - } - - // Reset input state - if (IsInputInitialized) - { - Mouse.SetVisible(true); - Mouse.SetGrabbed(false); - Mouse.SetRelativeMode(false); - - // Mouse.SetCursor() - Skipped as per plan if complex; SDL default cursor should apply. - } - } - catch (Exception resetEx) - { - Console.Error.WriteLine($"Night.Framework.Run (DefaultErrorHandler): Exception during state reset: {resetEx.ToString()}"); - canDrawError = false; // If reset fails, drawing might be unsafe. - } - - if (canDrawError) - { - try - { - // Simple error display loop - string fullErrorText = $"Error: {e.Message}\n\n{e.StackTrace}"; - - // Shorten for display if too long, or make it scrollable if we had font rendering - // For now, just display what fits or make user copy. - Window.SetTitle($"Error - {gameInstance?.GetType().Name ?? "Night Game"}"); - - bool runningErrorLoop = true; - while (runningErrorLoop && Window.IsOpen()) - { - while (SDL.PollEvent(out SDL.Event ev)) - { - if (ev.Type == (uint)SDL.EventType.Quit) - { - runningErrorLoop = false; - Window.Close(); - break; - } - - if (ev.Type == (uint)SDL.EventType.KeyDown) - { - if (ev.Key.Key == SDL.Keycode.Escape) - { - runningErrorLoop = false; - Window.Close(); - break; - } - - // Check for Ctrl+C - SDL.Keymod.Ctrl is a flag - if (ev.Key.Key == SDL.Keycode.C && ((SDL.GetModState() & SDL.Keymod.Ctrl) != 0)) - { - try - { - if (Night.System.SetClipboardText(fullErrorText)) - { - Console.WriteLine("(Error copied to clipboard)"); - } - else - { - Console.WriteLine($"(Failed to copy error to clipboard: {SDL.GetError()})"); - } - } - catch (Exception clipEx) - { - Console.WriteLine($"(Exception trying to copy to clipboard: {clipEx.Message})"); - } - } - } - } - - if (!runningErrorLoop) - { - break; - } - - Graphics.Clear(new Color(89, 157, 220, 255)); // Blue background - - // Graphics.Print functionality is NOT available. - // We will just show a blue screen and title. User must check console. - // If Night.Font was available: - // Graphics.SetColor(Night.Color.Black); - // Graphics.Print($"Error: {e.Message}", 10, 10, Window.GetWidth() - 20); - // Graphics.Print($"Press ESC to quit. Ctrl+C to copy.", 10, Window.GetHeight() - 30); - Graphics.Present(); - Timer.Sleep(0.01f); // Sleep for 10ms - } - } - catch (Exception drawEx) - { - Console.Error.WriteLine($"Night.Framework.Run (DefaultErrorHandler): Exception during error display loop: {drawEx.ToString()}"); - } - } - else - { - Console.WriteLine("Night.Framework.Run (DefaultErrorHandler): Cannot display visual error. Check console. Press Ctrl+C in console to quit if frozen."); - - // Loop to keep process alive for a bit for console reading, or just exit. - // For now, just let it fall through to finally block. - } - - // Ensure the main loop knows to terminate - if (Window.IsOpen()) - { - Window.Close(); - } - } - - private static void CleanUpSDLAndWindow() - { - // Shutdown window and related resources (renderer, etc.) - // This should happen before SDL.QuitSubSystem for Video. - // This case should ideally not be hit if _inErrorState or loop conditions were managed correctly - if (Window.IsOpen()) - { - Console.WriteLine("Night.Framework.Run (CleanUpSDLAndWindow): Window was still open, attempting to close."); - Window.Close(); // This will set _isWindowOpen to false - } - - // Window.Shutdown() handles destroying window, renderer, and SDL.QuitSubSystem(SDL.InitFlags.Video) - // It's important that Shutdown is called AFTER the error handler's visual loop might have used the window/renderer. - Window.Shutdown(); - - CleanUpSDL(); - } - - private static void CleanUpSDL() - { - if (isSdlInitialized) - { - // SDL.QuitSubSystem was already called for Video by Window.Shutdown(). - // We only need to quit other subsystems explicitly initialized by Run if they weren't covered. - // However, SDL.Quit() handles all initialized subsystems. - SDL.Quit(); - isSdlInitialized = false; - IsInputInitialized = false; - initializedSubsystems = 0; - } - } - - private static string GetFormattedPlatformString() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - try - { - string macOSVersion = string.Empty; - string darwinVersion = string.Empty; - - // Get macOS version - ProcessStartInfo swVersPsi = new ProcessStartInfo - { - FileName = "sw_vers", - Arguments = "-productVersion", - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true, - }; - using (Process swVersProcess = Process.Start(swVersPsi)!) - { - macOSVersion = swVersProcess.StandardOutput.ReadToEnd().Trim(); - swVersProcess.WaitForExit(); - } - - // Get Darwin kernel version - ProcessStartInfo unamePsi = new ProcessStartInfo - { - FileName = "uname", - Arguments = "-r", - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true, - }; - using (Process unameProcess = Process.Start(unamePsi)!) - { - darwinVersion = unameProcess.StandardOutput.ReadToEnd().Trim(); - unameProcess.WaitForExit(); - } - - if (!string.IsNullOrEmpty(macOSVersion) && !string.IsNullOrEmpty(darwinVersion)) - { - return $"Platform: macOS {macOSVersion} (Darwin {darwinVersion})"; - } - } - catch (Exception ex) - { - // Log the exception or handle it as needed, then fall back. - Console.WriteLine($"Night.Framework.Run: Could not retrieve detailed macOS version info: {ex.Message}"); - } - } - - // Fallback for non-macOS platforms or if macOS version retrieval fails - return $"Platform: {RuntimeInformation.OSDescription} ({RuntimeInformation.OSArchitecture})"; - } - } -} diff --git a/src/Night/Framework/Framework.Events.cs b/src/Night/Framework/Framework.Events.cs new file mode 100644 index 00000000..3cf874d6 --- /dev/null +++ b/src/Night/Framework/Framework.Events.cs @@ -0,0 +1,408 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; + +// using Night.Joysticks; // This was incorrect as Joysticks is a static class, types are in Night namespace. +using System.Runtime.InteropServices; + +using SDL3; + +namespace Night +{ + /// + /// Provides the core framework functionalities, including the main game loop and event processing. + /// This partial class specifically handles SDL event processing. + /// + public static partial class Framework + { + private static void ProcessSdlEvents(IGame game) + { + while (SDL.PollEvent(out SDL.Event e) && !inErrorState) + { + var eventType = (SDL.EventType)e.Type; + Logger.Debug($"SDL Event polled: {eventType}"); + if (eventType == SDL.EventType.Quit) + { + Logger.Info("SDL_QUIT event received. Closing window."); + Window.Close(); + } + else if (eventType == SDL.EventType.KeyDown) + { + try + { + game.KeyPressed((KeySymbol)e.Key.Key, (KeyCode)e.Key.Scancode, e.Key.Repeat); + } + catch (Exception exUser) + { + HandleGameException(exUser, game); + } + } + else if (eventType == SDL.EventType.KeyUp) + { + try + { + game.KeyReleased((KeySymbol)e.Key.Key, (KeyCode)e.Key.Scancode); + } + catch (Exception exUser) + { + HandleGameException(exUser, game); + } + } + else if (eventType == SDL.EventType.MouseButtonDown) + { + try + { + game.MousePressed((int)e.Button.X, (int)e.Button.Y, (MouseButton)e.Button.Button, e.Button.Which == SDL.TouchMouseID, e.Button.Clicks); + } + catch (Exception exUser) + { + HandleGameException(exUser, game); + } + } + else if (eventType == SDL.EventType.MouseButtonUp) + { + try + { + game.MouseReleased((int)e.Button.X, (int)e.Button.Y, (MouseButton)e.Button.Button, e.Button.Which == SDL.TouchMouseID, e.Button.Clicks); + } + catch (Exception exUser) + { + HandleGameException(exUser, game); + } + } + else if (eventType == SDL.EventType.JoystickAdded) + { + Logger.Info($"SDL_JOYSTICKADDED event: Joystick instance ID {e.JDevice.Which}"); + Joystick? newJoystick = Night.Joysticks.AddJoystick(e.JDevice.Which); + if (newJoystick != null) + { + try + { + game.JoystickAdded(newJoystick); + } + catch (Exception exUser) + { + HandleGameException(exUser, game); + } + } + else + { + Logger.Warn($"Failed to add joystick with instance ID {e.JDevice.Which} via Joysticks.AddJoystick."); + } + } + else if (eventType == SDL.EventType.JoystickRemoved) + { + Logger.Info($"SDL_JOYSTICKREMOVED event: Joystick instance ID {e.JDevice.Which}"); + Joystick? removedJoystick = Night.Joysticks.RemoveJoystick(e.JDevice.Which); + if (removedJoystick != null) + { + try + { + game.JoystickRemoved(removedJoystick); + } + catch (Exception exUser) + { + HandleGameException(exUser, game); + } + finally + { + removedJoystick.Dispose(); // Ensure joystick is disposed after event callback + } + } + else + { + Logger.Warn($"Failed to remove joystick with instance ID {e.JDevice.Which} via Joysticks.RemoveJoystick (it might have already been removed or was never fully added)."); + } + } + else if (eventType == SDL.EventType.JoystickAxisMotion) + { + Logger.Debug($"SDL_JOYSTICKAXISMOTION event: Joystick {e.JAxis.Which}, Axis {e.JAxis.Axis}, Value {e.JAxis.Value}"); + Joystick? joystick = Night.Joysticks.GetJoystickByInstanceId(e.JAxis.Which); + if (joystick != null) + { + try + { + float normalizedValue = NormalizeSdlAxisValue(e.JAxis.Value); + game.JoystickAxis(joystick, (int)e.JAxis.Axis, normalizedValue); + } + catch (Exception exUser) + { + HandleGameException(exUser, game); + } + } + else + { + Logger.Warn($"Received JoystickAxisMotion for unknown joystick instance ID {e.JAxis.Which}"); + } + } + else if (eventType == SDL.EventType.JoystickButtonDown) + { + Logger.Debug($"SDL_JOYSTICKBUTTONDOWN event: Joystick {e.JButton.Which}, Button {e.JButton.Button}"); + Joystick? joystick = Night.Joysticks.GetJoystickByInstanceId(e.JButton.Which); + if (joystick != null) + { + try + { + game.JoystickPressed(joystick, (int)e.JButton.Button); + } + catch (Exception exUser) + { + HandleGameException(exUser, game); + } + } + else + { + Logger.Warn($"Received JoystickButtonDown for unknown joystick instance ID {e.JButton.Which}"); + } + } + else if (eventType == SDL.EventType.JoystickButtonUp) + { + Logger.Debug($"SDL_JOYSTICKBUTTONUP event: Joystick {e.JButton.Which}, Button {e.JButton.Button}"); + Joystick? joystick = Night.Joysticks.GetJoystickByInstanceId(e.JButton.Which); + if (joystick != null) + { + try + { + game.JoystickReleased(joystick, (int)e.JButton.Button); + } + catch (Exception exUser) + { + HandleGameException(exUser, game); + } + } + else + { + Logger.Warn($"Received JoystickButtonUp for unknown joystick instance ID {e.JButton.Which}"); + } + } + else if (eventType == SDL.EventType.JoystickHatMotion) + { + Logger.Debug($"SDL_JOYSTICKHATMOTION event: Joystick {e.JHat.Which}, Hat {e.JHat.Hat}, Value {(JoystickHat)e.JHat.Value}"); + Joystick? joystick = Night.Joysticks.GetJoystickByInstanceId(e.JHat.Which); + if (joystick != null) + { + try + { + game.JoystickHat(joystick, (int)e.JHat.Hat, (JoystickHat)e.JHat.Value); + } + catch (Exception exUser) + { + HandleGameException(exUser, game); + } + } + else + { + Logger.Warn($"Received JoystickHatMotion for unknown joystick instance ID {e.JHat.Which}"); + } + } + else if (eventType == SDL.EventType.GamepadAxisMotion) + { + Logger.Debug($"SDL_GAMEPADAXISMOTION event: Joystick {e.GAxis.Which}, Axis {(SDL.GamepadAxis)e.GAxis.Axis}, Value {e.GAxis.Value}"); + Joystick? joystick = Night.Joysticks.GetJoystickByInstanceId(e.GAxis.Which); + if (joystick != null) + { + if (joystick.IsGamepad()) + { + try + { + float normalizedValue = NormalizeSdlAxisValue(e.GAxis.Value); + Night.GamepadAxis nightAxis = MapSdlGamepadAxisToNight((SDL.GamepadAxis)e.GAxis.Axis); + game.GamepadAxis(joystick, nightAxis, normalizedValue); + } + catch (Exception exUser) + { + HandleGameException(exUser, game); + } + } + } + else + { + Logger.Warn($"Received GamepadAxisMotion for unknown joystick instance ID {e.GAxis.Which}"); + } + } + else if (eventType == SDL.EventType.GamepadButtonDown) + { + Logger.Debug($"SDL_GAMEPADBUTTONDOWN event: Joystick {e.GButton.Which}, Button {(SDL.GamepadButton)e.GButton.Button}"); + Joystick? joystick = Night.Joysticks.GetJoystickByInstanceId(e.GButton.Which); + if (joystick != null) + { + if (joystick.IsGamepad()) + { + try + { + Night.GamepadButton nightButton = MapSdlGamepadButtonToNight((SDL.GamepadButton)e.GButton.Button); + game.GamepadPressed(joystick, nightButton); + } + catch (Exception exUser) + { + HandleGameException(exUser, game); + } + } + } + else + { + Logger.Warn($"Received GamepadButtonDown for unknown joystick instance ID {e.GButton.Which}"); + } + } + else if (eventType == SDL.EventType.GamepadButtonUp) + { + Logger.Debug($"SDL_GAMEPADBUTTONUP event: Joystick {e.GButton.Which}, Button {(SDL.GamepadButton)e.GButton.Button}"); + Joystick? joystick = Night.Joysticks.GetJoystickByInstanceId(e.GButton.Which); + if (joystick != null) + { + if (joystick.IsGamepad()) + { + try + { + Night.GamepadButton nightButton = MapSdlGamepadButtonToNight((SDL.GamepadButton)e.GButton.Button); + game.GamepadReleased(joystick, nightButton); + } + catch (Exception exUser) + { + HandleGameException(exUser, game); + } + } + } + else + { + Logger.Warn($"Received GamepadButtonUp for unknown joystick instance ID {e.GButton.Which}"); + } + } + else if (eventType == SDL.EventType.DropFile) + { + Logger.Debug($"SDL_DROPFILE event: Window {e.Drop.WindowID}"); + if (e.Drop.Data != IntPtr.Zero) + { + try + { + var path = Marshal.PtrToStringUTF8(e.Drop.Data); + if (path != null) + { + game.FileDropped(new DroppedFile(path)); + } + } + catch (Exception exUser) + { + HandleGameException(exUser, game); + } + finally + { + // TODO: Free the data associated with the drop event. + // The SDL documentation for SDL_DropEvent states that the `file` member + // (e.Drop.Data) must be freed by the caller using SDL_free. + // The current version of the SDL3-CS wrapper does not expose a public + // method to call SDL_free. This will result in a small memory leak + // for each dropped file. This should be addressed when the wrapper is updated. + // SDL.free(e.Drop.Data); + } + } + } + + // Other Gamepad event handling will be added here in later phases + } + } + + private static float NormalizeSdlAxisValue(short value) + { + // SDL axis values range from -32768 (SDL_JOYSTICK_AXIS_MIN) to 32767 (SDL_JOYSTICK_AXIS_MAX). + // We want to normalize this to -1.0f to 1.0f. + if (value == 0) + { + return 0.0f; + } + else if (value == -32768) + { + return -1.0f; + } + + // For positive values, max is 32767. For negative, min is -32768 (already handled). + // So, for positive values, divide by 32767.0f. + // For negative values (excluding -32768), divide by 32768.0f to maintain symmetry around 0. + return value < 0 ? value / 32768.0f : value / 32767.0f; + } + + private static Night.GamepadAxis MapSdlGamepadAxisToNight(SDL.GamepadAxis sdlAxis) + { + switch (sdlAxis) + { + case SDL.GamepadAxis.LeftX: + return Night.GamepadAxis.LeftX; + case SDL.GamepadAxis.LeftY: + return Night.GamepadAxis.LeftY; + case SDL.GamepadAxis.RightX: + return Night.GamepadAxis.RightX; + case SDL.GamepadAxis.RightY: + return Night.GamepadAxis.RightY; + case SDL.GamepadAxis.LeftTrigger: + return Night.GamepadAxis.TriggerLeft; + case SDL.GamepadAxis.RightTrigger: + return Night.GamepadAxis.TriggerRight; + default: + Logger.Warn($"Unknown SDL.GamepadAxis: {sdlAxis}. Defaulting to LeftX."); + return Night.GamepadAxis.LeftX; // Or throw an exception + } + } + + private static Night.GamepadButton MapSdlGamepadButtonToNight(SDL.GamepadButton sdlButton) + { + switch (sdlButton) + { + case SDL.GamepadButton.South: // A + return Night.GamepadButton.A; + case SDL.GamepadButton.East: // B + return Night.GamepadButton.B; + case SDL.GamepadButton.West: // X + return Night.GamepadButton.X; + case SDL.GamepadButton.North: // Y + return Night.GamepadButton.Y; + case SDL.GamepadButton.Back: + return Night.GamepadButton.Back; + case SDL.GamepadButton.Guide: + return Night.GamepadButton.Guide; + case SDL.GamepadButton.Start: + return Night.GamepadButton.Start; + case SDL.GamepadButton.LeftStick: + return Night.GamepadButton.LeftStick; + case SDL.GamepadButton.RightStick: + return Night.GamepadButton.RightStick; + case SDL.GamepadButton.LeftShoulder: + return Night.GamepadButton.LeftShoulder; + case SDL.GamepadButton.RightShoulder: + return Night.GamepadButton.RightShoulder; + case SDL.GamepadButton.DPadUp: + return Night.GamepadButton.DPUp; + case SDL.GamepadButton.DPadDown: + return Night.GamepadButton.DPDown; + case SDL.GamepadButton.DPadLeft: + return Night.GamepadButton.DPLeft; + case SDL.GamepadButton.DPadRight: + return Night.GamepadButton.DPRight; + + // SDL.GamepadButton.Misc1, Paddle1, Paddle2, Paddle3, Paddle4, Touchpad are not in Night.GamepadButton + default: + Logger.Warn($"Unknown SDL.GamepadButton: {sdlButton}. Defaulting to A."); + return Night.GamepadButton.A; // Or throw an exception + } + } + } +} diff --git a/src/Night/Framework/Framework.Run.cs b/src/Night/Framework/Framework.Run.cs new file mode 100644 index 00000000..44d5e222 --- /dev/null +++ b/src/Night/Framework/Framework.Run.cs @@ -0,0 +1,462 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; + +using Night; +using Night.Log; + +using SDL3; + +namespace Night +{ + /// + /// Manages the main game loop and coordination of game states. + /// Provides the main entry point to run a game. + /// + public static partial class Framework + { + private const int MaxDeltaHistorySamples = 60; // Store up to 1 second of deltas at 60fps + + private static readonly object SdlLock = new object(); // Thread synchronization for SDL operations + private static readonly ILogger Logger = LogManager.GetLogger("Framework"); + private static bool isSdlInitialized = false; // Tracks if SDL is currently active globally + private static SDL.InitFlags initializedSubsystemsFlags = 0; + + // Delegate to hold a reference to the log output function to prevent garbage collection. + private static SDL.LogOutputFunction? sdlLogOutputFunction; + + private static int frameCount = 0; + private static double fpsTimeAccumulator = 0.0; + private static List deltaHistory = new List(); + + private static bool inErrorState = false; + + /// + /// Gets a value indicating whether a flag indicating whether the core SDL systems, particularly for input, + /// have been successfully initialized by this Framework's Run method. + /// + public static bool IsInputInitialized { get; internal set; } = false; + + /// + /// Runs the game instance. + /// The game loop will internally call Load, Update, and Draw methods + /// on the provided game logic. + /// This method will initialize and shut down required SDL subsystems. + /// + /// The game interface to run. Must implement . + /// The parsed command-line arguments. Optional; if null, default settings are used. + public static void Run(IGame game, CLI? cliArgs = null) + { + if (game == null) + { + Logger.Error("gameLogic cannot be null."); + return; + } + + cliArgs?.ApplySettings(); + + inErrorState = false; + IsInputInitialized = false; + + ConfigurationManager.LoadConfig(); + var windowConfig = ConfigurationManager.CurrentConfig.Window; + + if (cliArgs == null || !cliArgs.IsSilentMode) + { + string nightVersionString = VersionInfo.GetVersion(); + string sdlVersionString = NightSDL.GetVersion(); + Console.WriteLine($"Night Engine: v{nightVersionString}"); + Console.WriteLine($"SDL: v{sdlVersionString}"); + Console.WriteLine(GetFormattedPlatformString()); + Console.WriteLine($"Framework: {RuntimeInformation.FrameworkDescription}"); + } + + bool sdlSuccessfullyInitializedThisRun = false; + + try + { + var videoDriver = Environment.GetEnvironmentVariable("SDL_VIDEODRIVER"); + bool isHeadlessEnv = string.Equals(videoDriver, "dummy", StringComparison.OrdinalIgnoreCase) || + string.Equals(videoDriver, "offscreen", StringComparison.OrdinalIgnoreCase); + + if (isHeadlessEnv) + { + // Configure for a headless run. + Logger.Info($"Headless mode explicitly requested via SDL_VIDEODRIVER='{videoDriver}'."); + sdlLogOutputFunction = (userdata, category, priority, message) => { }; + SDL.SetLogOutputFunction(sdlLogOutputFunction, nint.Zero); + _ = SDL.SetHint(SDL.Hints.VideoDriver, videoDriver!); + _ = SDL.SetHint(SDL.Hints.RenderDriver, "software"); + LogManager.MinLevel = LogLevel.Debug; + } + else + { + // We are in a headed mode (either by default, or forced) + Logger.Info("Headed mode detected. Using default drivers."); + } + + lock (SdlLock) + { + if (!isSdlInitialized) + { + Logger.Debug("Global isSdlInitialized is false. Attempting SDL.Init()."); + initializedSubsystemsFlags = SDL.InitFlags.Video | SDL.InitFlags.Events | SDL.InitFlags.Joystick | SDL.InitFlags.Gamepad; + if (!SDL.Init(initializedSubsystemsFlags)) + { + string sdlError = SDL.GetError(); + Logger.Error($"SDL_Init failed: {sdlError}"); + + // Special handling for macOS headed mode when video init fails + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !isHeadlessEnv) + { + Logger.Info("macOS headed mode video init failed. Attempting workarounds..."); + + // Clean up and try with different configuration + SDL.Quit(); + + // Try removing all hints and letting SDL auto-detect + _ = SDL.SetHint(SDL.Hints.VideoDriver, string.Empty); + _ = SDL.SetHint(SDL.Hints.MacBackgroundApp, "0"); + + Logger.Debug("Retrying SDL.Init() with auto-detection for macOS headed mode."); + if (!SDL.Init(initializedSubsystemsFlags)) + { + string secondError = SDL.GetError(); + Logger.Error($"SDL_Init auto-detection also failed: {secondError}"); + Logger.Error("macOS manual testing requires:"); + Logger.Error("1. Screen Recording permission for your terminal/IDE in System Preferences"); + Logger.Error("2. Running from a GUI application with proper entitlements"); + Logger.Error("3. Consider running: SDL_VIDEODRIVER=dummy mise man-test (for headless testing)"); + return; + } + + Logger.Info("SDL.Init() successful with auto-detection on macOS."); + } + + // Only fallback when user explicitly requested headless mode but it failed + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && + isHeadlessEnv && + sdlError.Contains("No available video device")) + { + Logger.Info("macOS headless mode failed. Retrying with explicit dummy driver configuration."); + + // Clean up and retry with more explicit dummy driver setup + SDL.Quit(); + _ = SDL.SetHint(SDL.Hints.VideoDriver, "dummy"); + _ = SDL.SetHint(SDL.Hints.RenderDriver, "software"); + + Logger.Debug("Retrying SDL.Init() with explicit dummy driver configuration for macOS."); + if (!SDL.Init(initializedSubsystemsFlags)) + { + Logger.Error($"SDL_Init explicit dummy fallback also failed: {SDL.GetError()}"); + return; + } + + Logger.Info("SDL.Init() successful with explicit dummy driver on macOS."); + } + else + { + return; + } + } + else + { + Logger.Info("SDL.Init() successful."); + } + + // Now that SDL is initialized, we can check available drivers + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !isHeadlessEnv) + { + try + { + int numDrivers = SDL.GetNumVideoDrivers(); + Logger.Debug($"Available video drivers: {numDrivers}"); + for (int i = 0; i < numDrivers; i++) + { + string driver = SDL.GetVideoDriver(i); + Logger.Debug($" Driver {i}: {driver}"); + } + + string? currentDriver = SDL.GetCurrentVideoDriver(); + Logger.Info($"Successfully initialized with video driver: {currentDriver ?? "unknown"}"); + } + catch (Exception ex) + { + Logger.Warn($"Could not enumerate video drivers: {ex.Message}"); + } + } + + isSdlInitialized = true; + sdlSuccessfullyInitializedThisRun = true; + } + else + { + Logger.Debug("Global isSdlInitialized is true. Skipping SDL.Init()."); + } + } + + IsInputInitialized = (initializedSubsystemsFlags & SDL.InitFlags.Events) == SDL.InitFlags.Events; + Logger.Debug($"IsInputInitialized set to {IsInputInitialized}."); + + SDL.WindowFlags sdlFlags = 0; + if (windowConfig.Resizable) + { + sdlFlags |= SDL.WindowFlags.Resizable; + } + + if (windowConfig.Borderless) + { + sdlFlags |= SDL.WindowFlags.Borderless; + } + + if (windowConfig.HighDPI) + { + sdlFlags |= SDL.WindowFlags.HighPixelDensity; + } + + Logger.Debug($"Calling Window.SetMode with Width={windowConfig.Width}, Height={windowConfig.Height}, Flags={sdlFlags}"); + + bool modeSet = Window.SetMode(windowConfig.Width, windowConfig.Height, sdlFlags); + Logger.Debug($"Window.SetMode returned {modeSet}."); + + if (!modeSet) + { + Logger.Error($"Window.SetMode returned false. Window.Handle: {Window.Handle}, Window.IsOpen(): {Window.IsOpen()}. SDL Error: {SDL.GetError()}"); + return; + } + + Window.SetTitle(windowConfig.Title ?? "Night Game"); + Logger.Info($"Window title set to '{Window.GetMode().Title}'. IsOpen: {Window.IsOpen()}"); + + if (!Window.IsOpen()) + { + // This condition implies modeSet was true, but IsOpen is now false. + Logger.Warn($"Window.IsOpen() is false AFTER modeSet was true and title was set. Window.Handle: {Window.Handle}. SDL Error: {SDL.GetError()}"); + } + else + { + Logger.Debug($"Window.IsOpen() is true after SetMode and SetTitle. Window.Handle: {Window.Handle}"); + } + + if (windowConfig.Fullscreen) + { + Logger.Debug($"Attempting to set fullscreen: {windowConfig.FullscreenType}."); + FullscreenType fsType = windowConfig.FullscreenType.ToLowerInvariant() == "exclusive" + ? FullscreenType.Exclusive + : FullscreenType.Desktop; + if (!Window.SetFullscreen(true, fsType)) + { + Logger.Warn($"Failed to set initial fullscreen mode from configuration: {SDL.GetError()}"); + } + else + { + Logger.Debug($"SetFullscreen successful."); + } + } + + if (Window.RendererPtr != nint.Zero) + { + Logger.Debug($"Attempting to set VSync: {windowConfig.VSync}."); + if (!SDL.SetRenderVSync(Window.RendererPtr, windowConfig.VSync ? 1 : 0)) + { + Logger.Warn($"Failed to set initial VSync mode from configuration: {SDL.GetError()}"); + } + else + { + Logger.Debug($"SetRenderVSync successful."); + } + } + + if (windowConfig.X.HasValue && windowConfig.Y.HasValue && Window.Handle != nint.Zero) + { + Logger.Debug($"Setting window position to X={windowConfig.X.Value}, Y={windowConfig.Y.Value}."); + _ = SDL.SetWindowPosition(Window.Handle, windowConfig.X.Value, windowConfig.Y.Value); + } + + if (!string.IsNullOrEmpty(windowConfig.IconPath) && Window.Handle != nint.Zero) + { + string iconFullPath = windowConfig.IconPath; + if (!Path.IsPathRooted(iconFullPath)) + { + iconFullPath = Path.Combine(AppContext.BaseDirectory, iconFullPath); + } + + Logger.Debug($"Setting window icon from '{iconFullPath}'."); + if (!Window.SetIcon(iconFullPath)) + { + Logger.Warn($"Failed to set window icon from configuration: '{iconFullPath}'. Check path and image format."); + } + else + { + Logger.Debug($"Window icon set successfully."); + } + } + + Logger.Info($"Proceeding to game.Load(). Window.IsOpen(): {Window.IsOpen()}, Window.Handle: {Window.Handle}"); + try + { + game.Load(); + Logger.Info($"game.Load() completed. Window.IsOpen(): {Window.IsOpen()}, Window.Handle: {Window.Handle}"); + if (!Window.IsOpen()) + { + Logger.Error($"Window is NOT open after game.Load() completed. SDL Error: {SDL.GetError()}"); + } + } + catch (Exception e) + { + Logger.Error($"Exception during game.Load(): {e.Message}", e); + HandleGameException(e, game); + if (inErrorState) + { + return; + } + } + + if (!Window.IsOpen()) + { + Logger.Fatal($"CRITICAL CHECK - Window is not open after game.Load() for {game.GetType().FullName}. Exiting game loop early. SDL Error if relevant: {SDL.GetError()}"); + return; + } + + Logger.Info($"Starting main loop. Window.IsOpen(): {Window.IsOpen()}"); + Night.Timer.Initialize(); + frameCount = 0; + fpsTimeAccumulator = 0.0; + deltaHistory.Clear(); + var loopCount = 0; + + while (Window.IsOpen() && !inErrorState) + { + loopCount++; + double deltaTime = Night.Timer.Step(); + frameCount++; + fpsTimeAccumulator += deltaTime; + if (fpsTimeAccumulator >= 1.0) + { + Night.Timer.CurrentFPS = frameCount; + frameCount = 0; + fpsTimeAccumulator -= 1.0; + } + + deltaHistory.Add(deltaTime); + if (deltaHistory.Count > MaxDeltaHistorySamples) + { + deltaHistory.RemoveAt(0); + } + + if (deltaHistory.Count > 0) + { + Night.Timer.CurrentAverageDelta = deltaHistory.Average(); + } + + ProcessSdlEvents(game); // Call to the new method in Framework.Events.cs + + if (inErrorState) + { + break; + } + + if (!inErrorState) + { + try + { + Logger.Debug($"Loop {loopCount}: Calling game.Update()"); + game.Update((float)deltaTime); + Logger.Debug($"Loop {loopCount}: game.Update() returned"); + } + catch (Exception exUser) + { + HandleGameException(exUser, game); + if (inErrorState) + { + break; + } + } + } + + if (!inErrorState) + { + try + { + Logger.Debug($"Loop {loopCount}: Calling game.Draw() and Graphics.Present()"); + game.Draw(); + Night.Graphics.Present(); + Logger.Debug($"Loop {loopCount}: game.Draw() and Graphics.Present() returned"); + } + catch (Exception exUser) + { + HandleGameException(exUser, game); + if (inErrorState) + { + break; + } + } + } + } + + Logger.Info($"Main loop ended. Window.IsOpen(): {Window.IsOpen()}, inErrorState: {inErrorState}, LoopCount: {loopCount}"); + } + catch (Exception ex) + { + Logger.Fatal($"An UNEXPECTED FRAMEWORK error occurred: {ex.ToString()}", ex); + HandleGameException(ex, null); + } + finally + { + Logger.Debug($"Entering finally block. sdlSuccessfullyInitializedThisRun: {sdlSuccessfullyInitializedThisRun}, isSdlInitialized (static): {isSdlInitialized}"); + Window.Shutdown(); + Night.Joysticks.ClearJoysticks(); // Clear joystick resources + + lock (SdlLock) + { + if (sdlSuccessfullyInitializedThisRun) + { + Logger.Info($"SDL was initialized this run. Quitting SDL subsystems and SDL."); + if (initializedSubsystemsFlags != 0) + { + SDL.QuitSubSystem(initializedSubsystemsFlags); + Logger.Debug($"QuitSubSystem({initializedSubsystemsFlags}) called."); + initializedSubsystemsFlags = 0; + } + + SDL.Quit(); + Logger.Info($"SDL.Quit() called."); + isSdlInitialized = false; + } + else + { + Logger.Debug($"SDL was not initialized this run or Init failed. Skipping SDL.Quit(). Global isSdlInitialized: {isSdlInitialized}"); + } + } + + IsInputInitialized = false; + Logger.Debug($"Exiting finally block. IsInputInitialized: {IsInputInitialized}, isSdlInitialized (static): {isSdlInitialized}"); + } + } + } +} diff --git a/src/Night/Framework/Framework.cs b/src/Night/Framework/Framework.cs new file mode 100644 index 00000000..31862400 --- /dev/null +++ b/src/Night/Framework/Framework.cs @@ -0,0 +1,374 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; + +using Night; +using Night.Log; + +using SDL3; + +namespace Night +{ + /// + /// Manages the main game loop and coordination of game states. + /// Provides the main entry point to run a game. + /// + public static partial class Framework + { + /// + /// Gets the version of the Night Engine as a string. + /// + /// The version of the Night Engine as a string. + public static string GetVersion() + { + return VersionInfo.GetVersion(); + } + + private static void HandleGameException(Exception e, IGame? gameInstance) + { + Logger.Error($"HandleGameException: Error: {e.Message}", e); + inErrorState = true; + var customHandler = Night.Error.GetHandler(); + if (customHandler != null) + { + try + { + customHandler(e); + if (Window.IsOpen()) + { + Window.Close(); + } + } + catch (Exception exHandler) + { + Logger.Fatal($"CRITICAL: Exception in custom error handler: {exHandler.ToString()}", exHandler); + Logger.Error($"Original game error: {e.ToString()}", e); + if (Window.IsOpen()) + { + Window.Close(); + } + } + } + else + { + DefaultErrorHandler(e, gameInstance); + } + } + + private static void DefaultErrorHandler(Exception e, IGame? gameInstance) + { + Logger.Error("--- Night Engine: Default Error Handler ---"); + Logger.Error($"An error occurred in the game: {e.GetType().Name}", e); + Logger.Error($"Message: {e.Message}"); + Logger.Error("Stack Trace:"); + Logger.Error(e.StackTrace ?? "No stack trace available"); + Logger.Error("-------------------------------------------"); + + bool canDrawError = false; + try + { + if (!Window.IsOpen() || (Window.RendererPtr == nint.Zero)) + { + Logger.Warn("(DefaultErrorHandler): Window or Graphics not initialized. Attempting to set mode..."); + if (Window.SetMode(800, 600, SDL.WindowFlags.Resizable)) + { + Logger.Info("(DefaultErrorHandler): Window mode set to 800x600."); + canDrawError = Window.RendererPtr != nint.Zero; + } + else + { + Logger.Error($"(DefaultErrorHandler): Failed to set window mode. SDL Error: {SDL.GetError()}"); + } + } + else + { + canDrawError = true; + } + + if (IsInputInitialized) + { + Mouse.SetVisible(true); + Mouse.SetGrabbed(false); + Mouse.SetRelativeMode(false); + } + } + catch (Exception resetEx) + { + Logger.Error($"(DefaultErrorHandler): Exception during state reset: {resetEx.ToString()}", resetEx); + canDrawError = false; + } + + if (canDrawError) + { + try + { + string fullErrorText = $"Error: {e.Message}\n\n{e.StackTrace}"; + Window.SetTitle($"Error - {gameInstance?.GetType().Name ?? "Night Game"}"); + bool runningErrorLoop = true; + while (runningErrorLoop && Window.IsOpen()) + { + while (SDL.PollEvent(out SDL.Event ev)) + { + if (ev.Type == (uint)SDL.EventType.Quit) + { + runningErrorLoop = false; + Window.Close(); + break; + } + + if (ev.Type == (uint)SDL.EventType.KeyDown) + { + if (ev.Key.Key == SDL.Keycode.Escape) + { + runningErrorLoop = false; + Window.Close(); + break; + } + } + } + + if (!runningErrorLoop) + { + break; + } + + _ = SDL.SetRenderDrawColor(Window.RendererPtr, 30, 30, 30, 255); + _ = SDL.RenderClear(Window.RendererPtr); + _ = SDL.RenderPresent(Window.RendererPtr); + SDL.Delay(16); + } + } + catch (Exception drawEx) + { + Logger.Error($"(DefaultErrorHandler): Exception during error display loop: {drawEx.ToString()}", drawEx); + } + } + + if (Window.IsOpen()) + { + Window.Close(); + } + } + + private static string GetFormattedPlatformString() + { + string platformSpecificInfo; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + platformSpecificInfo = "Windows"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + global::System.Version osVersion = global::System.Environment.OSVersion.Version; // e.g., 15.4.1 + string versionString = osVersion.ToString(3); // Ensures Major.Minor.Patch format + + string? marketingName = GetMacOsMarketingName(osVersion.Major); + if (!string.IsNullOrEmpty(marketingName)) + { + platformSpecificInfo = $"macOS {marketingName} {versionString}"; + } + else + { + // Fallback if marketing name not found + platformSpecificInfo = $"macOS {versionString}"; + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + string? distroInfo = GetLinuxDistroInfoInternal(); + string? kernelVersion = GetLinuxKernelVersionInternal(); + var parts = new List(); + + if (!string.IsNullOrEmpty(distroInfo)) + { + parts.Add(distroInfo); + } + else + { + parts.Add("Linux"); // Fallback if distro info is not available + } + + if (!string.IsNullOrEmpty(kernelVersion)) + { + parts.Add($"(Kernel {kernelVersion})"); + } + + platformSpecificInfo = string.Join(" ", parts); // SA1513 for line 229 (closing 'if') + } + else + { + platformSpecificInfo = RuntimeInformation.OSDescription; + } + + return $"Platform: {platformSpecificInfo} {RuntimeInformation.OSArchitecture}"; + } + + private static string? GetMacOsMarketingName(int majorVersion) + { + // This list should be updated as new macOS versions are released. + return majorVersion switch + { + 15 => "Sequoia", + 14 => "Sonoma", + 13 => "Ventura", + 12 => "Monterey", + 11 => "Big Sur", + + // Older versions can be added if necessary + _ => null, // No specific marketing name known for this major version + }; + } + + private static string? GetLinuxDistroInfoInternal() + { + const string osReleasePath = "/etc/os-release"; + try + { + if (File.Exists(osReleasePath)) + { + var lines = File.ReadAllLines(osReleasePath); + string? prettyName = null; + string? name = null; + string? version = null; + + foreach (var line in lines) + { + if (line.StartsWith("PRETTY_NAME=", StringComparison.Ordinal)) + { + prettyName = line.Substring("PRETTY_NAME=".Length).Trim('"'); + break; // PRETTY_NAME is preferred + } + else if (line.StartsWith("NAME=", StringComparison.Ordinal)) + { + name = line.Substring("NAME=".Length).Trim('"'); + } + else if (line.StartsWith("VERSION=", StringComparison.Ordinal)) + { + version = line.Substring("VERSION=".Length).Trim('"'); + } + } + + if (!string.IsNullOrEmpty(prettyName)) + { + return prettyName; + } + else if (!string.IsNullOrEmpty(name)) + { + return string.IsNullOrEmpty(version) ? name : $"{name} {version}"; + } + + Logger.Debug($"Could not parse relevant fields (PRETTY_NAME, NAME, VERSION) from '{osReleasePath}'."); // SA1513 for line 302 (closing 'else if') + } + else + { + Logger.Debug($"Linux distribution information file '{osReleasePath}' not found."); + } + } + catch (IOException ex) + { + Logger.Warn($"IO error reading '{osReleasePath}': {ex.Message}"); + } + catch (UnauthorizedAccessException ex) + { + Logger.Warn($"Permission denied reading '{osReleasePath}': {ex.Message}"); + } + + // Catch-all for other unexpected errors // SA1108 + catch (Exception ex) + { + Logger.Warn($"Failed to read or parse '{osReleasePath}': {ex.Message}"); + } + + return null; + } + + private static string? GetLinuxKernelVersionInternal() + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "uname", + Arguments = "-r", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, // SA1413: Added trailing comma + }; // SA1108: Moved comment "// Capture errors as well" from previous line + + using (var process = Process.Start(startInfo)) + { + if (process == null) + { + Logger.Warn("Failed to start 'uname' process (Process.Start returned null)."); + return null; + } + + string kernelVersion = process.StandardOutput.ReadToEnd().Trim(); + string errorOutput = process.StandardError.ReadToEnd().Trim(); + process.WaitForExit(); + + if (process.ExitCode != 0) + { + Logger.Warn($"'uname -r' exited with code {process.ExitCode}. Error: '{errorOutput}'."); + return null; + } + + if (!string.IsNullOrEmpty(errorOutput) && string.IsNullOrEmpty(kernelVersion)) + { + Logger.Warn($"'uname -r' produced error output: '{errorOutput}'."); // IDE0055: Fixed indentation (15 -> 12) + } + + return string.IsNullOrEmpty(kernelVersion) ? null : kernelVersion; // SA1513 for line 361 (closing 'if') + } + } + + // Typically "file not found" or permission issues // SA1108 + catch (Win32Exception ex) + { + Logger.Warn($"Failed to execute 'uname -r'. Is 'uname' in PATH and executable? Error: {ex.Message}"); // IDE0055: Fixed indentation (10 -> 8) + } + + // e.g. if StandardOutput/Error not redirected // SA1108 + catch (InvalidOperationException ex) + { + Logger.Warn($"Invalid operation while trying to run 'uname -r': {ex.Message}"); // IDE0055: Fixed indentation (10 -> 8) + } + + // Catch other potential exceptions // SA1108 + catch (Exception ex) + { + Logger.Warn($"An unexpected error occurred while executing 'uname -r': {ex.Message}"); + } + + return null; + } + } +} diff --git a/src/Night/Game.cs b/src/Night/Game.cs new file mode 100644 index 00000000..f7b92e0f --- /dev/null +++ b/src/Night/Game.cs @@ -0,0 +1,327 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; + +using SDL3; + +namespace Night +{ + /// + /// Base class for a game that can be run by the Night Engine. + /// Game developers can inherit from this class and override specific methods + /// to implement their game logic, rather than implementing all methods from . + /// + public abstract class Game : IGame + { + /// + /// Called exactly once when the game starts for loading resources. + /// Override this method to load game-specific assets. + /// + public virtual void Load() + { + // Default implementation is empty. + } + + /// + /// Callback function used to update the state of the game every frame. + /// Override this method to implement game logic. + /// + /// The time elapsed since the last frame, in seconds. + public virtual void Update(double deltaTime) + { + // Default implementation is empty. + } + + /// + /// Callback function used to draw on the screen every frame. + /// Override this method to render game visuals. + /// + public virtual void Draw() + { + // Default implementation is empty. + } + + /// + /// Callback function triggered when a key is pressed. + /// Override this method to handle key press events. + /// + /// The logical key symbol that was pressed. + /// The physical key (scancode) that was pressed. + /// True if this is a key repeat event, false otherwise. + public virtual void KeyPressed(KeySymbol key, KeyCode scancode, bool isRepeat) + { + // Default implementation is empty. + } + + /// + /// Callback function triggered when a key is released. + /// Override this method to handle key release events. + /// + /// The logical key symbol that was released. + /// The physical key (scancode) that was released. + public virtual void KeyReleased(KeySymbol key, KeyCode scancode) + { + // Default implementation is empty. + } + + /// + /// Callback function triggered when a mouse button is pressed. + /// Override this method to handle mouse button press events. + /// + /// The x-coordinate of the mouse cursor relative to the window. + /// The y-coordinate of the mouse cursor relative to the window. + /// The mouse button that was pressed. + /// True if the event was generated by a touch input device, false otherwise. + /// The number of clicks (1 for single-click, 2 for double-click, etc.). + public virtual void MousePressed(int x, int y, MouseButton button, bool istouch, int presses) + { + // Default implementation is empty. + } + + /// + /// Callback function triggered when a mouse button is released. + /// Override this method to handle mouse button release events. + /// + /// The x-coordinate of the mouse cursor relative to the window. + /// The y-coordinate of the mouse cursor relative to the window. + /// The mouse button that was released. + /// True if the event was generated by a touch input device, false otherwise. + /// The number of clicks (typically 1 for release, but may vary). + public virtual void MouseReleased(int x, int y, MouseButton button, bool istouch, int presses) + { + // Default implementation is empty. + } + + /// + /// Callback function triggered when a joystick is connected. + /// Override this method to handle joystick connection events. + /// + /// The Joystick object representing the connected device. + public virtual void JoystickAdded(Joystick joystick) + { + // Default implementation is empty. + } + + /// + /// Callback function triggered when a joystick is disconnected. + /// Override this method to handle joystick disconnection events. + /// + /// The Joystick object representing the disconnected device. + public virtual void JoystickRemoved(Joystick joystick) + { + // Default implementation is empty. + } + + /// + /// Callback function triggered when a joystick axis moves. + /// Override this method to handle joystick axis motion events. + /// + /// The Joystick object. + /// The index of the axis that moved. + /// The new value of the axis, typically in the range -1.0 to 1.0. + public virtual void JoystickAxis(Joystick joystick, int axis, float value) + { + // Default implementation is empty. + } + + /// + /// Callback function triggered when a joystick button is pressed. + /// Override this method to handle joystick button press events. + /// + /// The Joystick object. + /// The index of the button that was pressed. + public virtual void JoystickPressed(Joystick joystick, int button) + { + // Default implementation is empty. + } + + /// + /// Callback function triggered when a joystick button is released. + /// Override this method to handle joystick button release events. + /// + /// The Joystick object. + /// The index of the button that was released. + public virtual void JoystickReleased(Joystick joystick, int button) + { + // Default implementation is empty. + } + + /// + /// Callback function triggered when a joystick hat direction changes. + /// Override this method to handle joystick hat motion events. + /// + /// The Joystick object. + /// The index of the hat that changed. + /// The new direction of the hat. + public virtual void JoystickHat(Joystick joystick, int hat, JoystickHat direction) + { + // Default implementation is empty. + } + + /// + /// Callback function triggered when a virtual gamepad axis is moved. + /// Override this method to handle gamepad axis motion events. + /// + /// The Joystick object (which is also a gamepad). + /// The virtual gamepad axis that moved. + /// The new value of the axis, typically in the range -1.0 to 1.0. + public virtual void GamepadAxis(Joystick joystick, GamepadAxis axis, float value) + { + // Default implementation is empty. + } + + /// + /// Callback function triggered when a virtual gamepad button is pressed. + /// Override this method to handle gamepad button press events. + /// + /// The Joystick object (which is also a gamepad). + /// The virtual gamepad button that was pressed. + public virtual void GamepadPressed(Joystick joystick, GamepadButton button) + { + // Default implementation is empty. + } + + /// + /// Callback function triggered when a virtual gamepad button is released. + /// Override this method to handle gamepad button release events. + /// + /// The Joystick object (which is also a gamepad). + /// The virtual gamepad button that was released. + public virtual void GamepadReleased(Joystick joystick, GamepadButton button) + { + // Default implementation is empty. + } + + /// + /// Callback function triggered when a file is dropped onto the window. + /// Override this method to handle file drop events. + /// + /// The file that was dropped. + public virtual void FileDropped(DroppedFile file) + { + // Default implementation is empty. + } + + /// + /// Provides the main game loop iteration logic as a callable function. + /// This default implementation mirrors LÖVE's `love.run()` behavior for a single frame, + /// returning a function that, when called, executes one iteration of the game loop. + /// + /// + /// A function (mainLoopIteration) which handles one frame, including event processing, + /// updates, drawing, and timing. The returned function returns: + /// - null: To indicate the game loop should continue. + /// - int value (e.g., 0): To indicate the game should exit with the specified status code. + /// + public virtual Func Run() + { + // This lambda represents one iteration of the main game loop. + return () => + { + // Process SDL events + SDL.Event sdlEvent; + while (SDL.PollEvent(out sdlEvent)) + { + var eventType = (SDL.EventType)sdlEvent.Type; + switch (eventType) + { + case SDL.EventType.Quit: + // If Quit() returns true, it means allow the game to close. + if (this.Quit()) + { + // Signal to exit the game loop with status 0. + return 0; + } + + // If Quit() returns false, the quit attempt is cancelled; continue processing. + break; + case SDL.EventType.KeyDown: + this.KeyPressed((KeySymbol)sdlEvent.Key.Key, (KeyCode)sdlEvent.Key.Scancode, sdlEvent.Key.Repeat); + break; + case SDL.EventType.KeyUp: + this.KeyReleased((KeySymbol)sdlEvent.Key.Key, (KeyCode)sdlEvent.Key.Scancode); + break; + case SDL.EventType.MouseButtonDown: + this.MousePressed( + (int)sdlEvent.Button.X, + (int)sdlEvent.Button.Y, + (MouseButton)sdlEvent.Button.Button, + sdlEvent.Button.Which == SDL.TouchMouseID, + sdlEvent.Button.Clicks); + break; + case SDL.EventType.MouseButtonUp: + this.MouseReleased( + (int)sdlEvent.Button.X, + (int)sdlEvent.Button.Y, + (MouseButton)sdlEvent.Button.Button, + sdlEvent.Button.Which == SDL.TouchMouseID, + sdlEvent.Button.Clicks); + break; + + // TODO: Implement full joystick and gamepad event handling here + // similar to Framework.Events.cs if this Run() model is adopted. + // e.g., JoystickAdded, JoystickRemoved, JoystickAxis, JoystickButton, etc. + // e.g., GamepadAxis, GamepadButton, etc. + } + } + + // Update delta time + double dt = Night.Timer.Step(); + + // Call user's update logic + this.Update(dt); + + // Draw graphics + // Only draw if the window is actually open + if (Night.Window.IsOpen()) + { + // Assuming Night.Graphics.GetBackgroundColor() and Night.Graphics.Clear(Color) exist + // based on available test file names (GraphicsBackgroundColorTests, GraphicsClearTest). + Night.Graphics.Clear(Night.Graphics.GetBackgroundColor()); + this.Draw(); + Night.Graphics.Present(); + } + + // Brief sleep, as in LÖVE's example + Night.Timer.Sleep(0.001); + + // Signal to continue the game loop. + return null; + }; + } + + /// + /// Callback function triggered when the game is about to close. + /// The default implementation allows the game to quit. + /// + /// + /// False to cancel the quit attempt (and continue running the game), + /// true to allow the game to close. + /// + public virtual bool Quit() + { + // Default behavior: allow the game to quit. + return true; + } + } +} diff --git a/src/Night/Graphics/Graphics.State.cs b/src/Night/Graphics/Graphics.State.cs new file mode 100644 index 00000000..7e1a4ac6 --- /dev/null +++ b/src/Night/Graphics/Graphics.State.cs @@ -0,0 +1,45 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +namespace Night +{ + /// + /// Provides 2D graphics rendering functionality. This part handles graphics state. + /// + public static partial class Graphics + { + private static Color backgroundColor = Color.Black; // Default to black (0,0,0,255) + + /// + /// Gets the current background color. + /// + /// The current background . + /// + /// This reflects the color set by the last call to , + /// or the default color if Clear hasn't been called. + /// + public static Color GetBackgroundColor() + { + return backgroundColor; + } + } +} diff --git a/src/Night/Graphics/Graphics.cs b/src/Night/Graphics/Graphics.cs index 5a7dabae..6f61b61a 100644 --- a/src/Night/Graphics/Graphics.cs +++ b/src/Night/Graphics/Graphics.cs @@ -25,6 +25,7 @@ using System.Runtime.InteropServices; using Night; +using Night.Log; using SDL3; @@ -33,8 +34,10 @@ namespace Night /// /// Provides 2D graphics rendering functionality. /// - public static class Graphics + public static partial class Graphics { + private static readonly ILogger Logger = LogManager.GetLogger("Night.Graphics.Graphics"); + /// Loads an image file and creates a new Sprite. /// Path to the image file. /// A new Sprite or null if loading fails. @@ -43,13 +46,13 @@ public static class Graphics IntPtr rendererPtr = Window.RendererPtr; if (rendererPtr == IntPtr.Zero) { - Console.WriteLine("Error in Graphics.NewImage: Renderer pointer is null. Was Window.SetMode called successfully?"); + Logger.Error("Renderer pointer is null. Was Window.SetMode called successfully?"); return null; } if (!File.Exists(filePath)) { - Console.WriteLine($"Error in Graphics.NewImage: Image file not found at '{filePath}'."); + Logger.Error($"Image file not found at '{filePath}'."); return null; } @@ -58,7 +61,7 @@ public static class Graphics if (surfacePtr == IntPtr.Zero) { string sdlError = SDL.GetError(); - Console.WriteLine($"Error in Graphics.NewImage: Failed to load image into surface from '{filePath}'. SDL_image Error: {sdlError}"); + Logger.Error($"Failed to load image into surface from '{filePath}'. SDL_image Error: {sdlError}"); return null; } @@ -68,7 +71,7 @@ public static class Graphics if (width <= 0 || height <= 0) { - Console.WriteLine($"Error: Invalid surface dimensions ({width}x{height}) for '{filePath}'."); + Logger.Error($"Invalid surface dimensions ({width}x{height}) for '{filePath}'."); SDL.DestroySurface(surfacePtr); return null; } @@ -79,7 +82,7 @@ public static class Graphics if (texturePtr == IntPtr.Zero) { string sdlError = SDL.GetError(); - Console.WriteLine($"Error in Graphics.NewImage: Failed to create texture from surface for '{filePath}'. SDL Error: {sdlError}"); + Logger.Error($"Failed to create texture from surface for '{filePath}'. SDL Error: {sdlError}"); return null; } @@ -93,14 +96,14 @@ public static void SetColor(Color color) IntPtr rendererPtr = Window.RendererPtr; if (rendererPtr == IntPtr.Zero) { - Console.WriteLine("Error in Graphics.SetColor: Renderer pointer is null. Was Window.SetMode called successfully?"); + Logger.Error("Renderer pointer is null. Was Window.SetMode called successfully?"); return; } if (!SDL.SetRenderDrawColor(rendererPtr, color.R, color.G, color.B, color.A)) { string sdlError = SDL.GetError(); - Console.WriteLine($"Error in Graphics.SetColor (SetRenderDrawColor): {sdlError}"); + Logger.Error($"SetRenderDrawColor failed: {sdlError}"); } } @@ -125,7 +128,7 @@ public static void Rectangle(DrawMode mode, float x, float y, float width, float IntPtr rendererPtr = Window.RendererPtr; if (rendererPtr == IntPtr.Zero) { - Console.WriteLine("Error in Graphics.Rectangle: Renderer pointer is null. Was Window.SetMode called successfully?"); + Logger.Error("Renderer pointer is null. Was Window.SetMode called successfully?"); return; } @@ -219,7 +222,7 @@ public static void Rectangle(DrawMode mode, float x, float y, float width, float if (!success) { string sdlError = SDL.GetError(); - Console.WriteLine($"Error in Graphics.Rectangle (Mode: {mode}): {sdlError}"); + Logger.Error($"Rectangle rendering failed (Mode: {mode}): {sdlError}"); } } @@ -235,14 +238,14 @@ public static void Line(float x1, float y1, float x2, float y2) IntPtr rendererPtr = Window.RendererPtr; if (rendererPtr == IntPtr.Zero) { - Console.WriteLine("Error in Graphics.Line: Renderer pointer is null. Was Window.SetMode called successfully?"); + Logger.Error("Renderer pointer is null. Was Window.SetMode called successfully?"); return; } if (!SDL.RenderLine(rendererPtr, x1, y1, x2, y2)) { string sdlError = SDL.GetError(); - Console.WriteLine($"Error in Graphics.Line: {sdlError}"); + Logger.Error($"Line rendering failed: {sdlError}"); } } @@ -255,13 +258,13 @@ public static void Line(PointF[] points) IntPtr rendererPtr = Window.RendererPtr; if (rendererPtr == IntPtr.Zero) { - Console.WriteLine("Error in Graphics.Line (multiple points): Renderer pointer is null. Was Window.SetMode called successfully?"); + Logger.Error("Renderer pointer is null. Was Window.SetMode called successfully?"); return; } if (points == null || points.Length < 2) { - Console.WriteLine("Error in Graphics.Line (multiple points): At least two points are required to draw lines."); + Logger.Error("At least two points are required to draw lines."); return; } @@ -274,7 +277,7 @@ public static void Line(PointF[] points) if (!SDL.RenderLines(rendererPtr, sdlPoints, sdlPoints.Length)) { string sdlError = SDL.GetError(); - Console.WriteLine($"Error in Graphics.Line (multiple points): {sdlError}"); + Logger.Error($"Multiple points line rendering failed: {sdlError}"); } } @@ -288,13 +291,13 @@ public static void Polygon(DrawMode mode, PointF[] vertices) IntPtr rendererPtr = Window.RendererPtr; if (rendererPtr == IntPtr.Zero) { - Console.WriteLine("Error in Graphics.Polygon: Renderer pointer is null. Was Window.SetMode called successfully?"); + Logger.Error("Renderer pointer is null. Was Window.SetMode called successfully?"); return; } if (vertices == null || vertices.Length < 3) { - Console.WriteLine("Error in Graphics.Polygon: At least three vertices are required to draw a polygon."); + Logger.Error("At least three vertices are required to draw a polygon."); return; } @@ -312,7 +315,7 @@ public static void Polygon(DrawMode mode, PointF[] vertices) if (!SDL.RenderLines(rendererPtr, lineVertices, lineVertices.Length)) { string sdlError = SDL.GetError(); - Console.WriteLine($"Error in Graphics.Polygon (Line Mode): {sdlError}"); + Logger.Error($"Polygon rendering failed (Line Mode): {sdlError}"); } } else @@ -373,7 +376,7 @@ public static void Polygon(DrawMode mode, PointF[] vertices) sizeof(byte))) { string sdlError = SDL.GetError(); - Console.WriteLine($"Error in Graphics.Polygon (Fill Mode - RenderGeometryRaw): {sdlError}"); + Logger.Error($"Polygon rendering failed (Fill Mode - RenderGeometryRaw): {sdlError}"); } } finally @@ -414,7 +417,7 @@ public static void Circle( IntPtr rendererPtr = Window.RendererPtr; if (rendererPtr == IntPtr.Zero) { - Console.WriteLine("Error in Graphics.Circle: Renderer pointer is null. Was Window.SetMode called successfully?"); + Logger.Error("Renderer pointer is null. Was Window.SetMode called successfully?"); return; } @@ -464,7 +467,7 @@ public static void Circle( if (!success) { string sdlError = SDL.GetError(); - Console.WriteLine($"Error in Graphics.Circle (Mode: {mode}): {sdlError}"); + Logger.Error($"Circle rendering failed (Mode: {mode}): {sdlError}"); } } @@ -495,17 +498,16 @@ public static void Draw( return; } - // Check if sprite texture is null if (sprite.Texture == IntPtr.Zero) { - Console.WriteLine("Error in Graphics.Draw: Sprite or sprite texture is null."); + Logger.Error("Sprite or sprite texture is null."); return; } IntPtr rendererPtr = Window.RendererPtr; if (rendererPtr == IntPtr.Zero) { - Console.WriteLine("Error in Graphics.Draw: Renderer pointer is null. Was Window.SetMode called successfully?"); + Logger.Error("Renderer pointer is null. Was Window.SetMode called successfully?"); return; } @@ -539,7 +541,7 @@ public static void Draw( if (!SDL.RenderTextureRotated(rendererPtr, sprite.Texture, IntPtr.Zero, dstRectPtr, angleInDegrees, centerPointPtr, SDL.FlipMode.None)) { string sdlError = SDL.GetError(); - Console.WriteLine($"Error in Graphics.Draw (RenderTextureRotated): {sdlError}"); + Logger.Error($"RenderTextureRotated failed: {sdlError}"); } } finally @@ -565,22 +567,24 @@ public static void Clear(Color color) IntPtr rendererPtr = Window.RendererPtr; if (rendererPtr == IntPtr.Zero) { - Console.WriteLine("Error in Graphics.Clear: Renderer pointer is null. Was Window.SetMode called successfully?"); + Logger.Error("Renderer pointer is null. Was Window.SetMode called successfully?"); return; } + backgroundColor = color; // Store the new background color + // Set color for clearing if (!SDL.SetRenderDrawColor(rendererPtr, color.R, color.G, color.B, color.A)) { string sdlError = SDL.GetError(); - Console.WriteLine($"Error in Graphics.Clear (SetRenderDrawColor): {sdlError}"); + Logger.Error($"SetRenderDrawColor failed: {sdlError}"); return; // Return if color setting fails, to avoid clearing with wrong color } if (!SDL.RenderClear(rendererPtr)) { string sdlError = SDL.GetError(); - Console.WriteLine($"Error in Graphics.Clear (RenderClear): {sdlError}"); + Logger.Error($"RenderClear failed: {sdlError}"); } } @@ -592,12 +596,10 @@ public static void Present() IntPtr rendererPtr = Window.RendererPtr; if (rendererPtr == IntPtr.Zero) { - Console.WriteLine("Error in Graphics.Present: Renderer pointer is null. Was Window.SetMode called successfully?"); + Logger.Error("Renderer pointer is null. Was Window.SetMode called successfully?"); return; } - // Set color for drawing (though Present itself doesn't draw, good practice if any last-minute things were to be added here) - // SDL.SetRenderDrawColor( Night.Window.RendererPtr, currentColor.R, currentColor.G, currentColor.B, currentColor.A ); _ = SDL.RenderPresent(Window.RendererPtr); } } diff --git a/src/Night/IGame.cs b/src/Night/IGame.cs index 25da0aa6..7353fb46 100644 --- a/src/Night/IGame.cs +++ b/src/Night/IGame.cs @@ -59,9 +59,7 @@ public interface IGame /// /// The logical key symbol that was released. /// The physical key (scancode) that was released. - void KeyReleased(KeySymbol key, KeyCode scancode) - { /* Optional: Default empty implementation */ - } + void KeyReleased(KeySymbol key, KeyCode scancode); /// /// Callback function triggered when a mouse button is pressed. @@ -71,9 +69,7 @@ void KeyReleased(KeySymbol key, KeyCode scancode) /// The mouse button that was pressed. /// True if the event was generated by a touch input device, false otherwise. /// The number of clicks (1 for single-click, 2 for double-click, etc.). - void MousePressed(int x, int y, MouseButton button, bool istouch, int presses) - { /* Optional: Default empty implementation */ - } + void MousePressed(int x, int y, MouseButton button, bool istouch, int presses); /// /// Callback function triggered when a mouse button is released. @@ -83,8 +79,98 @@ void MousePressed(int x, int y, MouseButton button, bool istouch, int presses) /// The mouse button that was released. /// True if the event was generated by a touch input device, false otherwise. /// The number of clicks (typically 1 for release, but may vary). - void MouseReleased(int x, int y, MouseButton button, bool istouch, int presses) - { /* Optional: Default empty implementation */ - } + void MouseReleased(int x, int y, MouseButton button, bool istouch, int presses); + + /// + /// Callback function triggered when a joystick is connected. + /// + /// The Joystick object representing the connected device. + void JoystickAdded(Joystick joystick); + + /// + /// Callback function triggered when a joystick is disconnected. + /// + /// The Joystick object representing the disconnected device. + void JoystickRemoved(Joystick joystick); + + /// + /// Callback function triggered when a joystick axis moves. + /// + /// The Joystick object. + /// The index of the axis that moved. + /// The new value of the axis, typically in the range -1.0 to 1.0. + void JoystickAxis(Joystick joystick, int axis, float value); + + /// + /// Callback function triggered when a joystick button is pressed. + /// + /// The Joystick object. + /// The index of the button that was pressed. + void JoystickPressed(Joystick joystick, int button); + + /// + /// Callback function triggered when a joystick button is released. + /// + /// The Joystick object. + /// The index of the button that was released. + void JoystickReleased(Joystick joystick, int button); + + /// + /// Callback function triggered when a joystick hat direction changes. + /// + /// The Joystick object. + /// The index of the hat that changed. + /// The new direction of the hat. + void JoystickHat(Joystick joystick, int hat, JoystickHat direction); + + /// + /// Callback function triggered when a virtual gamepad axis is moved. + /// + /// The Joystick object (which is also a gamepad). + /// The virtual gamepad axis that moved. + /// The new value of the axis, typically in the range -1.0 to 1.0. + void GamepadAxis(Joystick joystick, GamepadAxis axis, float value); + + /// + /// Callback function triggered when a virtual gamepad button is pressed. + /// + /// The Joystick object (which is also a gamepad). + /// The virtual gamepad button that was pressed. + void GamepadPressed(Joystick joystick, GamepadButton button); + + /// + /// Callback function triggered when a virtual gamepad button is released. + /// + /// The Joystick object (which is also a gamepad). + /// The virtual gamepad button that was released. + void GamepadReleased(Joystick joystick, GamepadButton button); + + /// + /// Callback function triggered when a file is dropped onto the window. + /// + /// The file that was dropped. + void FileDropped(DroppedFile file); + + /// + /// The main callback function, containing the main loop logic. + /// A sensible default is used when not overridden. + /// This function, when obtained, should be called repeatedly by the engine's main loop. + /// + /// + /// A function (mainLoopIteration) which handles one frame, including events and rendering, when called. + /// The returned mainLoopIteration function returns an optional integer: + /// - null: Continue the game loop. + /// - int value: Exit the game loop with the specified status code. + /// + Func Run(); + + /// + /// Callback function triggered when the game is about to close. + /// + /// + /// False to cancel the quit attempt (and continue running the game), + /// true to allow the game to close. + /// + bool Quit(); } } diff --git a/src/Night/Joysticks/Joystick.cs b/src/Night/Joysticks/Joystick.cs new file mode 100644 index 00000000..b70380f5 --- /dev/null +++ b/src/Night/Joysticks/Joystick.cs @@ -0,0 +1,821 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + +using SDL3; + +namespace Night +{ + /// + /// Virtual gamepad axes. + /// + public enum GamepadAxis + { + /// + /// The horizontal axis of the left analog stick. + /// + LeftX, + + /// + /// The vertical axis of the left analog stick. + /// + LeftY, + + /// + /// The horizontal axis of the right analog stick. + /// + RightX, + + /// + /// The vertical axis of the right analog stick. + /// + RightY, + + /// + /// The left trigger axis. + /// + TriggerLeft, + + /// + /// The right trigger axis. + /// + TriggerRight, + } + + /// + /// Virtual gamepad buttons. + /// + public enum GamepadButton + { + /// + /// The 'A' button (often cross or bottom face button, SDL_GAMEPAD_BUTTON_SOUTH). + /// + A, + + /// + /// The 'B' button (often circle or right face button, SDL_GAMEPAD_BUTTON_EAST). + /// + B, + + /// + /// The 'X' button (often square or left face button, SDL_GAMEPAD_BUTTON_WEST). + /// + X, + + /// + /// The 'Y' button (often triangle or top face button, SDL_GAMEPAD_BUTTON_NORTH). + /// + Y, + + // Aliases for face buttons + + /// + /// Alias for the 'A' button (South face button). + /// + South = A, + + /// + /// Alias for the 'B' button (East face button). + /// + East = B, + + /// + /// Alias for the 'X' button (West face button). + /// + West = X, + + /// + /// Alias for the 'Y' button (North face button). + /// + North = Y, + + /// + /// The 'Back' or 'Select' button. + /// + Back, + + /// + /// The 'Guide' or 'Home' button (e.g., Xbox button). + /// + Guide, + + /// + /// The 'Start' button. + /// + Start, + + /// + /// The D-pad left button. + /// + DPLeft, + + /// + /// The D-pad right button. + /// + DPRight, + + /// + /// The D-pad up button. + /// + DPUp, + + /// + /// The D-pad down button. + /// + DPDown, + + /// + /// The left shoulder button (bumper). + /// + LeftShoulder, + + /// + /// The right shoulder button (bumper). + /// + RightShoulder, + + /// + /// The left analog stick click button. + /// + LeftStick, + + /// + /// The right analog stick click button. + /// + RightStick, + } + + /// + /// Joystick hat positions. + /// + public enum JoystickHat : byte + { + /// + /// Hat is centered. (SDL_HAT_CENTERED). + /// + Centered = 0x00, + + /// + /// Hat is pressed up. (SDL_HAT_UP). + /// + Up = 0x01, + + /// + /// Hat is pressed right. (SDL_HAT_RIGHT). + /// + Right = 0x02, + + /// + /// Hat is pressed down. (SDL_HAT_DOWN). + /// + Down = 0x04, + + /// + /// Hat is pressed left. (SDL_HAT_LEFT). + /// + Left = 0x08, + + /// + /// Hat is pressed up and right. (SDL_HAT_RIGHTUP). + /// + RightUp = Right | Up, // 0x03 + + /// + /// Hat is pressed down and right. (SDL_HAT_RIGHTDOWN). + /// + RightDown = Right | Down, // 0x06 + + /// + /// Hat is pressed up and left. (SDL_HAT_LEFTUP). + /// + LeftUp = Left | Up, // 0x09 + + /// + /// Hat is pressed down and left. (SDL_HAT_LEFTDOWN). + /// + LeftDown = Left | Down, // 0x0C + } + + /// + /// Types of Joystick inputs. + /// + public enum JoystickInputType + { + /// + /// The input is an axis. + /// + Axis, + + /// + /// The input is a button. + /// + Button, + + /// + /// The input is a hat. + /// + Hat, + } + + /// + /// OS-independent device info of the joystick. + /// + public struct DeviceInfo + { + /// + /// The vendor ID of the joystick. + /// + public int Vendor; + + /// + /// The product ID of the joystick. + /// + public int Product; + + /// + /// The version number of the joystick. + /// + public int Version; + + /// + /// The GUID of the joystick. + /// + public string Guid; + } + + /// + /// Represents the vibration motor strengths. + /// + public struct VibrationStrength + { + /// + /// The strength of the left vibration motor (0-1). + /// + public float Left; + + /// + /// The strength of the right vibration motor (0-1). + /// + public float Right; + } + + /// + /// Represents the result of a gamepad mapping query. + /// + public struct GamepadMappingResult + { + /// + /// Indicates whether a valid mapping exists. + /// + public bool IsValid; + + /// + /// The type of the input (axis, button, or hat). + /// + public JoystickInputType Type; + + /// + /// The index of the input (e.g., axis index, button index). + /// + public int InputIndex; + + /// + /// The hat value, only relevant if is . + /// + public JoystickHat HatValue; + } + + /// + /// Represents a physical joystick. + /// + public class Joystick : IDisposable + { + private readonly uint instanceId; + private readonly IntPtr joystickDevicePtr; + private IntPtr gamepadDevicePtr = IntPtr.Zero; + private bool disposed = false; + private bool isConnected = true; // Assume connected on creation, updated by events + + /// + /// Initializes a new instance of the class. + /// Joystick instances are typically obtained via methods in the Night.Joysticks.Joysticks class. + /// + /// The SDL instance ID of the joystick. + /// Thrown if the joystick cannot be opened. + internal Joystick(uint instanceId) + { + this.instanceId = instanceId; + this.joystickDevicePtr = SDL.OpenJoystick(instanceId); + if (this.joystickDevicePtr == IntPtr.Zero) + { + throw new InvalidOperationException($"Failed to open joystick with ID {instanceId}: {SDL.GetError()}"); + } + + if (SDL.IsGamepad(instanceId)) + { + this.gamepadDevicePtr = SDL.OpenGamepad(instanceId); + if (this.gamepadDevicePtr == IntPtr.Zero) + { + // Log warning, but don't fail construction. It's still a valid joystick. + // Night.Log.LogManager.GetLogger("Joystick").Warn($"Joystick {instanceId} is a gamepad, but failed to open as gamepad: {SDL.GetError()}"); + } + } + } + + /// + /// Finalizes an instance of the class. + /// + ~Joystick() + { + this.Dispose(false); + } + + /// + /// Gets the SDL instance ID of this joystick. + /// + internal uint InstanceId => this.instanceId; + + /// + /// Gets the button, axis or hat that a virtual gamepad axis is bound to. + /// (This is a complex LÖVE feature and is currently not fully implemented.) + /// + /// The virtual gamepad axis to check. + /// A GamepadMappingResult indicating the physical input. Currently returns IsValid = false. + public static GamepadMappingResult GetGamepadMapping(GamepadAxis axis) + { + _ = axis; // Parameter is required for overload and future implementation. + + // SDL_GetGamepadBindings might be useful here but is complex to parse. + // For now, returning not valid. + return new GamepadMappingResult { IsValid = false }; + } + + /// + /// Gets the button, axis or hat that a virtual gamepad button is bound to. + /// (This is a complex LÖVE feature and is currently not fully implemented.) + /// + /// The virtual gamepad button to check. + /// A GamepadMappingResult indicating the physical input. Currently returns IsValid = false. + public static GamepadMappingResult GetGamepadMapping(GamepadButton button) + { + _ = button; // Parameter is required for overload and future implementation. + + // SDL_GetGamepadBindings might be useful here but is complex to parse. + // For now, returning not valid. + return new GamepadMappingResult { IsValid = false }; + } + + /// + /// Gets the direction of each axis. + /// + /// An array of floats, one for each axis direction, or an empty array if disposed or an error occurs. + public float[] GetAxes() + { + if (this.disposed) + { + return Array.Empty(); + } + + int axisCount = SDL.GetNumJoystickAxes(this.joystickDevicePtr); + if (axisCount < 0) + { + return Array.Empty(); + } + + float[] axes = new float[axisCount]; + for (int i = 0; i < axisCount; i++) + { + axes[i] = this.GetAxis(i); + } + + return axes; + } + + /// + /// Gets the direction of an axis. + /// + /// The index of the axis. + /// The direction of the specified axis (-1.0 to 1.0), or 0.0f if disposed. + public float GetAxis(int axisIndex) + { + if (this.disposed) + { + return 0.0f; + } + + short rawValue = SDL.GetJoystickAxis(this.joystickDevicePtr, axisIndex); + return NormalizeAxisValue(rawValue); + } + + /// + /// Gets the number of axes on the joystick. + /// + /// The number of axes, or 0 if disposed or an error occurs. + public int GetAxisCount() + { + if (this.disposed) + { + return 0; + } + + int count = SDL.GetNumJoystickAxes(this.joystickDevicePtr); + return count < 0 ? 0 : count; + } + + /// + /// Gets the number of buttons on the joystick. + /// + /// The number of buttons, or 0 if disposed or an error occurs. + public int GetButtonCount() + { + if (this.disposed) + { + return 0; + } + + int count = SDL.GetNumJoystickButtons(this.joystickDevicePtr); + return count < 0 ? 0 : count; + } + + /// + /// Gets the OS-independent device info of the joystick. + /// + /// The device information. + public DeviceInfo GetDeviceInfo() + { + if (this.disposed) + { + return new DeviceInfo { Guid = string.Empty }; + } + + SDL.GUID sdlGuid = SDL.GetJoystickGUIDForID(this.instanceId); + SDL.GetJoystickGUIDInfo(sdlGuid, out short vendor, out short product, out short version, out _); + byte[] guidBuffer = new byte[37]; // SDL_GUID_STRING_LENGTH is 36 + null terminator + SDL.GUIDToString(sdlGuid, ref guidBuffer, guidBuffer.Length); + string guidString = Encoding.UTF8.GetString(guidBuffer).Split('\0')[0]; + + return new DeviceInfo + { + Vendor = vendor, + Product = product, + Version = version, + Guid = guidString, + }; + } + + /// + /// Gets a stable GUID unique to the type of the physical joystick. + /// + /// The joystick's GUID as a string, or an empty string if disposed. + public string GetGuid() + { + if (this.disposed) + { + return string.Empty; + } + + SDL.GUID sdlGuid = SDL.GetJoystickGUIDForID(this.instanceId); + byte[] guidBuffer = new byte[37]; // SDL_GUID_STRING_LENGTH is 36 + null terminator + SDL.GUIDToString(sdlGuid, ref guidBuffer, guidBuffer.Length); + return Encoding.UTF8.GetString(guidBuffer).Split('\0')[0]; + } + + /// + /// Gets the direction of a virtual gamepad axis. + /// + /// The virtual gamepad axis. + /// The direction of the gamepad axis (-1.0 to 1.0), or 0.0f if not a gamepad or disposed. + public float GetGamepadAxis(GamepadAxis axis) + { + if (this.disposed || this.gamepadDevicePtr == IntPtr.Zero) + { + return 0.0f; + } + + SDL.GamepadAxis sdlAxis = MapNightGamepadAxisToSdl(axis); + short rawValue = SDL.GetGamepadAxis(this.gamepadDevicePtr, sdlAxis); + return NormalizeAxisValue(rawValue); + } + + /// + /// Gets the full gamepad mapping string of this Joystick, if it's recognized as a gamepad. + /// + /// The gamepad mapping string, or null if not a gamepad or disposed. + public string? GetGamepadMappingString() + { + if (this.disposed || this.gamepadDevicePtr == IntPtr.Zero) + { + return null; + } + + // SDL.GetJoystickMappingForID or similar not found in current bindings. + return null; + } + + /// + /// Gets the direction of a hat. + /// + /// The index of the hat. + /// The direction of the specified hat, or JoystickHat.Centered if disposed or an error occurs. + public JoystickHat GetHat(int hatIndex) + { + if (this.disposed) + { + return JoystickHat.Centered; + } + + // SDL.GetJoystickHat returns an enum like SDL.JoystickHat or SDL.Hat + // We need to cast it to byte to match our enum definition based on SDL_HAT_* values + byte sdlHatValue = (byte)SDL.GetJoystickHat(this.joystickDevicePtr, hatIndex); + return (JoystickHat)sdlHatValue; + } + + /// + /// Gets the number of hats on the joystick. + /// + /// The number of hats, or 0 if disposed or an error occurs. + public int GetHatCount() + { + if (this.disposed) + { + return 0; + } + + int count = SDL.GetNumJoystickHats(this.joystickDevicePtr); + return count < 0 ? 0 : count; + } + + /// + /// Gets the joystick's unique instance identifier. + /// + /// The joystick's ID (matches SDL_JoystickID). + public uint GetId() + { + return this.instanceId; + } + + /// + /// Gets the name of the joystick. + /// + /// The joystick's name, or an empty string if disposed. + public string GetName() + { + if (this.disposed) + { + return string.Empty; + } + + return SDL.GetJoystickName(this.joystickDevicePtr) ?? string.Empty; + } + + /// + /// Gets the current vibration motor strengths on a Joystick with rumble support. + /// + /// The current vibration strengths (0-1). Returns (0,0) if not supported or disposed. + public VibrationStrength GetVibration() + { + if (this.disposed || !this.IsVibrationSupported()) + { + return new VibrationStrength { Left = 0f, Right = 0f }; + } + + // SDL_GetJoystickRumble is for current state, not supported in SDL3-CS directly for get. + // LÖVE's getVibration implies it knows the last set state. We'll have to store it. + // For now, this is a simplification. A more complete impl would store last set values. + // SDL3 doesn't have a direct "get current rumble state" function. + // This function in LÖVE might return the last values set by love.joystick.setVibration. + // We will need to add private fields to store _currentLeftVibration and _currentRightVibration + // and update them in SetVibration. For now, returning 0. + return new VibrationStrength { Left = 0f, Right = 0f }; // Placeholder + } + + /// + /// Gets whether the Joystick is connected. + /// This is managed by the Joysticks class based on add/remove events. + /// + /// True if the joystick is considered connected, false otherwise. + public bool IsConnected() + { + return !this.disposed && this.isConnected; + } + + /// + /// Checks if a specific joystick button is pressed. + /// + /// The index of the button. + /// True if the button is pressed, false otherwise or if disposed. + public bool IsDown(int buttonIndex) + { + if (this.disposed) + { + return false; + } + + return SDL.GetJoystickButton(this.joystickDevicePtr, buttonIndex); + } + + /// + /// Checks if the Joystick is recognized as a gamepad by SDL. + /// + /// True if it's a gamepad and successfully opened as one, false otherwise or if disposed. + public bool IsGamepad() + { + return !this.disposed && this.gamepadDevicePtr != IntPtr.Zero; + } + + /// + /// Checks if a virtual gamepad button is pressed. + /// + /// The virtual gamepad button. + /// True if the button is pressed, false otherwise, if not a gamepad, or if disposed. + public bool IsGamepadDown(GamepadButton button) + { + if (this.disposed || this.gamepadDevicePtr == IntPtr.Zero) + { + return false; + } + + SDL.GamepadButton sdlButton = MapNightGamepadButtonToSdl(button); + return SDL.GetGamepadButton(this.gamepadDevicePtr, sdlButton); + } + + /// + /// Checks if the Joystick supports vibration (rumble). + /// + /// True if vibration is supported, false otherwise or if disposed. + public bool IsVibrationSupported() + { + if (this.disposed) + { + return false; + } + + // SDL.JoystickHasRumble or similar not found in current bindings. + return false; + } + + /// + /// Sets the vibration motor strengths on the Joystick. + /// + /// Strength of the left motor (0.0 to 1.0). + /// Strength of the right motor (0.0 to 1.0). + /// Duration of the rumble in seconds. LÖVE's API implies continuous until changed; use a long duration or 0 for infinite if SDL supports. + public void SetVibration(float left, float right, float durationSeconds = 1.0f) + { + if (this.disposed || !this.IsVibrationSupported()) + { + return; + } + + ushort leftStrength = (ushort)(Math.Clamp(left, 0f, 1f) * 65535); + ushort rightStrength = (ushort)(Math.Clamp(right, 0f, 1f) * 65535); + _ = (uint)(Math.Max(0, durationSeconds) * 1000); + + // SDL_RumbleJoystick: 0 duration means play for 0ms (i.e. stop). + // To make it "continuous until changed" like LÖVE, we might need to manage this. + // For now, a call to SetVibration(0,0) will stop it. + // If LÖVE implies it stays on, a very large duration could be used, + // but SDL_RumbleJoystick might not support "infinite". + // Let's use the provided duration, or a default if not specified by LÖVE's direct equivalent. + // LÖVE's love.joystick.setVibration() does not take duration. It's a state. + // So, if left/right are > 0, we rumble for a "long time", if 0,0 we stop. + if (leftStrength == 0 && rightStrength == 0) + { + _ = SDL.RumbleJoystick(this.joystickDevicePtr, 0, 0, 0); // Stop rumble + } + else + { + // Use a long duration to simulate continuous rumble until next SetVibration call + _ = SDL.RumbleJoystick(this.joystickDevicePtr, (short)leftStrength, (short)rightStrength, 30000); // 30 seconds, effectively "on" + } + } + + /// + /// Releases resources used by the Joystick object. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Sets the connected state. Internal use by Joysticks class. + /// + /// True if connected, false otherwise. + internal void SetConnectedState(bool connected) + { + this.isConnected = connected; + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + // Dispose managed state (managed objects). + } + + // Free unmanaged resources (unmanaged objects) and override a finalizer below. + if (this.gamepadDevicePtr != IntPtr.Zero) + { + SDL.CloseGamepad(this.gamepadDevicePtr); + this.gamepadDevicePtr = IntPtr.Zero; + } + + if (this.joystickDevicePtr != IntPtr.Zero) + { + SDL.CloseJoystick(this.joystickDevicePtr); + + // _joystickDevicePtr is readonly, so cannot set to IntPtr.Zero here. + // This is fine as _disposed flag handles access. + } + + this.disposed = true; + this.isConnected = false; + } + } + + private static float NormalizeAxisValue(short rawValue) + { + if (rawValue == 0) + { + return 0.0f; + } + else if (rawValue > 0) + { + return rawValue / 32767.0f; + } + else + { + // rawValue < 0 + return rawValue / 32768.0f; + } + } + + private static SDL.GamepadAxis MapNightGamepadAxisToSdl(GamepadAxis axis) + { + return axis switch + { + GamepadAxis.LeftX => SDL.GamepadAxis.LeftX, + GamepadAxis.LeftY => SDL.GamepadAxis.LeftY, + GamepadAxis.RightX => SDL.GamepadAxis.RightX, + GamepadAxis.RightY => SDL.GamepadAxis.RightY, + GamepadAxis.TriggerLeft => SDL.GamepadAxis.LeftTrigger, + GamepadAxis.TriggerRight => SDL.GamepadAxis.RightTrigger, + _ => SDL.GamepadAxis.Invalid, + }; + } + + private static SDL.GamepadButton MapNightGamepadButtonToSdl(GamepadButton button) + { + return button switch + { + GamepadButton.A => SDL.GamepadButton.South, + GamepadButton.B => SDL.GamepadButton.East, + GamepadButton.X => SDL.GamepadButton.West, + GamepadButton.Y => SDL.GamepadButton.North, + GamepadButton.Back => SDL.GamepadButton.Back, + GamepadButton.Guide => SDL.GamepadButton.Guide, + GamepadButton.Start => SDL.GamepadButton.Start, + GamepadButton.LeftStick => SDL.GamepadButton.LeftStick, + GamepadButton.RightStick => SDL.GamepadButton.RightStick, + GamepadButton.LeftShoulder => SDL.GamepadButton.LeftShoulder, + GamepadButton.RightShoulder => SDL.GamepadButton.RightShoulder, + GamepadButton.DPUp => SDL.GamepadButton.DPadUp, + GamepadButton.DPDown => SDL.GamepadButton.DPadDown, + GamepadButton.DPLeft => SDL.GamepadButton.DPadLeft, + GamepadButton.DPRight => SDL.GamepadButton.DPadRight, + _ => SDL.GamepadButton.Invalid, + }; + } + } +} diff --git a/src/Night/Joysticks/Joysticks.cs b/src/Night/Joysticks/Joysticks.cs new file mode 100644 index 00000000..42553c07 --- /dev/null +++ b/src/Night/Joysticks/Joysticks.cs @@ -0,0 +1,137 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Night +{ + /// + /// Provides functionality for managing and querying joysticks. + /// Corresponds to LÖVE's `love.joystick` module. + /// + public static class Joysticks + { + private static readonly Dictionary ActiveJoysticks = new Dictionary(); + + /// + /// Gets a list of connected Joysticks. + /// Corresponds to `love.joystick.getJoysticks()`. + /// + /// A list of currently connected objects. + public static List GetJoysticks() + { + // Return a copy to prevent external modification of the internal list + return ActiveJoysticks.Values.ToList(); + } + + /// + /// Gets the number of connected joysticks. + /// Corresponds to `love.joystick.getJoystickCount()`. + /// (Renamed from `love.joystick.getNumJoysticks` in LÖVE). + /// + /// The number of connected joysticks. + public static int GetJoystickCount() + { + return ActiveJoysticks.Count; + } + + /// + /// Gets an active joystick by its SDL instance ID. + /// + /// The SDL instance ID of the joystick. + /// The instance if found and active, otherwise null. + public static Joystick? GetJoystickByInstanceId(uint instanceId) + { + _ = ActiveJoysticks.TryGetValue(instanceId, out Joystick? joystickInstance); + return joystickInstance; + } + + /// + /// Adds a joystick to the active list when an SDL_EVENT_JOYSTICK_ADDED event occurs. + /// + /// The SDL instance ID of the joystick to add. + /// The newly created and added instance, or null if it failed to open. + internal static Joystick? AddJoystick(uint instanceId) + { + if (ActiveJoysticks.ContainsKey(instanceId)) + { + // Already exists, perhaps an erroneous event or already handled. + // Night.Log.LogManager.GetLogger("Joysticks").Warn($"Joystick with instance ID {instanceId} already exists in ActiveJoysticks."); + return ActiveJoysticks[instanceId]; + } + + try + { + Joystick newJoystick = new Joystick(instanceId); + ActiveJoysticks[instanceId] = newJoystick; + + // Night.Log.LogManager.GetLogger("Joysticks").Info($"Joystick added: ID {newJoystick.GetId()}, Name '{newJoystick.GetName()}', InstanceID {instanceId}"); + return newJoystick; + } + catch (InvalidOperationException) + { + // Night.Log.LogManager.GetLogger("Joysticks").Error($"Failed to add joystick with instance ID {instanceId}: {ex.Message}"); + return null; + } + } + + /// + /// Removes a joystick from the active list when an SDL_EVENT_JOYSTICK_REMOVED event occurs. + /// The Joystick object is returned so it can be passed to the event callback before disposal. + /// + /// The SDL instance ID of the joystick to remove. + /// The removed instance if found, otherwise null. + internal static Joystick? RemoveJoystick(uint instanceId) + { + if (ActiveJoysticks.TryGetValue(instanceId, out Joystick? joystickInstance)) + { + _ = ActiveJoysticks.Remove(instanceId); + joystickInstance.SetConnectedState(false); // Mark as disconnected + + // Night.Log.LogManager.GetLogger("Joysticks").Info($"Joystick removed: ID {joystickInstance.GetId()}, Name '{joystickInstance.GetName()}', InstanceID {instanceId}"); + return joystickInstance; + } + else + { + // Night.Log.LogManager.GetLogger("Joysticks").Warn($"Attempted to remove joystick with instance ID {instanceId}, but it was not found in ActiveJoysticks."); + return null; + } + } + + /// + /// Clears all active joysticks. Called during framework shutdown. + /// + internal static void ClearJoysticks() + { + foreach (var joystick in ActiveJoysticks.Values) + { + joystick.Dispose(); + } + + ActiveJoysticks.Clear(); + + // Night.Log.LogManager.GetLogger("Joysticks").Info("All active joysticks cleared and disposed."); + } + } +} diff --git a/src/Night/Keyboard/Keyboard.cs b/src/Night/Keyboard/Keyboard.cs index 0df014a3..d8333ed6 100644 --- a/src/Night/Keyboard/Keyboard.cs +++ b/src/Night/Keyboard/Keyboard.cs @@ -24,6 +24,7 @@ using System.Runtime.InteropServices; using Night; +using Night.Log; using SDL3; @@ -34,6 +35,8 @@ namespace Night /// public static class Keyboard { + private static readonly ILogger Logger = LogManager.GetLogger("Night.Keyboard.Keyboard"); + /// /// Checks whether a certain key is down. /// @@ -43,7 +46,7 @@ public static bool IsDown(KeyCode key) { if (!Framework.IsInputInitialized) { - Console.WriteLine("Warning: Night.Keyboard.IsDown called before input system is initialized. Returning false."); + Logger.Warn("Night.Keyboard.IsDown called before input system is initialized. Returning false."); return false; } @@ -51,7 +54,7 @@ public static bool IsDown(KeyCode key) if (keyboardState == null) { - Console.WriteLine("Warning: SDL.GetKeyboardState returned a null array."); + Logger.Warn("SDL.GetKeyboardState returned a null array."); return false; } @@ -64,7 +67,7 @@ public static bool IsDown(KeyCode key) if ((int)sdlScancode >= keyboardState.Length || (int)sdlScancode < 0) { - Console.WriteLine($"Warning: Scancode {(int)sdlScancode} is out of bounds (numKeys: {keyboardState.Length})."); + Logger.Warn($"Scancode {(int)sdlScancode} is out of bounds (numKeys: {keyboardState.Length})."); return false; } diff --git a/src/Night/Log/ILogSink.cs b/src/Night/Log/ILogSink.cs new file mode 100644 index 00000000..f77370d2 --- /dev/null +++ b/src/Night/Log/ILogSink.cs @@ -0,0 +1,36 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +namespace Night +{ + /// + /// Interface for log sinks, which are destinations for log entries. + /// + public interface ILogSink + { + /// + /// Writes a log entry to the sink. + /// + /// The log entry to write. + void Write(LogEntry entry); + } +} diff --git a/src/Night/Log/ILogger.cs b/src/Night/Log/ILogger.cs new file mode 100644 index 00000000..69a78190 --- /dev/null +++ b/src/Night/Log/ILogger.cs @@ -0,0 +1,85 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; + +namespace Night +{ + /// + /// Interface for logging messages. + /// + public interface ILogger + { + /// + /// Logs a message with the specified log level. + /// + /// The severity level of the message. + /// The message to log. + /// Optional. The exception associated with the message. + void Log(LogLevel level, string message, Exception? exception = null); + + /// + /// Checks if the specified log level is enabled for this logger. + /// + /// The log level to check. + /// true if the log level is enabled; otherwise, false. + bool IsEnabled(LogLevel level); + + /// + /// Logs a trace message. + /// + /// The message to log. + void Trace(string message); + + /// + /// Logs a debug message. + /// + /// The message to log. + void Debug(string message); + + /// + /// Logs an informational message. + /// + /// The message to log. + void Info(string message); + + /// + /// Logs a warning message. + /// + /// The message to log. + void Warn(string message); + + /// + /// Logs an error message. + /// + /// The message to log. + /// Optional. The exception associated with the message. + void Error(string message, Exception? exception = null); + + /// + /// Logs a fatal error message. + /// + /// The message to log. + /// Optional. The exception associated with the message. + void Fatal(string message, Exception? exception = null); + } +} diff --git a/src/Night/Log/LogEntry.cs b/src/Night/Log/LogEntry.cs new file mode 100644 index 00000000..26ff5808 --- /dev/null +++ b/src/Night/Log/LogEntry.cs @@ -0,0 +1,80 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; + +namespace Night +{ + /// + /// Represents a single log message. + /// + public record LogEntry + { + /// + /// Initializes a new instance of the class. + /// Initializes a new instance of the record. + /// + /// The UTC timestamp of the log event. + /// The log level. + /// The log message. + /// The category name of the logger. + /// The optional exception. + public LogEntry( + DateTime timestampUtc, + LogLevel level, + string message, + string categoryName, + Exception? exception = null) + { + this.TimestampUtc = timestampUtc; + this.Level = level; + this.Message = message ?? string.Empty; + this.CategoryName = categoryName ?? string.Empty; + this.Exception = exception; + } + + /// + /// Gets the Coordinated Universal Time (UTC) when the log entry was created. + /// + public DateTime TimestampUtc { get; } + + /// + /// Gets the severity level of the log entry. + /// + public LogLevel Level { get; } + + /// + /// Gets the formatted log message. + /// + public string Message { get; } + + /// + /// Gets the optional exception associated with the log entry. + /// + public Exception? Exception { get; } + + /// + /// Gets the category name or source of the log entry. + /// + public string CategoryName { get; } + } +} diff --git a/src/Night/Log/LogLevel.cs b/src/Night/Log/LogLevel.cs new file mode 100644 index 00000000..7b7a4ca2 --- /dev/null +++ b/src/Night/Log/LogLevel.cs @@ -0,0 +1,60 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +namespace Night +{ + /// + /// Defines the severity levels for log messages. + /// + public enum LogLevel + { + /// + /// Detailed information, typically of interest only when diagnosing problems. + /// + Trace, + + /// + /// Information that is diagnostically helpful to people more than just developers. + /// + Debug, + + /// + /// Generally useful information to log (service start/stop, configuration assumptions, etc). + /// + Information, + + /// + /// Indicates a potential problem or an unexpected event. + /// + Warning, + + /// + /// Indicates a failure in the current operation or task, not necessarily application-wide. + /// + Error, + + /// + /// A critical error that might lead to application termination. + /// + Fatal, + } +} diff --git a/src/Night/Log/LogManager.cs b/src/Night/Log/LogManager.cs new file mode 100644 index 00000000..15d33fcb --- /dev/null +++ b/src/Night/Log/LogManager.cs @@ -0,0 +1,269 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +using Night.Log.Sinks; + +namespace Night +{ + /// + /// Manages logger instances and log sinks for the Night logging system. + /// + public static class LogManager + { + private static readonly ConcurrentDictionary Loggers = new ConcurrentDictionary(); + private static readonly List Sinks = new List(); + private static readonly object SinksLock = new object(); + + private static SystemConsoleSink? systemConsoleSinkInstance; + private static FileSink? fileSinkInstance; + + /// + /// Gets or sets the global minimum log level. + /// Only log entries with a level equal to or higher than this will be dispatched to sinks. + /// Defaults to . + /// + public static LogLevel MinLevel { get; set; } = LogLevel.Information; + + /// + /// Gets a logger instance for the specified category name. + /// + /// The category name for the logger. + /// An instance. + public static ILogger GetLogger(string categoryName) + { + if (string.IsNullOrEmpty(categoryName)) + { + throw new ArgumentNullException(nameof(categoryName)); + } + + return Loggers.GetOrAdd(categoryName, name => new Logger(name)); + } + + /// + /// Adds a log sink to the logging system. + /// + /// The log sink to add. + public static void AddSink(ILogSink sink) + { + if (sink == null) + { + throw new ArgumentNullException(nameof(sink)); + } + + lock (SinksLock) + { + if (!Sinks.Contains(sink)) + { + Sinks.Add(sink); + } + } + } + + /// + /// Removes a log sink from the logging system. + /// + /// The log sink to remove. + public static void RemoveSink(ILogSink sink) + { + if (sink == null) + { + throw new ArgumentNullException(nameof(sink)); + } + + lock (SinksLock) + { + _ = Sinks.Remove(sink); + } + } + + /// + /// Removes all log sinks from the logging system. + /// + public static void ClearSinks() + { + lock (SinksLock) + { + Sinks.Clear(); + } + } + + /// + /// Enables or disables the system console log sink. + /// + /// True to enable, false to disable. + public static void EnableSystemConsoleSink(bool enable) + { + lock (SinksLock) + { + if (enable) + { + if (systemConsoleSinkInstance == null) + { + systemConsoleSinkInstance = new SystemConsoleSink(); + AddSink(systemConsoleSinkInstance); + } + } + else + { + if (systemConsoleSinkInstance != null) + { + RemoveSink(systemConsoleSinkInstance); + systemConsoleSinkInstance = null; + } + } + } + } + + /// + /// Checks if the system console sink is currently enabled. + /// + /// True if enabled, false otherwise. + public static bool IsSystemConsoleSinkEnabled() + { + lock (SinksLock) + { + return systemConsoleSinkInstance != null && Sinks.Contains(systemConsoleSinkInstance); + } + } + + /// + /// Configures and enables the file log sink, replacing any existing file sink. + /// + /// The path to the log file. + public static void ConfigureFileSink(string filePath) + { + // TODO: The epic's manual tests (e.g., project/epics/logger-tasks.md:262) + // imply FileSink should support its own MinLevel. + // This overload exists to satisfy current Game.cs compilation. + // FileSink.cs needs enhancement for sink-specific level filtering. + // For now, we use a default or rely on global MinLevel. + ConfigureFileSink(filePath, LogLevel.Trace); // Default to Trace for the sink itself + } + + /// + /// Configures and enables the file log sink, replacing any existing file sink. + /// + /// The path to the log file. + /// The minimum log level for this specific file sink. + public static void ConfigureFileSink(string filePath, LogLevel minLevelForFile) + { + if (string.IsNullOrEmpty(filePath)) + { + throw new ArgumentNullException(nameof(filePath)); + } + + lock (SinksLock) + { + if (fileSinkInstance != null) + { + RemoveSink(fileSinkInstance); + + // If FileSink implemented IDisposable, we would call _fileSinkInstance.Dispose(); here + fileSinkInstance = null; + } + + // FileSink constructor now accepts minLevelForFile. + try + { + fileSinkInstance = new FileSink(filePath, minLevelForFile); + AddSink(fileSinkInstance); + } + catch (Exception ex) + { + // Log configuration errors to System.Diagnostics.Trace + Trace.WriteLine($"Night.Log.LogManager: Error configuring FileSink for path '{filePath}': {ex.Message}"); + Trace.WriteLine(ex.StackTrace); + fileSinkInstance = null; // Ensure it's null if configuration failed + } + } + } + + /// + /// Disables the file log sink if it is currently active. + /// + public static void DisableFileSink() + { + lock (SinksLock) + { + if (fileSinkInstance != null) + { + RemoveSink(fileSinkInstance); + + // If FileSink implemented IDisposable, we would call _fileSinkInstance.Dispose(); here + fileSinkInstance = null; + } + } + } + + /// + /// Dispatches a log entry to all active sinks if its level meets the . + /// This method is intended for internal use by implementations. + /// + /// The log entry to dispatch. + internal static void Dispatch(LogEntry entry) + { + if (entry == null) + { + // Or throw ArgumentNullException, depending on desired strictness + return; + } + + if (entry.Level < MinLevel) + { + return; + } + + List currentSinks; + lock (SinksLock) + { + // Create a copy to iterate over, avoiding issues if sinks are modified during dispatch + currentSinks = new List(Sinks); + } + + foreach (var sinkInstance in currentSinks) + { + try + { + // Check for sink-specific log levels, e.g., for FileSink. + if (sinkInstance is FileSink specificFileSink && entry.Level < specificFileSink.MinLevel) + { + continue; // Skip this sink if the entry's level is below the FileSink's specific minimum. + } + + sinkInstance.Write(entry); + } + catch (Exception ex) + { + // Log sink errors to System.Diagnostics.Trace to avoid recursive logging or crashing. + Trace.WriteLine($"Night.Log.LogManager: Error in sink '{sinkInstance.GetType().FullName}': {ex.Message}"); + Trace.WriteLine(ex.StackTrace); + } + } + } + } +} diff --git a/src/Night/Log/Logger.cs b/src/Night/Log/Logger.cs new file mode 100644 index 00000000..23ca620f --- /dev/null +++ b/src/Night/Log/Logger.cs @@ -0,0 +1,106 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; + +namespace Night +{ + /// + /// Default implementation of the interface. + /// + internal class Logger : ILogger + { + private readonly string categoryName; + + /// + /// Initializes a new instance of the class. + /// + /// The category name for this logger instance. + /// + /// This constructor is internal as loggers should be obtained via . + /// + internal Logger(string categoryName) + { + this.categoryName = categoryName ?? throw new ArgumentNullException(nameof(categoryName)); + } + + /// + public bool IsEnabled(LogLevel level) + { + return level >= LogManager.MinLevel; + } + + /// + public void Log(LogLevel level, string message, Exception? exception = null) + { + if (!this.IsEnabled(level)) + { + return; + } + + var logEntry = new LogEntry( + DateTime.UtcNow, + level, + message, + this.categoryName, + exception); + + LogManager.Dispatch(logEntry); + } + + /// + public void Trace(string message) + { + this.Log(LogLevel.Trace, message); + } + + /// + public void Debug(string message) + { + this.Log(LogLevel.Debug, message); + } + + /// + public void Info(string message) + { + this.Log(LogLevel.Information, message); + } + + /// + public void Warn(string message) + { + this.Log(LogLevel.Warning, message); + } + + /// + public void Error(string message, Exception? exception = null) + { + this.Log(LogLevel.Error, message, exception); + } + + /// + public void Fatal(string message, Exception? exception = null) + { + this.Log(LogLevel.Fatal, message, exception); + } + } +} diff --git a/src/Night/Log/Sinks/FileSink.cs b/src/Night/Log/Sinks/FileSink.cs new file mode 100644 index 00000000..56f5913b --- /dev/null +++ b/src/Night/Log/Sinks/FileSink.cs @@ -0,0 +1,117 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace Night.Log.Sinks +{ + /// + /// A log sink that writes log entries to a specified file. + /// + public class FileSink : ILogSink + { + private readonly string filePath; + private readonly object lockObject = new object(); + + /// + /// Initializes a new instance of the class. + /// + /// The full path to the log file. + /// The minimum log level for this specific sink. + /// Thrown if filePath is null or empty. + public FileSink(string filePath, LogLevel minLevel) + { + if (string.IsNullOrEmpty(filePath)) + { + throw new ArgumentNullException(nameof(filePath), "File path cannot be null or empty."); + } + + this.filePath = filePath; + this.MinLevel = minLevel; + this.EnsureDirectoryExists(); + } + + /// + /// Gets the minimum log level for this file sink. + /// Only messages at this level or higher will be written by this sink, + /// assuming they also pass the global LogManager.MinLevel. + /// + public LogLevel MinLevel { get; } + + /// + public void Write(LogEntry entry) + { + var messageBuilder = new StringBuilder(); + _ = messageBuilder.Append($"[{entry.TimestampUtc:yyyy-MM-dd HH:mm:ss.fff}Z] "); + _ = messageBuilder.Append($"[{entry.Level,-11}] "); // Padded for alignment + _ = messageBuilder.Append($"[{entry.CategoryName}] "); + _ = messageBuilder.Append(entry.Message); + + if (entry.Exception != null) + { + _ = messageBuilder.AppendLine(); + _ = messageBuilder.Append($" Exception: {entry.Exception.GetType().FullName}: {entry.Exception.Message}"); + if (!string.IsNullOrEmpty(entry.Exception.StackTrace)) + { + _ = messageBuilder.AppendLine(); + _ = messageBuilder.Append($" Stack Trace: {entry.Exception.StackTrace.Replace(Environment.NewLine, Environment.NewLine + " ")}"); + } + } + + lock (this.lockObject) + { + try + { + // Ensure directory exists right before writing, in case it was deleted externally. + this.EnsureDirectoryExists(); + File.AppendAllText(this.filePath, messageBuilder.ToString() + Environment.NewLine, Encoding.UTF8); + } + catch (Exception ex) + { + // Log to diagnostics trace as a fallback. + // This prevents a logging failure from crashing the application or affecting other sinks. + Trace.WriteLine($"Night.Log.Sinks.FileSink: Failed to write to log file '{this.filePath}'. Error: {ex.Message}"); + } + } + } + + private void EnsureDirectoryExists() + { + try + { + string? directoryName = Path.GetDirectoryName(this.filePath); + if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName)) + { + _ = Directory.CreateDirectory(directoryName); + } + } + catch (Exception ex) + { + // Log to diagnostics trace as a fallback, to avoid logger-ception or silent failure. + Trace.WriteLine($"Night.Log.Sinks.FileSink: Failed to create directory for log file '{this.filePath}'. Error: {ex.Message}"); + } + } + } +} diff --git a/src/Night/Log/Sinks/InGameConsoleSink.cs b/src/Night/Log/Sinks/InGameConsoleSink.cs new file mode 100644 index 00000000..faa8efb9 --- /dev/null +++ b/src/Night/Log/Sinks/InGameConsoleSink.cs @@ -0,0 +1,78 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Night.Log.Sinks +{ + /// + /// A log sink that buffers log entries in memory for an in-game console display. + /// + public class InGameConsoleSink : ILogSink + { + private readonly ConcurrentQueue logEntries; + private readonly int? capacity; + + /// + /// Initializes a new instance of the class. + /// + /// The maximum number of log entries to store. If null, the buffer is unbounded. + public InGameConsoleSink(int? capacity = null) + { + if (capacity.HasValue && capacity.Value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be a positive integer if specified."); + } + + this.logEntries = new ConcurrentQueue(); + this.capacity = capacity; + } + + /// + public void Write(LogEntry entry) + { + this.logEntries.Enqueue(entry); + + if (this.capacity.HasValue) + { + while (this.logEntries.Count > this.capacity.Value && this.logEntries.TryDequeue(out _)) + { + // Dequeue oldest entries if capacity is exceeded + } + } + } + + /// + /// Retrieves a snapshot of the current log entries buffered by this sink. + /// + /// An enumerable collection of objects. + public IEnumerable GetEntries() + { + // Returns a copy to prevent modification of the internal collection + // and to ensure thread safety during enumeration. + return this.logEntries.ToList(); + } + } +} diff --git a/src/Night/Log/Sinks/MemorySink.cs b/src/Night/Log/Sinks/MemorySink.cs new file mode 100644 index 00000000..72812ee8 --- /dev/null +++ b/src/Night/Log/Sinks/MemorySink.cs @@ -0,0 +1,83 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Night +{ + /// + /// An implementation that stores recent log entries in memory. + /// + public class MemorySink : ILogSink + { + private readonly int capacity; + private readonly ConcurrentQueue entries; + + /// + /// Initializes a new instance of the class. + /// + /// The maximum number of log entries to store. Defaults to 100. + public MemorySink(int capacity = 100) + { + if (capacity <= 0) + { + throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be greater than zero."); + } + + this.capacity = capacity; + this.entries = new ConcurrentQueue(); + } + + /// + /// Writes the specified log entry to the in-memory buffer. + /// If the buffer exceeds capacity, the oldest entry is removed. + /// + /// The log entry to write. + public void Write(LogEntry entry) + { + if (entry == null) + { + return; + } + + this.entries.Enqueue(entry); + + // Maintain capacity + while (this.entries.Count > this.capacity && this.entries.TryDequeue(out _)) + { + // Dequeued an old entry to maintain capacity + } + } + + /// + /// Retrieves a snapshot of the currently buffered log entries. + /// + /// An enumerable collection of objects. + public IEnumerable GetEntries() + { + return this.entries.ToList(); // Return a copy + } + } +} diff --git a/src/Night/Log/Sinks/SystemConsoleSink.cs b/src/Night/Log/Sinks/SystemConsoleSink.cs new file mode 100644 index 00000000..4ea420a3 --- /dev/null +++ b/src/Night/Log/Sinks/SystemConsoleSink.cs @@ -0,0 +1,57 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Globalization; + +namespace Night +{ + /// + /// An implementation that writes log entries to the system console. + /// + public class SystemConsoleSink : ILogSink + { + /// + /// Writes the specified log entry to the system console. + /// + /// The log entry to write. + public void Write(LogEntry entry) + { + if (entry == null) + { + return; + } + + // Format: YYYY-MM-DDTHH:mm:ss.fffZ [LEVEL] [Category] Message + // Exception details are appended on new lines if present. + string timestamp = entry.TimestampUtc.ToString("o", CultureInfo.InvariantCulture); // ISO 8601 + string level = entry.Level.ToString().ToUpperInvariant(); + + Console.WriteLine($"{timestamp} [{level}] [{entry.CategoryName}] {entry.Message}"); + + if (entry.Exception != null) + { + Console.WriteLine(entry.Exception.ToString()); + } + } + } +} diff --git a/src/Night/Mouse/Mouse.cs b/src/Night/Mouse/Mouse.cs index 0dc792a3..9860b485 100644 --- a/src/Night/Mouse/Mouse.cs +++ b/src/Night/Mouse/Mouse.cs @@ -23,6 +23,7 @@ using System; using Night; +using Night.Log; using SDL3; @@ -33,6 +34,8 @@ namespace Night /// public static class Mouse { + private static readonly ILogger Logger = LogManager.GetLogger("Night.Mouse.Mouse"); + /// /// Checks whether a certain mouse button is down. /// This function does not detect mouse wheel scrolling. @@ -43,7 +46,7 @@ public static bool IsDown(MouseButton button) { if (!Framework.IsInputInitialized) { - Console.WriteLine("Warning: Night.Mouse.IsDown called before input system is initialized. Returning false."); + Logger.Warn("Night.Mouse.IsDown called before input system is initialized. Returning false."); return false; } @@ -83,7 +86,7 @@ public static (int X, int Y) GetPosition() { if (!Framework.IsInputInitialized) { - Console.WriteLine("Warning: Night.Mouse.GetPosition called before input system is initialized. Returning (0,0)."); + Logger.Warn("Night.Mouse.GetPosition called before input system is initialized. Returning (0,0)."); return (0, 0); } diff --git a/src/Night/Night.csproj b/src/Night/Night.csproj index 1c34499a..15e167fa 100644 --- a/src/Night/Night.csproj +++ b/src/Night/Night.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/Night/System/System.cs b/src/Night/System/System.cs index 4ed6da6c..81ecb768 100644 --- a/src/Night/System/System.cs +++ b/src/Night/System/System.cs @@ -20,15 +20,154 @@ // 3. This notice may not be removed or altered from any source distribution. // +using SDL3; + namespace Night { - using SDL3; + /// + /// Represents the basic state of the system's power supply. + /// + public enum PowerState + { + /// + /// Cannot determine power status, or an error occurred. + /// + Unknown, + + /// + /// Not plugged in, running on the battery. + /// + Battery, + + /// + /// Plugged in, no battery available. + /// + NoBattery, + + /// + /// Plugged in, charging battery. + /// + Charging, + + /// + /// Plugged in, battery charged. + /// + Charged, + } /// /// Provides access to system-level information and functions. /// public static class System { + /// + /// Gets the current text in the system's clipboard. + /// + /// Clipboard text as a string. + public static string GetClipboardText() + { + return SDL.GetClipboardText(); + } + + /// + /// Gets the current operating system. + /// This function is similar to LÖVE's love.system.getOS(). + /// + /// + /// The current operating system: "OS X", "Windows", "Linux", "Android", or "iOS". + /// Returns the raw platform string from SDL if the OS is not one of the above. + /// + public static string GetOS() + { + string sdlPlatform = SDL.GetPlatform(); + switch (sdlPlatform) + { + case "Windows": + return "Windows"; + case "Mac OS X": + case "macOS": + return "OS X"; + case "Linux": + return "Linux"; + case "Android": + return "Android"; + case "iOS": + return "iOS"; + default: + // If SDL returns something unexpected, pass it through. + // This helps in identifying new/unhandled platforms. + return sdlPlatform; + } + } + + /// + /// Gets information about the system's power supply. + /// This function is similar to LÖVE's love.system.getPowerInfo(). + /// + /// + /// A tuple containing: + /// + /// state: The basic state of the power supply (). + /// percent: Percentage of battery life left (0-100), or null if not applicable/determinable. + /// seconds: Seconds of battery life left, or null if not applicable/determinable. + /// + /// + public static (PowerState State, int? Percent, int? Seconds) GetPowerInfo() + { + SDL.PowerState sdlState = SDL.GetPowerInfo(out int seconds, out int percent); + + PowerState nightState; + switch (sdlState) + { + case SDL.PowerState.OnBattery: + nightState = PowerState.Battery; + break; + case SDL.PowerState.NoBattery: + nightState = PowerState.NoBattery; + break; + case SDL.PowerState.Charging: + nightState = PowerState.Charging; + break; + case SDL.PowerState.Charged: + nightState = PowerState.Charged; + break; + case SDL.PowerState.Error: + case SDL.PowerState.Unknown: + default: + nightState = PowerState.Unknown; + break; + } + + // SDL returns -1 for seconds/percent if not available or error + int? nullablePercent = percent == -1 ? null : (int?)percent; + int? nullableSeconds = seconds == -1 ? null : (int?)seconds; + + return (nightState, nullablePercent, nullableSeconds); + } + + /// + /// Gets the amount of logical processors in the system. + /// + /// Amount of logical processors. + /// + /// The number includes the threads reported if technologies such as Intel's Hyper-threading are enabled. + /// For example, on a 4-core CPU with Hyper-threading, this function will return 8. + /// + public static int GetProcessorCount() + { + return SDL.GetNumLogicalCPUCores(); + } + + /// + /// Opens a URL with the user's web or file browser. + /// + /// The URL to open. Must be formatted as a proper URL. + /// Whether the URL was opened successfully. + public static bool OpenURL(string url) + { + return SDL.OpenURL(url); + } + /// /// Puts text in the system's clipboard. /// @@ -38,11 +177,5 @@ public static bool SetClipboardText(string text) { return SDL.SetClipboardText(text); } - - // TODO: Consider adding GetClipboardText if in scope for future versions. - // public static string GetClipboardText() - // { - // return SDL.GetClipboardText(); - // } } } diff --git a/src/Night/Timer/Timer.cs b/src/Night/Timer/Timer.cs index 0de06564..6ed4a07c 100644 --- a/src/Night/Timer/Timer.cs +++ b/src/Night/Timer/Timer.cs @@ -166,10 +166,6 @@ internal static void Initialize() // Initialize for the first call to Step() LastStepTime = SDL.GetPerformanceCounter(); - - // _timerStartTime is already initialized at class load (line 14) and should remain as such - // to reflect "time since module loaded" for GetTime(). - // Do not re-assign _timerStartTime here. } } } diff --git a/src/Night/VersionInfo.cs b/src/Night/VersionInfo.cs index 0fbbffff..4438939e 100644 --- a/src/Night/VersionInfo.cs +++ b/src/Night/VersionInfo.cs @@ -31,13 +31,13 @@ public static class VersionInfo /// Gets the full semantic version string (e.g., "1.0.0", "1.2.3-beta.1"). /// This value is updated by the GitHub release Action. /// - public const string Version = "0.0.1"; + public const string Version = "0.0.2"; /// /// Gets the developer-assigned codename for the current version. /// This value is manually updated by the developer. /// - public const string CodeName = "Initial Codename"; // Placeholder + public const string CodeName = "Initial Codename"; // TODO: Placeholder /// /// Gets the Semantic Version of the Night library. @@ -50,4 +50,3 @@ public static string GetVersion() } } } - diff --git a/src/Night/Window/Window.Display.cs b/src/Night/Window/Window.Display.cs new file mode 100644 index 00000000..f3906eba --- /dev/null +++ b/src/Night/Window/Window.Display.cs @@ -0,0 +1,219 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; + +using Night.Log; + +using SDL3; + +namespace Night +{ + /// + /// Contains display-related functionality for the class. + /// + public static partial class Window + { + /// + /// Gets the number of connected monitors. + /// + /// The number of currently connected displays. + public static int GetDisplayCount() + { + uint[]? displays = SDL.GetDisplays(out int count); + if (displays == null || count < 0) + { + return 0; + } + + return count; + } + + /// + /// Gets the width and height of the desktop. + /// + /// The index of the display to query (0 for the primary display). + /// A tuple containing the width and height of the desktop, or (0,0) if an error occurs. + public static (int Width, int Height) GetDesktopDimensions(int displayIndex = 0) + { + uint[]? actualDisplayIDs = SDL.GetDisplays(out int displayCount); + if (actualDisplayIDs == null || displayCount <= 0 || displayIndex < 0 || displayIndex >= displayCount) + { + return (0, 0); + } + + uint targetDisplayID = actualDisplayIDs[displayIndex]; + + SDL.DisplayMode? mode = SDL.GetDesktopDisplayMode(targetDisplayID); + if (mode == null) + { + return (0, 0); + } + + return (mode.Value.W, mode.Value.H); + } + + /// + /// Gets whether the window is fullscreen. + /// + /// A tuple: (bool IsFullscreen, FullscreenType FsType). + /// IsFullscreen is true if the window is in any fullscreen mode, false otherwise. + /// FsType indicates the type of fullscreen mode used. + public static (bool IsFullscreen, FullscreenType FsType) GetFullscreen() + { + if (window == nint.Zero) + { + return (false, currentFullscreenType); + } + + var flags = SDL.GetWindowFlags(window); + + if ((flags & SDL.WindowFlags.Fullscreen) != 0) + { + return (true, FullscreenType.Exclusive); + } + + if (currentFullscreenType == FullscreenType.Desktop && (flags & SDL.WindowFlags.Borderless) != 0) + { + return (true, FullscreenType.Desktop); + } + + return (false, currentFullscreenType); + } + + /// + /// Enters or exits fullscreen. + /// + /// Whether to enter or exit fullscreen mode. + /// The type of fullscreen mode to use (Desktop or Exclusive). + /// True if the operation was successful, false otherwise. + public static bool SetFullscreen(bool fullscreen, FullscreenType fsType = FullscreenType.Desktop) + { + if (window == nint.Zero) + { + return false; + } + + if (fullscreen) + { + currentFullscreenType = fsType; + if (fsType == FullscreenType.Exclusive) + { + uint displayID = SDL.GetDisplayForWindow(window); + if (displayID == 0 && !string.IsNullOrEmpty(SDL.GetError())) + { + return false; + } + + SDL.DisplayMode? dm = SDL.GetDesktopDisplayMode(displayID); + if (dm.HasValue) + { + if (!SDL.SetWindowFullscreenMode(window, dm.Value)) + { + return false; + } + } + else + { + return false; + } + } + else + { + if (!SDL.SetWindowFullscreenMode(window, nint.Zero)) + { + Logger.Warn($"SetFullscreen (Desktop): SDL_SetWindowFullscreenMode(NULL) failed: {SDL.GetError()}"); + } + + if (!SDL.SetWindowBordered(window, false)) + { + Logger.Error($"SetFullscreen (Desktop): SDL_SetWindowBordered(false) failed: {SDL.GetError()}"); + return false; + } + + uint displayID = SDL.GetDisplayForWindow(window); + string errorCheck = SDL.GetError(); + if (displayID == 0 && !string.IsNullOrEmpty(errorCheck)) + { + Logger.Error($"SetFullscreen (Desktop): SDL_GetDisplayForWindow failed: {errorCheck}"); + return false; + } + + var (desktopW, desktopH) = GetDesktopDimensionsForDisplayID(displayID); + + if (desktopW > 0 && desktopH > 0) + { + _ = SDL.SetWindowPosition(window, 0, 0); + if (!SDL.SetWindowSize(window, desktopW, desktopH)) + { + Logger.Warn($"SetFullscreen (Desktop): SDL_SetWindowSize({desktopW},{desktopH}) failed: {SDL.GetError()}"); + } + } + else + { + Logger.Error($"SetFullscreen (Desktop): GetDesktopDimensionsForDisplayID failed for display {displayID}."); + return false; + } + } + } + else + { + currentFullscreenType = FullscreenType.Desktop; + if (!SDL.SetWindowFullscreenMode(window, nint.Zero)) + { + Logger.Warn($"SetFullscreen (Exit): SDL_SetWindowFullscreenMode(NULL) failed: {SDL.GetError()}"); + } + + if (!SDL.SetWindowBordered(window, true)) + { + Logger.Error($"SetFullscreen (Exit): SDL_SetWindowBordered(true) failed: {SDL.GetError()}"); + return false; + } + + var config = ConfigurationManager.CurrentConfig.Window; + int restoreWidth = config.Width > 0 ? config.Width : 800; + int restoreHeight = config.Height > 0 ? config.Height : 600; + + if (!SDL.SetWindowSize(window, restoreWidth, restoreHeight)) + { + Logger.Warn($"SetFullscreen (Exit): SDL_SetWindowSize({restoreWidth},{restoreHeight}) failed: {SDL.GetError()}"); + } + + if (config.X.HasValue && config.Y.HasValue) + { + _ = SDL.SetWindowPosition(window, config.X.Value, config.Y.Value); + } + else + { + _ = SDL.SetWindowPosition(window, (int)SDL.WindowposCenteredMask, (int)SDL.WindowposCenteredMask); // Assumes primary display (display 0) + } + + _ = SDL.RaiseWindow(window); + } + + return true; + } + } +} diff --git a/src/Night/Window/Window.Mode.cs b/src/Night/Window/Window.Mode.cs new file mode 100644 index 00000000..f89409bc --- /dev/null +++ b/src/Night/Window/Window.Mode.cs @@ -0,0 +1,266 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; + +using Night.Log; + +using SDL3; + +namespace Night +{ + /// + /// Contains window mode and attribute functionality for the class. + /// + public static partial class Window + { + /// + /// Sets the display mode and properties of the window. + /// + /// The width of the window. + /// The height of the window. + /// SDL Window flags to apply. + /// True if the mode was set successfully, false otherwise. + public static bool SetMode(int width, int height, SDL.WindowFlags flags) + { + lock (WindowLock) + { + Logger.Info($"Attempting to set mode {width}x{height} with flags: {flags}"); + Logger.Debug($"Current Thread ID: {Thread.CurrentThread.ManagedThreadId}"); + + if (window != nint.Zero) + { + Logger.Info($"Existing window found (Handle: {window}). Destroying old window and renderer."); + if (renderer != nint.Zero) + { + SDL.DestroyRenderer(renderer); + renderer = nint.Zero; + Logger.Debug("Old renderer destroyed."); + } + + SDL.DestroyWindow(window); + window = nint.Zero; + isWindowOpen = false; + Logger.Debug("Old window destroyed."); + } + + Logger.Debug("[PRE-CREATE] Clearing any previous SDL errors"); + _ = SDL.ClearError(); + string preCreateError = SDL.GetError(); + Logger.Debug($"[PRE-CREATE] SDL error after clear: '{preCreateError}'"); + + if (SDL.GetCurrentVideoDriver() == "offscreen") + { + // The offscreen driver on macOS may require a graphics backend hint even for software rendering. + // We use the OpenGL flag to bypass potential Metal initialization issues in a headless environment. + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Logger.Info("Offscreen driver on macOS detected. Adding OpenGL flag for test window creation."); + flags |= SDL.WindowFlags.OpenGL; + } + } + + Logger.Debug($"[PRE-CREATE] About to call SDL.CreateWindow with parameters:"); + Logger.Debug($" - title: 'Night Engine'"); + Logger.Debug($" - width: {width}"); + Logger.Debug($" - height: {height}"); + Logger.Debug($" - flags: {flags} (0x{(uint)flags:X})"); + Logger.Debug($" - Thread ID: {Thread.CurrentThread.ManagedThreadId}"); + + window = SDL.CreateWindow("Night Engine", width, height, flags); + + Logger.Debug($"[POST-CREATE] SDL.CreateWindow returned: {window} (0x{window:X})"); + + if (window == nint.Zero) + { + isWindowOpen = false; + Logger.Error($"[POST-CREATE] SDL.CreateWindow FAILED - returned null pointer"); + + string immediateError = SDL.GetError(); + Logger.Error($"[POST-CREATE] Immediate SDL.GetError(): '{immediateError}'"); + + SDL.Delay(10); + string delayedError1 = SDL.GetError(); + Logger.Error($"[POST-CREATE] SDL.GetError() after 10ms delay: '{delayedError1}'"); + + SDL.Delay(50); + string delayedError2 = SDL.GetError(); + Logger.Error($"[POST-CREATE] SDL.GetError() after 60ms total delay: '{delayedError2}'"); + + Logger.Debug($"[POST-CREATE] Attempting to get video driver info for diagnostics..."); + try + { + string videoDriver = SDL.GetCurrentVideoDriver() ?? string.Empty; + Logger.Debug($"[POST-CREATE] Current video driver: '{videoDriver}'"); + } + catch (Exception ex) + { + Logger.Warn($"[POST-CREATE] Failed to get video driver: {ex.Message}"); + } + + Logger.Error($"SDL.CreateWindow failed. Final SDL Error: '{delayedError2}'"); + return false; + } + + Logger.Info($"SDL.CreateWindow succeeded. New Window Handle: {window}"); + + string? initialRendererError = null; + renderer = SDL.CreateRenderer(window, null); + if (renderer == nint.Zero) + { + initialRendererError = SDL.GetError() ?? "Unknown error (hardware renderer)"; + Logger.Warn($"SDL.CreateRenderer (hardware) failed: {initialRendererError}. Attempting software renderer."); + + nint surface = SDL.GetWindowSurface(window); + if (surface == nint.Zero) + { + string windowSurfaceError = SDL.GetError() ?? "Unknown error (getting window surface for software renderer)"; + string relevantError = string.IsNullOrEmpty(initialRendererError) || initialRendererError.Contains("Unknown error") ? windowSurfaceError : initialRendererError; + Logger.Error($"SDL.GetWindowSurface failed. Relevant Error: {relevantError}"); + SDL.DestroyWindow(window); + window = nint.Zero; + isWindowOpen = false; + return false; + } + + Logger.Debug("SDL.GetWindowSurface succeeded for software fallback."); + + renderer = SDL.CreateSoftwareRenderer(surface); + if (renderer == nint.Zero) + { + string softwareRendererError = SDL.GetError() ?? "Unknown error (software renderer)"; + string combinedError = string.IsNullOrEmpty(initialRendererError) || initialRendererError.Contains("Unknown error") ? softwareRendererError : initialRendererError; + if (!string.IsNullOrEmpty(softwareRendererError) && !softwareRendererError.Contains("Unknown error") && softwareRendererError != initialRendererError) + { + combinedError += $" (Software attempt also failed: {softwareRendererError})"; + } + + Logger.Error($"SDL.CreateSoftwareRenderer failed. Combined/Relevant Error: {combinedError}"); + SDL.DestroyWindow(window); + window = nint.Zero; + isWindowOpen = false; + return false; + } + + Logger.Info($"Successfully created software renderer. RendererPtr: {renderer}"); + } + else + { + Logger.Info($"Successfully created hardware renderer. RendererPtr: {renderer}"); + } + + isWindowOpen = true; + Logger.Info($"SetMode completed. isWindowOpen: {isWindowOpen}, Window.Handle: {Handle}, RendererPtr: {RendererPtr}"); + return true; + } + } + + /// + /// Gets the current window mode (width, height, and flags). + /// + /// A WindowMode struct containing width, height, and current flags. + public static WindowMode GetMode() + { + if (window == nint.Zero) + { + return new WindowMode + { + Width = 0, + Height = 0, + PixelWidth = 0, + PixelHeight = 0, + Fullscreen = false, + FullscreenType = currentFullscreenType, + Borderless = false, + Resizable = false, + HighDpi = false, + MinWidth = 0, + MinHeight = 0, + MaxWidth = 0, + MaxHeight = 0, + X = 0, + Y = 0, + Title = string.Empty, + Vsync = 0, + Msaa = 0, + Centered = false, + Display = 0, + RefreshRate = 0, + }; + } + + _ = SDL.GetWindowSize(window, out int w, out int h); + _ = SDL.GetWindowSizeInPixels(window, out int pw, out int ph); + var flags = SDL.GetWindowFlags(window); + var (isFullscreen, fsType) = GetFullscreen(); + + _ = SDL.GetWindowMinimumSize(window, out int minW, out int minH); + _ = SDL.GetWindowMaximumSize(window, out int maxW, out int maxH); + _ = SDL.GetWindowPosition(window, out int x, out int y); + string title = SDL.GetWindowTitle(window) ?? string.Empty; + + int vsyncState = 0; + if (renderer != nint.Zero) + { + if (SDL.GetRenderVSync(renderer, out int vsyncEnabledValue)) + { + vsyncState = vsyncEnabledValue; + } + } + + uint currentDisplayID = SDL.GetDisplayForWindow(window); + bool isCentered = false; + if (currentDisplayID != 0) + { + isCentered = x == (int)(SDL.WindowposCenteredMask | currentDisplayID) && y == (int)(SDL.WindowposCenteredMask | currentDisplayID); + } + + return new WindowMode + { + Width = w, + Height = h, + PixelWidth = pw, + PixelHeight = ph, + Fullscreen = isFullscreen, + FullscreenType = fsType, + Borderless = (flags & SDL.WindowFlags.Borderless) != 0, + Resizable = (flags & SDL.WindowFlags.Resizable) != 0, + HighDpi = (flags & SDL.WindowFlags.HighPixelDensity) != 0, + MinWidth = minW, + MinHeight = minH, + MaxWidth = maxW, + MaxHeight = maxH, + X = x, + Y = y, + Title = title, + Vsync = vsyncState, + Msaa = 0, + Centered = isCentered, + Display = (int)currentDisplayID, + RefreshRate = (int)(SDL.GetCurrentDisplayMode(currentDisplayID)?.RefreshRate ?? 0), + }; + } + } +} diff --git a/src/Night/Window/Window.cs b/src/Night/Window/Window.cs index 82ff70c8..56a2ae03 100644 --- a/src/Night/Window/Window.cs +++ b/src/Night/Window/Window.cs @@ -23,6 +23,9 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; +using System.Threading; + +using Night.Log; using SDL3; @@ -31,12 +34,13 @@ namespace Night /// /// Provides an interface for modifying and retrieving information about the program's window. /// - public static class Window + public static partial class Window { + private static readonly ILogger Logger = LogManager.GetLogger("Night.Window.Window"); + private static readonly object WindowLock = new object(); // Thread synchronization for window operations + private static nint window = nint.Zero; private static nint renderer = nint.Zero; - - // private static bool isVideoInitialized = false; // Removed: SDL lifecycle managed externally (e.g., by SDLFixture or Framework.Run) private static bool isWindowOpen = false; private static FullscreenType currentFullscreenType = FullscreenType.Desktop; private static ImageData? currentIconData = null; @@ -63,13 +67,13 @@ public static bool SetIcon(string imagePath) if (window == nint.Zero) { - Console.WriteLine("Night.Window.SetIcon: Window handle is null. Icon not set."); + Logger.Warn("Window handle is null. Icon not set."); return false; } if (string.IsNullOrEmpty(imagePath)) { - Console.WriteLine("Night.Window.SetIcon: imagePath is null or empty. Icon not set."); + Logger.Warn("imagePath is null or empty. Icon not set."); return false; } @@ -78,7 +82,7 @@ public static bool SetIcon(string imagePath) if (loadedSurfacePtr == nint.Zero) { string imgError = SDL.GetError(); - Console.WriteLine($"Night.Window.SetIcon: Failed to load image '{imagePath}' using SDL_image. Error: {imgError}"); + Logger.Error($"Failed to load image '{imagePath}' using SDL_image. Error: {imgError}"); return false; } @@ -88,7 +92,7 @@ public static bool SetIcon(string imagePath) if (convertedSurfacePtr == nint.Zero) { string sdlError = SDL.GetError(); - Console.WriteLine($"Night.Window.SetIcon: Failed to convert surface to target format. SDL Error: {sdlError}"); + Logger.Error($"Failed to convert surface to target format. SDL Error: {sdlError}"); SDL.DestroySurface(loadedSurfacePtr); return false; } @@ -98,7 +102,7 @@ public static bool SetIcon(string imagePath) if (!SDL.SetWindowIcon(window, convertedSurfacePtr)) { string sdlError = SDL.GetError(); - Console.WriteLine($"Night.Window.SetIcon: SDL_SetWindowIcon failed. SDL Error: {sdlError}"); + Logger.Error($"SDL_SetWindowIcon failed. SDL Error: {sdlError}"); return false; } @@ -110,7 +114,7 @@ public static bool SetIcon(string imagePath) if (detailsPtr == IntPtr.Zero) { string sdlError = SDL.GetError(); - Console.WriteLine($"Night.Window.SetIcon: Failed to get pixel format details. SDL Error: {sdlError}"); + Logger.Error($"Failed to get pixel format details. SDL Error: {sdlError}"); return false; } @@ -119,7 +123,7 @@ public static bool SetIcon(string imagePath) if (bytesPerPixel != 4) { - Console.WriteLine($"Night.Window.SetIcon: Converted surface is not 4bpp as expected for RGBA. Actual bpp: {bytesPerPixel}, Format: {convertedSurfaceStruct.Format}"); + Logger.Error($"Converted surface is not 4bpp as expected for RGBA. Actual bpp: {bytesPerPixel}, Format: {convertedSurfaceStruct.Format}"); return false; } @@ -131,7 +135,7 @@ public static bool SetIcon(string imagePath) } catch (Exception e) { - Console.WriteLine($"Night.Window.SetIcon: Error processing surface or creating ImageData. Error: {e.Message}"); + Logger.Error($"Error processing surface or creating ImageData.", e); return false; } finally @@ -157,57 +161,6 @@ public static bool SetIcon(string imagePath) return currentIconData; } - /// - /// Sets the display mode and properties of the window. - /// - /// The width of the window. - /// The height of the window. - /// SDL Window flags to apply. - /// True if the mode was set successfully, false otherwise. - public static bool SetMode(int width, int height, SDL.WindowFlags flags) - { - // Assuming SDL Video subsystem is initialized by the caller (e.g., Framework.Run or SDLFixture for tests) - // if (!isVideoInitialized) // Removed - // { - // if (!SDL.InitSubSystem(SDL.InitFlags.Video)) // Removed - // { - // return false; - // } - // isVideoInitialized = true; // Removed - // } - if (window != nint.Zero) - { - if (renderer != nint.Zero) - { - SDL.DestroyRenderer(renderer); - renderer = nint.Zero; - } - - SDL.DestroyWindow(window); - window = nint.Zero; - isWindowOpen = false; - } - - window = SDL.CreateWindow("Night Engine", width, height, flags); - if (window == nint.Zero) - { - isWindowOpen = false; - return false; - } - - renderer = SDL.CreateRenderer(window, null); - if (renderer == nint.Zero) - { - SDL.DestroyWindow(window); - window = nint.Zero; - isWindowOpen = false; - return false; - } - - isWindowOpen = true; - return true; - } - /// /// Sets the window title. /// @@ -233,189 +186,21 @@ public static void SetTitle(string title) /// True if the window is open, false otherwise. public static bool IsOpen() { - return isWindowOpen && window != nint.Zero; + // Added more explicit check for debugging + bool result = isWindowOpen && window != nint.Zero && renderer != nint.Zero; + + return result; } /// /// Signals that the window should close. - /// This is typically called by the engine when a quit event is received. - /// TODO: Does this need to align with Love2D more? https://love2d.org/wiki/love.window.close. /// public static void Close() { + Logger.Info($"Window.Close called. Setting isWindowOpen to false. Current window handle: {window}"); isWindowOpen = false; } - /// - /// Gets the number of connected monitors. - /// - /// The number of currently connected displays. - public static int GetDisplayCount() - { - // Assuming SDL Video subsystem is initialized by the caller - // if (!isVideoInitialized) // Removed - // { - // EnsureVideoInitialized(); // Removed - // } - uint[]? displays = SDL.GetDisplays(out int count); - if (displays == null || count < 0) - { - return 0; - } - - return count; - } - - /// - /// Gets the width and height of the desktop. - /// - /// The index of the display to query (0 for the primary display). - /// A tuple containing the width and height of the desktop, or (0,0) if an error occurs. - public static (int Width, int Height) GetDesktopDimensions(int displayIndex = 0) - { - // Assuming SDL Video subsystem is initialized by the caller - // if (!isVideoInitialized) // Removed - // { - // EnsureVideoInitialized(); // Removed - // } - uint[]? actualDisplayIDs = SDL.GetDisplays(out int displayCount); - if (actualDisplayIDs == null || displayCount <= 0 || displayIndex < 0 || displayIndex >= displayCount) - { - return (0, 0); - } - - uint targetDisplayID = actualDisplayIDs[displayIndex]; - - SDL.DisplayMode? mode = SDL.GetDesktopDisplayMode(targetDisplayID); - if (mode == null) - { - return (0, 0); - } - - return (mode.Value.W, mode.Value.H); - } - - /// - /// Gets whether the window is fullscreen. - /// - /// A tuple: (bool IsFullscreen, FullscreenType FsType). - /// IsFullscreen is true if the window is in any fullscreen mode, false otherwise. - /// FsType indicates the type of fullscreen mode used. - public static (bool IsFullscreen, FullscreenType FsType) GetFullscreen() - { - if (window == nint.Zero) - { - return (false, currentFullscreenType); - } - - var flags = SDL.GetWindowFlags(window); - - // Check for SDL's native/exclusive fullscreen first - if ((flags & SDL.WindowFlags.Fullscreen) != 0) - { - return (true, FullscreenType.Exclusive); - } - - // Check for our "Desktop Fullscreen" mode - if (currentFullscreenType == FullscreenType.Desktop && (flags & SDL.WindowFlags.Borderless) != 0) - { - return (true, FullscreenType.Desktop); - } - - return (false, currentFullscreenType); - } - - /// - /// Enters or exits fullscreen. The display to use when entering fullscreen is chosen - /// based on which display the window is currently in, if multiple monitors are connected. - /// - /// Whether to enter or exit fullscreen mode. - /// The type of fullscreen mode to use (Desktop or Exclusive). - /// True if the operation was successful, false otherwise. - public static bool SetFullscreen(bool fullscreen, FullscreenType fsType = FullscreenType.Desktop) - { - if (window == nint.Zero) - { - return false; - } - - if (fullscreen) - { - currentFullscreenType = fsType; - if (fsType == FullscreenType.Exclusive) - { - uint displayID = SDL.GetDisplayForWindow(window); - if (displayID == 0 && SDL.GetError() != null && SDL.GetError().Length > 0) - { - return false; - } - - SDL.DisplayMode? dm = SDL.GetDesktopDisplayMode(displayID); - if (dm.HasValue) - { - if (!SDL.SetWindowFullscreenMode(window, dm.Value)) - { - return false; - } - } - else - { - return false; - } - } - else - { - if (!SDL.SetWindowFullscreenMode(window, nint.Zero)) - { - // This might not be critical if it fails. - } - - if (!SDL.SetWindowBordered(window, false)) - { - return false; - } - - uint displayID = SDL.GetDisplayForWindow(window); - if (displayID == 0 && SDL.GetError() != null && SDL.GetError().Length > 0) - { - return false; - } - - var (desktopW, desktopH) = GetDesktopDimensionsForDisplayID(displayID); - - if (desktopW > 0 && desktopH > 0) - { - _ = SDL.SetWindowPosition(window, 0, 0); - if (!SDL.SetWindowSize(window, desktopW, desktopH)) - { - // Even if this fails to resize, we've set it borderless. - // The issue of it not resizing is separate from the borderless toggle. - } - } - else - { - return false; - } - } - } - else - { - currentFullscreenType = FullscreenType.Desktop; // Conceptually, when we exit, we are aiming for a non-fullscreen desktop window. - _ = SDL.SetWindowFullscreenMode(window, nint.Zero); // Turn off SDL's exclusive fullscreen - - if (!SDL.SetWindowBordered(window, true)) - { - return false; - } - - _ = SDL.RestoreWindow(window); - _ = SDL.SetWindowSize(window, 800, 600); // Explicitly set a defined windowed size. - _ = SDL.RaiseWindow(window); - } - - return true; - } - /// /// Gets a list of available fullscreen display modes for a given display. /// @@ -423,11 +208,6 @@ public static bool SetFullscreen(bool fullscreen, FullscreenType fsType = Fullsc /// A list of (Width, Height) tuples representing available modes, or an empty list on error. public static List<(int Width, int Height)> GetFullscreenModes(int displayIndex = 0) { - // Assuming SDL Video subsystem is initialized by the caller - // if (!isVideoInitialized) // Removed - // { - // EnsureVideoInitialized(); // Removed - // } var modesList = new List<(int Width, int Height)>(); var uniqueModes = new HashSet<(int Width, int Height)>(); @@ -438,7 +218,6 @@ public static bool SetFullscreen(bool fullscreen, FullscreenType fsType = Fullsc } uint targetDisplayID = actualDisplayIDs[displayIndex]; - SDL.DisplayMode[]? displayModes = SDL.GetFullscreenDisplayModes(targetDisplayID, out int count); if (displayModes == null || count <= 0 || displayModes.Length != count) @@ -459,188 +238,126 @@ public static bool SetFullscreen(bool fullscreen, FullscreenType fsType = Fullsc } /// - /// Gets the current window mode (width, height, and flags). + /// Gets the DPI scaling factor of the display containing the window. /// - /// A WindowMode struct containing width, height, and current flags. - public static WindowMode GetMode() + /// The DPI scaling factor (e.g., 1.0f for 96 DPI, 2.0f for 192 DPI), or 1.0f if unable to determine. + public static float GetDPIScale() { if (window == nint.Zero) { - return new WindowMode { Width = 0, Height = 0, Fullscreen = false, FullscreenType = currentFullscreenType, Borderless = false }; + return 1.0f; } - _ = SDL.GetWindowSize(window, out int w, out int h); - var flags = SDL.GetWindowFlags(window); - - bool isSdlExclusiveFullscreen = (flags & SDL.WindowFlags.Fullscreen) != 0; - bool isSdlBorderless = (flags & SDL.WindowFlags.Borderless) != 0; - FullscreenType reportedFsType = currentFullscreenType; - - bool actualReportedFullscreenState; - - if (isSdlExclusiveFullscreen) + uint displayID = SDL.GetDisplayForWindow(window); + if (displayID == 0 && !string.IsNullOrEmpty(SDL.GetError())) { - actualReportedFullscreenState = true; - reportedFsType = FullscreenType.Exclusive; - } - else if (isSdlBorderless) - { - if (currentFullscreenType == FullscreenType.Desktop) + displayID = SDL.GetPrimaryDisplay(); + if (displayID == 0 && !string.IsNullOrEmpty(SDL.GetError())) { - uint currentDisplayID = SDL.GetDisplayForWindow(window); - if (currentDisplayID != 0) - { - var (desktopW, desktopH) = GetDesktopDimensionsForDisplayID(currentDisplayID); - if (w == desktopW && h == desktopH) - { - actualReportedFullscreenState = true; - } - else - { - actualReportedFullscreenState = false; - } - } - else - { - actualReportedFullscreenState = false; - } - } - else - { - actualReportedFullscreenState = false; + return 1.0f; } } - else - { - actualReportedFullscreenState = false; - } - - return new WindowMode - { - Width = w, - Height = h, - Fullscreen = actualReportedFullscreenState, - FullscreenType = reportedFsType, - Borderless = isSdlBorderless, - }; - } - /// - /// Gets the DPI scale factor of the display containing the window. - /// - /// The DPI scale factor, or 1.0f on error or if not applicable. - public static float GetDPIScale() - { - if (window == nint.Zero) + if (displayID == 0) { return 1.0f; } - float dpiScale = SDL.GetWindowDisplayScale(window); - if (dpiScale <= 0f) + float contentScale = SDL.GetDisplayContentScale(displayID); + if (contentScale > 0.0f) { - return 1.0f; + return contentScale; } + else + { + _ = SDL.GetWindowSize(window, out int windowWidth, out _); + _ = SDL.GetWindowSizeInPixels(window, out int pixelWidth, out _); + + if (windowWidth > 0 && pixelWidth > 0 && pixelWidth != windowWidth) + { + return (float)pixelWidth / windowWidth; + } - return dpiScale; + return 1.0f; + } } /// - /// Converts a value from density-independent units to pixels, using the window's current DPI scale. + /// Converts a value from logical units to pixels, using the window's DPI scale. /// - /// The value in density-independent units. - /// The equivalent value in pixels. + /// The value in logical units. + /// The value in pixels. public static float ToPixels(float value) { return value * GetDPIScale(); } /// - /// Converts a value from pixels to density-independent units, using the window's current DPI scale. + /// Converts a value from pixels to logical units, using the window's DPI scale. /// /// The value in pixels. - /// The equivalent value in density-independent units. + /// The value in logical units. public static float FromPixels(float value) { - float dpiScale = GetDPIScale(); - if (dpiScale == 0f) - { - return value; - } - - return value / dpiScale; + float scale = GetDPIScale(); + return scale == 0 ? value : value / scale; } /// - /// Internal method to shut down the window and renderer, and quit the video subsystem. - /// Should be called by the FrameworkLoop at the end of the application. + /// Cleans up window and renderer resources. /// internal static void Shutdown() { - if (renderer != nint.Zero) + lock (WindowLock) { - SDL.DestroyRenderer(renderer); - renderer = nint.Zero; - } + Logger.Info($"Shutdown called. Current window: {window}, renderer: {renderer}"); - if (window != nint.Zero) - { - SDL.DestroyWindow(window); - window = nint.Zero; - } + if (renderer != nint.Zero) + { + SDL.DestroyRenderer(renderer); + renderer = nint.Zero; + Logger.Debug("Renderer destroyed."); + } - // Do not call SDL.QuitSubSystem here. Lifecycle managed externally. - // if (isVideoInitialized) // Removed - // { - // SDL.QuitSubSystem(SDL.InitFlags.Video); // Removed - // isVideoInitialized = false; // Removed - // } - isWindowOpen = false; + if (window != nint.Zero) + { + SDL.DestroyWindow(window); + window = nint.Zero; + Logger.Debug("Window destroyed."); + } + + ResetInternalState(); + Logger.Debug("State reset."); + } } /// - /// Resets the internal static state of the Window class without quitting the SDL video subsystem. - /// This is intended for use in testing scenarios where the SDL lifecycle is managed externally. + /// Resets internal state variables of the Window module. /// internal static void ResetInternalState() { - if (renderer != nint.Zero) - { - SDL.DestroyRenderer(renderer); - renderer = nint.Zero; - } - - if (window != nint.Zero) - { - SDL.DestroyWindow(window); - window = nint.Zero; - } - - // Do not call SDL.QuitSubSystem(SDL.InitFlags.Video) here. - // Only reset the internal flag for Night.Window's own state. - // isVideoInitialized = false; // Removed as the field is removed. + Logger.Debug("ResetInternalState called."); isWindowOpen = false; - currentIconData = null; currentFullscreenType = FullscreenType.Desktop; + currentIconData = null; } - // EnsureVideoInitialized() method removed as Night.Window no longer manages SDL video subsystem init. - /// - /// Gets the dimensions of the desktop for a specific display ID. + /// Helper to get desktop dimensions for a specific display ID. /// - /// The actual ID of the display to query. - /// A tuple containing the width and height of the desktop, or (0,0) if an error occurs. + /// The SDL display ID. + /// Tuple of (Width, Height), or (0,0) on error. private static (int Width, int Height) GetDesktopDimensionsForDisplayID(uint displayID) { - // Assuming SDL Video subsystem is initialized by the caller - // if (!isVideoInitialized) // Removed - // { - // EnsureVideoInitialized(); // Removed - // } + if (displayID == 0) + { + return (0, 0); + } + SDL.DisplayMode? mode = SDL.GetDesktopDisplayMode(displayID); if (mode == null) { + Logger.Warn($"GetDesktopDimensionsForDisplayID: Failed to get desktop display mode for display {displayID}. SDL Error: {SDL.GetError()}"); return (0, 0); } diff --git a/src/Night/Window/WindowMode.cs b/src/Night/Window/WindowMode.cs index 402ebb31..1d7ca68f 100644 --- a/src/Night/Window/WindowMode.cs +++ b/src/Night/Window/WindowMode.cs @@ -30,15 +30,25 @@ namespace Night public struct WindowMode { /// - /// Gets or sets the window width in pixels. + /// Gets or sets the window width in logical units. /// public int Width; /// - /// Gets or sets the window height in pixels. + /// Gets or sets the window height in logical units. /// public int Height; + /// + /// Gets or sets the window width in physical pixels. + /// + public int PixelWidth; + + /// + /// Gets or sets the window height in physical pixels. + /// + public int PixelHeight; + /// /// Gets or sets a value indicating whether the window is in fullscreen mode. /// @@ -90,7 +100,17 @@ public struct WindowMode public int MinHeight; /// - /// Gets or sets a value indicating whether high-dpi mode is allowed on Retina displays (macOS). + /// Gets or sets the maximum width of the window, if resizable. + /// + public int MaxWidth; + + /// + /// Gets or sets the maximum height of the window, if resizable. + /// + public int MaxHeight; + + /// + /// Gets or sets a value indicating whether high-dpi mode is allowed. /// public bool HighDpi; @@ -108,5 +128,10 @@ public struct WindowMode /// Gets or sets the y-coordinate of the window's position. /// public int Y; + + /// + /// Gets or sets the window title. + /// + public string Title; } } diff --git a/src/SampleGame/Game.cs b/src/SampleGame/Game.cs deleted file mode 100644 index 2b368d01..00000000 --- a/src/SampleGame/Game.cs +++ /dev/null @@ -1,287 +0,0 @@ -// -// zlib license -// -// Copyright (c) 2025 Danny Solivan, Night Circle -// -// This software is provided 'as-is', without any express or implied -// warranty. In no event will the authors be held liable for any damages -// arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose, -// including commercial applications, and to alter it and redistribute it -// freely, subject to the following restrictions: -// -// 1. The origin of this software must not be misrepresented; you must not -// claim that you wrote the original software. If you use this software -// in a product, an acknowledgment in the product documentation would be -// appreciated but is not required. -// 2. Altered source versions must be plainly marked as such, and must not be -// misrepresented as being the original software. -// 3. This notice may not be removed or altered from any source distribution. -// - -using System; -using System.Collections.Generic; -using System.IO; - -using Night; - -using SDL3; - -namespace SampleGame; - -/// -/// Main game class for the platformer sample. -/// Implements the interface for Night.Engine integration. -/// -public class Game : IGame -{ - private Player player; - private List platforms; - private Night.Sprite? platformSprite; - private Night.Rectangle goalPlatform; - private bool goalReachedMessageShown = false; // To ensure message prints only once - - /// - /// Initializes a new instance of the class. - /// - public Game() - { - this.player = new Player(); - this.platforms = new List(); - } - - /// - /// Loads game assets and initializes game state. - /// Called once at the start of the game by the Night.Engine. - /// - public void Load() - { - // _ = Window.SetMode(800, 600, SDL.WindowFlags.Resizable); - // Window.SetTitle("Night Platformer Sample"); - // Window settings will now be driven by config.json (or defaults if not present/configured) - this.player.Load(); - - // Load platform sprite - string baseDirectory = AppContext.BaseDirectory; - string platformImageRelativePath = Path.Combine("assets", "images", "pixel_green.png"); - string platformImageFullPath = Path.Combine(baseDirectory, platformImageRelativePath); - this.platformSprite = Graphics.NewImage(platformImageFullPath); - if (this.platformSprite == null) - { - Console.WriteLine($"Game.Load: Failed to load platform sprite at '{platformImageFullPath}'. Platforms will not be drawn."); - } - - // Initialize platforms (as per docs/epics/epic7-design.md) - this.platforms.Add(new Night.Rectangle(50, 500, 700, 50)); - this.platforms.Add(new Night.Rectangle(200, 400, 150, 30)); - this.platforms.Add(new Night.Rectangle(450, 300, 100, 30)); - this.goalPlatform = new Night.Rectangle(600, 200, 100, 30); - this.platforms.Add(this.goalPlatform); - - // Set the window icon (assuming icon is in assets/icon.png relative to executable) - // This path will be resolved by Night.Framework if specified in config.json via IconPath. - // If not in config, or if this call is made after Framework has set from config, - // this explicit call can override or set it if not in config. - // For the sample, we'll rely on the config first, but this shows direct API usage. - // If you want the SampleGame to ALWAYS use a specific icon regardless of config, call it here. - // For now, we let config drive it. If you want to test direct SetIcon: - string iconRelativePath = Path.Combine("assets", "icon.png"); - string iconFullPath = Path.Combine(AppContext.BaseDirectory, iconRelativePath); - _ = Window.SetIcon(iconFullPath); - Console.WriteLine($"Attempted to set icon from Game.Load. Current icon: {Window.GetIcon()}"); - } - - /// - /// Updates the game state. - /// Called every frame by the Night.Engine. - /// - /// The time elapsed since the last frame, in seconds. - public void Update(double deltaTime) - { - this.player.Update(deltaTime, this.platforms); - - // Check if player reached the goal platform - // Adjust playerBounds slightly for the goal check to ensure "touching" counts, - // as player might be perfectly aligned on top. - Night.Rectangle playerBoundsForGoalCheck = new Night.Rectangle((int)this.player.X, (int)this.player.Y, this.player.Width, this.player.Height + 1); - if (CheckAABBCollision(playerBoundsForGoalCheck, this.goalPlatform) && !this.goalReachedMessageShown) - { - // Simple win condition: print a message. - // A real game might change state, show a UI, etc. - Console.WriteLine("Congratulations! Goal Reached!"); - this.goalReachedMessageShown = true; // Set flag so it doesn't print again - - // Optionally, could close the game or trigger another action: - // Window.Close(); // Window class will be in Night.Framework - } - } - - /// - /// Draws the game scene. - /// Called every frame by the Night.Engine after Update. - /// - public void Draw() - { - Graphics.Clear(new Night.Color(135, 206, 235)); // Sky blue background - - // Draw platforms - if (this.platformSprite != null) - { - foreach (var platform in this.platforms) - { - // Scale the 1x1 pixel sprite to the platform's dimensions - Graphics.Draw( - this.platformSprite, - platform.X, - platform.Y, - 0, - platform.Width, - platform.Height); - } - } - - this.player.Draw(); - - // --- Graphics Shape Drawing Demonstration (Top-Left Corner) --- - // All coordinates and sizes are adjusted to fit in a smaller area. - // Base offset for the demo shapes - int demoXOffset = 10; - int demoYOffset = 10; - int shapeSize = 20; // General size for smaller shapes - int spacing = 5; // Spacing between shapes - - // Rectangle Demo - Graphics.SetColor(Night.Color.Red); - Graphics.Rectangle(Night.DrawMode.Fill, demoXOffset, demoYOffset, shapeSize, shapeSize / 2); // Smaller Red Rectangle - Graphics.SetColor(Night.Color.Black); - Graphics.Rectangle(Night.DrawMode.Line, demoXOffset, demoYOffset, shapeSize, shapeSize / 2); - - demoXOffset += shapeSize + spacing; // Move right for next shape - - Graphics.SetColor(0, 0, 255, 128); // Semi-transparent Blue - Graphics.Rectangle(Night.DrawMode.Line, demoXOffset, demoYOffset, shapeSize - 5, shapeSize + 5); // Adjusted Blue Rectangle - - demoXOffset += (shapeSize - 5) + spacing; // Move right - - // Circle Demo - Graphics.SetColor(Night.Color.Green); - Graphics.Circle(Night.DrawMode.Fill, demoXOffset + (shapeSize / 2), demoYOffset + (shapeSize / 2), shapeSize / 2); // Smaller Green Circle - Graphics.SetColor(Night.Color.Black); - Graphics.Circle(Night.DrawMode.Line, demoXOffset + (shapeSize / 2), demoYOffset + (shapeSize / 2), shapeSize / 2, 12); // 12 segments - - demoXOffset += shapeSize + spacing; // Move right - - Graphics.SetColor(Night.Color.Yellow); - Graphics.Circle(Night.DrawMode.Line, demoXOffset + (shapeSize / 3), demoYOffset + (shapeSize / 3), shapeSize / 3, 6); // Smaller Hexagon - - // Reset X offset for a new "row" of shapes if needed, or continue right - // For this demo, we'll just continue right and assume enough horizontal space for this small demo. - // If more shapes were added, a new row would be demoYOffset += shapeSize + spacing; demoXOffset = 10; - demoXOffset += (shapeSize / 3 * 2) + spacing; // Move right based on hexagon diameter - - // Line Demo - Graphics.SetColor(Night.Color.Magenta); - Graphics.Line(demoXOffset, demoYOffset, demoXOffset + shapeSize, demoYOffset + (shapeSize / 2)); // Smaller Magenta Line - - demoXOffset += shapeSize + spacing; - - Night.PointF[] linePoints = new Night.PointF[] - { - new Night.PointF(demoXOffset, demoYOffset), - new Night.PointF(demoXOffset + (shapeSize / 3), demoYOffset + (shapeSize / 2)), - new Night.PointF(demoXOffset + (shapeSize * 2 / 3), demoYOffset), - new Night.PointF(demoXOffset + shapeSize, demoYOffset + (shapeSize / 2)), - }; - Graphics.SetColor(Night.Color.Cyan); - Graphics.Line(linePoints); // Smaller Polyline in Cyan - - demoXOffset += shapeSize + spacing; - - // Polygon Demo - Night.PointF[] triangleVertices = new Night.PointF[] - { - new Night.PointF(demoXOffset + (shapeSize / 2), demoYOffset), - new Night.PointF(demoXOffset + shapeSize, demoYOffset + shapeSize), - new Night.PointF(demoXOffset, demoYOffset + shapeSize), - }; - Graphics.SetColor(new Night.Color(255, 165, 0)); // Orange - Graphics.Polygon(Night.DrawMode.Fill, triangleVertices); // Smaller Orange Triangle - Graphics.SetColor(Night.Color.Black); - Graphics.Polygon(Night.DrawMode.Line, triangleVertices); - - demoXOffset += shapeSize + spacing; - - Night.PointF[] pentagonVertices = new Night.PointF[] - { - new Night.PointF(demoXOffset + (shapeSize / 2), demoYOffset), - new Night.PointF(demoXOffset + shapeSize, demoYOffset + (shapeSize / 3)), - new Night.PointF(demoXOffset + (shapeSize * 2 / 3), demoYOffset + shapeSize), - new Night.PointF(demoXOffset + (shapeSize / 3), demoYOffset + shapeSize), - new Night.PointF(demoXOffset, demoYOffset + (shapeSize / 3)), - }; - Graphics.SetColor(new Night.Color(75, 0, 130)); // Indigo - Graphics.Polygon(Night.DrawMode.Line, pentagonVertices); // Smaller Pentagon - - // --- Test Large Filled Rectangle --- - Graphics.SetColor(Night.Color.Blue); - Graphics.Rectangle(Night.DrawMode.Fill, 300, 200, 200, 150); // Large Blue Filled Rectangle Test - - // --- End Test Large Filled Rectangle --- - } - - /// - /// Handles key press events. - /// Called by Night.Engine when a key is pressed. - /// - /// The of the pressed key. - /// The (physical key code) of the pressed key. - /// True if this is a repeat key event, false otherwise. - public void KeyPressed(Night.KeySymbol key, Night.KeyCode scancode, bool isRepeat) - { - // Minimal key handling for now, primarily for closing the window. - if (key == Night.KeySymbol.Escape) - { - Window.Close(); - } - - // Test error triggering - if (key == Night.KeySymbol.E && !isRepeat) - { - throw new InvalidOperationException("Test error triggered by pressing 'E' in SampleGame!"); - } - - // --- Night.Window Demo: Toggle Fullscreen --- - if (key == Night.KeySymbol.F11) - { - var (isFullscreen, _) = Window.GetFullscreen(); - bool success = Window.SetFullscreen(!isFullscreen, Night.FullscreenType.Desktop); - Console.WriteLine($"SetFullscreen to {!isFullscreen} (Desktop) attempt: {(success ? "Success" : "Failed")}"); - var newMode = Window.GetMode(); - Console.WriteLine($"New Window Mode: {newMode.Width}x{newMode.Height}, Fullscreen: {newMode.Fullscreen}, Type: {newMode.FullscreenType}, Borderless: {newMode.Borderless}"); - } - - if (key == Night.KeySymbol.F10) - { - var (isFullscreen, _) = Window.GetFullscreen(); - bool success = Window.SetFullscreen(!isFullscreen, Night.FullscreenType.Exclusive); - Console.WriteLine($"SetFullscreen to {!isFullscreen} (Exclusive) attempt: {(success ? "Success" : "Failed")}"); - var newMode = Window.GetMode(); - Console.WriteLine($"New Window Mode: {newMode.Width}x{newMode.Height}, Fullscreen: {newMode.Fullscreen}, Type: {newMode.FullscreenType}, Borderless: {newMode.Borderless}"); - } - - // --- End Night.Window Demo --- - } - - // Helper for collision detection (AABB) - private static bool CheckAABBCollision(Night.Rectangle rect1, Night.Rectangle rect2) - { - // True if the rectangles are overlapping - return rect1.X < rect2.X + rect2.Width && - rect1.X + rect1.Width > rect2.X && - rect1.Y < rect2.Y + rect2.Height && - rect1.Y + rect1.Height > rect2.Y; - } -} - -// Program class removed from here, will be in Program.cs diff --git a/src/SampleGame/Player.cs b/src/SampleGame/Player.cs index 128742ea..3b03a2be 100644 --- a/src/SampleGame/Player.cs +++ b/src/SampleGame/Player.cs @@ -48,7 +48,6 @@ public class Player /// public Player() { - // Initialize properties in Load() this.isGrounded = false; // Start in the air or assume Load sets initial grounded state } @@ -119,23 +118,57 @@ public void Load() /// /// The time elapsed since the last frame, in seconds. /// A list of objects representing solid platforms. - public void Update(double deltaTime, List platforms) + /// The current value of the joystick's horizontal axis (e.g., left stick X). + /// The current direction of the joystick's hat (e.g., D-pad). + /// True if the joystick 'A' button is currently pressed. + public void Update(double deltaTime, List platforms, float joystickAxisValue, Night.JoystickHat hatDirection, bool joystickAButtonPressed) { float dt = (float)deltaTime; + const float joystickDeadzone = 0.2f; + + // Logger.Debug( + // $"Player.Update START: dt={dt.ToString("F5", CultureInfo.InvariantCulture)}, " + + // $"X={this.X.ToString("F2", CultureInfo.InvariantCulture)}, Y={this.Y.ToString("F2", CultureInfo.InvariantCulture)}, " + + // $"vX={this.velocityX.ToString("F2", CultureInfo.InvariantCulture)}, vY={this.velocityY.ToString("F2", CultureInfo.InvariantCulture)}, " + + // $"Grounded={this.isGrounded}"); // 1. Handle Input & Apply Jump Impulse this.velocityX = 0; - if (Keyboard.IsDown(KeyCode.Left) || Keyboard.IsDown(KeyCode.A)) + + // Joystick Hat (D-Pad) input - highest priority for horizontal movement + if ((hatDirection & Night.JoystickHat.Left) != 0) { this.velocityX = -HorizontalSpeed; } - - if (Keyboard.IsDown(KeyCode.Right) || Keyboard.IsDown(KeyCode.D)) + else if ((hatDirection & Night.JoystickHat.Right) != 0) { this.velocityX = HorizontalSpeed; } - bool tryingToJump = Keyboard.IsDown(KeyCode.Space); + // Joystick Axis input - next priority if D-Pad is not active + else if (Math.Abs(joystickAxisValue) > joystickDeadzone) + { + this.velocityX = joystickAxisValue * HorizontalSpeed; + } + + // Keyboard input - lowest priority if no joystick input for horizontal movement + else + { + if (Keyboard.IsDown(KeyCode.Left) || Keyboard.IsDown(KeyCode.A)) + { + this.velocityX = -HorizontalSpeed; + } + + if (Keyboard.IsDown(KeyCode.Right) || Keyboard.IsDown(KeyCode.D)) + { + // If left was also pressed, this will override. If only right, it sets. + // If both, right takes precedence here due to order. + this.velocityX = HorizontalSpeed; + } + } + + // Jump input + bool tryingToJump = joystickAButtonPressed || Keyboard.IsDown(KeyCode.Space); if (tryingToJump && this.isGrounded) { this.velocityY = JumpStrength; @@ -256,14 +289,10 @@ public void Update(double deltaTime, List platforms) this.isGrounded = newIsGroundedThisFrame; - // If a jump was initiated and _isGrounded became false, - // and player is still moving upwards (_velocityY < 0), they are not grounded. - // This ensures that if a jump starts, _isGrounded remains false until landing. - // Check if jump was initiated *this frame* - if (tryingToJump && this.velocityY < 0) - { - this.isGrounded = false; - } + // Logger.Debug( + // $"Player.Update END: X={this.X.ToString("F2", CultureInfo.InvariantCulture)}, Y={this.Y.ToString("F2", CultureInfo.InvariantCulture)}, " + + // $"vX={this.velocityX.ToString("F2", CultureInfo.InvariantCulture)}, vY={this.velocityY.ToString("F2", CultureInfo.InvariantCulture)}, " + + // $"Grounded={this.isGrounded}"); // Prevent player from going off-screen left/right (simple boundary) // These values should ideally come from Window.GetWidth/Height if game resizes @@ -287,6 +316,7 @@ public void Update(double deltaTime, List platforms) /// public void Draw() { + // Logger.Debug($"Player.Draw: X={this.X.ToString("F2", CultureInfo.InvariantCulture)}, Y={this.Y.ToString("F2", CultureInfo.InvariantCulture)}"); if (this.playerSprite != null) { // If player_sprite_blue_32x64.png is exactly 32x64, scaleX and scaleY are 1. diff --git a/src/SampleGame/Program.cs b/src/SampleGame/Program.cs index 06c1d710..0f895e55 100644 --- a/src/SampleGame/Program.cs +++ b/src/SampleGame/Program.cs @@ -34,9 +34,10 @@ public class Program /// The main entry point for the application. /// Initializes and runs the game using the Night.Framework. /// - public static void Main() + /// Command-line arguments. + public static void Main(string[] args) { - Framework.Run(new Game()); + Framework.Run(new SamplePlatformerGame(), new CLI(args)); } } } diff --git a/src/SampleGame/SampleGame.csproj b/src/SampleGame/SampleGame.csproj index 43bd1525..245fa07a 100644 --- a/src/SampleGame/SampleGame.csproj +++ b/src/SampleGame/SampleGame.csproj @@ -29,24 +29,48 @@ SDL3.dll PreserveNewest
+ + libSDL3.0.dylib + PreserveNewest + libSDL3.dylib PreserveNewest + + SDL3 + PreserveNewest + + + libSDL3.so.0 + PreserveNewest + libSDL3.so PreserveNewest + + SDL3 + PreserveNewest + SDL3_image.dll PreserveNewest + + libSDL3_image.0.dylib + PreserveNewest + libSDL3_image.dylib PreserveNewest + + libSDL3_image.so.0 + PreserveNewest + libSDL3_image.so PreserveNewest diff --git a/src/SampleGame/SamplePlatformerGame.cs b/src/SampleGame/SamplePlatformerGame.cs new file mode 100644 index 00000000..dfcf8a67 --- /dev/null +++ b/src/SampleGame/SamplePlatformerGame.cs @@ -0,0 +1,540 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Night; + +using Night.Log.Sinks; + +using SDL3; + +namespace SampleGame; + +/// +/// Main game class for the platformer sample. +/// Inherits from for Night.Engine integration. +/// +public class SamplePlatformerGame : Night.Game +{ + private Player player; + private List platforms; + private Night.Sprite? platformSprite; + + // private static readonly ILogger Logger = LogManager.GetLogger(nameof(Game)); // Removed + private Night.Rectangle goalPlatform; + private bool goalReachedMessageShown = false; // To ensure message prints only once + + // Joystick input state + private float joystickAxis0Value = 0.0f; + private Night.JoystickHat joystickHat0Direction = Night.JoystickHat.Centered; // Fully qualified name + private bool joystickAButtonPressed = false; + private uint? inputProvidingJoystickId = null; // Store the ID of the joystick providing input (generic) + + // Gamepad specific input state + private float gamepadLeftXValue = 0.0f; + private bool gamepadAButtonPressed = false; + private uint? gamepadProvidingJoystickId = null; // Store the ID of the joystick providing gamepad input + + /// + /// Initializes a new instance of the class. + /// + public SamplePlatformerGame() + { + this.player = new Player(); + this.platforms = new List(); + } + + /// + /// Loads game assets and initializes game state. + /// Called once at the start of the game by the Night.Engine. + /// + public override void Load() + { + // _ = Window.SetMode(800, 600, SDL.WindowFlags.Resizable); + // Window.SetTitle("Night Platformer Sample"); + // Window settings will now be driven by config.json (or defaults if not present/configured) + this.player.Load(); + + // Load platform sprite + string baseDirectory = AppContext.BaseDirectory; + string platformImageRelativePath = Path.Combine("assets", "images", "pixel_green.png"); + string platformImageFullPath = Path.Combine(baseDirectory, platformImageRelativePath); + this.platformSprite = Graphics.NewImage(platformImageFullPath); + if (this.platformSprite == null) + { + Console.WriteLine($"SamplePlatformerGame.Load: Failed to load platform sprite at '{platformImageFullPath}'. Platforms will not be drawn."); + } + + // Initialize platforms (as per docs/epics/epic7-design.md) + this.platforms.Add(new Night.Rectangle(50, 500, 700, 50)); + this.platforms.Add(new Night.Rectangle(200, 400, 150, 30)); + this.platforms.Add(new Night.Rectangle(450, 300, 100, 30)); + this.goalPlatform = new Night.Rectangle(600, 200, 100, 30); + this.platforms.Add(this.goalPlatform); + + // Set the window icon (assuming icon is in assets/icon.ico relative to executable) + // This path will be resolved by Night.Framework if specified in config.json via IconPath. + // If not in config, or if this call is made after Framework has set from config, + // this explicit call can override or set it if not in config. + // For the sample, we'll rely on the config first, but this shows direct API usage. + // If you want the SampleGame to ALWAYS use a specific icon regardless of config, call it here. + // For now, we let config drive it. If you want to test direct SetIcon: + string iconRelativePath = Path.Combine("assets", "icon.ico"); + string iconFullPath = Path.Combine(AppContext.BaseDirectory, iconRelativePath); + _ = Window.SetIcon(iconFullPath); + Console.WriteLine($"Attempted to set icon from SamplePlatformerGame.Load. Current icon: {Window.GetIcon()}"); + } + + /// + /// Updates the game state. + /// Called every frame by the Night.Engine. + /// + /// The time elapsed since the last frame, in seconds. + public override void Update(double deltaTime) + { + // Logger.Debug($"SamplePlatformerGame.Update: deltaTime={deltaTime:F5}"); + + // Check if the input-providing joystick is still connected + float finalHorizontalInput = 0.0f; + Night.JoystickHat finalHatDirection = Night.JoystickHat.Centered; + bool finalJumpPressed = false; + + // Prioritize gamepad input if available and from the same joystick + if (this.gamepadProvidingJoystickId.HasValue) + { + Joystick? gamepadJoystick = Night.Joysticks.GetJoystickByInstanceId(this.gamepadProvidingJoystickId.Value); + if (gamepadJoystick != null && gamepadJoystick.IsConnected() && gamepadJoystick.IsGamepad()) + { + finalHorizontalInput = this.gamepadLeftXValue; + finalJumpPressed = this.gamepadAButtonPressed; + + // Gamepad typically doesn't directly map to a single "hat" for player movement in this simple setup, + // so we might still use raw joystick hat if needed, or ignore for gamepad. + // For simplicity, if gamepad is active for axis/button, we might ignore raw hat. + // Or, if player.Update needs hat, we could still get it from raw joystick state. + // For now, let's assume gamepad axis/button overrides hat for player control. + finalHatDirection = Night.JoystickHat.Centered; // Or decide how to integrate if player needs it + + // Log polled gamepad state for verification + Console.WriteLine($"SampleGame.Update: Polled Gamepad ID {gamepadJoystick.GetId()}: LeftX: {gamepadJoystick.GetGamepadAxis(Night.GamepadAxis.LeftX):F4}, A Button: {gamepadJoystick.IsGamepadDown(Night.GamepadButton.A)}"); + } + else + { + // Gamepad-providing joystick disconnected or no longer a gamepad + this.gamepadLeftXValue = 0.0f; + this.gamepadAButtonPressed = false; + this.gamepadProvidingJoystickId = null; + } + } + + // If gamepad input wasn't used, or to supplement it (e.g., for hat), check raw joystick input + if (this.inputProvidingJoystickId.HasValue && (!this.gamepadProvidingJoystickId.HasValue || this.gamepadProvidingJoystickId.Value != this.inputProvidingJoystickId.Value)) + { + Joystick? rawJoystick = Night.Joysticks.GetJoystickByInstanceId(this.inputProvidingJoystickId.Value); + if (rawJoystick != null && rawJoystick.IsConnected()) + { + if (!this.gamepadProvidingJoystickId.HasValue) + { + finalHorizontalInput = this.joystickAxis0Value; + finalJumpPressed = this.joystickAButtonPressed; + } + + finalHatDirection = this.joystickHat0Direction; // Always take raw hat for now + } + else + { + // Raw input-providing joystick disconnected + this.joystickAxis0Value = 0.0f; + this.joystickHat0Direction = Night.JoystickHat.Centered; + this.joystickAButtonPressed = false; + this.inputProvidingJoystickId = null; + } + } + + // If both gamepadProvidingJoystickId and inputProvidingJoystickId are null, inputs remain 0/false/Centered. + this.player.Update(deltaTime, this.platforms, finalHorizontalInput, finalHatDirection, finalJumpPressed); + + // Check if player reached the goal platform + // Adjust playerBounds slightly for the goal check to ensure "touching" counts, + // as player might be perfectly aligned on top. + Night.Rectangle playerBoundsForGoalCheck = new Night.Rectangle((int)this.player.X, (int)this.player.Y, this.player.Width, this.player.Height + 1); + if (CheckAABBCollision(playerBoundsForGoalCheck, this.goalPlatform) && !this.goalReachedMessageShown) + { + // Simple win condition: print a message. + // A real game might change state, show a UI, etc. + Console.WriteLine("Congratulations! Goal Reached!"); + this.goalReachedMessageShown = true; // Set flag so it doesn't print again + + // Optionally, could close the game or trigger another action: + // Window.Close(); // Window class will be in Night.Framework + } + } + + /// + /// Draws the game scene. + /// Called every frame by the Night.Engine after Update. + /// + public override void Draw() + { + // Logger.Debug("SamplePlatformerGame.Draw START"); + Graphics.Clear(new Night.Color(135, 206, 235)); // Sky blue background + + // Draw platforms + if (this.platformSprite != null) + { + foreach (var platform in this.platforms) + { + // Scale the 1x1 pixel sprite to the platform's dimensions + Graphics.Draw( + this.platformSprite, + platform.X, + platform.Y, + 0, + platform.Width, + platform.Height); + } + } + + this.player.Draw(); + + // --- Graphics Shape Drawing Demonstration (Top-Left Corner) --- + // All coordinates and sizes are adjusted to fit in a smaller area. + // Base offset for the demo shapes + int demoXOffset = 10; + int demoYOffset = 10; + int shapeSize = 20; // General size for smaller shapes + int spacing = 5; // Spacing between shapes + + // Rectangle Demo + Graphics.SetColor(Night.Color.Red); + Graphics.Rectangle(Night.DrawMode.Fill, demoXOffset, demoYOffset, shapeSize, shapeSize / 2); // Smaller Red Rectangle + Graphics.SetColor(Night.Color.Black); + Graphics.Rectangle(Night.DrawMode.Line, demoXOffset, demoYOffset, shapeSize, shapeSize / 2); + + demoXOffset += shapeSize + spacing; // Move right for next shape + + Graphics.SetColor(0, 0, 255, 128); // Semi-transparent Blue + Graphics.Rectangle(Night.DrawMode.Line, demoXOffset, demoYOffset, shapeSize - 5, shapeSize + 5); // Adjusted Blue Rectangle + + demoXOffset += (shapeSize - 5) + spacing; // Move right + + // Circle Demo + Graphics.SetColor(Night.Color.Green); + Graphics.Circle(Night.DrawMode.Fill, demoXOffset + (shapeSize / 2), demoYOffset + (shapeSize / 2), shapeSize / 2); // Smaller Green Circle + Graphics.SetColor(Night.Color.Black); + Graphics.Circle(Night.DrawMode.Line, demoXOffset + (shapeSize / 2), demoYOffset + (shapeSize / 2), shapeSize / 2, 12); // 12 segments + + demoXOffset += shapeSize + spacing; // Move right + + Graphics.SetColor(Night.Color.Yellow); + Graphics.Circle(Night.DrawMode.Line, demoXOffset + (shapeSize / 3), demoYOffset + (shapeSize / 3), shapeSize / 3, 6); // Smaller Hexagon + + // Reset X offset for a new "row" of shapes if needed, or continue right + // For this demo, we'll just continue right and assume enough horizontal space for this small demo. + // If more shapes were added, a new row would be demoYOffset += shapeSize + spacing; demoXOffset = 10; + demoXOffset += (shapeSize / 3 * 2) + spacing; // Move right based on hexagon diameter + + // Line Demo + Graphics.SetColor(Night.Color.Magenta); + Graphics.Line(demoXOffset, demoYOffset, demoXOffset + shapeSize, demoYOffset + (shapeSize / 2)); // Smaller Magenta Line + + demoXOffset += shapeSize + spacing; + + Night.PointF[] linePoints = new Night.PointF[] + { + new Night.PointF(demoXOffset, demoYOffset), + new Night.PointF(demoXOffset + (shapeSize / 3), demoYOffset + (shapeSize / 2)), + new Night.PointF(demoXOffset + (shapeSize * 2 / 3), demoYOffset), + new Night.PointF(demoXOffset + shapeSize, demoYOffset + (shapeSize / 2)), + }; + Graphics.SetColor(Night.Color.Cyan); + Graphics.Line(linePoints); // Smaller Polyline in Cyan + + demoXOffset += shapeSize + spacing; + + // Polygon Demo + Night.PointF[] triangleVertices = new Night.PointF[] + { + new Night.PointF(demoXOffset + (shapeSize / 2), demoYOffset), + new Night.PointF(demoXOffset + shapeSize, demoYOffset + shapeSize), + new Night.PointF(demoXOffset, demoYOffset + shapeSize), + }; + Graphics.SetColor(new Night.Color(255, 165, 0)); // Orange + Graphics.Polygon(Night.DrawMode.Fill, triangleVertices); // Smaller Orange Triangle + Graphics.SetColor(Night.Color.Black); + Graphics.Polygon(Night.DrawMode.Line, triangleVertices); + + demoXOffset += shapeSize + spacing; + + Night.PointF[] pentagonVertices = new Night.PointF[] + { + new Night.PointF(demoXOffset + (shapeSize / 2), demoYOffset), + new Night.PointF(demoXOffset + shapeSize, demoYOffset + (shapeSize / 3)), + new Night.PointF(demoXOffset + (shapeSize * 2 / 3), demoYOffset + shapeSize), + new Night.PointF(demoXOffset + (shapeSize / 3), demoYOffset + shapeSize), + new Night.PointF(demoXOffset, demoYOffset + (shapeSize / 3)), + }; + Graphics.SetColor(new Night.Color(75, 0, 130)); // Indigo + Graphics.Polygon(Night.DrawMode.Line, pentagonVertices); // Smaller Pentagon + + // --- Test Large Filled Rectangle --- + Graphics.SetColor(Night.Color.Blue); + Graphics.Rectangle(Night.DrawMode.Fill, 300, 200, 200, 150); // Large Blue Filled Rectangle Test + + // --- End Test Large Filled Rectangle --- + } + + /// + /// Handles key press events. + /// Called by Night.Engine when a key is pressed. + /// + /// The of the pressed key. + /// The (physical key code) of the pressed key. + /// True if this is a repeat key event, false otherwise. + public override void KeyPressed(Night.KeySymbol key, Night.KeyCode scancode, bool isRepeat) + { + // Minimal key handling for now, primarily for closing the window. + if (key == Night.KeySymbol.Escape) + { + Window.Close(); + } + + // Test error triggering + if (key == Night.KeySymbol.E && !isRepeat) + { + throw new InvalidOperationException("Test error triggered by pressing 'E' in SamplePlatformerGame!"); + } + + // --- Night.Window Demo: Toggle Fullscreen --- + if (key == Night.KeySymbol.F11) + { + var (isFullscreen, _) = Window.GetFullscreen(); + bool success = Window.SetFullscreen(!isFullscreen, Night.FullscreenType.Desktop); + Console.WriteLine($"SetFullscreen to {!isFullscreen} (Desktop) attempt: {(success ? "Success" : "Failed")}"); + var newMode = Window.GetMode(); + Console.WriteLine($"New Window Mode: {newMode.Width}x{newMode.Height}, Fullscreen: {newMode.Fullscreen}, Type: {newMode.FullscreenType}, Borderless: {newMode.Borderless}"); + } + + if (key == Night.KeySymbol.F10) + { + var (isFullscreen, _) = Window.GetFullscreen(); + bool success = Window.SetFullscreen(!isFullscreen, Night.FullscreenType.Exclusive); + Console.WriteLine($"SetFullscreen to {!isFullscreen} (Exclusive) attempt: {(success ? "Success" : "Failed")}"); + var newMode = Window.GetMode(); + Console.WriteLine($"New Window Mode: {newMode.Width}x{newMode.Height}, Fullscreen: {newMode.Fullscreen}, Type: {newMode.FullscreenType}, Borderless: {newMode.Borderless}"); + } + } + + // KeyReleased, MousePressed, and MouseReleased are inherited from Night.Game (default empty implementations) + // and do not need to be overridden here if no specific action is required. + + /// + /// Called when a joystick is connected. + /// + /// The joystick that was connected. + public override void JoystickAdded(Joystick joystick) + { + Console.WriteLine($"SampleGame: Joystick Added! ID: {joystick.GetId()}, Name: '{joystick.GetName()}'"); + Console.WriteLine($"SampleGame: Total Joysticks: {Night.Joysticks.GetJoystickCount()}"); + var joysticks = Night.Joysticks.GetJoysticks(); + Console.WriteLine($"SampleGame: Night.Joysticks.GetJoysticks().Count: {joysticks.Count}"); + foreach (var j in joysticks) + { + Console.WriteLine($" - Joystick ID: {j.GetId()}, Name: '{j.GetName()}', Connected: {j.IsConnected()}"); + } + } + + /// + /// Called when a joystick is disconnected. + /// + /// The joystick that was disconnected. + public override void JoystickRemoved(Joystick joystick) + { + // Note: joystick.IsConnected() will likely be false here as Joysticks.RemoveJoystick sets it. + Console.WriteLine($"SampleGame: Joystick Removed! ID: {joystick.GetId()}, Name: '{joystick.GetName()}', WasConnected: {joystick.IsConnected()}"); + if (this.inputProvidingJoystickId.HasValue && this.inputProvidingJoystickId.Value == joystick.GetId()) + { + // The joystick that was providing input has been removed, reset stored values. + this.joystickAxis0Value = 0.0f; + this.joystickHat0Direction = Night.JoystickHat.Centered; // Fully qualified name + this.joystickAButtonPressed = false; + this.inputProvidingJoystickId = null; + Console.WriteLine($"SampleGame: Raw input-providing joystick (ID: {joystick.GetId()}) was removed. Resetting its input state."); + } + + if (this.gamepadProvidingJoystickId.HasValue && this.gamepadProvidingJoystickId.Value == joystick.GetId()) + { + this.gamepadLeftXValue = 0.0f; + this.gamepadAButtonPressed = false; + this.gamepadProvidingJoystickId = null; + Console.WriteLine($"SampleGame: Gamepad input-providing joystick (ID: {joystick.GetId()}) was removed. Resetting its input state."); + } + + Console.WriteLine($"SampleGame: Total Joysticks after removal: {Night.Joysticks.GetJoystickCount()}"); + var joysticks = Night.Joysticks.GetJoysticks(); + Console.WriteLine($"SampleGame: Night.Joysticks.GetJoysticks().Count after removal: {joysticks.Count}"); + foreach (var j in joysticks) + { + Console.WriteLine($" - Remaining Joystick ID: {j.GetId()}, Name: '{j.GetName()}', Connected: {j.IsConnected()}"); + } + } + + /// + /// Called when a joystick axis moves. + /// + /// The joystick whose axis moved. + /// The index of the axis. + /// The new value of the axis (-1.0 to 1.0). + public override void JoystickAxis(Joystick joystick, int axis, float value) + { + Console.WriteLine($"SampleGame: Joystick Axis! ID: {joystick.GetId()}, Axis: {axis}, Value: {value:F4}"); + + // Typically left stick X-axis + if (axis == 0) + { + this.joystickAxis0Value = value; + this.inputProvidingJoystickId = (uint)joystick.GetId(); // Record which joystick is providing this input, cast to uint + } + } + + /// + /// Called when a joystick button is pressed. + /// + /// The joystick whose button was pressed. + /// The index of the button. + public override void JoystickPressed(Joystick joystick, int button) + { + Console.WriteLine($"SampleGame: Joystick Pressed! ID: {joystick.GetId()}, Button: {button}"); + + // Assuming 'A' button (South) corresponds to raw button index 0 for many controllers, + // or if we had a mapping to Night.GamepadButton.A, we'd check that. + // For raw joystick, we'll assume button 0 is a common primary action button. + // This part will be more robust in Phase 4 with GamepadPressed. + // Assuming raw button 0 is 'A'/South for testing P3 + if (button == 0) + { + this.joystickAButtonPressed = true; + this.inputProvidingJoystickId = joystick.GetId(); + } + } + + /// + /// Called when a joystick button is released. + /// + /// The joystick whose button was released. + /// The index of the button. + public override void JoystickReleased(Joystick joystick, int button) + { + Console.WriteLine($"SampleGame: Joystick Released! ID: {joystick.GetId()}, Button: {button}"); + + // Assuming raw button 0 is 'A'/South + if (button == 0) + { + this.joystickAButtonPressed = false; + + // We don't reset _inputProvidingJoystickId here, as other inputs might still be active from this joystick. + // It will be reset if the joystick is disconnected or if another joystick provides input. + } + } + + /// + /// Called when a joystick hat direction changes. + /// + /// The joystick whose hat changed. + /// The index of the hat. + /// The new direction of the hat. + public override void JoystickHat(Joystick joystick, int hat, JoystickHat direction) + { + Console.WriteLine($"SampleGame: Joystick Hat! ID: {joystick.GetId()}, Hat: {hat}, Direction: {direction}"); + + // Typically the first D-Pad/Hat + if (hat == 0) + { + this.joystickHat0Direction = direction; + this.inputProvidingJoystickId = (uint)joystick.GetId(); // Record which joystick is providing this input, cast to uint + } + } + + /// + /// Called when a virtual gamepad axis is moved. + /// + /// The joystick whose virtual gamepad axis moved. + /// The virtual gamepad axis. + /// The new value of the virtual gamepad axis (-1.0 to 1.0). + public override void GamepadAxis(Joystick joystick, Night.GamepadAxis axis, float value) + { + Console.WriteLine($"SampleGame: Gamepad Axis! ID: {joystick.GetId()}, Axis: {axis}, Value: {value:F4}"); + if (axis == Night.GamepadAxis.LeftX) + { + this.gamepadLeftXValue = value; + this.gamepadProvidingJoystickId = joystick.GetId(); + } + } + + /// + /// Called when a virtual gamepad button is pressed. + /// + /// The joystick whose virtual gamepad button was pressed. + /// The virtual gamepad button. + public override void GamepadPressed(Joystick joystick, Night.GamepadButton button) + { + Console.WriteLine($"SampleGame: Gamepad Pressed! ID: {joystick.GetId()}, Button: {button}"); + if (button == Night.GamepadButton.A || button == Night.GamepadButton.South) + { + this.gamepadAButtonPressed = true; + this.gamepadProvidingJoystickId = joystick.GetId(); + } + } + + /// + /// Called when a virtual gamepad button is released. + /// + /// The joystick whose virtual gamepad button was released. + /// The virtual gamepad button. + public override void GamepadReleased(Joystick joystick, Night.GamepadButton button) + { + Console.WriteLine($"SampleGame: Gamepad Released! ID: {joystick.GetId()}, Button: {button}"); + if (button == Night.GamepadButton.A || button == Night.GamepadButton.South) + { + this.gamepadAButtonPressed = false; + + // Do not reset gamepadProvidingJoystickId here, other gamepad inputs might be active. + } + } + + // Helper for collision detection (AABB) + private static bool CheckAABBCollision(Night.Rectangle rect1, Night.Rectangle rect2) + { + // True if the rectangles are overlapping + return rect1.X < rect2.X + rect2.Width && + rect1.X + rect1.Width > rect2.X && + rect1.Y < rect2.Y + rect2.Height && + rect1.Y + rect1.Height > rect2.Y; + } +} diff --git a/src/SampleGame/Samples/Platformer.cs b/src/SampleGame/Samples/Platformer.cs index b74f1e4b..489f234c 100644 --- a/src/SampleGame/Samples/Platformer.cs +++ b/src/SampleGame/Samples/Platformer.cs @@ -32,9 +32,9 @@ namespace SampleGame; /// /// A sample platformer game implementation using the Night engine. -/// Implements the interface for Night.Engine integration. +/// Inherits from to leverage default game loop and event handling. /// -public class Platformer : IGame +public class Platformer : Game { private Player player; private List platforms; @@ -55,7 +55,7 @@ public Platformer() /// Loads game assets and initializes game state for the platformer. /// Called once at the start of the game by the Night.Engine. /// - public void Load() + public override void Load() { _ = Night.Window.SetMode(800, 600, SDL.WindowFlags.Resizable); Night.Window.SetTitle("Night Platformer Sample"); @@ -83,9 +83,10 @@ public void Load() /// Called every frame by the Night.Engine. /// /// The time elapsed since the last frame, in seconds. - public void Update(double deltaTime) + public override void Update(double deltaTime) { - this.player.Update(deltaTime, this.platforms); + // Pass default joystick values as this sample isn't the primary focus for joystick control. + this.player.Update(deltaTime, this.platforms, 0.0f, Night.JoystickHat.Centered, false); Night.Rectangle playerBoundsForGoalCheck = new Night.Rectangle((int)this.player.X, (int)this.player.Y, this.player.Width, this.player.Height + 1); if (CheckAABBCollision(playerBoundsForGoalCheck, this.goalPlatform) && !this.goalReachedMessageShown) @@ -100,7 +101,7 @@ public void Update(double deltaTime) /// Draws the platformer game scene. /// Called every frame by the Night.Engine after Update. /// - public void Draw() + public override void Draw() { Night.Graphics.Clear(new Night.Color(135, 206, 235)); // Sky blue background @@ -121,28 +122,94 @@ public void Draw() } this.player.Draw(); - - // Player and Level drawing logic will go here in later tasks. } /// /// Handles key press events for the platformer game. - /// Called by Night.Engine when a key is pressed. /// /// The of the pressed key. /// The (physical key code) of the pressed key. /// True if this is a repeat key event, false otherwise. - public void KeyPressed(KeySymbol key, KeyCode scancode, bool isRepeat) + public override void KeyPressed(KeySymbol key, KeyCode scancode, bool isRepeat) { - // Minimal key handling for now, primarily for closing the window. - // System.Console.WriteLine($"SampleGame: KeyPressed - KeySymbol: {key}, Scancode: {scancode}, IsRepeat: {isRepeat}"); + // Minimal key handling, primarily for closing the window. if (key == KeySymbol.Escape) { Console.WriteLine("SampleGame: Escape key pressed, closing window."); Window.Close(); } + } + + /// + public override void KeyReleased(KeySymbol key, KeyCode scancode) + { + // base.KeyReleased(key, scancode); // Call base if you want to extend, or just leave empty. + } + + /// + public override void MousePressed(int x, int y, MouseButton button, bool istouch, int presses) + { + // base.MousePressed(x, y, button, istouch, presses); + } + + /// + public override void MouseReleased(int x, int y, MouseButton button, bool istouch, int presses) + { + // base.MouseReleased(x, y, button, istouch, presses); + } + + /// + public override void JoystickAdded(Joystick joystick) + { + // base.JoystickAdded(joystick); + } + + /// + public override void JoystickRemoved(Joystick joystick) + { + // base.JoystickRemoved(joystick); + } + + /// + public override void JoystickAxis(Joystick joystick, int axis, float value) + { + // base.JoystickAxis(joystick, axis, value); + } + + /// + public override void JoystickPressed(Joystick joystick, int button) + { + // base.JoystickPressed(joystick, button); + } + + /// + public override void JoystickReleased(Joystick joystick, int button) + { + // base.JoystickReleased(joystick, button); + } + + /// + public override void JoystickHat(Joystick joystick, int hat, JoystickHat direction) + { + // base.JoystickHat(joystick, hat, direction); + } + + /// + public override void GamepadAxis(Joystick joystick, GamepadAxis axis, float value) + { + // base.GamepadAxis(joystick, axis, value); + } - // Player input (movement, jump) will be handled in Player.Update using Night.Keyboard.IsDown(). + /// + public override void GamepadPressed(Joystick joystick, GamepadButton button) + { + // base.GamepadPressed(joystick, button); + } + + /// + public override void GamepadReleased(Joystick joystick, GamepadButton button) + { + // base.GamepadReleased(joystick, button); } // Helper for collision detection (AABB) diff --git a/src/SampleGame/Samples/PlatformerGame.cs b/src/SampleGame/Samples/PlatformerGame.cs index 5c1290d7..b90749b1 100644 --- a/src/SampleGame/Samples/PlatformerGame.cs +++ b/src/SampleGame/Samples/PlatformerGame.cs @@ -34,6 +34,6 @@ public class PlatformerGame /// public static void PlatformerGameMain() { - Night.Framework.Run(new Platformer()); + Night.Framework.Run(new Platformer(), new CLI(System.Array.Empty())); } } diff --git a/src/SampleGame/assets/images/pixel_green.aseprite b/src/SampleGame/assets/images/pixel_green.aseprite new file mode 100644 index 00000000..f33ce2c4 Binary files /dev/null and b/src/SampleGame/assets/images/pixel_green.aseprite differ diff --git a/src/SampleGame/assets/images/pixel_green.pixi b/src/SampleGame/assets/images/pixel_green.pixi deleted file mode 100644 index 4474dc8f..00000000 Binary files a/src/SampleGame/assets/images/pixel_green.pixi and /dev/null differ diff --git a/src/SampleGame/assets/images/pixel_green.png b/src/SampleGame/assets/images/pixel_green.png index f8d4f83c..0aa409e3 100644 Binary files a/src/SampleGame/assets/images/pixel_green.png and b/src/SampleGame/assets/images/pixel_green.png differ diff --git a/src/SampleGame/assets/images/player_sprite_blue_32x64.png b/src/SampleGame/assets/images/player_sprite_blue_32x64.png index 68e1117f..e4b911d0 100644 Binary files a/src/SampleGame/assets/images/player_sprite_blue_32x64.png and b/src/SampleGame/assets/images/player_sprite_blue_32x64.png differ diff --git a/src/SampleGame/assets/images/test_texture.png b/src/SampleGame/assets/images/test_texture.png deleted file mode 100644 index 5ef1140e..00000000 Binary files a/src/SampleGame/assets/images/test_texture.png and /dev/null differ diff --git a/tests/Core/BaseTestCase.cs b/tests/Core/BaseTestCase.cs new file mode 100644 index 00000000..ea5136b7 --- /dev/null +++ b/tests/Core/BaseTestCase.cs @@ -0,0 +1,92 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Diagnostics; + +using Night; + +namespace NightTest.Core +{ + /// + /// Abstract base class for all test cases, providing common functionality. + /// Implements ITestCase. + /// + public abstract class BaseTestCase : ITestCase + { + // Public Properties + + /// + /// Gets the stopwatch used to measure the duration of the test case. + /// + public Stopwatch TestStopwatch { get; } = new Stopwatch(); + + /// + /// Gets or sets the current status of the test case. + /// Its value can be asserted by xUnit test methods. + /// + public TestStatus CurrentStatus { get; protected set; } = TestStatus.NotRun; + + /// + /// Gets or sets details about the test execution, such as error messages or success information. + /// Its value can be asserted by xUnit test methods. + /// + public string Details { get; protected set; } = "Test has not started."; + + /// + public abstract string Name { get; } + + /// + public virtual TestType Type => TestType.Automated; + + /// + public abstract string Description { get; } + + /// + /// Public method to record a test failure. + /// + /// Specific details about the failure. + /// The exception that caused the failure, if any. + public void RecordFailure(string failureDetails, Exception? ex = null) + { + this.CurrentStatus = TestStatus.Failed; + if (ex != null) + { + this.Details = $"{failureDetails} - Exception: {ex.GetType().Name}: {ex.Message}"; + } + else + { + this.Details = failureDetails; + } + } + + /// + /// Public method to record a test success. + /// + /// Specific details about the success. + public void RecordSuccess(string successDetails) + { + this.CurrentStatus = TestStatus.Passed; + this.Details = successDetails; + } + } +} diff --git a/tests/Core/FileDropTestGame.cs b/tests/Core/FileDropTestGame.cs new file mode 100644 index 00000000..b874ab98 --- /dev/null +++ b/tests/Core/FileDropTestGame.cs @@ -0,0 +1,109 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System.Runtime.InteropServices; + +using Night; + +using SDL3; + +using Xunit; + +namespace NightTest.Core +{ + /// + /// A test game for verifying file drop events. + /// + public class FileDropTestGame : GameTestCase + { + private readonly string expectedPath; + private string? actualPath; + + /// + /// Initializes a new instance of the class. + /// + /// The expected path of the dropped file. + public FileDropTestGame(string expectedPath) + { + this.expectedPath = expectedPath; + } + + /// + public override string Name => "FileDropEventTest"; + + /// + public override string Description => "Tests that the file drop event is triggered correctly."; + + /// + public override void FileDropped(DroppedFile file) + { + this.actualPath = file.Path; + Assert.Equal(this.expectedPath, this.actualPath); + this.CurrentStatus = TestStatus.Passed; + this.Details = $"File dropped with correct path: {this.actualPath}"; + this.EndTest(); + } + + /// + protected override void Load() + { + // Create a proper SDL drop event + // Note: In SDL3, we need to use UTF8 encoding for the Data field + // For testing purposes, we'll create the event structure manually + var dropEvent = new SDL.Event + { + Type = (uint)SDL.EventType.DropFile, + Drop = new SDL.DropEvent + { + Type = SDL.EventType.DropFile, + Timestamp = SDL.GetTicksNS(), + WindowID = SDL.GetWindowID(Window.Handle), + X = 0, + Y = 0, + Source = IntPtr.Zero, + Data = Marshal.StringToCoTaskMemUTF8(this.expectedPath), // Use UTF8 encoding + }, + }; + + bool success = SDL.PushEvent(ref dropEvent); + + // Don't free the memory immediately - let SDL handle it + // The framework will handle cleanup as noted in the TODO comment + if (!success) + { + this.RecordFailure($"Failed to push SDL drop event: {SDL.GetError()}"); + this.EndTest(); + } + } + + /// + protected override void Update(double deltaTime) + { + // The test will be driven by the FileDropped event + // We can add a timeout condition here if we want + if (this.CheckCompletionAfterDuration(1000, () => this.actualPath != null, passDetails: () => $"File drop event received with path: {this.actualPath}", failDetailsTimeout: () => "Test failed: Timed out waiting for file drop event.")) + { + return; + } + } + } +} diff --git a/tests/Core/GameTestCase.cs b/tests/Core/GameTestCase.cs new file mode 100644 index 00000000..267cec4e --- /dev/null +++ b/tests/Core/GameTestCase.cs @@ -0,0 +1,429 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Diagnostics; + +using Night; + +namespace NightTest.Core +{ + /// + /// Abstract base class for test cases to reduce boilerplate. + /// Implements ITestCase and Night.IGame. + /// + public abstract class GameTestCase : BaseTestCase, IGame + { + // Protected Properties + + /// + /// Gets or sets a value indicating whether the test case has finished its execution. + /// + protected bool IsDone { get; set; } = false; + + /// + /// Gets the current frame count since the test started. Incremented in Update. + /// + protected int CurrentFrameCount { get; private set; } = 0; + + /// + /// Loads and initializes the test case. This is the entry point called by the test runner + /// or game loop, fulfilling the interface. + /// It performs common base setup and then calls . + /// + void IGame.Load() + { + this.IsDone = false; + this.CurrentStatus = TestStatus.NotRun; // Reverted: Enum does not contain 'Running' + this.Details = "Test is running..."; + this.CurrentFrameCount = 0; + this.TestStopwatch.Reset(); + this.TestStopwatch.Start(); + + // Call the virtual InternalLoad, allowing intermediate classes to intercept. + this.InternalLoad(); + } + + /// + /// Updates the test case logic. This is the entry point called by the test runner + /// or game loop, fulfilling the interface. + /// + /// Time elapsed since the last frame. + void IGame.Update(double deltaTime) + { + if (this.IsDone) + { + return; + } + + this.CurrentFrameCount++; + + try + { + // Call the virtual InternalUpdate, allowing intermediate classes to intercept. + this.InternalUpdate(deltaTime); + } + catch (System.Exception ex) + { + // Record failure if an unhandled exception occurs in the test's Update logic + this.RecordFailure($"Unhandled exception in Update: {ex.GetType().Name} - {ex.Message}", ex); + } + } + + /// + /// Draws the test case. This is the entry point called by the test runner + /// or game loop, fulfilling the interface. + /// It calls . + /// + void IGame.Draw() + { + // Call the virtual InternalDraw, allowing intermediate classes to intercept. + this.InternalDraw(); + } + + /// + /// Called when a key is pressed. Default is empty. + /// + /// The key symbol that was pressed. + /// The physical key code. + /// True if this is a key repeat event, false otherwise. + public virtual void KeyPressed(KeySymbol key, KeyCode scancode, bool isRepeat) + { + if (this.IsDone || isRepeat) + { + return; + } + } + + /// + /// Called when a key is released. Default is empty. + /// + /// The key symbol that was released. + /// The physical key code. + public virtual void KeyReleased(KeySymbol key, KeyCode scancode) + { + } + + /// + /// Called when a mouse button is pressed. + /// Base implementation handles clicks for manual confirmation UI. + /// + /// The x-coordinate of the mouse click. + /// The y-coordinate of the mouse click. + /// The mouse button that was pressed. + /// True if the event was generated by a touch input, false otherwise. + /// The number of clicks, 1 for single-click, 2 for double-click, etc. + public virtual void MousePressed(int x, int y, MouseButton button, bool istouch, int presses) + { + } + + /// + /// Called when a mouse button is released. Default is empty. + /// + /// The x-coordinate of the mouse release. + /// The y-coordinate of the mouse release. + /// The mouse button that was released. + /// True if the event was generated by a touch input, false otherwise. + /// The number of clicks (usually 1 for release, but can be higher for some systems/drivers if tracking click counts on release). + public virtual void MouseReleased(int x, int y, MouseButton button, bool istouch, int presses) + { + } + + /// + public virtual void JoystickAdded(Joystick joystick) + { + } + + /// + public virtual void JoystickRemoved(Joystick joystick) + { + } + + /// + public virtual void JoystickAxis(Joystick joystick, int axis, float value) + { + } + + /// + public virtual void JoystickPressed(Joystick joystick, int button) + { + } + + /// + public virtual void JoystickReleased(Joystick joystick, int button) + { + } + + /// + public virtual void JoystickHat(Joystick joystick, int hat, JoystickHat direction) + { + } + + /// + public virtual void GamepadAxis(Joystick joystick, GamepadAxis axis, float value) + { + } + + /// + public virtual void GamepadPressed(Joystick joystick, GamepadButton button) + { + } + + /// + public virtual void GamepadReleased(Joystick joystick, GamepadButton button) + { + } + + /// + public Func Run() + { + throw new NotImplementedException(); + } + + /// + public bool Quit() + { + throw new NotImplementedException(); + } + + /// + public virtual void FileDropped(DroppedFile file) + { + } + + /// + /// Public method to record a test failure. + /// + /// Specific details about the failure. + /// The exception that caused the failure, if any. + public new void RecordFailure(string failureDetails, Exception? ex = null) + { + if (this.IsDone) + { + return; + } + + base.RecordFailure(failureDetails, ex); + this.EndTest(); + } + + /// + /// Public method to record a test success. + /// + /// Specific details about the success. + public new void RecordSuccess(string successDetails) + { + if (this.IsDone) + { + return; + } + + base.RecordSuccess(successDetails); + this.EndTest(); + } + + /// + /// Performs the specific load logic for the test case. + /// Derived classes can override this method to implement their core load behavior. + /// This method is called by the method, which is ultimately + /// invoked via the explicit interface implementation. + /// Base implementation is empty. + /// + protected virtual void Load() + { + } + + /// + /// Intermediate virtual method that can be overridden by specialized base classes + /// (like ) to inject logic before or after + /// the concrete test's method is called. + /// By default, it directly calls the concrete . + /// + protected virtual void InternalLoad() + { + // Calls the abstract Load implemented by concrete test classes + this.Load(); + } + + /// + /// Performs the specific update logic for the test case. + /// Derived classes must override this method to implement their core update behavior. + /// This method is called by the explicit interface implementation. + /// + /// Time elapsed since the last frame. + protected abstract void Update(double deltaTime); + + /// + /// Intermediate virtual method that can be overridden by specialized base classes + /// (like ) to inject logic before or after + /// the concrete test's method is called. + /// By default, it directly calls the concrete . + /// + /// Time elapsed since the last frame. + protected virtual void InternalUpdate(double deltaTime) + { + // Calls the abstract Update implemented by concrete test classes (e.g., GraphicsClearColorTest or automated tests) + this.Update(deltaTime); + } + + /// + /// Performs the specific draw logic for the test case. + /// Derived classes can override this method to implement their core draw behavior. + /// This method is called by the method, which is ultimately + /// invoked via the explicit interface implementation. + /// Base implementation is empty. + /// + protected virtual void Draw() + { + } + + /// + /// Intermediate virtual method that can be overridden by specialized base classes + /// (like ) to inject logic before or after + /// the concrete test's method is called. + /// By default, it directly calls the concrete . + /// + protected virtual void InternalDraw() + { + // Calls the abstract Draw implemented by concrete test classes + this.Draw(); + } + + /// + /// Helper method to stop the stopwatch, record results, and close the window. + /// Call this when your test logic determines completion (pass or fail). + /// Ensure CurrentStatus and Details are set appropriately before calling. + /// + protected virtual void EndTest() + { + if (this.IsDone) + { + return; + } + + this.TestStopwatch.Stop(); + + if (Night.Window.IsOpen()) + { + Night.Window.Close(); + } + + this.IsDone = true; + } + + /// + /// Checks if the test should complete based on a duration. + /// Sets CurrentStatus, Details, and calls EndTest if completion occurs. + /// + /// The duration in milliseconds to wait. + /// An optional function that must return true for the test to pass. If null, test passes on timeout. + /// Details message if the test passes. + /// Details message if the test fails due to timeout (and no successCondition or it was false). + /// Details message if the test fails because successCondition was false at timeout. + /// True if the test completed (passed or failed) by this call, false otherwise. + protected bool CheckCompletionAfterDuration( + double milliseconds, + Func? successCondition = null, + Func? passDetails = null, + Func? failDetailsTimeout = null, + Func? failDetailsCondition = null) + { + if (this.IsDone) + { + return true; + } + + // First, check for timeout + if (this.TestStopwatch.ElapsedMilliseconds >= milliseconds) + { + // On timeout, check the success condition one last time + if (successCondition != null && successCondition()) + { + // If condition is met at the very end, it's a success + this.RecordSuccess(passDetails?.Invoke() ?? $"Test passed at timeout: Condition met after {this.TestStopwatch.ElapsedMilliseconds}ms."); + } + else + { + // If condition is not met, it's a failure due to timeout + this.RecordFailure(failDetailsCondition?.Invoke() ?? failDetailsTimeout?.Invoke() ?? $"Test failed: Timed out after {milliseconds}ms."); + } + + this.EndTest(); // End the test on timeout regardless of success or failure + return true; + } + + // If not timed out, check the success condition + if (successCondition != null && successCondition()) + { + this.RecordSuccess(passDetails?.Invoke() ?? $"Test passed: Condition met at {this.TestStopwatch.ElapsedMilliseconds}ms."); + this.EndTest(); // End the test as soon as the condition is met + return true; + } + + return false; // Test is not yet complete + } + + /// + /// Checks if the test should complete based on a number of frames. + /// Sets CurrentStatus, Details, and calls EndTest if completion occurs. + /// + /// The number of frames to wait. + /// An optional function that must return true for the test to pass. If null, test passes after frameCount. + /// Details message if the test passes. + /// Details message if the test fails due to exceeding frame limit (and no successCondition or it was false). + /// Details message if the test fails because successCondition was false at frame limit. + /// True if the test completed (passed or failed) by this call, false otherwise. + protected bool CheckCompletionAfterFrames( + int frameCount, + Func? successCondition = null, + Func? passDetails = null, + Func? failDetailsFrameLimit = null, + Func? failDetailsCondition = null) + { + if (this.IsDone) + { + return true; // Already done, report as handled + } + + if (this.CurrentFrameCount >= frameCount) + { + if (successCondition == null || successCondition()) + { + this.CurrentStatus = TestStatus.Passed; + this.Details = passDetails != null ? passDetails() : "Test passed: Met condition or reached frame limit."; + } + else + { + this.CurrentStatus = TestStatus.Failed; + + // If condition failed, use failDetailsCondition, otherwise (timeout without specific condition failure) use failDetailsFrameLimit + this.Details = failDetailsCondition != null ? failDetailsCondition() : (failDetailsFrameLimit != null ? failDetailsFrameLimit() : "Test failed: Condition not met or frame limit exceeded."); + } + + this.EndTest(); + return true; // Test completed + } + + return false; // Test not yet completed + } + } +} diff --git a/tests/Core/ITestCase.cs b/tests/Core/ITestCase.cs new file mode 100644 index 00000000..16dbc579 --- /dev/null +++ b/tests/Core/ITestCase.cs @@ -0,0 +1,46 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +namespace NightTest.Core +{ + /// + /// Defines the contract for a runnable test case. + /// Test cases must also implement Night.IGame to be executed by the framework. + /// + public interface ITestCase + { + /// + /// Gets the unique name of the test case. + /// + string Name { get; } + + /// + /// Gets the type of the test case (e.g., Automated, Manual). + /// + TestType Type { get; } + + /// + /// Gets a brief description of what the test case does. + /// + string Description { get; } + } +} diff --git a/tests/Core/ManualTestCase.cs b/tests/Core/ManualTestCase.cs new file mode 100644 index 00000000..33809330 --- /dev/null +++ b/tests/Core/ManualTestCase.cs @@ -0,0 +1,320 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; + +using Night; + +namespace NightTest.Core +{ + /// + /// Abstract base class for manual test cases, providing common UI and interaction logic. + /// Inherits from GameTestCase. + /// + public abstract class ManualTestCase : GameTestCase + { + // Constants + private const int ButtonWidth = 120; + private const int ButtonHeight = 50; + private const int ButtonPadding = 20; + private static readonly Color PassButtonColor = new Color(0, 180, 0); // Green + private static readonly Color FailButtonColor = new Color(200, 0, 0); // Red + private static readonly Color ButtonBorderColor = Color.White; + + // Private Fields + + /// + /// The current UI mode for manual input. + /// + private ManualInputUIMode currentManualInputUIMode = ManualInputUIMode.None; + + /// + /// A value indicating whether the confirmation prompt is currently active. + /// + private bool confirmationPromptActive; + + private Rectangle passButtonRect; + private Rectangle failButtonRect; + + /// + /// Defines the UI mode for manual test input confirmation. + /// + protected enum ManualInputUIMode + { + /// + /// No manual input UI is active. + /// + None, + + /// + /// The test is awaiting user confirmation via the UI (Pass/Fail buttons). + /// + AwaitingConfirmation, + } + + /// + public override TestType Type => TestType.Manual; + + /// + /// Gets the console prompt message displayed during manual confirmation. + /// + protected string ManualConfirmationConsolePrompt { get; private set; } = string.Empty; + + /// + /// Gets the timeout in milliseconds for manual tests awaiting confirmation. Defaults to 30 seconds. + /// + protected double ManualTestTimeoutMilliseconds { get; } = 30000; + + /// + /// Gets the suggested delay in milliseconds before a manual test prompt is shown. Defaults to 200ms. + /// Derived classes can use this value to time their call to RequestManualConfirmation. + /// + protected double ManualTestPromptDelayMilliseconds { get; } = 200; + + /// + public override void KeyPressed(KeySymbol key, KeyCode scancode, bool isRepeat) + { + base.KeyPressed(key, scancode, isRepeat); + if (this.IsDone || isRepeat) + { + return; + } + + // Handling for ESC key to fail manual tests during confirmation + if (this.Type == TestType.Manual && this.currentManualInputUIMode == ManualInputUIMode.AwaitingConfirmation && scancode == KeyCode.Escape) + { + Console.WriteLine($"MANUAL TEST '{this.Name}': FAILED by user pressing ESCAPE."); + this.CurrentStatus = TestStatus.Failed; + this.Details = this.ManualConfirmationConsolePrompt + " - User pressed ESCAPE to fail."; + this.EndTest(); + return; // Test is over + } + } + + /// + public override void MousePressed(int x, int y, MouseButton button, bool istouch, int presses) + { + base.MousePressed(x, y, button, istouch, presses); + if (this.IsDone) + { + return; + } + + if (this.currentManualInputUIMode == ManualInputUIMode.AwaitingConfirmation && button == MouseButton.Left && !istouch) + { + if (this.passButtonRect.Width > 0 && // Ensure buttons are initialized + x >= this.passButtonRect.X && x <= this.passButtonRect.X + this.passButtonRect.Width && + y >= this.passButtonRect.Y && y <= this.passButtonRect.Y + this.passButtonRect.Height) + { + Console.WriteLine($"MANUAL TEST '{this.Name}': PASSED by user click."); + this.CurrentStatus = TestStatus.Passed; + this.Details = this.ManualConfirmationConsolePrompt + " - User confirmed: PASSED."; + this.currentManualInputUIMode = ManualInputUIMode.None; + this.EndTest(); + } + else if (this.failButtonRect.Width > 0 && // Ensure buttons are initialized + x >= this.failButtonRect.X && x <= this.failButtonRect.X + this.failButtonRect.Width && + y >= this.failButtonRect.Y && y <= this.failButtonRect.Y + this.failButtonRect.Height) + { + Console.WriteLine($"MANUAL TEST '{this.Name}': FAILED by user click."); + this.CurrentStatus = TestStatus.Failed; + this.Details = this.ManualConfirmationConsolePrompt + " - User confirmed: FAILED."; + this.currentManualInputUIMode = ManualInputUIMode.None; + this.EndTest(); + } + } + } + + /// + /// Overrides the internal load hook from + /// to inject manual test-specific initialization logic + /// before allowing the concrete test's method to run. + /// This method is sealed to ensure this control flow. + /// + protected sealed override void InternalLoad() + { + // Perform ManualTestCase specific initialization + this.currentManualInputUIMode = ManualInputUIMode.None; + this.ManualConfirmationConsolePrompt = string.Empty; + this.confirmationPromptActive = false; + + // Call the base InternalLoad, which will in turn call the concrete test's Load() method. + base.InternalLoad(); + } + + /// + /// Overrides the internal update hook from + /// to inject manual test-specific logic, such as timeout checks, + /// before allowing the concrete test's method to run. + /// This method is sealed to ensure this control flow. + /// + /// Time elapsed since the last frame. + protected sealed override void InternalUpdate(double deltaTime) + { + // Handle timeout for manual tests + if (this.Type == TestType.Manual && this.currentManualInputUIMode == ManualInputUIMode.AwaitingConfirmation) + { + if (this.TestStopwatch.ElapsedMilliseconds > this.ManualTestTimeoutMilliseconds) + { + Console.WriteLine($"MANUAL TEST '{this.Name}': Timed out after {this.ManualTestTimeoutMilliseconds / 1000} seconds."); + this.CurrentStatus = TestStatus.Failed; + this.Details = this.ManualConfirmationConsolePrompt + " - Test timed out."; + this.EndTest(); + return; // Test is over + } + } + + // If not timed out or otherwise completed, call the base InternalUpdate, + // which will in turn call the concrete test's Update() method. + if (!this.IsDone) + { + base.InternalUpdate(deltaTime); + } + } + + /// + /// Overrides the internal draw hook from + /// to allow the concrete test's method to run first, + /// then draws manual test-specific UI elements (like Pass/Fail buttons), + /// and finally calls . + /// This method is sealed to ensure this control flow. + /// + protected sealed override void InternalDraw() + { + // First, call the base InternalDraw, which will execute the concrete test's Draw() method. + base.InternalDraw(); + + // Then, draw ManualTestCase specific UI elements. + if (this.currentManualInputUIMode == ManualInputUIMode.AwaitingConfirmation) + { + // Ensure button rects are calculated if window is valid + if (this.passButtonRect.Width == 0 && Window.IsOpen()) + { + var windowMode = Window.GetMode(); + if (windowMode.Width > 0 && windowMode.Height > 0) + { + this.CalculateButtonPositions(windowMode.Width, windowMode.Height); + } + } + + if (this.passButtonRect.Width > 0) + { + // Draw Pass Button (Green) + Graphics.SetColor(PassButtonColor); + Graphics.Rectangle(DrawMode.Fill, this.passButtonRect.X, this.passButtonRect.Y, this.passButtonRect.Width, this.passButtonRect.Height); + Graphics.SetColor(ButtonBorderColor); // Border + Graphics.Rectangle(DrawMode.Line, this.passButtonRect.X, this.passButtonRect.Y, this.passButtonRect.Width, this.passButtonRect.Height); + + // Draw Fail Button (Red) + Graphics.SetColor(FailButtonColor); + Graphics.Rectangle(DrawMode.Fill, this.failButtonRect.X, this.failButtonRect.Y, this.failButtonRect.Width, this.failButtonRect.Height); + Graphics.SetColor(ButtonBorderColor); // Border + Graphics.Rectangle(DrawMode.Line, this.failButtonRect.X, this.failButtonRect.Y, this.failButtonRect.Width, this.failButtonRect.Height); + } + } + + // Finally, present the graphics for all manual tests. + Night.Graphics.Present(); + } + + // Note: The public override Draw() is removed as its logic is now in InternalDraw(). + + /// + /// Initiates a manual confirmation step for the test. + /// Displays a prompt in the console and renders Pass/Fail buttons in the game window. + /// + /// The question or instruction to display in the console for the user. + protected void RequestManualConfirmation(string consolePrompt) + { + if (this.Type != TestType.Manual) + { + Console.WriteLine($"Warning: RequestManualConfirmation called for a non-manual test: {this.Name}. Ignoring."); + return; + } + + if (!this.confirmationPromptActive && this.TestStopwatch.ElapsedMilliseconds > this.ManualTestPromptDelayMilliseconds) + { + this.confirmationPromptActive = true; + this.ManualConfirmationConsolePrompt = consolePrompt; + this.currentManualInputUIMode = ManualInputUIMode.AwaitingConfirmation; + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine($"\n--- MANUAL CONFIRMATION REQUIRED for test: '{this.Name}' ---"); + Console.ResetColor(); + Console.WriteLine(this.ManualConfirmationConsolePrompt); + Console.WriteLine("Please observe the game window. Click the GREEN box to PASS, or the RED box to FAIL."); + Console.WriteLine("(Alternatively, press ESCAPE to fail and quit this specific test.)"); + + // Attempt to calculate button positions immediately if window is available + if (Window.IsOpen()) + { + var windowMode = Window.GetMode(); + if (windowMode.Width > 0 && windowMode.Height > 0) + { + this.CalculateButtonPositions(windowMode.Width, windowMode.Height); + } + } + } + } + + /// + protected override void EndTest() + { + if (this.IsDone) + { + return; + } + + // If a manual test is quit externally (e.g. ESC key in test case) before confirmation, + // and status hasn't been set by button click, mark as failed. + if (this.Type == TestType.Manual && this.currentManualInputUIMode == ManualInputUIMode.AwaitingConfirmation && this.CurrentStatus == TestStatus.NotRun) + { + this.CurrentStatus = TestStatus.Failed; + this.Details = this.ManualConfirmationConsolePrompt + " - Test quit prematurely by user before confirmation."; + Console.WriteLine($"MANUAL TEST '{this.Name}': Test quit prematurely. Marked as FAILED."); + } + + this.currentManualInputUIMode = ManualInputUIMode.None; // Ensure this is reset before base call + + base.EndTest(); + } + + // Private Methods + private void CalculateButtonPositions(int windowWidth, int windowHeight) + { + int totalButtonsWidth = (ButtonWidth * 2) + ButtonPadding; + int startX = (windowWidth - totalButtonsWidth) / 2; + if (startX < ButtonPadding) + { + startX = ButtonPadding; // Ensure buttons are not off-screen left + } + + int buttonY = windowHeight - ButtonHeight - ButtonPadding; + if (buttonY < ButtonPadding) + { + buttonY = ButtonPadding; // Ensure buttons are not off-screen bottom + } + + this.passButtonRect = new Rectangle(startX, buttonY, ButtonWidth, ButtonHeight); + this.failButtonRect = new Rectangle(startX + ButtonWidth + ButtonPadding, buttonY, ButtonWidth, ButtonHeight); + } + } +} diff --git a/tests/Core/ModTestCase.cs b/tests/Core/ModTestCase.cs new file mode 100644 index 00000000..3530b108 --- /dev/null +++ b/tests/Core/ModTestCase.cs @@ -0,0 +1,80 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Diagnostics; + +using Night; + +namespace NightTest.Core +{ + /// + /// Abstract mod test base class for test cases to reduce boilerplate. + /// Implements ITestCase. + /// Mod tests are closer in nature to unit tests and do not rely on a + /// IGame instance to run. + /// + public abstract class ModTestCase : BaseTestCase + { + // Public Properties + + /// + /// Gets the success message for the test. + /// This message is used by the test runner if the Run() method completes without exceptions. + /// + public abstract string SuccessMessage { get; } + + /// + /// When implemented by a derived class, contains the specific test logic and assertions. + /// This method will be called by the test runner (e.g., TestGroup) and should not + /// contain try-catch blocks for assertion failures or calls to RecordSuccess/RecordFailure. + /// + public abstract void Run(); + + /// + /// Public method to record a test success, typically called by an xUnit wrapper when an exception occurs. + /// + /// Specific details about the success. + public new void RecordSuccess(string successDetails) + { + this.CurrentStatus = TestStatus.Passed; + this.Details = successDetails; + } + + /// + /// Sets the initial state of the test before execution by the test runner. + /// + internal void PrepareForRun() + { + this.TestStopwatch.Restart(); + this.Details = "Test is preparing to run."; + } + + /// + /// Finalizes the test run, typically called by the test runner. + /// + internal void FinalizeRun() + { + this.TestStopwatch.Stop(); + } + } +} diff --git a/tests/Core/TestGroup.cs b/tests/Core/TestGroup.cs new file mode 100644 index 00000000..cca948d2 --- /dev/null +++ b/tests/Core/TestGroup.cs @@ -0,0 +1,123 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Diagnostics; + +using Night; + +using Xunit; +using Xunit.Abstractions; + +namespace NightTest.Core +{ + /// + /// Base class for grouping xUnit tests for IGame test cases. + /// + public class TestGroup + { + private readonly ITestOutputHelper outputHelper; + + /// + /// Initializes a new instance of the class. + /// + /// The xUnit test output helper for logging. + public TestGroup(ITestOutputHelper outputHelper) + { + this.outputHelper = outputHelper; + + // Clear any sinks from previous test runs and add our custom xUnit sink. + LogManager.ClearSinks(); + LogManager.AddSink(new XUnitLogSink(this.outputHelper)); + } + + /// + /// Run the IGame test case. + /// + /// The test case to run. + public void Run_GameTestCase(GameTestCase testCase) + { + Assert.NotNull(testCase); + + this.outputHelper.WriteLine($"Starting IGame test: {testCase.Name}"); + this.outputHelper.WriteLine($" Description: {testCase.Description}"); + this.outputHelper.WriteLine($" Type: {testCase.Type}"); + + try + { + Night.Framework.Run(testCase); + } + catch (Exception ex) + { + // If Night.Framework.Run throws before testCase.Load() or if testCase itself is problematic early, + // the Assert.NotNull would have already caught a null testCase argument. + // If the exception happens *during* testCase execution, testCase is still the same valid object. + this.outputHelper.WriteLine($"IGame test '{testCase.Name}' threw an unhandled exception: {ex.Message}\n{ex.StackTrace}"); + testCase.RecordFailure($"Unhandled exception: {ex.Message}", ex); + } + + this.outputHelper.WriteLine($"IGame test '{testCase.Name}' completed."); + this.outputHelper.WriteLine($" Status: {testCase.CurrentStatus}"); + this.outputHelper.WriteLine($" Details: {testCase.Details}"); + this.outputHelper.WriteLine($" Duration: {testCase.TestStopwatch.ElapsedMilliseconds}ms"); + + Assert.Equal(TestStatus.Passed, testCase.CurrentStatus); + } + + /// + /// Run the mod test case. + /// + /// The mod test case to run. + public void Run_ModTestCase(ModTestCase testCase) + { + Assert.NotNull(testCase); + + this.outputHelper.WriteLine($"Starting mod test: {testCase.Name}"); + this.outputHelper.WriteLine($" Description: {testCase.Description}"); + this.outputHelper.WriteLine($" Type: {testCase.Type}"); + + testCase.PrepareForRun(); + try + { + testCase.Run(); + testCase.RecordSuccess(testCase.SuccessMessage); + } + catch (Exception ex) + { + this.outputHelper.WriteLine($"Test '{testCase.Name}' threw an exception: {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}"); + testCase.RecordFailure($"Test execution failed: {ex.Message}", ex); + throw; + } + finally + { + testCase.FinalizeRun(); + } + + this.outputHelper.WriteLine($"Mod test '{testCase.Name}' completed."); + this.outputHelper.WriteLine($" Status: {testCase.CurrentStatus}"); + this.outputHelper.WriteLine($" Details: {testCase.Details}"); + this.outputHelper.WriteLine($" Duration: {testCase.TestStopwatch.ElapsedMilliseconds}ms"); + + Assert.Equal(TestStatus.Passed, testCase.CurrentStatus); + } + } +} diff --git a/tests/Core/TestTypes.cs b/tests/Core/TestTypes.cs new file mode 100644 index 00000000..5de9f876 --- /dev/null +++ b/tests/Core/TestTypes.cs @@ -0,0 +1,66 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +namespace NightTest.Core +{ + /// + /// Represents the type of a test case. + /// + public enum TestType + { + /// + /// Automated test. + /// + Automated, + + /// + /// Manual test. + /// + Manual, + } + + /// + /// Represents the status of a test case execution. + /// + public enum TestStatus + { + /// + /// The test has not been run yet. + /// + NotRun, + + /// + /// The test completed successfully. + /// + Passed, + + /// + /// The test completed with errors. + /// + Failed, + + /// + /// The test was intentionally skipped (e.g., due to filtering). + /// + Skipped, + } +} diff --git a/tests/Core/XUnitLogSink.cs b/tests/Core/XUnitLogSink.cs new file mode 100644 index 00000000..fea1eadf --- /dev/null +++ b/tests/Core/XUnitLogSink.cs @@ -0,0 +1,73 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System.Globalization; +using System.Text; + +using Night; + +using Xunit.Abstractions; + +namespace NightTest.Core +{ + /// + /// An ILogSink implementation that writes log entries to the xUnit test output. + /// + public class XUnitLogSink : ILogSink + { + private readonly ITestOutputHelper outputHelper; + + /// + /// Initializes a new instance of the class. + /// + /// The xUnit test output helper. + public XUnitLogSink(ITestOutputHelper outputHelper) + { + this.outputHelper = outputHelper; + } + + /// + public void Write(LogEntry entry) + { + var sb = new StringBuilder(); + _ = sb.Append(entry.TimestampUtc.ToString("yyyy-MM-dd'T'HH:mm:ss.fffffff'Z'", CultureInfo.InvariantCulture)); + _ = sb.Append(" [").Append(entry.Level.ToString().ToUpperInvariant()).Append(']'); + _ = sb.Append(" [").Append(entry.CategoryName).Append(']'); + _ = sb.Append(' ').Append(entry.Message); + + if (entry.Exception != null) + { + _ = sb.Append('\n').Append(entry.Exception); + } + + try + { + this.outputHelper.WriteLine(sb.ToString()); + } + catch (InvalidOperationException) + { + // This can happen if a test completes while a background thread is still logging. + // It's safe to ignore in this context. + } + } + } +} diff --git a/tests/Groups/Configuration/ConfigurationGroup.cs b/tests/Groups/Configuration/ConfigurationGroup.cs new file mode 100644 index 00000000..9b426c1f --- /dev/null +++ b/tests/Groups/Configuration/ConfigurationGroup.cs @@ -0,0 +1,80 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Generic; + +using Night; + +using NightTest.Core; + +using Xunit; +using Xunit.Abstractions; + +namespace NightTest.Groups.Configuration +{ + /// + /// Tests for the functionality. + /// + [Collection("SequentialTests")] + public class ConfigurationGroup : TestGroup + { + /// + /// Initializes a new instance of the class. + /// + /// The xUnit test output helper for logging. + public ConfigurationGroup(ITestOutputHelper outputHelper) + : base(outputHelper) + { + } + + /// + /// Runs ModTestCases for the GameConfig feature within the Configuration module. + /// This includes tests for getting and setting GameConfig properties, + /// and indirectly tests AudioConfig, WindowConfig, and ModuleConfig functionality + /// as they are part of GameConfig. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_ConfigurationGameConfig_ModTests() + { + this.Run_ModTestCase(new ConfigurationGameConfig_GetSet()); + } + + /// + /// Runs ModTestCases for the ConfigurationManager feature within the Configuration module. + /// This includes tests for IsLoaded property, and various scenarios of LoadConfig method + /// such as already loaded, file not existing, empty file, invalid JSON, and deserialization to null. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_ConfigurationManager_ModTests() + { + this.Run_ModTestCase(new ConfigurationManager_IsLoadedTest()); + this.Run_ModTestCase(new ConfigurationManager_LoadConfig_AlreadyLoadedTest()); + this.Run_ModTestCase(new ConfigurationManager_LoadConfig_FileNotExistsTest()); + this.Run_ModTestCase(new ConfigurationManager_LoadConfig_EmptyFileTest()); + this.Run_ModTestCase(new ConfigurationManager_LoadConfig_InvalidJsonTest()); + this.Run_ModTestCase(new ConfigurationManager_LoadConfig_DeserializesToNullTest()); + } + } +} diff --git a/tests/Groups/Configuration/ConfigurationManagerTests.cs b/tests/Groups/Configuration/ConfigurationManagerTests.cs new file mode 100644 index 00000000..12f17aa5 --- /dev/null +++ b/tests/Groups/Configuration/ConfigurationManagerTests.cs @@ -0,0 +1,343 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; +using System.Reflection; + +using Night; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Configuration +{ + /// + /// Tests the property. + /// + public class ConfigurationManager_IsLoadedTest : ModTestCase + { + /// + public override string Name => "Configuration.Manager.IsLoaded"; + + /// + public override string Description => "Tests the IsLoaded property of ConfigurationManager."; + + /// + public override string SuccessMessage => "ConfigurationManager.IsLoaded property behaves as expected."; + + /// + public override void Run() + { + ConfigurationManagerTestHelper.ResetConfigurationManager(); + Assert.False(ConfigurationManager.IsLoaded, "IsLoaded should be false initially."); + + ConfigurationManager.LoadConfig(); // Load with default (no file) + + Assert.True(ConfigurationManager.IsLoaded, "IsLoaded should be true after LoadConfig is called."); + ConfigurationManagerTestHelper.ResetConfigurationManager(); // Cleanup + } + } + + /// + /// Tests that does not reload the configuration + /// if it has already been loaded. + /// + public class ConfigurationManager_LoadConfig_AlreadyLoadedTest : ModTestCase + { + /// + public override string Name => "Configuration.Manager.LoadConfig.AlreadyLoaded"; + + /// + public override string Description => "Tests that LoadConfig does not reload if already loaded."; + + /// + public override string SuccessMessage => "LoadConfig correctly skips reloading if already loaded."; + + /// + public override void Run() + { + string? tempDir = null; + try + { + ConfigurationManagerTestHelper.ResetConfigurationManager(); + tempDir = ConfigurationManagerTestHelper.CreateTempConfigDirectory(); + + // Initial load (e.g., file not found, uses defaults) + ConfigurationManager.LoadConfig(tempDir); + GameConfig initialConfig = ConfigurationManager.CurrentConfig; + Assert.True(ConfigurationManager.IsLoaded, "Should be loaded after first call."); + + // Create a config file that *would* change settings if loaded + ConfigurationManagerTestHelper.CreateConfigFile(tempDir, "{\"Window\": {\"Title\": \"Specific Test Title\"}}"); + + // Attempt to load again + ConfigurationManager.LoadConfig(tempDir); + GameConfig secondConfig = ConfigurationManager.CurrentConfig; + + // Assert that the config hasn't changed because it shouldn't have reloaded + // This assumes GameConfig has a comparable Title or we check a specific default. + // For simplicity, we'll assume the default GameConfig() constructor sets a non-"Specific Test Title". + Assert.NotEqual("Specific Test Title", secondConfig.Window.Title); // Removed message for NotEqual + Assert.Same(initialConfig, secondConfig); // Removed message for Same + } + finally + { + ConfigurationManagerTestHelper.ResetConfigurationManager(); + ConfigurationManagerTestHelper.CleanupTempDirectory(tempDir); + } + } + } + + /// + /// Tests the behavior of when the + /// 'config.json' file does not exist. Expects default configuration to be used. + /// + public class ConfigurationManager_LoadConfig_FileNotExistsTest : ModTestCase + { + /// + public override string Name => "Configuration.Manager.LoadConfig.FileNotExists"; + + /// + public override string Description => "Tests LoadConfig behavior when config.json does not exist; should use defaults."; + + /// + public override string SuccessMessage => "LoadConfig uses default configuration when config.json is not found."; + + /// + public override void Run() + { + string? tempDir = null; + try + { + ConfigurationManagerTestHelper.ResetConfigurationManager(); + tempDir = ConfigurationManagerTestHelper.CreateTempConfigDirectory(); + GameConfig defaultConfig = new GameConfig(); // For comparison + + ConfigurationManager.LoadConfig(tempDir); // tempDir is empty + + Assert.True(ConfigurationManager.IsLoaded, "IsLoaded should be true."); + Assert.Equal(defaultConfig.Window.Title, ConfigurationManager.CurrentConfig.Window.Title); + Assert.Equal(defaultConfig.Window.Width, ConfigurationManager.CurrentConfig.Window.Width); + } + finally + { + ConfigurationManagerTestHelper.ResetConfigurationManager(); + ConfigurationManagerTestHelper.CleanupTempDirectory(tempDir); + } + } + } + + /// + /// Tests the behavior of when 'config.json' + /// is empty. Expects default configuration to be used. + /// + public class ConfigurationManager_LoadConfig_EmptyFileTest : ModTestCase + { + /// + public override string Name => "Configuration.Manager.LoadConfig.EmptyFile"; + + /// + public override string Description => "Tests LoadConfig behavior with an empty config.json; should use defaults."; + + /// + public override string SuccessMessage => "LoadConfig uses default configuration for an empty config.json."; + + /// + public override void Run() + { + string? tempDir = null; + try + { + ConfigurationManagerTestHelper.ResetConfigurationManager(); + tempDir = ConfigurationManagerTestHelper.CreateTempConfigDirectory(); + ConfigurationManagerTestHelper.CreateConfigFile(tempDir, string.Empty); // Create empty config file + GameConfig defaultConfig = new GameConfig(); + + ConfigurationManager.LoadConfig(tempDir); + + Assert.True(ConfigurationManager.IsLoaded, "IsLoaded should be true."); + Assert.Equal(defaultConfig.Window.Title, ConfigurationManager.CurrentConfig.Window.Title); + } + finally + { + ConfigurationManagerTestHelper.ResetConfigurationManager(); + ConfigurationManagerTestHelper.CleanupTempDirectory(tempDir); + } + } + } + + /// + /// Tests the behavior of when 'config.json' + /// contains malformed JSON. Expects default configuration to be used. + /// + public class ConfigurationManager_LoadConfig_InvalidJsonTest : ModTestCase + { + /// + public override string Name => "Configuration.Manager.LoadConfig.InvalidJson"; + + /// + public override string Description => "Tests LoadConfig behavior with a malformed config.json; should use defaults."; + + /// + public override string SuccessMessage => "LoadConfig uses default configuration for invalid JSON."; + + /// + public override void Run() + { + string? tempDir = null; + try + { + ConfigurationManagerTestHelper.ResetConfigurationManager(); + tempDir = ConfigurationManagerTestHelper.CreateTempConfigDirectory(); + ConfigurationManagerTestHelper.CreateConfigFile(tempDir, "{ \"Window\": { \"Title\": \"Test\", "); // Invalid JSON + GameConfig defaultConfig = new GameConfig(); + + ConfigurationManager.LoadConfig(tempDir); + + Assert.True(ConfigurationManager.IsLoaded, "IsLoaded should be true."); + Assert.Equal(defaultConfig.Window.Title, ConfigurationManager.CurrentConfig.Window.Title); + } + finally + { + ConfigurationManagerTestHelper.ResetConfigurationManager(); + ConfigurationManagerTestHelper.CleanupTempDirectory(tempDir); + } + } + } + + /// + /// Tests the behavior of when the JSON content + /// of 'config.json' deserializes to null. Expects default configuration to be used. + /// + public class ConfigurationManager_LoadConfig_DeserializesToNullTest : ModTestCase + { + /// + public override string Name => "Configuration.Manager.LoadConfig.DeserializesToNull"; + + /// + public override string Description => "Tests LoadConfig behavior when JSON deserializes to null; should use defaults."; + + /// + public override string SuccessMessage => "LoadConfig uses default configuration when deserialization results in null."; + + /// + public override void Run() + { + string? tempDir = null; + try + { + ConfigurationManagerTestHelper.ResetConfigurationManager(); + tempDir = ConfigurationManagerTestHelper.CreateTempConfigDirectory(); + + // Valid JSON structure, but perhaps for a different type or content that results in null for GameConfig + // For GameConfig, an empty object might deserialize to a default GameConfig, not null. + // A more direct way to test the "loadedConfig == null" path (line 82 in ConfigurationManager) + // would be if JsonSerializer.Deserialize itself returned null for some valid-looking JSON. + // This test assumes such a scenario is possible, e.g. "null" as content. + ConfigurationManagerTestHelper.CreateConfigFile(tempDir, "null"); + GameConfig defaultConfig = new GameConfig(); + + ConfigurationManager.LoadConfig(tempDir); + + Assert.True(ConfigurationManager.IsLoaded, "IsLoaded should be true."); + Assert.Equal(defaultConfig.Window.Title, ConfigurationManager.CurrentConfig.Window.Title); + } + finally + { + ConfigurationManagerTestHelper.ResetConfigurationManager(); + ConfigurationManagerTestHelper.CleanupTempDirectory(tempDir); + } + } + } + + /// + /// Provides helper methods for testing the . + /// This includes resetting its static state and managing temporary files/directories for tests. + /// + internal static class ConfigurationManagerTestHelper + { + private static FieldInfo? isLoadedField; + private static FieldInfo? currentConfigField; + + /// + /// Initializes static members of the class. + /// Retrieves reflection info for private static fields of . + /// + static ConfigurationManagerTestHelper() + { + isLoadedField = typeof(ConfigurationManager).GetField("isLoaded", BindingFlags.NonPublic | BindingFlags.Static); + currentConfigField = typeof(ConfigurationManager).GetField("currentConfig", BindingFlags.NonPublic | BindingFlags.Static); + } + + /// + /// Resets the static state of the to its default. + /// This sets 'isLoaded' to false and 'currentConfig' to a new default instance. + /// + public static void ResetConfigurationManager() + { + isLoadedField?.SetValue(null, false); + currentConfigField?.SetValue(null, new GameConfig()); // Reset to default config + } + + /// + /// Creates a temporary directory for configuration test files. + /// + /// The path to the created temporary directory. + public static string CreateTempConfigDirectory() + { + string tempDir = Path.Combine(Path.GetTempPath(), "NightTest_Config_" + Guid.NewGuid().ToString()); + _ = Directory.CreateDirectory(tempDir); + return tempDir; + } + + /// + /// Cleans up (deletes) a specified temporary directory and its contents. + /// + /// The path to the temporary directory to delete. + public static void CleanupTempDirectory(string? directoryPath) + { + if (!string.IsNullOrEmpty(directoryPath) && Directory.Exists(directoryPath)) + { + try + { + Directory.Delete(directoryPath, true); + } + catch (Exception ex) + { + Console.WriteLine($"Error cleaning up temp directory '{directoryPath}': {ex.Message}"); + } + } + } + + /// + /// Creates a 'config.json' file with the specified content in the given directory. + /// + /// The directory where the config file will be created. + /// The string content to write to the config file. + public static void CreateConfigFile(string directoryPath, string content) + { + File.WriteAllText(Path.Combine(directoryPath, "config.json"), content); + } + } +} diff --git a/tests/Groups/Configuration/GameConfigTest.cs b/tests/Groups/Configuration/GameConfigTest.cs new file mode 100644 index 00000000..7bbe3a72 --- /dev/null +++ b/tests/Groups/Configuration/GameConfigTest.cs @@ -0,0 +1,144 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; + +using Night; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Configuration +{ + /// + /// Tests for Night.Configuration.GameConfig. + /// + public class ConfigurationGameConfig_GetSet : ModTestCase + { + /// + public override string Name => "Configuration.GameConfig"; + + /// + public override string Description => "Tests getters and setters."; + + /// + public override string SuccessMessage => "GameConfig getters and setters passed successfully."; + + /// + public override void Run() + { + GameConfig config = new GameConfig(); + config.Identity = "TestGame"; + Assert.Equal("TestGame", config.Identity); + + config.AppendIdentity = true; + Assert.True(config.AppendIdentity); + + config.Version = "1.0.0"; + Assert.Equal("1.0.0", config.Version); + + config.Console = true; + Assert.True(config.Console); + + config.AccelerometerJoystick = false; + Assert.False(config.AccelerometerJoystick); + + config.ExternalStorage = true; + Assert.True(config.ExternalStorage); + + config.GammaCorrect = false; + Assert.False(config.GammaCorrect); + + config.Audio = new AudioConfig + { + MixWithSystem = true, + }; + Assert.True(config.Audio.MixWithSystem); + + config.Window = new WindowConfig + { + Title = "Test Window", + IconPath = "icon.png", + Width = 1024, + Height = 768, + X = 100, + Y = 200, + MinWidth = 800, + MinHeight = 600, + Resizable = true, + Borderless = true, + Fullscreen = false, + FullscreenType = "exclusive", + VSync = true, + HighDPI = true, + MSAA = 4, + Depth = 24, + Stencil = 8, + Display = 0, + UseDPIScale = false, + }; + Assert.Equal("Test Window", config.Window.Title); + Assert.Equal("icon.png", config.Window.IconPath); + Assert.Equal(1024, config.Window.Width); + Assert.Equal(768, config.Window.Height); + Assert.Equal(100, config.Window.X); + Assert.Equal(200, config.Window.Y); + Assert.Equal(800, config.Window.MinWidth); + Assert.Equal(600, config.Window.MinHeight); + Assert.True(config.Window.Resizable); + Assert.True(config.Window.Borderless); + Assert.False(config.Window.Fullscreen); + Assert.Equal("exclusive", config.Window.FullscreenType); + Assert.True(config.Window.VSync); + Assert.True(config.Window.HighDPI); + Assert.Equal(4, config.Window.MSAA); + Assert.Equal(24, config.Window.Depth); + Assert.Equal(8, config.Window.Stencil); + Assert.Equal(0, config.Window.Display); + Assert.False(config.Window.UseDPIScale); + + config.Modules = new ModulesConfig + { + Audio = false, + Data = false, + Event = false, + Font = false, + Graphics = false, + Image = false, + Joystick = false, + Keyboard = false, + Math = false, + Mouse = false, + Physics = false, + Sound = false, + System = false, + Timer = false, + Touch = false, + Video = false, + WindowModule = false, + Thread = false, + }; + } + } +} diff --git a/tests/Groups/Filesystem/AppendTests.cs b/tests/Groups/Filesystem/AppendTests.cs new file mode 100644 index 00000000..10f2b51f --- /dev/null +++ b/tests/Groups/Filesystem/AppendTests.cs @@ -0,0 +1,211 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System.IO; +using System.Text; + +using Night; + +using NightTest.Core; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Base class for Filesystem.Append tests, handling setup and cleanup of the save directory. + /// + public abstract class BaseAppendTest : GameTestCase + { +#pragma warning disable SA1401 // Fields should be private + /// + /// The unique identity used for this test group to isolate the save directory. + /// + protected readonly string TestIdentity = "NightTest_Append"; +#pragma warning restore SA1401 // Fields should be private + + /// + protected override void Load() + { + Night.Filesystem.SetIdentity(this.TestIdentity); + var saveRoot = Night.Filesystem.GetSaveDirectory(); + + // Clean up from previous runs + if (Directory.Exists(saveRoot)) + { + Directory.Delete(saveRoot, true); + } + + _ = Directory.CreateDirectory(saveRoot); + } + } + + /// + /// Tests appending a string to a new file. + /// + public class Append_String_NewFile : BaseAppendTest + { + /// + public override string Name => "Filesystem.Append_String_NewFile"; + + /// + public override string Description => "Tests appending a string to a new file in the save directory."; + + /// + protected override void Update(double deltaTime) + { + var (success, error) = Night.Filesystem.Append("new_file.txt", "Hello"); + if (!success) + { + this.RecordFailure($"Append failed: {error}"); + this.EndTest(); + return; + } + + var content = File.ReadAllText(Path.Combine(Night.Filesystem.GetSaveDirectory(), "new_file.txt")); + if (content == "Hello") + { + this.RecordSuccess("Successfully appended to a new file."); + } + else + { + this.RecordFailure($"File content mismatch. Expected 'Hello', got '{content}'."); + } + + this.EndTest(); + } + } + + /// + /// Tests appending a string to an already existing file. + /// + public class Append_String_ExistingFile : BaseAppendTest + { + /// + public override string Name => "Filesystem.Append_String_ExistingFile"; + + /// + public override string Description => "Tests appending a string to an existing file."; + + /// + protected override void Load() + { + base.Load(); + File.WriteAllText(Path.Combine(Night.Filesystem.GetSaveDirectory(), "existing.txt"), "Initial."); + } + + /// + protected override void Update(double deltaTime) + { + var (success, error) = Night.Filesystem.Append("existing.txt", " Appended."); + if (!success) + { + this.RecordFailure($"Append failed: {error}"); + this.EndTest(); + return; + } + + var content = File.ReadAllText(Path.Combine(Night.Filesystem.GetSaveDirectory(), "existing.txt")); + if (content == "Initial. Appended.") + { + this.RecordSuccess("Successfully appended to an existing file."); + } + else + { + this.RecordFailure($"File content mismatch. Expected 'Initial. Appended.', got '{content}'."); + } + + this.EndTest(); + } + } + + /// + /// Tests appending to a file located within a subdirectory that needs to be created. + /// + public class Append_String_WithPath : BaseAppendTest + { + /// + public override string Name => "Filesystem.Append_String_WithPath"; + + /// + public override string Description => "Tests appending to a file in a subdirectory of the save directory."; + + /// + protected override void Update(double deltaTime) + { + var (success, error) = Night.Filesystem.Append("subdir/path.txt", "Subdir content"); + if (!success) + { + this.RecordFailure($"Append failed: {error}"); + this.EndTest(); + return; + } + + var filePath = Path.Combine(Night.Filesystem.GetSaveDirectory(), "subdir", "path.txt"); + if (File.Exists(filePath) && File.ReadAllText(filePath) == "Subdir content") + { + this.RecordSuccess("Successfully appended to a file in a new subdirectory."); + } + else + { + this.RecordFailure("File was not created or content is incorrect in subdirectory."); + } + + this.EndTest(); + } + } + + /// + /// Tests appending a byte array to a new file. + /// + public class Append_Bytes_NewFile : BaseAppendTest + { + /// + public override string Name => "Filesystem.Append_Bytes_NewFile"; + + /// + public override string Description => "Tests appending bytes to a new file."; + + /// + protected override void Update(double deltaTime) + { + var data = Encoding.UTF8.GetBytes("Byte data"); + var (success, error) = Night.Filesystem.Append("bytes.txt", data); + if (!success) + { + this.RecordFailure($"Append failed: {error}"); + this.EndTest(); + return; + } + + var content = File.ReadAllBytes(Path.Combine(Night.Filesystem.GetSaveDirectory(), "bytes.txt")); + if (Encoding.UTF8.GetString(content) == "Byte data") + { + this.RecordSuccess("Successfully appended bytes to a new file."); + } + else + { + this.RecordFailure("Byte content mismatch."); + } + + this.EndTest(); + } + } +} diff --git a/tests/Groups/Filesystem/DirectoryTests.cs b/tests/Groups/Filesystem/DirectoryTests.cs new file mode 100644 index 00000000..ba30de8e --- /dev/null +++ b/tests/Groups/Filesystem/DirectoryTests.cs @@ -0,0 +1,203 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; + +using Night; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Tests for Night.Filesystem.CreateDirectory(). + /// + public class FilesystemCreateDirectory_NewSingleDirTest : ModTestCase + { + /// + public override string Name => "Filesystem.CreateDirectory.NewSingleDir"; + + /// + public override string Description => "Tests CreateDirectory for a new single directory."; + + /// + public override string SuccessMessage => "Successfully created a new single directory."; + + /// + public override void Run() + { + var testDirName = Path.Combine(Path.GetTempPath(), "night_test_createdir_single"); + if (Directory.Exists(testDirName)) + { + Directory.Delete(testDirName, true); + } + + try + { + var created = Night.Filesystem.CreateDirectory(testDirName); + Assert.True(created, "CreateDirectory should return true for a new directory."); + Assert.True(Directory.Exists(testDirName), "Directory should exist after creation."); + } + finally + { + if (Directory.Exists(testDirName)) + { + Directory.Delete(testDirName, true); + } + } + } + } + + /// + /// Tests CreateDirectory when the directory already exists. + /// + public class FilesystemCreateDirectory_ExistingDirTest : ModTestCase + { + /// + public override string Name => "Filesystem.CreateDirectory.ExistingDir"; + + /// + public override string Description => "Tests CreateDirectory when the directory already exists, expecting false."; + + /// + public override string SuccessMessage => "CreateDirectory correctly returned false for an existing directory."; + + /// + public override void Run() + { + var testDirName = Path.Combine(Path.GetTempPath(), "night_test_createdir_existing"); + _ = Directory.CreateDirectory(testDirName); // Ensure it exists + + try + { + var created = Night.Filesystem.CreateDirectory(testDirName); + Assert.False(created, "CreateDirectory should return false for an existing directory."); + Assert.True(Directory.Exists(testDirName), "Directory should still exist."); + } + finally + { + if (Directory.Exists(testDirName)) + { + Directory.Delete(testDirName, true); + } + } + } + } + + /// + /// Tests CreateDirectory for nested directories. + /// + public class FilesystemCreateDirectory_NestedDirTest : ModTestCase + { + /// + public override string Name => "Filesystem.CreateDirectory.NestedDir"; + + /// + public override string Description => "Tests CreateDirectory for creating nested directories."; + + /// + public override string SuccessMessage => "Successfully created nested directories."; + + /// + public override void Run() + { + var parentDirName = Path.Combine(Path.GetTempPath(), "night_test_createdir_parent"); + var nestedDirName = Path.Combine(parentDirName, "child", "grandchild"); + + if (Directory.Exists(parentDirName)) + { + Directory.Delete(parentDirName, true); + } + + try + { + var created = Night.Filesystem.CreateDirectory(nestedDirName); + Assert.True(created, "CreateDirectory should return true for nested directories."); + Assert.True(Directory.Exists(nestedDirName), "Nested directory should exist after creation."); + } + finally + { + if (Directory.Exists(parentDirName)) + { + Directory.Delete(parentDirName, true); + } + } + } + } + + /// + /// Tests argument validation for CreateDirectory. + /// + public class FilesystemCreateDirectory_ArgumentValidationTest : ModTestCase + { + /// + public override string Name => "Filesystem.CreateDirectory.ArgumentValidation"; + + /// + public override string Description => "Tests argument validation for CreateDirectory (null path, empty path)."; + + /// + public override string SuccessMessage => "Argument validation for CreateDirectory works correctly."; + + /// + public override void Run() + { + _ = Assert.Throws(() => Night.Filesystem.CreateDirectory(null!)); + _ = Assert.Throws(() => Night.Filesystem.CreateDirectory(string.Empty)); + _ = Assert.Throws(() => Night.Filesystem.CreateDirectory(" ")); + } + } + + /// + /// Tests GetAppdataDirectory returns a valid path and creates the directory. + /// + public class FilesystemGetAppdataDirectory_ReturnsValidPathTest : ModTestCase + { + /// + public override string Name => "Filesystem.GetAppdataDirectory.ReturnsValidPath"; + + /// + public override string Description => "Tests GetAppdataDirectory returns a valid path and creates the directory."; + + /// + public override string SuccessMessage => "GetAppdataDirectory returned a valid, existing path."; + + /// + public override void Run() + { + // Note: This test can have side effects by creating a directory in the user's + // real AppData folder. This is acceptable for this test suite's scope. + var appDataDir = Night.Filesystem.GetAppdataDirectory(); + + Assert.False(string.IsNullOrWhiteSpace(appDataDir), "Appdata directory path should not be null or whitespace."); + Assert.True(Directory.Exists(appDataDir), "Appdata directory should be created by the method."); + + // Further check if the path seems reasonable (contains app name, etc.) + // This is a basic check and might need adjustment based on final GetAppdataDirectory logic. + var expectedEnd = "NightDefault"; + Assert.EndsWith(expectedEnd, appDataDir); + } + } +} diff --git a/tests/Groups/Filesystem/DroppedFileTests.cs b/tests/Groups/Filesystem/DroppedFileTests.cs new file mode 100644 index 00000000..49998d5a --- /dev/null +++ b/tests/Groups/Filesystem/DroppedFileTests.cs @@ -0,0 +1,54 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using Night; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Tests for Night.DroppedFile. + /// + public class DroppedFile_PathCorrectness : ModTestCase + { + /// + public override string Name => "DroppedFile.PathCorrectness"; + + /// + public override string Description => "Tests that the Path property of DroppedFile is correctly set."; + + /// + public override string SuccessMessage => "Successfully verified DroppedFile path handling."; + + /// + public override void Run() + { + const string testPath = "/path/to/some/file.txt"; + var droppedFile = new DroppedFile(testPath); + + Assert.Equal(testPath, droppedFile.Path); + } + } +} diff --git a/tests/Groups/Filesystem/FileDropEventTest.cs b/tests/Groups/Filesystem/FileDropEventTest.cs new file mode 100644 index 00000000..e85c10b9 --- /dev/null +++ b/tests/Groups/Filesystem/FileDropEventTest.cs @@ -0,0 +1,62 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System.Runtime.InteropServices; + +using Night; + +using NightTest.Core; + +using SDL3; + +using Xunit; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Tests the file drop event. + /// + public class FileDropEventTest : ModTestCase + { + /// + public override string Name => "FileDropEventTest"; + + /// + public override string Description => "Tests that the file drop event is triggered correctly."; + + /// + public override string SuccessMessage => "File drop event test passed."; + + /// + public override void Run() + { + const string testPath = "/path/to/some/file.txt"; + var testGame = new FileDropTestGame(testPath); + + // The test game will now handle the event and verify the path + // We need to run the game loop for a short time to process the event + Night.Framework.Run(testGame); + + Assert.Equal(TestStatus.Passed, testGame.CurrentStatus); + } + } +} diff --git a/tests/Groups/Filesystem/FileSystemInfoTest.cs b/tests/Groups/Filesystem/FileSystemInfoTest.cs new file mode 100644 index 00000000..c62bb468 --- /dev/null +++ b/tests/Groups/Filesystem/FileSystemInfoTest.cs @@ -0,0 +1,85 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; +using System.Linq; +using System.Text; + +using Night; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Tests for Night.FileSystemInfo() with no parameter values. + /// + public class FileSystemInfo_Constructor_DefaultValues : ModTestCase + { + /// + public override string Name => "Night.FileSystemInfo.Constructor.DefaultValues"; + + /// + public override string Description => "Tests FileSystemInfo constructor with default values."; + + /// + public override string SuccessMessage => "Successfully created FileSystemInfo with default values."; + + /// + public override void Run() + { + var info = new Night.FileSystemInfo(); + Assert.NotNull(info); + Assert.Equal(FileType.None, info.Type); + Assert.Null(info.Size); + Assert.Null(info.ModTime); + } + } + + /// + /// Tests for Night.FileSystemInfo() with parameter values. + /// + public class FileSystemInfo_Constructor : ModTestCase + { + /// + public override string Name => "Night.FileSystemInfo.Constructor"; + + /// + public override string Description => "Tests FileSystemInfo constructor with parameter values."; + + /// + public override string SuccessMessage => "Successfully created FileSystemInfo with specified values."; + + /// + public override void Run() + { + var info = new Night.FileSystemInfo(FileType.File, 12345, 1622547800); + Assert.NotNull(info); + Assert.Equal(FileType.File, info.Type); + Assert.Equal(12345, info.Size); + Assert.Equal(1622547800, info.ModTime); + } + } +} diff --git a/tests/Groups/Filesystem/FilesystemGroup.cs b/tests/Groups/Filesystem/FilesystemGroup.cs new file mode 100644 index 00000000..ee8b7829 --- /dev/null +++ b/tests/Groups/Filesystem/FilesystemGroup.cs @@ -0,0 +1,303 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Generic; + +using Night; + +using NightTest.Core; + +using Xunit; +using Xunit.Abstractions; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Tests for the Night.Filesystem functionality. + /// + [Collection("SequentialTests")] + public partial class FilesystemGroup : TestGroup + { + /// + /// Initializes a new instance of the class. + /// + /// The xUnit test output helper for logging. + public FilesystemGroup(ITestOutputHelper outputHelper) + : base(outputHelper) + { + } + + /// + /// Runs all Filesystem.Lines mod test cases. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_FilesystemLines_ModTests() + { + this.Run_ModTestCase(new FilesystemLines_ReadStandardFileTest()); + this.Run_ModTestCase(new FilesystemLines_ReadEmptyFileTest()); + this.Run_ModTestCase(new FilesystemLines_FileNotFoundTest()); + this.Run_ModTestCase(new FilesystemLines_ReadSingleLineFileTest()); + this.Run_ModTestCase(new FilesystemLines_NullPathTest()); + this.Run_ModTestCase(new FilesystemLines_EmptyPathTest()); + this.Run_ModTestCase(new FilesystemLines_ReadDirectoryTest()); + this.Run_ModTestCase(new FilesystemLines_LockedFileTest()); + this.Run_ModTestCase(new FilesystemLines_ThrowsArgumentNullExceptionOnNullPathTest()); + this.Run_ModTestCase(new FilesystemLines_ThrowsArgumentExceptionOnEmptyPathTest()); + } + + // Tests from FilesystemGetInfoTests.cs + + /// + /// Runs all Filesystem.GetInfo mod test cases. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_FilesystemGetInfo_ModTests() + { + this.Run_ModTestCase(new FilesystemGetInfo_NullPath_ReturnsNullTest()); + this.Run_ModTestCase(new FilesystemGetInfo_EmptyPath_ReturnsNullTest()); + this.Run_ModTestCase(new FilesystemGetInfo_PathDoesNotExist_ReturnsNullTest()); + this.Run_ModTestCase(new FilesystemGetInfo_FileExists_NoFilter_ReturnsFileInfoTest()); + this.Run_ModTestCase(new FilesystemGetInfo_FileExists_MatchingFilter_ReturnsFileInfoTest()); + this.Run_ModTestCase(new FilesystemGetInfo_FileExists_NonMatchingFilter_ReturnsNullTest()); + this.Run_ModTestCase(new FilesystemGetInfo_DirectoryExists_NoFilter_ReturnsDirectoryInfoTest()); + this.Run_ModTestCase(new FilesystemGetInfo_DirectoryExists_MatchingFilter_ReturnsDirectoryInfoTest()); + this.Run_ModTestCase(new FilesystemGetInfo_DirectoryExists_NonMatchingFilter_ReturnsNullTest()); + this.Run_ModTestCase(new FilesystemGetInfo_PopulateNullInfoObject_ReturnsNullTest()); + this.Run_ModTestCase(new FilesystemGetInfo_PopulateValidInfo_FileExists_PopulatesAndReturnsInfoTest()); + this.Run_ModTestCase(new FilesystemGetInfo_PopulateValidInfo_DirectoryExists_PopulatesAndReturnsInfoTest()); + this.Run_ModTestCase(new FilesystemGetInfo_PopulateValidInfo_PathDoesNotExist_ReturnsNullTest()); + this.Run_ModTestCase(new FilesystemGetInfo_PopulateWithFilter_NullInfoObject_ReturnsNullTest()); + this.Run_ModTestCase(new FilesystemGetInfo_PopulateWithFilter_FileExists_MatchingFilter_PopulatesInfoTest()); + this.Run_ModTestCase(new FilesystemGetInfo_PopulateWithFilter_FileExists_NonMatchingFilter_ReturnsNullTest()); + this.Run_ModTestCase(new FilesystemGetInfo_PopulateWithFilter_DirectoryExists_MatchingFilter_PopulatesInfoTest()); + this.Run_ModTestCase(new FilesystemGetInfo_PopulateWithFilter_DirectoryExists_NonMatchingFilter_ReturnsNullTest()); + this.Run_ModTestCase(new FilesystemGetInfo_PopulateWithFilter_PathDoesNotExist_ReturnsNullTest()); + + // this.Run_ModTestCase(new FilesystemGetInfo_SymbolicLinkTest()); // Disabled due to privilege issues on test runner + } + + // Tests from ReadWriteTests.cs + + /// + /// Runs all Filesystem.ReadBytes mod test cases. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_FilesystemReadBytes_ModTests() + { + this.Run_ModTestCase(new FilesystemReadBytes_ReadExistingFileTest()); + this.Run_ModTestCase(new FilesystemReadBytes_FileNotFoundTest()); + } + + /// + /// Runs all Filesystem.ReadText mod test cases. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_FilesystemReadText_ModTests() + { + this.Run_ModTestCase(new FilesystemReadText_ReadExistingFileTest()); + this.Run_ModTestCase(new FilesystemReadText_FileNotFoundTest()); + } + + /// + /// Runs all Filesystem.Read mod test cases. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_FilesystemRead_ModTests() + { + this.Run_ModTestCase(new FilesystemRead_String_ReadExistingFileTest()); + this.Run_ModTestCase(new FilesystemRead_Data_ReadExistingFileTest()); + this.Run_ModTestCase(new FilesystemRead_FileNotFoundTest()); + this.Run_ModTestCase(new FilesystemRead_String_PartialReadTest()); + this.Run_ModTestCase(new FilesystemRead_Data_PartialReadTest()); + this.Run_ModTestCase(new FilesystemRead_EmptyFileTest()); + this.Run_ModTestCase(new FilesystemRead_ArgumentValidationTest()); + this.Run_ModTestCase(new FilesystemRead_String_ReadDirectoryTest()); + this.Run_ModTestCase(new FilesystemRead_Data_ReadDirectoryTest()); + this.Run_ModTestCase(new FilesystemRead_String_LockedFileTest()); + this.Run_ModTestCase(new FilesystemRead_Data_LockedFileTest()); + this.Run_ModTestCase(new FilesystemRead_UnauthorizedAccessTest()); + this.Run_ModTestCase(new FilesystemRead_DecodingErrorTest()); + this.Run_ModTestCase(new FilesystemRead_CappingLogicForLargeFileTest()); + } + + /// + /// Runs all Filesystem.CreateDirectory mod test cases. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_FilesystemCreateDirectory_ModTests() + { + this.Run_ModTestCase(new FilesystemCreateDirectory_NewSingleDirTest()); + this.Run_ModTestCase(new FilesystemCreateDirectory_ExistingDirTest()); + this.Run_ModTestCase(new FilesystemCreateDirectory_NestedDirTest()); + this.Run_ModTestCase(new FilesystemCreateDirectory_ArgumentValidationTest()); + } + + /// + /// Runs a test to ensure returns a valid path. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_FilesystemGetAppdataDirectory_ReturnsValidPathTest() + { + this.Run_ModTestCase(new FilesystemGetAppdataDirectory_ReturnsValidPathTest()); + } + + /// + /// Runs all Filesystem.GetSaveDirectory mod test cases. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_FilesystemGetSaveDirectory_ModTests() + { + this.Run_ModTestCase(new GetSaveDirectory_DefaultIdentityTest()); + this.Run_ModTestCase(new GetSaveDirectory_CustomIdentityTest()); + } + + /// + /// Runs all mod test cases. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_NightFile_ModTests() + { + this.Run_ModTestCase(new NightFile_Constructor()); + this.Run_ModTestCase(new NightFile_OpenClose()); + this.Run_ModTestCase(new NightFile_OpenModes()); + this.Run_ModTestCase(new NightFile_OpenInvalidCases()); + this.Run_ModTestCase(new NightFile_Read_Full()); + this.Run_ModTestCase(new NightFile_Read_Bytes()); + this.Run_ModTestCase(new NightFile_Read_BytesCounted()); + this.Run_ModTestCase(new NightFile_Dispose()); + } + + /// + /// Runs all mod test cases. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_FilesystemWrite_ModTests() + { + this.Run_ModTestCase(new FilesystemWrite_String_BasicNewFileTest()); + this.Run_ModTestCase(new FilesystemWrite_Bytes_BasicNewFileTest()); + this.Run_ModTestCase(new FilesystemWrite_String_OverwriteExistingFileTest()); + this.Run_ModTestCase(new FilesystemWrite_Bytes_OverwriteExistingFileTest()); + this.Run_ModTestCase(new FilesystemWrite_String_WithSizeTest()); + this.Run_ModTestCase(new FilesystemWrite_Bytes_WithSizeTest()); + this.Run_ModTestCase(new FilesystemWrite_String_SizeLargerThanDataTest()); + this.Run_ModTestCase(new FilesystemWrite_Bytes_SizeLargerThanDataTest()); + this.Run_ModTestCase(new FilesystemWrite_String_EmptyStringTest()); + this.Run_ModTestCase(new FilesystemWrite_Bytes_EmptyArrayTest()); + this.Run_ModTestCase(new FilesystemWrite_String_ZeroSizeTest()); + this.Run_ModTestCase(new FilesystemWrite_Bytes_ZeroSizeTest()); + this.Run_ModTestCase(new FilesystemWrite_ArgumentValidationTest()); + this.Run_ModTestCase(new FilesystemWrite_CreateDirectoryTest()); + this.Run_ModTestCase(new FilesystemWrite_PathIsDirectoryTest()); + this.Run_ModTestCase(new FilesystemWrite_Error_InvalidArgumentCharsTest()); + this.Run_ModTestCase(new FilesystemWrite_Error_IOExceptionLockedTest()); + + // TODO: Fix these tests + // this.Run_ModTestCase(new FilesystemWrite_Error_PathTooLongTest()); + // this.Run_ModTestCase(new FilesystemWrite_Error_SecurityExceptionTest()); + // this.Run_ModTestCase(new FilesystemWrite_Error_NotSupportedTest()); + // this.Run_ModTestCase(new FilesystemWrite_Error_DirectoryNotFoundUnmappedDriveTest()); + } + + /// + /// Runs all mod test cases. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_FileSystemInfo_ModTests() + { + this.Run_ModTestCase(new FileSystemInfo_Constructor_DefaultValues()); + this.Run_ModTestCase(new FileSystemInfo_Constructor()); + } + + /// + /// Runs the FileDropEventTest. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_FileDropEventTest() + { + this.Run_ModTestCase(new FileDropEventTest()); + } + + /// + /// Runs all Filesystem.Remove mod test cases. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_FilesystemRemove_ModTests() + { + this.Run_GameTestCase(new RemoveFileTest()); + this.Run_GameTestCase(new RemoveEmptyDirTest()); + this.Run_GameTestCase(new RemoveNonEmptyDirTest()); + this.Run_GameTestCase(new RemoveOutsideSaveDirTest()); + this.Run_GameTestCase(new RemoveNotFoundTest()); + } + + /// + /// Runs all Filesystem.NewFileData mod test cases. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_FilesystemNewFileData_ModTests() + { + this.Run_GameTestCase(new NewFileDataFromBytesTest()); + this.Run_GameTestCase(new NewFileDataFromStringTest()); + } + + /// + /// Runs all Filesystem.GetDirectoryItems mod test cases. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_FilesystemGetDirectoryItems_ModTests() + { + this.Run_GameTestCase(new GetDirectoryItems_SaveAndSource_Combined()); + this.Run_GameTestCase(new GetDirectoryItems_SaveOnly()); + this.Run_GameTestCase(new GetDirectoryItems_SourceOnly()); + this.Run_GameTestCase(new GetDirectoryItems_NotFound()); + } + + /// + /// Runs all Filesystem.Append mod test cases. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_FilesystemAppend_Tests() + { + this.Run_GameTestCase(new Append_String_NewFile()); + this.Run_GameTestCase(new Append_String_ExistingFile()); + this.Run_GameTestCase(new Append_String_WithPath()); + this.Run_GameTestCase(new Append_Bytes_NewFile()); + } + } +} diff --git a/tests/Groups/Filesystem/GetDirectoryItemsTests.cs b/tests/Groups/Filesystem/GetDirectoryItemsTests.cs new file mode 100644 index 00000000..f67fb8a4 --- /dev/null +++ b/tests/Groups/Filesystem/GetDirectoryItemsTests.cs @@ -0,0 +1,253 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using Night; + +using NightTest.Core; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Base class for GetDirectoryItems tests, handling setup and cleanup. + /// + public abstract class BaseGetDirectoryItemsTest : GameTestCase + { + /// + /// Gets the name of the temporary directory used for testing. + /// +#pragma warning disable SA1401 // Fields should be private + protected readonly string TestDirName = "gdi_test_dir"; + + /// + /// Gets the full path to the test directory within the save directory. + /// + protected string testSaveDir = string.Empty; + + /// + /// Gets the full path to the test directory within the source directory. + /// + protected string testSourceDir = string.Empty; +#pragma warning restore SA1401 // Fields should be private + + private readonly string testIdentity = "NightTest_GDI"; + + /// + protected override void Load() + { + Night.Filesystem.SetIdentity(this.testIdentity); + var saveRoot = Night.Filesystem.GetSaveDirectory(); + var sourceRoot = Night.Filesystem.GetSource(); + + this.testSaveDir = Path.Combine(saveRoot, this.TestDirName); + this.testSourceDir = Path.Combine(sourceRoot, this.TestDirName); + + // Clean up from previous runs to ensure a clean slate + if (Directory.Exists(this.testSaveDir)) + { + Directory.Delete(this.testSaveDir, true); + } + + if (Directory.Exists(this.testSourceDir)) + { + Directory.Delete(this.testSourceDir, true); + } + } + + /// + protected override void Update(double deltaTime) + { + // Base update is empty; derived classes implement test logic. + } + } + + /// + /// Tests GetDirectoryItems with files in both save and source directories. + /// + public class GetDirectoryItems_SaveAndSource_Combined : BaseGetDirectoryItemsTest + { + /// + public override string Name => "Filesystem.GetDirectoryItems_SaveAndSource_Combined"; + + /// + public override string Description => "Tests GetDirectoryItems with files in both save and source directories."; + + /// + protected override void Load() + { + base.Load(); + + _ = Directory.CreateDirectory(this.testSaveDir); + _ = Directory.CreateDirectory(this.testSourceDir); + + File.WriteAllText(Path.Combine(this.testSaveDir, "file_save.txt"), "save"); + File.WriteAllText(Path.Combine(this.testSourceDir, "file_source.txt"), "source"); + File.WriteAllText(Path.Combine(this.testSaveDir, "file_both.txt"), "save_version"); + File.WriteAllText(Path.Combine(this.testSourceDir, "file_both.txt"), "source_version"); + _ = Directory.CreateDirectory(Path.Combine(this.testSaveDir, "subdir_save")); + _ = Directory.CreateDirectory(Path.Combine(this.testSourceDir, "subdir_source")); + } + + /// + protected override void Update(double deltaTime) + { + var items = Night.Filesystem.GetDirectoryItems(this.TestDirName).ToList(); + var expectedItems = new List { "file_both.txt", "file_save.txt", "file_source.txt", "subdir_save", "subdir_source" }; + + items.Sort(StringComparer.Ordinal); + expectedItems.Sort(StringComparer.Ordinal); + + bool success = items.SequenceEqual(expectedItems); + + if (success) + { + this.RecordSuccess("Correctly listed and merged items from save and source."); + } + else + { + this.RecordFailure($"Item list mismatch. Expected: [{string.Join(", ", expectedItems)}], Got: [{string.Join(", ", items)}]"); + } + + this.EndTest(); + } + } + + /// + /// Tests GetDirectoryItems with files only in the save directory. + /// + public class GetDirectoryItems_SaveOnly : BaseGetDirectoryItemsTest + { + /// + public override string Name => "Filesystem.GetDirectoryItems_SaveOnly"; + + /// + public override string Description => "Tests GetDirectoryItems with files only in the save directory."; + + /// + protected override void Load() + { + base.Load(); + _ = Directory.CreateDirectory(this.testSaveDir); + File.WriteAllText(Path.Combine(this.testSaveDir, "save_file.txt"), "data"); + _ = Directory.CreateDirectory(Path.Combine(this.testSaveDir, "save_subdir")); + } + + /// + protected override void Update(double deltaTime) + { + var items = Night.Filesystem.GetDirectoryItems(this.TestDirName).ToList(); + var expectedItems = new List { "save_file.txt", "save_subdir" }; + + items.Sort(StringComparer.Ordinal); + expectedItems.Sort(StringComparer.Ordinal); + + bool success = items.SequenceEqual(expectedItems); + + if (success) + { + this.RecordSuccess("Correctly listed items from save directory."); + } + else + { + this.RecordFailure($"Item list mismatch. Expected: [{string.Join(", ", expectedItems)}], Got: [{string.Join(", ", items)}]"); + } + + this.EndTest(); + } + } + + /// + /// Tests GetDirectoryItems with files only in the source directory. + /// + public class GetDirectoryItems_SourceOnly : BaseGetDirectoryItemsTest + { + /// + public override string Name => "Filesystem.GetDirectoryItems_SourceOnly"; + + /// + public override string Description => "Tests GetDirectoryItems with files only in the source directory."; + + /// + protected override void Load() + { + base.Load(); + _ = Directory.CreateDirectory(this.testSourceDir); + File.WriteAllText(Path.Combine(this.testSourceDir, "source_file.txt"), "data"); + _ = Directory.CreateDirectory(Path.Combine(this.testSourceDir, "source_subdir")); + } + + /// + protected override void Update(double deltaTime) + { + var items = Night.Filesystem.GetDirectoryItems(this.TestDirName).ToList(); + var expectedItems = new List { "source_file.txt", "source_subdir" }; + + items.Sort(StringComparer.Ordinal); + expectedItems.Sort(StringComparer.Ordinal); + + bool success = items.SequenceEqual(expectedItems); + + if (success) + { + this.RecordSuccess("Correctly listed items from source directory."); + } + else + { + this.RecordFailure($"Item list mismatch. Expected: [{string.Join(", ", expectedItems)}], Got: [{string.Join(", ", items)}]"); + } + + this.EndTest(); + } + } + + /// + /// Tests GetDirectoryItems on a non-existent directory. + /// + public class GetDirectoryItems_NotFound : BaseGetDirectoryItemsTest + { + /// + public override string Name => "Filesystem.GetDirectoryItems_NotFound"; + + /// + public override string Description => "Tests GetDirectoryItems on a non-existent directory."; + + /// + protected override void Update(double deltaTime) + { + var items = Night.Filesystem.GetDirectoryItems("non_existent_dir_gdi").ToList(); + if (items.Count == 0) + { + this.RecordSuccess("Returned an empty list for a non-existent path as expected."); + } + else + { + this.RecordFailure($"Expected an empty list but got {items.Count} items."); + } + + this.EndTest(); + } + } +} diff --git a/tests/Groups/Filesystem/GetInfoTests.cs b/tests/Groups/Filesystem/GetInfoTests.cs new file mode 100644 index 00000000..bd078f94 --- /dev/null +++ b/tests/Groups/Filesystem/GetInfoTests.cs @@ -0,0 +1,1151 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; + +using Night; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Tests that returns null when the path is null. + /// + public class FilesystemGetInfo_NullPath_ReturnsNullTest : ModTestCase + { + /// + public override string Name => "Filesystem.GetInfo.NullPathMod"; + + /// + public override string Description => "Tests GetInfo with a null path, expecting null (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo with null path correctly returned null."; + + /// + public override void Run() + { + // Test with null, suppress warning as it's a test case + var info = Night.Filesystem.GetInfo(null!); + + // Assert + Assert.Null(info); + } + } + + /// + /// Tests that returns null when the path is empty. + /// + public class FilesystemGetInfo_EmptyPath_ReturnsNullTest : ModTestCase + { + /// + public override string Name => "Filesystem.GetInfo.EmptyPathMod"; + + /// + public override string Description => "Tests GetInfo with an empty path, expecting null (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo with empty path correctly returned null."; + + /// + public override void Run() + { + // Arrange + string path = string.Empty; + + // Act + var info = Night.Filesystem.GetInfo(path); + + // Assert + Assert.Null(info); + } + } + + /// + /// Tests that returns null for a path that does not exist. + /// + public class FilesystemGetInfo_PathDoesNotExist_ReturnsNullTest : ModTestCase + { + private readonly string nonExistentPath = Path.Combine(Path.GetTempPath(), $"night_test_non_existent_mod_{Guid.NewGuid()}"); + + /// + public override string Name => "Filesystem.GetInfo.PathDoesNotExistMod"; + + /// + public override string Description => "Tests GetInfo for a non-existent path, expecting null (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo for a non-existent path correctly returned null."; + + /// + public override void Run() + { + // Arrange + // Ensure path does not exist (highly unlikely for a GUID-based name, but good practice) + if (File.Exists(this.nonExistentPath)) + { + File.Delete(this.nonExistentPath); + } + + if (Directory.Exists(this.nonExistentPath)) + { + Directory.Delete(this.nonExistentPath); + } + + // Act + var info = Night.Filesystem.GetInfo(this.nonExistentPath); + + // Assert + Assert.Null(info); + } + } + + /// + /// Tests that returns correct info for an existing file when no filter is applied. + /// + public class FilesystemGetInfo_FileExists_NoFilter_ReturnsFileInfoTest : ModTestCase + { + private const string TestFileContent = "Hello Night!"; + private readonly string testFileName = Path.Combine(Path.GetTempPath(), $"night_test_file_exists_mod_{Guid.NewGuid()}.txt"); + + /// + public override string Name => "Filesystem.GetInfo.FileExistsNoFilterMod"; + + /// + public override string Description => "Tests GetInfo for an existing file with no filter, expecting correct FileSystemInfo (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo for an existing file with no filter returned correct FileSystemInfo."; + + /// + public override void Run() + { + // Arrange + long expectedSize = 0; + long expectedModTime = 0; + try + { + File.WriteAllText(this.testFileName, TestFileContent); + var fileInfo = new FileInfo(this.testFileName); + expectedSize = fileInfo.Length; + + // Ensure LastWriteTimeUtc is fresh for comparison + fileInfo.LastWriteTimeUtc = DateTime.UtcNow; + expectedModTime = new DateTimeOffset(fileInfo.LastWriteTimeUtc).ToUnixTimeSeconds(); + } + catch (Exception ex) + { + Assert.Fail($"Test setup failed: Could not create test file '{this.testFileName}'. {ex.Message}"); + } + + try + { + // Act + var info = Night.Filesystem.GetInfo(this.testFileName); + + // Assert + Assert.NotNull(info); + Assert.Equal(FileType.File, info.Type); + Assert.Equal(expectedSize, info.Size); + _ = Assert.NotNull(info.ModTime); + Assert.True(Math.Abs(info.ModTime.Value - expectedModTime) <= 2, $"Expected ModTime around {expectedModTime}, but got {info.ModTime}. Difference: {Math.Abs((info.ModTime ?? 0) - expectedModTime)}"); + } + finally + { + // Cleanup + if (File.Exists(this.testFileName)) + { + File.Delete(this.testFileName); + } + } + } + } + + /// + /// Tests that returns correct info for a file that matches the filter. + /// + public class FilesystemGetInfo_FileExists_MatchingFilter_ReturnsFileInfoTest : ModTestCase + { + private const string TestFileContent = "Filter Match!"; + private readonly string testFileName = Path.Combine(Path.GetTempPath(), $"night_test_file_match_filter_mod_{Guid.NewGuid()}.txt"); + + /// + public override string Name => "Filesystem.GetInfo.FileExistsMatchingFilterMod"; + + /// + public override string Description => "Tests GetInfo for an existing file with FileType.File filter, expecting correct FileSystemInfo (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo for an existing file with FileType.File filter returned correct FileSystemInfo."; + + /// + public override void Run() + { + // Arrange + try + { + File.WriteAllText(this.testFileName, TestFileContent); + + // Ensure LastWriteTimeUtc is fresh for comparison + new FileInfo(this.testFileName).LastWriteTimeUtc = DateTime.UtcNow; + } + catch (Exception ex) + { + Assert.Fail($"Test setup failed: Could not create test file '{this.testFileName}'. {ex.Message}"); + } + + try + { + // Act + var info = Night.Filesystem.GetInfo(this.testFileName, FileType.File); + + // Assert + Assert.NotNull(info); + Assert.Equal(FileType.File, info.Type); + } + finally + { + // Cleanup + if (File.Exists(this.testFileName)) + { + File.Delete(this.testFileName); + } + } + } + } + + /// + /// Tests that returns null for a file that does not match the filter. + /// + public class FilesystemGetInfo_FileExists_NonMatchingFilter_ReturnsNullTest : ModTestCase + { + private const string TestFileContent = "Filter No Match!"; + private readonly string testFileName = Path.Combine(Path.GetTempPath(), $"night_test_file_nonmatch_filter_mod_{Guid.NewGuid()}.txt"); + + /// + public override string Name => "Filesystem.GetInfo.FileExistsNonMatchingFilterMod"; + + /// + public override string Description => "Tests GetInfo for an existing file with FileType.Directory filter, expecting null (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo for an existing file with FileType.Directory filter correctly returned null."; + + /// + public override void Run() + { + // Arrange + try + { + File.WriteAllText(this.testFileName, TestFileContent); + } + catch (Exception ex) + { + Assert.Fail($"Test setup failed: Could not create test file '{this.testFileName}'. {ex.Message}"); + } + + try + { + // Act + var info = Night.Filesystem.GetInfo(this.testFileName, FileType.Directory); + + // Assert + Assert.Null(info); + } + finally + { + // Cleanup + if (File.Exists(this.testFileName)) + { + try + { + File.Delete(this.testFileName); + } + catch (Exception ex) + { + // Logging the warning to output, as ModTestCase doesn't have a Details property to append to in the same way + // and this.Details is not typically used for cleanup warnings in ModTestCase. + // Consider using ITestOutputHelper if more detailed logging is needed here. + Debug.WriteLine($"Warning: Failed to delete test file '{this.testFileName}': {ex.Message}"); + } + } + } + } + } + + /// + /// Tests that returns correct info for a directory with no filter. + /// + public class FilesystemGetInfo_DirectoryExists_NoFilter_ReturnsDirectoryInfoTest : ModTestCase + { + private readonly string testDirName = Path.Combine(Path.GetTempPath(), $"night_test_dir_exists_mod_{Guid.NewGuid()}"); + + /// + public override string Name => "Filesystem.GetInfo.DirectoryExistsNoFilterMod"; + + /// + public override string Description => "Tests GetInfo for an existing directory with no filter, expecting correct FileSystemInfo (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo for an existing directory with no filter returned correct FileSystemInfo."; + + /// + public override void Run() + { + // Arrange + long expectedModTime = 0; + try + { + _ = Directory.CreateDirectory(this.testDirName); + var dirInfo = new DirectoryInfo(this.testDirName); + + // Ensure LastWriteTimeUtc is fresh for comparison + dirInfo.LastWriteTimeUtc = DateTime.UtcNow; + expectedModTime = new DateTimeOffset(dirInfo.LastWriteTimeUtc).ToUnixTimeSeconds(); + } + catch (Exception ex) + { + Assert.Fail($"Test setup failed: Could not create test directory '{this.testDirName}'. {ex.Message}"); + } + + try + { + // Act + var info = Night.Filesystem.GetInfo(this.testDirName); + + // Assert + Assert.NotNull(info); + Assert.Equal(FileType.Directory, info.Type); + Assert.Null(info.Size); // Size should be null for directories + _ = Assert.NotNull(info.ModTime); + Assert.True(Math.Abs(info.ModTime.Value - expectedModTime) <= 2, $"Expected ModTime around {expectedModTime}, but got {info.ModTime}. Difference: {Math.Abs((info.ModTime ?? 0) - expectedModTime)}"); + } + finally + { + // Cleanup + if (Directory.Exists(this.testDirName)) + { + Directory.Delete(this.testDirName, true); + } + } + } + } + + /// + /// Tests that returns correct info for a directory that matches the filter. + /// + public class FilesystemGetInfo_DirectoryExists_MatchingFilter_ReturnsDirectoryInfoTest : ModTestCase + { + private readonly string testDirName = Path.Combine(Path.GetTempPath(), $"night_test_dir_match_filter_mod_{Guid.NewGuid()}"); + + /// + public override string Name => "Filesystem.GetInfo.DirectoryExistsMatchingFilterMod"; + + /// + public override string Description => "Tests GetInfo for an existing directory with FileType.Directory filter, expecting correct FileSystemInfo (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo for an existing directory with FileType.Directory filter returned correct FileSystemInfo."; + + /// + public override void Run() + { + // Arrange + try + { + _ = Directory.CreateDirectory(this.testDirName); + + // Ensure LastWriteTimeUtc is fresh for comparison + new DirectoryInfo(this.testDirName).LastWriteTimeUtc = DateTime.UtcNow; + } + catch (Exception ex) + { + Assert.Fail($"Test setup failed: Could not create test directory '{this.testDirName}'. {ex.Message}"); + } + + try + { + // Act + var info = Night.Filesystem.GetInfo(this.testDirName, FileType.Directory); + + // Assert + Assert.NotNull(info); + Assert.Equal(FileType.Directory, info.Type); + } + finally + { + // Cleanup + if (Directory.Exists(this.testDirName)) + { + Directory.Delete(this.testDirName, true); + } + } + } + } + + /// + /// Tests that returns null for a directory that does not match the filter. + /// + public class FilesystemGetInfo_DirectoryExists_NonMatchingFilter_ReturnsNullTest : ModTestCase + { + private readonly string testDirName = Path.Combine(Path.GetTempPath(), $"night_test_dir_nonmatch_filter_mod_{Guid.NewGuid()}"); + + /// + public override string Name => "Filesystem.GetInfo.DirectoryExistsNonMatchingFilterMod"; + + /// + public override string Description => "Tests GetInfo for an existing directory with FileType.File filter, expecting null (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo for an existing directory with FileType.File filter correctly returned null."; + + /// + public override void Run() + { + // Arrange + try + { + _ = Directory.CreateDirectory(this.testDirName); + } + catch (Exception ex) + { + Assert.Fail($"Test setup failed: Could not create test directory '{this.testDirName}'. {ex.Message}"); + } + + try + { + // Act + var info = Night.Filesystem.GetInfo(this.testDirName, FileType.File); + + // Assert + Assert.Null(info); + } + finally + { + // Cleanup + if (Directory.Exists(this.testDirName)) + { + Directory.Delete(this.testDirName, true); + } + } + } + } + + /// + /// Tests that returns null when the info object to populate is null. + /// + public class FilesystemGetInfo_PopulateNullInfoObject_ReturnsNullTest : ModTestCase + { + private readonly string testFileName = Path.Combine(Path.GetTempPath(), $"night_test_populate_null_info_{Guid.NewGuid()}.txt"); + + /// + public override string Name => "Filesystem.GetInfo.PopulateNullInfoObjectMod"; + + /// + public override string Description => "Tests GetInfo with a null FileSystemInfo object to populate, expecting null (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo with a null FileSystemInfo object correctly returned null."; + + /// + public override void Run() + { + // Arrange + Night.FileSystemInfo? infoToPopulate = null; + try + { + File.WriteAllText(this.testFileName, "content"); // File needs to exist for GetInfo to proceed + } + catch (Exception ex) + { + Assert.Fail($"Test setup failed: Could not create test file '{this.testFileName}'. {ex.Message}"); + } + + try + { + // Act +#pragma warning disable CS8604 // Possible null reference argument. Test case specifically for null. + var resultInfo = Night.Filesystem.GetInfo(this.testFileName, infoToPopulate!); +#pragma warning restore CS8604 + + // Assert + Assert.Null(resultInfo); + } + finally + { + if (File.Exists(this.testFileName)) + { + File.Delete(this.testFileName); + } + } + } + } + + /// + /// Tests that correctly populates a valid info object for an existing file. + /// + public class FilesystemGetInfo_PopulateValidInfo_FileExists_PopulatesAndReturnsInfoTest : ModTestCase + { + private const string TestFileContent = "Populate Me!"; + private readonly string testFileName = Path.Combine(Path.GetTempPath(), $"night_test_populate_file_mod_{Guid.NewGuid()}.txt"); + + /// + public override string Name => "Filesystem.GetInfo.PopulateValidInfoFileExistsMod"; + + /// + public override string Description => "Tests GetInfo with a valid FileSystemInfo object for an existing file, expecting it to be populated (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo with a valid FileSystemInfo object for an existing file populated and returned it."; + + /// + public override void Run() + { + // Arrange + var infoToPopulate = new Night.FileSystemInfo(FileType.Other, null, null); // Initial dummy values + long expectedSize = 0; + long expectedModTime = 0; + + try + { + File.WriteAllText(this.testFileName, TestFileContent); + var fileInfo = new FileInfo(this.testFileName); + expectedSize = fileInfo.Length; + fileInfo.LastWriteTimeUtc = DateTime.UtcNow; + expectedModTime = new DateTimeOffset(fileInfo.LastWriteTimeUtc).ToUnixTimeSeconds(); + } + catch (Exception ex) + { + Assert.Fail($"Test setup failed: Could not create test file '{this.testFileName}'. {ex.Message}"); + } + + try + { + // Act + var resultInfo = Night.Filesystem.GetInfo(this.testFileName, infoToPopulate); + + // Assert + Assert.NotNull(resultInfo); + Assert.Same(infoToPopulate, resultInfo); // Should be the same instance + Assert.Equal(FileType.File, resultInfo.Type); + Assert.Equal(expectedSize, resultInfo.Size); + _ = Assert.NotNull(resultInfo.ModTime); + Assert.True(Math.Abs(resultInfo.ModTime.Value - expectedModTime) <= 2, $"Expected ModTime around {expectedModTime}, but got {resultInfo.ModTime}. Difference: {Math.Abs((resultInfo.ModTime ?? 0) - expectedModTime)}"); + } + finally + { + if (File.Exists(this.testFileName)) + { + File.Delete(this.testFileName); + } + } + } + } + + /// + /// Tests that correctly populates a valid info object for an existing directory. + /// + public class FilesystemGetInfo_PopulateValidInfo_DirectoryExists_PopulatesAndReturnsInfoTest : ModTestCase + { + private readonly string testDirName = Path.Combine(Path.GetTempPath(), $"night_test_populate_dir_mod_{Guid.NewGuid()}"); + + /// + public override string Name => "Filesystem.GetInfo.PopulateValidInfoDirectoryExistsMod"; + + /// + public override string Description => "Tests GetInfo with a valid FileSystemInfo object for an existing directory, expecting it to be populated (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo with a valid FileSystemInfo object for an existing directory populated and returned it."; + + /// + public override void Run() + { + // Arrange + var infoToPopulate = new Night.FileSystemInfo(FileType.Other, 123, 123); // Initial dummy values + long expectedModTime = 0; + + try + { + _ = Directory.CreateDirectory(this.testDirName); + var dirInfo = new DirectoryInfo(this.testDirName); + dirInfo.LastWriteTimeUtc = DateTime.UtcNow; + expectedModTime = new DateTimeOffset(dirInfo.LastWriteTimeUtc).ToUnixTimeSeconds(); + } + catch (Exception ex) + { + Assert.Fail($"Test setup failed: Could not create test directory '{this.testDirName}'. {ex.Message}"); + } + + try + { + // Act + var resultInfo = Night.Filesystem.GetInfo(this.testDirName, infoToPopulate); + + // Assert + Assert.NotNull(resultInfo); + Assert.Same(infoToPopulate, resultInfo); + Assert.Equal(FileType.Directory, resultInfo.Type); + Assert.Null(resultInfo.Size); // Size should be null for directories + _ = Assert.NotNull(resultInfo.ModTime); + Assert.True(Math.Abs(resultInfo.ModTime.Value - expectedModTime) <= 2, $"Expected ModTime around {expectedModTime}, but got {resultInfo.ModTime}. Difference: {Math.Abs((resultInfo.ModTime ?? 0) - expectedModTime)}"); + } + finally + { + if (Directory.Exists(this.testDirName)) + { + Directory.Delete(this.testDirName, true); + } + } + } + } + + /// + /// Tests that returns null when the path does not exist. + /// + public class FilesystemGetInfo_PopulateValidInfo_PathDoesNotExist_ReturnsNullTest : ModTestCase + { + private readonly string nonExistentPath = Path.Combine(Path.GetTempPath(), $"night_test_populate_non_existent_mod_{Guid.NewGuid()}"); + + /// + public override string Name => "Filesystem.GetInfo.PopulateValidInfoPathDoesNotExistMod"; + + /// + public override string Description => "Tests GetInfo with a valid FileSystemInfo object for a non-existent path, expecting null (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo with a valid FileSystemInfo object for a non-existent path correctly returned null."; + + /// + public override void Run() + { + // Arrange + var infoToPopulate = new Night.FileSystemInfo(FileType.File, 10, 100); // Initial dummy values + if (File.Exists(this.nonExistentPath)) + { + File.Delete(this.nonExistentPath); + } + + if (Directory.Exists(this.nonExistentPath)) + { + Directory.Delete(this.nonExistentPath, true); + } + + // Act + var resultInfo = Night.Filesystem.GetInfo(this.nonExistentPath, infoToPopulate); + + // Assert + Assert.Null(resultInfo); + + // Original infoToPopulate should remain unchanged by the GetInfo call if path doesn't exist + Assert.Equal(FileType.File, infoToPopulate.Type); + Assert.Equal(10, infoToPopulate.Size); + Assert.Equal(100, infoToPopulate.ModTime); + } + } + + /// + /// Tests that returns null when the info object is null. + /// + public class FilesystemGetInfo_PopulateWithFilter_NullInfoObject_ReturnsNullTest : ModTestCase + { + private readonly string testFileName = Path.Combine(Path.GetTempPath(), $"night_test_populate_filter_null_info_{Guid.NewGuid()}.txt"); + + /// + public override string Name => "Filesystem.GetInfo.PopulateWithFilterNullInfoObjectMod"; + + /// + public override string Description => "Tests GetInfo (filter overload) with a null FileSystemInfo object, expecting null (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo (filter overload) with a null FileSystemInfo object correctly returned null."; + + /// + public override void Run() + { + // Arrange + Night.FileSystemInfo? infoToPopulate = null; + try + { + File.WriteAllText(this.testFileName, "content"); + } + catch (Exception ex) + { + Assert.Fail($"Test setup failed: Could not create test file '{this.testFileName}'. {ex.Message}"); + } + + try + { + // Act +#pragma warning disable CS8604 // Possible null reference argument. + var resultInfo = Night.Filesystem.GetInfo(this.testFileName, FileType.File, infoToPopulate); +#pragma warning restore CS8604 + + // Assert + Assert.Null(resultInfo); + } + finally + { + if (File.Exists(this.testFileName)) + { + File.Delete(this.testFileName); + } + } + } + } + + /// + /// Tests that populates info for a file with a matching filter. + /// + public class FilesystemGetInfo_PopulateWithFilter_FileExists_MatchingFilter_PopulatesInfoTest : ModTestCase + { + private readonly string testFileName = Path.Combine(Path.GetTempPath(), $"night_test_populate_file_match_filter_mod_{Guid.NewGuid()}.txt"); + + /// + public override string Name => "Filesystem.GetInfo.PopulateWithFilterFileMatchingMod"; + + /// + public override string Description => "Tests GetInfo (filter overload) for a file with matching filter, expecting populated info (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo (filter overload) for a file with matching filter populated info."; + + /// + public override void Run() + { + // Arrange + var infoToPopulate = new Night.FileSystemInfo(FileType.Other, 0, 0); + long expectedSize = 0; + long expectedModTime = 0; + try + { + File.WriteAllText(this.testFileName, "content"); + var fileInfo = new FileInfo(this.testFileName); + expectedSize = fileInfo.Length; + fileInfo.LastWriteTimeUtc = DateTime.UtcNow; + expectedModTime = new DateTimeOffset(fileInfo.LastWriteTimeUtc).ToUnixTimeSeconds(); + } + catch (Exception ex) + { + Assert.Fail($"Test setup failed: Could not create test file '{this.testFileName}'. {ex.Message}"); + } + + try + { + // Act + var resultInfo = Night.Filesystem.GetInfo(this.testFileName, FileType.File, infoToPopulate); + + // Assert + Assert.NotNull(resultInfo); + Assert.Same(infoToPopulate, resultInfo); + Assert.Equal(FileType.File, resultInfo.Type); + Assert.Equal(expectedSize, resultInfo.Size); + _ = Assert.NotNull(resultInfo.ModTime); + Assert.True(Math.Abs(resultInfo.ModTime.Value - expectedModTime) <= 2); + } + finally + { + if (File.Exists(this.testFileName)) + { + File.Delete(this.testFileName); + } + } + } + } + + /// + /// Tests that returns null for a file with a non-matching filter. + /// + public class FilesystemGetInfo_PopulateWithFilter_FileExists_NonMatchingFilter_ReturnsNullTest : ModTestCase + { + private readonly string testFileName = Path.Combine(Path.GetTempPath(), $"night_test_populate_file_nonmatch_filter_mod_{Guid.NewGuid()}.txt"); + + /// + public override string Name => "Filesystem.GetInfo.PopulateWithFilterFileNonMatchingMod"; + + /// + public override string Description => "Tests GetInfo (filter overload) for a file with non-matching filter, expecting null (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo (filter overload) for a file with non-matching filter returned null."; + + /// + public override void Run() + { + // Arrange + var infoToPopulate = new Night.FileSystemInfo(FileType.Other, 0, 0); + try + { + File.WriteAllText(this.testFileName, "content"); + } + catch (Exception ex) + { + Assert.Fail($"Test setup failed: Could not create test file '{this.testFileName}'. {ex.Message}"); + } + + try + { + // Act + var resultInfo = Night.Filesystem.GetInfo(this.testFileName, FileType.Directory, infoToPopulate); + + // Assert + Assert.Null(resultInfo); + + // Original infoToPopulate should remain unchanged + Assert.Equal(FileType.Other, infoToPopulate.Type); + } + finally + { + if (File.Exists(this.testFileName)) + { + File.Delete(this.testFileName); + } + } + } + } + + /// + /// Tests that populates info for a directory with a matching filter. + /// + public class FilesystemGetInfo_PopulateWithFilter_DirectoryExists_MatchingFilter_PopulatesInfoTest : ModTestCase + { + private readonly string testDirName = Path.Combine(Path.GetTempPath(), $"night_test_populate_dir_match_filter_mod_{Guid.NewGuid()}"); + + /// + public override string Name => "Filesystem.GetInfo.PopulateWithFilterDirectoryMatchingMod"; + + /// + public override string Description => "Tests GetInfo (filter overload) for a directory with matching filter, expecting populated info (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo (filter overload) for a directory with matching filter populated info."; + + /// + public override void Run() + { + // Arrange + var infoToPopulate = new Night.FileSystemInfo(FileType.Other, 0, 0); + long expectedModTime = 0; + try + { + _ = Directory.CreateDirectory(this.testDirName); + var dirInfo = new DirectoryInfo(this.testDirName); + dirInfo.LastWriteTimeUtc = DateTime.UtcNow; + expectedModTime = new DateTimeOffset(dirInfo.LastWriteTimeUtc).ToUnixTimeSeconds(); + } + catch (Exception ex) + { + Assert.Fail($"Test setup failed: Could not create test directory '{this.testDirName}'. {ex.Message}"); + } + + try + { + // Act + var resultInfo = Night.Filesystem.GetInfo(this.testDirName, FileType.Directory, infoToPopulate); + + // Assert + Assert.NotNull(resultInfo); + Assert.Same(infoToPopulate, resultInfo); + Assert.Equal(FileType.Directory, resultInfo.Type); + Assert.Null(resultInfo.Size); + _ = Assert.NotNull(resultInfo.ModTime); + Assert.True(Math.Abs(resultInfo.ModTime.Value - expectedModTime) <= 2); + } + finally + { + if (Directory.Exists(this.testDirName)) + { + Directory.Delete(this.testDirName, true); + } + } + } + } + + /// + /// Tests that returns null for a directory with a non-matching filter. + /// + public class FilesystemGetInfo_PopulateWithFilter_DirectoryExists_NonMatchingFilter_ReturnsNullTest : ModTestCase + { + private readonly string testDirName = Path.Combine(Path.GetTempPath(), $"night_test_populate_dir_nonmatch_filter_mod_{Guid.NewGuid()}"); + + /// + public override string Name => "Filesystem.GetInfo.PopulateWithFilterDirectoryNonMatchingMod"; + + /// + public override string Description => "Tests GetInfo (filter overload) for a directory with non-matching filter, expecting null (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo (filter overload) for a directory with non-matching filter returned null."; + + /// + public override void Run() + { + // Arrange + var infoToPopulate = new Night.FileSystemInfo(FileType.Other, 0, 0); + try + { + _ = Directory.CreateDirectory(this.testDirName); + } + catch (Exception ex) + { + Assert.Fail($"Test setup failed: Could not create test directory '{this.testDirName}'. {ex.Message}"); + } + + try + { + // Act + var resultInfo = Night.Filesystem.GetInfo(this.testDirName, FileType.File, infoToPopulate); + + // Assert + Assert.Null(resultInfo); + Assert.Equal(FileType.Other, infoToPopulate.Type); + } + finally + { + if (Directory.Exists(this.testDirName)) + { + try + { + Directory.Delete(this.testDirName); + } + catch (Exception ex) + { + this.Details += $" | Warning: Failed to delete test directory '{this.testDirName}': {ex.Message}"; + } + } + } + } + } + + // TODO: Add tests for other GetInfo overloads: + // - GetInfo(string path, FileSystemInfo info) + // - GetInfo(string path, FileType filterType, FileSystemInfo info) + // - GetInfo_EmptyPath_ReturnsNull + // - GetInfo_WithFilterTypeDirectory_Matches + // - GetInfo_WithFilterTypeDirectory_Mismatches +} + +/// +/// Tests that returns null for a non-existent path. +/// +public class FilesystemGetInfo_PopulateWithFilter_PathDoesNotExist_ReturnsNullTest : ModTestCase +{ + private readonly string nonExistentPath = Path.Combine(Path.GetTempPath(), $"night_test_populate_filter_non_existent_mod_{Guid.NewGuid()}"); + + /// + public override string Name => "Filesystem.GetInfo.PopulateWithFilterPathDoesNotExistMod"; + + /// + public override string Description => "Tests GetInfo (filter overload) for a non-existent path, expecting null (Mod Test)."; + + /// + public override string SuccessMessage => "GetInfo (filter overload) for a non-existent path correctly returned null."; + + /// + public override void Run() + { + // Arrange + var infoToPopulate = new Night.FileSystemInfo(FileType.File, 10, 100); + if (File.Exists(this.nonExistentPath)) + { + File.Delete(this.nonExistentPath); + } + + if (Directory.Exists(this.nonExistentPath)) + { + Directory.Delete(this.nonExistentPath, true); + } + + // Act + var resultInfo = Night.Filesystem.GetInfo(this.nonExistentPath, FileType.File, infoToPopulate); + + // Assert + Assert.Null(resultInfo); + Assert.Equal(FileType.File, infoToPopulate.Type); + Assert.Equal(10, infoToPopulate.Size); + Assert.Equal(100, infoToPopulate.ModTime); + } +} + +/// +/// Tests GetInfo for an existing file symbolic link. +/// +public class FilesystemGetInfo_SymbolicLinkTest : ModTestCase +{ + private string testTargetFileName = string.Empty; + private string testSymlinkFileName = string.Empty; + + /// + public override string Name => "Filesystem.GetInfo.SymbolicLink"; + + /// + public override string Description => "Tests GetInfo for an existing file symbolic link, checking Type, Size, and ModTime of the link itself across platforms."; + + /// + public override string SuccessMessage => "Successfully retrieved correct info for an existing file symbolic link."; + + /// + public override void Run() + { + this.GenerateUniqueFileNames(); + long expectedSymlinkModTime; + long expectedSymlinkSize; + + try + { + // Create target file + File.WriteAllText(this.testTargetFileName, "Hello from symlink target!"); + System.Threading.Thread.Sleep(100); // Allow time for filesystem operations to settle + + // Create symlink (platform-specific) + ProcessStartInfo processStartInfo; + string commandArguments; + + if (OperatingSystem.IsWindows()) + { + processStartInfo = new ProcessStartInfo("cmd.exe"); + commandArguments = $"/c mklink \"{this.testSymlinkFileName}\" \"{this.testTargetFileName}\""; + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + processStartInfo = new ProcessStartInfo("/bin/sh"); // Or /bin/bash + + // Ensure paths are quoted for ln -s + commandArguments = $"-c \"ln -s '{this.testTargetFileName.Replace("'", "'\\''")}' '{this.testSymlinkFileName.Replace("'", "'\\''")}'\""; + } + else + { + Assert.Fail($"Symbolic link creation test is not supported on this OS: {RuntimeInformation.OSDescription}"); + return; // Should not be reached if Assert.True fails + } + + processStartInfo.Arguments = commandArguments; + processStartInfo.RedirectStandardOutput = true; + processStartInfo.RedirectStandardError = true; + processStartInfo.UseShellExecute = false; + processStartInfo.CreateNoWindow = true; + + using (var process = Process.Start(processStartInfo)) + { + if (process == null) + { + throw new Xunit.Sdk.XunitException($"Failed to start process for symbolic link creation ({processStartInfo.FileName})."); + } + + string processOutput = process.StandardOutput.ReadToEnd(); + string processError = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + if (process.ExitCode != 0) + { + throw new Xunit.Sdk.XunitException($"Symbolic link creation failed with exit code {process.ExitCode}. OS: {RuntimeInformation.OSDescription}. Command: {processStartInfo.FileName} {processStartInfo.Arguments}. Output: {processOutput}. Error: {processError}. This may be due to insufficient permissions."); + } + } + + Assert.True(File.Exists(this.testSymlinkFileName), $"Symbolic link was reported as created, but does not exist at the expected path: {this.testSymlinkFileName}"); + + var symlinkFileInfo = new FileInfo(this.testSymlinkFileName); + + if (OperatingSystem.IsWindows()) + { + Assert.True((symlinkFileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint, "Created file on Windows is not a reparse point (symlink) as expected."); + expectedSymlinkSize = 0L; + } + else + { + // FileInfo.Length for a symlink is the length of the target path string. + expectedSymlinkSize = symlinkFileInfo.Length; + + // A more direct check for symlink on Unix via File.ResolveLinkTarget() + Assert.NotNull(System.IO.File.ResolveLinkTarget(this.testSymlinkFileName, false)); + } + + System.Threading.Thread.Sleep(200); + + // On some OSes (like macOS), File.SetLastWriteTimeUtc on a symlink changes the target's time. + // To get a reliable mod time for the link itself, we might need to rely on its creation time + // or accept that it might reflect the target's time if recently created/modified. + // For simplicity, we'll get the current time of the link. + // If more precise control is needed, OS-specific 'touch' commands for symlinks would be required. + symlinkFileInfo.LastWriteTimeUtc = DateTime.UtcNow; // Attempt to update link's own mod time + expectedSymlinkModTime = new DateTimeOffset(new FileInfo(this.testSymlinkFileName).LastWriteTimeUtc).ToUnixTimeSeconds(); + + var info = Night.Filesystem.GetInfo(this.testSymlinkFileName); + + Assert.NotNull(info); + Assert.Equal(FileType.Symlink, info.Type); + Assert.Equal(expectedSymlinkSize, info.Size); + _ = Assert.NotNull(info.ModTime); + + // Increased tolerance for ModTime due to potential OS/FS differences in handling symlink mod times. + Assert.True(Math.Abs(info.ModTime.Value - expectedSymlinkModTime) <= 5, $"Expected ModTime for symlink around {expectedSymlinkModTime}, but got {info.ModTime}. Difference: {Math.Abs(info.ModTime.Value - expectedSymlinkModTime)}"); + } + finally + { + this.CleanUpFiles(); + } + } + + private void GenerateUniqueFileNames() + { + string guid = Guid.NewGuid().ToString("N"); + this.testTargetFileName = Path.Combine(Path.GetTempPath(), $"night_test_getinfo_symlink_target_{guid}.txt"); + this.testSymlinkFileName = Path.Combine(Path.GetTempPath(), $"night_test_getinfo_symlink_{guid}.txt"); + } + + private void CleanUpFiles() + { + // Best effort cleanup + try + { + if (!string.IsNullOrEmpty(this.testSymlinkFileName) && File.Exists(this.testSymlinkFileName)) + { + // On Unix-like systems, File.Delete works for symlinks. + // On Windows, File.Delete on a symlink deletes the symlink, not the target. + File.Delete(this.testSymlinkFileName); + } + } + catch (Exception ex) + { + Console.WriteLine($"Warning: Failed to delete symlink file '{this.testSymlinkFileName}': {ex.Message}"); + } + + try + { + if (!string.IsNullOrEmpty(this.testTargetFileName) && File.Exists(this.testTargetFileName)) + { + File.Delete(this.testTargetFileName); + } + } + catch (Exception ex) + { + Console.WriteLine($"Warning: Failed to delete target file '{this.testTargetFileName}': {ex.Message}"); + } + } +} diff --git a/tests/Groups/Filesystem/GetSaveDirectoryTests.cs b/tests/Groups/Filesystem/GetSaveDirectoryTests.cs new file mode 100644 index 00000000..3a44e911 --- /dev/null +++ b/tests/Groups/Filesystem/GetSaveDirectoryTests.cs @@ -0,0 +1,124 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; +using System.Runtime.InteropServices; + +using Night; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Tests for Night.Filesystem.GetSaveDirectory(). + /// + public class GetSaveDirectory_DefaultIdentityTest : ModTestCase + { + /// + public override string Name => "Filesystem.GetSaveDirectory.DefaultIdentity"; + + /// + public override string Description => "Tests GetSaveDirectory with the default identity and ensures directory creation."; + + /// + public override string SuccessMessage => "GetSaveDirectory with default identity returned a valid, existing path."; + + /// + public override void Run() + { + Night.Filesystem.SetIdentity(null); // Ensure default identity + var saveDir = Night.Filesystem.GetSaveDirectory(); + + Assert.False(string.IsNullOrWhiteSpace(saveDir), "Save directory path should not be null or whitespace."); + Assert.True(Directory.Exists(saveDir), "Save directory should be created by the method."); + + var expectedIdentity = "NightDefault"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var expectedPath = Path.Combine(appData, "Night", expectedIdentity); + Assert.Equal(expectedPath, saveDir); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var appSupport = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support"); + var expectedPath = Path.Combine(appSupport, "Night", expectedIdentity); + Assert.Equal(expectedPath, saveDir); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var expectedBase = Environment.GetEnvironmentVariable("XDG_DATA_HOME") ?? Path.Combine(home, ".local", "share"); + var expectedPath = Path.Combine(expectedBase, "night", expectedIdentity); + Assert.Equal(expectedPath, saveDir); + } + } + } + + /// + /// Tests GetSaveDirectory with a custom identity. + /// + public class GetSaveDirectory_CustomIdentityTest : ModTestCase + { + private const string CustomIdentity = "MyCustomGame"; + + /// + public override string Name => "Filesystem.GetSaveDirectory.CustomIdentity"; + + /// + public override string Description => "Tests GetSaveDirectory with a custom identity."; + + /// + public override string SuccessMessage => "GetSaveDirectory with custom identity returned the correct path."; + + /// + public override void Run() + { + Night.Filesystem.SetIdentity(CustomIdentity); + var saveDir = Night.Filesystem.GetSaveDirectory(); + + try + { + Assert.True(Directory.Exists(saveDir), "Save directory with custom identity should be created."); + Assert.EndsWith(CustomIdentity, saveDir); + } + finally + { + // Cleanup: remove the custom directory + if (Directory.Exists(saveDir)) + { + var parentDir = Directory.GetParent(saveDir); + if (parentDir != null && (parentDir.Name == "Night" || parentDir.Name == "night")) + { + Directory.Delete(saveDir, true); + } + } + + Night.Filesystem.SetIdentity(null); // Reset to default + } + } + } +} diff --git a/tests/Groups/Filesystem/LinesArgumentExceptionTests.cs b/tests/Groups/Filesystem/LinesArgumentExceptionTests.cs new file mode 100644 index 00000000..cf03cf36 --- /dev/null +++ b/tests/Groups/Filesystem/LinesArgumentExceptionTests.cs @@ -0,0 +1,75 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Tests that Filesystem.Lines(filePath) throws ArgumentNullException when filePath is null. + /// + public class FilesystemLines_ThrowsArgumentNullExceptionOnNullPathTest : ModTestCase + { + /// + public override string Name => "Filesystem.Lines.ThrowsArgumentNullExceptionOnNullPath"; + + /// + public override string Description => "Tests that Filesystem.Lines(filePath) throws ArgumentNullException when filePath is null."; + + /// + public override string SuccessMessage => "Filesystem.Lines(null) correctly threw ArgumentNullException."; + + /// + public override void Run() + { + var exception = Assert.Throws(() => Night.Filesystem.Lines(null!)); + Assert.Equal("filePath", exception.ParamName); + } + } + + /// + /// Tests that Filesystem.Lines(filePath) throws ArgumentException when filePath is empty. + /// + public class FilesystemLines_ThrowsArgumentExceptionOnEmptyPathTest : ModTestCase + { + /// + public override string Name => "Filesystem.Lines.ThrowsArgumentExceptionOnEmptyPath"; + + /// + public override string Description => "Tests that Filesystem.Lines(filePath) throws ArgumentException when filePath is empty."; + + /// + public override string SuccessMessage => "Filesystem.Lines(\"\") correctly threw ArgumentException."; + + /// + public override void Run() + { + var exception = Assert.Throws(() => Night.Filesystem.Lines(string.Empty)); + Assert.Equal("filePath", exception.ParamName); + Assert.Contains("File path cannot be empty.", exception.Message); + } + } +} diff --git a/tests/Groups/Filesystem/LinesTests.cs b/tests/Groups/Filesystem/LinesTests.cs new file mode 100644 index 00000000..e28ff0d9 --- /dev/null +++ b/tests/Groups/Filesystem/LinesTests.cs @@ -0,0 +1,169 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using Night; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Tests reading lines from a standard text file with multiple lines. + /// + public class FilesystemLines_ReadStandardFileTest : ModTestCase + { + private const string TestFileName = "test_standard.txt"; + private readonly List expectedLines = new() + { + "Line 1", + "Line 2 with some different content.", + "A third line.", + }; + + /// + public override string Name => "Filesystem.Lines.ReadStandardFile"; + + /// + public override string Description => "Tests reading lines from a standard text file with multiple lines."; + + /// + public override string SuccessMessage => "Successfully read and verified all lines from the standard file."; + + /// + public override void Run() + { + try + { + File.WriteAllLines(TestFileName, this.expectedLines); + var linesRead = Night.Filesystem.Lines(TestFileName).ToList(); + Assert.Equal(this.expectedLines, linesRead); + } + finally + { + if (File.Exists(TestFileName)) + { + File.Delete(TestFileName); + } + } + } + } + + /// + /// Tests reading lines from an empty text file. + /// + public class FilesystemLines_ReadEmptyFileTest : ModTestCase + { + private const string TestFileName = "test_empty.txt"; + + /// + public override string Name => "Filesystem.Lines.ReadEmptyFile"; + + /// + public override string Description => "Tests reading lines from an empty text file."; + + /// + public override string SuccessMessage => "Successfully read an empty file, resulting in an empty enumerable."; + + /// + public override void Run() + { + try + { + File.WriteAllText(TestFileName, string.Empty); + var linesRead = Night.Filesystem.Lines(TestFileName).ToList(); + Assert.Empty(linesRead); + } + finally + { + if (File.Exists(TestFileName)) + { + File.Delete(TestFileName); + } + } + } + } + + /// + /// Tests that Night.Filesystem.Lines throws FileNotFoundException for a non-existent file. + /// + public class FilesystemLines_FileNotFoundTest : ModTestCase + { + /// + public override string Name => "Filesystem.Lines.FileNotFound"; + + /// + public override string Description => "Tests that Night.Filesystem.Lines throws FileNotFoundException for a non-existent file."; + + /// + public override string SuccessMessage => "Successfully caught FileNotFoundException for a non-existent file."; + + /// + public override void Run() + { + // Attempt to iterate. ToList() forces enumeration. + _ = Assert.Throws(() => Night.Filesystem.Lines("this_file_should_definitely_not_exist.txt").ToList()); + } + } + + /// + /// Tests reading lines from a text file containing only a single line. + /// + public class FilesystemLines_ReadSingleLineFileTest : ModTestCase + { + private const string TestFileName = "test_single_line.txt"; + private const string ExpectedLineContent = "This is the only line."; + + /// + public override string Name => "Filesystem.Lines.ReadSingleLineFile"; + + /// + public override string Description => "Tests reading lines from a text file containing only a single line."; + + /// + public override string SuccessMessage => "Successfully read and verified the single line from the file."; + + /// + public override void Run() + { + try + { + File.WriteAllText(TestFileName, ExpectedLineContent); + var linesRead = Night.Filesystem.Lines(TestFileName).ToList(); + _ = Assert.Single(linesRead); + Assert.Equal(ExpectedLineContent, linesRead[0]); + } + finally + { + if (File.Exists(TestFileName)) + { + File.Delete(TestFileName); + } + } + } + } +} diff --git a/tests/Groups/Filesystem/NewFileDataTests.cs b/tests/Groups/Filesystem/NewFileDataTests.cs new file mode 100644 index 00000000..6da7700f --- /dev/null +++ b/tests/Groups/Filesystem/NewFileDataTests.cs @@ -0,0 +1,128 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System.Linq; +using System.Text; + +using Night; + +using NightTest.Core; + +namespace NightTest.Groups.Filesystem; + +/// +/// Tests creating FileData from a byte array. +/// +public class NewFileDataFromBytesTest : GameTestCase +{ + /// + public override string Name => "FileData.CreateFromBytes"; + + /// + public override string Description => "Tests creating FileData from a byte array."; + + /// + protected override void Update(double deltaTime) + { + var content = "Hello, Bytes!"; + var bytes = Encoding.UTF8.GetBytes(content); + var fileData = Night.Filesystem.NewFileData(bytes, "test.bin"); + + if (fileData.GetString() != content) + { + this.RecordFailure($"GetString() returned '{fileData.GetString()}' instead of '{content}'."); + return; + } + + if (fileData.GetSize() != bytes.Length) + { + this.RecordFailure($"GetSize() returned {fileData.GetSize()} instead of {bytes.Length}."); + return; + } + + if (fileData.GetFilenameHint() != "test.bin") + { + this.RecordFailure($"GetFilenameHint() returned '{fileData.GetFilenameHint()}' instead of 'test.bin'."); + return; + } + + if (fileData.GetExtension() != ".bin") + { + this.RecordFailure($"GetExtension() returned '{fileData.GetExtension()}' instead of '.bin'."); + return; + } + + this.RecordSuccess("Successfully created FileData from bytes and verified all properties."); + } +} + +/// +/// Tests creating FileData from a string. +/// +public class NewFileDataFromStringTest : GameTestCase +{ + /// + public override string Name => "FileData.CreateFromString"; + + /// + public override string Description => "Tests creating FileData from a string."; + + /// + protected override void Update(double deltaTime) + { + var content = "Hello, String!"; + var fileData = Night.Filesystem.NewFileData(content, "test.txt"); + var bytes = Encoding.UTF8.GetBytes(content); + + if (fileData.GetString() != content) + { + this.RecordFailure($"GetString() returned '{fileData.GetString()}' instead of '{content}'."); + return; + } + + if (fileData.GetSize() != bytes.Length) + { + this.RecordFailure($"GetSize() returned {fileData.GetSize()} instead of {bytes.Length}."); + return; + } + + if (Enumerable.SequenceEqual(fileData.GetBytes(), bytes) == false) + { + this.RecordFailure($"GetBytes() did not return the expected byte array."); + return; + } + + if (fileData.GetFilenameHint() != "test.txt") + { + this.RecordFailure($"GetFilenameHint() returned '{fileData.GetFilenameHint()}' instead of 'test.txt'."); + return; + } + + if (fileData.GetExtension() != ".txt") + { + this.RecordFailure($"GetExtension() returned '{fileData.GetExtension()}' instead of '.txt'."); + return; + } + + this.RecordSuccess("Successfully created FileData from a string and verified all properties."); + } +} diff --git a/tests/Groups/Filesystem/NightFileTests.cs b/tests/Groups/Filesystem/NightFileTests.cs new file mode 100644 index 00000000..80cfecf9 --- /dev/null +++ b/tests/Groups/Filesystem/NightFileTests.cs @@ -0,0 +1,511 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; +using System.Text; + +using Night; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Tests the constructor of the class. + /// + public class NightFile_Constructor : ModTestCase + { + /// + public override string Name => "Filesystem.NightFile.Constructor"; + + /// + public override string Description => "Tests constructor validation and initial state."; + + /// + public override string SuccessMessage => "Constructor validated successfully."; + + /// + public override void Run() + { + // Arrange, Act & Assert + _ = Assert.Throws(() => new NightFile(null!)); + _ = Assert.Throws(() => new NightFile(string.Empty)); + + var filename = "test.txt"; + var file = new NightFile(filename); + + Assert.Equal(filename, file.Filename); + Assert.False(file.IsOpen); + } + } + + /// + /// Tests basic open and close functionality of the class. + /// + public class NightFile_OpenClose : ModTestCase + { + /// + public override string Name => "Filesystem.NightFile.OpenClose"; + + /// + public override string Description => "Tests basic open and close functionality."; + + /// + public override string SuccessMessage => "Open and Close functionality works as expected."; + + /// + public override void Run() + { + var tempFile = Path.GetTempFileName(); + try + { + var file = new NightFile(tempFile); + Assert.False(file.IsOpen, "File should not be open initially."); + + // Test Open(Night.FileMode) + var (openSuccess, openError) = file.Open(Night.FileMode.Read); + Assert.True(openSuccess, $"Open should succeed: {openError}"); + Assert.True(file.IsOpen, "File should be open after Open()."); + + var (closeSuccess, closeError) = file.Close(); + Assert.True(closeSuccess, $"Close should succeed: {closeError}"); + Assert.False(file.IsOpen, "File should be closed after Close()."); + + // Test Open(string) + (openSuccess, openError) = file.Open("r"); + Assert.True(openSuccess, $"Open with 'r' should succeed: {openError}"); + Assert.True(file.IsOpen, "File should be open after Open('r')."); + + (closeSuccess, closeError) = file.Close(); + Assert.True(closeSuccess, $"Close should succeed again: {closeError}"); + Assert.False(file.IsOpen, "File should be closed again."); + + // Test closing an already closed file + (closeSuccess, closeError) = file.Close(); + Assert.True(closeSuccess, $"Closing an already closed file should succeed: {closeError}"); + } + finally + { + File.Delete(tempFile); + } + } + } + + /// + /// Tests opening files in various modes (r, w, a) with the class. + /// + public class NightFile_OpenModes : ModTestCase + { + /// + public override string Name => "Filesystem.NightFile.OpenModes"; + + /// + public override string Description => "Tests opening files in various modes (r, w, a)."; + + /// + public override string SuccessMessage => "File open modes (read, write, append) behave correctly."; + + /// + public override void Run() + { + var nonexistentFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var existingFile = Path.GetTempFileName(); + var fileContent = "Hello, Night!"; + File.WriteAllText(existingFile, fileContent); + + try + { + // Read Mode + using (var file = new NightFile(nonexistentFile)) + { + var (success, _) = file.Open(Night.FileMode.Read); + Assert.False(success, "Opening a nonexistent file for read should fail."); + } + + using (var file = new NightFile(existingFile)) + { + var (success, error) = file.Open(Night.FileMode.Read); + Assert.True(success, $"Opening an existing file for read should succeed: {error}"); + Assert.True(file.IsOpen); + _ = file.Close(); + } + + // Write Mode + using (var file = new NightFile(nonexistentFile)) + { + var (success, error) = file.Open(Night.FileMode.Write); + Assert.True(success, $"Opening a nonexistent file for write should succeed: {error}"); + _ = file.Close(); // Close before Assert.True(File.Exists) to ensure write flush + Assert.True(File.Exists(nonexistentFile), "File should be created in write mode."); + } + + // File.Delete for nonexistentFile in write mode is now done in finally block, + // but it's created and closed within the using block. + using (var file = new NightFile(existingFile)) + { + var (success, error) = file.Open(Night.FileMode.Write); + Assert.True(success, $"Opening an existing file for write should succeed: {error}"); + _ = file.Close(); // Close before checking length + Assert.Equal(0L, new FileInfo(existingFile).Length); + } + + // Append Mode + using (var file = new NightFile(nonexistentFile)) + { + var (success, error) = file.Open(Night.FileMode.Append); + Assert.True(success, $"Opening a nonexistent file for append should succeed: {error}"); + _ = file.Close(); // Close before Assert.True(File.Exists) + Assert.True(File.Exists(nonexistentFile), "File should be created in append mode."); + } + + // File.Delete for nonexistentFile in append mode is now done in finally block. + using (var file = new NightFile(existingFile)) + { + File.WriteAllText(existingFile, fileContent); // Restore content + var (success, error) = file.Open(Night.FileMode.Append); + Assert.True(success, $"Opening an existing file for append should succeed: {error}"); + _ = file.Close(); // Close before checking length + Assert.Equal((long)fileContent.Length, new FileInfo(existingFile).Length); + } + } + finally + { + if (File.Exists(nonexistentFile)) + { + File.Delete(nonexistentFile); + } + + if (File.Exists(existingFile)) + { + File.Delete(existingFile); + } + } + } + } + + /// + /// Tests invalid open scenarios for the class. + /// + public class NightFile_OpenInvalidCases : ModTestCase + { + /// + public override string Name => "Filesystem.NightFile.Open.InvalidCases"; + + /// + public override string Description => "Tests invalid open scenarios like opening an already open or disposed file."; + + /// + public override string SuccessMessage => "Invalid open scenarios handled correctly."; + + /// + public override void Run() + { + var tempFile = Path.GetTempFileName(); + try + { + // Already open + using (var file = new NightFile(tempFile)) + { + _ = file.Open("r"); + var (success, _) = file.Open("r"); + Assert.False(success, "Opening an already open file should fail."); + _ = file.Close(); + } + + // Disposed + using (var file = new NightFile(tempFile)) + { + file.Dispose(); + var (success, _) = file.Open("r"); + Assert.False(success, "Opening a disposed file should fail."); + } + + // Invalid mode string + using (var file = new NightFile(tempFile)) + { + var (success, error) = file.Open("xyz"); + Assert.False(success, "Opening with an invalid mode string should fail."); + Assert.Contains("Invalid file mode string", error!); + } + } + finally + { + File.Delete(tempFile); + } + } + } + + /// + /// Tests reading the entire content of a file as a string with the class. + /// + public class NightFile_Read_Full : ModTestCase + { + /// + public override string Name => "Filesystem.NightFile.Read.Full"; + + /// + public override string Description => "Tests reading the entire content of a file as a string."; + + /// + public override string SuccessMessage => "Reading full file content as string works correctly."; + + /// + public override void Run() + { + var tempFile = Path.GetTempFileName(); + var content = "Line 1\nLine 2"; + + // Initial setup of the file content + File.WriteAllText(tempFile, content, Encoding.UTF8); + + try + { + // Test case 1: Operations on a NightFile instance before proper read setup + using (var file = new NightFile(tempFile)) + { + // Attempt to read when not open + var (data, error) = file.Read(); + Assert.Null(data); + Assert.NotNull(error); + Assert.Contains("File is not open for reading", error); + + // Attempt to read when open for writing + _ = file.Open(Night.FileMode.Write); + (data, error) = file.Read(); + Assert.Null(data); + Assert.NotNull(error); + Assert.Contains("File is not open for reading", error); + _ = file.Close(); + + // This NightFile instance is now closed and will be disposed by the using statement. + // The file 'tempFile' was truncated by opening in Write mode. + } + + // Test case 2: Actual read test after ensuring the previous NightFile is disposed + // Re-write the original content because the previous Write mode truncated it. + // This File.WriteAllText should now succeed as the previous NightFile is disposed. + File.WriteAllText(tempFile, content, Encoding.UTF8); + + using (var file = new NightFile(tempFile)) + { + _ = file.Open(Night.FileMode.Read); + var (data, error) = file.Read(); + Assert.Null(error); + Assert.Equal(content, data); + + // Second read should return empty string as cursor is at the end + (data, error) = file.Read(); + Assert.Null(error); + Assert.Equal(string.Empty, data); + + _ = file.Close(); + + // This NightFile instance is now closed and will be disposed by the using statement. + } + } + finally + { + File.Delete(tempFile); + } + } + } + + /// + /// Tests reading all remaining bytes from a file with the class. + /// + public class NightFile_Read_Bytes : ModTestCase + { + /// + public override string Name => "Filesystem.NightFile.Read.Bytes"; + + /// + public override string Description => "Tests reading all remaining bytes from a file."; + + /// + public override string SuccessMessage => "Reading all bytes works correctly."; + + /// + public override void Run() + { + var tempFile = Path.GetTempFileName(); + var content = new byte[] { 1, 2, 3, 4, 5 }; + File.WriteAllBytes(tempFile, content); + + try + { + using (var file = new NightFile(tempFile)) + { + var (data, error) = file.ReadBytes(); + Assert.Null(data); + Assert.Equal("File is not open for reading.", error); + + _ = file.Open(Night.FileMode.Read); + (data, error) = file.ReadBytes(); + Assert.Null(error); + Assert.Equal(content, data); + + // Second read should return empty array + (data, error) = file.ReadBytes(); + Assert.Null(error); + Assert.Empty(data!); + + _ = file.Close(); + + // Reading after a partial read + _ = file.Open(Night.FileMode.Read); // Re-open for this sub-test + var (partialData, _) = file.ReadBytes(2); + Assert.Equal(new byte[] { 1, 2 }, partialData); + (data, error) = file.ReadBytes(); + Assert.Null(error); + Assert.Equal(new byte[] { 3, 4, 5 }, data); + _ = file.Close(); + } + } + finally + { + File.Delete(tempFile); + } + } + } + + /// + /// Tests reading a specific number of bytes from a file with the class. + /// + public class NightFile_Read_BytesCounted : ModTestCase + { + /// + public override string Name => "Filesystem.NightFile.Read.BytesCounted"; + + /// + public override string Description => "Tests reading a specific number of bytes from a file."; + + /// + public override string SuccessMessage => "Reading a specific number of bytes works correctly."; + + /// + public override void Run() + { + var tempFile = Path.GetTempFileName(); + var content = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + File.WriteAllBytes(tempFile, content); + + try + { + using (var file = new NightFile(tempFile)) + { + _ = file.Open(Night.FileMode.Read); + + // Read 0 or negative bytes + var (data, error) = file.ReadBytes(0); + Assert.Null(error); + Assert.Empty(data!); + + (data, error) = file.ReadBytes(-5); + Assert.Null(error); + Assert.Empty(data!); + + // Read specific number of bytes + (data, error) = file.ReadBytes(4); + Assert.Null(error); + Assert.Equal(new byte[] { 0, 1, 2, 3 }, data); + + // Read more bytes than available + (data, error) = file.ReadBytes(100); + Assert.Null(error); + Assert.Equal(new byte[] { 4, 5, 6, 7, 8, 9 }, data); + + // Read at EOF + (data, error) = file.ReadBytes(1); + Assert.Null(error); + Assert.Empty(data!); + + _ = file.Close(); + } + } + finally + { + File.Delete(tempFile); + } + } + } + + /// + /// Tests the Dispose method and behavior of a disposed object. + /// + public class NightFile_Dispose : ModTestCase + { + /// + public override string Name => "Filesystem.NightFile.Dispose"; + + /// + public override string Description => "Tests the Dispose method and behavior of a disposed object."; + + /// + public override string SuccessMessage => "Dispose works correctly, and disposed object behaves as expected."; + + /// + public override void Run() + { + var tempFile = Path.GetTempFileName(); + try + { + // Dispose on a file that was never opened + using (var file = new NightFile(tempFile)) + { + file.Dispose(); + Assert.False(file.IsOpen); + } + + // Open a file, then dispose it + using (var file = new NightFile(tempFile)) + { + _ = file.Open("r"); + Assert.True(file.IsOpen); + file.Dispose(); + Assert.False(file.IsOpen, "IsOpen should be false after dispose"); + + // Calling methods on a disposed object should fail gracefully + var (success, error) = file.Open("r"); + Assert.False(success); + Assert.Equal("Cannot open a disposed file.", error); + + var (data, readError) = file.Read(); + Assert.Null(data); + Assert.NotNull(readError); + + var (closeSuccess, _) = file.Close(); + Assert.True(closeSuccess, "Close on a disposed file should succeed without error."); + + // Call dispose multiple times + file.Dispose(); + } + } + finally + { + File.Delete(tempFile); + } + } + } +} diff --git a/tests/Groups/Filesystem/ReadBytesTests.cs b/tests/Groups/Filesystem/ReadBytesTests.cs new file mode 100644 index 00000000..d731fa29 --- /dev/null +++ b/tests/Groups/Filesystem/ReadBytesTests.cs @@ -0,0 +1,96 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System.IO; + +using Night; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Tests for Night.Filesystem.ReadBytes(). + /// + public class FilesystemReadBytes_ReadExistingFileTest : ModTestCase + { + private readonly string testFileName = Path.Combine(Path.GetTempPath(), "night_test_readbytes_file.bin"); + private readonly byte[] expectedContent = { 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x4E, 0x69, 0x67, 0x68, 0x74 }; // "Hello Night" + + /// + public override string Name => "Filesystem.ReadBytes.ReadExistingFile"; + + /// + public override string Description => "Tests ReadBytes for an existing binary file."; + + /// + public override string SuccessMessage => "Successfully read bytes from an existing file."; + + /// + public override void Run() + { + try + { + File.WriteAllBytes(this.testFileName, this.expectedContent); + byte[] actualContent = Night.Filesystem.ReadBytes(this.testFileName); + Assert.Equal(this.expectedContent, actualContent); + } + finally + { + if (File.Exists(this.testFileName)) + { + File.Delete(this.testFileName); + } + } + } + } + + /// + /// Tests Night.Filesystem.ReadBytes for a non-existent file. + /// + public class FilesystemReadBytes_FileNotFoundTest : ModTestCase + { + private readonly string nonExistentFile = Path.Combine(Path.GetTempPath(), "night_test_readbytes_nonexistent.bin"); + + /// + public override string Name => "Filesystem.ReadBytes.FileNotFound"; + + /// + public override string Description => "Tests ReadBytes throws FileNotFoundException for a non-existent file."; + + /// + public override string SuccessMessage => "Successfully caught FileNotFoundException for ReadBytes."; + + /// + public override void Run() + { + if (File.Exists(this.nonExistentFile)) + { + File.Delete(this.nonExistentFile); // Ensure it doesn't exist + } + + _ = Assert.Throws(() => Night.Filesystem.ReadBytes(this.nonExistentFile)); + } + } +} diff --git a/tests/Groups/Filesystem/ReadLogicTests.cs b/tests/Groups/Filesystem/ReadLogicTests.cs new file mode 100644 index 00000000..dc6237d0 --- /dev/null +++ b/tests/Groups/Filesystem/ReadLogicTests.cs @@ -0,0 +1,101 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; +using System.Linq; + +using Night; // For LogManager, MemorySink, LogLevel, LogEntry + +// Removed: using Night.Filesystem; // This was incorrect as Filesystem is a static class +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Tests Filesystem.Read's capping logic when file length > int.MaxValue, + /// expecting a warning and an error return due to likely OutOfMemoryException. + /// + public class FilesystemRead_CappingLogicForLargeFileTest : ModTestCase + { + /// + public override string Name => "Filesystem.Read.CappingLogicForLargeFile"; + + /// + public override string Description => "Tests Filesystem.Read's capping logic when file length > int.MaxValue, expecting a warning and an error return due to likely OOM."; + + /// + public override string SuccessMessage => "Filesystem.Read logged warning for >int.MaxValue file length and returned an error as expected."; + + /// + public override void Run() + { + string? tempFilePath = null; + MemorySink? memorySink = null; + long testFileLength = (long)int.MaxValue + 1; + + try + { + tempFilePath = Path.GetTempFileName(); + + // Create a sparse file larger than int.MaxValue + using (var fs = new System.IO.FileStream(tempFilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.None)) + { + fs.SetLength(testFileLength); + } + + memorySink = new MemorySink(); + Night.LogManager.AddSink(memorySink); + + var (contents, bytesRead, errorMsg) = Night.Filesystem.Read(Night.Filesystem.ContainerType.Data, tempFilePath, sizeToRead: null); + + // Assertions + Assert.Null(contents); + Assert.Null(bytesRead); + Assert.NotNull(errorMsg); + + // Check for the specific warning log + bool warningLogged = memorySink.GetEntries().Any(entry => + entry.Level == Night.LogLevel.Warning && + entry.Message.Contains($"Requested read size ({testFileLength} bytes) for '{tempFilePath}' exceeds int.MaxValue. Capping read at {int.MaxValue} bytes.")); + Assert.True(warningLogged, "Expected warning log for capping read size was not found."); + + // Check if the error message indicates an OutOfMemoryException or a general unexpected error + Assert.Contains("An unexpected error occurred", errorMsg, StringComparison.OrdinalIgnoreCase); + } + finally + { + if (memorySink != null) + { + Night.LogManager.RemoveSink(memorySink); + } + + if (tempFilePath != null && File.Exists(tempFilePath)) + { + File.Delete(tempFilePath); + } + } + } + } +} diff --git a/tests/Groups/Filesystem/ReadTests.cs b/tests/Groups/Filesystem/ReadTests.cs new file mode 100644 index 00000000..6aeb5b6e --- /dev/null +++ b/tests/Groups/Filesystem/ReadTests.cs @@ -0,0 +1,325 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; +using System.Linq; +using System.Text; + +using Night; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Tests for Night.Filesystem.Read(name, sizeToRead) - string overload. + /// + public class FilesystemRead_String_ReadExistingFileTest : ModTestCase + { + private readonly string testFileName = Path.Combine(Path.GetTempPath(), "night_test_read_string_existing.txt"); + private readonly string expectedContent = "Hello Night from Read String Test!"; + + /// + public override string Name => "Filesystem.Read.String.ReadExistingFile"; + + /// + public override string Description => "Tests Read(name, size) for an existing text file, returning string."; + + /// + public override string SuccessMessage => "Successfully read string content and byte count matches."; + + /// + public override void Run() + { + try + { + File.WriteAllText(this.testFileName, this.expectedContent, new UTF8Encoding(false)); + + (string? actualContent, long? bytesRead, string? errorMsg) = Night.Filesystem.Read(this.testFileName); + + Assert.Null(errorMsg); + Assert.Equal(this.expectedContent, actualContent); + Assert.Equal(Encoding.UTF8.GetByteCount(this.expectedContent), bytesRead); + } + finally + { + if (File.Exists(this.testFileName)) + { + File.Delete(this.testFileName); + } + } + } + } + + /// + /// Tests for Night.Filesystem.Read(ContainerType.Data, name, sizeToRead). + /// + public class FilesystemRead_Data_ReadExistingFileTest : ModTestCase + { + private readonly string testFileName = Path.Combine(Path.GetTempPath(), "night_test_read_data_existing.bin"); + private readonly byte[] expectedContent = { 0x01, 0x02, 0x03, 0xAA, 0xBB, 0xCC }; + + /// + public override string Name => "Filesystem.Read.Data.ReadExistingFile"; + + /// + public override string Description => "Tests Read(ContainerType.Data, name, size) for an existing binary file."; + + /// + public override string SuccessMessage => "Successfully read byte[] content and byte count matches."; + + /// + public override void Run() + { + try + { + File.WriteAllBytes(this.testFileName, this.expectedContent); + (object? actualContentObj, long? bytesRead, string? errorMsg) = Night.Filesystem.Read(Night.Filesystem.ContainerType.Data, this.testFileName); + + Assert.Null(errorMsg); + byte[]? actualContent = Assert.IsType(actualContentObj); + Assert.Equal(this.expectedContent, actualContent); + Assert.Equal(this.expectedContent.Length, bytesRead); + } + finally + { + if (File.Exists(this.testFileName)) + { + File.Delete(this.testFileName); + } + } + } + } + + /// + /// Tests Night.Filesystem.Read for a non-existent file. + /// + public class FilesystemRead_FileNotFoundTest : ModTestCase + { + private readonly string nonExistentFile = Path.Combine(Path.GetTempPath(), "night_test_read_nonexistent.txt"); + + /// + public override string Name => "Filesystem.Read.FileNotFound"; + + /// + public override string Description => "Tests Read returns (null, null, 'File not found.') for a non-existent file."; + + /// + public override string SuccessMessage => "Correctly returned (null, null, 'File not found.') for non-existent file."; + + /// + public override void Run() + { + if (File.Exists(this.nonExistentFile)) + { + File.Delete(this.nonExistentFile); // Ensure it doesn't exist + } + + (string? contents, long? bytesRead, string? errorMsg) = Night.Filesystem.Read(this.nonExistentFile); + + Assert.Null(contents); + Assert.Null(bytesRead); + Assert.Equal("File not found.", errorMsg); + } + } + + /// + /// Tests Night.Filesystem.Read for partial string read. + /// + public class FilesystemRead_String_PartialReadTest : ModTestCase + { + private readonly string testFileName = Path.Combine(Path.GetTempPath(), "night_test_read_string_partial.txt"); + private readonly string fullContent = "This is the full content."; + private readonly string expectedContent = "This is"; // 7 bytes + private readonly long bytesToRead = 7; + + /// + public override string Name => "Filesystem.Read.String.PartialRead"; + + /// + public override string Description => "Tests Read(name, size) for a partial read, returning string."; + + /// + public override string SuccessMessage => "Successfully read partial string content."; + + /// + public override void Run() + { + try + { + File.WriteAllText(this.testFileName, this.fullContent, new UTF8Encoding(false)); + (string? actualContent, long? actualBytesRead, string? errorMsg) = Night.Filesystem.Read(this.testFileName, this.bytesToRead); + + Assert.Null(errorMsg); + Assert.Equal(this.expectedContent, actualContent); + Assert.Equal(this.bytesToRead, actualBytesRead); + } + finally + { + if (File.Exists(this.testFileName)) + { + File.Delete(this.testFileName); + } + } + } + } + + /// + /// Tests Night.Filesystem.Read for partial data read. + /// + public class FilesystemRead_Data_PartialReadTest : ModTestCase + { + private readonly string testFileName = Path.Combine(Path.GetTempPath(), "night_test_read_data_partial.bin"); + private readonly byte[] fullContent = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; + private readonly byte[] expectedContent = { 0x01, 0x02, 0x03, 0x04 }; + private readonly long bytesToRead = 4; + + /// + public override string Name => "Filesystem.Read.Data.PartialRead"; + + /// + public override string Description => "Tests Read(ContainerType.Data, name, size) for a partial read."; + + /// + public override string SuccessMessage => "Successfully read partial byte[] content and byte count matches."; + + /// + public override void Run() + { + try + { + File.WriteAllBytes(this.testFileName, this.fullContent); + (object? actualContentObj, long? actualBytesRead, string? errorMsg) = Night.Filesystem.Read(Night.Filesystem.ContainerType.Data, this.testFileName, this.bytesToRead); + + Assert.Null(errorMsg); + byte[]? actualContent = Assert.IsType(actualContentObj); + Assert.Equal(this.expectedContent, actualContent); + Assert.Equal(this.bytesToRead, actualBytesRead); + } + finally + { + if (File.Exists(this.testFileName)) + { + File.Delete(this.testFileName); + } + } + } + } + + /// + /// Tests Night.Filesystem.Read for an empty file. + /// + public class FilesystemRead_EmptyFileTest : ModTestCase + { + private readonly string testFileName = Path.Combine(Path.GetTempPath(), "night_test_read_empty.txt"); + + /// + public override string Name => "Filesystem.Read.EmptyFile"; + + /// + public override string Description => "Tests Read for an empty file, expecting empty string/data and 0 bytes read."; + + /// + public override string SuccessMessage => "Successfully read empty file as string and data (empty content, 0 bytes read)."; + + /// + public override void Run() + { + try + { + File.WriteAllBytes(this.testFileName, Array.Empty()); // Create empty file + + // Test as string + (string? strContents, long? strBytesRead, string? strErrorMsg) = Night.Filesystem.Read(this.testFileName); + Assert.Null(strErrorMsg); + Assert.Equal(string.Empty, strContents); + Assert.Equal(0, strBytesRead); + + // Test as data + (object? dataContentsObj, long? dataBytesRead, string? dataErrorMsg) = Night.Filesystem.Read(Night.Filesystem.ContainerType.Data, this.testFileName); + Assert.Null(dataErrorMsg); + byte[]? dataContents = Assert.IsType(dataContentsObj); + Assert.Empty(dataContents); + Assert.Equal(0, dataBytesRead); + } + finally + { + if (File.Exists(this.testFileName)) + { + File.Delete(this.testFileName); + } + } + } + } + + /// + /// Tests Night.Filesystem.Read with invalid arguments. + /// + public class FilesystemRead_ArgumentValidationTest : ModTestCase + { + /// + public override string Name => "Filesystem.Read.ArgumentValidation"; + + /// + public override string Description => "Tests Read with various invalid arguments (null/empty filename, negative size)."; + + /// + public override string SuccessMessage => "All argument validation tests passed for Read."; + + /// + public override void Run() + { + // Test null name + (_, _, string? errorMsgNull) = Night.Filesystem.Read(null!); + Assert.Equal("File name cannot be null or empty.", errorMsgNull); + + // Test empty name + (_, _, string? errorMsgEmpty) = Night.Filesystem.Read(string.Empty); + Assert.Equal("File name cannot be null or empty.", errorMsgEmpty); + + string dummyFile = Path.Combine(Path.GetTempPath(), "night_test_read_dummy_arg.txt"); + try + { + // Test negative size + File.WriteAllText(dummyFile, "dummy"); + (_, _, string? errorMsgNegative) = Night.Filesystem.Read(dummyFile, -5); + Assert.Equal("Size to read cannot be negative.", errorMsgNegative); + + // Test zero size + (string? zeroContent, long? zeroBytes, string? zeroError) = Night.Filesystem.Read(dummyFile, 0); + Assert.Null(zeroError); + Assert.Equal(string.Empty, zeroContent); + Assert.Equal(0, zeroBytes); + } + finally + { + if (File.Exists(dummyFile)) + { + File.Delete(dummyFile); + } + } + } + } +} diff --git a/tests/Groups/Filesystem/ReadTests2.cs b/tests/Groups/Filesystem/ReadTests2.cs new file mode 100644 index 00000000..7ec2ed3c --- /dev/null +++ b/tests/Groups/Filesystem/ReadTests2.cs @@ -0,0 +1,503 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; +using System.Linq; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Text; + +using Night; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Tests Filesystem.Lines() with a null file path. + /// + public class FilesystemLines_NullPathTest : ModTestCase + { + /// + public override string Name => "Filesystem.Lines.NullPath"; + + /// + public override string Description => "Tests that Filesystem.Lines(null) throws ArgumentNullException."; + + /// + public override string SuccessMessage => "Successfully threw ArgumentNullException for null path."; + + /// + public override void Run() + { + _ = Assert.Throws(() => Night.Filesystem.Lines(null!).ToList()); + } + } + + /// + /// Tests Filesystem.Lines() with an empty file path. + /// + public class FilesystemLines_EmptyPathTest : ModTestCase + { + /// + public override string Name => "Filesystem.Lines.EmptyPath"; + + /// + public override string Description => "Tests that Filesystem.Lines(\"\") throws ArgumentException."; + + /// + public override string SuccessMessage => "Successfully threw ArgumentException for empty path."; + + /// + public override void Run() + { + _ = Assert.Throws(() => Night.Filesystem.Lines(string.Empty).ToList()); + } + } + + /// + /// Tests Filesystem.Lines() when trying to read a directory. + /// + public class FilesystemLines_ReadDirectoryTest : ModTestCase + { + private readonly string tempDirPath = Path.Combine(Path.GetTempPath(), "night_test_lines_dir"); + + /// + public override string Name => "Filesystem.Lines.ReadDirectory"; + + /// + public override string Description => "Tests Filesystem.Lines() on a directory throws UnauthorizedAccessException."; + + /// + public override string SuccessMessage => "Successfully threw UnauthorizedAccessException when reading a directory."; + + /// + public override void Run() + { + if (Directory.Exists(this.tempDirPath)) + { + Directory.Delete(this.tempDirPath, true); + } + + _ = Directory.CreateDirectory(this.tempDirPath); + + try + { + // Attempting to iterate lines on a directory should fail. + // File.ReadLines, which Filesystem.Lines uses, throws UnauthorizedAccessException for directories. + _ = Assert.Throws(() => Night.Filesystem.Lines(this.tempDirPath).ToList()); + } + finally + { + if (Directory.Exists(this.tempDirPath)) + { + Directory.Delete(this.tempDirPath, true); + } + } + } + } + + /// + /// Tests Filesystem.Lines() when trying to read a locked file. + /// + public class FilesystemLines_LockedFileTest : ModTestCase + { + private readonly string tempFilePath = Path.Combine(Path.GetTempPath(), "night_test_lines_locked.txt"); + + /// + public override string Name => "Filesystem.Lines.LockedFile"; + + /// + public override string Description => "Tests Filesystem.Lines() on a locked file throws IOException."; + + /// + public override string SuccessMessage => "Successfully threw IOException when reading a locked file."; + + /// + public override void Run() + { + if (File.Exists(this.tempFilePath)) + { + File.Delete(this.tempFilePath); + } + + // Create an empty file to lock + File.WriteAllText(this.tempFilePath, "lock me"); + + FileStream? lockStream = null; + try + { + // Lock the file + lockStream = new FileStream(this.tempFilePath, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.None); + + // Attempting to iterate lines on a locked file should fail. + _ = Assert.Throws(() => Night.Filesystem.Lines(this.tempFilePath).ToList()); + } + finally + { + lockStream?.Dispose(); + if (File.Exists(this.tempFilePath)) + { + File.Delete(this.tempFilePath); + } + } + } + } + + /// + /// Tests Filesystem.Read(string) when trying to read a directory. + /// + public class FilesystemRead_String_ReadDirectoryTest : ModTestCase + { + private readonly string tempDirPath = Path.Combine(Path.GetTempPath(), "night_test_read_s_dir"); + + /// + public override string Name => "Filesystem.Read.String.ReadDirectory"; + + /// + public override string Description => "Tests Read(string) on a directory returns 'File not found.' error."; + + /// + public override string SuccessMessage => "Correctly returned error for Read(string) on a directory."; + + /// + public override void Run() + { + if (Directory.Exists(this.tempDirPath)) + { + Directory.Delete(this.tempDirPath, true); + } + + _ = Directory.CreateDirectory(this.tempDirPath); + + try + { + (string? contents, long? bytesRead, string? errorMsg) = Night.Filesystem.Read(this.tempDirPath); + + Assert.Null(contents); + Assert.Null(bytesRead); + Assert.Equal("File not found.", errorMsg); + } + finally + { + if (Directory.Exists(this.tempDirPath)) + { + Directory.Delete(this.tempDirPath, true); + } + } + } + } + + /// + /// Tests Filesystem.Read(ContainerType.Data) when trying to read a directory. + /// + public class FilesystemRead_Data_ReadDirectoryTest : ModTestCase + { + private readonly string tempDirPath = Path.Combine(Path.GetTempPath(), "night_test_read_d_dir"); + + /// + public override string Name => "Filesystem.Read.Data.ReadDirectory"; + + /// + public override string Description => "Tests Read(ContainerType.Data) on a directory returns 'File not found.' error."; + + /// + public override string SuccessMessage => "Correctly returned error for Read(ContainerType.Data) on a directory."; + + /// + public override void Run() + { + if (Directory.Exists(this.tempDirPath)) + { + Directory.Delete(this.tempDirPath, true); + } + + _ = Directory.CreateDirectory(this.tempDirPath); + + try + { + (object? contents, long? bytesRead, string? errorMsg) = Night.Filesystem.Read(Night.Filesystem.ContainerType.Data, this.tempDirPath); + + Assert.Null(contents); + Assert.Null(bytesRead); + Assert.Equal("File not found.", errorMsg); + } + finally + { + if (Directory.Exists(this.tempDirPath)) + { + Directory.Delete(this.tempDirPath, true); + } + } + } + } + + /// + /// Tests Filesystem.Read(string) when trying to read a locked file. + /// + public class FilesystemRead_String_LockedFileTest : ModTestCase + { + private readonly string tempFilePath = Path.Combine(Path.GetTempPath(), "night_test_read_s_locked.txt"); + + /// + public override string Name => "Filesystem.Read.String.LockedFile"; + + /// + public override string Description => "Tests Read(string) on a locked file returns an IO error."; + + /// + public override string SuccessMessage => "Correctly returned IO error for Read(string) on a locked file."; + + /// + public override void Run() + { + if (File.Exists(this.tempFilePath)) + { + File.Delete(this.tempFilePath); + } + + File.WriteAllText(this.tempFilePath, "lock me"); // Create file to lock + + FileStream? lockStream = null; + try + { + lockStream = new FileStream(this.tempFilePath, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.None); // Lock the file + + (string? contents, long? bytesRead, string? errorMsg) = Night.Filesystem.Read(this.tempFilePath); + + Assert.Null(contents); + Assert.Null(bytesRead); // BytesRead might be 0 if error occurs before any read attempt, or null. The code returns null for BytesRead in this case. + Assert.NotNull(errorMsg); + Assert.StartsWith("IO error:", errorMsg); + } + finally + { + lockStream?.Dispose(); + if (File.Exists(this.tempFilePath)) + { + File.Delete(this.tempFilePath); + } + } + } + } + + /// + /// Tests Filesystem.Read(ContainerType.Data) when trying to read a locked file. + /// + public class FilesystemRead_Data_LockedFileTest : ModTestCase + { + private readonly string tempFilePath = Path.Combine(Path.GetTempPath(), "night_test_read_d_locked.txt"); + + /// + public override string Name => "Filesystem.Read.Data.LockedFile"; + + /// + public override string Description => "Tests Read(ContainerType.Data) on a locked file returns an IO error."; + + /// + public override string SuccessMessage => "Correctly returned IO error for Read(ContainerType.Data) on a locked file."; + + /// + public override void Run() + { + if (File.Exists(this.tempFilePath)) + { + File.Delete(this.tempFilePath); + } + + File.WriteAllText(this.tempFilePath, "lock me data"); // Create file to lock + + FileStream? lockStream = null; + try + { + lockStream = new FileStream(this.tempFilePath, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.None); // Lock the file + + (object? contents, long? bytesRead, string? errorMsg) = Night.Filesystem.Read(Night.Filesystem.ContainerType.Data, this.tempFilePath); + + Assert.Null(contents); + Assert.Null(bytesRead); + Assert.NotNull(errorMsg); + Assert.StartsWith("IO error:", errorMsg); + } + finally + { + lockStream?.Dispose(); + if (File.Exists(this.tempFilePath)) + { + File.Delete(this.tempFilePath); + } + } + } + } + + /// + /// Tests Filesystem.Read() for UnauthorizedAccessException. + /// This test is Windows-specific due to ACL manipulation. + /// + public class FilesystemRead_UnauthorizedAccessTest : ModTestCase + { + private readonly string tempFilePath = Path.Combine(Path.GetTempPath(), "night_test_read_unauthorized.txt"); + + /// + public override string Name => "Filesystem.Read.UnauthorizedAccess"; + + /// + public override string Description => "Tests Read() returns 'Unauthorized access.' when file permissions deny read."; + + /// + public override string SuccessMessage => "Correctly returned 'Unauthorized access.' for permission-denied file."; + + /// + public override void Run() + { + if (!OperatingSystem.IsWindows()) + { + // On non-Windows platforms, we can't easily manipulate ACLs in a standard way. + // The test will "pass" by not running its core logic or failing assertions. + // Xunit might report this as Passed or Skipped depending on runner verbosity and configuration. + // For ModTestCase, simply returning is the cleanest way to achieve this. + Console.WriteLine("Skipping FilesystemRead_UnauthorizedAccessTest on non-Windows platform."); + return; + } + + if (File.Exists(this.tempFilePath)) + { + File.Delete(this.tempFilePath); + } + + File.WriteAllText(this.tempFilePath, "test content"); + FileSecurity? originalSecurity = null; + bool securityChanged = false; + + try + { +#pragma warning disable CA1416 // Validate platform compatibility + FileInfo fileInfo = new FileInfo(this.tempFilePath); + originalSecurity = fileInfo.GetAccessControl(); + FileSystemRights denyRights = FileSystemRights.ReadData | FileSystemRights.ReadAttributes | FileSystemRights.ReadExtendedAttributes | FileSystemRights.ReadPermissions; + + // It's important to use the current user for the deny rule. + WindowsIdentity currentUser = WindowsIdentity.GetCurrent(); + FileSystemAccessRule denyRule = new FileSystemAccessRule( + currentUser.User ?? throw new InvalidOperationException("Could not get current user SID."), // Should have a SID + denyRights, + AccessControlType.Deny); + + originalSecurity.AddAccessRule(denyRule); + fileInfo.SetAccessControl(originalSecurity); + securityChanged = true; +#pragma warning restore CA1416 // Validate platform compatibility + + (string? contents, long? bytesRead, string? errorMsg) = Night.Filesystem.Read(this.tempFilePath); + + Assert.Null(contents); + Assert.Null(bytesRead); + Assert.Equal("Unauthorized access.", errorMsg); + } + finally + { + if (securityChanged && originalSecurity != null && OperatingSystem.IsWindows()) + { +#pragma warning disable CA1416 // Validate platform compatibility + // Attempt to restore original permissions. This might fail if the deny rule was too effective. + // Best effort cleanup. + try + { + FileInfo fileInfo = new FileInfo(this.tempFilePath); + FileSecurity currentSecurity = fileInfo.GetAccessControl(); + WindowsIdentity currentUser = WindowsIdentity.GetCurrent(); + FileSystemAccessRule ruleToRemove = new FileSystemAccessRule( + currentUser.User!, + FileSystemRights.ReadData | FileSystemRights.ReadAttributes | FileSystemRights.ReadExtendedAttributes | FileSystemRights.ReadPermissions, + AccessControlType.Deny); + _ = currentSecurity.RemoveAccessRule(ruleToRemove); // Try removing the specific rule + fileInfo.SetAccessControl(currentSecurity); + } + catch (Exception ex) + { + // Log if restoration fails, but don't let it fail the test. + Console.WriteLine($"Warning: Failed to fully restore permissions for {this.tempFilePath}. Manual cleanup may be needed. Error: {ex.Message}"); + } +#pragma warning restore CA1416 // Validate platform compatibility + } + + if (File.Exists(this.tempFilePath)) + { + File.Delete(this.tempFilePath); + } + } + } + } + + /// + /// Tests Filesystem.Read() for generic Exception when UTF-8 decoding fails. + /// + public class FilesystemRead_DecodingErrorTest : ModTestCase + { + private readonly string tempFilePath = Path.Combine(Path.GetTempPath(), "night_test_read_decoding_error.txt"); + + /// + public override string Name => "Filesystem.Read.DecodingError"; + + /// + public override string Description => "Tests Read() returns 'An unexpected error occurred:' for invalid UTF-8 sequence."; + + /// + public override string SuccessMessage => "Correctly returned 'An unexpected error occurred:' for decoding error."; + + /// + public override void Run() + { + if (File.Exists(this.tempFilePath)) + { + File.Delete(this.tempFilePath); + } + + // Create a file with an invalid UTF-8 sequence (e.g., an isolated surrogate or an overlong sequence part) + // 0xC3 followed by 0x28 is an example of an invalid sequence (start of a 2-byte char, but 0x28 is not a valid continuation) + byte[] invalidUtf8Bytes = { 0xC3, 0x28 }; + File.WriteAllBytes(this.tempFilePath, invalidUtf8Bytes); + + try + { + (string? contents, long? bytesRead, string? errorMsg) = Night.Filesystem.Read(this.tempFilePath); + + // Encoding.UTF8.GetString by default replaces invalid sequences with '�'. + // So, no exception is thrown, and errorMsg should be null. + // The content will be the string with replacement characters. + Assert.Equal("�(", contents); + Assert.Equal(invalidUtf8Bytes.Length, bytesRead); + Assert.Null(errorMsg); + } + finally + { + if (File.Exists(this.tempFilePath)) + { + File.Delete(this.tempFilePath); + } + } + } + } +} diff --git a/tests/Groups/Filesystem/ReadTextTests.cs b/tests/Groups/Filesystem/ReadTextTests.cs new file mode 100644 index 00000000..4c478c52 --- /dev/null +++ b/tests/Groups/Filesystem/ReadTextTests.cs @@ -0,0 +1,96 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System.IO; + +using Night; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Tests for Night.Filesystem.ReadText(). + /// + public class FilesystemReadText_ReadExistingFileTest : ModTestCase + { + private readonly string testFileName = Path.Combine(Path.GetTempPath(), "night_test_readtext_file.txt"); + private readonly string expectedContent = "Hello Night Text!"; + + /// + public override string Name => "Filesystem.ReadText.ReadExistingFile"; + + /// + public override string Description => "Tests ReadText for an existing text file."; + + /// + public override string SuccessMessage => "Successfully read text from an existing file."; + + /// + public override void Run() + { + try + { + File.WriteAllText(this.testFileName, this.expectedContent); + string actualContent = Night.Filesystem.ReadText(this.testFileName); + Assert.Equal(this.expectedContent, actualContent); + } + finally + { + if (File.Exists(this.testFileName)) + { + File.Delete(this.testFileName); + } + } + } + } + + /// + /// Tests Night.Filesystem.ReadText for a non-existent file. + /// + public class FilesystemReadText_FileNotFoundTest : ModTestCase + { + private readonly string nonExistentFile = Path.Combine(Path.GetTempPath(), "night_test_readtext_nonexistent.txt"); + + /// + public override string Name => "Filesystem.ReadText.FileNotFound"; + + /// + public override string Description => "Tests ReadText throws FileNotFoundException for a non-existent file."; + + /// + public override string SuccessMessage => "Successfully caught FileNotFoundException for ReadText."; + + /// + public override void Run() + { + if (File.Exists(this.nonExistentFile)) + { + File.Delete(this.nonExistentFile); // Ensure it doesn't exist + } + + _ = Assert.Throws(() => Night.Filesystem.ReadText(this.nonExistentFile)); + } + } +} diff --git a/tests/Groups/Filesystem/RemoveTests.cs b/tests/Groups/Filesystem/RemoveTests.cs new file mode 100644 index 00000000..f889fb1d --- /dev/null +++ b/tests/Groups/Filesystem/RemoveTests.cs @@ -0,0 +1,238 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; + +using Night; + +using NightTest.Core; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Base class for Filesystem.Remove tests, handling setup and teardown. + /// + public abstract class BaseRemoveTest : GameTestCase + { + /// + /// Gets the path to the save directory used for this test run. + /// +#pragma warning disable SA1401 // Fields should be private + protected string saveDir = string.Empty; +#pragma warning restore SA1401 // Fields should be private + private readonly string testIdentity = "NightTest_Remove"; + + /// + /// Sets up the test environment by setting a unique filesystem identity + /// and cleaning up any artifacts from previous test runs. + /// + protected override void Load() + { + Night.Filesystem.SetIdentity(this.testIdentity); + this.saveDir = Night.Filesystem.GetSaveDirectory(); + + // Clean up previous test runs + if (Directory.Exists(this.saveDir)) + { + Directory.Delete(this.saveDir, true); + } + + _ = Directory.CreateDirectory(this.saveDir); + } + + /// + /// The main update loop for the test case. + /// + /// The time elapsed since the last frame. + protected override void Update(double deltaTime) + { + // Most tests are synchronous and will complete in one frame + } + } + + /// + /// Tests successfully removing a file from the save directory. + /// + public class RemoveFileTest : BaseRemoveTest + { + /// + public override string Name => "Filesystem.Remove File"; + + /// + public override string Description => "Tests successfully removing a file from the save directory."; + + /// + protected override void Update(double deltaTime) + { + var testFile = "testfile.txt"; + var fullPath = Path.Combine(this.saveDir, testFile); + File.WriteAllText(fullPath, "delete me"); + + if (!File.Exists(fullPath)) + { + this.RecordFailure("Setup failed: Could not create test file."); + return; + } + + var result = Night.Filesystem.Remove(testFile); + + if (result && !File.Exists(fullPath)) + { + this.RecordSuccess("Successfully removed file and it no longer exists."); + } + else + { + this.RecordFailure($"Remove returned {result} but file existence is {File.Exists(fullPath)}."); + } + } + } + + /// + /// Tests successfully removing an empty directory from the save directory. + /// + public class RemoveEmptyDirTest : BaseRemoveTest + { + /// + public override string Name => "Filesystem.Remove Empty Directory"; + + /// + public override string Description => "Tests successfully removing an empty directory from the save directory."; + + /// + protected override void Update(double deltaTime) + { + var testDir = "emptydir"; + var fullPath = Path.Combine(this.saveDir, testDir); + _ = Directory.CreateDirectory(fullPath); + + if (!Directory.Exists(fullPath)) + { + this.RecordFailure("Setup failed: Could not create test directory."); + return; + } + + var result = Night.Filesystem.Remove(testDir); + + if (result && !Directory.Exists(fullPath)) + { + this.RecordSuccess("Successfully removed empty directory and it no longer exists."); + } + else + { + this.RecordFailure($"Remove returned {result} but directory existence is {Directory.Exists(fullPath)}."); + } + } + } + + /// + /// Tests that removing a non-empty directory fails. + /// + public class RemoveNonEmptyDirTest : BaseRemoveTest + { + /// + public override string Name => "Filesystem.Remove Non-Empty Directory"; + + /// + public override string Description => "Tests that removing a non-empty directory fails."; + + /// + protected override void Update(double deltaTime) + { + var testDir = "nonemptydir"; + var fullPath = Path.Combine(this.saveDir, testDir); + _ = Directory.CreateDirectory(fullPath); + File.WriteAllText(Path.Combine(fullPath, "somefile.txt"), "content"); + + var result = Night.Filesystem.Remove(testDir); + + if (!result && Directory.Exists(fullPath)) + { + this.RecordSuccess("Correctly failed to remove non-empty directory."); + } + else + { + this.RecordFailure($"Remove returned {result} for a non-empty directory."); + } + } + } + + /// + /// Tests that removing a file outside the save directory fails. + /// + public class RemoveOutsideSaveDirTest : BaseRemoveTest + { + /// + public override string Name => "Filesystem.Remove Outside Save Directory"; + + /// + public override string Description => "Tests that removing a file outside the save directory fails."; + + /// + protected override void Update(double deltaTime) + { + // Create a file outside the save dir to attempt to delete + var tempFile = Path.GetTempFileName(); + var relativePath = Path.GetRelativePath(this.saveDir, tempFile); + + var result = Night.Filesystem.Remove(relativePath); + + File.Delete(tempFile); // Clean up + + if (!result) + { + this.RecordSuccess("Correctly failed to remove a file outside the save directory."); + } + else + { + this.RecordFailure("Incorrectly succeeded in removing a file outside the save directory."); + } + } + } + + /// + /// Tests that removing a non-existent path fails gracefully. + /// + public class RemoveNotFoundTest : BaseRemoveTest + { + /// + public override string Name => "Filesystem.Remove Non-Existent Path"; + + /// + public override string Description => "Tests that removing a non-existent path fails gracefully."; + + /// + protected override void Update(double deltaTime) + { + var result = Night.Filesystem.Remove("nonexistent.file"); + + if (!result) + { + this.RecordSuccess("Correctly failed to remove a non-existent file."); + } + else + { + this.RecordFailure("Incorrectly succeeded when trying to remove a non-existent file."); + } + } + } +} diff --git a/tests/Groups/Filesystem/WriteTests.cs b/tests/Groups/Filesystem/WriteTests.cs new file mode 100644 index 00000000..d1309d5b --- /dev/null +++ b/tests/Groups/Filesystem/WriteTests.cs @@ -0,0 +1,463 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; +using System.Linq; +using System.Text; + +using Night; +using Night.Log; // Added for ILogger and LogManager + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Base class for Filesystem.Write ModTestCases, providing common setup and teardown logic + /// for creating and cleaning up temporary test files and directories. + /// + public abstract class BaseFilesystemWriteTest : ModTestCase + { + /// + /// Gets or sets the path to the temporary file used for the current test. + /// This path is automatically generated within a temporary test directory. + /// + protected string TestFilePath { get; set; } = string.Empty; + + /// + /// Gets the path to the temporary directory created for filesystem write tests. + /// + protected string TestDirectoryPath { get; private set; } = string.Empty; + + /// + /// Sets up the test environment by creating a temporary directory and defining a test file path. + /// It then calls and ensures cleanup of created files/directories. + /// + public override void Run() + { + this.TestDirectoryPath = Path.Combine(Path.GetTempPath(), "NightEngineWriteTests"); + _ = Directory.CreateDirectory(this.TestDirectoryPath); // Ensure base test directory exists + this.TestFilePath = Path.Combine(this.TestDirectoryPath, Guid.NewGuid().ToString("N") + ".txt"); + + try + { + this.ExecuteWriteTestLogic(); + } + finally + { + if (File.Exists(this.TestFilePath)) + { + File.Delete(this.TestFilePath); + } + + // Attempt to clean up the directory if it's empty, otherwise leave it for other tests. + // This is a simple cleanup; more robust might be needed if tests run in parallel and create subdirs. + if (Directory.Exists(this.TestDirectoryPath) && !Directory.EnumerateFileSystemEntries(this.TestDirectoryPath).Any()) + { + try + { + Directory.Delete(this.TestDirectoryPath); + } + catch (IOException) + { + // Ignore if deletion fails, another test might still be using it or created something. + } + } + } + } + + /// + /// Contains the specific test logic for a Filesystem.Write test case. + /// This method is called by the method after setup and before teardown. + /// + protected abstract void ExecuteWriteTestLogic(); + } + + /// + /// Tests writing a basic string to a new file using Filesystem.Write. + /// + public class FilesystemWrite_String_BasicNewFileTest : BaseFilesystemWriteTest + { + /// + public override string Name => "Filesystem.Write.String.BasicNewFile"; + + /// + public override string Description => "Tests writing a string to a new file."; + + /// + public override string SuccessMessage => "Successfully wrote string to a new file and verified content."; + + /// + protected override void ExecuteWriteTestLogic() + { + string content = "Hello, Night Engine!"; + var (success, errorMessage) = Night.Filesystem.Write(this.TestFilePath, content); + + Assert.True(success, $"Write operation failed: {errorMessage}"); + Assert.Null(errorMessage); + Assert.True(File.Exists(this.TestFilePath), "Test file was not created."); + + string fileContent = File.ReadAllText(this.TestFilePath); + Assert.Equal(content, fileContent); + } + } + + /// + /// Tests writing a basic byte array to a new file using Filesystem.Write. + /// + public class FilesystemWrite_Bytes_BasicNewFileTest : BaseFilesystemWriteTest + { + /// + public override string Name => "Filesystem.Write.Bytes.BasicNewFile"; + + /// + public override string Description => "Tests writing a byte array to a new file."; + + /// + public override string SuccessMessage => "Successfully wrote byte array to a new file and verified content."; + + /// + protected override void ExecuteWriteTestLogic() + { + byte[] content = Encoding.UTF8.GetBytes("Hello, Bytes!"); + var (success, errorMessage) = Night.Filesystem.Write(this.TestFilePath, content); + + Assert.True(success, $"Write operation failed: {errorMessage}"); + Assert.Null(errorMessage); + Assert.True(File.Exists(this.TestFilePath), "Test file was not created."); + + byte[] fileContent = File.ReadAllBytes(this.TestFilePath); + Assert.Equal(content, fileContent); + } + } + + /// + /// Tests overwriting an existing file with string data using Filesystem.Write. + /// + public class FilesystemWrite_String_OverwriteExistingFileTest : BaseFilesystemWriteTest + { + /// + public override string Name => "Filesystem.Write.String.OverwriteExistingFile"; + + /// + public override string Description => "Tests overwriting an existing file with a string."; + + /// + public override string SuccessMessage => "Successfully overwrote existing file with string and verified content."; + + /// + protected override void ExecuteWriteTestLogic() + { + string initialContent = "Initial content."; + File.WriteAllText(this.TestFilePath, initialContent); // Pre-populate the file + + string newContent = "New overwritten content!"; + var (success, errorMessage) = Night.Filesystem.Write(this.TestFilePath, newContent); + + Assert.True(success, $"Write operation failed: {errorMessage}"); + Assert.Null(errorMessage); + + string fileContent = File.ReadAllText(this.TestFilePath); + Assert.Equal(newContent, fileContent); + } + } + + /// + /// Tests overwriting an existing file with byte array data using Filesystem.Write. + /// + public class FilesystemWrite_Bytes_OverwriteExistingFileTest : BaseFilesystemWriteTest + { + /// + public override string Name => "Filesystem.Write.Bytes.OverwriteExistingFile"; + + /// + public override string Description => "Tests overwriting an existing file with a byte array."; + + /// + public override string SuccessMessage => "Successfully overwrote existing file with byte array and verified content."; + + /// + protected override void ExecuteWriteTestLogic() + { + byte[] initialContent = Encoding.UTF8.GetBytes("Initial byte content."); + File.WriteAllBytes(this.TestFilePath, initialContent); // Pre-populate the file + + byte[] newContent = Encoding.UTF8.GetBytes("New overwritten byte content!"); + var (success, errorMessage) = Night.Filesystem.Write(this.TestFilePath, newContent); + + Assert.True(success, $"Write operation failed: {errorMessage}"); + Assert.Null(errorMessage); + + byte[] fileContent = File.ReadAllBytes(this.TestFilePath); + Assert.Equal(newContent, fileContent); + } + } + + /// + /// Tests writing a portion of a string to a file using the size parameter with Filesystem.Write. + /// + public class FilesystemWrite_String_WithSizeTest : BaseFilesystemWriteTest + { + /// + public override string Name => "Filesystem.Write.String.WithSize"; + + /// + public override string Description => "Tests writing a string with a specific size parameter."; + + /// + public override string SuccessMessage => "Successfully wrote partial string using size parameter and verified content."; + + /// + protected override void ExecuteWriteTestLogic() + { + string fullContent = "This is a full string."; // UTF-8: 22 bytes + byte[] fullContentBytes = Encoding.UTF8.GetBytes(fullContent); + long sizeToWrite = 10; // Write first 10 bytes + + var (success, errorMessage) = Night.Filesystem.Write(this.TestFilePath, fullContent, sizeToWrite); + + Assert.True(success, $"Write operation failed: {errorMessage}"); + Assert.Null(errorMessage); + Assert.True(File.Exists(this.TestFilePath), "Test file was not created."); + + byte[] fileBytes = File.ReadAllBytes(this.TestFilePath); + Assert.Equal(sizeToWrite, fileBytes.Length); + + string expectedWrittenString = Encoding.UTF8.GetString(fullContentBytes, 0, (int)sizeToWrite); + string actualWrittenString = Encoding.UTF8.GetString(fileBytes); + Assert.Equal(expectedWrittenString, actualWrittenString); + } + } + + /// + /// Tests writing a portion of a byte array to a file using the size parameter with Filesystem.Write. + /// + public class FilesystemWrite_Bytes_WithSizeTest : BaseFilesystemWriteTest + { + /// + public override string Name => "Filesystem.Write.Bytes.WithSize"; + + /// + public override string Description => "Tests writing a byte array with a specific size parameter."; + + /// + public override string SuccessMessage => "Successfully wrote partial byte array using size parameter and verified content."; + + /// + protected override void ExecuteWriteTestLogic() + { + byte[] fullContent = Encoding.UTF8.GetBytes("This is a full byte array."); + long sizeToWrite = 12; + + var (success, errorMessage) = Night.Filesystem.Write(this.TestFilePath, fullContent, sizeToWrite); + + Assert.True(success, $"Write operation failed: {errorMessage}"); + Assert.Null(errorMessage); + Assert.True(File.Exists(this.TestFilePath), "Test file was not created."); + + byte[] fileBytes = File.ReadAllBytes(this.TestFilePath); + Assert.Equal(sizeToWrite, fileBytes.Length); + Assert.Equal(fullContent.Take((int)sizeToWrite).ToArray(), fileBytes); + } + } + + /// + /// Tests writing a string where the specified size is larger than the actual string data. + /// Expects the entire string to be written. + /// + public class FilesystemWrite_String_SizeLargerThanDataTest : BaseFilesystemWriteTest + { + /// + public override string Name => "Filesystem.Write.String.SizeLargerThanData"; + + /// + public override string Description => "Tests writing a string with size larger than actual data."; + + /// + public override string SuccessMessage => "Successfully wrote full string when size was larger and verified content."; + + /// + protected override void ExecuteWriteTestLogic() + { + string content = "Short string."; + long sizeToWrite = 100; // Larger than content + + var (success, errorMessage) = Night.Filesystem.Write(this.TestFilePath, content, sizeToWrite); + + Assert.True(success, $"Write operation failed: {errorMessage}"); + Assert.Null(errorMessage); + + string fileContent = File.ReadAllText(this.TestFilePath); + Assert.Equal(content, fileContent); + } + } + + /// + /// Tests writing a byte array where the specified size is larger than the actual byte array data. + /// Expects the entire byte array to be written. + /// + public class FilesystemWrite_Bytes_SizeLargerThanDataTest : BaseFilesystemWriteTest + { + /// + public override string Name => "Filesystem.Write.Bytes.SizeLargerThanData"; + + /// + public override string Description => "Tests writing a byte array with size larger than actual data."; + + /// + public override string SuccessMessage => "Successfully wrote full byte array when size was larger and verified content."; + + /// + protected override void ExecuteWriteTestLogic() + { + byte[] content = Encoding.UTF8.GetBytes("Short bytes."); + long sizeToWrite = 200; // Larger than content + + var (success, errorMessage) = Night.Filesystem.Write(this.TestFilePath, content, sizeToWrite); + + Assert.True(success, $"Write operation failed: {errorMessage}"); + Assert.Null(errorMessage); + + byte[] fileContent = File.ReadAllBytes(this.TestFilePath); + Assert.Equal(content, fileContent); + } + } + + /// + /// Tests writing an empty string to a file using Filesystem.Write. + /// Expects an empty file to be created. + /// + public class FilesystemWrite_String_EmptyStringTest : BaseFilesystemWriteTest + { + /// + public override string Name => "Filesystem.Write.String.EmptyString"; + + /// + public override string Description => "Tests writing an empty string."; + + /// + public override string SuccessMessage => "Successfully wrote an empty string, resulting in an empty file."; + + /// + protected override void ExecuteWriteTestLogic() + { + var (success, errorMessage) = Night.Filesystem.Write(this.TestFilePath, string.Empty); + + Assert.True(success, $"Write operation failed: {errorMessage}"); + Assert.Null(errorMessage); + Assert.True(File.Exists(this.TestFilePath), "Test file was not created for empty string write."); + + string fileContent = File.ReadAllText(this.TestFilePath); + Assert.Empty(fileContent); + } + } + + /// + /// Tests writing an empty byte array to a file using Filesystem.Write. + /// Expects an empty file to be created. + /// + public class FilesystemWrite_Bytes_EmptyArrayTest : BaseFilesystemWriteTest + { + /// + public override string Name => "Filesystem.Write.Bytes.EmptyArray"; + + /// + public override string Description => "Tests writing an empty byte array."; + + /// + public override string SuccessMessage => "Successfully wrote an empty byte array, resulting in an empty file."; + + /// + protected override void ExecuteWriteTestLogic() + { + var (success, errorMessage) = Night.Filesystem.Write(this.TestFilePath, Array.Empty()); + + Assert.True(success, $"Write operation failed: {errorMessage}"); + Assert.Null(errorMessage); + Assert.True(File.Exists(this.TestFilePath), "Test file was not created for empty byte array write."); + + byte[] fileContent = File.ReadAllBytes(this.TestFilePath); + Assert.Empty(fileContent); + } + } + + /// + /// Tests writing a string with the size parameter explicitly set to 0. + /// Expects an empty file to be created. + /// + public class FilesystemWrite_String_ZeroSizeTest : BaseFilesystemWriteTest + { + /// + public override string Name => "Filesystem.Write.String.ZeroSize"; + + /// + public override string Description => "Tests writing a string with size parameter set to 0."; + + /// + public override string SuccessMessage => "Successfully wrote string with size 0, resulting in an empty file."; + + /// + protected override void ExecuteWriteTestLogic() + { + var (success, errorMessage) = Night.Filesystem.Write(this.TestFilePath, "Some data", 0); + + Assert.True(success, $"Write operation failed: {errorMessage}"); + Assert.Null(errorMessage); + Assert.True(File.Exists(this.TestFilePath), "Test file was not created for zero size string write."); + + string fileContent = File.ReadAllText(this.TestFilePath); + Assert.Empty(fileContent); + } + } + + /// + /// Tests writing a byte array with the size parameter explicitly set to 0. + /// Expects an empty file to be created. + /// + public class FilesystemWrite_Bytes_ZeroSizeTest : BaseFilesystemWriteTest + { + /// + public override string Name => "Filesystem.Write.Bytes.ZeroSize"; + + /// + public override string Description => "Tests writing a byte array with size parameter set to 0."; + + /// + public override string SuccessMessage => "Successfully wrote byte array with size 0, resulting in an empty file."; + + /// + protected override void ExecuteWriteTestLogic() + { + var (success, errorMessage) = Night.Filesystem.Write(this.TestFilePath, Encoding.UTF8.GetBytes("Some data"), 0); + + Assert.True(success, $"Write operation failed: {errorMessage}"); + Assert.Null(errorMessage); + Assert.True(File.Exists(this.TestFilePath), "Test file was not created for zero size byte array write."); + + byte[] fileContent = File.ReadAllBytes(this.TestFilePath); + Assert.Empty(fileContent); + } + } +} diff --git a/tests/Groups/Filesystem/WriteTests2.cs b/tests/Groups/Filesystem/WriteTests2.cs new file mode 100644 index 00000000..04ff9202 --- /dev/null +++ b/tests/Groups/Filesystem/WriteTests2.cs @@ -0,0 +1,470 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +using Night; +using Night.Log; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Filesystem +{ + /// + /// Tests argument validation for the Filesystem.Write methods, + /// ensuring they fail correctly with invalid inputs. + /// + public class FilesystemWrite_ArgumentValidationTest : ModTestCase + { + /// + public override string Name => "Filesystem.Write.ArgumentValidation"; + + /// + public override string Description => "Tests argument validation for Filesystem.Write."; + + /// + public override string SuccessMessage => "Argument validation for Filesystem.Write behaves as expected."; + + /// + public override void Run() + { + string validPath = Path.Combine(Path.GetTempPath(), "NightEngineWriteTests", "valid.txt"); + _ = Directory.CreateDirectory(Path.GetDirectoryName(validPath)!); // Ensure dir exists + + // Null name + var (success, errorMessage) = Night.Filesystem.Write(null!, "data"); + Assert.False(success); + Assert.Equal("File name cannot be null or empty.", errorMessage); + + (success, errorMessage) = Night.Filesystem.Write(null!, Array.Empty()); + Assert.False(success); + Assert.Equal("File name cannot be null or empty.", errorMessage); + + // Empty name + (success, errorMessage) = Night.Filesystem.Write(string.Empty, "data"); + Assert.False(success); + Assert.Equal("File name cannot be null or empty.", errorMessage); + + (success, errorMessage) = Night.Filesystem.Write(string.Empty, Array.Empty()); + Assert.False(success); + Assert.Equal("File name cannot be null or empty.", errorMessage); + + // Null string data + (success, errorMessage) = Night.Filesystem.Write(validPath, (string)null!); + Assert.False(success); + Assert.Equal("Data to write cannot be null.", errorMessage); + + // Null byte data + (success, errorMessage) = Night.Filesystem.Write(validPath, (byte[])null!); + Assert.False(success); + Assert.Equal("Data to write cannot be null.", errorMessage); + + // Negative size + (success, errorMessage) = Night.Filesystem.Write(validPath, "data", -1); + Assert.False(success); + Assert.Equal("Size to write cannot be negative.", errorMessage); + + (success, errorMessage) = Night.Filesystem.Write(validPath, Encoding.UTF8.GetBytes("data"), -1); + Assert.False(success); + Assert.Equal("Size to write cannot be negative.", errorMessage); + + if (File.Exists(validPath)) + { + File.Delete(validPath); + } + + if (Directory.Exists(Path.GetDirectoryName(validPath)) && !Directory.EnumerateFileSystemEntries(Path.GetDirectoryName(validPath)!).Any()) + { + try + { + Directory.Delete(Path.GetDirectoryName(validPath)!); + } + catch + { /* ignore */ + } + } + } + } + + /// + /// Tests that Filesystem.Write automatically creates non-existent parent directories + /// when writing a file. + /// + public class FilesystemWrite_CreateDirectoryTest : BaseFilesystemWriteTest + { + /// + public override string Name => "Filesystem.Write.CreateDirectory"; + + /// + public override string Description => "Tests that Write creates non-existent parent directories."; + + /// + public override string SuccessMessage => "Successfully wrote file and created parent directory."; + + /// + protected override void ExecuteWriteTestLogic() + { + // Override TestFilePath to include a subdirectory that doesn't exist yet + string subDir = Guid.NewGuid().ToString("N"); + this.TestFilePath = Path.Combine(this.TestDirectoryPath, subDir, "file_in_subdir.txt"); + string parentOfTestFile = Path.GetDirectoryName(this.TestFilePath)!; + + Assert.False(Directory.Exists(parentOfTestFile), "Subdirectory should not exist before write."); + + string content = "Content in a new subdirectory."; + var (success, errorMessage) = Night.Filesystem.Write(this.TestFilePath, content); + + Assert.True(success, $"Write operation failed: {errorMessage}"); + Assert.Null(errorMessage); + Assert.True(Directory.Exists(parentOfTestFile), "Parent directory was not created."); + Assert.True(File.Exists(this.TestFilePath), "Test file was not created in subdirectory."); + + string fileContent = File.ReadAllText(this.TestFilePath); + Assert.Equal(content, fileContent); + + // Cleanup the created subdirectory + if (Directory.Exists(parentOfTestFile)) + { + Directory.Delete(parentOfTestFile, true); // Recursive delete for this specific test's subdir + } + } + } + + /// + /// Tests that Filesystem.Write fails correctly when attempting to write to a path + /// that is an existing directory. + /// + public class FilesystemWrite_PathIsDirectoryTest : BaseFilesystemWriteTest + { + /// + public override string Name => "Filesystem.Write.PathIsDirectory"; + + /// + public override string Description => "Tests writing to a path that is an existing directory."; + + /// + public override string SuccessMessage => "Write operation correctly failed when path is a directory."; + + /// + protected override void ExecuteWriteTestLogic() + { + // TestFilePath from BaseFilesystemWriteTest is a file path. + // For this test, we want 'name' to be a directory. + string directoryAsFilePath = Path.Combine(this.TestDirectoryPath, "existing_dir_as_file"); + _ = Directory.CreateDirectory(directoryAsFilePath); // Create it as a directory + + var (success, errorMessage) = Night.Filesystem.Write(directoryAsFilePath, "some data"); + + Assert.False(success, "Write operation should have failed for a directory path."); + Assert.NotNull(errorMessage); + + // Exact error message can be OS-dependent for "is a directory" or "access denied" + // For .NET FileStream, it's typically UnauthorizedAccessException + Assert.Contains("Unauthorized access", errorMessage, StringComparison.OrdinalIgnoreCase); + + // Cleanup + if (Directory.Exists(directoryAsFilePath)) + { + Directory.Delete(directoryAsFilePath); + } + } + } + + /// + /// Tests that Filesystem.Write fails correctly when the path contains invalid characters. + /// This should trigger an ArgumentException from the underlying FileStream. + /// + public class FilesystemWrite_Error_InvalidArgumentCharsTest : ModTestCase + { + /// + public override string Name => "Filesystem.Write.Error.InvalidArgumentChars"; + + /// + public override string Description => "Tests Write with a path containing invalid characters."; + + /// + public override string SuccessMessage => "Write operation correctly failed due to invalid characters in path."; + + /// + public override void Run() + { + string invalidFileName; + string expectedErrorSubstring; + string problematicCharDisplay; + + if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) + { + invalidFileName = "file_with_pipe|.txt"; + + // On Windows, '|' in filename leads to IOException, wrapped as "IO error:". + expectedErrorSubstring = "IO error"; + problematicCharDisplay = "|"; + } + else + { + // Use a null character, which should cause ArgumentException from FileStream. + invalidFileName = "file_with_null\0char.txt"; + + // Night.Filesystem.Write wraps ArgumentException as "Argument error:". + expectedErrorSubstring = "Argument error"; + problematicCharDisplay = "\\0"; + } + + // Define the base directory for test files to ensure it exists. + // Path.GetDirectoryName will correctly extract this base directory even if invalidFileName contains problematic characters. + string testFilesBaseDir = Path.Combine(Path.GetTempPath(), "NightEngineWriteTests"); + _ = Directory.CreateDirectory(testFilesBaseDir); + + string invalidPath = Path.Combine(testFilesBaseDir, invalidFileName); + + var (success, errorMessage) = Night.Filesystem.Write(invalidPath, "some data"); + + Assert.False(success, $"Write operation should have failed. OS: {System.Runtime.InteropServices.RuntimeInformation.OSDescription}, Char: '{problematicCharDisplay}', Path: {invalidPath}"); + Assert.NotNull(errorMessage); // Ensure there is an error message. + Assert.Contains(expectedErrorSubstring, errorMessage, StringComparison.OrdinalIgnoreCase); + + // Cleanup: Attempt to delete the directory if it was created and is empty. + // The invalid file itself wouldn't have been created. + string? dirName = Path.GetDirectoryName(invalidPath); + if (dirName != null && Directory.Exists(dirName) && !Directory.EnumerateFileSystemEntries(dirName).Any()) + { + try + { + Directory.Delete(dirName); + } + catch (IOException) + { /* Ignore cleanup errors */ + } + } + else if (dirName != null && Directory.Exists(dirName) && Directory.GetFiles(dirName).Length == 0 && Directory.GetDirectories(dirName).Length == 0) + { + try + { + Directory.Delete(dirName); + } + catch (IOException) + { /* Ignore cleanup errors */ + } + } + } + } + + /// + /// Tests that Filesystem.Write fails correctly when the path is too long. + /// + public class FilesystemWrite_Error_PathTooLongTest : ModTestCase + { + /// + public override string Name => "Filesystem.Write.Error.PathTooLong"; + + /// + public override string Description => "Tests Write with a path that exceeds system limits."; + + /// + public override string SuccessMessage => "Write operation correctly failed due to path being too long."; + + /// + public override void Run() + { + string baseDir = Path.Combine(Path.GetTempPath(), "NightEngineWriteTests"); + _ = Directory.CreateDirectory(baseDir); + + // Create a very long path. Max path is often around 260, but varies. + // Let's aim for something definitely too long. + string excessivelyLongName = new string('a', 300); + string longPath = Path.Combine(baseDir, excessivelyLongName, excessivelyLongName, $"{excessivelyLongName}.txt"); + + var (success, errorMessage) = Night.Filesystem.Write(longPath, "some data"); + + Assert.False(success, "Write operation should have failed for a path that is too long."); + Assert.NotNull(errorMessage); + Assert.Equal("The specified path, file name, or both exceed the system-defined maximum length.", errorMessage); + + // Cleanup + if (Directory.Exists(baseDir)) + { + try + { + Directory.Delete(baseDir, true); + } + catch (Exception) + { /* Ignore cleanup errors for problematic long paths */ + } + } + } + } + + /// + /// Tests that Filesystem.Write fails correctly when the path refers to an unmapped drive. + /// + public class FilesystemWrite_Error_DirectoryNotFoundUnmappedDriveTest : ModTestCase + { + /// + public override string Name => "Filesystem.Write.Error.DirectoryNotFoundUnmappedDrive"; + + /// + public override string Description => "Tests Write to an unmapped drive."; + + /// + public override string SuccessMessage => "Write operation correctly failed for an unmapped drive."; + + /// + public override void Run() + { + // Assume Z: is an unmapped drive. This is a common convention for such tests. + string unmappedPath = @"Z:\non_existent_dir_on_unmapped_drive\file.txt"; + + var (success, errorMessage) = Night.Filesystem.Write(unmappedPath, "some data"); + + Assert.False(success, "Write operation should have failed for an unmapped drive."); + Assert.NotNull(errorMessage); + Assert.Equal("The specified path is invalid (for example, it is on an unmapped drive).", errorMessage); + } + } + + /// + /// Tests that Filesystem.Write fails correctly due to an IOException (e.g., file locked). + /// + public class FilesystemWrite_Error_IOExceptionLockedTest : BaseFilesystemWriteTest + { + /// + public override string Name => "Filesystem.Write.Error.IOExceptionLockedTest"; + + /// + public override string Description => "Tests Write to a file that is locked."; + + /// + public override string SuccessMessage => "Write operation correctly failed due to IOException (file locked)."; + + /// + protected override void ExecuteWriteTestLogic() + { + // Create and lock the file + using (global::System.IO.FileStream lockStream = new global::System.IO.FileStream(this.TestFilePath, global::System.IO.FileMode.Create, global::System.IO.FileAccess.ReadWrite, global::System.IO.FileShare.None)) + { + lockStream.Write(Encoding.UTF8.GetBytes("locked content"), 0, 14); + lockStream.Flush(); + + var (success, errorMessage) = Night.Filesystem.Write(this.TestFilePath, "attempt to overwrite locked file"); + + Assert.False(success, "Write operation should have failed because the file is locked."); + Assert.NotNull(errorMessage); + Assert.StartsWith("IO error:", errorMessage, StringComparison.OrdinalIgnoreCase); + bool accessConflictMessage = errorMessage.Contains("being used by another process", StringComparison.OrdinalIgnoreCase) || + errorMessage.Contains("access to the path", StringComparison.OrdinalIgnoreCase); + Assert.True(accessConflictMessage, $"Expected an access conflict message, but got: {errorMessage}"); + } + } + } + + /// + /// Tests that Filesystem.Write fails correctly due to a SecurityException. + /// Triggering this reliably is hard; this attempts a common scenario but may be environment-dependent. + /// + public class FilesystemWrite_Error_SecurityExceptionTest : ModTestCase + { + private static readonly ILogger Logger = LogManager.GetLogger(nameof(FilesystemWrite_Error_SecurityExceptionTest)); + + /// + public override string Name => "Filesystem.Write.Error.SecurityException"; + + /// + public override string Description => "Tests Write when a SecurityException occurs (best effort)."; + + /// + public override string SuccessMessage => "Write operation correctly failed with a security error."; + + /// + public override void Run() + { + string potentiallyRestrictedPath = @"\\.\GLOBALROOT\Device\Null\ProtectedFile.txt"; + + var (success, errorMessage) = Night.Filesystem.Write(potentiallyRestrictedPath, "some data"); + + Assert.False(success, "Write operation should have failed."); + Assert.NotNull(errorMessage); + + bool isExpectedSecurityError = errorMessage == "A security error occurred."; + bool isUnauthorized = errorMessage.StartsWith("Unauthorized access", StringComparison.OrdinalIgnoreCase); + bool isIOError = errorMessage.StartsWith("IO error", StringComparison.OrdinalIgnoreCase); + bool isNotSupported = errorMessage.StartsWith("Operation not supported", StringComparison.OrdinalIgnoreCase); + + Assert.True( + isExpectedSecurityError || isUnauthorized || isIOError || isNotSupported, + $"Expected a security-related error, UnauthorizedAccess, IOException or NotSupportedException, but got: {errorMessage}"); + + if (isExpectedSecurityError) + { + Logger.Info("SecurityException test successfully triggered the specific SecurityException handler."); + } + else + { + Logger.Warn($"SecurityException test did not trigger the specific SecurityException handler with path '{potentiallyRestrictedPath}'. It triggered: {errorMessage}"); + } + } + } + + /// + /// Tests that Filesystem.Write fails correctly when the path is not supported (e.g., a device). + /// + public class FilesystemWrite_Error_NotSupportedTest : ModTestCase + { + private static readonly ILogger Logger = LogManager.GetLogger(nameof(FilesystemWrite_Error_NotSupportedTest)); + + /// + public override string Name => "Filesystem.Write.Error.NotSupported"; + + /// + public override string Description => "Tests Write with a path that is not supported (e.g., CON)."; + + /// + public override string SuccessMessage => "Write operation correctly failed due to an unsupported path."; + + /// + public override void Run() + { + string devicePath; + if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) + { + devicePath = "CON"; // Console output, not a regular file + } + else + { + Logger.Info("Skipping NotSupportedException test for non-Windows platform with 'CON' as it's Windows-specific. Awaiting a better cross-platform unsupported path example."); + Assert.True(true, "Test skipped on non-Windows for 'CON' device path."); + return; + } + + var (success, errorMessage) = Night.Filesystem.Write(devicePath, "some data"); + + Assert.False(success, "Write operation should have failed for an unsupported path."); + Assert.NotNull(errorMessage); + Assert.StartsWith("Operation not supported", errorMessage, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/tests/Groups/Framework/CLITests.cs b/tests/Groups/Framework/CLITests.cs new file mode 100644 index 00000000..085f483e --- /dev/null +++ b/tests/Groups/Framework/CLITests.cs @@ -0,0 +1,520 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; // Required for Path.Combine, AppContext.BaseDirectory +using System.Linq; + +using Night; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Framework +{ + /// + /// Tests for the CLI constructor focusing on default values with null or empty arguments. + /// + public class NightCLI_Constructor_DefaultValuesTest : ModTestCase + { + /// + public override string Name => "CLI.Constructor.DefaultValues"; + + /// + public override string Description => "Tests CLI constructor with null and empty arguments, ensuring default property values."; + + /// + public override string SuccessMessage => "CLI constructor default values verified successfully."; + + /// + public override void Run() + { + // Arrange & Act: Null arguments + var cliNull = new Night.CLI(null!); // Test with null, suppress warning as it's a test case + + // Assert: Null arguments + Assert.False(cliNull.IsSilentMode, "IsSilentMode should be false for null args."); + Assert.Null(cliNull.ParsedLogLevel); + Assert.False(cliNull.IsDebugMode, "IsDebugMode should be false for null args."); + Assert.False(cliNull.EnableSessionLog, "EnableSessionLog should be false for null args."); + Assert.Empty(cliNull.RemainingArgs); + + // Arrange & Act: Empty arguments + var cliEmpty = new Night.CLI(Array.Empty()); + + // Assert: Empty arguments + Assert.False(cliEmpty.IsSilentMode, "IsSilentMode should be false for empty args."); + Assert.Null(cliEmpty.ParsedLogLevel); + Assert.False(cliEmpty.IsDebugMode, "IsDebugMode should be false for empty args."); + Assert.False(cliEmpty.EnableSessionLog, "EnableSessionLog should be false for empty args."); + Assert.Empty(cliEmpty.RemainingArgs); + } + } + + /// + /// Tests for the CLI constructor focusing on silent mode flags (-s, --silent). + /// + public class NightCLI_Constructor_SilentModeTest : ModTestCase + { + /// + public override string Name => "CLI.Constructor.SilentMode"; + + /// + public override string Description => "Tests CLI constructor for -s and --silent flags."; + + /// + public override string SuccessMessage => "CLI silent mode flags parsed correctly."; + + /// + public override void Run() + { + // Arrange & Act: -s + var cliShort = new Night.CLI(new[] { "-s" }); + + // Assert: -s + Assert.True(cliShort.IsSilentMode, "IsSilentMode should be true for '-s'."); + + // Arrange & Act: --silent + var cliLong = new Night.CLI(new[] { "--silent" }); + + // Assert: --silent + Assert.True(cliLong.IsSilentMode, "IsSilentMode should be true for '--silent'."); + + // Arrange & Act: Case insensitivity + var cliCaps = new Night.CLI(new[] { "-S" }); + + // Assert: Case insensitivity + Assert.True(cliCaps.IsSilentMode, "IsSilentMode should be true for '-S' (case-insensitive)."); + } + } + + /// + /// Tests for the CLI constructor focusing on the --log-level argument. + /// + public class NightCLI_Constructor_LogLevelTest : ModTestCase + { + /// + public override string Name => "CLI.Constructor.LogLevel"; + + /// + public override string Description => "Tests CLI constructor for --log-level with valid, invalid, and missing values."; + + /// + public override string SuccessMessage => "--log-level argument parsing verified."; + + /// + public override void Run() + { + // Arrange & Act: Valid log level + var cliValid = new Night.CLI(new[] { "--log-level", "Debug" }); + + // Assert: Valid log level + Assert.Equal(LogLevel.Debug, cliValid.ParsedLogLevel); + Assert.Empty(cliValid.RemainingArgs); + + // Arrange & Act: Valid log level (case-insensitive) + var cliValidCase = new Night.CLI(new[] { "--log-level", "wArNiNg" }); + + // Assert: Valid log level (case-insensitive) + Assert.Equal(LogLevel.Warning, cliValidCase.ParsedLogLevel); + Assert.Empty(cliValidCase.RemainingArgs); + + // Arrange & Act: Invalid log level + var cliInvalid = new Night.CLI(new[] { "--log-level", "InvalidValue" }); + + // Assert: Invalid log level + Assert.Null(cliInvalid.ParsedLogLevel); + Assert.Equal(2, cliInvalid.RemainingArgs.Count); + Assert.Contains("--log-level", cliInvalid.RemainingArgs); + Assert.Contains("InvalidValue", cliInvalid.RemainingArgs); + + // Arrange & Act: Missing log level value + var cliMissingValue = new Night.CLI(new[] { "--log-level" }); + + // Assert: Missing log level value + Assert.Null(cliMissingValue.ParsedLogLevel); + _ = Assert.Single(cliMissingValue.RemainingArgs); + Assert.Contains("--log-level", cliMissingValue.RemainingArgs); + + // Arrange & Act: --log-level followed by another option (missing value) + var cliMissingValueFollowedByOpt = new Night.CLI(new[] { "--log-level", "--debug" }); + + // Assert: --log-level followed by another option + Assert.Null(cliMissingValueFollowedByOpt.ParsedLogLevel); + Assert.False(cliMissingValueFollowedByOpt.IsDebugMode, "--debug should not be parsed as IsDebugMode when it's consumed by --log-level."); + Assert.Equal(2, cliMissingValueFollowedByOpt.RemainingArgs.Count); // Both --log-level and --debug should be in remaining args + Assert.Contains("--log-level", cliMissingValueFollowedByOpt.RemainingArgs); + Assert.Contains("--debug", cliMissingValueFollowedByOpt.RemainingArgs); // --debug is treated as the invalid value for --log-level + } + } + + /// + /// Tests for the CLI constructor focusing on the --debug flag. + /// + public class NightCLI_Constructor_DebugModeTest : ModTestCase + { + /// + public override string Name => "CLI.Constructor.DebugMode"; + + /// + public override string Description => "Tests CLI constructor for --debug flag."; + + /// + public override string SuccessMessage => "CLI debug mode flag parsed correctly."; + + /// + public override void Run() + { + // Arrange & Act + var cli = new Night.CLI(new[] { "--debug" }); + + // Assert + Assert.True(cli.IsDebugMode, "IsDebugMode should be true for '--debug'."); + + // Arrange & Act: Case insensitivity + var cliCaps = new Night.CLI(new[] { "--DEBUG" }); + + // Assert: Case insensitivity + Assert.True(cliCaps.IsDebugMode, "IsDebugMode should be true for '--DEBUG' (case-insensitive)."); + } + } + + /// + /// Tests for the CLI constructor focusing on the --session-log flag. + /// + public class NightCLI_Constructor_SessionLogTest : ModTestCase + { + /// + public override string Name => "CLI.Constructor.SessionLog"; + + /// + public override string Description => "Tests CLI constructor for --session-log flag."; + + /// + public override string SuccessMessage => "CLI session log flag parsed correctly."; + + /// + public override void Run() + { + // Arrange & Act + var cli = new Night.CLI(new[] { "--session-log" }); + + // Assert + Assert.True(cli.EnableSessionLog, "EnableSessionLog should be true for '--session-log'."); + } + } + + /// + /// Tests for the CLI constructor focusing on how unrecognized arguments are handled. + /// + public class NightCLI_Constructor_RemainingArgsTest : ModTestCase + { + /// + public override string Name => "CLI.Constructor.RemainingArgs"; + + /// + public override string Description => "Tests CLI constructor for handling of unrecognized arguments."; + + /// + public override string SuccessMessage => "CLI remaining arguments handled correctly."; + + /// + public override void Run() + { + // Arrange & Act + var cli = new Night.CLI(new[] { "arg1", "--unknown", "value", "-x" }); + + // Assert + Assert.Equal(4, cli.RemainingArgs.Count); + Assert.Contains("arg1", cli.RemainingArgs); + Assert.Contains("--unknown", cli.RemainingArgs); + Assert.Contains("value", cli.RemainingArgs); + Assert.Contains("-x", cli.RemainingArgs); + } + } + + /// + /// Tests for the CLI constructor focusing on parsing a combination of different arguments. + /// + public class NightCLI_Constructor_CombinedArgsTest : ModTestCase + { + /// + public override string Name => "CLI.Constructor.CombinedArgs"; + + /// + public override string Description => "Tests CLI constructor with a combination of arguments."; + + /// + public override string SuccessMessage => "CLI combined arguments parsed correctly."; + + /// + public override void Run() + { + // Arrange & Act + var cli = new Night.CLI(new[] { "-s", "--log-level", "Error", "remaining1", "--debug", "remaining2", "--session-log" }); + + // Assert + Assert.True(cli.IsSilentMode); + Assert.Equal(LogLevel.Error, cli.ParsedLogLevel); + Assert.True(cli.IsDebugMode); + Assert.True(cli.EnableSessionLog); + Assert.Equal(2, cli.RemainingArgs.Count); + Assert.Contains("remaining1", cli.RemainingArgs); + Assert.Contains("remaining2", cli.RemainingArgs); + } + } + + /// + /// Tests for the CLI ApplySettings method focusing on how it affects LogManager.MinLevel. + /// + public class NightCLI_ApplySettings_LogLevelTest : ModTestCase + { + /// + public override string Name => "CLI.ApplySettings.LogLevel"; + + /// + public override string Description => "Tests CLI ApplySettings method for LogLevel changes."; + + /// + public override string SuccessMessage => "CLI ApplySettings correctly updated LogManager.MinLevel."; + + /// + public override void Run() + { + LogLevel originalMinLevel = LogManager.MinLevel; + try + { + // Arrange: CLI with specified log level + var cli = new Night.CLI(new[] { "--log-level", "Warning" }); + Assert.Equal(LogLevel.Warning, cli.ParsedLogLevel); // Pre-condition + + // Act + cli.ApplySettings(); + + // Assert + Assert.Equal(LogLevel.Warning, LogManager.MinLevel); + + // Arrange: CLI with no log level (should not change LogManager.MinLevel from its current state) + LogManager.MinLevel = LogLevel.Fatal; // Set a known state + var cliNoLevel = new Night.CLI(Array.Empty()); + + // Act + cliNoLevel.ApplySettings(); + + // Assert + Assert.Equal(LogLevel.Fatal, LogManager.MinLevel); // Should remain Fatal + } + finally + { + LogManager.MinLevel = originalMinLevel; // Restore original + } + } + } + + /// + /// Tests for the CLI ApplySettings method focusing on the effects of debug mode. + /// + public class NightCLI_ApplySettings_DebugModeTest : ModTestCase + { + /// + public override string Name => "CLI.ApplySettings.DebugMode"; + + /// + public override string Description => "Tests CLI ApplySettings method for debug mode effects."; + + /// + public override string SuccessMessage => "CLI ApplySettings correctly handled debug mode."; + + /// + public override void Run() + { + LogLevel originalMinLevel = LogManager.MinLevel; + bool originalConsoleSinkState = LogManager.IsSystemConsoleSinkEnabled(); + try + { + // Arrange: CLI with debug mode + var cli = new Night.CLI(new[] { "--debug" }); + Assert.True(cli.IsDebugMode); // Pre-condition + LogManager.MinLevel = LogLevel.Information; // Set a non-debug level + LogManager.EnableSystemConsoleSink(false); // Ensure console sink is off + + // Act + cli.ApplySettings(); + + // Assert + Assert.Equal(LogLevel.Debug, LogManager.MinLevel); + Assert.True(LogManager.IsSystemConsoleSinkEnabled(), "Console sink should be enabled in debug mode."); + } + finally + { + LogManager.MinLevel = originalMinLevel; + LogManager.EnableSystemConsoleSink(originalConsoleSinkState); // Restore original console sink state + } + } + } + + /// + /// Tests that the CLI ApplySettings method does not crash when session logging is enabled + /// and that it attempts to configure the session log. + /// + public class NightCLI_ApplySettings_SessionLogNoCrashTest : ModTestCase + { + /// + public override string Name => "CLI.ApplySettings.SessionLogNoCrash"; + + /// + public override string Description => "Tests that CLI ApplySettings with session log enabled does not crash and attempts configuration."; + + /// + public override string SuccessMessage => "CLI ApplySettings with session log ran without crashing."; + + // We can't easily check if the file sink was added without exposing LogManager.Sinks or FileSink details + // So this test primarily ensures no crash and that DisableFileSink can be called. + + /// + public override void Run() + { + LogLevel originalMinLevel = LogManager.MinLevel; + bool originalConsoleSinkState = LogManager.IsSystemConsoleSinkEnabled(); + + // Ensure no file sink from previous tests or other sources for a clean test environment. + // It's important that DisableFileSink() correctly nullifies and removes the sink. + LogManager.DisableFileSink(); + + string expectedSessionPath = Path.Combine(AppContext.BaseDirectory ?? ".", "session"); + bool sessionDirExistedInitially = Directory.Exists(expectedSessionPath); + + try + { + // Arrange: CLI with session log enabled + var cli = new Night.CLI(new[] { "--session-log" }); + Assert.True(cli.EnableSessionLog); // Pre-condition + + // Act: Apply settings. This will attempt to create a session log. + Exception? ex = null; + try + { + cli.ApplySettings(); // This should create "session" dir and a log file in it. + } + catch (Exception e) + { + ex = e; + } + + // Assert: No crash during ApplySettings + Assert.Null(ex); + + // Assert: Check if the "session" directory was created. + Assert.True(Directory.Exists(expectedSessionPath), "Session directory should be created by ApplySettings."); + } + finally + { + LogManager.MinLevel = originalMinLevel; + LogManager.EnableSystemConsoleSink(originalConsoleSinkState); + LogManager.DisableFileSink(); // Ensure file sink is disabled after test + + // Cleanup: Remove the created session directory and its contents + // only if it was created by this specific test run. + if (Directory.Exists(expectedSessionPath) && !sessionDirExistedInitially) + { + try + { + Directory.Delete(expectedSessionPath, true); + } + catch + { /* ignored, best effort */ + } + } + + // If it existed before and we are not supposed to touch it, we leave it. + // If it was created and delete failed, it might interfere with subsequent local runs, + // but CI environments are usually clean. + } + } + } + + /// + /// Tests that the CLI ApplySettings method handles remaining or invalid arguments + /// without crashing. Console output for warnings is not tested. + /// + public class NightCLI_ApplySettings_RemainingArgsWarningTest : ModTestCase + { + /// + public override string Name => "CLI.ApplySettings.RemainingArgsWarning"; + + /// + public override string Description => "Tests that ApplySettings handles remaining/invalid args (no crash, console output not tested)."; + + /// + public override string SuccessMessage => "ApplySettings handled remaining/invalid args without crash."; + + /// + public override void Run() + { + LogLevel originalMinLevel = LogManager.MinLevel; // Save original state + try + { + // Arrange: CLI with remaining args including a bad --log-level + var cli = new Night.CLI(new[] { "--log-level", "InvalidLevel", "otherArg" }); + Assert.Contains("InvalidLevel", cli.RemainingArgs); + Assert.Contains("--log-level", cli.RemainingArgs); + Assert.Contains("otherArg", cli.RemainingArgs); + + // Act & Assert: Ensure ApplySettings doesn't crash + // We are not capturing console output here as per simplicity. + Exception? ex = null; + try + { + cli.ApplySettings(); + } + catch (Exception e) + { + ex = e; + } + + Assert.Null(ex); + + // Arrange: CLI with --log-level missing its value + var cli2 = new Night.CLI(new[] { "--log-level" }); + Assert.Contains("--log-level", cli2.RemainingArgs); + + // Act & Assert + ex = null; + try + { + cli2.ApplySettings(); + } + catch (Exception e) + { + ex = e; + } + + Assert.Null(ex); + } + finally + { + LogManager.MinLevel = originalMinLevel; // Restore original state + } + } + } +} diff --git a/tests/Groups/Framework/FrameworkGetVersionTest.cs b/tests/Groups/Framework/FrameworkGetVersionTest.cs new file mode 100644 index 00000000..011cc7c3 --- /dev/null +++ b/tests/Groups/Framework/FrameworkGetVersionTest.cs @@ -0,0 +1,60 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using Night; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Framework +{ + /// + /// Tests the method. + /// + public class Framework_GetVersionTest : ModTestCase + { + /// + public override string Name => "Framework.GetVersion"; + + /// + public override string Description => "Tests that Night.Framework.GetVersion() returns the correct engine version."; + + /// + public override string SuccessMessage => "Night.Framework.GetVersion() returned the correct version string."; + + /// + /// Executes the test logic for GetVersion. + /// + public override void Run() + { + // Arrange + string expectedVersion = VersionInfo.GetVersion(); + + // Act + string actualVersion = Night.Framework.GetVersion(); + + // Assert + Assert.Equal(expectedVersion, actualVersion); + } + } +} diff --git a/tests/Groups/Framework/FrameworkGroup.cs b/tests/Groups/Framework/FrameworkGroup.cs new file mode 100644 index 00000000..e9cff135 --- /dev/null +++ b/tests/Groups/Framework/FrameworkGroup.cs @@ -0,0 +1,88 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using NightTest.Core; + +using Xunit; +using Xunit.Abstractions; + +namespace NightTest.Groups.Framework +{ + /// + /// Test group for Framework-related ModTestCases. + /// + [Collection("SequentialTests")] // Recommended for ModTestCases as well if they modify global state (like LogManager) + public class FrameworkGroup : TestGroup + { + /// + /// Initializes a new instance of the class. + /// + /// The xUnit test output helper. + public FrameworkGroup(ITestOutputHelper outputHelper) + : base(outputHelper) + { + } + + /// + /// Runs ModTestCases for the CLI feature within the Framework module. + /// This includes tests for CLI constructor arguments (default values, silent mode, log level, debug mode, session log, remaining args, combined args) + /// and ApplySettings method (log level, debug mode, session log, remaining args warning). + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_FrameworkCLI_ModTests() + { + this.Run_ModTestCase(new NightCLI_Constructor_DefaultValuesTest()); + this.Run_ModTestCase(new NightCLI_Constructor_SilentModeTest()); + this.Run_ModTestCase(new NightCLI_Constructor_LogLevelTest()); + this.Run_ModTestCase(new NightCLI_Constructor_DebugModeTest()); + this.Run_ModTestCase(new NightCLI_Constructor_SessionLogTest()); + this.Run_ModTestCase(new NightCLI_Constructor_RemainingArgsTest()); + this.Run_ModTestCase(new NightCLI_Constructor_CombinedArgsTest()); + this.Run_ModTestCase(new NightCLI_ApplySettings_LogLevelTest()); + this.Run_ModTestCase(new NightCLI_ApplySettings_DebugModeTest()); + this.Run_ModTestCase(new NightCLI_ApplySettings_SessionLogNoCrashTest()); + this.Run_ModTestCase(new NightCLI_ApplySettings_RemainingArgsWarningTest()); + } + + /// + /// Runs ModTestCases for the Framework.GetVersion method. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_FrameworkGetVersion_ModTests() + { + this.Run_ModTestCase(new Framework_GetVersionTest()); + } + + /// + /// Runs ModTestCases for the Framework.Run method. + /// This includes tests for handling null IGame instances. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_FrameworkRun_ModTests() + { + this.Run_ModTestCase(new FrameworkRun_NullIGame()); + } + } +} diff --git a/tests/Groups/Framework/FrameworkRunTest.cs b/tests/Groups/Framework/FrameworkRunTest.cs new file mode 100644 index 00000000..e23e3940 --- /dev/null +++ b/tests/Groups/Framework/FrameworkRunTest.cs @@ -0,0 +1,71 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.IO; +using System.Linq; + +using Night; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.Framework +{ + /// + /// Tests for the Framework.Run method with null IGame handling. + /// + public class FrameworkRun_NullIGame : ModTestCase + { + /// + public override string Name => "Night.Framework.Run"; + + /// + public override string Description => "Tests null IGame handling in Framework.Run()."; + + /// + public override string SuccessMessage => "Null game handling in Framework.Run() passed successfully."; + + /// + public override void Run() + { + try + { + // Act: Call Framework.Run with null IGame +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + Night.Framework.Run(null); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + } + catch (ArgumentNullException ex) + { + // Assert: Expect ArgumentNullException for null IGame + Assert.Equal("game", ex.ParamName); + } + catch (Exception ex) + { + // Fail if any other exception is thrown + Assert.Fail($"Unexpected exception type: {ex.GetType().Name} - {ex.Message}"); + } + } + } +} diff --git a/tests/Groups/Graphics/GraphicsBackgroundColorTests.cs b/tests/Groups/Graphics/GraphicsBackgroundColorTests.cs new file mode 100644 index 00000000..e6057999 --- /dev/null +++ b/tests/Groups/Graphics/GraphicsBackgroundColorTests.cs @@ -0,0 +1,157 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; + +using Night; + +using NightTest.Core; + +namespace NightTest.Groups.Graphics +{ + /// + /// Tests the default background color retrieval. + /// + public class GraphicsGetBackgroundColor_DefaultTest : GameTestCase + { + private Night.Color defaultColor; + + /// + public override string Name => "Graphics.GetBackgroundColor.Default"; + + /// + public override string Description => "Tests that GetBackgroundColor returns the default color (black) if Clear has not been called."; + + /// + protected override void Load() + { + base.Load(); + this.Details = "Getting default background color."; + try + { + this.defaultColor = Night.Graphics.GetBackgroundColor(); + } + catch (Exception e) + { + this.RecordFailure($"Exception during GetBackgroundColor: {e.Message}", e); + } + } + + /// + protected override void Update(double deltaTime) + { + if (this.IsDone) + { + return; + } + + if (this.CurrentStatus == TestStatus.Failed) + { + this.EndTest(); + return; + } + + // Default color is black (0,0,0,255) + bool rMatch = this.defaultColor.R == 0; + bool gMatch = this.defaultColor.G == 0; + bool bMatch = this.defaultColor.B == 0; + bool aMatch = this.defaultColor.A == 255; // Alpha should be 255 for opaque black + + if (rMatch && gMatch && bMatch && aMatch) + { + this.CurrentStatus = TestStatus.Passed; + this.Details = $"Default background color is correct (R={this.defaultColor.R}, G={this.defaultColor.G}, B={this.defaultColor.B}, A={this.defaultColor.A})."; + } + else + { + this.CurrentStatus = TestStatus.Failed; + this.Details = $"Default background color is incorrect. Expected (0,0,0,255), Got (R={this.defaultColor.R}, G={this.defaultColor.G}, B={this.defaultColor.B}, A={this.defaultColor.A})."; + } + + this.EndTest(); + } + } + + /// + /// Tests background color retrieval after calling Graphics.Clear(). + /// + public class GraphicsGetBackgroundColor_AfterClearTest : GameTestCase + { + private readonly Color testColorByte = new(51, 102, 153, 204); + private Night.Color retrievedColor; + + /// + public override string Name => "Graphics.GetBackgroundColor.AfterClear"; + + /// + public override string Description => "Tests that GetBackgroundColor returns the correct color after Graphics.Clear() is called."; + + /// + protected override void Load() + { + base.Load(); + this.Details = $"Clearing screen with color ({this.testColorByte.R}, {this.testColorByte.G}, {this.testColorByte.B}, {this.testColorByte.A}) and getting background color."; + try + { + Night.Graphics.Clear(this.testColorByte); + this.retrievedColor = Night.Graphics.GetBackgroundColor(); + } + catch (Exception e) + { + this.RecordFailure($"Exception during test setup: {e.Message}", e); + } + } + + /// + protected override void Update(double deltaTime) + { + if (this.IsDone) + { + return; + } + + if (this.CurrentStatus == TestStatus.Failed) + { + this.EndTest(); + return; + } + + bool rMatch = this.retrievedColor.R == this.testColorByte.R; + bool gMatch = this.retrievedColor.G == this.testColorByte.G; + bool bMatch = this.retrievedColor.B == this.testColorByte.B; + bool aMatch = this.retrievedColor.A == this.testColorByte.A; + + if (rMatch && gMatch && bMatch && aMatch) + { + this.CurrentStatus = TestStatus.Passed; + this.Details = $"Retrieved background color is correct (R={this.retrievedColor.R}, G={this.retrievedColor.G}, B={this.retrievedColor.B}, A={this.retrievedColor.A})."; + } + else + { + this.CurrentStatus = TestStatus.Failed; + this.Details = $"Retrieved background color is incorrect. Expected (R={this.testColorByte.R}, G={this.testColorByte.G}, B={this.testColorByte.B}, A={this.testColorByte.A}), Got (R={this.retrievedColor.R}, G={this.retrievedColor.G}, B={this.retrievedColor.B}, A={this.retrievedColor.A})."; + } + + this.EndTest(); + } + } +} diff --git a/tests/Groups/Graphics/GraphicsClearTest.cs b/tests/Groups/Graphics/GraphicsClearTest.cs new file mode 100644 index 00000000..3c05b8de --- /dev/null +++ b/tests/Groups/Graphics/GraphicsClearTest.cs @@ -0,0 +1,63 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; + +using Night; + +using NightTest.Core; + +namespace NightTest.Groups.Graphics +{ + /// + /// Tests the Graphics.Clear() method with a specific color. + /// Requires manual confirmation that the color is correct. + /// + public class GraphicsClearColorTest : ManualTestCase + { + private readonly Color skyBlue = new Color(135, 206, 235); + + /// + public override string Name => "Graphics.Clear"; + + /// + public override string Description => "Tests clearing the screen to sky blue (135, 206, 235). User must confirm color."; + + /// + protected override void Load() + { + this.Details = "Test running, displaying sky blue color."; + } + + /// + protected override void Update(double deltaTime) + { + this.RequestManualConfirmation("Is the screen cleared to a SKY BLUE color (like a clear daytime sky)?"); + } + + /// + protected override void Draw() + { + Night.Graphics.Clear(this.skyBlue); + } + } +} diff --git a/tests/Groups/Graphics/GraphicsGroup.cs b/tests/Groups/Graphics/GraphicsGroup.cs new file mode 100644 index 00000000..7ae543b2 --- /dev/null +++ b/tests/Groups/Graphics/GraphicsGroup.cs @@ -0,0 +1,71 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Generic; + +using Night; + +using NightTest.Core; + +using Xunit; +using Xunit.Abstractions; + +namespace NightTest.Groups.Graphics +{ + /// + /// Tests for the Night.Graphics functionality. + /// + [Collection("SequentialTests")] + public class GraphicsGroup : TestGroup + { + /// + /// Initializes a new instance of the class. + /// + /// The xUnit test output helper for logging. + public GraphicsGroup(ITestOutputHelper outputHelper) + : base(outputHelper) + { + } + + /// + /// Runs the GraphicsClearColorTest IGame instance. + /// + [Fact] + [Trait("TestType", "Manual")] + public void Run_GraphicsClearColorTest() + { + this.Run_GameTestCase(new GraphicsClearColorTest()); + } + + /// + /// Runs automated tests for Graphics.GetBackgroundColor. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_Graphics_GetBackgroundColor_GameTests() + { + this.Run_GameTestCase(new GraphicsGetBackgroundColor_DefaultTest()); + this.Run_GameTestCase(new GraphicsGetBackgroundColor_AfterClearTest()); + } + } +} diff --git a/tests/Groups/Joysticks/JoystickConnectionEventsTest.cs b/tests/Groups/Joysticks/JoystickConnectionEventsTest.cs new file mode 100644 index 00000000..08afe9fe --- /dev/null +++ b/tests/Groups/Joysticks/JoystickConnectionEventsTest.cs @@ -0,0 +1,190 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Generic; +using System.Linq; + +using Night; + +using NightTest.Core; + +namespace NightTest.Groups.Joysticks +{ + /// + /// Manual test case for verifying joystick connection and disconnection events. + /// + public class JoystickConnectionEventsTest : ManualTestCase + { + private TestState currentState = TestState.InitialPromptConnect; + private string instructionText = string.Empty; + private List consoleMessages = new List(); + private Joystick? lastConnectedJoystick; + private Joystick? lastDisconnectedJoystick; + + private enum TestState + { + InitialPromptConnect, + WaitingForConnect, + ConnectVerifiedPromptDisconnect, + WaitingForDisconnect, + DisconnectVerifiedPromptPassFail, + TestComplete, + } + + /// + public override string Name => "Joysticks.ConnectionEvents"; + + /// + public override string Description => "Manually tests JoystickAdded and JoystickRemoved events. " + + "User will be prompted to connect and disconnect a joystick and verify console output."; + + /// + public override void JoystickAdded(Joystick joystick) + { + base.JoystickAdded(joystick); + string msg = $"EVENT: Joystick ADDED - ID: {joystick.GetId()}, Name: '{joystick.GetName()}'"; + Console.WriteLine(msg); + this.consoleMessages.Add(msg); + this.lastConnectedJoystick = joystick; + + if (this.currentState == TestState.WaitingForConnect) + { + this.currentState = TestState.ConnectVerifiedPromptDisconnect; + this.UpdateInstructionText(); + } + } + + /// + public override void JoystickRemoved(Joystick joystick) + { + base.JoystickRemoved(joystick); + + // Note: joystick.IsConnected() will be false here. + string msg = $"EVENT: Joystick REMOVED - ID: {joystick.GetId()}, Name: '{joystick.GetName()}'"; + Console.WriteLine(msg); + this.consoleMessages.Add(msg); + this.lastDisconnectedJoystick = joystick; + + if (this.currentState == TestState.WaitingForDisconnect) + { + this.currentState = TestState.DisconnectVerifiedPromptPassFail; + this.UpdateInstructionText(); + } + } + + /// + protected override void Load() + { + base.Load(); + this.currentState = TestState.InitialPromptConnect; + this.UpdateInstructionText(); + this.consoleMessages.Clear(); + this.lastConnectedJoystick = null; + this.lastDisconnectedJoystick = null; + + // Ensure the window is a reasonable size for instructions + _ = Window.SetMode(800, 600, 0); // Added flags argument + Window.SetTitle(this.Name); + } + + /// + protected override void Update(double deltaTime) + { + if (this.IsDone || this.currentState == TestState.TestComplete) + { + return; + } + + // This test relies on user actions (plugging/unplugging joystick) + // and then confirming observations. The main logic is in event handlers + // and the final RequestManualConfirmation. + + // If we are in a state waiting for user to confirm via UI buttons + if (this.currentState == TestState.DisconnectVerifiedPromptPassFail) + { + // RequestManualConfirmation handles its own timing and activation logic. + // It will only show the prompt once after ManualTestPromptDelayMilliseconds. + this.RequestManualConfirmation("Did the console correctly log joystick ADD and REMOVE events with details? Check counts and names."); + } + } + + /// + protected override void Draw() + { + Night.Graphics.Clear(new Color(30, 30, 30)); // Dark grey background + + // Instruction text will be shown in the console. + // The Pass/Fail buttons are drawn by the ManualTestCase base class. + // No additional in-window text rendering will be done here as Night.Graphics.Print/DrawString is not available. + } + + /// + protected override void EndTest() + { + this.currentState = TestState.TestComplete; + this.UpdateInstructionText(); // Update text to show final status + base.EndTest(); + } + + private void UpdateInstructionText() + { + switch (this.currentState) + { + case TestState.InitialPromptConnect: + this.instructionText = "Please CONNECT a joystick/gamepad now.\n" + + "The test will proceed automatically upon detection."; + this.currentState = TestState.WaitingForConnect; // Move to waiting state + break; + case TestState.WaitingForConnect: + // This state is mostly for internal logic; instruction was set in InitialPromptConnect + this.instructionText = "WAITING for joystick connection..."; + break; + case TestState.ConnectVerifiedPromptDisconnect: + this.instructionText = $"Joystick '{this.lastConnectedJoystick?.GetName()}' (ID: {this.lastConnectedJoystick?.GetId()}) connected.\n" + + "VERIFY console output for ADDED event details.\n\n" + + "Now, please DISCONNECT the joystick.\n" + + "The test will proceed automatically upon detection."; + this.currentState = TestState.WaitingForDisconnect; // Move to waiting state + break; + case TestState.WaitingForDisconnect: + this.instructionText = $"WAITING for joystick '{this.lastConnectedJoystick?.GetName()}' to be disconnected..."; + break; + case TestState.DisconnectVerifiedPromptPassFail: + this.instructionText = $"Joystick '{this.lastDisconnectedJoystick?.GetName()}' (ID: {this.lastDisconnectedJoystick?.GetId()}) disconnected.\n" + + "VERIFY console output for REMOVED event details.\n\n" + + "If all console logs were correct, click PASS. Otherwise, click FAIL."; + + // The RequestManualConfirmation will be called in Update() + break; + case TestState.TestComplete: + this.instructionText = "Test complete. Status: " + this.CurrentStatus; + break; + default: + this.instructionText = "Unknown test state."; + break; + } + + Console.WriteLine(this.instructionText); + } + } +} diff --git a/tests/Groups/Joysticks/JoysticksGroup.cs b/tests/Groups/Joysticks/JoysticksGroup.cs new file mode 100644 index 00000000..1f236717 --- /dev/null +++ b/tests/Groups/Joysticks/JoysticksGroup.cs @@ -0,0 +1,55 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using NightTest.Core; + +using Xunit; +using Xunit.Abstractions; + +namespace NightTest.Groups.Joysticks +{ + /// + /// Test group for Joystick related functionalities. + /// + [Collection("SequentialTests")] // Important for tests that interact with the game window + public class JoysticksGroup : TestGroup + { + /// + /// Initializes a new instance of the class. + /// + /// The xUnit test output helper. + public JoysticksGroup(ITestOutputHelper outputHelper) + : base(outputHelper) + { + } + + /// + /// Runs the manual test case for joystick connection and disconnection events. + /// + [Fact] + [Trait("TestType", "Manual")] + public void Run_JoystickConnectionEventsTest() + { + this.Run_GameTestCase(new JoystickConnectionEventsTest()); + } + } +} diff --git a/tests/Groups/SDL/SDLGroup.cs b/tests/Groups/SDL/SDLGroup.cs new file mode 100644 index 00000000..5a4f70b1 --- /dev/null +++ b/tests/Groups/SDL/SDLGroup.cs @@ -0,0 +1,57 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using NightTest.Core; + +using Xunit; +using Xunit.Abstractions; + +namespace NightTest.Groups.SDL +{ + /// + /// Test group for SDL related functionality. + /// + [Collection("SequentialTests")] + public class SDLGroup : TestGroup + { + /// + /// Initializes a new instance of the class. + /// + /// The xUnit output helper. + public SDLGroup(ITestOutputHelper outputHelper) + : base(outputHelper) + { + } + + /// + /// Runs all GameTestCases for NightSDL functionality. + /// This includes tests for GetVersion and GetError. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_NightSDL_GameTests() + { + this.Run_GameTestCase(new NightSDL_GetVersionTest()); + this.Run_GameTestCase(new NightSDL_GetErrorTest()); + } + } +} diff --git a/tests/Groups/SDL/SDLTests.cs b/tests/Groups/SDL/SDLTests.cs new file mode 100644 index 00000000..48dde779 --- /dev/null +++ b/tests/Groups/SDL/SDLTests.cs @@ -0,0 +1,107 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System.Text.RegularExpressions; + +using Night; + +using NightTest.Core; + +namespace NightTest.Groups.SDL +{ + /// + /// Tests that returns a correctly formatted version string. + /// + public class NightSDL_GetVersionTest : GameTestCase + { + /// + public override string Name => "NightSDL.GetVersion Format"; + + /// + public override string Description => "Tests if NightSDL.GetVersion() returns a string in 'major.minor.patch' format."; + + /// + protected override void Update(double deltaTime) + { + string version = NightSDL.GetVersion(); + bool isMatch = Regex.IsMatch(version, @"^\d+\.\d+\.\d+$"); + + if (isMatch) + { + this.Details = $"Version string '{version}' is correctly formatted."; + this.CurrentStatus = TestStatus.Passed; + } + else + { + this.Details = $"Version string '{version}' is NOT correctly formatted. Expected format: 'major.minor.patch'."; + this.CurrentStatus = TestStatus.Failed; + } + + this.EndTest(); + } + } + + /// + /// Tests that returns an empty string when no SDL error has occurred. + /// + public class NightSDL_GetErrorTest : GameTestCase + { + /// + public override string Name => "NightSDL.GetError No Error"; + + /// + public override string Description => "Tests if NightSDL.GetError() returns an empty string when no SDL error is set."; + + /// + protected override void Update(double deltaTime) + { + // The try-catch-finally for general exceptions and ensuring EndTest() is called + // has been moved to GameTestCase.IGame.Update(). + + // Ensure CurrentStatus is Running if not already set by Load, + // though Load should have set it. + // This specific check can remain if there's a concern it might still be NotRun + // when Update is called, though the GameTestCase.Load() should prevent this. + // For now, let's assume GameTestCase.Load() correctly sets it to Running. + // if (this.CurrentStatus == TestStatus.NotRun) + // { + // this.CurrentStatus = TestStatus.Running; + // this.Details = "Test execution started in Update..."; + // } + _ = SDL3.SDL.ClearError(); // Ensure no pre-existing error + string error = NightSDL.GetError(); + + if (string.IsNullOrEmpty(error)) + { + this.Details = "NightSDL.GetError() returned an empty string as expected."; + this.CurrentStatus = TestStatus.Passed; + } + else + { + this.Details = $"NightSDL.GetError() returned '{error}' but an empty string was expected after SDL.ClearError()."; + this.CurrentStatus = TestStatus.Failed; + } + + this.EndTest(); + } + } +} diff --git a/tests/Groups/System/SystemGetOSTest.cs b/tests/Groups/System/SystemGetOSTest.cs new file mode 100644 index 00000000..c8c4c707 --- /dev/null +++ b/tests/Groups/System/SystemGetOSTest.cs @@ -0,0 +1,85 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System.Runtime.InteropServices; // For RuntimeInformation + +using NightTest.Core; + +using SDL3; + +using Xunit; + +namespace NightTest.Groups.SystemTests +{ + /// + /// Tests the method. + /// + public class SystemGetOS_ReturnsCorrectPlatformStringTest : ModTestCase + { + /// + public override string Name => "System.GetOS.ReturnsCorrectPlatformString"; + + /// + public override string Description => "Tests that Night.System.GetOS() returns a platform string " + + "consistent with .NET's RuntimeInformation and LÖVE API conventions."; + + /// + public override string SuccessMessage => "Night.System.GetOS() returned a platform string " + + "consistent with .NET's RuntimeInformation and LÖVE API conventions."; + + /// + public override void Run() + { + // Arrange + string expectedOsString = string.Empty; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + expectedOsString = "Windows"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + expectedOsString = "OS X"; + } + + // TODO: Implement when Android and iOS get supported by Night. + /* else if (RuntimeInformation.IsOSPlatform(OSPlatform.Android)) + { + expectedOsString = "Android"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.IOS)) + { + expectedOsString = "iOS"; + } */ + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + expectedOsString = "Linux"; + } + + // Act + string actualOsString = Night.System.GetOS(); + + // Assert + Assert.Equal(expectedOsString, actualOsString); + } + } +} diff --git a/tests/Groups/System/SystemGetPowerInfoTests.cs b/tests/Groups/System/SystemGetPowerInfoTests.cs new file mode 100644 index 00000000..777c9766 --- /dev/null +++ b/tests/Groups/System/SystemGetPowerInfoTests.cs @@ -0,0 +1,83 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; // For Enum.IsDefined + +using Night; // For Night.System and Night.PowerState + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.SystemTests +{ + /// + /// Tests the method. + /// + public class SystemGetPowerInfo_ReturnsValidDataTest : ModTestCase + { + /// + public override string Name => "System.GetPowerInfo.ReturnsValidData"; + + /// + public override string Description => "Tests that Night.System.GetPowerInfo() returns a valid power state, " + + "percentage (0-100 or null), and seconds remaining (non-negative or null)."; + + /// + public override string SuccessMessage => "Night.System.GetPowerInfo() returned data in the expected valid format."; + + /// + public override void Run() + { + // Arrange & Act + (PowerState State, int? Percent, int? Seconds) powerInfo = Night.System.GetPowerInfo(); + + // Assert + Assert.True(Enum.IsDefined(typeof(PowerState), powerInfo.State), $"Invalid PowerState returned: {powerInfo.State}"); + + if (powerInfo.Percent.HasValue) + { + Assert.InRange(powerInfo.Percent.Value, 0, 100); + } + + if (powerInfo.Seconds.HasValue) + { + Assert.True(powerInfo.Seconds.Value >= 0, $"Expected seconds to be >= 0 or null, but got {powerInfo.Seconds.Value}."); + } + + // Additional check to ensure that if state is NoBattery, percent and seconds are likely null. + // This is a reasonable expectation but not a strict guarantee from SDL. + // The primary assertions above cover the validity of the values. + if (powerInfo.State == PowerState.NoBattery) + { + // It's common for Percent and Seconds to be null if NoBattery is reported. + // No direct assertion here as SDL behavior can vary; the core validity is already checked. + } + + // Similar consideration for Unknown state. + if (powerInfo.State == PowerState.Unknown) + { + // It's common for Percent and Seconds to be null if Unknown is reported. + } + } + } +} diff --git a/tests/Groups/System/SystemGetProcessorCountTest.cs b/tests/Groups/System/SystemGetProcessorCountTest.cs new file mode 100644 index 00000000..e6fb2bd8 --- /dev/null +++ b/tests/Groups/System/SystemGetProcessorCountTest.cs @@ -0,0 +1,59 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using Night; + +using NightTest.Core; + +using Xunit; + +namespace NightTest.Groups.SystemTests +{ + /// + /// Tests the method + /// to ensure it returns a positive value. + /// + public class SystemGetProcessorCount_ReturnsPositiveValueTest : ModTestCase + { + /// + public override string Name => "System.GetProcessorCount.ReturnsPositiveValue"; + + /// + public override string Description => "Tests if Night.System.GetProcessorCount() returns a value greater than or equal to 1."; + + /// + public override string SuccessMessage => "Night.System.GetProcessorCount() returned a positive value as expected."; + + /// + public override void Run() + { + // Arrange + int processorCount; + + // Act + processorCount = Night.System.GetProcessorCount(); + + // Assert + Assert.True(processorCount >= 1, $"Expected processor count to be >= 1, but got {processorCount}."); + } + } +} diff --git a/tests/Groups/System/SystemGroup.cs b/tests/Groups/System/SystemGroup.cs new file mode 100644 index 00000000..ab1ceff3 --- /dev/null +++ b/tests/Groups/System/SystemGroup.cs @@ -0,0 +1,57 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using NightTest.Core; + +using Xunit; +using Xunit.Abstractions; + +namespace NightTest.Groups.SystemTests +{ + /// + /// Test group for Night.System module. + /// + [Collection("SequentialTests")] // As per guidelines, though likely not strictly needed for only ModTestCases + public class SystemGroup : TestGroup + { + /// + /// Initializes a new instance of the class. + /// + /// The xUnit output helper. + public SystemGroup(ITestOutputHelper outputHelper) + : base(outputHelper) + { + } + + /// + /// Runs module tests for the Night.System functionality. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_System_ModTests() + { + this.Run_ModTestCase(new SystemGetOS_ReturnsCorrectPlatformStringTest()); + this.Run_ModTestCase(new SystemGetProcessorCount_ReturnsPositiveValueTest()); + this.Run_ModTestCase(new SystemGetPowerInfo_ReturnsValidDataTest()); + } + } +} diff --git a/tests/Groups/System/SystemOpenURLTests.cs b/tests/Groups/System/SystemOpenURLTests.cs new file mode 100644 index 00000000..87605b6e --- /dev/null +++ b/tests/Groups/System/SystemOpenURLTests.cs @@ -0,0 +1,82 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using Night; + +using NightTest.Core; + +namespace NightTest.Groups.SystemTests +{ + /// + /// Manual test case for Night.System.OpenURL(). + /// + public class SystemOpenURL_UserConfirmationTest : ManualTestCase + { + private const string TestUrl = "https://www.example.com"; + private bool urlOpenedAttempted = false; + + /// + public override string Name => "System.OpenURL.UserConfirmation"; + + /// + public override string Description => "User must confirm that Night.System.OpenURL correctly opens a specified URL."; + + /// + protected override void Load() + { + base.Load(); + this.Details = $"Attempting to open URL: {TestUrl}. Please observe if your browser opens it."; + } + + /// + protected override void Update(double deltaTime) + { + if (this.IsDone) + { + return; + } + + if (!this.urlOpenedAttempted) + { + _ = Night.System.OpenURL(TestUrl); + this.urlOpenedAttempted = true; + } + + if (this.TestStopwatch.ElapsedMilliseconds > this.ManualTestPromptDelayMilliseconds) + { + this.RequestManualConfirmation($"Did the URL '{TestUrl}' open correctly in your browser or file explorer?"); + } + } + + /// + protected override void Draw() + { + Night.Graphics.Clear(Night.Color.Black); + + // Night.Graphics.DrawString($"Testing Night.System.OpenURL...", 10, 10, Night.Color.White); // Commented out due to DrawString issues + // Night.Graphics.DrawString($"Attempting to open: {TestUrl}", 10, 30, Night.Color.White); // Commented out due to DrawString issues + + // The base.Draw() method will handle drawing the prompt when active. + base.Draw(); + } + } +} diff --git a/tests/Groups/Timer/GetAverageDeltaTest.cs b/tests/Groups/Timer/GetAverageDeltaTest.cs new file mode 100644 index 00000000..b13214fe --- /dev/null +++ b/tests/Groups/Timer/GetAverageDeltaTest.cs @@ -0,0 +1,62 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using Night; + +using NightTest.Core; + +namespace NightTest.Groups.Timer +{ + /// + /// Tests the Timer.GetAverageDelta() method. + /// + public class GetAverageDeltaTest : GameTestCase + { + /// + public override string Name => "Timer.GetAverageDelta"; + + /// + public override string Description => "Tests the Night.Timer.GetAverageDelta() method."; + + /// + protected override void Update(double deltaTime) + { + double finalAvgDelta = 0; + + _ = this.CheckCompletionAfterDuration( + 201, // > 200ms + successCondition: () => + { + if (this.CurrentFrameCount > 10) + { + finalAvgDelta = Night.Timer.GetAverageDelta(); + return true; + } + + return false; + }, + passDetails: () => $"Timer.GetAverageDelta() observed. Last reported value: {finalAvgDelta:F6}. Test ran for >200ms and >10 frames.", + failDetailsTimeout: null, + failDetailsCondition: () => "Timer.GetAverageDelta() test failed: Did not exceed 10 frames within 200ms."); + } + } +} diff --git a/tests/Groups/Timer/GetDeltaTest.cs b/tests/Groups/Timer/GetDeltaTest.cs new file mode 100644 index 00000000..2a0ed28d --- /dev/null +++ b/tests/Groups/Timer/GetDeltaTest.cs @@ -0,0 +1,68 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System.Collections.Generic; +using System.Linq; + +using Night; + +using NightTest.Core; + +namespace NightTest.Groups.Timer +{ + /// + /// Tests the Timer.GetDelta() method. + /// + public class GetDeltaTest : GameTestCase + { + private List deltas = new List(); + + /// + public override string Name => "Timer.GetDelta"; + + /// + public override string Description => "Tests the Night.Timer.GetDelta() method by collecting delta values."; + + /// + protected override void Load() + { + this.deltas.Clear(); + } + + /// + protected override void Update(double deltaTime) + { + this.deltas.Add(Night.Timer.GetDelta()); // Collect delta each frame this update is called + + _ = this.CheckCompletionAfterDuration( + 201, // > 200ms + successCondition: () => this.CurrentFrameCount > 10, + passDetails: () => + { + float averageDelta = this.deltas.Count > 0 ? this.deltas.Average() : 0f; + return $"Timer.GetDelta() test collected {this.deltas.Count} values. Average delta from Timer.GetDelta(): {averageDelta:F6}. Test ran for >200ms and >10 frames."; + }, + failDetailsTimeout: null, + failDetailsCondition: () => "Timer.GetDelta() test failed: Did not exceed 10 frames collecting deltas within 200ms."); + } + } +} diff --git a/tests/Groups/Timer/GetFPSTest.cs b/tests/Groups/Timer/GetFPSTest.cs new file mode 100644 index 00000000..176b6fdc --- /dev/null +++ b/tests/Groups/Timer/GetFPSTest.cs @@ -0,0 +1,62 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using Night; + +using NightTest.Core; + +namespace NightTest.Groups.Timer +{ + /// + /// Tests the Timer.GetFPS() method. + /// + public class GetFPSTest : GameTestCase + { + /// + public override string Name => "Timer.GetFPS"; + + /// + public override string Description => "Tests the Night.Timer.GetFPS() method by observing its value over a short period."; + + /// + protected override void Update(double deltaTime) + { + int finalFps = 0; // To capture FPS in the success condition + + _ = this.CheckCompletionAfterDuration( + 201, // > 200ms + successCondition: () => + { + if (this.CurrentFrameCount > 10) + { + finalFps = Night.Timer.GetFPS(); // Get FPS when conditions are met + return true; + } + + return false; + }, + passDetails: () => $"Timer.GetFPS() test observed. Last reported FPS: {finalFps}. Test ran for >200ms and >10 frames.", + failDetailsTimeout: null, + failDetailsCondition: () => "Timer.GetFPS() test failed: Did not exceed 10 frames within 200ms."); + } + } +} diff --git a/tests/Groups/Timer/GetTimeTest.cs b/tests/Groups/Timer/GetTimeTest.cs new file mode 100644 index 00000000..d665e8fe --- /dev/null +++ b/tests/Groups/Timer/GetTimeTest.cs @@ -0,0 +1,69 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using Night; + +using NightTest.Core; + +namespace NightTest.Groups.Timer +{ + /// + /// Tests the Timer.GetTime() method. + /// + public class GetTimeTest : GameTestCase + { + private double startTime = 0; + private double endTime = 0; + + /// + public override string Name => "Timer.GetTime"; + + /// + public override string Description => "Tests the Night.Timer.GetTime() method by measuring time passage."; + + /// + protected override void Load() + { + this.startTime = Night.Timer.GetTime(); + } + + /// + protected override void Update(double deltaTime) + { + // The IsDone check is handled by GameTestCase.Update before calling this. + _ = this.CheckCompletionAfterDuration( + 500, + successCondition: () => + { + this.endTime = Night.Timer.GetTime(); + return true; // Condition for passing is simply reaching the duration + }, + passDetails: () => // Use a lambda to construct details with captured values + { + double elapsed = this.endTime - this.startTime; + return $"Timer.GetTime() test completed. Start: {this.startTime:F6}s, End: {this.endTime:F6}s. Elapsed: {elapsed:F6}s (Expected ~0.5s)."; + }); + } + + // Draw() override removed, will use empty GameTestCase.Draw() + } +} diff --git a/tests/Groups/Timer/SleepTest.cs b/tests/Groups/Timer/SleepTest.cs new file mode 100644 index 00000000..446d8417 --- /dev/null +++ b/tests/Groups/Timer/SleepTest.cs @@ -0,0 +1,142 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Diagnostics; + +using Night; + +using NightTest.Core; + +namespace NightTest.Groups.Timer +{ + /// + /// Tests the Timer.Sleep() method. + /// + public class SleepTest : GameTestCase + { + private const double SleepDurationSeconds = 0.25; // Sleep for 250ms + + // Using TestStopwatch from base class for overall test duration + // Need a separate stopwatch for measuring sleep itself + private Stopwatch internalStopwatch = new Stopwatch(); + + /// + public override string Name => "Timer.Sleep"; + + /// + public override string Description => $"Tests the Night.Timer.Sleep() method by sleeping for {SleepDurationSeconds}s."; + + /// + protected override void Load() + { + this.internalStopwatch.Reset(); + this.internalStopwatch.Start(); + Night.Timer.Sleep(SleepDurationSeconds); + this.internalStopwatch.Stop(); + + double elapsedMs = this.internalStopwatch.ElapsedMilliseconds; + + if (elapsedMs >= SleepDurationSeconds * 1000 * 0.9 && elapsedMs <= SleepDurationSeconds * 1000 * 1.6) + { + this.CurrentStatus = TestStatus.Passed; + this.Details = $"Timer.Sleep({SleepDurationSeconds}) executed. Measured duration: {elapsedMs}ms. Expected ~{SleepDurationSeconds * 1000}ms."; + } + else + { + this.CurrentStatus = TestStatus.Failed; + this.Details = $"Timer.Sleep({SleepDurationSeconds}) executed. Measured duration: {elapsedMs}ms. Expected ~{SleepDurationSeconds * 1000}ms. Deviation too large."; + } + + // Since this test completes its logic here, call EndTest immediately. + // TestStopwatch started by IGame.Load will correctly measure the duration of this Load phase. + this.EndTest(); + } + + /// + protected override void Update(double deltaTime) + { + if (!this.IsDone) + { + this.Details = "Test did not complete in Load as expected."; + this.CurrentStatus = TestStatus.Failed; + this.EndTest(); // Ensure it quits if it somehow reaches here and isn't done. + } + } + } + + /// + /// Tests the Timer.Sleep(seconds) method where time sleep is less than 0. + /// + public class SleepTest_EarlyReturn : GameTestCase + { + private const double SleepDurationSeconds = -1; // Invalid time + private const double ExpectedSleepDurationMs = 0.001; // Expect no sleep + + // Using TestStopwatch from base class for overall test duration + // Need a separate stopwatch for measuring sleep itself + private Stopwatch internalStopwatch = new Stopwatch(); + + /// + public override string Name => "Timer.Sleep"; + + /// + public override string Description => $"Tests the Night.Timer.Sleep() method by sleeping for {SleepDurationSeconds}s."; + + /// + protected override void Load() + { + this.internalStopwatch.Reset(); + this.internalStopwatch.Start(); + Night.Timer.Sleep(SleepDurationSeconds); + this.internalStopwatch.Stop(); + + double elapsedMs = this.internalStopwatch.ElapsedMilliseconds; + + if (elapsedMs <= ExpectedSleepDurationMs) + { + this.CurrentStatus = TestStatus.Passed; + this.Details = $"Timer.Sleep({SleepDurationSeconds}) executed. Measured duration: {elapsedMs}ms. Expected less than {SleepDurationSeconds * 1000}ms."; + } + else + { + this.CurrentStatus = TestStatus.Failed; + this.Details = $"Timer.Sleep({SleepDurationSeconds}) executed. Measured duration: {elapsedMs}ms. Expected less than {SleepDurationSeconds * 1000}ms."; + } + + // Since this test completes its logic here, call EndTest immediately. + // TestStopwatch started by IGame.Load will correctly measure the duration of this Load phase. + this.EndTest(); + } + + /// + protected override void Update(double deltaTime) + { + if (!this.IsDone) + { + this.Details = "Test did not complete in Load as expected."; + this.CurrentStatus = TestStatus.Failed; + this.EndTest(); // Ensure it quits if it somehow reaches here and isn't done. + } + } + } +} diff --git a/tests/Groups/Timer/StepTest.cs b/tests/Groups/Timer/StepTest.cs new file mode 100644 index 00000000..3bca64a7 --- /dev/null +++ b/tests/Groups/Timer/StepTest.cs @@ -0,0 +1,81 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System.Collections.Generic; +using System.Linq; + +using Night; + +using NightTest.Core; + +namespace NightTest.Groups.Timer +{ + /// + /// Tests the Timer.Step() method. + /// + public class StepTest : GameTestCase + { + private int stepCount = 0; + private List stepDeltas = new List(); + + /// + public override string Name => "Timer.Step"; + + /// + public override string Description => "Tests the Night.Timer.Step() method by calling it multiple times and observing delta values."; + + /// + protected override void Load() + { + this.stepCount = 0; + this.stepDeltas.Clear(); + + // Framework calls Timer.Initialize(), which sets LastStepTime. + // Then Framework calls Timer.Step() before the first Update. + // So, the first Night.Timer.Step() in Update here should give a small delta. + } + + /// + protected override void Update(double deltaTime) + { + // The framework has already called Timer.Step() and the result is in `deltaTime` / Timer.GetDelta(). + // To test Timer.Step() somewhat independently, we can call it again. + // Note: The *first* call to Timer.Step() in an application's lifetime (or after a long pause) + // might have a larger delta if LastStepTime was zero or very old. + // Timer.Initialize() sets LastStepTime, and framework calls Step() before first Update. + double directStepDelta = Night.Timer.Step(); // Call it directly to get its current calculation + this.stepDeltas.Add(directStepDelta); + this.stepCount++; // Still need _stepCount for the number of direct calls. + + _ = this.CheckCompletionAfterDuration( + 201, + successCondition: () => this.stepCount > 10, + passDetails: () => + { + double averageDirectStepDelta = this.stepDeltas.Count > 0 ? this.stepDeltas.Average() : 0.0; + return $"Timer.Step() called {this.stepCount} times directly. Average delta from these calls: {averageDirectStepDelta:F6}. Test ran for >200ms."; + }, + failDetailsTimeout: null, + failDetailsCondition: () => "Timer.Step() test failed: Did not make >10 direct calls within 200ms."); + } + } +} diff --git a/tests/Groups/Timer/TimerGroup.cs b/tests/Groups/Timer/TimerGroup.cs new file mode 100644 index 00000000..74813662 --- /dev/null +++ b/tests/Groups/Timer/TimerGroup.cs @@ -0,0 +1,67 @@ +// +// zlib license +// +// Copyright (c) 2025 Danny Solivan, Night Circle +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +using System; +using System.Collections.Generic; + +using Night; + +using NightTest.Core; + +using Xunit; +using Xunit.Abstractions; + +namespace NightTest.Groups.Timer +{ + /// + /// Tests for the Night.Timer functionality. + /// + [Collection("SequentialTests")] + public class TimerGroup : TestGroup + { + /// + /// Initializes a new instance of the class. + /// + /// The xUnit test output helper for logging. + public TimerGroup(ITestOutputHelper outputHelper) + : base(outputHelper) + { + } + + /// + /// Runs all GameTestCases for the Timer module. + /// This includes tests for GetTime, GetFPS, GetDelta, GetAverageDelta, Sleep, and Step functionality. + /// + [Fact] + [Trait("TestType", "Automated")] + public void Run_Timer_GameTests() + { + this.Run_GameTestCase(new GetTimeTest()); + this.Run_GameTestCase(new GetFPSTest()); + this.Run_GameTestCase(new GetDeltaTest()); + this.Run_GameTestCase(new GetAverageDeltaTest()); + this.Run_GameTestCase(new SleepTest()); + this.Run_GameTestCase(new SleepTest_EarlyReturn()); + this.Run_GameTestCase(new StepTest()); + } + } +} diff --git a/tests/Night.Tests/Configuration/ConfigurationManagerTests.cs b/tests/Night.Tests/Configuration/ConfigurationManagerTests.cs deleted file mode 100644 index 749468ef..00000000 --- a/tests/Night.Tests/Configuration/ConfigurationManagerTests.cs +++ /dev/null @@ -1,290 +0,0 @@ -// -// zlib license -// -// Copyright (c) 2025 Danny Solivan, Night Circle -// -// This software is provided 'as-is', without any express or implied -// warranty. In no event will the authors be held liable for any damages -// arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose, -// including commercial applications, and to alter it and redistribute it -// freely, subject to the following restrictions: -// -// 1. The origin of this software must not be misrepresented; you must not -// claim that you wrote the original software. If you use this software -// in a product, an acknowledgment in the product documentation would be -// appreciated but is not required. -// 2. Altered source versions must be plainly marked as such, and must not be -// misrepresented as being the original software. -// 3. This notice may not be removed or altered from any source distribution. -// - -using System; -using System.IO; -using System.Reflection; -using System.Text.Json; - -using Night; - -using Xunit; - -namespace Night.Tests.Configuration -{ - /// - /// Tests for the class. - /// - public class ConfigurationManagerTests : IDisposable - { - private const string TestDirName = "config_test_temp"; - private readonly string testDirectoryPath; - private readonly string configFilePath; - - /// - /// Initializes a new instance of the class. - /// Sets up a temporary directory for config files. - /// - public ConfigurationManagerTests() - { - ResetConfigurationManager(); // Ensure a clean state for each test - - var assemblyLocation = Path.GetDirectoryName(typeof(ConfigurationManagerTests).Assembly.Location); - if (string.IsNullOrEmpty(assemblyLocation)) - { - throw new InvalidOperationException("Could not determine the assembly location for test setup."); - } - - this.testDirectoryPath = Path.Combine(assemblyLocation, TestDirName); - _ = Directory.CreateDirectory(this.testDirectoryPath); - this.configFilePath = Path.Combine(this.testDirectoryPath, "config.json"); - } - - /// - /// Disposes of the test resources by deleting the temporary directory. - /// - public void Dispose() - { - // Attempt to reset again to ensure no lingering state affects other test classes - // though xUnit typically isolates test classes. - ResetConfigurationManager(); - if (Directory.Exists(this.testDirectoryPath)) - { - Directory.Delete(this.testDirectoryPath, true); - } - } - - /// - /// Tests that LoadConfig uses default settings and sets IsLoaded to true when config.json is not found. - /// - [Fact] - public void LoadConfig_FileNotFound_UsesDefaultsAndIsLoaded() - { - // Arrange - // Ensure config file does not exist (covered by test setup and Dispose) - - // Act - ConfigurationManager.LoadConfig(this.testDirectoryPath); - - // Assert - Assert.True(ConfigurationManager.IsLoaded); - Assert.NotNull(ConfigurationManager.CurrentConfig); - - // Check a few default values from GameConfig and its nested objects - Assert.Equal("Night Game", ConfigurationManager.CurrentConfig.Window.Title); // Default from WindowConfig - Assert.Equal(800, ConfigurationManager.CurrentConfig.Window.Width); // Default from WindowConfig - Assert.True(ConfigurationManager.CurrentConfig.Audio.MixWithSystem); // Default from AudioConfig - } - - /// - /// Tests that LoadConfig uses default settings and sets IsLoaded to true when config.json is empty. - /// - [Fact] - public void LoadConfig_EmptyFile_UsesDefaultsAndIsLoaded() - { - // Arrange - File.WriteAllText(this.configFilePath, string.Empty); - - // Act - ConfigurationManager.LoadConfig(this.testDirectoryPath); - - // Assert - Assert.True(ConfigurationManager.IsLoaded); - Assert.NotNull(ConfigurationManager.CurrentConfig); - Assert.Equal("Night Game", ConfigurationManager.CurrentConfig.Window.Title); - Assert.Equal(800, ConfigurationManager.CurrentConfig.Window.Width); - } - - /// - /// Tests that LoadConfig uses default settings and sets IsLoaded to true when config.json contains invalid JSON. - /// - [Fact] - public void LoadConfig_InvalidJson_UsesDefaultsAndIsLoaded() - { - // Arrange - File.WriteAllText(this.configFilePath, "{ \"invalidJson\": "); // Incomplete JSON - - // Act - ConfigurationManager.LoadConfig(this.testDirectoryPath); - - // Assert - Assert.True(ConfigurationManager.IsLoaded); - Assert.NotNull(ConfigurationManager.CurrentConfig); - Assert.Equal("Night Game", ConfigurationManager.CurrentConfig.Window.Title); - Assert.Equal(800, ConfigurationManager.CurrentConfig.Window.Width); - } - - /// - /// Tests that LoadConfig correctly loads settings from a valid config.json and sets IsLoaded to true. - /// - [Fact] - public void LoadConfig_ValidJson_LoadsConfigAndIsLoaded() - { - // Arrange - var expectedConfig = new GameConfig - { - Identity = "my-custom-game", - Version = "1.0.0", - Console = true, - Window = new WindowConfig { Title = "My Custom Game", Width = 1920, Height = 1080, Fullscreen = true, VSync = false, Borderless = true }, - Audio = new AudioConfig { MixWithSystem = false }, - Modules = new ModulesConfig { Graphics = false, Audio = false }, - }; - string jsonContent = JsonSerializer.Serialize(expectedConfig, new JsonSerializerOptions { WriteIndented = true }); - File.WriteAllText(this.configFilePath, jsonContent); - - // Act - ConfigurationManager.LoadConfig(this.testDirectoryPath); - - // Assert - Assert.True(ConfigurationManager.IsLoaded); - Assert.NotNull(ConfigurationManager.CurrentConfig); - var actualConfig = ConfigurationManager.CurrentConfig; - - Assert.Equal(expectedConfig.Identity, actualConfig.Identity); - Assert.Equal(expectedConfig.Version, actualConfig.Version); - Assert.Equal(expectedConfig.Console, actualConfig.Console); - - Assert.Equal(expectedConfig.Window.Title, actualConfig.Window.Title); - Assert.Equal(expectedConfig.Window.Width, actualConfig.Window.Width); - Assert.Equal(expectedConfig.Window.Height, actualConfig.Window.Height); - Assert.Equal(expectedConfig.Window.Fullscreen, actualConfig.Window.Fullscreen); - Assert.Equal(expectedConfig.Window.VSync, actualConfig.Window.VSync); - Assert.True(expectedConfig.Window.Borderless == actualConfig.Window.Borderless); - - Assert.Equal(expectedConfig.Audio.MixWithSystem, actualConfig.Audio.MixWithSystem); - - Assert.Equal(expectedConfig.Modules.Graphics, actualConfig.Modules.Graphics); - Assert.Equal(expectedConfig.Modules.Audio, actualConfig.Modules.Audio); - - // ... add more assertions for other properties as needed - } - - /// - /// Tests that LoadConfig loads the configuration only once, even if called multiple times. - /// - [Fact] - public void LoadConfig_LoadsOnlyOnce() - { - // Arrange - // Config with a specific window title for the initial load - var initialWindowConfig = new WindowConfig { Title = "Initial Load Title" }; - var initialGameConfig = new GameConfig { Window = initialWindowConfig }; - string initialJsonContent = JsonSerializer.Serialize(initialGameConfig); - File.WriteAllText(this.configFilePath, initialJsonContent); - - // Act - ConfigurationManager.LoadConfig(this.testDirectoryPath); // First load - - // Assert first load - Assert.True(ConfigurationManager.IsLoaded); - Assert.Equal(initialGameConfig.Window.Title, ConfigurationManager.CurrentConfig.Window.Title); - - // Arrange for second attempt - change the config file content - var updatedWindowConfig = new WindowConfig { Title = "Updated Load Attempt Title" }; - var updatedGameConfig = new GameConfig { Window = updatedWindowConfig }; - string updatedJsonContent = JsonSerializer.Serialize(updatedGameConfig); - File.WriteAllText(this.configFilePath, updatedJsonContent); - - // Act - attempt to load again - ConfigurationManager.LoadConfig(this.testDirectoryPath); - - // Assert - config should NOT have changed because it's already loaded - Assert.True(ConfigurationManager.IsLoaded); // Still true - Assert.Equal(initialGameConfig.Window.Title, ConfigurationManager.CurrentConfig.Window.Title); // Should be from initial load - Assert.NotEqual(updatedGameConfig.Window.Title, ConfigurationManager.CurrentConfig.Window.Title); // Should not be updated - } - - /// - /// Tests that CurrentConfig returns the default configuration when LoadConfig has not been called. - /// - [Fact] - public void CurrentConfig_ReturnsDefaultWhenNotLoaded() - { - // Arrange (Reset ensures it's not loaded in constructor) - - // Act - var config = ConfigurationManager.CurrentConfig; - - // Assert - Assert.False(ConfigurationManager.IsLoaded); - Assert.NotNull(config); - Assert.Equal("Night Game", config.Window.Title); // Default value from WindowConfig - Assert.Equal(800, config.Window.Width); // Default value from WindowConfig - } - - /// - /// Tests that IsLoaded is false before LoadConfig is called. - /// - [Fact] - public void IsLoaded_IsFalseInitially() - { - // Arrange (Reset ensures it's not loaded) - // ResetConfigurationManager(); // Done in constructor - - // Assert - Assert.False(ConfigurationManager.IsLoaded); - } - - // Helper to reset the static state of ConfigurationManager for isolated tests. - // This is done via reflection as there's no public reset method. - private static void ResetConfigurationManager() - { - // Find the isLoaded field - var isLoadedField = typeof(ConfigurationManager).GetField("isLoaded", BindingFlags.NonPublic | BindingFlags.Static); - if (isLoadedField != null) - { - isLoadedField.SetValue(null, false); - } - else - { - // Consider how to handle if the field isn't found, e.g., throw or log. - // For test robustness, you might want to ensure this field exists. - Console.WriteLine("Warning: isLoaded field not found in ConfigurationManager. Test isolation may be affected."); - } - - // Find the currentConfig field - var currentConfigField = typeof(ConfigurationManager).GetField("currentConfig", BindingFlags.NonPublic | BindingFlags.Static); - if (currentConfigField != null) - { - currentConfigField.SetValue(null, new GameConfig()); // Reset to a new default instance - } - else - { - Console.WriteLine("Warning: currentConfig field not found in ConfigurationManager. Test isolation may be affected."); - } - - // Find the _configLock field and reset if it's a Lazy or similar re-entrant lock that needs resetting. - // For a simple `object` lock, resetting isn't typically needed unless it holds state. - // If _configLock is, for example, a specific lock implementation that could be 'stuck', - // you might need to re-initialize it: - // var configLockField = typeof(ConfigurationManager).GetField("_configLock", BindingFlags.NonPublic | BindingFlags.Static); - // if (configLockField != null) - // { - // configLockField.SetValue(null, new object()); - // } - - // If there are any other static members that hold state, reset them here too. - // Example: If ConfigurationManager subscribed to static events, unsubscribe here. - } - } -} diff --git a/tests/Night.Tests/Filesystem/FilesystemTests.cs b/tests/Night.Tests/Filesystem/FilesystemTests.cs deleted file mode 100644 index a7deb288..00000000 --- a/tests/Night.Tests/Filesystem/FilesystemTests.cs +++ /dev/null @@ -1,357 +0,0 @@ -// -// zlib license -// -// Copyright (c) 2025 Danny Solivan, Night Circle -// -// This software is provided 'as-is', without any express or implied -// warranty. In no event will the authors be held liable for any damages -// arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose, -// including commercial applications, and to alter it and redistribute it -// freely, subject to the following restrictions: -// -// 1. The origin of this software must not be misrepresented; you must not -// claim that you wrote the original software. If you use this software -// in a product, an acknowledgment in the product documentation would be -// appreciated but is not required. -// 2. Altered source versions must be plainly marked as such, and must not be -// misrepresented as being the original software. -// 3. This notice may not be removed or altered from any source distribution. -// - -using System; -using System.IO; -using System.Text; - -using Night; - -using Xunit; - -/// -/// Tests for the class. -/// -public class FilesystemTests : IDisposable -{ - private const string TestDir = "test_filesystem_temp"; - private const string TestFile = "test_file.txt"; - private const string TestSubDir = "test_subdir"; - private readonly string testFilePath; - private readonly string testDirPath; - private readonly string testSubDirPath; - private readonly string testSymlinkFilePath; - private readonly string testSymlinkDirPath; - - /// - /// Initializes a new instance of the class. - /// Sets up the test environment by creating temporary directories and files. - /// - public FilesystemTests() - { - // Create a temporary directory for test files relative to the test execution directory - var executionPath = Path.GetDirectoryName(typeof(FilesystemTests).Assembly.Location); - var testDirFullPath = Path.Combine(executionPath!, TestDir); - _ = Directory.CreateDirectory(testDirFullPath); - - this.testFilePath = Path.Combine(testDirFullPath, TestFile); - this.testDirPath = Path.Combine(testDirFullPath, "actual_dir_for_symlink"); - this.testSubDirPath = Path.Combine(testDirFullPath, TestSubDir); - this.testSymlinkFilePath = Path.Combine(testDirFullPath, "symlink_file.txt"); - this.testSymlinkDirPath = Path.Combine(testDirFullPath, "symlink_dir"); - - File.WriteAllText(this.testFilePath, "Hello Night Engine!"); - _ = Directory.CreateDirectory(this.testDirPath); - _ = Directory.CreateDirectory(this.testSubDirPath); - - // Create symlinks if supported (Windows requires admin rights or dev mode) - try - { - _ = File.CreateSymbolicLink(this.testSymlinkFilePath, this.testFilePath); - } - catch (IOException ex) - { - Console.WriteLine($"Could not create file symlink: {ex.Message}. This test might be skipped or fail if symlinks are essential."); - } - catch (Exception ex) - { - Console.WriteLine($"An unexpected error occurred during file symlink creation: {ex.Message}"); - } - - try - { - _ = Directory.CreateSymbolicLink(this.testSymlinkDirPath, this.testDirPath); - } - catch (IOException ex) - { - Console.WriteLine($"Could not create directory symlink: {ex.Message}. This test might be skipped or fail if symlinks are essential."); - } - catch (Exception ex) - { - Console.WriteLine($"An unexpected error occurred during directory symlink creation: {ex.Message}"); - } - } - - /// - /// Disposes of the test resources by deleting the temporary directory. - /// - public void Dispose() - { - // Clean up the temporary directory - var executionPath = Path.GetDirectoryName(typeof(FilesystemTests).Assembly.Location); - var testDirFullPath = Path.Combine(executionPath!, TestDir); - if (Directory.Exists(testDirFullPath)) - { - Directory.Delete(testDirFullPath, true); - } - } - - /// - /// Tests that GetInfo returns null when the path is null. - /// - [Fact] - public void GetInfo_NullPath_ReturnsNull() - { - Assert.Null(Night.Filesystem.GetInfo(null!)); - } - - /// - /// Tests that GetInfo returns null when the path is empty. - /// - [Fact] - public void GetInfo_EmptyPath_ReturnsNull() - { - Assert.Null(Night.Filesystem.GetInfo(string.Empty)); - } - - /// - /// Tests that GetInfo returns null for a non-existent path. - /// - [Fact] - public void GetInfo_NonExistentPath_ReturnsNull() - { - var executionPath = Path.GetDirectoryName(typeof(FilesystemTests).Assembly.Location); - var testDirFullPath = Path.Combine(executionPath!, TestDir); - Assert.Null(Night.Filesystem.GetInfo(Path.Combine(testDirFullPath, "non_existent_file.txt"))); - } - - /// - /// Tests that GetInfo returns correct FileSystemInfo for an existing file. - /// - [Fact] - public void GetInfo_ExistingFile_ReturnsFileInfo() - { - var info = Night.Filesystem.GetInfo(this.testFilePath); - Assert.NotNull(info); - Assert.Equal(Night.FileType.File, info.Type); - Assert.Equal(new FileInfo(this.testFilePath).Length, info.Size); - _ = Assert.NotNull(info.ModTime); - } - - /// - /// Tests that GetInfo returns correct FileSystemInfo for an existing file when filtered by type File. - /// - [Fact] - public void GetInfo_ExistingFile_WithFilterTypeFile_ReturnsFileInfo() - { - var info = Night.Filesystem.GetInfo(this.testFilePath, Night.FileType.File); - Assert.NotNull(info); - Assert.Equal(Night.FileType.File, info.Type); - } - - /// - /// Tests that GetInfo returns null for an existing file when filtered by type Directory. - /// - [Fact] - public void GetInfo_ExistingFile_WithFilterTypeDirectory_ReturnsNull() - { - var info = Night.Filesystem.GetInfo(this.testFilePath, Night.FileType.Directory); - Assert.Null(info); - } - - /// - /// Tests that GetInfo returns correct FileSystemInfo for an existing directory. - /// - [Fact] - public void GetInfo_ExistingDirectory_ReturnsDirectoryInfo() - { - var info = Night.Filesystem.GetInfo(this.testSubDirPath); - Assert.NotNull(info); - Assert.Equal(Night.FileType.Directory, info.Type); - Assert.Null(info.Size); // Size is null for directories in our implementation - _ = Assert.NotNull(info.ModTime); - } - - /// - /// Tests that GetInfo returns correct FileSystemInfo for an existing directory when filtered by type Directory. - /// - [Fact] - public void GetInfo_ExistingDirectory_WithFilterTypeDirectory_ReturnsDirectoryInfo() - { - var info = Night.Filesystem.GetInfo(this.testSubDirPath, Night.FileType.Directory); - Assert.NotNull(info); - Assert.Equal(Night.FileType.Directory, info.Type); - } - - /// - /// Tests that GetInfo returns null for an existing directory when filtered by type File. - /// - [Fact] - public void GetInfo_ExistingDirectory_WithFilterTypeFile_ReturnsNull() - { - var info = Night.Filesystem.GetInfo(this.testSubDirPath, Night.FileType.File); - Assert.Null(info); - } - - /// - /// Tests that GetInfo returns correct FileSystemInfo for a file symlink. - /// - [Fact] - public void GetInfo_FileSymlink_ReturnsSymlinkInfo() - { - if (!File.Exists(this.testSymlinkFilePath) && !Directory.Exists(this.testSymlinkFilePath) /* Symlink could point to dir or file, check both just in case File.Exists is tricky with broken file symlinks */) - { - // Skip if symlink creation failed (e.g. permissions on Windows or if it points to a now-deleted item and File.Exists returns false) - Console.WriteLine($"Skipping symlink test for file: {this.testSymlinkFilePath} as it does not exist or could not be verified."); - return; - } - - var info = Night.Filesystem.GetInfo(this.testSymlinkFilePath); - Assert.NotNull(info); - Assert.Equal(Night.FileType.Symlink, info.Type); - } - - /// - /// Tests that GetInfo returns correct FileSystemInfo for a directory symlink. - /// - [Fact] - public void GetInfo_DirectorySymlink_ReturnsSymlinkInfo() - { - if (!Directory.Exists(this.testSymlinkDirPath)) - { - // Skip if symlink creation failed (e.g. permissions on Windows) - Console.WriteLine($"Skipping symlink test for directory: {this.testSymlinkDirPath} as it does not exist or could not be verified."); - return; - } - - var info = Night.Filesystem.GetInfo(this.testSymlinkDirPath); - Assert.NotNull(info); - Assert.Equal(Night.FileType.Symlink, info.Type); - } - - /// - /// Tests that GetInfo correctly populates an existing FileSystemInfo object for a valid path. - /// - [Fact] - public void GetInfo_PopulatesExistingObject_ValidPath() - { - var existingInfo = new Night.FileSystemInfo(Night.FileType.File, 0, 0); // Dummy initial values - var result = Night.Filesystem.GetInfo(this.testFilePath, existingInfo); - - Assert.NotNull(result); - Assert.Same(existingInfo, result); // Ensure it's the same object - Assert.Equal(Night.FileType.File, existingInfo.Type); - Assert.Equal(new FileInfo(this.testFilePath).Length, existingInfo.Size); - _ = Assert.NotNull(existingInfo.ModTime); - } - - /// - /// Tests that GetInfo correctly populates (or doesn't) an existing FileSystemInfo for a non-existent path. - /// - [Fact] - public void GetInfo_PopulatesExistingObject_NonExistentPath_ReturnsNullAndDoesNotChangeObject() - { - var executionPath = Path.GetDirectoryName(typeof(FilesystemTests).Assembly.Location); - var testDirFullPath = Path.Combine(executionPath!, TestDir); - var nonExistentPath = Path.Combine(testDirFullPath, "non_existent_file.txt"); - - var originalType = Night.FileType.File; // Use a different type than default if possible - long originalSize = 123; - long originalModTime = DateTime.UtcNow.Ticks; // Arbitrary non-zero value - - var existingInfo = new Night.FileSystemInfo(originalType, originalSize, originalModTime); - - var result = Night.Filesystem.GetInfo(nonExistentPath, existingInfo); - - Assert.Null(result); - Assert.Equal(originalType, existingInfo.Type); - Assert.Equal(originalSize, existingInfo.Size); - Assert.Equal(originalModTime, existingInfo.ModTime); - } - - /// - /// Tests that GetInfo populates an existing object correctly with a type filter when path and type match. - /// - [Fact] - public void GetInfo_PopulatesExistingObjectWithFilter_ValidPathAndType() - { - var existingInfo = new Night.FileSystemInfo(Night.FileType.Directory, 0, 0); // Initial dummy type - var result = Night.Filesystem.GetInfo(this.testFilePath, Night.FileType.File, existingInfo); - - Assert.NotNull(result); - Assert.Same(existingInfo, result); - Assert.Equal(Night.FileType.File, existingInfo.Type); - } - - /// - /// Tests that GetInfo returns null when populating an existing object if path exists but type filter doesn't match. - /// - [Fact] - public void GetInfo_PopulatesExistingObjectWithFilter_PathExistsButWrongType_ReturnsNull() - { - var existingInfo = new Night.FileSystemInfo(Night.FileType.File, 123, DateTime.UtcNow.Ticks); - var originalType = existingInfo.Type; - var originalSize = existingInfo.Size; - var originalModTime = existingInfo.ModTime; - - var result = Night.Filesystem.GetInfo(this.testFilePath, Night.FileType.Directory, existingInfo); - - Assert.Null(result); - Assert.Equal(originalType, existingInfo.Type); - Assert.Equal(originalSize, existingInfo.Size); - Assert.Equal(originalModTime, existingInfo.ModTime); - } - - /// - /// Tests that ReadBytes returns the correct byte array for an existing file. - /// - [Fact] - public void ReadBytes_ExistingFile_ReturnsCorrectBytes() - { - var expectedBytes = File.ReadAllBytes(this.testFilePath); - var actualBytes = Night.Filesystem.ReadBytes(this.testFilePath); - Assert.Equal(expectedBytes, actualBytes); - } - - /// - /// Tests that ReadBytes throws FileNotFoundException for a non-existent file. - /// - [Fact] - public void ReadBytes_NonExistentFile_ThrowsFileNotFound() - { - var executionPath = Path.GetDirectoryName(typeof(FilesystemTests).Assembly.Location); - var testDirFullPath = Path.Combine(executionPath!, TestDir); - _ = Assert.Throws(() => Night.Filesystem.ReadBytes(Path.Combine(testDirFullPath, "no_such_file.dat"))); - } - - /// - /// Tests that ReadText returns the correct string for an existing file (UTF-8). - /// - [Fact] - public void ReadText_ExistingFile_ReturnsCorrectText() - { - var expectedText = File.ReadAllText(this.testFilePath, Encoding.UTF8); - var actualText = Night.Filesystem.ReadText(this.testFilePath); - Assert.Equal(expectedText, actualText); - } - - /// - /// Tests that ReadText throws FileNotFoundException for a non-existent file. - /// - [Fact] - public void ReadText_NonExistentFile_ThrowsFileNotFound() - { - var executionPath = Path.GetDirectoryName(typeof(FilesystemTests).Assembly.Location); - var testDirFullPath = Path.Combine(executionPath!, TestDir); - _ = Assert.Throws(() => Night.Filesystem.ReadText(Path.Combine(testDirFullPath, "no_such_file.txt"))); - } -} diff --git a/tests/Night.Tests/Graphics/GraphicsTests.cs b/tests/Night.Tests/Graphics/GraphicsTests.cs deleted file mode 100644 index b0d264df..00000000 --- a/tests/Night.Tests/Graphics/GraphicsTests.cs +++ /dev/null @@ -1,349 +0,0 @@ -// -// zlib license -// -// Copyright (c) 2025 Danny Solivan, Night Circle -// -// This software is provided 'as-is', without any express or implied -// warranty. In no event will the authors be held liable for any damages -// arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose, -// including commercial applications, and to alter it and redistribute it -// freely, subject to the following restrictions: -// -// 1. The origin of this software must not be misrepresented; you must not -// claim that you wrote the original software. If you use this software -// in a product, an acknowledgment in the product documentation would be -// appreciated but is not required. -// 2. Altered source versions must be plainly marked as such, and must not be -// misrepresented as being the original software. -// 3. This notice may not be removed or altered from any source distribution. -// - -using System; - -using Night; // Assuming Night.Graphics is in the Night namespace - -using Xunit; - -// Since Night.Window.RendererPtr is crucial and often checked for null, -// and we can't easily initialize a renderer in unit tests, -// we might need a way to simulate its state or test paths that handle null. -// For now, tests will assume RendererPtr is null if not otherwise set by a (future) test helper. -namespace Night.Tests.Graphics -{ - /// - /// Contains unit tests for the class. - /// - public class GraphicsTests - { - // Helper to simulate a null Window.RendererPtr scenario if needed, - // though direct manipulation of static members of other classes in tests can be tricky. - // For now, we rely on the default state or what Night.Window provides. - - /// - /// Tests that returns null when the file path is null. - /// - [Fact] - public void NewImage_NullFilePath_ReturnsNull() - { - // Act - var image = Night.Graphics.NewImage(null!); // Pass null with null-forgiving operator - - // Assert - Assert.Null(image); - } - - /// - /// Tests that returns null for a non-existent file. - /// - [Fact] - public void NewImage_NonExistentFile_ReturnsNull() - { - // Arrange - string nonExistentFilePath = "path/to/non_existent_image.png"; - - // Act - var image = Night.Graphics.NewImage(nonExistentFilePath); - - // Assert - Assert.Null(image); - } - - // Note: Testing NewImage success case requires a valid SDL renderer and an actual file, - // which is more of an integration test. Unit tests focus on C# logic paths. - - /// - /// Tests that - /// does not throw an exception when the provided sprite is null. - /// - [Fact] - public void Draw_NullSprite_DoesNotThrow() - { - // Arrange - Sprite nullSprite = null!; - - // Act & Assert - // We expect it to return early without throwing an exception. - var exception = Record.Exception(() => Night.Graphics.Draw(nullSprite, 0, 0)); - Assert.Null(exception); - } - - /// - /// Tests that - /// does not throw an exception when the sprite's texture is null (IntPtr.Zero). - /// - [Fact] - public void Draw_SpriteWithNullTexture_DoesNotThrow() - { - // Arrange - // Create a Sprite instance but simulate its internal Texture being null. - // This requires Sprite to have a constructor or a way to be instantiated - // for testing purposes, even if its Texture is not valid. - // Assuming Sprite constructor allows creating an instance that might later be found to have a null texture. - // If Sprite constructor itself throws on null texture, this test needs adjustment. - // For now, let's assume we can create a 'dummy' sprite. - // A more direct way would be if Sprite had an internal/test constructor or if we used a mock. - // Given the current Sprite structure, direct instantiation for this specific case is hard. - // Let's assume a scenario where a Sprite object exists but its Texture is somehow null. - // This test is more conceptual without deeper mocking/refactoring of Sprite for testability. - // For now, we'll rely on the null check for the sprite object itself. - // A more robust test would involve a Sprite instance where sprite.Texture is IntPtr.Zero. - // This might require a test-specific constructor or property setter on Sprite. - // As Graphics.Draw checks `sprite.Texture == IntPtr.Zero`, we can simulate this if Sprite allows. - // Let's assume a Sprite can be created with a zero IntPtr texture for testing. - var spriteWithNullTexture = new Sprite(IntPtr.Zero, 0, 0); // This matches the constructor - - // Act & Assert - var exception = Record.Exception(() => Night.Graphics.Draw(spriteWithNullTexture, 0, 0)); - Assert.Null(exception); // Expect console output, but no throw - } - - /// - /// Tests that - /// does not throw an exception for invalid (zero or negative) dimensions. - /// - /// The width of the rectangle. - /// The height of the rectangle. - [Theory] - [InlineData(0, 10)] - [InlineData(10, 0)] - [InlineData(-1, 10)] - [InlineData(10, -1)] - public void Rectangle_InvalidDimensions_DoesNotThrow(float width, float height) - { - // Act & Assert - var exception = Record.Exception(() => Night.Graphics.Rectangle(DrawMode.Fill, 0, 0, width, height)); - Assert.Null(exception); // Expects to return early - } - - /// - /// Tests that - /// does not throw an exception when the points array is null. - /// - [Fact] - public void Line_NullPointsArray_DoesNotThrow() - { - // Act & Assert - var exception = Record.Exception(() => Night.Graphics.Line(null!)); - Assert.Null(exception); // Expects console output and early return - } - - /// - /// Tests that - /// does not throw an exception when the points array contains fewer than two points. - /// - [Fact] - public void Line_PointsArrayWithLessThanTwoPoints_DoesNotThrow() - { - // Arrange - var points = new PointF[] { new PointF(0, 0) }; - - // Act & Assert - var exception = Record.Exception(() => Night.Graphics.Line(points)); - Assert.Null(exception); // Expects console output and early return - } - - /// - /// Tests that - /// does not throw an exception when the vertices array is null. - /// - [Fact] - public void Polygon_NullVerticesArray_DoesNotThrow() - { - // Act & Assert - var exception = Record.Exception(() => Night.Graphics.Polygon(DrawMode.Fill, null!)); - Assert.Null(exception); // Expects console output and early return - } - - /// - /// Tests that - /// does not throw an exception when the vertices array contains fewer than three vertices. - /// - [Fact] - public void Polygon_VerticesArrayWithLessThanThreeVertices_DoesNotThrow() - { - // Arrange - var vertices = new PointF[] { new PointF(0, 0), new PointF(1, 1) }; - - // Act & Assert - var exception = Record.Exception(() => Night.Graphics.Polygon(DrawMode.Fill, vertices)); - Assert.Null(exception); // Expects console output and early return - } - - /// - /// Tests that - /// uses a default segment count and does not throw for invalid (zero or negative) segment inputs. - /// - /// The number of segments for the circle. - [Theory] - [InlineData(0)] - [InlineData(-1)] - public void Circle_InvalidSegments_UsesDefaultAndDoesNotThrow(int segments) - { - // Act & Assert - // This test also implicitly checks if RendererPtr is null, it should not throw. - var exception = Record.Exception(() => Night.Graphics.Circle(DrawMode.Line, 0, 0, 10, segments)); - Assert.Null(exception); - } - - /// - /// Tests that - /// uses a zero radius and does not throw if a negative radius is provided. - /// - [Fact] - public void Circle_NegativeRadius_UsesZeroRadiusAndDoesNotThrow() - { - // Act & Assert - var exception = Record.Exception(() => Night.Graphics.Circle(DrawMode.Line, 0, 0, -10, 12)); - Assert.Null(exception); - } - - // Tests for methods when RendererPtr is null - // These assume Window.RendererPtr is null by default in a test environment - // or requires a specific setup to make it non-null for other tests. - - /// - /// Tests that - /// does not throw an exception when the renderer is null. - /// - [Fact] - public void SetColor_NullRenderer_DoesNotThrow() - { - // Act & Assert - var exception = Record.Exception(() => Night.Graphics.SetColor(new Color(255, 255, 255, 255))); - Assert.Null(exception); // Expects console output - } - - /// - /// Tests that - /// does not throw an exception when the renderer is null. - /// - [Fact] - public void Rectangle_NullRenderer_DoesNotThrow() - { - // Act & Assert - var exception = Record.Exception(() => Night.Graphics.Rectangle(DrawMode.Fill, 0, 0, 10, 10)); - Assert.Null(exception); // Expects console output - } - - /// - /// Tests that - /// does not throw an exception when the renderer is null. - /// - [Fact] - public void Line_Single_NullRenderer_DoesNotThrow() - { - // Act & Assert - var exception = Record.Exception(() => Night.Graphics.Line(0, 0, 1, 1)); - Assert.Null(exception); // Expects console output - } - - /// - /// Tests that - /// does not throw an exception when the renderer is null. - /// - [Fact] - public void Line_Multiple_NullRenderer_DoesNotThrow() - { - // Arrange - var points = new PointF[] { new PointF(0, 0), new PointF(1, 1), new PointF(2, 2) }; - - // Act & Assert - var exception = Record.Exception(() => Night.Graphics.Line(points)); - Assert.Null(exception); // Expects console output - } - - /// - /// Tests that - /// does not throw an exception when the renderer is null. - /// - [Fact] - public void Polygon_NullRenderer_DoesNotThrow() - { - // Arrange - var vertices = new PointF[] { new PointF(0, 0), new PointF(1, 0), new PointF(0, 1) }; - - // Act & Assert - var exception = Record.Exception(() => Night.Graphics.Polygon(DrawMode.Line, vertices)); - Assert.Null(exception); // Expects console output - } - - /// - /// Tests that - /// does not throw an exception when the renderer is null. - /// - [Fact] - public void Circle_NullRenderer_DoesNotThrow() - { - // Act & Assert - var exception = Record.Exception(() => Night.Graphics.Circle(DrawMode.Fill, 0, 0, 10, 12)); - Assert.Null(exception); // Expects console output - } - - /// - /// Tests that - /// does not throw an exception when the renderer is null. - /// This also covers the case where the sprite itself might be valid but the renderer isn't. - /// - [Fact] - public void Draw_NullRenderer_DoesNotThrow() - { - // Arrange - // Assume a Sprite can be created even if it cannot be effectively drawn without a renderer. - // This Sprite constructor will need to handle such cases gracefully or be mockable. - // For this test, we are focusing on the Graphics.Draw method's behavior. - // If Sprite needs a valid texture path for construction, this test setup needs to be adjusted. - // Let's assume we can create a dummy sprite for this test as we did for Draw_SpriteWithNullTexture - var dummySprite = new Sprite(IntPtr.Zero, 10, 10); // Or any valid-looking sprite that doesn't rely on renderer for creation - - // Act & Assert - var exception = Record.Exception(() => Night.Graphics.Draw(dummySprite, 0, 0)); - Assert.Null(exception); // Expects console output - } - - /// - /// Tests that - /// does not throw an exception when the renderer is null. - /// - [Fact] - public void Clear_NullRenderer_DoesNotThrow() - { - // Act & Assert - var exception = Record.Exception(() => Night.Graphics.Clear(new Color(0, 0, 0, 255))); - Assert.Null(exception); // Expects console output - } - - /// - /// Tests that - /// does not throw an exception when the renderer is null. - /// - [Fact] - public void Present_NullRenderer_DoesNotThrow() - { - // Act & Assert - var exception = Record.Exception(() => Night.Graphics.Present()); - Assert.Null(exception); // Expects console output - } - } -} diff --git a/tests/Night.Tests/Keyboard/KeyboardTests.cs b/tests/Night.Tests/Keyboard/KeyboardTests.cs deleted file mode 100644 index ec5b98e4..00000000 --- a/tests/Night.Tests/Keyboard/KeyboardTests.cs +++ /dev/null @@ -1,118 +0,0 @@ -// -// zlib license -// -// Copyright (c) 2025 Danny Solivan, Night Circle -// -// This software is provided 'as-is', without any express or implied -// warranty. In no event will the authors be held liable for any damages -// arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose, -// including commercial applications, and to alter it and redistribute it -// freely, subject to the following restrictions: -// -// 1. The origin of this software must not be misrepresented; you must not -// claim that you wrote the original software. If you use this software -// in a product, an acknowledgment in the product documentation would be -// appreciated but is not required. -// 2. Altered source versions must be plainly marked as such, and must not be -// misrepresented as being the original software. -// 3. This notice may not be removed or altered from any source distribution. -// - -using System; - -using Night; - -using SDL3; - -using Xunit; - -namespace Night.Tests.Keyboard -{ - /// - /// Contains unit tests for the class. - /// - public class KeyboardTests - { - /// - /// Tests that returns false - /// when the input system is not initialized. - /// - [Fact] - public void IsDown_InputSystemNotInitialized_ReturnsFalse() - { - // Arrange - bool originalInputState = Framework.IsInputInitialized; - Framework.IsInputInitialized = false; - - // Act - bool result = Night.Keyboard.IsDown(Night.KeyCode.A); - - // Assert - Assert.False(result); - - // Cleanup - Framework.IsInputInitialized = originalInputState; - } - - /// - /// Tests that returns false - /// for an unknown KeyCode. - /// - [Fact] - public void IsDown_UnknownKeyCode_ReturnsFalse() - { - // Arrange - bool originalInputState = Framework.IsInputInitialized; - Framework.IsInputInitialized = true; // Ensure this check passes - - // Act - // Cast an SDL.Scancode that is explicitly 'Unknown' to Night.KeyCode - // This simulates a scenario where a KeyCode might not have a valid mapping. - bool result = Night.Keyboard.IsDown((Night.KeyCode)SDL3.SDL.Scancode.Unknown); - - // Assert - Assert.False(result); - - // Cleanup - Framework.IsInputInitialized = originalInputState; - } - - /// - /// Tests that returns false - /// when the scancode is out of bounds for the keyboard state array. - /// - [Fact] - public void IsDown_ScancodeOutOfBounds_ReturnsFalse() - { - // Arrange - bool originalInputState = Framework.IsInputInitialized; - Framework.IsInputInitialized = true; // Ensure this check passes - - // Act - // Using a large integer value cast to KeyCode to simulate an out-of-bounds scancode. - // SDL.GetKeyboardState() would return an array, and if the scancode index is - // outside this array's bounds, the method should handle it gracefully. - // The actual Keyboard.cs code checks against keyboardState.Length. - // We are testing the C# logic path for this condition. - bool result = Night.Keyboard.IsDown((Night.KeyCode)int.MaxValue); - - // Assert - Assert.False(result); - - // Cleanup - Framework.IsInputInitialized = originalInputState; - } - - // Note: Testing the scenario where SDL.GetKeyboardState() itself returns null - // is difficult without mocking SDL, which is avoided per the testing plan. - // The Keyboard.IsDown() method already includes a null check for keyboardState - // (Keyboard.cs line 52), so this path is covered by defensive programming. - - // Note: Testing the scenario where a key is actually reported as "down" by SDL - // (i.e., keyboardState[(int)sdlScancode] is true) is also beyond the scope of - // these unit tests as it would require OS-level interaction or SDL mocking. - // The tests focus on the C# logic paths within Night.Keyboard.IsDown(). - } -} diff --git a/tests/Night.Tests/Mouse/MouseTests.cs b/tests/Night.Tests/Mouse/MouseTests.cs deleted file mode 100644 index 4d55d20d..00000000 --- a/tests/Night.Tests/Mouse/MouseTests.cs +++ /dev/null @@ -1,335 +0,0 @@ -// -// zlib license -// -// Copyright (c) 2025 Danny Solivan, Night Circle -// -// This software is provided 'as-is', without any express or implied -// warranty. In no event will the authors be held liable for any damages -// arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose, -// including commercial applications, and to alter it and redistribute it -// freely, subject to the following restrictions: -// -// 1. The origin of this software must not be misrepresented; you must not -// claim that you wrote the original software. If you use this software -// in a product, an acknowledgment in the product documentation would be -// appreciated but is not required. -// 2. Altered source versions must be plainly marked as such, and must not be -// misrepresented as being the original software. -// 3. This notice may not be removed or altered from any source distribution. -// - -using System; -using System.IO; - -using Night; - -using Xunit; - -namespace Night.Tests.Mouse -{ - /// - /// Contains unit tests for the class. - /// These tests focus on the C# logic paths and parameter validation, - /// without relying on a fully initialized SDL environment or actual mouse hardware. - /// - [Trait("Module", "Mouse")] - public class MouseTests : IDisposable - { - private readonly StringWriter stringWriter; - private readonly TextWriter originalOutput; - - /// - /// Initializes a new instance of the class. - /// Sets up console output redirection. - /// - public MouseTests() - { - this.stringWriter = new StringWriter(); - this.originalOutput = Console.Out; - Console.SetOut(this.stringWriter); - - // Ensure a clean state for IsInputInitialized before each test - Framework.IsInputInitialized = false; - - // Window.Handle will be nint.Zero by default in a test context - // unless Window.SetMode is called and succeeds. - } - - /// - /// Cleans up resources after each test, restoring console output - /// and resetting framework state. - /// - public void Dispose() - { - Console.SetOut(this.originalOutput); - this.stringWriter.Dispose(); - Framework.IsInputInitialized = false; // Reset for other test classes - } - - /// - /// Tests that returns false and logs a warning - /// when the input system is not initialized. - /// - [Fact] - public void IsDown_InputNotInitialized_ReturnsFalseAndLogsWarning() - { - // Arrange - Framework.IsInputInitialized = false; - - // Act - bool result = Night.Mouse.IsDown(MouseButton.Left); - - // Assert - Assert.False(result); - Assert.Contains("Warning: Night.Mouse.IsDown called before input system is initialized. Returning false.", this.stringWriter.ToString()); - } - - /// - /// Tests that returns false - /// when an unknown mouse button is queried, even if input is initialized. - /// - [Fact] - public void IsDown_UnknownButton_ReturnsFalse() - { - // Arrange - Framework.IsInputInitialized = true; // Simulate initialized input - - // Act - bool result = Night.Mouse.IsDown(MouseButton.Unknown); - - // Assert - Assert.False(result); - } - - /// - /// Tests that returns false - /// for a known button when input is initialized but SDL isn't fully mocked (expects SDL.GetMouseState to be callable). - /// This test primarily ensures no crash and that the logic path for known buttons is hit. - /// Actual button state depends on SDL and is out of scope for pure C# unit tests. - /// - /// The mouse button to test. - [Theory] - [InlineData(MouseButton.Left)] - [InlineData(MouseButton.Right)] - [InlineData(MouseButton.Middle)] - [InlineData(MouseButton.X1)] - [InlineData(MouseButton.X2)] - public void IsDown_KnownButtonInputInitialized_ReturnsFalseByDefault(MouseButton button) - { - // Arrange - Framework.IsInputInitialized = true; - - // Act - // In a test environment without actual SDL mouse state, this should default to false. - // We are primarily testing that the switch statement and SDL call path doesn't crash. - bool result = Night.Mouse.IsDown(button); - - // Assert - Assert.False(result); // Assuming no buttons are pressed in a bare test environment - } - - /// - /// Tests that returns (0,0) and logs a warning - /// when the input system is not initialized. - /// - [Fact] - public void GetPosition_InputNotInitialized_ReturnsZeroZeroAndLogsWarning() - { - // Arrange - Framework.IsInputInitialized = false; - - // Act - var (x, y) = Night.Mouse.GetPosition(); - - // Assert - Assert.Equal(0, x); - Assert.Equal(0, y); - Assert.Contains("Warning: Night.Mouse.GetPosition called before input system is initialized.", this.stringWriter.ToString()); - } - - /// - /// Tests that returns (0,0) by default - /// when input is initialized but SDL isn't fully mocked. - /// This test primarily ensures no crash. Actual position depends on SDL. - /// - [Fact] - public void GetPosition_InputInitialized_ReturnsZeroZeroByDefault() - { - // Arrange - Framework.IsInputInitialized = true; - - // Act - // In a test environment without actual SDL mouse state, this should default to (0,0) - // after SDL.GetMouseState is called. - var (x, y) = Night.Mouse.GetPosition(); - - // Assert - Assert.Equal(0, x); - Assert.Equal(0, y); - } - - /// - /// Tests that does not throw an exception - /// when the input system is not initialized. - /// - [Fact] - public void SetVisible_InputNotInitialized_DoesNotThrow() - { - // Arrange - Framework.IsInputInitialized = false; - - // Act - Exception? ex = Record.Exception(() => Night.Mouse.SetVisible(true)); - - // Assert - Assert.Null(ex); - } - - /// - /// Tests that does not throw an exception - /// when the input system is initialized. Actual SDL call behavior is not asserted. - /// - /// The visibility state to test. - [Theory] - [InlineData(true)] - [InlineData(false)] - public void SetVisible_InputInitialized_DoesNotThrow(bool visible) - { - // Arrange - Framework.IsInputInitialized = true; - - // Act - Exception? ex = Record.Exception(() => Night.Mouse.SetVisible(visible)); - - // Assert - Assert.Null(ex); - } - - /// - /// Tests that does not throw an exception - /// when the input system is not initialized. - /// - [Fact] - public void SetGrabbed_InputNotInitialized_DoesNotThrow() - { - // Arrange - Framework.IsInputInitialized = false; - - // Act - Exception? ex = Record.Exception(() => Night.Mouse.SetGrabbed(true)); - - // Assert - Assert.Null(ex); - } - - /// - /// Tests that does not throw an exception - /// when the input system is initialized but the window handle is zero. - /// - [Fact] - public void SetGrabbed_WindowHandleZero_DoesNotThrow() - { - // Arrange - Framework.IsInputInitialized = true; - - // Window.Handle is nint.Zero by default in tests if Window.SetMode hasn't been called. - // Act - Exception? ex = Record.Exception(() => Night.Mouse.SetGrabbed(true)); - - // Assert - Assert.Null(ex); - } - - /// - /// Tests that does not throw an exception - /// when the input system and a (mocked/conceptual) window handle are initialized. - /// Actual SDL call behavior is not asserted. - /// - /// - /// This test assumes that if were non-zero, the SDL call would be attempted. - /// Since we cannot easily set Window.Handle to non-zero without a full SetMode, - /// this test is similar to WindowHandleZero if SetMode is not called. - /// The critical path tested here is that it doesn't fail before the SDL call. - /// - /// The grabbed state to test. - [Theory] - [InlineData(true)] - [InlineData(false)] - public void SetGrabbed_InputAndWindowInitialized_DoesNotThrow(bool grabbed) - { - // Arrange - Framework.IsInputInitialized = true; - - // For this test, we assume Window.Handle might be non-zero if SetMode was called. - // However, without calling SetMode, it remains Zero. The method should still not throw - // in its C# part. If Window.Handle is Zero, it returns early. - // If it were non-zero, it would attempt SDL_SetWindowMouseGrab. - // Act - Exception? ex = Record.Exception(() => Night.Mouse.SetGrabbed(grabbed)); - - // Assert - Assert.Null(ex); - } - - /// - /// Tests that does not throw an exception - /// when the input system is not initialized. - /// - [Fact] - public void SetRelativeMode_InputNotInitialized_DoesNotThrow() - { - // Arrange - Framework.IsInputInitialized = false; - - // Act - Exception? ex = Record.Exception(() => Night.Mouse.SetRelativeMode(true)); - - // Assert - Assert.Null(ex); - } - - /// - /// Tests that does not throw an exception - /// when the input system is initialized but the window handle is zero. - /// - [Fact] - public void SetRelativeMode_WindowHandleZero_DoesNotThrow() - { - // Arrange - Framework.IsInputInitialized = true; - - // Window.Handle is nint.Zero by default in tests. - // Act - Exception? ex = Record.Exception(() => Night.Mouse.SetRelativeMode(true)); - - // Assert - Assert.Null(ex); - } - - /// - /// Tests that does not throw an exception - /// when the input system and a (mocked/conceptual) window handle are initialized. - /// Actual SDL call behavior is not asserted. - /// - /// - /// Similar to SetGrabbed, this tests the C# path. - /// - /// The relative mode state to test. - [Theory] - [InlineData(true)] - [InlineData(false)] - public void SetRelativeMode_InputAndWindowInitialized_DoesNotThrow(bool enabled) - { - // Arrange - Framework.IsInputInitialized = true; - - // Act - Exception? ex = Record.Exception(() => Night.Mouse.SetRelativeMode(enabled)); - - // Assert - Assert.Null(ex); - } - } -} diff --git a/tests/Night.Tests/Night.Tests.csproj b/tests/Night.Tests/Night.Tests.csproj deleted file mode 100644 index fe9aadc9..00000000 --- a/tests/Night.Tests/Night.Tests.csproj +++ /dev/null @@ -1,66 +0,0 @@ - - - - net9.0 - Night.Tests - enable - enable - false - true - bin\$(Configuration)\$(TargetFramework)\Night.Tests.xml - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - SDL3.dll - PreserveNewest - - - libSDL3.dylib - PreserveNewest - - - libSDL3.so - PreserveNewest - - - - - SDL3_image.dll - PreserveNewest - - - libSDL3_image.dylib - PreserveNewest - - - libSDL3_image.so - PreserveNewest - - - - diff --git a/tests/Night.Tests/SDL/NightSDLTests.cs b/tests/Night.Tests/SDL/NightSDLTests.cs deleted file mode 100644 index 8aa63e9e..00000000 --- a/tests/Night.Tests/SDL/NightSDLTests.cs +++ /dev/null @@ -1,104 +0,0 @@ -// -// zlib license -// -// Copyright (c) 2025 Danny Solivan, Night Circle -// -// This software is provided 'as-is', without any express or implied -// warranty. In no event will the authors be held liable for any damages -// arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose, -// including commercial applications, and to alter it and redistribute it -// freely, subject to the following restrictions: -// -// 1. The origin of this software must not be misrepresented; you must not -// claim that you wrote the original software. If you use this software -// in a product, an acknowledgment in the product documentation would be -// appreciated but is not required. -// 2. Altered source versions must be plainly marked as such, and must not be -// misrepresented as being the original software. -// 3. This notice may not be removed or altered from any source distribution. -// - -using System; - -using Night; - -using Xunit; - -namespace Night.Tests.SDL -{ - /// - /// Contains unit tests for the class. - /// - public class NightSDLTests - { - /// - /// Tests that correctly parses a packed SDL version integer - /// into the "major.minor.patch" string format. - /// - [Fact] - public void GetVersion_ValidSDLPackedVersion_ReturnsCorrectStringFormat() - { - // Arrange - // SDL_VERSIONNUM(major, minor, patch) = ((major) * 1000000 + (minor) * 1000 + (patch)) - // Example: SDL 3.1.2 would be (3 * 1000000) + (1 * 1000) + 2 = 3001002 - // However, NightSDL.GetVersion() calls SDL.GetVersion() which returns a different packed format. - // SDL.GetVersion() returns: (major << 22) | (minor << 12) | patch - // Let's assume SDL.GetVersion() returned a value corresponding to 3.17.28 for testing the parsing logic. - // The actual SDL.GetVersion() is a P/Invoke, so we test NightSDL's parsing of its *output*. - // NightSDL.GetVersion() itself *re-parses* the int from SDL.GetVersion(). - // The current NightSDL.GetVersion() parsing logic is: - // int major = sdl_version / 1000000; - // int minor = (sdl_version / 1000) % 1000; - // int patch = sdl_version % 1000; - // So, we need to construct an input that fits this logic. - // int mockSDLVersionInt = (3 * 1000000) + (17 * 1000) + 28; // Simulates 3.17.28 based on NightSDL's parsing - // string expectedVersionString = "3.17.28"; - - // Act - // We cannot directly mock SDL.GetVersion() without more complex setups. - // Instead, we will test a helper method that encapsulates the parsing logic, - // or we acknowledge this test is limited to the re-parsing if SDL.GetVersion() was already called. - // For now, let's assume we are testing the parsing logic as it is in NightSDL.GetVersion() - // by calling it. This means the test relies on the actual linked SDL version if not careful. - // To truly unit test the parsing, NightSDL.GetVersion would need to take the int as a param, - // or SDL.GetVersion would need to be mockable. - // Given the constraints, we'll call NightSDL.GetVersion() and verify its output format, - // understanding it uses the real SDL version. The specific value check is less critical - // than the format and the fact that it doesn't throw. - // A more robust test would be to refactor NightSDL.GetVersion to: - // public static string GetVersion() { return ParseVersion(SDL.GetVersion()); } - // internal static string ParseVersion(int sdlVersion) { /* parsing logic */ } - // Then test ParseVersion directly. - - // For this iteration, we'll test the existing GetVersion. - // We can't control the input to SDL.GetVersion(), so we check the output format. - string actualVersionString = NightSDL.GetVersion(); - - // Assert - Assert.NotNull(actualVersionString); - Assert.Matches(@"^\d+\.\d+\.\d+$", actualVersionString); - - // If we could mock/control the input to the parsing part: - // Assert.Equal(expectedVersionString, NightSDL.ParseVersion(mockSDLVersionInt)); // Hypothetical - } - - /// - /// Tests that returns a string. - /// - [Fact] - public void GetError_WhenCalled_ReturnsString() - { - // Arrange - // No specific arrangement needed as SDL.GetError() state is external. - - // Act - string? error = NightSDL.GetError(); - - // Assert - Assert.NotNull(error); // Should return at least an empty string, not null. - _ = Assert.IsType(error); - } - } -} diff --git a/tests/Night.Tests/Timer/TimerTests.cs b/tests/Night.Tests/Timer/TimerTests.cs deleted file mode 100644 index 9fbbe215..00000000 --- a/tests/Night.Tests/Timer/TimerTests.cs +++ /dev/null @@ -1,352 +0,0 @@ -// -// zlib license -// -// Copyright (c) 2025 Danny Solivan, Night Circle -// -// This software is provided 'as-is', without any express or implied -// warranty. In no event will the authors be held liable for any damages -// arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose, -// including commercial applications, and to alter it and redistribute it -// freely, subject to the following restrictions: -// -// 1. The origin of this software must not be misrepresented; you must not -// claim that you wrote the original software. If you use this software -// in a product, an acknowledgment in the product documentation would be -// appreciated but is not required. -// 2. Altered source versions must be plainly marked as such, and must not be -// misrepresented as being the original software. -// 3. This notice may not be removed or altered from any source distribution. -// - -using System; -using System.Diagnostics; -using System.Threading; - -using Night; - -using SDL3; - -using Xunit; - -namespace Night.Tests.Timer -{ - /// - /// Contains unit tests for the class. - /// - public class TimerTests : IDisposable - { - private const double AcceptableTimeEpsilon = 0.15; // 150ms epsilon for time comparisons (increased due to CI variability) - private const float AcceptableDeltaEpsilon = 0.001f; // Epsilon for float delta comparisons - private readonly ulong initialPerformanceFrequency; - private readonly ulong initialLastStepTime; - private readonly float initialCurrentDelta; - private readonly double initialCurrentAverageDelta; - private readonly int initialCurrentFPS; - - /// - /// Initializes a new instance of the class. - /// Sets up SDL timer subsystem for the tests. - /// - public TimerTests() - { - // Initialize SDL with no specific flags, as timer functions are generally available. - if (!SDL3.SDL.Init(0)) - { - throw new InvalidOperationException($"SDL_Init Error: {SDL3.SDL.GetError()}"); - } - - // Store initial Timer static values to restore them later if tests modify them directly. - // This is important because Timer is a static class and state persists between tests. - this.initialPerformanceFrequency = Night.Timer.PerformanceFrequency; - this.initialLastStepTime = Night.Timer.LastStepTime; - this.initialCurrentDelta = Night.Timer.CurrentDelta; - this.initialCurrentAverageDelta = Night.Timer.CurrentAverageDelta; - this.initialCurrentFPS = Night.Timer.CurrentFPS; - - // TimerStartTime is readonly. - // For simplicity, we'll re-initialize Timer fully in Dispose or before specific tests - // if a test heavily manipulates static state in a way that Initialize() can't fix. - // For now, Initialize() should reset most things. - Night.Timer.Initialize(); // Ensure a baseline initialization - } - - /// - /// Disposes of resources used by the test class. - /// Quits the SDL timer subsystem. - /// - public void Dispose() - { - // Restore initial static values to ensure test isolation for subsequent test classes - // or if a test runner reuses the AppDomain. - Night.Timer.PerformanceFrequency = this.initialPerformanceFrequency; - Night.Timer.LastStepTime = this.initialLastStepTime; - Night.Timer.CurrentDelta = this.initialCurrentDelta; - Night.Timer.CurrentAverageDelta = this.initialCurrentAverageDelta; - Night.Timer.CurrentFPS = this.initialCurrentFPS; - - // TimerStartTime cannot be reset directly. - // Re-calling Initialize might be needed if tests mess with it too much, - // but GetTime() relies on the original TimerStartTime. - // For most tests, a fresh Initialize() at the start is good. - SDL3.SDL.QuitSubSystem(0); // Quit with no specific flags - SDL3.SDL.Quit(); - } - - /// - /// Tests that sets performance frequency and last step time. - /// - [Fact] - public void Initialize_WhenSdlTimerSubsystemIsActive_SetsPerformanceFrequencyAndLastStepTime() - { - Night.Timer.Initialize(); - Assert.True(Night.Timer.PerformanceFrequency > 0, "PerformanceFrequency should be greater than 0."); - Assert.True(Night.Timer.LastStepTime > 0, "LastStepTime should be greater than 0."); - } - - /// - /// Tests that returns a non-negative value after initialization. - /// - [Fact] - public void GetTime_AfterInitialization_ReturnsNonNegativeValue() - { - Night.Timer.Initialize(); - double time = Night.Timer.GetTime(); - Assert.True(time >= 0.0, $"GetTime() returned {time}, expected non-negative."); - } - - /// - /// Tests that returns an increased value after waiting. - /// - [Fact] - public void GetTime_AfterWaiting_ReturnsIncreasedValue() - { - Night.Timer.Initialize(); - double startTime = Night.Timer.GetTime(); - Thread.Sleep(10); // Wait for 10 milliseconds - double endTime = Night.Timer.GetTime(); - Assert.True(endTime > startTime, $"endTime ({endTime}) was not greater than startTime ({startTime})."); - } - - /// - /// Tests that returns zero when performance frequency is zero. - /// - [Fact] - public void GetTime_WhenPerformanceFrequencyIsZero_ReturnsZero() - { - Night.Timer.Initialize(); // Initialize to get a valid LastStepTime etc. - ulong originalFrequency = Night.Timer.PerformanceFrequency; // Store to restore - Night.Timer.PerformanceFrequency = 0; - try - { - double time = Night.Timer.GetTime(); - Assert.Equal(0.0, time); - } - finally - { - Night.Timer.PerformanceFrequency = originalFrequency; // Restore - } - } - - /// - /// Tests that returns the correct internally set FPS. - /// - [Fact] - public void GetFPS_WhenCurrentFPSSetInternally_ReturnsCorrectValue() - { - Night.Timer.Initialize(); - Night.Timer.CurrentFPS = 30; - Assert.Equal(30, Night.Timer.GetFPS()); - Night.Timer.CurrentFPS = 60; - Assert.Equal(60, Night.Timer.GetFPS()); - } - - /// - /// Tests that returns the correct internally set delta. - /// - [Fact] - public void GetDelta_WhenCurrentDeltaSetInternally_ReturnsCorrectValue() - { - Night.Timer.Initialize(); - Night.Timer.CurrentDelta = 0.016f; - Assert.Equal(0.016f, Night.Timer.GetDelta(), AcceptableDeltaEpsilon); - Night.Timer.CurrentDelta = 0.032f; - Assert.Equal(0.032f, Night.Timer.GetDelta(), AcceptableDeltaEpsilon); - } - - /// - /// Tests that returns the correct internally set average delta. - /// - [Fact] - public void GetAverageDelta_WhenCurrentAverageDeltaSetInternally_ReturnsCorrectValue() - { - Night.Timer.Initialize(); - Night.Timer.CurrentAverageDelta = 0.033; - Assert.Equal(0.033, Night.Timer.GetAverageDelta(), 5); // Default precision for double - Night.Timer.CurrentAverageDelta = 0.017; - Assert.Equal(0.017, Night.Timer.GetAverageDelta(), 5); - } - - /// - /// Tests that pauses execution for approximately the specified positive duration. - /// - [Fact] - public void Sleep_WithPositiveDuration_PausesExecutionApproximately() - { - Night.Timer.Initialize(); - double sleepDurationSeconds = 0.02; // 20 ms - Stopwatch sw = Stopwatch.StartNew(); - Night.Timer.Sleep(sleepDurationSeconds); - sw.Stop(); - double elapsedSeconds = sw.Elapsed.TotalSeconds; - Assert.True(elapsedSeconds >= sleepDurationSeconds - 0.005, $"Sleep was too short. Expected ~{sleepDurationSeconds}, got {elapsedSeconds}"); - - // Allow for some overhead, so don't check upper bound too strictly - Assert.True(elapsedSeconds < sleepDurationSeconds + AcceptableTimeEpsilon, $"Sleep was too long. Expected ~{sleepDurationSeconds}, got {elapsedSeconds}"); - } - - /// - /// Tests that with zero duration returns immediately. - /// - [Fact] - public void Sleep_WithZeroDuration_ReturnsImmediately() - { - Night.Timer.Initialize(); - Stopwatch sw = Stopwatch.StartNew(); - Night.Timer.Sleep(0); - sw.Stop(); - Assert.True(sw.Elapsed.TotalSeconds < AcceptableTimeEpsilon, $"Sleep(0) took too long: {sw.Elapsed.TotalSeconds}s"); - } - - /// - /// Tests that with negative duration returns immediately. - /// - [Fact] - public void Sleep_WithNegativeDuration_ReturnsImmediately() - { - Night.Timer.Initialize(); - Stopwatch sw = Stopwatch.StartNew(); - Night.Timer.Sleep(-1.0); - sw.Stop(); - Assert.True(sw.Elapsed.TotalSeconds < AcceptableTimeEpsilon, $"Sleep(-1.0) took too long: {sw.Elapsed.TotalSeconds}s"); - } - - /// - /// Tests that after initialization returns a small positive delta and updates CurrentDelta. - /// - [Fact] - public void Step_AfterInitialization_ReturnsSmallPositiveDeltaAndUpdateCurrentDelta() - { - Night.Timer.Initialize(); - - // Allow a very small delay for the first step to be non-zero - Thread.Sleep(1); - double stepDelta = Night.Timer.Step(); - - Assert.True(stepDelta > 0, $"stepDelta ({stepDelta}) should be positive."); - - // Max clamp is 0.0666 - Assert.True(stepDelta < 0.0667, $"stepDelta ({stepDelta}) should be less than max clamp value initially."); - Assert.Equal((float)stepDelta, Night.Timer.GetDelta(), AcceptableDeltaEpsilon); - } - - /// - /// Tests that called sequentially updates delta and last step time. - /// - [Fact] - public void Step_CalledSequentially_UpdatesDeltaAndLastStepTime() - { - Night.Timer.Initialize(); - - // Ensure first step is non-zero - Thread.Sleep(1); - _ = Night.Timer.Step(); - ulong lastStepTime1 = Night.Timer.LastStepTime; - - // Wait a bit for time to pass - Thread.Sleep(5); - - _ = Night.Timer.Step(); - ulong lastStepTime2 = Night.Timer.LastStepTime; - float delta2 = Night.Timer.GetDelta(); - - Assert.True(lastStepTime2 > lastStepTime1, "LastStepTime should increase after sequential Step calls."); - Assert.True(delta2 > 0, "Second delta should be positive."); - - // Delta can be similar if calls are very fast, so primarily check LastStepTime update - } - - /// - /// Tests that clamps delta to max when calculated delta exceeds it. - /// - [Fact] - public void Step_WhenCalculatedDeltaExceedsMax_ClampsDeltaToMax() - { - Night.Timer.Initialize(); - ulong perfFrequency = Night.Timer.PerformanceFrequency; - - // Guard against invalid frequency from SDL_Init failure or mock - if (perfFrequency == 0 || perfFrequency == 1) - { - Night.Timer.PerformanceFrequency = 1000000000; // Mock a realistic frequency if needed - perfFrequency = Night.Timer.PerformanceFrequency; - } - - // Simulate 1 second having passed since last step - Night.Timer.LastStepTime = SDL3.SDL.GetPerformanceCounter() - perfFrequency; - - double stepDelta = Night.Timer.Step(); - const double expectedClampedDelta = 0.0666; - - Assert.Equal(expectedClampedDelta, stepDelta, 4); // Compare with tolerance - Assert.Equal((float)expectedClampedDelta, Night.Timer.GetDelta(), 4); - } - - /// - /// Tests that returns zero delta and sets LastStepTime when LastStepTime was zero. - /// - [Fact] - public void Step_WhenLastStepTimeIsZero_ReturnsZeroDeltaAndSetsLastStepTime() - { - // Standard init - Night.Timer.Initialize(); - - // Force condition - Night.Timer.LastStepTime = 0; - - double stepDelta = Night.Timer.Step(); - - Assert.Equal(0.0, stepDelta); - Assert.True(Night.Timer.LastStepTime > 0, "LastStepTime should be set after Step() if it was zero."); - Assert.Equal(0.0f, Night.Timer.GetDelta()); - } - - /// - /// Tests that returns zero delta when PerformanceFrequency is zero. - /// - [Fact] - public void Step_WhenPerformanceFrequencyIsZero_ReturnsZeroDelta() - { - // Standard init - Night.Timer.Initialize(); - ulong originalFrequency = Night.Timer.PerformanceFrequency; - - // Force condition - Night.Timer.PerformanceFrequency = 0; - Night.Timer.LastStepTime = SDL3.SDL.GetPerformanceCounter() - 1000; // Ensure LastStepTime is not zero - - try - { - double stepDelta = Night.Timer.Step(); - Assert.Equal(0.0, stepDelta); - Assert.Equal(0.0f, Night.Timer.GetDelta()); - } - finally - { - // Restore - Night.Timer.PerformanceFrequency = originalFrequency; - } - } - } -} diff --git a/tests/NightTest.csproj b/tests/NightTest.csproj new file mode 100644 index 00000000..7f7481fd --- /dev/null +++ b/tests/NightTest.csproj @@ -0,0 +1,115 @@ + + + + Library + net9.0 + enable + enable + 13.0 + NightTest +false + false + true + bin/$(Configuration)/$(TargetFramework)/NightTest.xml + $(NoWarn);SA1402 + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + PreserveNewest + + + PreserveNewest + + + + + + SDL3.dll + PreserveNewest + + + libSDL3.0.dylib + PreserveNewest + + + libSDL3.dylib + PreserveNewest + + + SDL3 + PreserveNewest + + + libSDL3.so.0 + PreserveNewest + + + libSDL3.so + PreserveNewest + + + SDL3 + PreserveNewest + + + + + SDL3_image.dll + PreserveNewest + + + libSDL3_image.0.dylib + PreserveNewest + + + libSDL3_image.dylib + PreserveNewest + + + libSDL3_image.so.0 + PreserveNewest + + + libSDL3_image.so + PreserveNewest + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/tests/app.manifest b/tests/app.manifest new file mode 100644 index 00000000..0d85b480 --- /dev/null +++ b/tests/app.manifest @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + PerMonitorV2, PerMonitor + True/PM + + + + diff --git a/tests/config.json b/tests/config.json new file mode 100644 index 00000000..62aaa9e3 --- /dev/null +++ b/tests/config.json @@ -0,0 +1,11 @@ +{ + "window": { + "title": "Night Test Window", + "width": 800, + "height": 600, + "resizable": true, + "vsync": false, + "fullscreen": false, + "borderless": false + } +} diff --git a/tests/Night.Tests/stylecop.json b/tests/stylecop.json similarity index 100% rename from tests/Night.Tests/stylecop.json rename to tests/stylecop.json diff --git a/tools/crunch/linux/crunch b/tools/crunch/linux/crunch deleted file mode 100644 index 3ea6550d..00000000 Binary files a/tools/crunch/linux/crunch and /dev/null differ diff --git a/tools/crunch/macos/crunch b/tools/crunch/macos/crunch deleted file mode 100644 index e6079590..00000000 Binary files a/tools/crunch/macos/crunch and /dev/null differ diff --git a/tools/crunch/windows/crunch.exe b/tools/crunch/windows/crunch.exe deleted file mode 100644 index b80ea7d3..00000000 Binary files a/tools/crunch/windows/crunch.exe and /dev/null differ