Adds plan to support other import / export formats on android.

This commit is contained in:
2026-09-07 17:03:53 -07:00
parent edc6415ad5
commit 60a26ba81d
2 changed files with 407 additions and 0 deletions

164
docs/Import-Export.md Normal file
View File

@@ -0,0 +1,164 @@
# Import & Export Architecture & Plan
This document outlines the modular architecture and step-by-step technical roadmap for handling monster stat block imports, exports, and sharing across multiple formats in the **Monster Cards** application.
---
## 1. Modular Importer Architecture
To keep import/export code clean and maintainable, all format parsers implement a shared interface:
```java
public interface EntityImporter<T> {
/**
* Determines whether the given input string or payload can be handled by this importer.
*/
boolean canImport(@NonNull String input);
/**
* Parses the raw input string into internal domain memory objects (e.g., Monster).
*/
@NonNull
T parse(@NonNull String input) throws Exception;
}
```
The output of any `EntityImporter` is an in-memory domain model (such as `Monster`), which is then passed to `MonsterImportFragment` for UI preview and Room database persistence.
---
## 2. Step-by-Step Implementation Roadmap
```
┌────────────────----------------────────────────────────┐
│ Step 0: Restrict File Extension Intent Filters │
└────────────────────────────────────────────────────────┘
│
v
┌────────────────----------------────────────────────────┐
│ Step 1: Refactor Import Code to Shared Interface │
└────────────────────────────────────────────────────────┘
│
v
┌────────────────----------------────────────────────────┐
│ Step 2: Update Tetra-cube Importer to Newest Format │
└────────────────────────────────────────────────────────┘
│
v
┌────────────────----------------────────────────────────┐
│ Step 3: Import from D&D Beyond Character URL │
└────────────────────────────────────────────────────────┘
│
v
┌────────────────----------------────────────────────────┐
│ Step 4: Export to Custom Internal Format (Open5e) │
└────────────────────────────────────────────────────────┘
│
v
┌────────────────----------------────────────────────────┐
│ Step 5: Import from Custom Internal Format (Open5e) │
└────────────────────────────────────────────────────────┘
│
v
┌────────────────----------------────────────────────────┐
│ Step 6: Generic Android Share Button Feature │
└────────────────────────────────────────────────────────┘
│
v
┌────────────────----------------────────────────────────┐
│ Step 7: Specific Share Targets (NFC, Bluetooth, URL) │
└────────────────────────────────────────────────────────┘
```
---
### Step 0: Restrict File Extension Intent Filters
Restrict the Android app so it reacts **only** to `.monster` and `.monster.txt` files instead of all generic `.txt` files.
1. **`AndroidManifest.xml`**:
Add `android:pathPattern` and `android:pathSuffix` constraints (`.monster` and `.monster.txt`) to the `<intent-filter>` for `MainActivity`.
2. **`MainActivity.java`**:
Implement runtime display name validation querying `OpenableColumns.DISPLAY_NAME` via `ContentResolver` to filter out non-monster files passed via `content://` URIs.
---
### Step 1: Refactor Import Code into Importer Architecture
Extract the current parsing logic out of `MonsterImportHelper.java` into a standalone importer class implementing `EntityImporter<Monster>`.
- **New Class**: `TetraCubeMonsterImporter implements EntityImporter<Monster>`
- **Responsibilities**:
- `canImport(json)`: Validates that the JSON string contains Tetra-cube properties (e.g. `hitDice`, `armorName`, `strPoints`).
- `parse(json)`: Converts the raw JSON payload into an in-memory `Monster` object.
---
### Step 2: Update Tetra-cube Importer to Newest Format
Update `TetraCubeMonsterImporter` to support newer fields from the Tetra-cube generator source (`js/statblock-script.js`):
- **New Fields to Parse**:
- `bonusActions`: Array of `{ name, desc }` objects $\rightarrow$ mapped to `monster.actions` or bonus action structure.
- `mythics` / `isMythic` / `mythicDescription`: Array of mythic action objects and intro text $\rightarrow$ mapped to `monster.legendaryActions`.
- `blind`: Parse boolean flag to append `"(blind beyond this radius)"` to `blindsight`.
- Section intro text: Parse `legendariesDescription`, `lairDescription`, `regionalDescription`.
---
### Step 3: Import from D&D Beyond Character URL
Allow users to import monsters or characters directly from a D&D Beyond URL.
- **Target URL Structure**:
`https://www.dndbeyond.com/characters/49074997` (where `49074997` is the internal character ID).
- **Service Endpoints**:
D&D Beyond character pages load data from background JSON endpoints such as:
`https://character-service.dndbeyond.com/character/v2/character/49074997`
- **New Importer Class**: `DnDBeyondImporter implements EntityImporter<Monster>`
- **Pipeline**:
1. Extract character ID (`49074997`) from user-provided D&D Beyond URL.
2. Perform an asynchronous HTTP request (via OkHttp/Retrofit) to fetch the character JSON payload.
3. Map D&D Beyond JSON fields (stats, modifiers, AC, HP, speed, actions, traits, spells) to our internal `Monster` memory model.
4. Pass the parsed `Monster` to `MonsterImportFragment` for review and persistence.
---
### Step 4: Export to Custom Internal Format (Open5e Specification)
Implement export capability to output monsters and collections using our two-tier Open5e JSON specification (`/Users/tom/Projects/TTRPG/CharacterDataFiles/rulesets/open5e/import-export.md`).
- **New Class**: `Open5eExporter`
- **Output Architecture**:
- **Universal Envelope (`entity.json`)**: `uuid`, `ruleset_id: "open5e"`, `entity_type: "character"`, `display_name`, `version`, `tags`, `properties`.
- **Character Payload (`character.json`)**: Export ability scores, combat vitals (AC, HP formula, speed), proficiency bonuses, actions, reactions, legendary actions, features, and equipped items (`weapon.json`, `armor.json`, `shield.json`, `spell.json`).
---
### Step 5: Import from Custom Internal Format (Open5e Specification)
Implement import capability for custom Open5e JSON files.
- **New Class**: `Open5eImporter implements EntityImporter<Monster>`
- **Pipeline**:
- `canImport(json)`: Validates `"ruleset_id": "open5e"` and `"entity_type": "character"`.
- `parse(json)`: Unpacks `properties` into internal `Monster` domain models.
---
### Step 6: Generic Android Share Button Feature
Implement a generic "Share" feature allowing users to export and share monsters or collections from detail screens.
- **UI Action**: Add a "Share" item to action menus in `MonsterDetailFragment` and `CollectionDetailFragment`.
- **Android Intent**: Uses standard Android `ACTION_SEND` intent with `Intent.EXTRA_STREAM` or `Intent.EXTRA_TEXT` to pass exported files to other apps (e.g. Email, Drive, Messaging, Files).
---
### Step 7: Specific Share Targets & Channels
Extend the generic sharing feature with specialized, direct sharing channels (to be implemented as individual sub-steps):
- **Sub-Step 7.1 (NFC Sharing)**: Share monster data directly between devices via NFC (NDEF records / Android Beam).
- **Sub-Step 7.2 (Bluetooth / Wi-Fi Direct)**: Share monster files directly between nearby Android devices via Bluetooth / Wi-Fi.
- **Sub-Step 7.3 (Web URL with Embedded Payload)**: Generate a shareable Web URL containing compressed/base64-encoded monster JSON data.

