reldens
Version:
Reldens - MMORPG Platform
328 lines (265 loc) • 10.8 kB
Markdown
# Room Images and Tileset Override System
## Overview
This document explains how the room scene images upload system works and how the `overrideSceneImagesWithMapFile` option automatically synchronizes scene images with the Tiled map file tilesets.
## Configuration
**Config Path:** `server/rooms/maps/overrideSceneImagesWithMapFile`
**Type:** Boolean
**Default:** `true`
**Location:** Database `config` table or environment variable
When enabled, the system uses the Tiled map file as the source of truth for scene images, automatically overriding the `scene_images` field with images listed in the map's tilesets.
## File Locations
### Source Code
- **Validator:** `lib/admin/server/room-map-tilesets-validator.js`
- **Subscriber:** `lib/admin/server/subscribers/rooms-entity-subscriber.js`
- **File Upload Renderer:** `lib/admin/server/rooms-file-upload-renderer.js`
- **Admin Plugin:** `lib/admin/server/plugin.js`
### Admin Interface
- **Tileset File Item Template:** `theme/admin/templates/fields/edit/tileset-file-item.html`
- **Tileset Alert Wrapper Template:** `theme/admin/templates/fields/edit/tileset-alert-wrapper.html`
- **Client JS:** `theme/admin/reldens-admin-client.js`
- **Client CSS:** `theme/admin/reldens-admin-client.css`
- **Router:** `npm-packages/reldens-cms/lib/admin-manager/router-contents.js`
## Database Schema
### Rooms Table
- `id` - Room identifier
- `map_filename` - Tiled map JSON file (e.g., `reldens-forest.json`)
- `scene_images` - Comma-separated list of tileset images (e.g., `reldens-forest.png,reldens-town.png`)
### Upload Configuration
Both fields are configured as upload fields:
- `map_filename` - Single file upload, bucket: `theme/assets/maps`
- `scene_images` - Multiple file upload, bucket: `theme/assets/images`
## System Flow
### 1. Initial Room Creation
**User Actions:**
1. Navigate to Admin → Rooms → Create New
2. Upload map JSON file to `map_filename` field
3. Upload tileset images to `scene_images` field
4. Click Save
**System Processing:**
1. **Upload Phase** - Files saved to respective buckets
2. **Validation Phase** - `validateUploadedFiles()` checks required fields
3. **Save Phase** - Entity created in database
4. **Post-Save Event** - `reldens.adminAfterEntitySave` fires
5. **Validator Execution** - `RoomMapTilesetsValidator.validate()` runs
**Validator Logic:**
```javascript
// Check if override is enabled
overrideEnabled = config.getWithoutLogs('server/rooms/maps/overrideSceneImagesWithMapFile', true)
// Read map file
mapData = readMapFile(bucket, mapFilename, roomId)
// Extract tileset images from map JSON
tilesetImages = extractTilesetImages(mapData.tilesets)
// Example: ['reldens-forest.png']
// Compare with current scene_images
if (tilesetImages !== currentSceneImages) {
// Validate all images exist in scene_images bucket
if (validateImagesExist(tilesetImages, sceneImagesBucket)) {
// Override scene_images with tileset images
roomsRepository.updateById(roomId, {scene_images: tilesetImages.join(',')})
}
}
```
### 2. Room Editing
**User Actions:**
1. Navigate to Admin → Rooms → Edit Room
2. View existing files in both fields
3. Modify files or click Save without changes
**Edit Form Population:**
**Event:** `reldens.adminEditPropertiesPopulation`
**Flow:**
```javascript
// 1. Event emitted with room data
event = {
driverResource, // Entity configuration
renderedEditProperties, // Form properties
loadedEntity, // Room from database
entityId: 'rooms',
entityData: loadedEntity
}
// 2. RoomsEntitySubscriber.populateEditFormTilesetImages() executes
if (overrideSceneImagesWithMapFile) {
// Extract tileset images from map file
tilesetImages = validator.extractTilesetImagesFromEntity(entityData, driverResource)
// Inject into form properties
renderedEditProperties.tilesetImages = tilesetImages
renderedEditProperties.overrideSceneImagesEnabled = true
}
// 3. RoomsFileUploadRenderer processes scene_images field
// Event: reldens.adminBeforeFieldRender
if (propertyKey === 'scene_images' && tilesetImages.length > 0) {
// Render each file with protection flag
for each file:
renderedFileItems.push(render tileset-file-item.html with {
filename,
isProtected: tilesetImages.includes(filename)
})
// Wrap files in alert container
templateData.renderedFiles = render tileset-alert-wrapper.html
}
// 4. Template renders with tileset protection
{{^isProtected}}
<button class="remove-upload-btn">X</button> -
{{/isProtected}}
{{filename}}
```
**Result:**
- Protected images (tilesets): NO remove button
- Non-protected images: Remove button shown
- Alert icon displays with info message
### 3. Saving Changes
**Scenario A: No Files Changed**
1. User clicks Save without uploading/removing files
2. Validation passes (existing files satisfy requirement)
3. Entity updated with form data
4. Post-save validator runs
5. If scene_images matches tilesets → No action
6. If mismatch → Override with tileset images
**Scenario B: Add New Image**
1. User uploads additional image to `scene_images`
2. `prepareUploadPatchData()` appends new file to existing files
3. Entity saved with: `existing_images.png,new_image.png`
4. Post-save validator runs
5. Validates tileset images exist
6. **Overrides** scene_images with ONLY tileset images (removes non-tileset images)
**Scenario C: Remove Non-Protected Image**
1. User clicks X button on non-protected image
2. Client adds filename to `removed_scene_images` hidden input
3. `prepareUploadPatchData()` filters removed files
4. Entity saved with filtered list
5. Post-save validator runs
6. Overrides with tileset images (removes non-tileset files)
**Scenario D: Attempt Remove Protected Image (Prevented)**
1. Protected image (tileset) has NO remove button
2. User cannot remove it through UI
3. Alert icon displays: "Images specified in the tileset can't be removed since the option overrideSceneImagesWithMapFile is active."
### 4. Map File Update
**User Updates Map File:**
1. User replaces `map_filename` with new Tiled map
2. New map references different tileset images
3. Entity saved
4. Post-save validator executes
5. Reads new map file tilesets
6. **Replaces** scene_images with new tileset images
7. Old images no longer referenced (user must manage cleanup)
## Technical Details
### Map File Structure
**Example: reldens-forest.json**
```json
{
"tilesets": [
{
"columns": 14,
"firstgid": 1,
"image": "reldens-forest.png",
"imageheight": 408,
"imagewidth": 476,
"name": "reldens-forest",
"tilecount": 168
}
]
}
```
**Extraction Logic:**
```javascript
extractTilesetImages(mapData) {
let tilesets = mapData.tilesets || []
let images = []
for (let tileset of tilesets) {
let tilesetImage = tileset.image // 'reldens-forest.png' or '../images/reldens-forest.png'
let imageFileName = tilesetImage.split('/').pop() // Extract filename only
if (!images.includes(imageFileName)) {
images.push(imageFileName)
}
}
return images // ['reldens-forest.png']
}
```
### Validation Logic
**Array Comparison (validator):**
```javascript
arraysAreEqual(array1, array2) {
if (array1.length !== array2.length) {
return false
}
let sorted1 = [...array1].sort()
let sorted2 = [...array2].sort()
for (let i = 0; i < sorted1.length; i++) {
if (sorted1[i] !== sorted2[i]) {
return false
}
}
return true
}
```
**Image Existence Validation (validator):**
```javascript
validateImagesExist(tilesetImages, sceneImagesBucket, roomId, mapFilename) {
for (let imageFileName of tilesetImages) {
let imageFilePath = FileHandler.joinPaths(sceneImagesBucket, imageFileName)
if (!FileHandler.exists(imageFilePath)) {
return false
}
}
return true
}
```
### Client-Side Protection
**File Item Template (tileset-file-item.html):**
```html
<p class="upload-current-file" data-field="{{&fieldName}}" data-filename="{{&filename}}">
{{^isProtected}}
<button type="button" class="remove-upload-btn" data-field="{{&fieldName}}" data-filename="{{&filename}}" title="REMOVE">X</button> -
{{/isProtected}}
{{&filename}}
</p>
```
**Alert Wrapper Template (tileset-alert-wrapper.html):**
```html
<div class="tileset-alert-wrapper">
<div class="upload-files-with-alert">
{{{renderedFileItems}}}
</div>
<div class="tileset-alert-icon-container">
<img src="/assets/admin/alert.png" class="tileset-alert-icon" alt="Info" title="Images specified in the tileset can't be removed since the option overrideSceneImagesWithMapFile is active.">
<span class="tileset-info-message hidden">Images specified in the tileset can't be removed since the option overrideSceneImagesWithMapFile is active.</span>
</div>
</div>
```
**JavaScript Toggle (reldens-admin-client.js):**
```javascript
document.querySelectorAll('.tileset-alert-icon').forEach(icon => {
icon.addEventListener('click', () => {
let message = icon.nextElementSibling
if (message?.classList.contains('tileset-info-message')) {
message.classList.toggle('hidden')
}
})
})
```
## Benefits
1. **Consistency:** Scene images always match map tilesets
2. **Automation:** No manual sync between map and images
3. **Single Source of Truth:** Tiled map file controls image references
4. **Developer Experience:** Edit maps in Tiled, changes auto-sync
## Limitations
1. **One-Way Sync:** Map → Database only (not bidirectional)
2. **Cleanup Required:** Removing tileset from map doesn't delete old image files
3. **Override Always Wins:** Manual changes to scene_images get overwritten on next save
4. **Requires Config:** Must enable `overrideSceneImagesWithMapFile` to activate
## Disabling the Feature
To disable tileset override and manage images manually:
**Option 1: Database Config**
```sql
UPDATE config
SET value = '0'
WHERE path = 'server/rooms/maps/overrideSceneImagesWithMapFile';
```
**Option 2: Environment Variable**
```bash
RELDENS_SERVER_ROOMS_MAPS_OVERRIDESCENEIMAGESWITHMAPFILE=0
```
**Result:**
- Post-save validation skipped
- All images show remove buttons
- Full manual control over scene_images field
- Map file and scene_images can diverge