# Format de carte source SkiEO 1.5 Le format `skieo-map` décrit une carte indépendamment des packages cuits d'Unreal Engine. Le fichier portable `.skieomap` contient les données source ; le mod `SkiEO-CustomMaps` lit le heightmap et construit le terrain au chargement. Les fichiers `.pak`, `.ucas` et `.utoc` sont interdits. Cette règle est appliquée par l'éditeur, le validateur Python et le chargeur Lua. ## Paquet portable Un `.skieomap` est un ZIP **Store ou Deflate**, utilisable comme un fichier unique : ```text AlpineValley.skieomap custom-map.json Terrain/ heightmap.r16 Scripts/ main.lua Assets/ trails.geojson chalet.glb ``` Le lecteur vérifie les CRC et accepte les ZIP compressés courants. Le module natif fourni accélère Deflate ; un lecteur Lua borné reste disponible. Les ZIP64 et archives chiffrées ne sont pas acceptés. Un dossier contenant la même arborescence reste accepté pendant le développement. Depuis 1.5, `dependencies` peut déclarer les mods requis ; voir le [contrat Steam Workshop](steam-workshop.md). Les anciennes cartes 1.x restent compatibles. ## Manifeste minimal ```json { "$schema": "https://skieo.community/schemas/custom-map-1.json", "format": { "name": "skieo-map", "version": "1.5.0" }, "id": "example.alpine-valley", "metadata": { "name": "Alpine Valley", "version": "1.0.0", "authors": ["Example Author"], "description": "A small alpine sandbox.", "language": "en", "localizations": { "fr": { "description": "Un petit bac à sable alpin." } } }, "terrain": { "size_meters": [8064, 8064], "elevation_meters": [850, 2310], "origin_centimeters": [-403200, -403200, 0], "source": { "file": "Terrain/heightmap.r16", "format": "r16le", "vertices": [2017, 2017], "spacing_meters": 4, "row_order": "north_to_south", "height_scale_meters": 0.025, "height_offset_meters": 724 }, "runtime": { "sample_step": 4, "tile_quads": 63, "uv_tile_meters": 20, "material": "/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial", "material_color": [0.18, 0.20, 0.22, 1], "snow_material": "/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial", "snow_color": [0.78, 0.84, 0.92, 1], "snow_offset_centimeters": 30 }, "preview": { "samples": [[0, 0.1, 0], [0.2, 1, 0.3], [0, 0.2, 0]] } }, "content": { "assets": [] } } ``` L'anglais est la langue d'affichage actuelle de Ski-E-O. Les champs principaux de `metadata` doivent donc être en anglais et `metadata.language` vaut `en` par défaut. `metadata.localizations` peut néanmoins préparer les traductions de `name`, `description` et `license`, indexées par une balise de langue BCP 47 (`fr`, `fr-CA`, etc.). Le chargeur conservera ces textes jusqu'à ce que le jeu propose une sélection de langue. Depuis la version 1.3, aucune carte du jeu ne sert de gabarit. Le chargeur ouvre `/Engine/Maps/Entry`, le niveau vide standard du moteur, avec le mode de jeu SkiResort. Il crée lui-même le paysage, l'environnement alpin, le soleil et le ciel avant le démarrage des acteurs. Ce choix appartient au mod ; le paquet portable ne contient aucun chemin de niveau Unreal. Les anciens manifestes 1.x avec `game.template_level` restent acceptés, mais ce champ est ignoré. Le bloc `game` est facultatif ; une carte source n'a pas besoin de numéro de build du jeu. `game.level` et `game.initial_save` restent interdits. `uv_tile_meters` fixe la répétition métrique des textures, indépendamment de la taille totale de la carte. Si `snow_material` est présent, le chargeur construit une seconde coque sans collision, décalée verticalement de `snow_offset_centimeters`. Le sol conserve son propre matériau et sa collision. Les tableaux `material_color` et `snow_color` teintent les matériaux qui exposent un paramètre `Color`. Sans matériau explicite, le monde indépendant utilise `M_Prop_albtex` avec l’albédo déclaré par la carte, ou la texture partagée `t_terrain_rock_alb` si aucun albédo n’est fourni, et `M_Snow` pour la neige. Les faces raides du sol utilisent une projection verticale de la texture pour éviter son étirement ; les matériaux et couleurs du manifeste sont respectés. La jupe verticale est calculée automatiquement depuis le périmètre du R16. Son pied se situe sous le point le plus bas de ce périmètre. La profondeur automatique vaut le maximum de 100 m, 4 % du petit côté de la carte et 20 % du relief du périmètre ; `skirt_depth_meters` peut remplacer ce calcul. Le matériau par défaut est `M_StriatedRock`, également utilisé par la jupe standard du jeu. `skirt_material` et `skirt_color` permettent de le personnaliser, et `skirt_enabled: false` de désactiver les parois. Aucun maillage préfabriqué ni fond horizontal couvrant la carte entière n'est requis. Après `FinalizePlacement`, le chargeur contrôle aussi les bâtiments contre le R16. Seuls ceux réellement sous le sol sont remontés à sa hauteur ; la tolérance est réglable par `placeable_below_ground_tolerance_centimeters` et un éventuel dégagement par `placeable_clearance_centimeters`. Les remontées demandent un traitement distinct : les gares et pylônes ont des coordonnées monde indépendantes de la position de l'acteur. Après les étapes natives d'initialisation, de génération et de chargement, le chargeur recale les `Waypoints`, les bases des `Towers` et leurs composants de maillage, puis reconstruit le câble et les sièges. Les points déjà au-dessus du terrain sont conservés, de même que la hauteur propre de chaque pylône. ## Présentation dans le menu et au chargement L'extension facultative `community.skieo.presentation` accompagne les cartes 1.x sans modifier leur terrain ou leur script. Le menu affiche la vignette, les compteurs et les difficultés ; le chargement réutilise cette illustration en fond et dans sa fiche. Exemple : ```json { "content": { "assets": ["Preview/veymont.png"] }, "extensions": { "community.skieo.presentation": { "image": "Preview/veymont.png", "trails": 20, "lifts": 9, "difficulties": ["beginner", "intermediate", "expert"] } } } ``` `image` doit aussi être déclaré dans `content.assets` pour être inclus dans le `.skieomap`. Le chargeur importe uniquement un PNG RGB/RGBA 8 bits, limité à 2048 pixels par dimension et 8 Mio, depuis le lecteur de paquet. Une image absente ou illisible laisse le pictogramme de montagne et le fond natif. Les compteurs sont des entiers positifs ou nuls déclarés par l'auteur, sans exécution du script de la carte. Les difficultés admises sont `beginner`, `intermediate`, `advanced` et `expert`. Les valeurs absentes s'affichent `—` et les difficultés non déclarées « Not specified ». Les articles Workshop sans manifeste installé n'affichent aucun compteur ni statistique de terrain. L'illustration de Veymont est une image de menu générée, et non une capture du terrain en jeu. Sa provenance et son prompt figurent dans [la documentation Veymont](../mod/SkiEO-Veymont/README.md#illustration-du-menu). ## Textures du terrain Depuis le format 1.4, `terrain.textures` déclare les images portables du sol. Les fichiers sont inclus automatiquement dans le paquet ; il ne faut pas les répéter dans `content.assets`. Le canal `albedo` utilise le paramètre `Albedo` du matériau de sol par défaut. Les autres canaux, par exemple `reflection`, se déclarent de la même façon et nécessitent un matériau exposant le paramètre correspondant dans `terrain.runtime.texture_parameters`. ```json "textures": { "albedo": { "file": "Terrain/limestone-albedo.png", "mips": [ "Terrain/limestone-albedo-mip01.png", "Terrain/limestone-albedo-mip02.png", "Terrain/limestone-albedo-mip03.png", "Terrain/limestone-albedo-mip04.png", "Terrain/limestone-albedo-mip05.png", "Terrain/limestone-albedo-mip06.png", "Terrain/limestone-albedo-mip07.png", "Terrain/limestone-albedo-mip08.png", "Terrain/limestone-albedo-mip09.png" ], "color_space": "srgb" } } ``` Les images sont des PNG RGB/RGBA 8 bits, carrés, de taille puissance de deux jusqu'à 512². `mips` contient tous les niveaux suivants, dans l'ordre, jusqu'à 1² ; pour une image 512², les neuf niveaux vont donc de 256² à 1². Le jeu importe les images puis assemble leur chaîne de détail complète, sans modifier une texture cuite. `color_space` vaut `srgb` pour les couleurs, `linear` pour les données, comme une carte de rugosité. Le matériau choisi doit exposer les paramètres indiqués ; déclarer une réflexion ne crée pas un shader de réflexion dans un matériau qui en est dépourvu. ```sh python3 tools/build_texture_mips.py source.png Terrain/limestone-albedo.png ``` Cet outil de préparation utilise Pillow. Le paquet résultant et le chargeur restent indépendants de Python et d'Unreal Editor. Une carte sans `textures` conserve le comportement précédent, avec la roche partagée du jeu. ## Encodage du terrain La version 1.2 accepte `r16le` et `r16be`. Chaque échantillon non signé est converti en altitude par : ```text altitude_m = valeur_r16 × height_scale_meters + height_offset_meters Z_unreal_cm = origin_centimeters.z + altitude_m × 100 ``` Les colonnes vont de l'ouest vers l'est. `row_order` précise si la première ligne va du nord vers le sud ou l'inverse. Les deux premiers éléments de `origin_centimeters` positionnent le sommet `[0, 0]` dans le monde Unreal. Le R16 reste à sa résolution d'origine dans le paquet. Les systèmes natifs utilisent une grille de 2017 × 2017 échantillons, rééchantillonnée sur toute l’emprise : ce nombre ne limite pas la carte à 8 km. Le pas natif vaut `size_meters[axe] / 2016` et peut différer entre X et Y. La source peut être rectangulaire et atteindre 16 384 sommets par axe. `sample_step` fixe la grille de base des maillages procéduraux à partir de la source : Veymont 0.6 utilise 2017 × 3025 points à 4 m, soit 505 × 757 points à 16 m avec une valeur de 4. Son cache natif est à 4 × 6 m. Voir le [guide des grandes cartes](large-maps.md). Dans le monde indépendant, les cellules dont la pente dépasse 45° sur un axe sont subdivisées jusqu'au pas source, avec au maximum quatre subdivisions par axe et 16 384 cellules raffinées. Si cette limite est atteinte, les cellules les plus pentues sont prioritaires. Les raccords partagent les échantillons des arêtes, y compris entre sections, et la neige utilise le même plan de raffinement. Les normales sont calculées au pas source. `tile_quads` découpe les maillages en sections pour limiter le coût de création et de collision. Ce raffinement conserve les altitudes du R16 ; il ne crée pas de surplombs ni de relief absent des données. `preview.samples` est une grille normalisée de 3 à 33 points par axe, utilisée uniquement dans le menu. L'éditeur la recalcule depuis le R16. ## Localisation et ressources Le bloc facultatif `location` accepte `latitude`, `longitude`, un libellé et `world_map_uv`. Ce dernier est un repère `[x, y]` normalisé sur la mappemonde illustrée du menu et donne un placement visuel plus précis qu'une projection géographique automatique. `content.assets` énumère les autres sources empaquetées. Les blocs `editor.layers` et `editor.asset_catalog` peuvent les décrire. Une ressource référencée par ces blocs doit aussi figurer dans `content.assets`. Les versions futures pourront importer des modèles de bâtiments depuis cette zone sans changer la définition du terrain. Un script peut lire une de ces ressources avec `api:read_asset(path)`. Le chargeur refuse tout chemin absent de `content.assets`, ce qui conserve la même isolation dans un dossier de développement et dans une archive. Veymont utilise ainsi `Vegetation/forest.skxy`, un flux portable composé de l'en-tête `SKXY`, de sa version et du nombre de points, puis de couples `float32` X/Y. Le chargeur recalcule Z depuis le R16 et reconstruit progressivement un ISM natif ; aucune instance d'arbre cuite par Unreal n'est transportée par la carte. ## Forêts : JSON source, SKXY dans le package Depuis Custom Maps 1.11, le chargeur accepte uniquement des arbres SKXY. `skieomod build` compile le JSON de positions défini par `build.forest.file` dans `Vegetation/Generated/forest.skxy`, déclare cet asset et prépare son chargement avec `api:load_forest_skxy` dans `map.ready`. Le JSON reste dans le projet et ne figure pas dans le package. ```json "build": {"forest": {"file": "Vegetation/forest.json"}} ``` Le fichier source contient un tableau de points `{ "x": 10, "y": -20 }` en mètres du monde, X vers l’est et Y vers le sud. Il est limité à 32 Mio et un million de points finis. Le build refuse les champs supplémentaires et les clés dupliquées. SKXY encode les coordonnées en centimètres float32. Le chargeur recalcule Z depuis le R16 et reconstruit les instances par lots. Le bouton de visibilité agit sur ces arbres, sans collision de gameplay. `build.heightmap.processing` configure le lissage, l’érosion thermique, l’érosion par drainage et le bruit. `build.vegetation` configure le biome, la forêt automatique et la texture de couverture herbe/roche. Le build dérive les métadonnées du relief traité et écrit directement les arbres en SKXY. La forêt automatique prend la priorité sur `build.forest`. Les paramètres `build` restent locaux et sont retirés du package. Le [guide anglais](vegetation.md) précise les algorithmes, unités, limites et valeurs par défaut ; le [schéma](../schemas/custom-map.schema.json) décrit la configuration. `load_forest_json` a été supprimé du chargeur. ## Chargement par le mod Le chargeur suit cette séquence : 1. validation du manifeste et de tous les chemins ; 2. affichage de l'écran de chargement et lecture du R16 ; 3. génération des textures de hauteur et des atlas Landscape avant le voyage ; 4. ouverture du monde vide avec le mode de jeu habituel ; 5. création du paysage, de ses composants, de l'environnement et de l'éclairage dans le hook UE4SS `RegisterInitGameStatePreHook`, avant World BeginPlay ; 6. initialisation du cache de hauteur et des overlays depuis la source, construction du terrain visible, de sa collision et de la coque de neige, puis ouverture de la saison, création d'un enneigement initial natif et invalidation de la navigation calculée avant installation des hauteurs ; 7. émission de `map.terrain_loaded`, puis de `map.ready` ; 8. exécution des actions de la carte ; 9. vérification finale des hauteurs et retrait de l'écran de chargement après achèvement des tâches de création de la station. L'écran reste affiché pendant la construction asynchrone. Une erreur ou cinq minutes sans progression produisent un diagnostic sans annoncer un succès. Les 16 atlas 512², leurs dix niveaux de mip, les 1024 composants Landscape et la texture de simulation sont des objets transitoires créés depuis le R16. Chaque composant reçoit son propre rectangle UV, ses limites et le paramètre matériel `Heightmap` correspondant. Leurs tableaux sont alloués par Unreal ; aucun fournisseur de stockage, composant ou cache de Sugar Bowl n'est repris. La végétation et la station sont créées ensuite depuis les sources portables. Le Landscape sert à initialiser les limites et les caches natifs. Le terrain visible et sa coque de neige sont des maillages procéduraux qui utilisent les matériaux partagés `M_Prop_albtex` (texture de roche) et `M_Snow` du jeu ; la jupe utilise séparément `M_StriatedRock`. Les permutations de shader Landscape nécessaires ne sont disponibles que dans les niveaux cuits. Le pas de base de ces maillages reste contrôlé par `sample_step` du manifeste ; les pentes fortes bénéficient du raffinement local décrit ci-dessus. Le mod initialise décembre avec 100 cm de neige tassée et 20 cm de poudreuse, à −5 °C, via `SkiResortSimulator.Snow.SetLevel`, sans charger de sauvegarde. La simulation native poursuit ensuite l'évolution de cet enneigement. Le contrôleur natif `ClayEnvironmentController` anime le soleil, la lune, l'exposition et les couleurs selon le calendrier et la météo. Une cubemap diurne du moteur fournit l'éclairage ambiant du monde neuf. Le matériau instancié des flocons suit cette lumière, y compris la nuit et pendant les tempêtes, et leurs composants ne projettent pas d'ombres. Le décalage vertical du R16 est appliqué une seule fois : la conversion native d'altitude est calibrée sur l'origine du monde et l'étendue de quantification du Landscape. Les textures natives sont réencodées à partir de zéro lorsque la source a un décalage positif. Leur plage inclut ainsi ce décalage et ne plafonne plus l'altitude affichée à la seule amplitude du R16. Les données portables et les positions visibles restent inchangées ; seule la quantification native change. Les sauvegardes natives conservent `/Engine/Maps/Entry` comme niveau. Le chargeur associe chaque identifiant de partie à la carte dans un fichier `SkiEO-CustomMap-.json` voisin des sauvegardes. Une nouvelle partie custom reçoit un GUID neuf, distinct de la partie précédente et de ses autosaves. Au chargement, le monde, le terrain, l'éclairage et la forêt sont reconstruits ; la station, l'économie, le calendrier et l'enneigement sauvegardés sont conservés. La simulation reste suspendue pendant toute la reconstruction. Les anciennes sauvegardes `Entry` sans association peuvent être migrées si une seule carte custom est installée. Le chargeur refuse de deviner entre plusieurs cartes ou de remplacer une carte absente. Une sauvegarde native d'un autre niveau ne déclenche pas ce chemin de restauration. Le fichier d'association doit accompagner les sauvegardes lorsqu'une partie est transférée sur une autre machine. Après l'ajout par lots d'une forêt SKXY, le chargeur force une mise à jour d'une instance existante. Ce rafraîchissement publie le buffer de rendu de l'ISM sans modifier les positions : le nombre d'arbres dessiné correspond alors au nombre d'instances de gameplay, au lieu de n'afficher qu'un petit lot intermédiaire. ## Scripts Lua Un point d'entrée facultatif se déclare ainsi : ```json { "script": { "language": "lua", "api": "1.0", "entry": "Scripts/main.lua", "permissions": ["gameplay"] } } ``` Exemple : ```lua return function(api) api:on("map.ready", function(event) api:log("La carte " .. event.map.id .. " est prête") api.game:set_time_rate(1) end) end ``` Événements de cycle de vie : | Événement | Moment | |---|---| | `map.before_load` | avant la préparation du monde indépendant | | `map.after_load` | juste après `OpenLevel` | | `map.terrain_loaded` | après création complète du terrain et des collisions | | `map.ready` | lorsque le script peut créer pistes, remontées et bâtiments | | `map.load_failed` | si la construction du terrain échoue | | `map.unloading` | avant le chargement d'une autre carte | Les événements de cycle de vie portent `event.restored = true` lorsqu'ils concernent une sauvegarde. Dans ce cas, le script doit reconstruire ses éléments transitoires (par exemple la forêt) et préserver les pistes, bâtiments et remontées que le jeu a restaurés. Veymont applique cette distinction. Un script qui construit des objets de façon asynchrone doit réserver une tâche **pendant** son callback `map.ready`, puis signaler sa fin : ```lua api:on("map.ready", function() local done = api:hold_loading() api:after(100, function() -- Construire la station ici, puis : done() -- ou done("message d'erreur") en cas d'échec end) end) ``` Plusieurs tâches peuvent coexister ; toutes doivent être terminées. Les appels répétés à `done()` et les callbacks d'une ancienne carte sont ignorés. Veymont signale la fin après création des bâtiments, remontées et pistes ; le chargeur attend ensuite la sortie de la pile de finalisation native avant de vérifier les positions et de retirer l'écran. Les tâches longues peuvent appeler `api:loading_progress("Création de la forêt", 0.75)` pour mettre à jour l'écran et signaler leur avancement (fraction de 0 à 1). Le chargement échoue après cinq minutes sans progression. Une forêt qui avance continue à se charger même si la durée totale dépasse cinq minutes. L'API expose aussi les événements de calendrier, météo, pistes et objets constructibles documentés dans `api.lua`. L'environnement par défaut ne donne pas accès à `io`, `os`, `debug`, `package`, `require`, `load` ou aux objets Unreal. `skieo.unrestricted` est réservé aux conversions historiques et ne doit jamais être activé automatiquement par l'éditeur. ## Éditeur et outils [`editor/index.html`](../editor/index.html) est une application autonome. Elle importe le R16, produit l'aperçu, édite le manifeste et le script, rouvre un paquet existant puis exporte un `.skieomap` compressé. Les mêmes opérations sont disponibles en ligne de commande : ```bash python3 tools/custom_maps.py validate mod/SkiEO-Veymont/custom-map.json python3 tools/custom_maps.py pack mod/SkiEO-Veymont/custom-map.json dist/Veymont.skieomap python3 tools/custom_maps.py inspect dist/Veymont.skieomap ``` Le schéma de référence est [`schemas/custom-map.schema.json`](../schemas/custom-map.schema.json). Le validateur vérifie en plus la sécurité des chemins, l'absence de contenu cuit, la taille exacte du R16, l'unicité des références et la présence de chaque fichier.