243
docs/ImportFromMonster.md Normal file
View File

@@ -0,0 +1,243 @@
# Import Process & Schema Documentation
> **Platform Note**: This documentation describes the import process and internal database schema for the **Android Application** (`com.majinnaibu.monstercards`).
---
## 1. Overview of the Import Process
The Monster Cards Android application can import monster stat blocks formatted in JSON from `.monster` or `.monster.txt` files shared or opened from other applications (such as file managers, messaging apps, or browsers).
```
+-------------------------------------------------------+
| External .monster / .monster.txt file (JSON Payload) |
+-------------------------------------------------------+
|
v
+-------------------------------------------------------+
| MainActivity (Intent Handling: ACTION_VIEW / SEND) |
| - Extracts raw JSON string from Content/File URI |
+-------------------------------------------------------+
|
v
+-------------------------------------------------------+
| MonsterImportFragment & MonsterImportViewModel |
| - MonsterImportHelper.fromJSON(json) parses JSON |
| - Maps JSON fields to internal Monster domain model |
| - Renders UI preview with calculated AC, HP, & Markdown|
+-------------------------------------------------------+
|
v
+-------------------------------------------------------+
| User clicks "Import" Menu Action |
| - Generates new UUID |
| - Persists Monster to Room Database (monsters table) |
| - Rebuilds FTS index (monsters_fts) |
+-------------------------------------------------------+
```
---
## 2. Source Format (.monster / .monster.txt) & File Extension Behavior
The `.monster` file format is generated by Tetra-cube's D&D 5e Statblock Generator (`statblock-script.js`).
### File Saving Behavior (`SavedData.SaveToFile`)
```javascript
SaveToFile: () => saveAs(new Blob([JSON.stringify(mon)], {
type: "text/plain;charset=utf-8"
}), mon.name.toLowerCase() + ".monster")
```
- **Desktop Browsers**: Save the file with the specified `.monster` filename.
- **Android Mobile Browsers & DownloadManager**: Because the `Blob` uses MIME type `text/plain;charset=utf-8`, Android file handlers append a `.txt` extension, creating `.monster.txt` files.
---
## 3. JSON Parsing & Field Mapping (`MonsterImportHelper.java`)
`MonsterImportHelper.fromJSON(String json)` uses Gson's `JsonParser` to parse the root JSON object and populate a `Monster` instance.
### Complete Field Mapping Reference
| JSON Field Name | Target `Monster` Field | Type / Default | Description / Conversion Logic |
| :--- | :--- | :--- | :--- |
| `name` | `monster.name` | `String` | Monster name |
| `size` | `monster.size` | `String` | Size string (e.g., `"medium"`, `"large"`) |
| `type` | `monster.type` | `String` | Creature type (e.g., `"humanoid"`, `"dragon"`) |
| `tag` | `monster.subtype` | `String` | Subtype / tag (e.g., `"elf"`, `"shapechanger"`) |
| `alignment` | `monster.alignment` | `String` | Alignment string (e.g., `"neutral good"`) |
| `hitDice` | `monster.hitDice` | `int` (default 1) | Number of hit dice |
| `armorName` | `monster.armorType` | `ArmorType` | Converted via `ArmorTypeConverter.armorTypeFromStringValue()` |
| `shieldBonus` | `monster.shieldBonus` | `int` | Shield AC bonus (+2 if present) |
| `natArmorBonus` | `monster.naturalArmorBonus` | `int` | Additional natural armor AC bonus |
| `otherArmorDesc` | `monster.otherArmorDescription` | `String` | Custom armor description string |
| `speed` | `monster.walkSpeed` | `int` | Walking speed in feet |
| `burrowSpeed` | `monster.burrowSpeed` | `int` | Burrowing speed in feet |
| `climbSpeed` | `monster.climbSpeed` | `int` | Climbing speed in feet |
| `flySpeed` | `monster.flySpeed` | `int` | Flying speed in feet |
| `hover` | `monster.canHover` | `boolean` | Whether flying creature can hover |
| `swimSpeed` | `monster.swimSpeed` | `int` | Swimming speed in feet |
| `customHP` | `monster.hasCustomHP` | `boolean` | Flag for override custom HP string |
| `customSpeed` | `monster.hasCustomSpeed` | `boolean` | Flag for override custom speed string |
| `hpText` | `monster.customHPDescription` | `String` | Custom HP text description |
| `speedDesc` | `monster.customSpeedDescription` | `String` | Custom speed text description |
| `strPoints` | `monster.strengthScore` | `int` (default 10) | Strength ability score |
| `dexPoints` | `monster.dexterityScore` | `int` (default 10) | Dexterity ability score |
| `conPoints` | `monster.constitutionScore` | `int` (default 10) | Constitution ability score |
| `intPoints` | `monster.intelligenceScore` | `int` (default 10) | Intelligence ability score |
| `wisPoints` | `monster.wisdomScore` | `int` (default 10) | Wisdom ability score |
| `chaPoints` | `monster.charismaScore` | `int` (default 10) | Charisma ability score |
| `blindsight` | `monster.senses` | `Set<String>` | Added as `"blindsight <N> ft."` if `N > 0` |
| `darkvision` | `monster.senses` | `Set<String>` | Added as `"darkvision <N> ft."` if `N > 0` |
| `tremorsense` | `monster.senses` | `Set<String>` | Added as `"tremorsense <N> ft."` if `N > 0` |
| `truesight` | `monster.senses` | `Set<String>` | Added as `"truesight <N> ft."` if `N > 0` |
| `telepathy` | `monster.telepathyRange` | `int` | Telepathy range in feet |
| `cr` | `monster.challengeRating` | `ChallengeRating` | Converted via `ChallengeRatingConverter` |
| `customCr` | `monster.customChallengeRatingDescription` | `String` | Custom CR text |
| `customProf` | `monster.customProficiencyBonus` | `int` | Custom proficiency bonus override |
| `abilities` | `monster.abilities` | `List<Trait>` | List of JSON objects `{ "name": "...", "desc": "..." }` |
| `actions` | `monster.actions` | `List<Trait>` | List of JSON objects `{ "name": "...", "desc": "..." }` |
| `bonusActions` | `monster.actions` *(Future)* | `List<Trait>` | Bonus actions array |
| `reactions` | `monster.reactions` | `List<Trait>` | List of JSON objects `{ "name": "...", "desc": "..." }` |
| `legendaries` | `monster.legendaryActions` | `List<Trait>` | List of JSON objects `{ "name": "...", "desc": "..." }` |
| `mythics` | `monster.legendaryActions` *(Future)* | `List<Trait>` | Mythic actions array |
| `lairs` | `monster.lairActions` | `List<Trait>` | List of JSON objects `{ "name": "...", "desc": "..." }` |
| `regionals` | `monster.regionalActions` | `List<Trait>` | List of JSON objects `{ "name": "...", "desc": "..." }` |
| `sthrows` | Saving throw proficiencies | `ProficiencyType` | Array of objects `{ "name": "str"\|"dex"\|"con"\|"int"\|"wis"\|"cha" }`. Sets proficiency to `PROFICIENT`. |
| `skills` | `monster.skills` | `Set<Skill>` | Array of `{ "name": "...", "stat": "...", "note": " (ex)" }`. `note == " (ex)"` sets `EXPERTISE`. |
| `damageTypes`, `specialdamage` | Immunities / Resistances / Vulnerabilities | `Set<String>` | Array of `{ "name": "...", "type": "i"\|"r"\|"v" }`. Sorted into immunities (`"i"`), resistances (`"r"`), or vulnerabilities (`"v"`). |
| `conditions` | `monster.conditionImmunities` | `Set<String>` | Array of `{ "name": "..." }` |
| `languages` | `monster.languages` | `Set<Language>` | Array of `{ "name": "...", "speaks": true\|false }` |
| `understandsBut` | `monster.understandsButDescription` | `String` | Language qualifier text |
---
## 4. Preview & Database Persistence
1. **ViewModel Computation (`MonsterImportViewModel.java`)**:
- Computes calculated stat block values:
- **AC**: Calculated based on `armorType`, dexterity modifier, shield bonus, natural armor bonus, or custom description.
- **HP**: Calculated as `floor(hitDice * ((dieSize + 1) / 2 + conModifier))` where `dieSize` is derived from size (e.g., Medium = d8, Large = d10).
- **Dynamic Placeholders**: Replaces placeholders in trait descriptions (e.g., `[STR ATK]`, `[WIS SAVE]`) with computed attack bonuses and spell save DCs based on ability scores and proficiency bonuses.
- **Markdown Rendering**: Trait and action descriptions are parsed through `CommonMarkHelper` to format inline HTML/Markdown for display.
2. **Persistence (`MonsterImportFragment.java`)**:
- When the user taps the **Import** action menu button (`R.id.menu_action_import_monster`):
1. Assigns a new `UUID` to `monster.id`.
2. Calls `MonsterRepository.addMonster(monster)` which executes an `INSERT` into the Room database asynchronously on an IO thread via RxJava3.
3. Shows a `Snackbar` confirming import success and navigates to the library/detail view for the imported monster.
---
## 5. Internal Database Schema (`monsters` Table)
The app uses **Room Database** (Database Name: `monsters`, Version: `5`).
### Table: `monsters`
| Column Name | SQLite Data Type | Default Value | Description / Room TypeConverter |
| :--- | :--- | :--- | :--- |
| `id` | `TEXT` | Primary Key | UUID string (`UUIDConverter`) |
| `name` | `TEXT` | `""` | Monster name |
| `size` | `TEXT` | `""` | Size category (e.g. `"medium"`) |
| `type` | `TEXT` | `""` | Creature type |
| `subtype` | `TEXT` | `""` | Creature tag / subtype |
| `alignment` | `TEXT` | `""` | Alignment |
| `strength_score` | `INTEGER` | `10` | Strength score |
| `strength_saving_throw_advantage` | `TEXT` | `"none"` | Advantage type (`"none"`, `"advantage"`, `"disadvantage"`) |
| `strength_saving_throw_proficiency` | `TEXT` | `"none"` | Proficiency type (`"none"`, `"proficient"`, `"expertise"`) |
| `dexterity_score` | `INTEGER` | `10` | Dexterity score |
| `dexterity_saving_throw_advantage` | `TEXT` | `"none"` | Dexterity saving throw advantage |
| `dexterity_saving_throw_proficiency` | `TEXT` | `"none"` | Dexterity saving throw proficiency |
| `constitution_score` | `INTEGER` | `10` | Constitution score |
| `constitution_saving_throw_advantage` | `TEXT` | `"none"` | Constitution saving throw advantage |
| `constitution_saving_throw_proficiency` | `TEXT` | `"none"` | Constitution saving throw proficiency |
| `intelligence_score` | `INTEGER` | `10` | Intelligence score |
| `intelligence_saving_throw_advantage` | `TEXT` | `"none"` | Intelligence saving throw advantage |
| `intelligence_saving_throw_proficiency` | `TEXT` | `"none"` | Intelligence saving throw proficiency |
| `wisdom_score` | `INTEGER` | `10` | Wisdom score |
| `wisdom_saving_throw_advantage` | `TEXT` | `"none"` | Wisdom saving throw advantage |
| `wisdom_saving_throw_proficiency` | `TEXT` | `"none"` | Wisdom saving throw proficiency |
| `charisma_score` | `INTEGER` | `10` | Charisma score |
| `charisma_saving_throw_advantage` | `TEXT` | `"none"` | Charisma saving throw advantage |
| `charisma_saving_throw_proficiency` | `TEXT` | `"none"` | Charisma saving throw proficiency |
| `armor_type` | `TEXT` | `"none"` | String representation of `ArmorType` (`ArmorTypeConverter`) |
| `shield_bonus` | `INTEGER` | `0` | Shield AC bonus |
| `natural_armor_bonus` | `INTEGER` | `0` | Natural armor bonus |
| `other_armor_description` | `TEXT` | `""` | Custom armor description |
| `hit_dice` | `INTEGER` | `1` | Hit dice count |
| `has_custom_hit_points` | `INTEGER` | `0` | Boolean flag (0 = false, 1 = true) |
| `custom_hit_points_description` | `TEXT` | `""` | Override HP string |
| `walk_speed` | `INTEGER` | `0` | Walk speed in ft |
| `burrow_speed` | `INTEGER` | `0` | Burrow speed in ft |
| `climb_speed` | `INTEGER` | `0` | Climb speed in ft |
| `fly_speed` | `INTEGER` | `0` | Fly speed in ft |
| `can_hover` | `INTEGER` | `0` | Boolean flag |
| `swim_speed` | `INTEGER` | `0` | Swim speed in ft |
| `has_custom_speed` | `INTEGER` | `0` | Boolean flag |
| `custom_speed_description` | `TEXT` | NULL | Override speed string |
| `challenge_rating` | `TEXT` | `"1"` | String representation of `ChallengeRating` (`ChallengeRatingConverter`) |
| `custom_challenge_rating_description` | `TEXT` | `""` | Custom CR text |
| `custom_proficiency_bonus` | `INTEGER` | `0` | Custom proficiency bonus override |
| `telepathy_range` | `INTEGER` | `0` | Telepathy range in ft |
| `understands_but_description` | `TEXT` | `""` | Language qualifier description |
| `senses` | `TEXT` | `"[]"` | JSON array of strings (`SetOfStringConverter`) |
| `skills` | `TEXT` | `"[]"` | JSON array of Skill objects (`SetOfSkillConverter`) |
| `damage_immunities` | `TEXT` | `"[]"` | JSON array of strings (`SetOfStringConverter`) |
| `damage_resistances` | `TEXT` | `"[]"` | JSON array of strings (`SetOfStringConverter`) |
| `damage_vulnerabilities` | `TEXT` | `"[]"` | JSON array of strings (`SetOfStringConverter`) |
| `condition_immunities` | `TEXT` | `"[]"` | JSON array of strings (`SetOfStringConverter`) |
| `languages` | `TEXT` | `"[]"` | JSON array of Language objects (`SetOfLanguageConverter`) |
| `abilities` | `TEXT` | `"[]"` | JSON array of Trait objects (`ListOfTraitsConverter`) |
| `actions` | `TEXT` | `"[]"` | JSON array of Trait objects (`ListOfTraitsConverter`) |
| `reactions` | `TEXT` | `"[]"` | JSON array of Trait objects (`ListOfTraitsConverter`) |
| `lair_actions` | `TEXT` | `"[]"` | JSON array of Trait objects (`ListOfTraitsConverter`) |
| `legendary_actions` | `TEXT` | `"[]"` | JSON array of Trait objects (`ListOfTraitsConverter`) |
| `regional_actions` | `TEXT` | `"[]"` | JSON array of Trait objects (`ListOfTraitsConverter`) |
### Virtual FTS Table: `monsters_fts`
Used for full-text search indexing:
```sql
CREATE VIRTUAL TABLE IF NOT EXISTS `monsters_fts`
USING FTS4(`name` TEXT, `size` TEXT, `type` TEXT, `subtype` TEXT, `alignment` TEXT, content=`monsters`);
```
---
## 6. Component Objects Schema (JSON Serialized in DB)
### 1. `Trait`
Used in `abilities`, `actions`, `reactions`, `lair_actions`, `legendary_actions`, and `regional_actions`:
```json
{
"name": "Keen Smell",
"description": "The monster has advantage on Wisdom (Perception) checks that rely on smell."
}
```
### 2. `Skill`
Used in `skills`:
```json
{
"name": "Perception",
"abilityScore": "WISDOM",
"advantageType": "NONE",
"proficiencyType": "PROFICIENT"
}
```
- **`abilityScore`**: `"STRENGTH"`, `"DEXTERITY"`, `"CONSTITUTION"`, `"INTELLIGENCE"`, `"WISDOM"`, `"CHARISMA"`.
- **`advantageType`**: `"NONE"`, `"ADVANTAGE"`, `"DISADVANTAGE"`.
- **`proficiencyType`**: `"NONE"`, `"PROFICIENT"`, `"EXPERTISE"`.
### 3. `Language`
Used in `languages`:
```json
{
"mName": "Common",
"mSpeaks": true
}
```
- **`mName`**: Name of the language (e.g., `"Elvish"`).
- **`mSpeaks`**: `true` if the creature can speak it; `false` if it only understands it.