Merge pull request #559 from GriffinRichards/custom-images

Update settings window
This commit is contained in:
GriffinR 2023-12-27 23:05:22 -05:00 committed by GitHub
commit 6a8d3a8197
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
85 changed files with 4409 additions and 1956 deletions

View File

@ -8,16 +8,26 @@ The **"Breaking Changes"** listed below are changes that have been made in the d
## [Unreleased]
### Added
- Adds an editor window under `Options -> Project Settings...` to customize the project-specific settings in `porymap.project.cfg` and `porymap.user.cfg`.
- Adds an editor window under `Options -> Custom Scripts...` for Porymap's API scripts.
- Support for 8BPP tileset tile images.
- Add an editor window under `Options -> Project Settings...` to customize the project-specific settings in `porymap.project.cfg` and `porymap.user.cfg`.
- Add an editor window under `Options -> Custom Scripts...` for Porymap's API scripts.
- Add a warning to warp events if they're on an incomaptible metatile behavior.
- Add settings for custom images, including the collision graphics, default event icons, and pokémon icons.
- Add settings to override any symbol or macro names Porymap expects to find.
- Add a zoom slider to the Collision tab.
- Add toggleable grids to the Tileset Editor.
- Support for custom metatile ID, collision, and elevation data sizes.
- Support for 8bpp tileset tile images.
### Changed
- The Collision tab now allows selection of any valid elevation/collision value.
- The Palette Editor now remembers the Bit Depth setting.
- The min/max levels on the Wild Pokémon tab will now adjust automatically if they invalidate each other.
- If the recent project directory doesn't exist Porymap will open an empty project instead of failing with a misleading error message.
- Settings under `Options` were relocated either to the `Preferences` window or `Options -> Project Settings`.
- Secret Base and Weather Trigger events are automatically disabled if their respective constants files fail to parse, instead of not opening the project.
- If a Pokémon icon fails to load Porymap will attempt to predict its filepath. If this also fails it will appear with a placeholder icon, and won't disppear when edited.
- The bits in metatile attribute masks are now allowed to be non-contiguous.
- Porymap will now attempt to read metatile attribute masks from the project.
### Fixed
- Fix text boxes in the Palette Editor calculating color incorrectly.
@ -32,6 +42,9 @@ The **"Breaking Changes"** listed below are changes that have been made in the d
- Fix heal location data being cleared if certain spaces aren't used in the table.
- Fix bad URL color contrast on dark themes.
- Fix some issues when too few/many pokémon are specified for a wild encounter group.
- Fix Porymap reporting errors for macros it doesn't use.
- Fix painting on the Collision tab with the opacity slider at 0 painting metatiles.
- Fix crashes when File->Reload Project fails.
## [5.1.1] - 2023-02-20
### Added

View File

@ -9,7 +9,7 @@ Selecting Collision Types
The Collision Type Selector is a tab next to the Metatile Selector. It features 32 total different collision types. The left column is for collision types that allow the player to walk through the tiles. These are denoted by white text. The right column is for collision types that are impassable by the player. These are denoted by red text.
The transparency slider above the collision types controls the transparency of the collision properties on the map view.
The transparency slider above the collision types controls the transparency of the collision properties on the map view. The slider at the bottom of the panel zooms the selector image in and out.
.. figure:: images/editing-map-collisions/map-collisions.png
:alt: Map Collisions View
@ -73,3 +73,6 @@ Multi-Level Collision Type |multi-level-collision-type-1| |multi-level-collision
.. |multi-level-collision-type-2|
image:: images/editing-map-collisions/multi-level-collision-type-2.png
.. note::
For advanced usage: Any valid elevation/collision value combination can be selected using the ``Elevation`` and ``Collision`` value spinners, even if it's not represented graphically on the selector image. You may also resize/replace this selector image under ``Options -> Project Settings``.

View File

@ -110,7 +110,7 @@ Target Map
Warp Events
-----------
Warp events are how the player is able to warp to other maps, such as entering a building. Double-clicking on a warp will automatically open the destination map and select the destination warp. This makes it very easy to navigate around in Porymap.
Warp events are how the player is able to warp to other maps, such as entering a building. Double-clicking on a warp will automatically open the destination map and select the destination warp. This makes it very easy to navigate around in Porymap. Warps need to be on specific metatiles to function as an exit; a warning will appear if the warp event is not on one of these metatiles.
.. figure:: images/editing-map-events/event-warp.png
:alt: Warp Event Properties

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

View File

@ -6,13 +6,16 @@ Porymap relies on the user maintaining a certain level of integrity with their p
This is a list of files that porymap reads from and writes to. Generally, if porymap writes
to a file, it probably is not a good idea to edit yourself unless otherwise noted.
The filepath that Porymap expects for each file can be overridden under the ``Project Files`` section of ``Options -> Project Settings``. A new path can be specified by entering it in the text box or choosing it with the folder button. Paths are expected to be relative to the root project folder. If no path is specified, or if the file/folder specified does not exist, then the default path will be used instead. The name of each setting in this section is listed in the table below under ``Override``.
The filepath that Porymap expects for each file can be overridden on the ``Files`` tab of ``Options -> Project Settings``. A new path can be specified by entering it in the text box or choosing it with the |button-folder| button. Paths are expected to be relative to the root project folder. If no path is specified, or if the file/folder specified does not exist, then the default path will be used instead. The name of each setting in this section is listed in the table below under ``Override``.
.. figure:: images/project-files/settings.png
:align: center
:width: 75%
:alt: Settings
.. |button-folder| image:: images/scripting-capabilities/folder.png
:width: 24
:height: 24
.. figure:: images/settings-and-options/tab-files.png
:alt: Files tab
.. _files:
.. csv-table::
:header: File Name,Read,Write,Override,Notes
@ -32,7 +35,7 @@ The filepath that Porymap expects for each file can be overridden under the ``Pr
data/tilesets/graphics.inc, yes, yes, ``tilesets_graphics_asm``, only if ``tilesets_headers`` can't be found
data/tilesets/metatiles.inc, yes, yes, ``tilesets_metatiles_asm``, only if ``tilesets_headers`` can't be found
data/tilesets/[primary|secondary]/\*, yes, yes, ``data_tilesets_folders``, default tileset data location
src/data/wild_encounters.json, yes, yes, ``json_wild_encounters``,
src/data/wild_encounters.json, yes, yes, ``json_wild_encounters``, optional (only required to use Wild Pokémon tab)
src/data/object_events/object_event_graphics_info_pointers.h, yes, no, ``data_obj_event_gfx_pointers``,
src/data/object_events/object_event_graphics_info.h, yes, no, ``data_obj_event_gfx_info``,
src/data/object_events/object_event_pic_tables.h, yes, no, ``data_obj_event_pic_tables``,
@ -41,18 +44,17 @@ The filepath that Porymap expects for each file can be overridden under the ``Pr
src/data/heal_locations.h, yes, yes, ``data_heal_locations``,
src/data/region_map/region_map_sections.json, yes, yes, ``json_region_map_entries``,
src/data/region_map/porymap_config.json, yes, yes, ``json_region_porymap_cfg``,
include/constants/global.h, yes, no, ``constants_global``, reads ``OBJECT_EVENT_TEMPLATES_COUNT``
include/constants/global.h, yes, no, ``constants_global``, reads ``define_obj_event_count``
include/constants/map_groups.h, no, yes, ``constants_map_groups``,
include/constants/items.h, yes, no, ``constants_items``,
include/constants/opponents.h, yes, no, ``constants_opponents``, reads max trainers constant
include/constants/flags.h, yes, no, ``constants_flags``,
include/constants/vars.h, yes, no, ``constants_vars``,
include/constants/weather.h, yes, no, ``constants_weather``,
include/constants/songs.h, yes, no, ``constants_songs``,
include/constants/items.h, yes, no, ``constants_items``, for Hidden Item events
include/constants/flags.h, yes, no, ``constants_flags``, for Object and Hidden Item events
include/constants/vars.h, yes, no, ``constants_vars``, for Trigger events
include/constants/weather.h, yes, no, ``constants_weather``, for map weather and Weather Triggers
include/constants/songs.h, yes, no, ``constants_songs``, for map music
include/constants/heal_locations.h, yes, yes, ``constants_heal_locations``,
include/constants/pokemon.h, yes, no, ``constants_pokemon``, reads min and max level constants
include/constants/pokemon.h, yes, no, ``constants_pokemon``, reads ``define_min_level`` and ``define_max_level``
include/constants/map_types.h, yes, no, ``constants_map_types``,
include/constants/trainer_types.h, yes, no, ``constants_trainer_types``,
include/constants/trainer_types.h, yes, no, ``constants_trainer_types``, for Object events
include/constants/secret_bases.h, yes, no, ``constants_secret_bases``, pokeemerald and pokeruby only
include/constants/event_object_movement.h, yes, no, ``constants_obj_event_movement``,
include/constants/event_objects.h, yes, no, ``constants_obj_events``,
@ -60,8 +62,75 @@ The filepath that Porymap expects for each file can be overridden under the ``Pr
include/constants/region_map_sections.h, yes, no, ``constants_region_map_sections``,
include/constants/metatile_labels.h, yes, yes, ``constants_metatile_labels``,
include/constants/metatile_behaviors.h, yes, no, ``constants_metatile_behaviors``,
include/constants/species.h, yes, no, ``constants_metatile_behaviors``, for the Wild Pokémon tab
include/global.fieldmap.h, yes, no, ``global_fieldmap``, reads map and tileset data masks
include/fieldmap.h, yes, no, ``constants_fieldmap``, reads tileset related constants
src/event_object_movement.c, yes, no, ``initial_facing_table``, reads ``gInitialMovementTypeFacingDirections``
src/pokemon_icon.c, yes, no, ``pokemon_icon_table``, reads files in ``gMonIconTable``
src/fieldmap.c, yes, no, ``fieldmap``, reads ``symbol_attribute_table``
src/event_object_movement.c, yes, no, ``initial_facing_table``, reads ``symbol_facing_directions``
src/pokemon_icon.c, yes, no, ``pokemon_icon_table``, reads files in ``symbol_pokemon_icon_table``
graphics/pokemon/\*/icon.png, yes, no, ``pokemon_gfx``, to search for Pokémon icons if they aren't found in ``symbol_pokemon_icon_table``
In addition to these files, there are some specific symbol and macro names that Porymap expects to find in your project. These can be overridden on the ``Identifiers`` tab of ``Options -> Project Settings``. The name of each setting in this section is listed in the table below under ``Override``. Overrides with ``regex`` in the name support the `regular expression syntax <https://perldoc.perl.org/perlre>`_ used by Qt.
.. figure:: images/settings-and-options/tab-identifiers.png
:alt: Files tab
.. _identifiers:
.. csv-table::
:header: Override,Default,Notes
:widths: 20, 20, 30
``symbol_facing_directions``, ``gInitialMovementTypeFacingDirections``, to set sprite frame for Object Events based on movement type
``symbol_obj_event_gfx_pointers``, ``gObjectEventGraphicsInfoPointers``, to map Object Event graphics IDs to graphics data
``symbol_pokemon_icon_table``, ``gMonIconTable``, to map species constants to icon images
``symbol_wild_encounters``, ``gWildMonHeaders``, output as the ``label`` property for the top-level wild ecounters JSON object
``symbol_heal_locations``, ``sHealLocations``, only if ``Respawn Map/NPC`` is disabled
``symbol_spawn_points``, ``sSpawnPoints``, only if ``Respawn Map/NPC`` is enabled
``symbol_spawn_maps``, ``sWhiteoutRespawnHealCenterMapIdxs``, values for Heal Locations ``Respawn Map`` field
``symbol_spawn_npcs``, ``sWhiteoutRespawnHealerNpcIds``, values for Heal Locations ``Respawn NPC`` field
``symbol_attribute_table``, ``sMetatileAttrMasks``, optionally read to get settings on ``Tilesets`` tab
``symbol_tilesets_prefix``, ``gTileset_``, for new tileset names and to extract base tileset names
``define_obj_event_count``, ``OBJECT_EVENT_TEMPLATES_COUNT``, to limit total Object Events
``define_min_level``, ``MIN_LEVEL``, minimum wild encounters level
``define_max_level``, ``MAX_LEVEL``, maximum wild encounters level
``define_tiles_primary``, ``NUM_TILES_IN_PRIMARY``,
``define_tiles_total``, ``NUM_TILES_TOTAL``,
``define_metatiles_primary``, ``NUM_METATILES_IN_PRIMARY``, total metatiles are calculated using metatile ID mask
``define_pals_primary``, ``NUM_PALS_IN_PRIMARY``,
``define_pals_total``, ``NUM_PALS_TOTAL``,
``define_map_size``, ``MAX_MAP_DATA_SIZE``, to limit map dimensions
``define_mask_metatile``, ``MAPGRID_METATILE_ID_MASK``, optionally read to get settings on ``Maps`` tab
``define_mask_collision``, ``MAPGRID_COLLISION_MASK``, optionally read to get settings on ``Maps`` tab
``define_mask_elevation``, ``MAPGRID_ELEVATION_MASK``, optionally read to get settings on ``Maps`` tab
``define_mask_behavior``, ``METATILE_ATTR_BEHAVIOR_MASK``, optionally read to get settings on ``Tilesets`` tab
``define_mask_layer``, ``METATILE_ATTR_LAYER_MASK``, optionally read to get settings on ``Tilesets`` tab
``define_attribute_behavior``, ``METATILE_ATTRIBUTE_BEHAVIOR``, name used to extract setting from ``symbol_attribute_table``
``define_attribute_layer``, ``METATILE_ATTRIBUTE_LAYER_TYPE``, name used to extract setting from ``symbol_attribute_table``
``define_attribute_terrain``, ``METATILE_ATTRIBUTE_TERRAIN``, name used to extract setting from ``symbol_attribute_table``
``define_attribute_encounter``, ``METATILE_ATTRIBUTE_ENCOUNTER_TYPE``, name used to extract setting from ``symbol_attribute_table``
``define_metatile_label_prefix``, ``METATILE_``, expected prefix for metatile label macro names
``define_heal_locations_prefix``, ``HEAL_LOCATION_``, output as prefix for Heal Location IDs if ``Respawn Map/NPC`` is disabled
``define_spawn_prefix``, ``SPAWN_``, output as prefix for Heal Location IDs if ``Respawn Map/NPC`` is enabled
``define_map_prefix``, ``MAP_``, expected prefix for map macro names
``define_map_dynamic``, ``DYNAMIC``, macro name after prefix for Dynamic maps
``define_map_empty``, ``UNDEFINED``, macro name after prefix for empty maps
``define_map_section_prefix``, ``MAPSEC_``, expected prefix for location macro names
``define_map_section_empty``, ``NONE``, macro name after prefix for empty region map sections
``define_map_section_count``, ``COUNT``, macro name after prefix for total number of region map sections
``regex_behaviors``, ``\bMB_``, regex to find metatile behavior macro names
``regex_obj_event_gfx``, ``\bOBJ_EVENT_GFX_``, regex to find Object Event graphics ID macro names
``regex_items``, ``\bITEM_(?!(B_)?USE_)``, regex to find item macro names
``regex_flags``, ``\bFLAG_``, regex to find flag macro names
``regex_vars``, ``\bVAR_``, regex to find var macro names
``regex_movement_types``, ``\bMOVEMENT_TYPE_``, regex to find movement type macro names
``regex_map_types``, ``\bMAP_TYPE_``, regex to find map type macro names
``regex_battle_scenes``, ``\bMAP_BATTLE_SCENE_``, regex to find battle scene macro names
``regex_weather``, ``\bWEATHER_``, regex to find map weather macro names
``regex_coord_event_weather``, ``\bCOORD_EVENT_WEATHER_``, regex to find weather trigger macro names
``regex_secret_bases``, ``\bSECRET_BASE_[A-Za-z0-9_]*_[0-9]+``, regex to find secret base ID macro names
``regex_sign_facing_directions``, ``\bBG_EVENT_PLAYER_FACING_``, regex to find sign facing direction macro names
``regex_trainer_types``, ``\bTRAINER_TYPE_``, regex to find trainer type macro names
``regex_music``, ``\b(SE|MUS)_``, regex to find music macro names
``regex_species``, ``\bSPECIES_``, regex to find species macro names

View File

@ -19,136 +19,147 @@ A selection of the settings in this file can be edited under ``Preferences...``,
Project settings
================
* :ref:`General <general>`
* :ref:`Maps <maps>`
* :ref:`Tilesets <tilesets>`
* :ref:`Events <events>`
* :ref:`Files & Identifiers <files-identifiers>`
A config file for project-specific settings is also created when opening a project in porymap for the first time. It is stored in your project root as ``porymap.project.cfg``. You may want to force commit this file so that other users will automatically have access to your project settings.
A second config file is created for user-specific settings. It is stored in your project root as ``porymap.user.cfg``. You should add this file to your gitignore.
The settings in ``porymap.project.cfg`` and ``porymap.user.cfg`` can be edited under ``Options -> Project Settings...``. Any changes made in this window will not take effect unless confirmed by selecting ``OK`` and then reloading the project.
Each of the settings in the ``Project Settings...`` window are described below.
.. |button-folder| image:: images/scripting-capabilities/folder.png
:width: 24
:height: 24
.. warning::
Changing any of the settings in the Project Settings Editor's red ``Warning`` box will require additional changes to your project to function correctly. Investigate the repository versions that have a setting natively supported to see what changes to your project are necessary.
.. |button-import-defaults| image:: images/settings-and-options/import-defaults.png
:height: 24
.. |button-restore-defaults| image:: images/settings-and-options/restore-defaults.png
:height: 24
.. |pokemon-icon-placeholder| image:: images/settings-and-options/pokemon-icon-placeholder.png
:width: 24
:height: 24
Preferences
-----------
.. _general:
.. figure:: images/settings-and-options/preferences.png
:align: left
:width: 60%
:alt: Preferences
Use Poryscript
If this is checked, a ``scripts.pory`` (and ``text.pory``, if applicable) file will be created alongside new maps, instead of a ``scripts.inc`` file. Additionally, ``.pory`` files will be considered when searching for scripts labels and when opening scripts files (in addition to the regular ``.inc`` files).
Defaults to ``unchecked``.
Field name: ``use_poryscript``
Show Wild Encounter Tables
If this is checked, the ``Wild Pokemon`` tab will be enabled and wild encounter data will be read from the project's encounters JSON file.
If no encounters JSON file is found this will be automatically unchecked.
Field name: ``use_encounter_json``
Default Tilesets
----------------
.. figure:: images/settings-and-options/default-tilesets.png
:align: left
:width: 60%
:alt: Default Tilesets
Default Primary/Secondary Tilesest
These will be the initially-selected tilesets when creating a new map, and will be used if a layout's tileset fails to load. If a default tileset is not found then the first tileset in the respective list will be used instead.
The default primary tileset is ``gTileset_General``.
The default secondary tileset is ``gTileset_PalletTown`` for ``pokefirered``, and ``gTileset_Petalburg`` for other versions.
Field names: ``default_primary_tileset`` and ``default_secondary_tileset``
New Map Defaults
----------------
.. figure:: images/settings-and-options/new-map-defaults.png
:align: left
:width: 60%
:alt: New Map Defaults
Border Metatiles
This is list of metatile ID values that will be used to fill the border on new maps. The spin boxes correspond to the top-left, top-right, bottom-left, and bottom-right border metatiles respectively.
If ``Enable Custom Border Size`` is checked, this will instead be a comma-separated list of metatile ID values that will be used to fill the border on new maps. Values in the list will be read sequentially to fill the new border left-to-right top-to-bottom. If the number of metatiles in the border for a new map is not the same as the number of values in the list then the border will be filled with metatile ID ``0x000`` instead.
Defaults to ``0x014``, ``0x015``, ``0x01C``, ``0x01D`` for ``pokefirered``, and ``0x1D4``, ``0x1D5``, ``0x1DC``, ``0x1DD`` for other versions.
Field name: ``new_map_border_metatiles``
Fill Metatile
This is the metatile ID value that will be used to fill new maps.
Defaults to ``0x1``.
Field name: ``new_map_metatile``
Elevation
This is the elevation that will be used to fill new maps. New maps will be filled with passable collision.
Defaults to ``3``.
Field name: ``new_map_elevation``
Create separate text file
If this is checked, a ``text.inc`` (or ``text.pory``) file will be created alongside new maps.
Defaults to ``unchecked`` for ``pokeemerald`` and ``checked`` for other versions.
Field name: ``create_map_text_file``
Prefabs
General
-------
.. figure:: images/settings-and-options/prefabs.png
:align: left
:width: 60%
:alt: Prefabs
.. figure:: images/settings-and-options/tab-general.png
:alt: General tab
Prefabs Path
This is the file path to a ``.json`` file that contains definitions of prefabs. This will be used to populate the ``Prefabs`` panel on the ``Map`` tab. If no path is specified prefabs will be saved to a new ``prefabs.json`` file in the root project folder. A new file can be selected with the folder button.
Use Poryscript
If this is checked, a ``scripts.pory`` (and ``text.pory``, if applicable) file will be created alongside new maps, instead of a ``scripts.inc`` file. Additionally, ``.pory`` files will be considered when searching for scripts labels and when opening scripts files (in addition to the regular ``.inc`` files).
The ``Import Defaults`` button will populate the specified file with version-specific prefabs constructed using the vanilla tilesets. This will overwrite any existing prefabs.
Defaults to ``unchecked``.
Field name: ``prefabs_filepath``.
Show Wild Encounter Tables
If this is checked, the ``Wild Pokemon`` tab will be enabled and wild encounter data will be read from the project's encounters JSON file.
Additionally, there is a ``prefabs_import_prompted`` field that should not be edited.
Defaults to ``checked``. If no encounters JSON file is found this will be automatically unchecked.
Prefabs
``Prefabs Path`` is the file path to a ``.json`` file that contains definitions of prefabs. This will be used to populate the ``Prefabs`` panel on the ``Map`` tab. If no path is specified prefabs will be saved to a new ``prefabs.json`` file in the root project folder. A new file can be selected with the |button-folder| button or by editing the file path.
Base game version
-----------------
The |button-import-defaults| button will populate the specified file with version-specific prefabs constructed using the vanilla tilesets. This will overwrite any existing prefabs.
.. figure:: images/settings-and-options/base-game-version.png
:align: left
:width: 60%
:alt: Base Game Version
Collision Graphics
``Image Path`` is a path to any image file you'd like to use to represent collision and elevation values on the ``Collision`` tab. A new file can be selected with the |button-folder| button or by editing the file path. The image will be evenly divided into segments, with each row representing an elevation value (starting with ``0`` at the top) and each column representing a collision value (starting with ``0`` on the left).
This is the name of base pret repository for this project. The options are ``pokeruby``, ``pokefirered``, and ``pokeemerald``, and can be selected (or automatically from the project folder name) when the project is first opened. Changing the base game version setting will prompt you to restore the default project settings for any of the three versions. You can also do this for the currently-selected base game version by selecting ``Restore Defaults`` at the bottom. For up-to-date projects changing this setting has no other effect.
Your image does not need to have a row/column for every valid elevation/collision value (for instance, the default collision values range from ``0-3``, but because ``2-3`` are semantically the same as ``1`` they are not displayed). You can specify the highest elevation and collision value represented on your image with ``Max Elevation`` and ``Max Collision``.
Field name: ``base_game_version``
Note: Images with transparency may not function correctly when displayed on the map.
The filepath defaults empty, which will use `Porymap's original image <https://github.com/huderlem/porymap/blob/master/resources/images/collisions.png>`_. ``Max Elevation`` and ``Max Collision`` default to ``15`` and ``1`` respectively.
Tilesets / Metatiles
--------------------
Pokémon Icons
Porymap can display Pokémon species icons that it reads from your project on the ``Wild Pokemon`` tab. If Porymap fails to load your icon image, or if you'd like to display your own icon in Porymap for any reason, you can select a new image with the |button-folder| button or by editing the file path. You can select a species with the dropdown to edit the path for a different icon.
.. figure:: images/settings-and-options/tilesets-metatiles.png
:align: left
:width: 60%
:alt: Tilesets / Metatiles
If your custom icon or the default icon fails to load a |pokemon-icon-placeholder| icon will be displayed.
Defaults to empty (the path in your project where Porymap expects to find each icon).
Base Game Version
This is the name of the base pret repository for this project. Changing this setting will prompt you to restore the default project settings for any of the three versions. You can also do this for the currently-selected base game version by selecting |button-restore-defaults| at the bottom of the window. Aside from determining the default settings in this window, the base game version also determines the default settings when initializing the region map and when importing default prefabs.
Defaults to ``pokeruby``, ``pokefirered``, or ``pokeemerald`` depending on the project folder name. If the folder name doesn't match you will be prompted to select a version on first launch.
.. _maps:
Maps
----
.. figure:: images/settings-and-options/tab-maps.png
:alt: Maps tab
Map Data Defaults
Border Metatiles
This is list of metatile ID values that will be used to fill the border on new maps. The spin boxes correspond to the top-left, top-right, bottom-left, and bottom-right border metatiles respectively.
If ``Enable Custom Border Size`` is checked, this will instead be a comma-separated list of metatile ID values that will be used to fill the border on new maps. Values in the list will be read sequentially to fill the new border left-to-right top-to-bottom. If the number of metatiles in the border for a new map is not the same as the number of values in the list then the border will be filled with metatile ID ``0x000`` instead.
Defaults to ``0x014``, ``0x015``, ``0x01C``, ``0x01D`` for ``pokefirered``, and ``0x1D4``, ``0x1D5``, ``0x1DC``, ``0x1DD`` for other versions.
Metatile ID
This is the metatile ID value that will be used to fill new maps.
Defaults to ``0x1``.
Collision
This is the collision value that will be used to fill new maps. It will also be used to set the default selection on the Collision tab when the project is first opened.
Defaults to ``0``.
Elevation
This is the elevation value that will be used to fill new maps. It will also be used to set the default selection on the Collision tab when the project is first opened.
Defaults to ``3``.
Create separate text file
If this is checked, a ``text.inc`` (or ``text.pory``) file will be created alongside new maps.
Defaults to ``unchecked`` for ``pokeemerald`` and ``checked`` for other versions.
Map Data Layout
Each of these three settings are bit masks that will be used to read and write an attribute of the data that makes up each map space (metatile ID, collision, and elevation). A warning will be displayed if any of the masks overlap. Their values may be read from ``#define`` s in your project, in which case editing will be disabled and you can change their values by modifying them in your project.
Default to being read from ``MAPGRID_METATILE_ID_MASK``, ``MAPGRID_COLLISION_MASK``, and ``MAPGRID_ELEVATION_MASK``. If they can't be read, they default to ``0x3FF``, ``0xC00``, and ``0xF000`` respectively.
Enable 'Floor Number'
If this is checked, a ``Floor Number`` option will become available on the ``Header`` tab and on the new map prompt. For more information see `Editing Map Headers <https://huderlem.github.io/porymap/manual/editing-map-header.html>`_.
Defaults to ``checked`` for ``pokefirered`` and ``unchecked`` for other versions.
Enable 'Allow Running/Biking/Escaping'
If this is checked, ``Allow Running``, ``Allow Biking``, and ``Allow Dig & Escape Rope`` options will become available on the ``Header`` tab and on the new map prompt. For more information see `Editing Map Headers <https://huderlem.github.io/porymap/manual/editing-map-header.html>`_.
Defaults to ``unchecked`` for ``pokeruby`` and ``checked`` for other versions.
Enable Custom Border Size
If this is checked, ``Border Width`` and ``Border Height`` options will become available under the ``Change Dimensions`` button and on the new map prompt. If it is unchecked all maps will use the default 2x2 dimensions.
Defaults to ``checked`` for ``pokefirered`` and ``unchecked`` for other versions.
.. _tilesets:
Tilesets
--------
.. figure:: images/settings-and-options/tab-tilesets.png
:alt: Tilesets tab
Default Primary/Secondary Tilesest
These will be the initially-selected tilesets when creating a new map, and will be used if a layout's tileset fails to load. If a default tileset is not found then the first tileset in the respective list will be used instead.
The default primary tileset is ``gTileset_General``.
The default secondary tileset is ``gTileset_PalletTown`` for ``pokefirered``, and ``gTileset_Petalburg`` for other versions.
Enable Triple Layer Metatiles
Metatile data normally consists of 2 layers with 4 tiles each. If this is checked, they should instead consist of 3 layers with 4 tiles each. Additionally, the ``Layer Type`` option in the ``Tileset Editor`` will be removed. Note that layer type data will still be read and written according to your ``Layer Type mask`` setting.
@ -157,155 +168,110 @@ Enable Triple Layer Metatiles
Defaults to ``unchecked``
Field name: ``enable_triple_layer_metatiles``
Attributes size
The number of bytes used per metatile for metatile attributes. The data in each of your project's ``metatile_attributes.bin`` files will be expected to be ``s * n``, where ``s`` is this size and ``n`` is the number of metatiles in the tileset. Additionally, new ``metatile_attributes.bin`` will be included in the project with a corresponding ``INCBIN_U8``, ``INCBIN_U16``, or ``INCBIN_U32`` directive.
Changing this setting will automatically enforce the new limit on the metatile attribute mask settings below.
Changing this setting will automatically enforce the new limit on the metatile attribute mask settings.
Defaults to ``4`` for ``pokefirered`` and ``2`` for other versions.
Field name: ``metatile_attributes_size``
Attribute masks
Each of the following four settings are bit masks that will be used to read and write a specific metatile attribute from the metatile attributes data. If you are instead importing metatile attribute data from AdvanceMap, a default mask value will be used to read the data, and the mask value specified here will be used to write the new file.
Each of these four settings are bit masks that will be used to read and write a specific metatile attribute from the metatile attributes data. If you are instead importing metatile attribute data from AdvanceMap, a default mask value will be used to read the data, and the mask value specified here will be used to write the new file.
If any of the mask values are set to ``0x0``, the corresponding option in the Tileset Editor will be removed. The maximum for all the attribute masks is determined by the Attributes size setting.
If any of the mask values are set to ``0x0``, the corresponding option in the Tileset Editor will be removed. The maximum for all the attribute masks is determined by the Attributes size setting. A warning will be displayed if any of the masks overlap.
.. warning::
If any of the metatile attribute masks have overlapping bits they may behave in unexpected ways. A warning will be logged in the Porymap log file if this happens
- Metatile Behavior mask
This is the mask value for the ``Metatile Behavior`` metatile attribute.
Defaults to being read from ``sMetatileAttrMasks`` or ``METATILE_ATTR_BEHAVIOR_MASK``. If these can't be read, defaults to ``0x1FF`` for ``pokefirered``, and ``0xFF`` for other versions.
Metatile Behavior mask
See Attribute masks. This is the mask value for the ``Metatile Behavior`` metatile attribute.
- Layer Type mask
This is the mask value for the ``Layer Type`` metatile attribute. If the value is set to ``0x0`` the ``Layer Type`` option will be disabled in the Tileset Editor, and all metatiles will be treated in the editor as if they had the ``Normal`` layer type.
Defaults to ``0x1FF`` for ``pokefirered``, and ``0xFF`` for other versions.
Defaults to being read from ``sMetatileAttrMasks`` or ``METATILE_ATTR_LAYER_MASK``. If these can't be read, defaults to ``0x60000000`` for ``pokefirered``, and ``0xF000`` for other versions.
Field name: ``metatile_behavior_mask``
- Encounter Type mask
This is the mask value for the ``Encounter Type`` metatile attribute.
Layer Type mask
See Attribute masks. This is the mask value for the ``Layer Type`` metatile attribute. If the value is set to ``0x0`` the ``Layer Type`` option will be disabled in the Tileset Editor, and all metatiles will be treated in the editor as if they had the ``Normal`` layer type.
Defaults to being read from ``sMetatileAttrMasks``. If this can't be read, defaults to ``0x7000000`` for ``pokefirered``, and ``0x0`` for other versions.
Defaults to ``0x60000000`` for ``pokefirered`` and ``0xF000`` for other versions.
- Terrain Type mask
This is the mask value for the ``Terrain Type`` metatile attribute.
Field name: ``metatile_layer_type_mask``
Encounter Type mask
See Attribute masks. This is the mask value for the ``Encounter Type`` metatile attribute.
Defaults to ``0x7000000`` for ``pokefirered`` and ``0x0`` for other versions.
Field name: ``metatile_encounter_type_mask``
Terrain Type mask
See Attribute masks. This is the mask value for the ``Terrain Type`` metatile attribute.
Defaults to ``0x3E00`` for ``pokefirered`` and ``0x0`` for other versions.
Field name: ``metatile_terrain_type_mask``
Defaults to being read from ``sMetatileAttrMasks``. If this can't be read, defaults to ``0x3E00`` for ``pokefirered``, and ``0x0`` for other versions.
Output 'callback' and 'isCompressed' fields
If these are checked, then ``callback`` and ``isCompressed`` fields will be output in the C data for new tilesets. Their default values will be ``NULL`` and ``TRUE``, respectively.
Defaults to ``checked`` for both.
Field names: ``tilesets_have_callback`` and ``tilesets_have_is_compressed``
Project Files
-------------
This is a list of the files and folders Porymap expects from your project. Each can be overridden by typing a new path or selecting a file/folder with the folder button. If the file/folder doesn't exist when the project is loaded then the default path will be used instead.
For more information on each of these files/folders, see https://huderlem.github.io/porymap/manual/project-files.html
Field name: ``path/<identifier>``
.. _events:
Events
------
.. figure:: images/settings-and-options/events.png
:align: left
:width: 60%
:alt: Events
.. figure:: images/settings-and-options/tab-events.png
:alt: Events tab
Default Icons
Each event group is represented by a unique icon on the ``Events`` tab of the main editor. Here you can provide filepaths to your own image files to replace these icons, either by selecting the |button-folder| button or by editing the file path directly.
Events in the ``Objects`` group will only use this icon if there are no graphics associated with their ``Sprite`` field.
The filepaths default to empty, which will use `Porymap's original icons <https://github.com/huderlem/porymap/blob/master/resources/images/Entities_16x16.png>`_.
Warp Behaviors
By default, Warp Events only function as exits if they're positioned on a metatile whose Metatile Behavior is treated specially in your project's code. If any Warp Events are positioned on a metatile that doesn't have one of these behaviors they will display a warning. Here you can disable that warning, or edit the list of behavior names that will silence the warning.
Defaults to ``unchecked``, i.e. the warning is enabled. The list of behaviors is initially populated with all the vanilla warp behavior names across pokeemerald, pokefirered, and pokeruby.
Enable Clone Objects
If this is checked Clone Object Events will be available on the ``Events`` tab. For more information see https://huderlem.github.io/porymap/manual/editing-map-events.html#clone-object-events
If this is checked Clone Object Events will be available on the ``Events`` tab. For more information see `Clone Object Events <https://huderlem.github.io/porymap/manual/editing-map-events.html#clone-object-events>`_.
Defaults to ``checked`` for ``pokefirered`` and ``unchecked`` for other versions.
Field name: ``enable_event_clone_object``
Enable Secret Bases
If this is checked Secret Base Events will be available on the ``Events`` tab. For more information see https://huderlem.github.io/porymap/manual/editing-map-events.html#secret-base-event
If this is checked Secret Base Events will be available on the ``Events`` tab. For more information see `Secret Base Events <https://huderlem.github.io/porymap/manual/editing-map-events.html#secret-base-event>`_.
Defaults to ``unchecked`` for ``pokefirered`` and ``checked`` for other versions.
Field name: ``enable_event_secret_base``
Enable Weather Triggers
If this is checked Weather Trigger Events will be available on the ``Events`` tab. For more information see https://huderlem.github.io/porymap/manual/editing-map-events.html#weather-trigger-events
If this is checked Weather Trigger Events will be available on the ``Events`` tab. For more information see `Weather Trigger Events <https://huderlem.github.io/porymap/manual/editing-map-events.html#weather-trigger-events>`_.
Defaults to ``unchecked`` for ``pokefirered`` and ``checked`` for other versions.
Field name: ``enable_event_weather_trigger``
Enable 'Quantity' for Hidden Items
If this is checked the ``Quantity`` property will be available for Hidden Item Events. For more information see https://huderlem.github.io/porymap/manual/editing-map-events.html#hidden-item-event
If this is checked the ``Quantity`` property will be available for Hidden Item Events. For more information see `Hidden Item Events <https://huderlem.github.io/porymap/manual/editing-map-events.html#hidden-item-event>`_.
Defaults to ``checked`` for ``pokefirered`` and ``unchecked`` for other versions.
Field name: ``enable_hidden_item_quantity``
Enable 'Requires Itemfinder' for Hidden Items
If this is checked the ``Requires Itemfinder`` property will be available for Hidden Item Events. For more information see https://huderlem.github.io/porymap/manual/editing-map-events.html#hidden-item-event
If this is checked the ``Requires Itemfinder`` property will be available for Hidden Item Events. For more information see `Hidden Item Events <https://huderlem.github.io/porymap/manual/editing-map-events.html#hidden-item-event>`_.
Defaults to ``checked`` for ``pokefirered`` and ``unchecked`` for other versions.
Field name: ``enable_hidden_item_requires_itemfinder``
Enable 'Repsawn Map/NPC' for Heal Locations
If this is checked the ``Respawn Map`` and ``Respawn NPC`` properties will be available for Heal Location events. For more information see https://huderlem.github.io/porymap/manual/editing-map-events.html#heal-location-healspots
If this is checked the ``Respawn Map`` and ``Respawn NPC`` properties will be available for Heal Location events. For more information see `Heal Locations <https://huderlem.github.io/porymap/manual/editing-map-events.html#heal-location-healspots>`_.
Defaults to ``checked`` for ``pokefirered`` and ``unchecked`` for other versions.
Field name: ``enable_heal_location_respawn_data``
.. _files-identifiers:
Maps
----
Files & Identifiers
-------------------
.. figure:: images/settings-and-options/maps.png
:align: left
:width: 60%
:alt: Maps
.. figure:: images/settings-and-options/tab-files.png
:alt: Files tab
Enable 'Floor Number'
If this is checked, a ``Floor Number`` option will become available on the ``Header`` tab and on the new map prompt. For more information see https://huderlem.github.io/porymap/manual/editing-map-header.html
.. figure:: images/settings-and-options/tab-identifiers.png
:alt: Identifiers tab
Defaults to ``checked`` for ``pokefirered`` and ``unchecked`` for other versions.
These two tabs provide a way to override the filepaths and symbol/macro names Porymap expects to find in your project.
Field name: ``enable_floor_number``
For ``Files``, each can be overridden by typing a new path or selecting a file/folder with the |button-folder| button. Paths are expected to be relative to the root project folder. If no path is specified, or if the file/folder specified does not exist, then the default path will be used instead.
Enable 'Allow Running/Biking/Escaping'
If this is checked, ``Allow Running``, ``Allow Biking``, and ``Allow Dig & Escape Rope`` options will become available on the ``Header`` tab and on the new map prompt. For more information see https://huderlem.github.io/porymap/manual/editing-map-header.html
For ``Identifiers``, each can be overridden by typing a new name in the line edit. Overrides with ``regex`` in the name support the `regular expression syntax <https://perldoc.perl.org/perlre>`_ used by Qt.
Defaults to ``unchecked`` for ``pokeruby`` and ``checked`` for other versions.
For more information on what each of these overrides does, see `Project Files <https://huderlem.github.io/porymap/manual/project-files.html>`_.
Field name: ``enable_map_allow_flags``
Enable Custom Border Size
If this is checked, ``Border Width`` and ``Border Height`` options will become available under the ``Change Dimensions`` button and on the new map prompt. If it is unchecked all maps will use the default 2x2 dimensions.
Defaults to ``checked`` for ``pokefirered`` and ``unchecked`` for other versions.
Field name: ``use_custom_border_size``
Additional Fields
-----------------
There are two additional fields in ``porymap.user.cfg`` that aren't described above.
``recent_map`` is the name of the most recently opened map and is updated automatically. This is the map that will be opened when the project is opened. If no map is found with this name (or if the field is empty) then the first map in the map list will be used instead.
``custom_scripts`` is a comma-separated list of filepaths to scripts for Porymap's API. These can be edited under ``Options -> Custom Scripts...``. For more information see https://huderlem.github.io/porymap/manual/scripting-capabilities.html

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>535</width>
<width>540</width>
<height>355</height>
</rect>
</property>
@ -109,7 +109,7 @@
<item>
<widget class="QLabel" name="label_Manual">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://huderlem.github.io/porymap/manual/scripting-capabilities.html&quot;&gt;&lt;span style=&quot; text-decoration: underline;&quot;&gt;What are custom scripts?&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://huderlem.github.io/porymap/manual/scripting-capabilities.html&quot;&gt;&lt;span style=&quot; text-decoration: underline;&quot;&gt;Help&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>

View File

@ -1144,7 +1144,14 @@
<property name="bottomMargin">
<number>3</number>
</property>
<item row="0" column="0">
<item row="3" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Opacity</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="3">
<widget class="QScrollArea" name="scrollArea_1">
<property name="widgetResizable">
<bool>true</bool>
@ -1154,8 +1161,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>428</width>
<height>696</height>
<width>427</width>
<height>512</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_7">
@ -1174,29 +1181,6 @@
<property name="spacing">
<number>0</number>
</property>
<item row="0" column="1">
<widget class="QLabel" name="label">
<property name="text">
<string>Opacity</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSlider" name="horizontalSlider_CollisionTransparency">
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>50</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<spacer name="horizontalSpacer_18">
<property name="orientation">
@ -1224,6 +1208,12 @@
<height>512</height>
</size>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
</widget>
</item>
<item row="2" column="2">
@ -1256,6 +1246,83 @@
</widget>
</widget>
</item>
<item row="6" column="0" colspan="3">
<widget class="QSlider" name="horizontalSlider_CollisionZoom">
<property name="minimum">
<number>10</number>
</property>
<property name="maximum">
<number>90</number>
</property>
<property name="value">
<number>30</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="4" column="0" colspan="3">
<widget class="QSlider" name="horizontalSlider_CollisionTransparency">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>50</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="0" column="0" colspan="3">
<widget class="QFrame" name="frame_5">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="1">
<widget class="QSpinBox" name="spinBox_SelectedElevation"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
<string>Collision</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="spinBox_SelectedCollision"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>Elevation</string>
</property>
</widget>
</item>
<item row="0" column="2">
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_prefabs">
@ -1314,8 +1381,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>398</width>
<height>631</height>
<width>382</width>
<height>611</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_14">
@ -1615,8 +1682,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>434</width>
<height>581</height>
<width>100</width>
<height>16</height>
</rect>
</property>
<property name="sizePolicy">
@ -1709,8 +1776,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>434</width>
<height>581</height>
<width>100</width>
<height>16</height>
</rect>
</property>
<property name="sizePolicy">
@ -1803,8 +1870,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>434</width>
<height>581</height>
<width>100</width>
<height>16</height>
</rect>
</property>
<property name="sizePolicy">
@ -1903,8 +1970,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>434</width>
<height>581</height>
<width>100</width>
<height>16</height>
</rect>
</property>
<property name="sizePolicy">
@ -1997,8 +2064,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>434</width>
<height>581</height>
<width>100</width>
<height>16</height>
</rect>
</property>
<property name="sizePolicy">
@ -2051,8 +2118,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>434</width>
<height>625</height>
<width>100</width>
<height>30</height>
</rect>
</property>
<property name="sizePolicy">
@ -2388,7 +2455,7 @@
<string>Custom fields will be added to the map.json file for the current map.</string>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderCascadingSectionResizes">
<bool>false</bool>

File diff suppressed because it is too large Load Diff

View File

@ -201,83 +201,41 @@
<bool>false</bool>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_BottomTop">
<property name="text">
<string>Bottom/Top</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="3">
<widget class="NoScrollComboBox" name="comboBox_metatileBehaviors"/>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_encounterType">
<property name="text">
<string>Encounter Type</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_terrainType">
<property name="text">
<string>Terrain Type</string>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="NoScrollComboBox" name="comboBox_terrainType"/>
</item>
<item row="12" column="0" colspan="2">
<widget class="QLabel" name="label_metatileLabel">
<property name="text">
<string>Metatile Label (Optional)</string>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<item row="1" column="1" colspan="3">
<widget class="NoScrollComboBox" name="comboBox_layerType"/>
</item>
<item row="13" column="2">
<widget class="QToolButton" name="copyButton_metatileLabel">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Copies the full metatile label to the clipboard.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../resources/images.qrc">
<normaloff>:/icons/clipboard.ico</normaloff>:/icons/clipboard.ico</iconset>
</property>
</widget>
<item row="7" column="0" colspan="4">
<widget class="NoScrollComboBox" name="comboBox_metatileBehaviors"/>
</item>
<item row="13" column="0" colspan="2">
<item row="13" column="0" colspan="3">
<widget class="QLineEdit" name="lineEdit_metatileLabel">
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_layerType">
<item row="12" column="0" colspan="3">
<widget class="QLabel" name="label_metatileLabel">
<property name="text">
<string>Layer Type</string>
<string>Metatile Label (Optional)</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<item row="2" column="0" colspan="3">
<widget class="QLabel" name="label_metatileBehavior">
<property name="text">
<string>Metatile Behavior</string>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="NoScrollComboBox" name="comboBox_encounterType"/>
</item>
<item row="1" column="0">
<widget class="QGraphicsView" name="graphicsView_metatileLayers">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>66</width>
@ -298,6 +256,54 @@
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_BottomTop">
<property name="text">
<string>Bottom/Top</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_encounterType">
<property name="text">
<string>Encounter Type</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_terrainType">
<property name="text">
<string>Terrain Type</string>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="NoScrollComboBox" name="comboBox_terrainType"/>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_layerType">
<property name="text">
<string>Layer Type</string>
</property>
</widget>
</item>
<item row="13" column="3">
<widget class="QToolButton" name="copyButton_metatileLabel">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Copies the full metatile label to the clipboard.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../resources/images.qrc">
<normaloff>:/icons/clipboard.ico</normaloff>:/icons/clipboard.ico</iconset>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="NoScrollComboBox" name="comboBox_encounterType"/>
</item>
</layout>
</widget>
</item>
@ -565,10 +571,6 @@
<addaction name="actionChange_Metatiles_Count"/>
<addaction name="actionChange_Palettes"/>
<addaction name="separator"/>
<addaction name="actionShow_Unused"/>
<addaction name="actionShow_Counts"/>
<addaction name="actionShow_UnusedTiles"/>
<addaction name="separator"/>
<addaction name="actionExport_Primary_Tiles_Image"/>
<addaction name="actionExport_Secondary_Tiles_Image"/>
<addaction name="actionExport_Primary_Metatiles_Image"/>
@ -584,8 +586,20 @@
<addaction name="actionUndo"/>
<addaction name="actionRedo"/>
</widget>
<widget class="QMenu" name="menuView">
<property name="title">
<string>View</string>
</property>
<addaction name="actionLayer_Grid"/>
<addaction name="actionMetatile_Grid"/>
<addaction name="separator"/>
<addaction name="actionShow_Counts"/>
<addaction name="actionShow_Unused"/>
<addaction name="actionShow_UnusedTiles"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuEdit"/>
<addaction name="menuView"/>
<addaction name="menuTools"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
@ -711,6 +725,25 @@
<string>Ctrl+V</string>
</property>
</action>
<action name="actionLayer_Grid">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Layer Grid</string>
</property>
</action>
<action name="actionMetatile_Grid">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Metatile Grid</string>
</property>
<property name="shortcut">
<string>Ctrl+G</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View File

@ -9,6 +9,8 @@
#include <QKeySequence>
#include <QMultiMap>
#include "events.h"
// In both versions the default new map border is a generic tree
#define DEFAULT_BORDER_RSE (QList<uint16_t>{0x1D4, 0x1D5, 0x1DC, 0x1DD})
#define DEFAULT_BORDER_FRLG (QList<uint16_t>{0x14, 0x15, 0x1C, 0x1D})
@ -36,8 +38,8 @@ protected:
virtual void onNewConfigFileCreated() = 0;
virtual void setUnreadKeys() = 0;
bool getConfigBool(QString key, QString value);
int getConfigInteger(QString key, QString value, int min, int max, int defaultValue);
uint32_t getConfigUint32(QString key, QString value, uint32_t min, uint32_t max, uint32_t defaultValue);
int getConfigInteger(QString key, QString value, int min = INT_MIN, int max = INT_MAX, int defaultValue = 0);
uint32_t getConfigUint32(QString key, QString value, uint32_t min = 0, uint32_t max = UINT_MAX, uint32_t defaultValue = 0);
private:
bool saveDisabled = false;
};
@ -54,16 +56,22 @@ public:
this->mapSortOrder = MapSortOrder::Group;
this->prettyCursors = true;
this->collisionOpacity = 50;
this->collisionZoom = 30;
this->metatilesZoom = 30;
this->showPlayerView = false;
this->showCursorTile = true;
this->showBorder = true;
this->showGrid = false;
this->showTilesetEditorMetatileGrid = false;
this->showTilesetEditorLayerGrid = true;
this->monitorFiles = true;
this->tilesetCheckerboardFill = true;
this->theme = "default";
this->textEditorOpenFolder = "";
this->textEditorGotoLine = "";
this->paletteEditorBitDepth = 24;
this->projectSettingsTab = 0;
this->warpBehaviorWarningDisabled = false;
}
void setRecentProject(QString project);
void setReopenOnLaunch(bool enabled);
@ -76,17 +84,22 @@ public:
void setProjectSettingsEditorGeometry(QByteArray, QByteArray);
void setCustomScriptsEditorGeometry(QByteArray, QByteArray);
void setCollisionOpacity(int opacity);
void setCollisionZoom(int zoom);
void setMetatilesZoom(int zoom);
void setShowPlayerView(bool enabled);
void setShowCursorTile(bool enabled);
void setShowBorder(bool enabled);
void setShowGrid(bool enabled);
void setShowTilesetEditorMetatileGrid(bool enabled);
void setShowTilesetEditorLayerGrid(bool enabled);
void setMonitorFiles(bool monitor);
void setTilesetCheckerboardFill(bool checkerboard);
void setTheme(QString theme);
void setTextEditorOpenFolder(const QString &command);
void setTextEditorGotoLine(const QString &command);
void setPaletteEditorBitDepth(int bitDepth);
void setProjectSettingsTab(int tab);
void setWarpBehaviorWarningDisabled(bool disabled);
QString getRecentProject();
bool getReopenOnLaunch();
MapSortOrder getMapSortOrder();
@ -98,17 +111,22 @@ public:
QMap<QString, QByteArray> getProjectSettingsEditorGeometry();
QMap<QString, QByteArray> getCustomScriptsEditorGeometry();
int getCollisionOpacity();
int getCollisionZoom();
int getMetatilesZoom();
bool getShowPlayerView();
bool getShowCursorTile();
bool getShowBorder();
bool getShowGrid();
bool getShowTilesetEditorMetatileGrid();
bool getShowTilesetEditorLayerGrid();
bool getMonitorFiles();
bool getTilesetCheckerboardFill();
QString getTheme();
QString getTextEditorOpenFolder();
QString getTextEditorGotoLine();
int getPaletteEditorBitDepth();
int getProjectSettingsTab();
bool getWarpBehaviorWarningDisabled();
protected:
virtual QString getConfigFilepath() override;
virtual void parseConfigKeyValue(QString key, QString value) override;
@ -138,17 +156,22 @@ private:
QByteArray customScriptsEditorGeometry;
QByteArray customScriptsEditorState;
int collisionOpacity;
int collisionZoom;
int metatilesZoom;
bool showPlayerView;
bool showCursorTile;
bool showBorder;
bool showGrid;
bool showTilesetEditorMetatileGrid;
bool showTilesetEditorLayerGrid;
bool monitorFiles;
bool tilesetCheckerboardFill;
QString theme;
QString textEditorOpenFolder;
QString textEditorGotoLine;
int paletteEditorBitDepth;
int projectSettingsTab;
bool warpBehaviorWarningDisabled;
};
extern PorymapConfig porymapConfig;
@ -159,6 +182,61 @@ enum BaseGameVersion {
pokeemerald,
};
enum ProjectIdentifier {
symbol_facing_directions,
symbol_obj_event_gfx_pointers,
symbol_pokemon_icon_table,
symbol_wild_encounters,
symbol_heal_locations,
symbol_spawn_points,
symbol_spawn_maps,
symbol_spawn_npcs,
symbol_attribute_table,
symbol_tilesets_prefix,
define_obj_event_count,
define_min_level,
define_max_level,
define_tiles_primary,
define_tiles_total,
define_metatiles_primary,
define_pals_primary,
define_pals_total,
define_map_size,
define_mask_metatile,
define_mask_collision,
define_mask_elevation,
define_mask_behavior,
define_mask_layer,
define_attribute_behavior,
define_attribute_layer,
define_attribute_terrain,
define_attribute_encounter,
define_metatile_label_prefix,
define_heal_locations_prefix,
define_spawn_prefix,
define_map_prefix,
define_map_dynamic,
define_map_empty,
define_map_section_prefix,
define_map_section_empty,
define_map_section_count,
regex_behaviors,
regex_obj_event_gfx,
regex_items,
regex_flags,
regex_vars,
regex_movement_types,
regex_map_types,
regex_battle_scenes,
regex_weather,
regex_coord_event_weather,
regex_secret_bases,
regex_sign_facing_directions,
regex_trainer_types,
regex_music,
regex_species,
};
enum ProjectFilePath {
data_map_folders,
data_scripts_folders,
@ -185,7 +263,6 @@ enum ProjectFilePath {
constants_global,
constants_map_groups,
constants_items,
constants_opponents,
constants_flags,
constants_vars,
constants_weather,
@ -201,9 +278,13 @@ enum ProjectFilePath {
constants_region_map_sections,
constants_metatile_labels,
constants_metatile_behaviors,
constants_species,
constants_fieldmap,
global_fieldmap,
fieldmap,
initial_facing_table,
pokemon_icon_table,
pokemon_gfx,
};
class ProjectConfig: public KeyValueConfigBase
@ -217,17 +298,28 @@ public:
// Reset non-version-specific settings
this->usePoryScript = false;
this->enableTripleLayerMetatiles = false;
this->newMapMetatileId = 1;
this->newMapElevation = 3;
this->defaultMetatileId = 1;
this->defaultElevation = 3;
this->defaultCollision = 0;
this->defaultPrimaryTileset = "gTileset_General";
this->prefabFilepath = QString();
this->prefabImportPrompted = false;
this->tilesetsHaveCallback = true;
this->tilesetsHaveIsCompressed = true;
this->filePaths.clear();
this->eventIconPaths.clear();
this->pokemonIconPaths.clear();
this->collisionSheetPath = QString();
this->collisionSheetWidth = 2;
this->collisionSheetHeight = 16;
this->blockMetatileIdMask = 0x03FF;
this->blockCollisionMask = 0x0C00;
this->blockElevationMask = 0xF000;
this->identifiers.clear();
this->readKeys.clear();
}
static const QMap<ProjectFilePath, std::pair<QString, QString>> defaultPaths;
static const QMap<ProjectIdentifier, QPair<QString, QString>> defaultIdentifiers;
static const QMap<ProjectFilePath, QPair<QString, QString>> defaultPaths;
static const QStringList versionStrings;
void reset(BaseGameVersion baseGameVersion);
void setBaseGameVersion(BaseGameVersion baseGameVersion);
@ -261,20 +353,28 @@ public:
bool getTripleLayerMetatilesEnabled();
int getNumLayersInMetatile();
int getNumTilesInMetatile();
void setNewMapMetatileId(uint16_t metatileId);
uint16_t getNewMapMetatileId();
void setNewMapElevation(int elevation);
int getNewMapElevation();
void setDefaultMetatileId(uint16_t metatileId);
uint16_t getDefaultMetatileId();
void setDefaultElevation(uint16_t elevation);
uint16_t getDefaultElevation();
void setDefaultCollision(uint16_t collision);
uint16_t getDefaultCollision();
void setNewMapBorderMetatileIds(QList<uint16_t> metatileIds);
QList<uint16_t> getNewMapBorderMetatileIds();
QString getDefaultPrimaryTileset();
QString getDefaultSecondaryTileset();
void setDefaultPrimaryTileset(QString tilesetName);
void setDefaultSecondaryTileset(QString tilesetName);
void setFilePath(QString pathId, QString path);
void setFilePath(ProjectFilePath pathId, QString path);
QString getFilePath(QString defaultPath, bool customOnly = false);
QString getFilePath(ProjectFilePath pathId, bool customOnly = false);
void setFilePath(const QString &pathId, const QString &path);
void setFilePath(ProjectFilePath pathId, const QString &path);
QString getCustomFilePath(ProjectFilePath pathId);
QString getCustomFilePath(const QString &pathId);
QString getFilePath(ProjectFilePath pathId);
void setIdentifier(ProjectIdentifier id, const QString &text);
void setIdentifier(const QString &id, const QString &text);
QString getCustomIdentifier(ProjectIdentifier id);
QString getCustomIdentifier(const QString &id);
QString getIdentifier(ProjectIdentifier id);
void setPrefabFilepath(QString filepath);
QString getPrefabFilepath();
void setPrefabImportPrompted(bool prompted);
@ -293,8 +393,28 @@ public:
void setMetatileTerrainTypeMask(uint32_t mask);
void setMetatileEncounterTypeMask(uint32_t mask);
void setMetatileLayerTypeMask(uint32_t mask);
uint16_t getBlockMetatileIdMask();
uint16_t getBlockCollisionMask();
uint16_t getBlockElevationMask();
void setBlockMetatileIdMask(uint16_t mask);
void setBlockCollisionMask(uint16_t mask);
void setBlockElevationMask(uint16_t mask);
bool getMapAllowFlagsEnabled();
void setMapAllowFlagsEnabled(bool enabled);
void setEventIconPath(Event::Group group, const QString &path);
QString getEventIconPath(Event::Group group);
void setPokemonIconPath(const QString &species, const QString &path);
QString getPokemonIconPath(const QString &species);
QHash<QString, QString> getPokemonIconPaths();
void setCollisionSheetPath(const QString &path);
QString getCollisionSheetPath();
void setCollisionSheetWidth(int width);
int getCollisionSheetWidth();
void setCollisionSheetHeight(int height);
int getCollisionSheetHeight();
void setWarpBehaviors(const QSet<uint32_t> &behaviors);
QSet<uint32_t> getWarpBehaviors();
protected:
virtual QString getConfigFilepath() override;
virtual void parseConfigKeyValue(QString key, QString value) override;
@ -304,6 +424,7 @@ protected:
private:
BaseGameVersion baseGameVersion;
QString projectDir;
QMap<ProjectIdentifier, QString> identifiers;
QMap<ProjectFilePath, QString> filePaths;
bool usePoryScript;
bool useCustomBorderSize;
@ -316,8 +437,9 @@ private:
bool enableFloorNumber;
bool createMapTextFile;
bool enableTripleLayerMetatiles;
uint16_t newMapMetatileId;
int newMapElevation;
uint16_t defaultMetatileId;
uint16_t defaultElevation;
uint16_t defaultCollision;
QList<uint16_t> newMapBorderMetatileIds;
QString defaultPrimaryTileset;
QString defaultSecondaryTileset;
@ -331,7 +453,16 @@ private:
uint32_t metatileTerrainTypeMask;
uint32_t metatileEncounterTypeMask;
uint32_t metatileLayerTypeMask;
uint16_t blockMetatileIdMask;
uint16_t blockCollisionMask;
uint16_t blockElevationMask;
bool enableMapAllowFlags;
QMap<Event::Group, QString> eventIconPaths;
QHash<QString, QString> pokemonIconPaths;
QString collisionSheetPath;
int collisionSheetWidth;
int collisionSheetHeight;
QSet<uint32_t> warpBehaviors;
};
extern ProjectConfig projectConfig;

27
include/core/bitpacker.h Normal file
View File

@ -0,0 +1,27 @@
#ifndef BITPACKER_H
#define BITPACKER_H
#include <QList>
class BitPacker
{
public:
BitPacker() = default;
BitPacker(uint32_t mask);
public:
void setMask(uint32_t mask);
uint32_t mask() const { return m_mask; }
uint32_t maxValue() const { return m_maxValue; }
uint32_t unpack(uint32_t data) const;
uint32_t pack(uint32_t value) const;
uint32_t clamp(uint32_t value) const;
private:
uint32_t m_mask = 0;
uint32_t m_maxValue = 0;
QList<uint32_t> m_setBits;
};
#endif // BITPACKER_H

View File

@ -14,10 +14,24 @@ public:
Block &operator=(const Block &);
bool operator ==(Block) const;
bool operator !=(Block) const;
uint16_t metatileId:10;
uint16_t collision:2;
uint16_t elevation:4;
void setMetatileId(uint16_t metatileId);
void setCollision(uint16_t collision);
void setElevation(uint16_t elevation);
uint16_t metatileId() const { return m_metatileId; }
uint16_t collision() const { return m_collision; }
uint16_t elevation() const { return m_elevation; }
uint16_t rawValue() const;
static void setLayout();
static uint16_t getMaxMetatileId();
static uint16_t getMaxCollision();
static uint16_t getMaxElevation();
static const uint16_t maxValue;
private:
uint16_t m_metatileId;
uint16_t m_collision;
uint16_t m_elevation;
};
#endif // BLOCK_H

View File

@ -111,6 +111,7 @@ public:
}
}
static QMap<Event::Group, const QPixmap*> icons;
// standard public methods
public:
@ -155,7 +156,7 @@ public:
const QMap<QString, QJsonValue> getCustomValues() { return this->customValues; }
void setCustomValues(const QMap<QString, QJsonValue> newCustomValues) { this->customValues = newCustomValues; }
virtual void loadPixmap(Project *project) = 0;
virtual void loadPixmap(Project *project);
void setPixmap(QPixmap newPixmap) { this->pixmap = newPixmap; }
QPixmap getPixmap() { return this->pixmap; }
@ -177,6 +178,7 @@ public:
static QString eventGroupToString(Event::Group group);
static QString eventTypeToString(Event::Type type);
static Event::Type eventTypeFromString(QString type);
static void setIcons();
// protected attributes
protected:
@ -337,14 +339,14 @@ public:
virtual QSet<QString> getExpectedFields() override;
virtual void loadPixmap(Project *) override;
void setDestinationMap(QString newDestinationMap) { this->destinationMap = newDestinationMap; }
QString getDestinationMap() { return this->destinationMap; }
void setDestinationWarpID(QString newDestinationWarpID) { this->destinationWarpID = newDestinationWarpID; }
QString getDestinationWarpID() { return this->destinationWarpID; }
void setWarningEnabled(bool enabled);
private:
QString destinationMap;
QString destinationWarpID;
@ -371,8 +373,6 @@ public:
virtual void setDefaultValues(Project *project) override = 0;
virtual QSet<QString> getExpectedFields() override = 0;
virtual void loadPixmap(Project *) override;
};
@ -472,8 +472,6 @@ public:
virtual void setDefaultValues(Project *project) override = 0;
virtual QSet<QString> getExpectedFields() override = 0;
virtual void loadPixmap(Project *project) override;
};
@ -614,8 +612,6 @@ public:
virtual QSet<QString> getExpectedFields() override { return QSet<QString>(); }
virtual void loadPixmap(Project *project) override;
void setIndex(int newIndex) { this->index = newIndex; }
int getIndex() { return this->index; }

View File

@ -71,7 +71,7 @@ public:
QList<int> metatileLayerOrder;
QList<float> metatileLayerOpacity;
void setName(QString mapName);
static QString mapConstantFromName(QString mapName);
static QString mapConstantFromName(QString mapName, bool includePrefix = true);
int getWidth();
int getHeight();
int getBorderWidth();

View File

@ -4,6 +4,7 @@
#include "tile.h"
#include "config.h"
#include "bitpacker.h"
#include <QImage>
#include <QPoint>
#include <QString>
@ -32,69 +33,49 @@ enum {
NUM_METATILE_TERRAIN_TYPES
};
class MetatileAttr
{
public:
MetatileAttr();
MetatileAttr(uint32_t mask, int shift);
public:
uint32_t mask;
int shift;
// Given the raw value for all attributes of a metatile
// Returns the extracted value for this attribute
uint32_t fromRaw(uint32_t raw) const { return (raw & this->mask) >> this->shift; }
// Given a value for this attribute
// Returns the raw value to OR together with the other attributes
uint32_t toRaw(uint32_t value) const { return (value << this->shift) & this->mask; }
// Given an arbitrary value to set for an attribute
// Returns a bounded value for that attribute
uint32_t getClamped(int value) const { return static_cast<uint32_t>(value) & (this->mask >> this->shift); }
};
class Metatile
{
public:
Metatile();
Metatile() = default;
Metatile(const Metatile &other) = default;
Metatile &operator=(const Metatile &other) = default;
Metatile(const int numTiles);
enum Attr {
Behavior,
TerrainType,
EncounterType,
LayerType,
Unused, // Preserve bits not used by the other attributes
};
public:
QList<Tile> tiles;
uint32_t behavior;
uint32_t terrainType;
uint32_t encounterType;
uint32_t layerType;
uint32_t unusedAttributes;
uint32_t getAttributes();
uint32_t getAttributes() const;
uint32_t getAttribute(Metatile::Attr attr) const { return this->attributes.value(attr, 0); }
void setAttributes(uint32_t data);
void setAttributes(uint32_t data, BaseGameVersion version);
void setAttribute(Metatile::Attr attr, uint32_t value);
void setBehavior(int value) { this->behavior = behaviorAttr.getClamped(value); }
void setTerrainType(int value) { this->terrainType = terrainTypeAttr.getClamped(value); }
void setEncounterType(int value) { this->encounterType = encounterTypeAttr.getClamped(value); }
void setLayerType(int value) { this->layerType = layerTypeAttr.getClamped(value); }
static uint32_t getBehaviorMask() { return behaviorAttr.mask; }
static uint32_t getTerrainTypeMask() { return terrainTypeAttr.mask; }
static uint32_t getEncounterTypeMask() { return encounterTypeAttr.mask; }
static uint32_t getLayerTypeMask() { return layerTypeAttr.mask; }
static uint32_t getBehaviorMask(BaseGameVersion version);
static uint32_t getTerrainTypeMask(BaseGameVersion version);
static uint32_t getEncounterTypeMask(BaseGameVersion version);
static uint32_t getLayerTypeMask(BaseGameVersion version);
// For convenience
uint32_t behavior() const { return this->getAttribute(Attr::Behavior); }
uint32_t terrainType() const { return this->getAttribute(Attr::TerrainType); }
uint32_t encounterType() const { return this->getAttribute(Attr::EncounterType); }
uint32_t layerType() const { return this->getAttribute(Attr::LayerType); }
void setBehavior(int value) { this->setAttribute(Attr::Behavior, static_cast<uint32_t>(value)); }
void setTerrainType(int value) { this->setAttribute(Attr::TerrainType, static_cast<uint32_t>(value)); }
void setEncounterType(int value) { this->setAttribute(Attr::EncounterType, static_cast<uint32_t>(value)); }
void setLayerType(int value) { this->setAttribute(Attr::LayerType, static_cast<uint32_t>(value)); }
static int getIndexInTileset(int);
static QPoint coordFromPixmapCoord(const QPointF &pixelCoord);
static uint32_t getDefaultAttributesMask(BaseGameVersion version, Metatile::Attr attr);
static uint32_t getMaxAttributesMask();
static int getDefaultAttributesSize(BaseGameVersion version);
static void setCustomLayout(Project*);
static void setLayout(Project*);
static QString getMetatileIdString(uint16_t metatileId) {
return "0x" + QString("%1").arg(metatileId, 3, 16, QChar('0')).toUpper();
return "0x" + QString("%1").arg(metatileId, 4, 16, QChar('0')).toUpper();
};
static QString getMetatileIdStringList(const QList<uint16_t> metatileIds) {
QStringList metatiles;
@ -103,37 +84,18 @@ public:
return metatiles.join(",");
};
inline bool operator==(const Metatile &other) {
return this->tiles == other.tiles && this->attributes == other.attributes;
}
inline bool operator!=(const Metatile &other) {
return !(operator==(other));
}
private:
// Stores how each attribute should be laid out for all metatiles, according to the user's config
static MetatileAttr behaviorAttr;
static MetatileAttr terrainTypeAttr;
static MetatileAttr encounterTypeAttr;
static MetatileAttr layerTypeAttr;
QMap<Metatile::Attr, uint32_t> attributes;
static uint32_t unusedAttrMask;
// Stores how each attribute should be laid out for all metatiles, according to the vanilla games
// Used to set default config values and import maps with AdvanceMap
static const QHash<QString, MetatileAttr> defaultLayoutFRLG;
static const QHash<QString, MetatileAttr> defaultLayoutRSE;
static const QHash<BaseGameVersion, const QHash<QString, MetatileAttr>*> defaultLayouts;
static void setCustomAttributeLayout(MetatileAttr *, uint32_t, uint32_t);
static bool isMaskTooSmall(MetatileAttr *, int);
static bool doMasksOverlap(QList<uint32_t>);
};
inline bool operator==(const Metatile &a, const Metatile &b) {
return a.behavior == b.behavior &&
a.layerType == b.layerType &&
a.encounterType == b.encounterType &&
a.terrainType == b.terrainType &&
a.unusedAttributes == b.unusedAttributes &&
a.tiles == b.tiles;
}
inline bool operator!=(const Metatile &a, const Metatile &b) {
return !(operator==(a, b));
}
#endif // METATILE_H

View File

@ -48,15 +48,15 @@ public:
void invalidateTextFile(const QString &path);
static int textFileLineCount(const QString &path);
QList<QStringList> parseAsm(const QString &filename);
int evaluateDefine(const QString&, const QMap<QString, int>&);
QStringList readCArray(const QString &filename, const QString &label);
QMap<QString, QStringList> readCArrayMulti(const QString &filename);
QMap<QString, QString> readNamedIndexCArray(const QString &text, const QString &label);
QString readCIncbin(const QString &text, const QString &label);
QMap<QString, QString> readCIncbinMulti(const QString &filepath);
QStringList readCIncbinArray(const QString &filename, const QString &label);
QMap<QString, int> readCDefines(const QString &filename, const QStringList &prefixes, QMap<QString, int> = { });
QStringList readCDefinesSorted(const QString&, const QStringList&, const QMap<QString, int>& = { });
QMap<QString, int> readCDefinesByPrefix(const QString &filename, QStringList prefixes);
QMap<QString, int> readCDefinesByName(const QString &filename, QStringList names);
QStringList readCDefineNames(const QString&, const QStringList&);
QMap<QString, QHash<QString, QString>> readCStructs(const QString &, const QString & = "", const QHash<int, QString> = { });
QList<QStringList> getLabelMacros(const QList<QStringList>&, const QString&);
QStringList getLabelValues(const QList<QStringList>&, const QString&);
@ -89,13 +89,16 @@ private:
QString file;
QString curDefine;
QHash<QString, QStringList> errorMap;
QList<Token> tokenizeExpression(QString expression, const QMap<QString, int> &knownIdentifiers);
int evaluateDefine(const QString&, const QString &, QMap<QString, int>*, QMap<QString, QString>*);
QList<Token> tokenizeExpression(QString, QMap<QString, int>*, QMap<QString, QString>*);
QList<Token> generatePostfix(const QList<Token> &tokens);
int evaluatePostfix(const QList<Token> &postfix);
void recordError(const QString &message);
void recordErrors(const QStringList &errors);
void logRecordedErrors();
QString createErrorMessage(const QString &message, const QString &expression);
QString readCDefinesFile(const QString &filename);
QMap<QString, int> readCDefines(const QString &filename, const QStringList &searchText, bool fullMatch);
static const QRegularExpression re_incScriptLabel;
static const QRegularExpression re_globalIncScriptLabel;

View File

@ -21,7 +21,10 @@ class Project;
struct LayoutSquare
{
LayoutSquare() : map_section("MAPSEC_NONE"), x(-1), y(-1), has_map(false) {}
LayoutSquare() : x(-1), y(-1), has_map(false) {
const QString prefix = projectConfig.getIdentifier(ProjectIdentifier::define_map_section_prefix);
map_section = prefix + projectConfig.getIdentifier(ProjectIdentifier::define_map_section_empty);
}
QString map_section;
int x;
int y;
@ -144,6 +147,10 @@ public:
void emitDisplay();
const QString section_prefix;
const QString default_map_section;
const QString count_map_section;
signals:
void mapNeedsDisplaying();

View File

@ -103,7 +103,8 @@ public:
QList<DraggablePixmapItem *> getObjects();
void updateCursorRectPos(int x, int y);
void setCursorRectVisible(bool visible);
void updateWarpEventWarning(Event *event);
void updateWarpEventWarnings();
bool eventLimitReached(Map *, Event::Type);
QGraphicsScene *scene = nullptr;
@ -138,6 +139,7 @@ public:
int scaleIndex = 2;
qreal collisionOpacity = 0.5;
static QList<QList<const QImage*>> collisionIcons;
void objectsView_onMousePress(QMouseEvent *event);
@ -151,6 +153,7 @@ public:
void scaleMapView(int);
static void openInTextEditor(const QString &path, int lineNum = 0);
bool eventLimitReached(Event::Type type);
void setCollisionGraphics();
public slots:
void openMapScripts() const;
@ -161,6 +164,10 @@ public slots:
void selectedEventIndexChanged(int index, Event::Group eventGroup);
private:
const QImage defaultCollisionImgSheet = QImage(":/images/collisions.png");
const QImage collisionPlaceholder = QImage(":/images/collisions_unknown.png");
QPixmap collisionSheetPixmap;
void setConnectionItemsVisible(bool);
void setBorderItemsVisible(bool, qreal = 1);
void setConnectionEditControlValues(MapConnection*);
@ -179,6 +186,7 @@ private:
void updateEncounterFields(EncounterFields newFields);
QString getMovementPermissionText(uint16_t collision, uint16_t elevation);
QString getMetatileDisplayMessage(uint16_t metatileId);
void setCollisionTabSpinBoxes(uint16_t collision, uint16_t elevation);
static bool startDetachedProcess(const QString &command,
const QString &workingDirectory = QString(),
qint64 *pid = nullptr);

View File

@ -159,6 +159,7 @@ public:
public slots:
void on_mainTabBar_tabBarClicked(int index);
void on_mapViewTab_tabBarClicked(int index);
void onWarpBehaviorWarningClicked();
private slots:
void on_action_Open_Project_triggered();
@ -276,11 +277,13 @@ private slots:
void on_pushButton_DeleteCustomHeaderField_clicked();
void on_tableWidget_CustomHeaderFields_cellChanged(int row, int column);
void on_horizontalSlider_MetatileZoom_valueChanged(int value);
void on_horizontalSlider_CollisionZoom_valueChanged(int value);
void on_pushButton_NewWildMonGroup_clicked();
void on_pushButton_DeleteWildMonGroup_clicked();
void on_pushButton_ConfigureEncountersJSON_clicked();
void on_pushButton_CreatePrefab_clicked();
void on_spinBox_SelectedElevation_valueChanged(int elevation);
void on_spinBox_SelectedCollision_valueChanged(int collision);
void on_actionRegion_Map_Editor_triggered();
void on_actionPreferences_triggered();
void togglePreferenceSpecificUi();
@ -348,7 +351,7 @@ private:
void openSubWindow(QWidget * window);
QString getExistingDirectory(QString);
bool openProject(QString dir);
QString getDefaultMap();
bool setInitialMap();
void setRecentMap(QString map_name);
QStandardItem* createMapItem(QString mapName, int groupNum, int inGroupNum);
@ -370,7 +373,7 @@ private:
void initMapSortOrder();
void initShortcuts();
void initExtraShortcuts();
void setProjectSpecificUIVisibility();
void setProjectSpecificUI();
void setWildEncountersUIEnabled(bool enabled);
void loadUserSettings();
void applyMapListFilter(QString filterText);
@ -387,7 +390,7 @@ private:
void initShortcutsEditor();
void initCustomScriptsEditor();
void connectSubEditorsToShortcutsEditor();
void openProjectSettingsEditor(int tab);
bool isProjectOpen();
void showExportMapImageWindow(ImageExporterMode mode);
void redrawMetatileSelection();

View File

@ -26,8 +26,7 @@ struct EventGraphics
bool inanimate;
};
// The constant and displayed name of the special map value used by warps with multiple potential destinations
static QString DYNAMIC_MAP_CONSTANT = "MAP_DYNAMIC";
// The displayed name of the special map value used by warps with multiple potential destinations
static QString DYNAMIC_MAP_NAME = "Dynamic";
class Project : public QObject
@ -76,18 +75,17 @@ public:
QStringList bgEventFacingDirections;
QStringList trainerTypes;
QStringList globalScriptLabels;
QMap<QString, QMap<QString, int>> metatileLabelsMap;
QMap<QString, int> unusedMetatileLabels;
QMap<QString, int> metatileBehaviorMap;
QMap<int, QString> metatileBehaviorMapInverse;
QMap<QString, QMap<QString, uint16_t>> metatileLabelsMap;
QMap<QString, uint16_t> unusedMetatileLabels;
QMap<QString, uint32_t> metatileBehaviorMap;
QMap<uint32_t, QString> metatileBehaviorMapInverse;
QMap<QString, QString> facingDirections;
ParseUtil parser;
QFileSystemWatcher fileWatcher;
QMap<QString, qint64> modifiedFileTimestamps;
bool usingAsmTilesets;
QString importExportPath;
const QPixmap entitiesPixmap = QPixmap(":/images/Entities_16x16.png");
QSet<QString> disabledSettingsNames;
void set_root(QString);
@ -173,12 +171,9 @@ public:
void saveTilesetPalettes(Tileset*);
QString defaultSong;
QStringList getVisibilities();
void appendTilesetLabel(QString label, QString isSecondaryStr);
bool readTilesetLabels();
bool readTilesetProperties();
bool readTilesetMetatileLabels();
bool readMaxMapDataSize();
bool readRegionMapSections();
bool readItemNames();
bool readFlagNames();
@ -200,6 +195,8 @@ public:
bool readObjEventGfxConstants();
bool readSongNames();
bool readEventGraphics();
bool readFieldmapProperties();
bool readFieldmapMasks();
QMap<QString, QMap<QString, QString>> readObjEventGfxInfo();
void setEventPixmap(Event *event, bool forceLoad = false);
@ -216,12 +213,13 @@ public:
QString getDefaultPrimaryTilesetLabel();
QString getDefaultSecondaryTilesetLabel();
QString getDynamicMapDefineName();
void updateTilesetMetatileLabels(Tileset *tileset);
QString buildMetatileLabelsText(const QMap<QString, int> defines);
QString buildMetatileLabelsText(const QMap<QString, uint16_t> defines);
QString findMetatileLabelsTileset(QString label);
void setImportExportPath(QString filename);
void applyParsedLimits();
static int getNumTilesPrimary();
static int getNumTilesTotal();
@ -248,13 +246,13 @@ private:
void saveHealLocationsData(Map *map);
void saveHealLocationsConstants();
QString getHealLocationsTableName();
void ignoreWatchedFileTemporarily(QString filepath);
static int num_tiles_primary;
static int num_tiles_total;
static int num_metatiles_primary;
static int num_metatiles_total;
static int num_pals_primary;
static int num_pals_total;
static int max_map_data_size;

View File

@ -1,8 +1,9 @@
#ifndef COLLISIONPIXMAPITEM_H
#define COLLISIONPIXMAPITEM_H
#include <QSpinBox>
#include "metatileselector.h"
#include "movementpermissionsselector.h"
#include "mappixmapitem.h"
#include "map.h"
#include "settings.h"
@ -10,13 +11,15 @@
class CollisionPixmapItem : public MapPixmapItem {
Q_OBJECT
public:
CollisionPixmapItem(Map *map, MovementPermissionsSelector *movementPermissionsSelector, MetatileSelector *metatileSelector, Settings *settings, qreal *opacity)
CollisionPixmapItem(Map *map, QSpinBox * selectedCollision, QSpinBox * selectedElevation, MetatileSelector *metatileSelector, Settings *settings, qreal *opacity)
: MapPixmapItem(map, metatileSelector, settings){
this->movementPermissionsSelector = movementPermissionsSelector;
this->selectedCollision = selectedCollision;
this->selectedElevation = selectedElevation;
this->opacity = opacity;
map->setCollisionItem(this);
}
MovementPermissionsSelector *movementPermissionsSelector;
QSpinBox * selectedCollision;
QSpinBox * selectedElevation;
qreal *opacity;
void updateMovementPermissionSelection(QGraphicsSceneMouseEvent *event);
virtual void paint(QGraphicsSceneMouseEvent*);
@ -28,6 +31,7 @@ public:
private:
unsigned actionId_ = 0;
QPoint previousPos;
void updateSelection(QPoint pos);
signals:
void mouseEvent(QGraphicsSceneMouseEvent *, CollisionPixmapItem *);

View File

@ -6,6 +6,7 @@
#include "noscrollspinbox.h"
#include "noscrollcombobox.h"
#include "mainwindow.h"
#include "events.h"
@ -22,7 +23,7 @@ public:
virtual void setup();
void initCustomAttributesTable();
virtual void connectSignals();
virtual void connectSignals(MainWindow *);
virtual void initialize();
virtual void populate(Project *project);
@ -69,7 +70,7 @@ public:
virtual void setup() override;
virtual void initialize() override;
virtual void connectSignals() override;
virtual void connectSignals(MainWindow *) override;
virtual void populate(Project *project) override;
public:
@ -101,7 +102,7 @@ public:
virtual void setup() override;
virtual void initialize() override;
virtual void connectSignals() override;
virtual void connectSignals(MainWindow *) override;
virtual void populate(Project *project) override;
public:
@ -124,12 +125,13 @@ public:
virtual void setup() override;
virtual void initialize() override;
virtual void connectSignals() override;
virtual void connectSignals(MainWindow *) override;
virtual void populate(Project *project) override;
public:
NoScrollComboBox *combo_dest_map;
NoScrollComboBox *combo_dest_warp;
QPushButton *warning;
private:
WarpEvent *warp;
@ -146,7 +148,7 @@ public:
virtual void setup() override;
virtual void initialize() override;
virtual void connectSignals() override;
virtual void connectSignals(MainWindow *) override;
virtual void populate(Project *project) override;
public:
@ -171,7 +173,7 @@ public:
virtual void setup() override;
virtual void initialize() override;
virtual void connectSignals() override;
virtual void connectSignals(MainWindow *) override;
virtual void populate(Project *project) override;
public:
@ -192,7 +194,7 @@ public:
virtual void setup() override;
virtual void initialize() override;
virtual void connectSignals() override;
virtual void connectSignals(MainWindow *) override;
virtual void populate(Project *project) override;
public:
@ -216,7 +218,7 @@ public:
virtual void setup() override;
virtual void initialize() override;
virtual void connectSignals() override;
virtual void connectSignals(MainWindow *) override;
virtual void populate(Project *project) override;
public:
@ -242,7 +244,7 @@ public:
virtual void setup() override;
virtual void initialize() override;
virtual void connectSignals() override;
virtual void connectSignals(MainWindow *) override;
virtual void populate(Project *project) override;
public:
@ -263,7 +265,7 @@ public:
virtual void setup() override;
virtual void initialize() override;
virtual void connectSignals() override;
virtual void connectSignals(MainWindow *) override;
virtual void populate(Project *project) override;
public:

View File

@ -19,6 +19,7 @@ public:
void setTilesets(Tileset*, Tileset*);
void setMetatile(Metatile*);
void clearLastModifiedCoords();
bool showGrid;
private:
Metatile* metatile;
Tileset *primaryTileset;

View File

@ -6,13 +6,19 @@
class MovementPermissionsSelector: public SelectablePixmapItem {
Q_OBJECT
public:
MovementPermissionsSelector(): SelectablePixmapItem(32, 32, 1, 1) {
MovementPermissionsSelector(QPixmap basePixmap)
: SelectablePixmapItem(MovementPermissionsSelector::CellWidth, MovementPermissionsSelector::CellHeight, 1, 1) {
this->basePixmap = basePixmap;
setAcceptHoverEvents(true);
}
void draw();
uint16_t getSelectedCollision();
uint16_t getSelectedElevation();
void select(uint16_t collision, uint16_t elevation);
void setBasePixmap(QPixmap pixmap);
static const int CellWidth;
static const int CellHeight;
protected:
void hoverMoveEvent(QGraphicsSceneHoverEvent*);
@ -20,6 +26,7 @@ protected:
private:
void setSelectedMovementPermissions(QPointF);
QPixmap basePixmap;
signals:
void hoveredMovementPermissionChanged(uint16_t, uint16_t);

View File

@ -12,6 +12,7 @@ public:
void wheelEvent(QWheelEvent *event);
void setTextItem(const QString &text);
void setNumberItem(int value);
void setHexItem(uint32_t value);
private:
void setItem(int index, const QString &text);

View File

@ -3,6 +3,7 @@
#include <QMainWindow>
#include "project.h"
#include "ui_projectsettingseditor.h"
class NoScrollComboBox;
class QAbstractButton;
@ -20,6 +21,10 @@ public:
explicit ProjectSettingsEditor(QWidget *parent = nullptr, Project *project = nullptr);
~ProjectSettingsEditor();
static const int eventsTab;
void setTab(int index);
void closeQuietly();
signals:
void reloadProject();
@ -31,6 +36,8 @@ private:
bool projectNeedsReload = false;
bool refreshing = false;
const QString baseDir;
QHash<QString, QString> editedPokemonIconPaths;
QString prevIconSpecies;
void initUi();
void connectSignals();
@ -46,15 +53,29 @@ private:
void setBorderMetatileIds(bool customSize, QList<uint16_t> metatileIds);
QList<uint16_t> getBorderMetatileIds(bool customSize);
void createConfigTextTable(const QList<QPair<QString, QString>> configPairs, bool filesTab);
void createProjectPathsTable();
void createProjectIdentifiersTable();
QString chooseProjectFile(const QString &defaultFilepath);
void choosePrefabsFile();
void chooseImageFile(QLineEdit * filepathEdit);
void chooseFile(QLineEdit * filepathEdit, const QString &description, const QString &extensions);
QString stripProjectDir(QString s);
void disableParsedSetting(QWidget * widget, const QString &name, const QString &filepath);
void updateMaskOverlapWarning(QLabel * warning, QList<UIntSpinBox*> masks);
QStringList getWarpBehaviorsList();
void setWarpBehaviorsList(QStringList list);
private slots:
void dialogButtonClicked(QAbstractButton *button);
void choosePrefabsFileClicked(bool);
void importDefaultPrefabsClicked(bool);
void updateAttributeLimits(const QString &attrSize);
void updatePokemonIconPath(const QString &species);
void markEdited();
void on_mainTabs_tabBarClicked(int index);
void updateBlockMaskOverlapWarning();
void updateAttributeMaskOverlapWarning();
void updateWarpBehaviorsList(bool adding);
};
#endif // PROJECTSETTINGSEDITOR_H

View File

@ -35,6 +35,9 @@ protected:
void mouseMoveEvent(QGraphicsSceneMouseEvent*);
void mouseReleaseEvent(QGraphicsSceneMouseEvent*);
virtual void drawSelection();
signals:
void selectionChanged(int, int, int, int);
};
#endif // SELECTABLEPIXMAPITEM_H

View File

@ -84,6 +84,8 @@ private slots:
void on_actionShow_Unused_toggled(bool checked);
void on_actionShow_Counts_toggled(bool checked);
void on_actionShow_UnusedTiles_toggled(bool checked);
void on_actionMetatile_Grid_triggered(bool checked);
void on_actionLayer_Grid_triggered(bool checked);
void on_actionUndo_triggered();

View File

@ -22,6 +22,7 @@ public:
QVector<uint16_t> usedMetatiles;
bool selectorShowUnused = false;
bool selectorShowCounts = false;
bool showGrid;
protected:
void mousePressEvent(QGraphicsSceneMouseEvent*);
@ -35,10 +36,12 @@ private:
Tileset *secondaryTileset = nullptr;
uint16_t selectedMetatile;
int numMetatilesWide;
int numMetatilesHigh;
uint16_t getMetatileId(int x, int y);
QPoint getMetatileIdCoords(uint16_t);
bool shouldAcceptEvent(QGraphicsSceneMouseEvent*);
int numRows(int numMetatiles);
int numRows();
void drawFilters();
void drawUnused();
void drawCounts();

View File

@ -64,4 +64,15 @@ signals:
void textChanged(const QString &text);
};
class UIntHexSpinBox : public UIntSpinBox
{
Q_OBJECT
public:
UIntHexSpinBox(QWidget *parent = nullptr) : UIntSpinBox(parent) {
this->setPrefix("0x");
this->setDisplayIntegerBase(16);
this->setHasPadding(true);
}
};
#endif // UINTSPINBOX_H

View File

@ -16,6 +16,7 @@ QMAKE_CXXFLAGS += -std=c++17 -Wall
QMAKE_TARGET_BUNDLE_PREFIX = com.pret
SOURCES += src/core/block.cpp \
src/core/bitpacker.cpp \
src/core/blockdata.cpp \
src/core/events.cpp \
src/core/heallocation.cpp \
@ -104,6 +105,7 @@ SOURCES += src/core/block.cpp \
src/ui/uintspinbox.cpp
HEADERS += include/core/block.h \
include/core/bitpacker.h \
include/core/blockdata.h \
include/core/events.h \
include/core/heallocation.h \

View File

@ -61,7 +61,9 @@
<file>icons/ui/midnight_branch_more.png</file>
<file>images/blank_tileset.png</file>
<file>images/collisions.png</file>
<file>images/collisions_unknown.png</file>
<file>images/Entities_16x16.png</file>
<file>images/pokemon_icon_placeholder.png</file>
<file>icons/clipboard.ico</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -16,7 +16,117 @@
#include <QAction>
#include <QAbstractButton>
const QMap<ProjectFilePath, std::pair<QString, QString>> ProjectConfig::defaultPaths = {
const QSet<uint32_t> defaultWarpBehaviors_RSE = {
0x0E, // MB_MOSSDEEP_GYM_WARP
0x0F, // MB_MT_PYRE_HOLE
0x1B, // MB_STAIRS_OUTSIDE_ABANDONED_SHIP
0x1C, // MB_SHOAL_CAVE_ENTRANCE
0x29, // MB_LAVARIDGE_GYM_B1F_WARP
0x60, // MB_NON_ANIMATED_DOOR
0x61, // MB_LADDER
0x62, // MB_EAST_ARROW_WARP
0x63, // MB_WEST_ARROW_WARP
0x64, // MB_NORTH_ARROW_WARP
0x65, // MB_SOUTH_ARROW_WARP
0x67, // MB_AQUA_HIDEOUT_WARP
0x68, // MB_LAVARIDGE_GYM_1F_WARP
0x69, // MB_ANIMATED_DOOR
0x6A, // MB_UP_ESCALATOR
0x6B, // MB_DOWN_ESCALATOR
0x6C, // MB_WATER_DOOR
0x6D, // MB_WATER_SOUTH_ARROW_WARP
0x6E, // MB_DEEP_SOUTH_WARP
0x70, // MB_UNION_ROOM_WARP
0x8D, // MB_PETALBURG_GYM_DOOR
0x91, // MB_SECRET_BASE_SPOT_RED_CAVE_OPEN
0x93, // MB_SECRET_BASE_SPOT_BROWN_CAVE_OPEN
0x95, // MB_SECRET_BASE_SPOT_YELLOW_CAVE_OPEN
0x97, // MB_SECRET_BASE_SPOT_TREE_LEFT_OPEN
0x99, // MB_SECRET_BASE_SPOT_SHRUB_OPEN
0x9B, // MB_SECRET_BASE_SPOT_BLUE_CAVE_OPEN
0x9D, // MB_SECRET_BASE_SPOT_TREE_RIGHT_OPEN
};
const QSet<uint32_t> defaultWarpBehaviors_FRLG = {
0x60, // MB_CAVE_DOOR
0x61, // MB_LADDER
0x62, // MB_EAST_ARROW_WARP
0x63, // MB_WEST_ARROW_WARP
0x64, // MB_NORTH_ARROW_WARP
0x65, // MB_SOUTH_ARROW_WARP
0x66, // MB_FALL_WARP
0x67, // MB_REGULAR_WARP
0x68, // MB_LAVARIDGE_1F_WARP
0x69, // MB_WARP_DOOR
0x6A, // MB_UP_ESCALATOR
0x6B, // MB_DOWN_ESCALATOR
0x6C, // MB_UP_RIGHT_STAIR_WARP
0x6D, // MB_UP_LEFT_STAIR_WARP
0x6E, // MB_DOWN_RIGHT_STAIR_WARP
0x6F, // MB_DOWN_LEFT_STAIR_WARP
0x71, // MB_UNION_ROOM_WARP
};
// TODO: symbol_wild_encounters should ultimately be removed from the table below. We can determine this name when we read the project.
const QMap<ProjectIdentifier, QPair<QString, QString>> ProjectConfig::defaultIdentifiers = {
// Symbols
{ProjectIdentifier::symbol_facing_directions, {"symbol_facing_directions", "gInitialMovementTypeFacingDirections"}},
{ProjectIdentifier::symbol_obj_event_gfx_pointers, {"symbol_obj_event_gfx_pointers", "gObjectEventGraphicsInfoPointers"}},
{ProjectIdentifier::symbol_pokemon_icon_table, {"symbol_pokemon_icon_table", "gMonIconTable"}},
{ProjectIdentifier::symbol_wild_encounters, {"symbol_wild_encounters", "gWildMonHeaders"}},
{ProjectIdentifier::symbol_heal_locations, {"symbol_heal_locations", "sHealLocations"}},
{ProjectIdentifier::symbol_spawn_points, {"symbol_spawn_points", "sSpawnPoints"}},
{ProjectIdentifier::symbol_spawn_maps, {"symbol_spawn_maps", "sWhiteoutRespawnHealCenterMapIdxs"}},
{ProjectIdentifier::symbol_spawn_npcs, {"symbol_spawn_npcs", "sWhiteoutRespawnHealerNpcIds"}},
{ProjectIdentifier::symbol_attribute_table, {"symbol_attribute_table", "sMetatileAttrMasks"}},
{ProjectIdentifier::symbol_tilesets_prefix, {"symbol_tilesets_prefix", "gTileset_"}},
// Defines
{ProjectIdentifier::define_obj_event_count, {"define_obj_event_count", "OBJECT_EVENT_TEMPLATES_COUNT"}},
{ProjectIdentifier::define_min_level, {"define_min_level", "MIN_LEVEL"}},
{ProjectIdentifier::define_max_level, {"define_max_level", "MAX_LEVEL"}},
{ProjectIdentifier::define_tiles_primary, {"define_tiles_primary", "NUM_TILES_IN_PRIMARY"}},
{ProjectIdentifier::define_tiles_total, {"define_tiles_total", "NUM_TILES_TOTAL"}},
{ProjectIdentifier::define_metatiles_primary, {"define_metatiles_primary", "NUM_METATILES_IN_PRIMARY"}},
{ProjectIdentifier::define_pals_primary, {"define_pals_primary", "NUM_PALS_IN_PRIMARY"}},
{ProjectIdentifier::define_pals_total, {"define_pals_total", "NUM_PALS_TOTAL"}},
{ProjectIdentifier::define_map_size, {"define_map_size", "MAX_MAP_DATA_SIZE"}},
{ProjectIdentifier::define_mask_metatile, {"define_mask_metatile", "MAPGRID_METATILE_ID_MASK"}},
{ProjectIdentifier::define_mask_collision, {"define_mask_collision", "MAPGRID_COLLISION_MASK"}},
{ProjectIdentifier::define_mask_elevation, {"define_mask_elevation", "MAPGRID_ELEVATION_MASK"}},
{ProjectIdentifier::define_mask_behavior, {"define_mask_behavior", "METATILE_ATTR_BEHAVIOR_MASK"}},
{ProjectIdentifier::define_mask_layer, {"define_mask_layer", "METATILE_ATTR_LAYER_MASK"}},
{ProjectIdentifier::define_attribute_behavior, {"define_attribute_behavior", "METATILE_ATTRIBUTE_BEHAVIOR"}},
{ProjectIdentifier::define_attribute_layer, {"define_attribute_layer", "METATILE_ATTRIBUTE_LAYER_TYPE"}},
{ProjectIdentifier::define_attribute_terrain, {"define_attribute_terrain", "METATILE_ATTRIBUTE_TERRAIN"}},
{ProjectIdentifier::define_attribute_encounter, {"define_attribute_encounter", "METATILE_ATTRIBUTE_ENCOUNTER_TYPE"}},
{ProjectIdentifier::define_metatile_label_prefix, {"define_metatile_label_prefix", "METATILE_"}},
{ProjectIdentifier::define_heal_locations_prefix, {"define_heal_locations_prefix", "HEAL_LOCATION_"}},
{ProjectIdentifier::define_spawn_prefix, {"define_spawn_prefix", "SPAWN_"}},
{ProjectIdentifier::define_map_prefix, {"define_map_prefix", "MAP_"}},
{ProjectIdentifier::define_map_dynamic, {"define_map_dynamic", "DYNAMIC"}},
{ProjectIdentifier::define_map_empty, {"define_map_empty", "UNDEFINED"}},
{ProjectIdentifier::define_map_section_prefix, {"define_map_section_prefix", "MAPSEC_"}},
{ProjectIdentifier::define_map_section_empty, {"define_map_section_empty", "NONE"}},
{ProjectIdentifier::define_map_section_count, {"define_map_section_count", "COUNT"}},
// Regex
{ProjectIdentifier::regex_behaviors, {"regex_behaviors", "\\bMB_"}},
{ProjectIdentifier::regex_obj_event_gfx, {"regex_obj_event_gfx", "\\bOBJ_EVENT_GFX_"}},
{ProjectIdentifier::regex_items, {"regex_items", "\\bITEM_(?!(B_)?USE_)"}}, // Exclude ITEM_USE_ and ITEM_B_USE_ constants
{ProjectIdentifier::regex_flags, {"regex_flags", "\\bFLAG_"}},
{ProjectIdentifier::regex_vars, {"regex_vars", "\\bVAR_"}},
{ProjectIdentifier::regex_movement_types, {"regex_movement_types", "\\bMOVEMENT_TYPE_"}},
{ProjectIdentifier::regex_map_types, {"regex_map_types", "\\bMAP_TYPE_"}},
{ProjectIdentifier::regex_battle_scenes, {"regex_battle_scenes", "\\bMAP_BATTLE_SCENE_"}},
{ProjectIdentifier::regex_weather, {"regex_weather", "\\bWEATHER_"}},
{ProjectIdentifier::regex_coord_event_weather, {"regex_coord_event_weather", "\\bCOORD_EVENT_WEATHER_"}},
{ProjectIdentifier::regex_secret_bases, {"regex_secret_bases", "\\bSECRET_BASE_[A-Za-z0-9_]*_[0-9]+"}},
{ProjectIdentifier::regex_sign_facing_directions, {"regex_sign_facing_directions", "\\bBG_EVENT_PLAYER_FACING_"}},
{ProjectIdentifier::regex_trainer_types, {"regex_trainer_types", "\\bTRAINER_TYPE_"}},
{ProjectIdentifier::regex_music, {"regex_music", "\\b(SE|MUS)_"}},
{ProjectIdentifier::regex_species, {"regex_species", "\\bSPECIES_"}},
};
const QMap<ProjectFilePath, QPair<QString, QString>> ProjectConfig::defaultPaths = {
{ProjectFilePath::data_map_folders, { "data_map_folders", "data/maps/"}},
{ProjectFilePath::data_scripts_folders, { "data_scripts_folders", "data/scripts/"}},
{ProjectFilePath::data_layouts_folders, { "data_layouts_folders", "data/layouts/"}},
@ -42,7 +152,6 @@ const QMap<ProjectFilePath, std::pair<QString, QString>> ProjectConfig::defaultP
{ProjectFilePath::constants_global, { "constants_global", "include/constants/global.h"}},
{ProjectFilePath::constants_map_groups, { "constants_map_groups", "include/constants/map_groups.h"}},
{ProjectFilePath::constants_items, { "constants_items", "include/constants/items.h"}},
{ProjectFilePath::constants_opponents, { "constants_opponents", "include/constants/opponents.h"}},
{ProjectFilePath::constants_flags, { "constants_flags", "include/constants/flags.h"}},
{ProjectFilePath::constants_vars, { "constants_vars", "include/constants/vars.h"}},
{ProjectFilePath::constants_weather, { "constants_weather", "include/constants/weather.h"}},
@ -58,11 +167,22 @@ const QMap<ProjectFilePath, std::pair<QString, QString>> ProjectConfig::defaultP
{ProjectFilePath::constants_region_map_sections, { "constants_region_map_sections", "include/constants/region_map_sections.h"}},
{ProjectFilePath::constants_metatile_labels, { "constants_metatile_labels", "include/constants/metatile_labels.h"}},
{ProjectFilePath::constants_metatile_behaviors, { "constants_metatile_behaviors", "include/constants/metatile_behaviors.h"}},
{ProjectFilePath::constants_species, { "constants_species", "include/constants/species.h"}},
{ProjectFilePath::constants_fieldmap, { "constants_fieldmap", "include/fieldmap.h"}},
{ProjectFilePath::global_fieldmap, { "global_fieldmap", "include/global.fieldmap.h"}},
{ProjectFilePath::fieldmap, { "fieldmap", "src/fieldmap.c"}},
{ProjectFilePath::pokemon_icon_table, { "pokemon_icon_table", "src/pokemon_icon.c"}},
{ProjectFilePath::initial_facing_table, { "initial_facing_table", "src/event_object_movement.c"}},
{ProjectFilePath::pokemon_gfx, { "pokemon_gfx", "graphics/pokemon/"}},
};
ProjectIdentifier reverseDefaultIdentifier(QString str) {
for (auto i = ProjectConfig::defaultIdentifiers.cbegin(), end = ProjectConfig::defaultIdentifiers.cend(); i != end; i++) {
if (i.value().first == str) return i.key();
}
return static_cast<ProjectIdentifier>(-1);
}
ProjectFilePath reverseDefaultPaths(QString str) {
for (auto it = ProjectConfig::defaultPaths.constKeyValueBegin(); it != ProjectConfig::defaultPaths.constKeyValueEnd(); ++it) {
if ((*it).second.first == str) return (*it).first;
@ -244,6 +364,8 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) {
this->customScriptsEditorState = bytesFromString(value);
} else if (key == "metatiles_zoom") {
this->metatilesZoom = getConfigInteger(key, value, 10, 100, 30);
} else if (key == "collision_zoom") {
this->collisionZoom = getConfigInteger(key, value, 10, 100, 30);
} else if (key == "show_player_view") {
this->showPlayerView = getConfigBool(key, value);
} else if (key == "show_cursor_tile") {
@ -252,6 +374,10 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) {
this->showBorder = getConfigBool(key, value);
} else if (key == "show_grid") {
this->showGrid = getConfigBool(key, value);
} else if (key == "show_tileset_editor_metatile_grid") {
this->showTilesetEditorMetatileGrid = getConfigBool(key, value);
} else if (key == "show_tileset_editor_layer_grid") {
this->showTilesetEditorLayerGrid = getConfigBool(key, value);
} else if (key == "monitor_files") {
this->monitorFiles = getConfigBool(key, value);
} else if (key == "tileset_checkerboard_fill") {
@ -267,6 +393,10 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) {
if (this->paletteEditorBitDepth != 15 && this->paletteEditorBitDepth != 24){
this->paletteEditorBitDepth = 24;
}
} else if (key == "project_settings_tab") {
this->projectSettingsTab = getConfigInteger(key, value, 0);
} else if (key == "warp_behavior_warning_disabled") {
this->warpBehaviorWarningDisabled = getConfigBool(key, value);
} else {
logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key));
}
@ -292,18 +422,23 @@ QMap<QString, QString> PorymapConfig::getKeyValueMap() {
map.insert("project_settings_editor_state", stringFromByteArray(this->projectSettingsEditorState));
map.insert("custom_scripts_editor_geometry", stringFromByteArray(this->customScriptsEditorGeometry));
map.insert("custom_scripts_editor_state", stringFromByteArray(this->customScriptsEditorState));
map.insert("collision_opacity", QString("%1").arg(this->collisionOpacity));
map.insert("metatiles_zoom", QString("%1").arg(this->metatilesZoom));
map.insert("collision_opacity", QString::number(this->collisionOpacity));
map.insert("collision_zoom", QString::number(this->collisionZoom));
map.insert("metatiles_zoom", QString::number(this->metatilesZoom));
map.insert("show_player_view", this->showPlayerView ? "1" : "0");
map.insert("show_cursor_tile", this->showCursorTile ? "1" : "0");
map.insert("show_border", this->showBorder ? "1" : "0");
map.insert("show_grid", this->showGrid ? "1" : "0");
map.insert("show_tileset_editor_metatile_grid", this->showTilesetEditorMetatileGrid ? "1" : "0");
map.insert("show_tileset_editor_layer_grid", this->showTilesetEditorLayerGrid ? "1" : "0");
map.insert("monitor_files", this->monitorFiles ? "1" : "0");
map.insert("tileset_checkerboard_fill", this->tilesetCheckerboardFill ? "1" : "0");
map.insert("theme", this->theme);
map.insert("text_editor_open_directory", this->textEditorOpenFolder);
map.insert("text_editor_goto_line", this->textEditorGotoLine);
map.insert("palette_editor_bit_depth", QString("%1").arg(this->paletteEditorBitDepth));
map.insert("palette_editor_bit_depth", QString::number(this->paletteEditorBitDepth));
map.insert("project_settings_tab", QString::number(this->projectSettingsTab));
map.insert("warp_behavior_warning_disabled", QString::number(this->warpBehaviorWarningDisabled));
return map;
}
@ -399,6 +534,11 @@ void PorymapConfig::setCollisionOpacity(int opacity) {
// don't auto-save here because this can be called very frequently.
}
void PorymapConfig::setCollisionZoom(int zoom) {
this->collisionZoom = zoom;
// don't auto-save here because this can be called very frequently.
}
void PorymapConfig::setMetatilesZoom(int zoom) {
this->metatilesZoom = zoom;
// don't auto-save here because this can be called very frequently.
@ -424,6 +564,16 @@ void PorymapConfig::setShowGrid(bool enabled) {
this->save();
}
void PorymapConfig::setShowTilesetEditorMetatileGrid(bool enabled) {
this->showTilesetEditorMetatileGrid = enabled;
this->save();
}
void PorymapConfig::setShowTilesetEditorLayerGrid(bool enabled) {
this->showTilesetEditorLayerGrid = enabled;
this->save();
}
void PorymapConfig::setTheme(QString theme) {
this->theme = theme;
}
@ -443,6 +593,11 @@ void PorymapConfig::setPaletteEditorBitDepth(int bitDepth) {
this->save();
}
void PorymapConfig::setProjectSettingsTab(int tab) {
this->projectSettingsTab = tab;
this->save();
}
QString PorymapConfig::getRecentProject() {
return this->recentProject;
}
@ -519,6 +674,10 @@ int PorymapConfig::getCollisionOpacity() {
return this->collisionOpacity;
}
int PorymapConfig::getCollisionZoom() {
return this->collisionZoom;
}
int PorymapConfig::getMetatilesZoom() {
return this->metatilesZoom;
}
@ -539,6 +698,14 @@ bool PorymapConfig::getShowGrid() {
return this->showGrid;
}
bool PorymapConfig::getShowTilesetEditorMetatileGrid() {
return this->showTilesetEditorMetatileGrid;
}
bool PorymapConfig::getShowTilesetEditorLayerGrid() {
return this->showTilesetEditorLayerGrid;
}
bool PorymapConfig::getMonitorFiles() {
return this->monitorFiles;
}
@ -563,6 +730,19 @@ int PorymapConfig::getPaletteEditorBitDepth() {
return this->paletteEditorBitDepth;
}
int PorymapConfig::getProjectSettingsTab() {
return this->projectSettingsTab;
}
void PorymapConfig::setWarpBehaviorWarningDisabled(bool disabled) {
this->warpBehaviorWarningDisabled = disabled;
this->save();
}
bool PorymapConfig::getWarpBehaviorWarningDisabled() {
return this->warpBehaviorWarningDisabled;
}
const QStringList ProjectConfig::versionStrings = {
"pokeruby",
"pokefirered",
@ -627,17 +807,17 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) {
this->createMapTextFile = getConfigBool(key, value);
} else if (key == "enable_triple_layer_metatiles") {
this->enableTripleLayerMetatiles = getConfigBool(key, value);
} else if (key == "new_map_metatile") {
this->newMapMetatileId = getConfigUint32(key, value, 0, 1023, 0);
} else if (key == "new_map_elevation") {
this->newMapElevation = getConfigInteger(key, value, 0, 15, 3);
} else if (key == "default_metatile") {
this->defaultMetatileId = getConfigUint32(key, value, 0, Block::maxValue);
} else if (key == "default_elevation") {
this->defaultElevation = getConfigUint32(key, value, 0, Block::maxValue);
} else if (key == "default_collision") {
this->defaultCollision = getConfigUint32(key, value, 0, Block::maxValue);
} else if (key == "new_map_border_metatiles") {
this->newMapBorderMetatileIds.clear();
QList<QString> metatileIds = value.split(",");
for (int i = 0; i < metatileIds.size(); i++) {
// TODO: The max of 1023 here should eventually reflect Project::num_metatiles_total-1,
// but the config is parsed well before that constant is.
int metatileId = getConfigUint32(key, metatileIds.at(i), 0, 1023, 0);
int metatileId = getConfigUint32(key, metatileIds.at(i), 0, Block::maxValue);
this->newMapBorderMetatileIds.append(metatileId);
}
} else if (key == "default_primary_tileset") {
@ -652,13 +832,19 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) {
}
this->metatileAttributesSize = size;
} else if (key == "metatile_behavior_mask") {
this->metatileBehaviorMask = getConfigUint32(key, value, 0, 0xFFFFFFFF, 0);
this->metatileBehaviorMask = getConfigUint32(key, value);
} else if (key == "metatile_terrain_type_mask") {
this->metatileTerrainTypeMask = getConfigUint32(key, value, 0, 0xFFFFFFFF, 0);
this->metatileTerrainTypeMask = getConfigUint32(key, value);
} else if (key == "metatile_encounter_type_mask") {
this->metatileEncounterTypeMask = getConfigUint32(key, value, 0, 0xFFFFFFFF, 0);
this->metatileEncounterTypeMask = getConfigUint32(key, value);
} else if (key == "metatile_layer_type_mask") {
this->metatileLayerTypeMask = getConfigUint32(key, value, 0, 0xFFFFFFFF, 0);
this->metatileLayerTypeMask = getConfigUint32(key, value);
} else if (key == "block_metatile_id_mask") {
this->blockMetatileIdMask = getConfigUint32(key, value, 0, Block::maxValue);
} else if (key == "block_collision_mask") {
this->blockCollisionMask = getConfigUint32(key, value, 0, Block::maxValue);
} else if (key == "block_elevation_mask") {
this->blockElevationMask = getConfigUint32(key, value, 0, Block::maxValue);
} else if (key == "enable_map_allow_flags") {
this->enableMapAllowFlags = getConfigBool(key, value);
#ifdef CONFIG_BACKWARDS_COMPATABILITY
@ -676,6 +862,13 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) {
} else {
logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key));
}
} else if (key.startsWith("ident/")) {
auto identifierId = reverseDefaultIdentifier(key.mid(6));
if (identifierId != static_cast<ProjectIdentifier>(-1)) {
this->setIdentifier(identifierId, value);
} else {
logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key));
}
} else if (key == "prefabs_filepath") {
this->prefabFilepath = value;
} else if (key == "prefabs_import_prompted") {
@ -684,6 +877,30 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) {
this->tilesetsHaveCallback = getConfigBool(key, value);
} else if (key == "tilesets_have_is_compressed") {
this->tilesetsHaveIsCompressed = getConfigBool(key, value);
} else if (key == "event_icon_path_object") {
this->eventIconPaths[Event::Group::Object] = value;
} else if (key == "event_icon_path_warp") {
this->eventIconPaths[Event::Group::Warp] = value;
} else if (key == "event_icon_path_coord") {
this->eventIconPaths[Event::Group::Coord] = value;
} else if (key == "event_icon_path_bg") {
this->eventIconPaths[Event::Group::Bg] = value;
} else if (key == "event_icon_path_heal") {
this->eventIconPaths[Event::Group::Heal] = value;
} else if (key.startsWith("pokemon_icon_path/")) {
this->pokemonIconPaths.insert(key.mid(18).toUpper(), value);
} else if (key == "collision_sheet_path") {
this->collisionSheetPath = value;
} else if (key == "collision_sheet_width") {
this->collisionSheetWidth = getConfigUint32(key, value, 1, Block::maxValue);
} else if (key == "collision_sheet_height") {
this->collisionSheetHeight = getConfigUint32(key, value, 1, Block::maxValue);
} else if (key == "warp_behaviors") {
this->warpBehaviors.clear();
value.remove(" ");
QStringList behaviorList = value.split(",", Qt::SkipEmptyParts);
for (auto s : behaviorList)
this->warpBehaviors.insert(getConfigUint32(key, s));
} else {
logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key));
}
@ -712,11 +929,12 @@ void ProjectConfig::setUnreadKeys() {
if (!readKeys.contains("new_map_border_metatiles")) this->newMapBorderMetatileIds = isPokefirered ? DEFAULT_BORDER_FRLG : DEFAULT_BORDER_RSE;
if (!readKeys.contains("default_secondary_tileset")) this->defaultSecondaryTileset = isPokefirered ? "gTileset_PalletTown" : "gTileset_Petalburg";
if (!readKeys.contains("metatile_attributes_size")) this->metatileAttributesSize = Metatile::getDefaultAttributesSize(this->baseGameVersion);
if (!readKeys.contains("metatile_behavior_mask")) this->metatileBehaviorMask = Metatile::getBehaviorMask(this->baseGameVersion);
if (!readKeys.contains("metatile_terrain_type_mask")) this->metatileTerrainTypeMask = Metatile::getTerrainTypeMask(this->baseGameVersion);
if (!readKeys.contains("metatile_encounter_type_mask")) this->metatileEncounterTypeMask = Metatile::getEncounterTypeMask(this->baseGameVersion);
if (!readKeys.contains("metatile_layer_type_mask")) this->metatileLayerTypeMask = Metatile::getLayerTypeMask(this->baseGameVersion);
if (!readKeys.contains("metatile_behavior_mask")) this->metatileBehaviorMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::Behavior);
if (!readKeys.contains("metatile_terrain_type_mask")) this->metatileTerrainTypeMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::TerrainType);
if (!readKeys.contains("metatile_encounter_type_mask")) this->metatileEncounterTypeMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::EncounterType);
if (!readKeys.contains("metatile_layer_type_mask")) this->metatileLayerTypeMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::LayerType);
if (!readKeys.contains("enable_map_allow_flags")) this->enableMapAllowFlags = (this->baseGameVersion != BaseGameVersion::pokeruby);
if (!readKeys.contains("warp_behaviors")) this->warpBehaviors = isPokefirered ? defaultWarpBehaviors_FRLG : defaultWarpBehaviors_RSE;
}
QMap<QString, QString> ProjectConfig::getKeyValueMap() {
@ -733,8 +951,9 @@ QMap<QString, QString> ProjectConfig::getKeyValueMap() {
map.insert("enable_floor_number", QString::number(this->enableFloorNumber));
map.insert("create_map_text_file", QString::number(this->createMapTextFile));
map.insert("enable_triple_layer_metatiles", QString::number(this->enableTripleLayerMetatiles));
map.insert("new_map_metatile", Metatile::getMetatileIdString(this->newMapMetatileId));
map.insert("new_map_elevation", QString::number(this->newMapElevation));
map.insert("default_metatile", Metatile::getMetatileIdString(this->defaultMetatileId));
map.insert("default_elevation", QString::number(this->defaultElevation));
map.insert("default_collision", QString::number(this->defaultCollision));
map.insert("new_map_border_metatiles", Metatile::getMetatileIdStringList(this->newMapBorderMetatileIds));
map.insert("default_primary_tileset", this->defaultPrimaryTileset);
map.insert("default_secondary_tileset", this->defaultSecondaryTileset);
@ -750,7 +969,30 @@ QMap<QString, QString> ProjectConfig::getKeyValueMap() {
map.insert("metatile_terrain_type_mask", "0x" + QString::number(this->metatileTerrainTypeMask, 16).toUpper());
map.insert("metatile_encounter_type_mask", "0x" + QString::number(this->metatileEncounterTypeMask, 16).toUpper());
map.insert("metatile_layer_type_mask", "0x" + QString::number(this->metatileLayerTypeMask, 16).toUpper());
map.insert("block_metatile_id_mask", "0x" + QString::number(this->blockMetatileIdMask, 16).toUpper());
map.insert("block_collision_mask", "0x" + QString::number(this->blockCollisionMask, 16).toUpper());
map.insert("block_elevation_mask", "0x" + QString::number(this->blockElevationMask, 16).toUpper());
map.insert("enable_map_allow_flags", QString::number(this->enableMapAllowFlags));
map.insert("event_icon_path_object", this->eventIconPaths[Event::Group::Object]);
map.insert("event_icon_path_warp", this->eventIconPaths[Event::Group::Warp]);
map.insert("event_icon_path_coord", this->eventIconPaths[Event::Group::Coord]);
map.insert("event_icon_path_bg", this->eventIconPaths[Event::Group::Bg]);
map.insert("event_icon_path_heal", this->eventIconPaths[Event::Group::Heal]);
for (auto i = this->pokemonIconPaths.cbegin(), end = this->pokemonIconPaths.cend(); i != end; i++){
const QString path = i.value();
if (!path.isEmpty()) map.insert("pokemon_icon_path/" + i.key(), path);
}
for (auto i = this->identifiers.cbegin(), end = this->identifiers.cend(); i != end; i++) {
map.insert("ident/"+defaultIdentifiers.value(i.key()).first, i.value());
}
map.insert("collision_sheet_path", this->collisionSheetPath);
map.insert("collision_sheet_width", QString::number(this->collisionSheetWidth));
map.insert("collision_sheet_height", QString::number(this->collisionSheetHeight));
QStringList warpBehaviorStrs;
for (auto value : this->warpBehaviors)
warpBehaviorStrs.append("0x" + QString("%1").arg(value, 2, 16, QChar('0')).toUpper());
map.insert("warp_behaviors", warpBehaviorStrs.join(","));
return map;
}
@ -791,7 +1033,7 @@ QString ProjectConfig::getProjectDir() {
return this->projectDir;
}
void ProjectConfig::setFilePath(ProjectFilePath pathId, QString path) {
void ProjectConfig::setFilePath(ProjectFilePath pathId, const QString &path) {
if (!defaultPaths.contains(pathId)) return;
if (path.isEmpty()) {
this->filePaths.remove(pathId);
@ -800,18 +1042,20 @@ void ProjectConfig::setFilePath(ProjectFilePath pathId, QString path) {
}
}
void ProjectConfig::setFilePath(QString defaultPath, QString newPath) {
this->setFilePath(reverseDefaultPaths(defaultPath), newPath);
void ProjectConfig::setFilePath(const QString &pathId, const QString &path) {
this->setFilePath(reverseDefaultPaths(pathId), path);
}
QString ProjectConfig::getFilePath(ProjectFilePath pathId, bool customOnly) {
const QString customPath = this->filePaths.value(pathId);
QString ProjectConfig::getCustomFilePath(ProjectFilePath pathId) {
return this->filePaths.value(pathId);
}
// When reading custom filepaths for the settings editor we don't care
// about the default path or whether the custom path exists.
if (customOnly)
return customPath;
QString ProjectConfig::getCustomFilePath(const QString &pathId) {
return this->getCustomFilePath(reverseDefaultPaths(pathId));
}
QString ProjectConfig::getFilePath(ProjectFilePath pathId) {
const QString customPath = this->getCustomFilePath(pathId);
if (!customPath.isEmpty()) {
// A custom filepath has been specified. If the file/folder exists, use that.
const QString absCustomPath = this->projectDir + QDir::separator() + customPath;
@ -825,8 +1069,33 @@ QString ProjectConfig::getFilePath(ProjectFilePath pathId, bool customOnly) {
}
QString ProjectConfig::getFilePath(QString defaultPath, bool customOnly) {
return this->getFilePath(reverseDefaultPaths(defaultPath), customOnly);
void ProjectConfig::setIdentifier(ProjectIdentifier id, const QString &text) {
if (!defaultIdentifiers.contains(id)) return;
QString copy(text);
if (copy.isEmpty()) {
this->identifiers.remove(id);
} else {
this->identifiers[id] = copy;
}
}
void ProjectConfig::setIdentifier(const QString &id, const QString &text) {
this->setIdentifier(reverseDefaultIdentifier(id), text);
}
QString ProjectConfig::getCustomIdentifier(ProjectIdentifier id) {
return this->identifiers.value(id);
}
QString ProjectConfig::getCustomIdentifier(const QString &id) {
return this->getCustomIdentifier(reverseDefaultIdentifier(id));
}
QString ProjectConfig::getIdentifier(ProjectIdentifier id) {
const QString customText = this->getCustomIdentifier(id);
if (!customText.isEmpty())
return customText;
return defaultIdentifiers.contains(id) ? defaultIdentifiers[id].second : QString();
}
void ProjectConfig::setBaseGameVersion(BaseGameVersion baseGameVersion) {
@ -956,22 +1225,31 @@ int ProjectConfig::getNumTilesInMetatile() {
return this->enableTripleLayerMetatiles ? 12 : 8;
}
void ProjectConfig::setNewMapMetatileId(uint16_t metatileId) {
this->newMapMetatileId = metatileId;
void ProjectConfig::setDefaultMetatileId(uint16_t metatileId) {
this->defaultMetatileId = metatileId;
this->save();
}
uint16_t ProjectConfig::getNewMapMetatileId() {
return this->newMapMetatileId;
uint16_t ProjectConfig::getDefaultMetatileId() {
return this->defaultMetatileId;
}
void ProjectConfig::setNewMapElevation(int elevation) {
this->newMapElevation = elevation;
void ProjectConfig::setDefaultElevation(uint16_t elevation) {
this->defaultElevation = elevation;
this->save();
}
int ProjectConfig::getNewMapElevation() {
return this->newMapElevation;
uint16_t ProjectConfig::getDefaultElevation() {
return this->defaultElevation;
}
void ProjectConfig::setDefaultCollision(uint16_t collision) {
this->defaultCollision = collision;
this->save();
}
uint16_t ProjectConfig::getDefaultCollision() {
return this->defaultCollision;
}
void ProjectConfig::setNewMapBorderMetatileIds(QList<uint16_t> metatileIds) {
@ -1082,6 +1360,33 @@ void ProjectConfig::setMetatileLayerTypeMask(uint32_t mask) {
this->save();
}
uint16_t ProjectConfig::getBlockMetatileIdMask() {
return this->blockMetatileIdMask;
}
uint16_t ProjectConfig::getBlockCollisionMask() {
return this->blockCollisionMask;
}
uint16_t ProjectConfig::getBlockElevationMask() {
return this->blockElevationMask;
}
void ProjectConfig::setBlockMetatileIdMask(uint16_t mask) {
this->blockMetatileIdMask = mask;
this->save();
}
void ProjectConfig::setBlockCollisionMask(uint16_t mask) {
this->blockCollisionMask = mask;
this->save();
}
void ProjectConfig::setBlockElevationMask(uint16_t mask) {
this->blockElevationMask = mask;
this->save();
}
bool ProjectConfig::getMapAllowFlagsEnabled() {
return this->enableMapAllowFlags;
}
@ -1091,6 +1396,64 @@ void ProjectConfig::setMapAllowFlagsEnabled(bool enabled) {
this->save();
}
void ProjectConfig::setEventIconPath(Event::Group group, const QString &path) {
this->eventIconPaths[group] = path;
this->save();
}
QString ProjectConfig::getEventIconPath(Event::Group group) {
return this->eventIconPaths.value(group);
}
void ProjectConfig::setPokemonIconPath(const QString &species, const QString &path) {
this->pokemonIconPaths[species] = path;
this->save();
}
QString ProjectConfig::getPokemonIconPath(const QString &species) {
return this->pokemonIconPaths.value(species);
}
QHash<QString, QString> ProjectConfig::getPokemonIconPaths() {
return this->pokemonIconPaths;
}
void ProjectConfig::setCollisionSheetPath(const QString &path) {
this->collisionSheetPath = path;
this->save();
}
QString ProjectConfig::getCollisionSheetPath() {
return this->collisionSheetPath;
}
void ProjectConfig::setCollisionSheetWidth(int width) {
this->collisionSheetWidth = width;
this->save();
}
int ProjectConfig::getCollisionSheetWidth() {
return this->collisionSheetWidth;
}
void ProjectConfig::setCollisionSheetHeight(int height) {
this->collisionSheetHeight = height;
this->save();
}
int ProjectConfig::getCollisionSheetHeight() {
return this->collisionSheetHeight;
}
void ProjectConfig::setWarpBehaviors(const QSet<uint32_t> &behaviors) {
this->warpBehaviors = behaviors;
this->save();
}
QSet<uint32_t> ProjectConfig::getWarpBehaviors() {
return this->warpBehaviors;
}
UserConfig userConfig;

51
src/core/bitpacker.cpp Normal file
View File

@ -0,0 +1,51 @@
#include "bitpacker.h"
#include <limits>
// Sometimes we can't explicitly define bitfields because we need to allow users to
// change the size and arrangement of its members. In those cases we use this
// convenience class to handle packing and unpacking each member.
BitPacker::BitPacker(uint32_t mask) {
this->setMask(mask);
}
void BitPacker::setMask(uint32_t mask) {
m_mask = mask;
// Precalculate the number and positions of the mask bits
m_setBits.clear();
for (int i = 0; mask != 0; mask >>= 1, i++)
if (mask & 1) m_setBits.append(1 << i);
// For masks with only contiguous bits m_maxValue is equivalent to (m_mask >> n), where n is the number of trailing 0's in m_mask.
m_maxValue = (m_setBits.length() >= 32) ? UINT_MAX : ((1 << m_setBits.length()) - 1);
}
// Given an arbitrary value to set for this bitfield member, returns a (potentially truncated) value that can later be packed losslessly.
uint32_t BitPacker::clamp(uint32_t value) const {
return (m_maxValue == UINT_MAX) ? value : (value % (m_maxValue + 1));
}
// Given packed data, returns the extracted value for the bitfield member.
// For masks with only contiguous bits this is equivalent to ((data & m_mask) >> n), where n is the number of trailing 0's in m_mask.
uint32_t BitPacker::unpack(uint32_t data) const {
uint32_t value = 0;
data &= m_mask;
for (int i = 0; i < m_setBits.length(); i++) {
if (data & m_setBits.at(i))
value |= (1 << i);
}
return value;
}
// Given a value for the bitfield member, returns the value to OR together with the other members.
// For masks with only contiguous bits this is equivalent to ((value << n) & m_mask), where n is the number of trailing 0's in m_mask.
uint32_t BitPacker::pack(uint32_t value) const {
uint32_t data = 0;
for (int i = 0; i < m_setBits.length(); i++) {
if (value == 0) return data;
if (value & 1) data |= m_setBits.at(i);
value >>= 1;
}
return data;
}

View File

@ -1,43 +1,87 @@
#include "block.h"
#include "bitpacker.h"
#include "config.h"
Block::Block() : metatileId(0), collision(0), elevation(0) { }
// Upper limit for metatile ID, collision, and elevation masks. Used externally.
const uint16_t Block::maxValue = 0xFFFF;
Block::Block(uint16_t metatileId, uint16_t collision, uint16_t elevation) :
metatileId(metatileId),
collision(collision),
elevation(elevation)
static BitPacker bitsMetatileId = BitPacker(0x3FF);
static BitPacker bitsCollision = BitPacker(0xC00);
static BitPacker bitsElevation = BitPacker(0xF000);
Block::Block() :
m_metatileId(0),
m_collision(0),
m_elevation(0)
{ }
Block::Block(uint16_t word) :
metatileId(word & 0x3ff),
collision((word >> 10) & 0x3),
elevation((word >> 12) & 0xf)
Block::Block(uint16_t metatileId, uint16_t collision, uint16_t elevation) :
m_metatileId(metatileId),
m_collision(collision),
m_elevation(elevation)
{ }
Block::Block(uint16_t data) :
m_metatileId(bitsMetatileId.unpack(data)),
m_collision(bitsCollision.unpack(data)),
m_elevation(bitsElevation.unpack(data))
{ }
Block::Block(const Block &other) :
metatileId(other.metatileId),
collision(other.collision),
elevation(other.elevation)
m_metatileId(other.m_metatileId),
m_collision(other.m_collision),
m_elevation(other.m_elevation)
{ }
Block &Block::operator=(const Block &other) {
metatileId = other.metatileId;
collision = other.collision;
elevation = other.elevation;
m_metatileId = other.m_metatileId;
m_collision = other.m_collision;
m_elevation = other.m_elevation;
return *this;
}
uint16_t Block::rawValue() const {
return static_cast<uint16_t>(
(metatileId & 0x3ff) +
((collision & 0x3) << 10) +
((elevation & 0xf) << 12));
return bitsMetatileId.pack(m_metatileId)
| bitsCollision.pack(m_collision)
| bitsElevation.pack(m_elevation);
}
void Block::setLayout() {
bitsMetatileId.setMask(projectConfig.getBlockMetatileIdMask());
bitsCollision.setMask(projectConfig.getBlockCollisionMask());
bitsElevation.setMask(projectConfig.getBlockElevationMask());
}
bool Block::operator ==(Block other) const {
return (metatileId == other.metatileId) && (collision == other.collision) && (elevation == other.elevation);
return (m_metatileId == other.m_metatileId)
&& (m_collision == other.m_collision)
&& (m_elevation == other.m_elevation);
}
bool Block::operator !=(Block other) const {
return !(operator ==(other));
}
void Block::setMetatileId(uint16_t metatileId) {
m_metatileId = bitsMetatileId.clamp(metatileId);
}
void Block::setCollision(uint16_t collision) {
m_collision = bitsCollision.clamp(collision);
}
void Block::setElevation(uint16_t elevation) {
m_elevation = bitsElevation.clamp(elevation);
}
uint16_t Block::getMaxMetatileId() {
return bitsMetatileId.maxValue();
}
uint16_t Block::getMaxCollision() {
return bitsCollision.maxValue();
}
uint16_t Block::getMaxElevation() {
return bitsElevation.maxValue();
}

View File

@ -4,7 +4,7 @@
#include "project.h"
#include "config.h"
QMap<Event::Group, const QPixmap*> Event::icons;
Event::~Event() {
if (this->eventFrame)
@ -35,7 +35,7 @@ int Event::getEventIndex() {
void Event::setDefaultValues(Project *) {
this->setX(0);
this->setY(0);
this->setElevation(3);
this->setElevation(projectConfig.getDefaultElevation());
}
void Event::readCustomValues(QJsonObject values) {
@ -126,6 +126,45 @@ Event::Type Event::eventTypeFromString(QString type) {
}
}
void Event::loadPixmap(Project *) {
const QPixmap * pixmap = Event::icons.value(this->getEventGroup());
this->pixmap = pixmap ? *pixmap : QPixmap();
}
void Event::setIcons() {
qDeleteAll(icons);
icons.clear();
const int w = 16;
const int h = 16;
static const QPixmap defaultIcons = QPixmap(":/images/Entities_16x16.png");
// Custom event icons may be provided by the user.
const int numIcons = qMin(defaultIcons.width() / w, static_cast<int>(Event::Group::None));
for (int i = 0; i < numIcons; i++) {
Event::Group group = static_cast<Event::Group>(i);
QString customIconPath = projectConfig.getEventIconPath(group);
if (customIconPath.isEmpty()) {
// No custom icon specified, use the default icon.
icons[group] = new QPixmap(defaultIcons.copy(i * w, 0, w, h));
continue;
}
// Try to load custom icon
QFileInfo info(customIconPath);
if (info.isRelative()) {
customIconPath = QDir::cleanPath(projectConfig.getProjectDir() + QDir::separator() + customIconPath);
}
const QPixmap customIcon = QPixmap(customIconPath);
if (customIcon.isNull()) {
// Custom icon failed to load, use the default icon.
icons[group] = new QPixmap(defaultIcons.copy(i * w, 0, w, h));
logWarn(QString("Failed to load custom event icon '%1', using default icon.").arg(customIconPath));
} else {
icons[group] = new QPixmap(customIcon.scaled(w, h));
}
}
}
Event *ObjectEvent::duplicate() {
@ -243,7 +282,7 @@ void ObjectEvent::loadPixmap(Project *project) {
if (!eventGfx || eventGfx->spritesheet.isNull()) {
// No sprite associated with this gfx constant.
// Use default sprite instead.
this->pixmap = project->entitiesPixmap.copy(0, 0, 16, 16);
Event::loadPixmap(project);
this->spriteWidth = 16;
this->spriteHeight = 16;
this->usingSprite = false;
@ -333,13 +372,14 @@ bool CloneObjectEvent::loadFromJson(QJsonObject json, Project *project) {
this->setTargetID(ParseUtil::jsonToInt(json["target_local_id"]));
// Ensure the target map constant is valid before adding it to the events.
const QString dynamicMapConstant = project->getDynamicMapDefineName();
QString mapConstant = ParseUtil::jsonToQString(json["target_map"]);
if (project->mapConstantsToMapNames.contains(mapConstant)) {
this->setTargetMap(project->mapConstantsToMapNames.value(mapConstant));
} else if (mapConstant == DYNAMIC_MAP_CONSTANT) {
} else if (mapConstant == dynamicMapConstant) {
this->setTargetMap(DYNAMIC_MAP_NAME);
} else {
logWarn(QString("Target Map constant '%1' is invalid. Using default '%2'.").arg(mapConstant).arg(DYNAMIC_MAP_CONSTANT));
logWarn(QString("Target Map constant '%1' is invalid. Using default '%2'.").arg(mapConstant).arg(dynamicMapConstant));
this->setTargetMap(DYNAMIC_MAP_NAME);
}
@ -389,7 +429,7 @@ void CloneObjectEvent::loadPixmap(Project *project) {
if (!eventGfx || eventGfx->spritesheet.isNull()) {
// No sprite associated with this gfx constant.
// Use default sprite instead.
this->pixmap = project->entitiesPixmap.copy(0, 0, 16, 16);
Event::loadPixmap(project);
this->spriteWidth = 16;
this->spriteHeight = 16;
this->usingSprite = false;
@ -444,13 +484,14 @@ bool WarpEvent::loadFromJson(QJsonObject json, Project *project) {
this->setDestinationWarpID(ParseUtil::jsonToQString(json["dest_warp_id"]));
// Ensure the warp destination map constant is valid before adding it to the warps.
const QString dynamicMapConstant = project->getDynamicMapDefineName();
QString mapConstant = ParseUtil::jsonToQString(json["dest_map"]);
if (project->mapConstantsToMapNames.contains(mapConstant)) {
this->setDestinationMap(project->mapConstantsToMapNames.value(mapConstant));
} else if (mapConstant == DYNAMIC_MAP_CONSTANT) {
} else if (mapConstant == dynamicMapConstant) {
this->setDestinationMap(DYNAMIC_MAP_NAME);
} else {
logWarn(QString("Destination Map constant '%1' is invalid. Using default '%2'.").arg(mapConstant).arg(DYNAMIC_MAP_CONSTANT));
logWarn(QString("Destination Map constant '%1' is invalid. Using default '%2'.").arg(mapConstant).arg(dynamicMapConstant));
this->setDestinationMap(DYNAMIC_MAP_NAME);
}
@ -478,14 +519,10 @@ QSet<QString> WarpEvent::getExpectedFields() {
return expectedFields;
}
void WarpEvent::loadPixmap(Project *project) {
this->pixmap = project->entitiesPixmap.copy(16, 0, 16, 16);
}
void CoordEvent::loadPixmap(Project *project) {
this->pixmap = project->entitiesPixmap.copy(32, 0, 16, 16);
void WarpEvent::setWarningEnabled(bool enabled) {
WarpFrame * frame = static_cast<WarpFrame*>(this->getEventFrame());
if (frame && frame->warning)
frame->warning->setVisible(enabled);
}
@ -632,12 +669,6 @@ QSet<QString> WeatherTriggerEvent::getExpectedFields() {
void BGEvent::loadPixmap(Project *project) {
this->pixmap = project->entitiesPixmap.copy(48, 0, 16, 16);
}
Event *SignEvent::duplicate() {
SignEvent *copy = new SignEvent();
@ -884,20 +915,17 @@ OrderedJson::object HealLocationEvent::buildEventJson(Project *) {
}
void HealLocationEvent::setDefaultValues(Project *) {
this->setElevation(3);
this->setElevation(projectConfig.getDefaultElevation());
if (!this->getMap())
return;
bool respawnEanbled = projectConfig.getHealLocationRespawnDataEnabled();
QString mapConstant = Map::mapConstantFromName(this->getMap()->name).remove(0,4);
QString prefix = respawnEanbled ? "SPAWN_" : "HEAL_LOCATION_";
bool respawnEnabled = projectConfig.getHealLocationRespawnDataEnabled();
const QString mapConstant = Map::mapConstantFromName(this->getMap()->name, false);
const QString prefix = projectConfig.getIdentifier(respawnEnabled ? ProjectIdentifier::define_spawn_prefix
: ProjectIdentifier::define_heal_locations_prefix);
this->setLocationName(mapConstant);
this->setIdName(prefix + mapConstant);
if (respawnEanbled) {
if (respawnEnabled) {
this->setRespawnMap(this->getMap()->name);
this->setRespawnNPC(1);
}
}
void HealLocationEvent::loadPixmap(Project *project) {
this->pixmap = project->entitiesPixmap.copy(64, 0, 16, 16);
}

View File

@ -26,7 +26,7 @@ HealLocation HealLocation::fromEvent(Event *fromEvent) {
healLocation.y = event->getY();
if (projectConfig.getHealLocationRespawnDataEnabled()) {
healLocation.respawnNPC = event->getRespawnNPC();
healLocation.respawnMap = Map::mapConstantFromName(event->getRespawnMap()).remove(0,4);
healLocation.respawnMap = Map::mapConstantFromName(event->getRespawnMap(), false);
}
return healLocation;
}

View File

@ -29,11 +29,12 @@ void Map::setName(QString mapName) {
constantName = mapConstantFromName(mapName);
}
QString Map::mapConstantFromName(QString mapName) {
QString Map::mapConstantFromName(QString mapName, bool includePrefix) {
// Transform map names of the form 'GraniteCave_B1F` into map constants like 'MAP_GRANITE_CAVE_B1F'.
static const QRegularExpression caseChange("([a-z])([A-Z])");
QString nameWithUnderscores = mapName.replace(caseChange, "\\1_\\2");
QString withMapAndUppercase = "MAP_" + nameWithUnderscores.toUpper();
const QString prefix = includePrefix ? projectConfig.getIdentifier(ProjectIdentifier::define_map_prefix) : "";
QString withMapAndUppercase = prefix + nameWithUnderscores.toUpper();
static const QRegularExpression underscores("_+");
QString constantName = withMapAndUppercase.replace(underscores, "_");
@ -160,7 +161,7 @@ QPixmap Map::render(bool ignoreCache, MapLayout * fromLayout, QRect bounds) {
QPoint metatile_origin = QPoint(map_x * 16, map_y * 16);
Block block = layout->blockdata.at(i);
QImage metatile_image = getMetatileImage(
block.metatileId,
block.metatileId(),
fromLayout ? fromLayout->tileset_primary : layout->tileset_primary,
fromLayout ? fromLayout->tileset_secondary : layout->tileset_secondary,
metatileLayerOrder,
@ -201,7 +202,7 @@ QPixmap Map::renderBorder(bool ignoreCache) {
changed_any = true;
Block block = layout->border.at(i);
uint16_t metatileId = block.metatileId;
uint16_t metatileId = block.metatileId();
QImage metatile_image = getMetatileImage(metatileId, layout->tileset_primary, layout->tileset_secondary, metatileLayerOrder, metatileLayerOpacity);
int map_y = width_ ? i / width_ : 0;
int map_x = width_ ? i % width_ : 0;
@ -364,14 +365,14 @@ void Map::setBlockdata(Blockdata blockdata, bool enableScriptCallback) {
uint16_t Map::getBorderMetatileId(int x, int y) {
int i = y * getBorderWidth() + x;
return layout->border[i].metatileId;
return layout->border[i].metatileId();
}
void Map::setBorderMetatileId(int x, int y, uint16_t metatileId, bool enableScriptCallback) {
int i = y * getBorderWidth() + x;
if (i < layout->border.size()) {
uint16_t prevMetatileId = layout->border[i].metatileId;
layout->border[i].metatileId = metatileId;
uint16_t prevMetatileId = layout->border[i].metatileId();
layout->border[i].setMetatileId(metatileId);
if (prevMetatileId != metatileId && enableScriptCallback) {
Scripting::cb_BorderMetatileChanged(x, y, prevMetatileId, metatileId);
}
@ -387,7 +388,7 @@ void Map::setBorderBlockData(Blockdata blockdata, bool enableScriptCallback) {
if (prevBlock != newBlock) {
layout->border.replace(i, newBlock);
if (enableScriptCallback)
Scripting::cb_BorderMetatileChanged(i % width, i / width, prevBlock.metatileId, newBlock.metatileId);
Scripting::cb_BorderMetatileChanged(i % width, i / width, prevBlock.metatileId(), newBlock.metatileId());
}
}
}
@ -404,25 +405,25 @@ void Map::_floodFillCollisionElevation(int x, int y, uint16_t collision, uint16_
continue;
}
uint old_coll = block.collision;
uint old_elev = block.elevation;
uint old_coll = block.collision();
uint old_elev = block.elevation();
if (old_coll == collision && old_elev == elevation) {
continue;
}
block.collision = collision;
block.elevation = elevation;
block.setCollision(collision);
block.setElevation(elevation);
setBlock(x, y, block, true);
if (getBlock(x + 1, y, &block) && block.collision == old_coll && block.elevation == old_elev) {
if (getBlock(x + 1, y, &block) && block.collision() == old_coll && block.elevation() == old_elev) {
todo.append(QPoint(x + 1, y));
}
if (getBlock(x - 1, y, &block) && block.collision == old_coll && block.elevation == old_elev) {
if (getBlock(x - 1, y, &block) && block.collision() == old_coll && block.elevation() == old_elev) {
todo.append(QPoint(x - 1, y));
}
if (getBlock(x, y + 1, &block) && block.collision == old_coll && block.elevation == old_elev) {
if (getBlock(x, y + 1, &block) && block.collision() == old_coll && block.elevation() == old_elev) {
todo.append(QPoint(x, y + 1));
}
if (getBlock(x, y - 1, &block) && block.collision == old_coll && block.elevation == old_elev) {
if (getBlock(x, y - 1, &block) && block.collision() == old_coll && block.elevation() == old_elev) {
todo.append(QPoint(x, y - 1));
}
}
@ -430,22 +431,22 @@ void Map::_floodFillCollisionElevation(int x, int y, uint16_t collision, uint16_
void Map::floodFillCollisionElevation(int x, int y, uint16_t collision, uint16_t elevation) {
Block block;
if (getBlock(x, y, &block) && (block.collision != collision || block.elevation != elevation)) {
if (getBlock(x, y, &block) && (block.collision() != collision || block.elevation() != elevation)) {
_floodFillCollisionElevation(x, y, collision, elevation);
}
}
void Map::magicFillCollisionElevation(int initialX, int initialY, uint16_t collision, uint16_t elevation) {
Block block;
if (getBlock(initialX, initialY, &block) && (block.collision != collision || block.elevation != elevation)) {
uint old_coll = block.collision;
uint old_elev = block.elevation;
if (getBlock(initialX, initialY, &block) && (block.collision() != collision || block.elevation() != elevation)) {
uint old_coll = block.collision();
uint old_elev = block.elevation();
for (int y = 0; y < getHeight(); y++) {
for (int x = 0; x < getWidth(); x++) {
if (getBlock(x, y, &block) && block.collision == old_coll && block.elevation == old_elev) {
block.collision = collision;
block.elevation = elevation;
if (getBlock(x, y, &block) && block.collision() == old_coll && block.elevation() == old_elev) {
block.setCollision(collision);
block.setElevation(elevation);
setBlock(x, y, block, true);
}
}

View File

@ -2,58 +2,24 @@
#include "tileset.h"
#include "project.h"
const QHash<QString, MetatileAttr> Metatile::defaultLayoutFRLG = {
{"behavior", MetatileAttr(0x000001FF, 0) },
{"terrainType", MetatileAttr(0x00003E00, 9) },
{"encounterType", MetatileAttr(0x07000000, 24) },
{"layerType", MetatileAttr(0x60000000, 29) },
// Stores how each attribute should be laid out for all metatiles, according to the vanilla games.
// Used to set default config values and import maps with AdvanceMap.
static const QMap<Metatile::Attr, BitPacker> attributePackersFRLG = {
{Metatile::Attr::Behavior, BitPacker(0x000001FF) },
{Metatile::Attr::TerrainType, BitPacker(0x00003E00) },
{Metatile::Attr::EncounterType, BitPacker(0x07000000) },
{Metatile::Attr::LayerType, BitPacker(0x60000000) },
//{Metatile::Attr::Unused, BitPacker(0x98FFC000) },
};
static const QMap<Metatile::Attr, BitPacker> attributePackersRSE = {
{Metatile::Attr::Behavior, BitPacker(0x00FF) },
//{Metatile::Attr::Unused, BitPacker(0x0F00) },
{Metatile::Attr::LayerType, BitPacker(0xF000) },
};
const QHash<QString, MetatileAttr> Metatile::defaultLayoutRSE = {
{"behavior", MetatileAttr(0x00FF, 0) },
{"terrainType", MetatileAttr() },
{"encounterType", MetatileAttr() },
{"layerType", MetatileAttr(0xF000, 12) },
};
static QMap<Metatile::Attr, BitPacker> attributePackers;
const QHash<BaseGameVersion, const QHash<QString, MetatileAttr>*> Metatile::defaultLayouts = {
{ BaseGameVersion::pokeruby, &defaultLayoutRSE },
{ BaseGameVersion::pokefirered, &defaultLayoutFRLG },
{ BaseGameVersion::pokeemerald, &defaultLayoutRSE },
};
MetatileAttr Metatile::behaviorAttr;
MetatileAttr Metatile::terrainTypeAttr;
MetatileAttr Metatile::encounterTypeAttr;
MetatileAttr Metatile::layerTypeAttr;
uint32_t Metatile::unusedAttrMask = 0;
MetatileAttr::MetatileAttr() :
mask(0),
shift(0)
{ }
MetatileAttr::MetatileAttr(uint32_t mask, int shift) :
mask(mask),
shift(shift)
{ }
Metatile::Metatile() :
behavior(0),
terrainType(0),
encounterType(0),
layerType(0),
unusedAttributes(0)
{ }
Metatile::Metatile(const int numTiles) :
behavior(0),
terrainType(0),
encounterType(0),
layerType(0),
unusedAttributes(0)
{
Metatile::Metatile(const int numTiles) {
Tile tile = Tile();
for (int i = 0; i < numTiles; i++) {
this->tiles.append(tile);
@ -74,120 +40,108 @@ QPoint Metatile::coordFromPixmapCoord(const QPointF &pixelCoord) {
return QPoint(x, y);
}
// Set the layout of a metatile attribute using the mask read from the config file
void Metatile::setCustomAttributeLayout(MetatileAttr * attr, uint32_t mask, uint32_t max) {
if (mask > max) {
uint32_t oldMask = mask;
mask &= max;
logWarn(QString("Metatile attribute mask '0x%1' has been truncated to '0x%2'")
.arg(QString::number(oldMask, 16).toUpper())
.arg(QString::number(mask, 16).toUpper()));
// Read and pack together this metatile's attributes.
uint32_t Metatile::getAttributes() const {
uint32_t data = 0;
for (auto i = this->attributes.cbegin(), end = this->attributes.cend(); i != end; i++){
const auto packer = attributePackers.value(i.key());
data |= packer.pack(i.value());
}
attr->mask = mask;
attr->shift = mask ? log2(mask & ~(mask - 1)) : 0; // Get position of the least significant set bit
}
// For checking whether a metatile attribute mask can contain all the available hard-coded options
bool Metatile::isMaskTooSmall(MetatileAttr * attr, int max) {
if (attr->mask == 0 || max <= 0) return false;
// Get position of the most significant set bit
uint32_t n = log2(max);
// Get a mask for all values 0 to max.
// This may fail for n > 30, but that's not possible here.
uint32_t rangeMask = (1 << (n + 1)) - 1;
return attr->getClamped(rangeMask) != rangeMask;
}
bool Metatile::doMasksOverlap(QList<uint32_t> masks) {
for (int i = 0; i < masks.length(); i++)
for (int j = i + 1; j < masks.length(); j++) {
if (masks.at(i) & masks.at(j))
return true;
}
return false;
}
void Metatile::setCustomLayout(Project * project) {
// Get the maximum size of any attribute mask
const QHash<int, uint32_t> maxMasks = {
{1, 0xFF},
{2, 0xFFFF},
{4, 0xFFFFFFFF},
};
const uint32_t maxMask = maxMasks.value(projectConfig.getMetatileAttributesSize(), 0);
// Set custom attribute masks from the config file
setCustomAttributeLayout(&Metatile::behaviorAttr, projectConfig.getMetatileBehaviorMask(), maxMask);
setCustomAttributeLayout(&Metatile::terrainTypeAttr, projectConfig.getMetatileTerrainTypeMask(), maxMask);
setCustomAttributeLayout(&Metatile::encounterTypeAttr, projectConfig.getMetatileEncounterTypeMask(), maxMask);
setCustomAttributeLayout(&Metatile::layerTypeAttr, projectConfig.getMetatileLayerTypeMask(), maxMask);
// Set mask for preserving any attribute bits not used by Porymap
Metatile::unusedAttrMask = ~(getBehaviorMask() | getTerrainTypeMask() | getEncounterTypeMask() | getLayerTypeMask());
Metatile::unusedAttrMask &= maxMask;
// Overlapping masks are technically ok, but probably not intended.
// Additionally, Porymap will not properly reflect that the values are linked.
if (doMasksOverlap({getBehaviorMask(), getTerrainTypeMask(), getEncounterTypeMask(), getLayerTypeMask()})) {
logWarn("Metatile attribute masks are overlapping. This may result in unexpected attribute values.");
}
// Warn the user if they have set a nonzero mask that is too small to contain its available options.
// They'll be allowed to select the options, but they'll be truncated to a different value when revisited.
if (!project->metatileBehaviorMapInverse.isEmpty()) {
int maxBehavior = project->metatileBehaviorMapInverse.lastKey();
if (isMaskTooSmall(&Metatile::behaviorAttr, maxBehavior))
logWarn(QString("Metatile Behavior mask is too small to contain all %1 available options.").arg(maxBehavior));
}
if (isMaskTooSmall(&Metatile::terrainTypeAttr, NUM_METATILE_TERRAIN_TYPES - 1))
logWarn(QString("Metatile Terrain Type mask is too small to contain all %1 available options.").arg(NUM_METATILE_TERRAIN_TYPES));
if (isMaskTooSmall(&Metatile::encounterTypeAttr, NUM_METATILE_ENCOUNTER_TYPES - 1))
logWarn(QString("Metatile Encounter Type mask is too small to contain all %1 available options.").arg(NUM_METATILE_ENCOUNTER_TYPES));
if (isMaskTooSmall(&Metatile::layerTypeAttr, NUM_METATILE_LAYER_TYPES - 1))
logWarn(QString("Metatile Layer Type mask is too small to contain all %1 available options.").arg(NUM_METATILE_LAYER_TYPES));
}
uint32_t Metatile::getAttributes() {
uint32_t attributes = this->unusedAttributes & Metatile::unusedAttrMask;
attributes |= Metatile::behaviorAttr.toRaw(this->behavior);
attributes |= Metatile::terrainTypeAttr.toRaw(this->terrainType);
attributes |= Metatile::encounterTypeAttr.toRaw(this->encounterType);
attributes |= Metatile::layerTypeAttr.toRaw(this->layerType);
return attributes;
return data;
}
// Unpack and insert metatile attributes from the given data.
void Metatile::setAttributes(uint32_t data) {
this->behavior = Metatile::behaviorAttr.fromRaw(data);
this->terrainType = Metatile::terrainTypeAttr.fromRaw(data);
this->encounterType = Metatile::encounterTypeAttr.fromRaw(data);
this->layerType = Metatile::layerTypeAttr.fromRaw(data);
this->unusedAttributes = data & Metatile::unusedAttrMask;
for (auto i = attributePackers.cbegin(), end = attributePackers.cend(); i != end; i++){
const auto packer = i.value();
this->setAttribute(i.key(), packer.unpack(data));
}
}
// Read attributes using a vanilla layout, then set them using the user's layout. For AdvanceMap import
// Unpack and insert metatile attributes from the given data using a vanilla layout. For AdvanceMap import
void Metatile::setAttributes(uint32_t data, BaseGameVersion version) {
const auto defaultLayout = Metatile::defaultLayouts.value(version);
this->setBehavior(defaultLayout->value("behavior").fromRaw(data));
this->setTerrainType(defaultLayout->value("terrainType").fromRaw(data));
this->setEncounterType(defaultLayout->value("encounterType").fromRaw(data));
this->setLayerType(defaultLayout->value("layerType").fromRaw(data));
const auto vanillaPackers = (version == BaseGameVersion::pokefirered) ? attributePackersFRLG : attributePackersRSE;
for (auto i = vanillaPackers.cbegin(), end = vanillaPackers.cend(); i != end; i++){
const auto packer = i.value();
this->setAttribute(i.key(), packer.unpack(data));
}
}
// Set the value for a metatile attribute, and fit it within the valid value range.
void Metatile::setAttribute(Metatile::Attr attr, uint32_t value) {
const auto packer = attributePackers.value(attr);
this->attributes.insert(attr, packer.clamp(value));
}
int Metatile::getDefaultAttributesSize(BaseGameVersion version) {
return (version == BaseGameVersion::pokefirered) ? 4 : 2;
}
uint32_t Metatile::getBehaviorMask(BaseGameVersion version) {
return Metatile::defaultLayouts.value(version)->value("behavior").mask;
uint32_t Metatile::getDefaultAttributesMask(BaseGameVersion version, Metatile::Attr attr) {
const auto vanillaPackers = (version == BaseGameVersion::pokefirered) ? attributePackersFRLG : attributePackersRSE;
return vanillaPackers.value(attr).mask();
}
uint32_t Metatile::getTerrainTypeMask(BaseGameVersion version) {
return Metatile::defaultLayouts.value(version)->value("terrainType").mask;
uint32_t Metatile::getMaxAttributesMask() {
static const QHash<int, uint32_t> maxMasks = {
{1, 0xFF},
{2, 0xFFFF},
{4, 0xFFFFFFFF},
};
return maxMasks.value(projectConfig.getMetatileAttributesSize(), 0);
}
uint32_t Metatile::getEncounterTypeMask(BaseGameVersion version) {
return Metatile::defaultLayouts.value(version)->value("encounterType").mask;
}
uint32_t Metatile::getLayerTypeMask(BaseGameVersion version) {
return Metatile::defaultLayouts.value(version)->value("layerType").mask;
void Metatile::setLayout(Project * project) {
uint32_t behaviorMask = projectConfig.getMetatileBehaviorMask();
uint32_t terrainTypeMask = projectConfig.getMetatileTerrainTypeMask();
uint32_t encounterTypeMask = projectConfig.getMetatileEncounterTypeMask();
uint32_t layerTypeMask = projectConfig.getMetatileLayerTypeMask();
// Calculate mask of bits not used by standard behaviors so we can preserve this data.
uint32_t unusedMask = ~(behaviorMask | terrainTypeMask | encounterTypeMask | layerTypeMask);
unusedMask &= Metatile::getMaxAttributesMask();
BitPacker packer = BitPacker(unusedMask);
attributePackers.clear();
attributePackers.insert(Metatile::Attr::Unused, unusedMask);
// Validate metatile behavior mask
packer.setMask(behaviorMask);
if (behaviorMask && !project->metatileBehaviorMapInverse.isEmpty()) {
uint32_t maxBehavior = project->metatileBehaviorMapInverse.lastKey();
if (packer.clamp(maxBehavior) != maxBehavior)
logWarn(QString("Metatile Behavior mask '0x%1' is insufficient to contain all available options.")
.arg(QString::number(behaviorMask, 16).toUpper()));
}
attributePackers.insert(Metatile::Attr::Behavior, packer);
// Validate terrain type mask
packer.setMask(terrainTypeMask);
const uint32_t maxTerrainType = NUM_METATILE_TERRAIN_TYPES - 1;
if (terrainTypeMask && packer.clamp(maxTerrainType) != maxTerrainType) {
logWarn(QString("Metatile Terrain Type mask '0x%1' is insufficient to contain all %2 available options.")
.arg(QString::number(terrainTypeMask, 16).toUpper())
.arg(maxTerrainType + 1));
}
attributePackers.insert(Metatile::Attr::TerrainType, packer);
// Validate encounter type mask
packer.setMask(encounterTypeMask);
const uint32_t maxEncounterType = NUM_METATILE_ENCOUNTER_TYPES - 1;
if (encounterTypeMask && packer.clamp(maxEncounterType) != maxEncounterType) {
logWarn(QString("Metatile Encounter Type mask '0x%1' is insufficient to contain all %2 available options.")
.arg(QString::number(encounterTypeMask, 16).toUpper())
.arg(maxEncounterType + 1));
}
attributePackers.insert(Metatile::Attr::EncounterType, packer);
// Validate terrain type mask
packer.setMask(layerTypeMask);
const uint32_t maxLayerType = NUM_METATILE_LAYER_TYPES - 1;
if (layerTypeMask && packer.clamp(maxLayerType) != maxLayerType) {
logWarn(QString("Metatile Layer Type mask '0x%1' is insufficient to contain all %2 available options.")
.arg(QString::number(layerTypeMask, 16).toUpper())
.arg(maxLayerType + 1));
}
attributePackers.insert(Metatile::Attr::LayerType, packer);
}

View File

@ -106,16 +106,31 @@ QList<QStringList> ParseUtil::parseAsm(const QString &filename) {
return parsed;
}
int ParseUtil::evaluateDefine(const QString &define, const QMap<QString, int> &knownDefines) {
QList<Token> tokens = tokenizeExpression(define, knownDefines);
// 'identifier' is the name of the #define to evaluate, e.g. 'FOO' in '#define FOO (BAR+1)'
// 'expression' is the text of the #define to evaluate, e.g. '(BAR+1)' in '#define FOO (BAR+1)'
// 'knownValues' is a pointer to a map of identifier->values for defines that have already been evaluated.
// 'unevaluatedExpressions' is a pointer to a map of identifier->expressions for defines that have not been evaluated. If this map contains any
// identifiers found in 'expression' then this function will be called recursively to evaluate that define first.
// This function will maintain the passed maps appropriately as new #defines are evaluated.
int ParseUtil::evaluateDefine(const QString &identifier, const QString &expression, QMap<QString, int> *knownValues, QMap<QString, QString> *unevaluatedExpressions) {
if (unevaluatedExpressions->contains(identifier))
unevaluatedExpressions->remove(identifier);
if (knownValues->contains(identifier))
return knownValues->value(identifier);
QList<Token> tokens = tokenizeExpression(expression, knownValues, unevaluatedExpressions);
QList<Token> postfixExpression = generatePostfix(tokens);
return evaluatePostfix(postfixExpression);
int value = evaluatePostfix(postfixExpression);
knownValues->insert(identifier, value);
return value;
}
QList<Token> ParseUtil::tokenizeExpression(QString expression, const QMap<QString, int> &knownIdentifiers) {
QList<Token> ParseUtil::tokenizeExpression(QString expression, QMap<QString, int> *knownValues, QMap<QString, QString> *unevaluatedExpressions) {
QList<Token> tokens;
QStringList tokenTypes = (QStringList() << "hex" << "decimal" << "identifier" << "operator" << "leftparen" << "rightparen");
static const QStringList tokenTypes = {"hex", "decimal", "identifier", "operator", "leftparen", "rightparen"};
static const QRegularExpression re("^(?<hex>0x[0-9a-fA-F]+)|(?<decimal>[0-9]+)|(?<identifier>[a-zA-Z_0-9]+)|(?<operator>[+\\-*\\/<>|^%]+)|(?<leftparen>\\()|(?<rightparen>\\))");
expression = expression.trimmed();
@ -129,10 +144,14 @@ QList<Token> ParseUtil::tokenizeExpression(QString expression, const QMap<QStrin
QString token = match.captured(tokenType);
if (!token.isEmpty()) {
if (tokenType == "identifier") {
if (knownIdentifiers.contains(token)) {
if (unevaluatedExpressions->contains(token)) {
// This expression depends on a define we know of but haven't evaluated. Evaluate it now
evaluateDefine(token, unevaluatedExpressions->value(token), knownValues, unevaluatedExpressions);
}
if (knownValues->contains(token)) {
// Any errors encountered when this identifier was evaluated should be recorded for this expression as well.
recordErrors(this->errorMap.value(token));
QString actualToken = QString("%1").arg(knownIdentifiers.value(token));
QString actualToken = QString("%1").arg(knownValues->value(token));
expression = expression.replace(0, token.length(), actualToken);
token = actualToken;
tokenType = "decimal";
@ -334,16 +353,12 @@ QStringList ParseUtil::readCIncbinArray(const QString &filename, const QString &
return paths;
}
QMap<QString, int> ParseUtil::readCDefines(const QString &filename,
const QStringList &prefixes,
QMap<QString, int> allDefines)
QString ParseUtil::readCDefinesFile(const QString &filename)
{
QMap<QString, int> filteredDefines;
this->file = filename;
if (this->file.isEmpty()) {
return filteredDefines;
return QString();
}
QString filepath = this->root + "/" + this->file;
@ -351,51 +366,98 @@ QMap<QString, int> ParseUtil::readCDefines(const QString &filename,
if (this->text.isNull()) {
logError(QString("Failed to read C defines file: '%1'").arg(filepath));
return filteredDefines;
return QString();
}
static const QRegularExpression re_extraChars("(//.*)|(\\/+\\*+[^*]*\\*+\\/+)");
this->text.replace(re_extraChars, "");
static const QRegularExpression re_extraSpaces("(\\\\\\s+)");
this->text.replace(re_extraSpaces, "");
allDefines.insert("FALSE", 0);
allDefines.insert("TRUE", 1);
return this->text;
}
// Read all the define names and their expressions in the specified file, then evaluate the ones matching the search text (and any they depend on).
// If 'fullMatch' is true, 'searchText' is a list of exact define names to evaluate and return.
// If 'fullMatch' is false, 'searchText' is a list of prefixes or regexes for define names to evaluate and return.
QMap<QString, int> ParseUtil::readCDefines(const QString &filename, const QStringList &searchText, bool fullMatch)
{
QMap<QString, int> filteredValues;
this->text = this->readCDefinesFile(filename);
if (this->text.isEmpty()) {
return filteredValues;
}
// Extract all the define names and expressions
QMap<QString, QString> allExpressions;
QMap<QString, QString> filteredExpressions;
static const QRegularExpression re("#define\\s+(?<defineName>\\w+)[^\\S\\n]+(?<defineValue>.+)");
QRegularExpressionMatchIterator iter = re.globalMatch(this->text);
while (iter.hasNext()) {
QRegularExpressionMatch match = iter.next();
const QString name = match.captured("defineName");
const QString expression = match.captured("defineValue");
// If name matches the search text record it for evaluation.
for (auto s : searchText) {
if ((fullMatch && name == s) || (!fullMatch && (name.startsWith(s) || QRegularExpression(s).match(name).hasMatch()))) {
filteredExpressions.insert(name, expression);
break;
}
}
allExpressions.insert(name, expression);
}
QMap<QString, int> allValues;
allValues.insert("FALSE", 0);
allValues.insert("TRUE", 1);
// Evaluate defines
this->errorMap.clear();
while (!filteredExpressions.isEmpty()) {
const QString name = filteredExpressions.firstKey();
const QString expression = filteredExpressions.take(name);
if (expression == " ") continue;
this->curDefine = name;
filteredValues.insert(name, evaluateDefine(name, expression, &allValues, &allExpressions));
logRecordedErrors(); // Only log errors for defines that Porymap is looking for
}
return filteredValues;
}
// Find and evaluate an unknown list of defines with a known name prefix.
QMap<QString, int> ParseUtil::readCDefinesByPrefix(const QString &filename, QStringList prefixes) {
prefixes.removeDuplicates();
return this->readCDefines(filename, prefixes, false);
}
// Find and evaluate a specific set of defines with known names.
QMap<QString, int> ParseUtil::readCDefinesByName(const QString &filename, QStringList names) {
names.removeDuplicates();
return this->readCDefines(filename, names, true);
}
// Similar to readCDefines, but for cases where we only need to show a list of define names.
// We can skip reading/evaluating any expressions (and by extension skip reporting any errors from this process).
QStringList ParseUtil::readCDefineNames(const QString &filename, const QStringList &prefixes) {
QStringList filteredNames;
this->text = this->readCDefinesFile(filename);
if (this->text.isEmpty()) {
return filteredNames;
}
static const QRegularExpression re("#define\\s+(?<defineName>\\w+)[^\\S\\n]+");
QRegularExpressionMatchIterator iter = re.globalMatch(this->text);
while (iter.hasNext()) {
QRegularExpressionMatch match = iter.next();
QString name = match.captured("defineName");
QString expression = match.captured("defineValue");
if (expression == " ") continue;
this->curDefine = name;
int value = evaluateDefine(expression, allDefines);
allDefines.insert(name, value);
for (QString prefix : prefixes) {
if (name.startsWith(prefix) || QRegularExpression(prefix).match(name).hasMatch()) {
// Only log errors for defines that Porymap is looking for
logRecordedErrors();
filteredDefines.insert(name, value);
filteredNames.append(name);
}
}
}
return filteredDefines;
}
QStringList ParseUtil::readCDefinesSorted(const QString &filename,
const QStringList &prefixes,
const QMap<QString, int> &knownDefines)
{
QMap<QString, int> defines = readCDefines(filename, prefixes, knownDefines);
// The defines should be sorted by their underlying value, not alphabetically.
// Reverse the map and read out the resulting keys in order.
QMultiMap<int, QString> definesInverse;
for (QString defineName : defines.keys()) {
definesInverse.insert(defines[defineName], defineName);
}
return definesInverse.values();
return filteredNames;
}
QStringList ParseUtil::readCArray(const QString &filename, const QString &label) {

View File

@ -17,7 +17,11 @@ using std::make_shared;
RegionMap::RegionMap(Project *project) {
RegionMap::RegionMap(Project *project) :
section_prefix(projectConfig.getIdentifier(ProjectIdentifier::define_map_section_prefix)),
default_map_section(section_prefix + projectConfig.getIdentifier(ProjectIdentifier::define_map_section_empty)),
count_map_section(section_prefix + projectConfig.getIdentifier(ProjectIdentifier::define_map_section_count))
{
this->project = project;
}
@ -146,7 +150,7 @@ bool RegionMap::loadLayout(poryjson::Json layoutJson) {
LayoutSquare square;
square.map_section = square_section_name;
square.has_map = (square_section_name != "MAPSEC_NONE" && !square_section_name.isEmpty());
square.has_map = (square_section_name != this->default_map_section && !square_section_name.isEmpty());
square.x = x;
square.y = y;
@ -204,7 +208,7 @@ bool RegionMap::loadLayout(poryjson::Json layoutJson) {
LayoutSquare square;
square.map_section = square_section_name;
square.has_map = (square_section_name != "MAPSEC_NONE" && !square_section_name.isEmpty());
square.has_map = (square_section_name != this->default_map_section && !square_section_name.isEmpty());
square.x = x;
square.y = y;
layout.append(square);
@ -228,7 +232,7 @@ bool RegionMap::loadLayout(poryjson::Json layoutJson) {
this->layout_qualifiers = qualifiers + " " + type;
this->layout_array_label = label;
static const QRegularExpression reSec("(?<sec>MAPSEC_[A-Za-z0-9_]+)");
static const QRegularExpression reSec(QString("(?<sec>%1[A-Za-z0-9_]+)").arg(this->section_prefix));
QRegularExpressionMatchIterator k = reSec.globalMatch(text);
QList<LayoutSquare> layout;
@ -242,7 +246,7 @@ bool RegionMap::loadLayout(poryjson::Json layoutJson) {
LayoutSquare square;
square.map_section = sec;
square.has_map = (sec != "MAPSEC_NONE" && !sec.isEmpty());
square.has_map = (sec != this->default_map_section && !sec.isEmpty());
square.x = x;
square.y = y;
layout.append(square);
@ -283,7 +287,7 @@ bool RegionMap::loadLayout(poryjson::Json layoutJson) {
QString square_section_name = section.trimmed();
LayoutSquare square;
square.map_section = square_section_name;
square.has_map = (square_section_name != "MAPSEC_NONE" && !square_section_name.isEmpty());
square.has_map = (square_section_name != this->default_map_section && !square_section_name.isEmpty());
square.x = x;
square.y = y;
layout.append(square);
@ -454,7 +458,7 @@ void RegionMap::saveLayout() {
}
void RegionMap::resetSquare(int index) {
this->layouts[this->current_layer][index].map_section = "MAPSEC_NONE";
this->layouts[this->current_layer][index].map_section = this->default_map_section;
this->layouts[this->current_layer][index].has_map = false;
}
@ -473,7 +477,7 @@ void RegionMap::replaceSection(QString oldSection, QString newSection) {
for (auto &square : this->layouts[this->current_layer]) {
if (square.map_section == oldSection) {
square.map_section = newSection;
square.has_map = (newSection != "MAPSEC_NONE");
square.has_map = (newSection != this->default_map_section);
}
}
}
@ -482,11 +486,11 @@ void RegionMap::swapSections(QString secA, QString secB) {
for (auto &square : this->layouts[this->current_layer]) {
if (square.map_section == secA) {
square.map_section = secB;
square.has_map = (square.map_section != "MAPSEC_NONE");
square.has_map = (square.map_section != this->default_map_section);
}
else if (square.map_section == secB) {
square.map_section = secA;
square.has_map = (square.map_section != "MAPSEC_NONE");
square.has_map = (square.map_section != this->default_map_section);
}
}
}
@ -725,7 +729,7 @@ void RegionMap::setSquareMapSection(int index, QString section) {
int layoutIndex = tilemapToLayoutIndex(index);
if (!(layoutIndex < 0 || !this->layouts.contains(this->current_layer))) {
this->layouts[this->current_layer][layoutIndex].map_section = section;
this->layouts[this->current_layer][layoutIndex].has_map = !(section == "MAPSEC_NONE" || section.isEmpty());
this->layouts[this->current_layer][layoutIndex].has_map = !(section == this->default_map_section || section.isEmpty());
}
}
@ -780,7 +784,7 @@ QString RegionMap::fixCase(QString caps) {
QString camel;
static const QRegularExpression re_braced("({.*})");
for (auto ch : caps.remove(re_braced).remove("MAPSEC")) {
for (auto ch : caps.remove(re_braced).remove(this->section_prefix)) {
if (ch == '_' || ch == ' ') {
big = true;
continue;

View File

@ -168,7 +168,10 @@ QString Tileset::getMetatileLabelPrefix()
QString Tileset::getMetatileLabelPrefix(const QString &name)
{
return QString("METATILE_%1_").arg(QString(name).replace("gTileset_", ""));
// Default is "gTileset_Name" --> "METATILE_Name_"
const QString tilesetPrefix = projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix);
const QString labelPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_metatile_label_prefix);
return QString("%1%2_").arg(labelPrefix).arg(QString(name).replace(tilesetPrefix, ""));
}
bool Tileset::metatileIsValid(uint16_t metatileId, Tileset *primaryTileset, Tileset *secondaryTileset) {
@ -343,7 +346,8 @@ QString Tileset::getExpectedDir(QString tilesetName, bool isSecondary)
static const QRegularExpression re("([a-z])([A-Z0-9])");
const QString category = isSecondary ? "secondary" : "primary";
const QString basePath = projectConfig.getFilePath(ProjectFilePath::data_tilesets_folders) + category + "/";
return basePath + tilesetName.replace("gTileset_", "").replace(re, "\\1_\\2").toLower();
const QString prefix = projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix);
return basePath + tilesetName.replace(prefix, "").replace(re, "\\1_\\2").toLower();
}
// Get the expected positions of the members in struct Tileset.

View File

@ -21,6 +21,9 @@
static bool selectNewEvents = false;
// 2D array mapping collision+elevation combos to an icon.
QList<QList<const QImage*>> Editor::collisionIcons;
Editor::Editor(Ui::MainWindow* ui)
{
this->ui = ui;
@ -49,6 +52,8 @@ Editor::~Editor()
delete this->playerViewRect;
delete this->cursorMapTileRect;
delete this->map_ruler;
for (auto sublist : collisionIcons)
qDeleteAll(sublist);
closeProject();
}
@ -146,6 +151,7 @@ void Editor::setEditingObjects() {
setConnectionsEditable(false);
this->cursorMapTileRect->setSingleTileMode();
this->cursorMapTileRect->setActive(false);
updateWarpEventWarnings();
setMapEditingButtonsEnabled(false);
}
@ -936,8 +942,10 @@ QString Editor::getMetatileDisplayMessage(uint16_t metatileId) {
QString message = QString("Metatile: %1").arg(Metatile::getMetatileIdString(metatileId));
if (label.size())
message += QString(" \"%1\"").arg(label);
if (metatile && metatile->behavior) // Skip MB_NORMAL
message += QString(", Behavior: %1").arg(this->project->metatileBehaviorMapInverse.value(metatile->behavior, QString::number(metatile->behavior)));
if (metatile && metatile->behavior() != 0) { // Skip MB_NORMAL
const QString behaviorStr = this->project->metatileBehaviorMapInverse.value(metatile->behavior(), "0x" + QString::number(metatile->behavior(), 16));
message += QString(", Behavior: %1").arg(behaviorStr);
}
return message;
}
@ -1022,7 +1030,7 @@ void Editor::onHoveredMapMetatileChanged(const QPoint &pos) {
this->updateCursorRectPos(x, y);
if (map_item->paintingMode == MapPixmapItem::PaintMode::Metatiles) {
int blockIndex = y * map->getWidth() + x;
int metatileId = map->layout->blockdata.at(blockIndex).metatileId;
int metatileId = map->layout->blockdata.at(blockIndex).metatileId();
this->ui->statusBar->showMessage(QString("X: %1, Y: %2, %3, Scale = %4x")
.arg(x)
.arg(y)
@ -1054,8 +1062,8 @@ void Editor::onHoveredMapMovementPermissionChanged(int x, int y) {
this->updateCursorRectPos(x, y);
if (map_item->paintingMode == MapPixmapItem::PaintMode::Metatiles) {
int blockIndex = y * map->getWidth() + x;
uint16_t collision = map->layout->blockdata.at(blockIndex).collision;
uint16_t elevation = map->layout->blockdata.at(blockIndex).elevation;
uint16_t collision = map->layout->blockdata.at(blockIndex).collision();
uint16_t elevation = map->layout->blockdata.at(blockIndex).elevation();
QString message = QString("X: %1, Y: %2, %3")
.arg(x)
.arg(y)
@ -1075,16 +1083,16 @@ void Editor::onHoveredMapMovementPermissionCleared() {
QString Editor::getMovementPermissionText(uint16_t collision, uint16_t elevation) {
QString message;
if (collision == 0 && elevation == 0) {
message = "Collision: Transition between elevations";
} else if (collision == 0 && elevation == 15) {
message = "Collision: Multi-Level (Bridge)";
} else if (collision == 0 && elevation == 1) {
message = "Collision: Surf";
} else if (collision == 0) {
message = QString("Collision: Passable, Elevation: %1").arg(elevation);
} else {
if (collision != 0) {
message = QString("Collision: Impassable (%1), Elevation: %2").arg(collision).arg(elevation);
} else if (elevation == 0) {
message = "Collision: Transition between elevations";
} else if (elevation == 15) {
message = "Collision: Multi-Level (Bridge)";
} else if (elevation == 1) {
message = "Collision: Surf";
} else {
message = QString("Collision: Passable, Elevation: %1").arg(elevation);
}
return message;
}
@ -1425,7 +1433,7 @@ void Editor::displayMapMovementPermissions() {
scene->removeItem(collision_item);
delete collision_item;
}
collision_item = new CollisionPixmapItem(map, this->movement_permissions_selector_item,
collision_item = new CollisionPixmapItem(map, ui->spinBox_SelectedCollision, ui->spinBox_SelectedElevation,
this->metatile_selector_item, this->settings, &this->collisionOpacity);
connect(collision_item, &CollisionPixmapItem::mouseEvent, this, &Editor::mouseEvent_collision);
connect(collision_item, &CollisionPixmapItem::hoveredMapMovementPermissionChanged,
@ -1484,12 +1492,15 @@ void Editor::displayMovementPermissionSelector() {
scene_collision_metatiles = new QGraphicsScene;
if (!movement_permissions_selector_item) {
movement_permissions_selector_item = new MovementPermissionsSelector();
movement_permissions_selector_item = new MovementPermissionsSelector(this->collisionSheetPixmap);
connect(movement_permissions_selector_item, &MovementPermissionsSelector::hoveredMovementPermissionChanged,
this, &Editor::onHoveredMovementPermissionChanged);
connect(movement_permissions_selector_item, &MovementPermissionsSelector::hoveredMovementPermissionCleared,
this, &Editor::onHoveredMovementPermissionCleared);
movement_permissions_selector_item->select(0, 3);
connect(movement_permissions_selector_item, &SelectablePixmapItem::selectionChanged, [this](int x, int y, int, int) {
this->setCollisionTabSpinBoxes(x, y);
});
movement_permissions_selector_item->select(projectConfig.getDefaultCollision(), projectConfig.getDefaultElevation());
}
scene_collision_metatiles->addItem(movement_permissions_selector_item);
@ -1979,6 +1990,37 @@ void Editor::redrawObject(DraggablePixmapItem *item) {
}
}
// Warp events display a warning if they're not positioned on a metatile with a warp behavior.
void Editor::updateWarpEventWarning(Event *event) {
if (porymapConfig.getWarpBehaviorWarningDisabled())
return;
if (!project || !map || !event || event->getEventType() != Event::Type::Warp)
return;
Block block;
Metatile * metatile = nullptr;
WarpEvent * warpEvent = static_cast<WarpEvent*>(event);
if (map->getBlock(warpEvent->getX(), warpEvent->getY(), &block)) {
metatile = Tileset::getMetatile(block.metatileId(), map->layout->tileset_primary, map->layout->tileset_secondary);
}
// metatile may be null if the warp is in the map border. Display the warning in this case
bool validWarpBehavior = metatile && projectConfig.getWarpBehaviors().contains(metatile->behavior());
warpEvent->setWarningEnabled(!validWarpBehavior);
}
// The warp event behavior warning is updated whenever the event moves or the event selection changes.
// It does not respond to changes in the underlying metatile. To capture the common case of a user painting
// metatiles on the Map tab then returning to the Events tab we update the warnings for all selected warp
// events when the Events tab is opened. This does not cover the case where metatiles are painted while
// still on the Events tab, such as by Undo/Redo or the scripting API.
void Editor::updateWarpEventWarnings() {
if (porymapConfig.getWarpBehaviorWarningDisabled())
return;
if (selected_events) {
for (auto selection : *selected_events)
updateWarpEventWarning(selection->event);
}
}
void Editor::shouldReselectEvents() {
selectNewEvents = true;
}
@ -2227,3 +2269,73 @@ void Editor::objectsView_onMousePress(QMouseEvent *event) {
}
selectingEvent = false;
}
void Editor::setCollisionTabSpinBoxes(uint16_t collision, uint16_t elevation) {
const QSignalBlocker blocker1(ui->spinBox_SelectedCollision);
const QSignalBlocker blocker2(ui->spinBox_SelectedElevation);
ui->spinBox_SelectedCollision->setValue(collision);
ui->spinBox_SelectedElevation->setValue(elevation);
}
// Custom collision graphics may be provided by the user.
void Editor::setCollisionGraphics() {
QString customPath = projectConfig.getCollisionSheetPath();
QImage imgSheet;
if (customPath.isEmpty()) {
// No custom collision image specified, use the default.
imgSheet = this->defaultCollisionImgSheet;
} else {
// Try to load custom collision image
QFileInfo info(customPath);
if (info.isRelative()) {
customPath = QDir::cleanPath(projectConfig.getProjectDir() + QDir::separator() + customPath);
}
imgSheet = QImage(customPath);
if (imgSheet.isNull()) {
// Custom collision image failed to load, use default
logWarn(QString("Failed to load custom collision image '%1', using default.").arg(customPath));
imgSheet = this->defaultCollisionImgSheet;
}
}
// Users are not required to provide an image that gives an icon for every elevation/collision combination.
// Instead they tell us how many are provided in their image by specifying the number of columns and rows.
const int imgColumns = projectConfig.getCollisionSheetWidth();
const int imgRows = projectConfig.getCollisionSheetHeight();
// Create a pixmap for the selector on the Collision tab. If a project was previously opened we'll also need to refresh the selector.
this->collisionSheetPixmap = QPixmap::fromImage(imgSheet).scaled(MovementPermissionsSelector::CellWidth * imgColumns,
MovementPermissionsSelector::CellHeight * imgRows);
if (this->movement_permissions_selector_item)
this->movement_permissions_selector_item->setBasePixmap(this->collisionSheetPixmap);
for (auto sublist : collisionIcons)
qDeleteAll(sublist);
collisionIcons.clear();
// Use the image sheet to create an icon for each collision/elevation combination.
// Any icons for combinations that aren't provided by the image sheet are also created now using default graphics.
const int w = 16, h = 16;
imgSheet = imgSheet.scaled(w * imgColumns, h * imgRows);
for (int collision = 0; collision <= Block::getMaxCollision(); collision++) {
// If (collision >= imgColumns) here, it's a valid collision value, but it is not represented with an icon on the image sheet.
// In this case we just use the rightmost collision icon. This is mostly to support the vanilla case, where technically 0-3
// are valid collision values, but 1-3 have the same meaning, so the vanilla collision selector image only has 2 columns.
int x = ((collision < imgColumns) ? collision : (imgColumns - 1)) * w;
QList<const QImage*> sublist;
for (int elevation = 0; elevation <= Block::getMaxElevation(); elevation++) {
if (elevation < imgRows) {
// This elevation has an icon on the image sheet, add it to the list
int y = elevation * h;
sublist.append(new QImage(imgSheet.copy(x, y, w, h)));
} else {
// This is a valid elevation value, but it has no icon on the image sheet.
// Give it a placeholder "?" icon (red if impassable, white otherwise)
sublist.append(new QImage(this->collisionPlaceholder.copy(x != 0 ? w : 0, 0, w, h)));
}
}
collisionIcons.append(sublist);
}
}

View File

@ -372,7 +372,8 @@ void MainWindow::setWildEncountersUIEnabled(bool enabled) {
ui->mainTabBar->setTabEnabled(4, enabled);
}
void MainWindow::setProjectSpecificUIVisibility()
// Update the UI using information we've read from the user's project files.
void MainWindow::setProjectSpecificUI()
{
this->setWildEncountersUIEnabled(userConfig.getEncounterJsonActive());
@ -391,6 +392,11 @@ void MainWindow::setProjectSpecificUIVisibility()
bool floorNumEnabled = projectConfig.getFloorNumberEnabled();
ui->spinBox_FloorNumber->setVisible(floorNumEnabled);
ui->label_FloorNumber->setVisible(floorNumEnabled);
Event::setIcons();
editor->setCollisionGraphics();
ui->spinBox_SelectedElevation->setMaximum(Block::getMaxElevation());
ui->spinBox_SelectedCollision->setMaximum(Block::getMaxCollision());
}
void MainWindow::mapSortOrder_changed(QAction *action)
@ -452,6 +458,9 @@ void MainWindow::loadUserSettings() {
ui->horizontalSlider_MetatileZoom->blockSignals(true);
ui->horizontalSlider_MetatileZoom->setValue(porymapConfig.getMetatilesZoom());
ui->horizontalSlider_MetatileZoom->blockSignals(false);
ui->horizontalSlider_CollisionZoom->blockSignals(true);
ui->horizontalSlider_CollisionZoom->setValue(porymapConfig.getCollisionZoom());
ui->horizontalSlider_CollisionZoom->blockSignals(false);
setTheme(porymapConfig.getTheme());
}
@ -504,14 +513,12 @@ bool MainWindow::openProject(QString dir) {
this->statusBar()->showMessage(QString("Opening project %1").arg(nativeDir));
bool success = true;
userConfig.setProjectDir(dir);
userConfig.load();
projectConfig.setProjectDir(dir);
projectConfig.load();
this->closeSupplementaryWindows();
this->setProjectSpecificUIVisibility();
this->newMapDefaultsSet = false;
Scripting::init(this);
@ -528,19 +535,17 @@ bool MainWindow::openProject(QString dir) {
this->preferenceEditor->updateFields();
});
editor->project->set_root(dir);
success = loadDataStructures()
&& populateMapList()
&& setMap(getDefaultMap(), true);
} else {
QString open_map = editor->map->name;
editor->project->fileWatcher.removePaths(editor->project->fileWatcher.files());
editor->project->clearMapCache();
editor->project->clearTilesetCache();
success = loadDataStructures() && populateMapList() && setMap(open_map, true);
}
projectOpenFailure = !success;
if (projectOpenFailure) {
this->projectOpenFailure = !(loadDataStructures()
&& populateMapList()
&& setInitialMap());
if (this->projectOpenFailure) {
this->statusBar()->showMessage(QString("Failed to open project %1").arg(nativeDir));
QMessageBox msgBox(this);
QString errorMsg = QString("There was an error opening the project %1. Please see %2 for full error details.\n\n%3")
@ -567,28 +572,31 @@ bool MainWindow::isProjectOpen() {
return !projectOpenFailure && editor && editor->project;
}
QString MainWindow::getDefaultMap() {
if (editor && editor->project) {
QList<QStringList> names = editor->project->groupedMapNames;
if (!names.isEmpty()) {
QString recentMap = userConfig.getRecentMap();
if (!recentMap.isNull() && recentMap.length() > 0) {
for (int i = 0; i < names.length(); i++) {
if (names.value(i).contains(recentMap)) {
return recentMap;
}
}
}
// Failing that, just get the first map in the list.
for (int i = 0; i < names.length(); i++) {
QStringList list = names.value(i);
if (list.length()) {
return list.value(0);
}
bool MainWindow::setInitialMap() {
QList<QStringList> names;
if (editor && editor->project)
names = editor->project->groupedMapNames;
QString recentMap = userConfig.getRecentMap();
if (!recentMap.isEmpty()) {
// Make sure the recent map is still in the map list
for (int i = 0; i < names.length(); i++) {
if (names.value(i).contains(recentMap)) {
return setMap(recentMap, true);
}
}
}
return QString();
// Failing that, just get the first map in the list.
for (int i = 0; i < names.length(); i++) {
QStringList list = names.value(i);
if (list.length()) {
return setMap(list.value(0), true);
}
}
logError("Failed to load any map names.");
return false;
}
void MainWindow::openSubWindow(QWidget * window) {
@ -635,7 +643,8 @@ void MainWindow::on_action_Reload_Project_triggered() {
warning.setIcon(QMessageBox::Warning);
if (warning.exec() == QMessageBox::Ok) {
openProject(editor->project->root);
if (!openProject(editor->project->root))
setWindowDisabled(true);
}
}
@ -645,7 +654,7 @@ bool MainWindow::setMap(QString map_name, bool scrollTreeView) {
return false;
}
if (!editor->setMap(map_name)) {
if (!editor || !editor->setMap(map_name)) {
logWarn(QString("Failed to set map to '%1'").arg(map_name));
return false;
}
@ -715,6 +724,7 @@ void MainWindow::refreshMapScene()
ui->graphicsView_Collision->setFixedSize(editor->movement_permissions_selector_item->pixmap().width() + 2, editor->movement_permissions_selector_item->pixmap().height() + 2);
on_horizontalSlider_MetatileZoom_valueChanged(ui->horizontalSlider_MetatileZoom->value());
on_horizontalSlider_CollisionZoom_valueChanged(ui->horizontalSlider_CollisionZoom->value());
}
void MainWindow::openWarpMap(QString map_name, int event_id, Event::Group event_group) {
@ -923,10 +933,10 @@ bool MainWindow::loadDataStructures() {
&& project->readBgEventFacingDirections()
&& project->readTrainerTypes()
&& project->readMetatileBehaviors()
&& project->readTilesetProperties()
&& project->readFieldmapProperties()
&& project->readFieldmapMasks()
&& project->readTilesetLabels()
&& project->readTilesetMetatileLabels()
&& project->readMaxMapDataSize()
&& project->readHealLocations()
&& project->readMiscellaneousConstants()
&& project->readSpeciesIconPaths()
@ -936,7 +946,8 @@ bool MainWindow::loadDataStructures() {
&& project->readEventGraphics()
&& project->readSongNames();
Metatile::setCustomLayout(project);
project->applyParsedLimits();
setProjectSpecificUI();
Scripting::populateGlobalObject(this);
return success && loadProjectCombos();
@ -1449,8 +1460,8 @@ void MainWindow::copy() {
collisions.clear();
for (int i = 0; i < metatiles.length(); i++) {
OrderedJson::object collision;
collision["collision"] = 0;
collision["elevation"] = 3;
collision["collision"] = projectConfig.getDefaultCollision();
collision["elevation"] = projectConfig.getDefaultElevation();
collisions.append(collision);
}
}
@ -1857,8 +1868,9 @@ void MainWindow::addNewEvent(Event::Type type) {
msgBox.setText("Failed to add new event");
if (Event::typeToGroup(type) == Event::Group::Object) {
msgBox.setInformativeText(QString("The limit for object events (%1) has been reached.\n\n"
"This limit can be adjusted with OBJECT_EVENT_TEMPLATES_COUNT in '%2'.")
"This limit can be adjusted with %2 in '%3'.")
.arg(editor->project->getMaxObjectEvents())
.arg(projectConfig.getIdentifier(ProjectIdentifier::define_obj_event_count))
.arg(projectConfig.getFilePath(ProjectFilePath::constants_global)));
}
msgBox.setDefaultButton(QMessageBox::Ok);
@ -2024,7 +2036,7 @@ void MainWindow::updateSelectedObjects() {
EventFrame *eventFrame = event->createEventFrame();
eventFrame->populate(this->editor->project);
eventFrame->initialize();
eventFrame->connectSignals();
eventFrame->connectSignals(this);
frames.append(eventFrame);
}
@ -2719,16 +2731,47 @@ void MainWindow::togglePreferenceSpecificUi() {
ui->actionOpen_Project_in_Text_Editor->setEnabled(true);
}
void MainWindow::on_actionProject_Settings_triggered() {
void MainWindow::openProjectSettingsEditor(int tab) {
if (!this->projectSettingsEditor) {
this->projectSettingsEditor = new ProjectSettingsEditor(this, this->editor->project);
connect(this->projectSettingsEditor, &ProjectSettingsEditor::reloadProject,
this, &MainWindow::on_action_Reload_Project_triggered);
}
this->projectSettingsEditor->setTab(tab);
openSubWindow(this->projectSettingsEditor);
}
void MainWindow::on_actionProject_Settings_triggered() {
this->openProjectSettingsEditor(porymapConfig.getProjectSettingsTab());
}
void MainWindow::onWarpBehaviorWarningClicked() {
static const QString text = QString(
"By default, Warp Events only function as exits if they're positioned on a metatile "
"whose Metatile Behavior is treated specially in your project's code."
);
static const QString informative = QString(
"<html><head/><body><p>"
"For instance, most floor metatiles in a cave have the behavior <b>MB_CAVE</b>, but the floor space in front of an "
"exit will have <b>MB_SOUTH_ARROW_WARP</b>, which is treated specially and will allow a Warp Event to warp the player. "
"You can see in the status bar what behavior a metatile has when you mouse over it, or by selecting it in the Tileset Editor."
"<br><br>"
"<b>Note</b>: Not all Warp Events that show this warning are incorrect! For example some warps may function "
"as a 1-way entrance, and others may have the metatile underneath them changed programmatically."
"<br><br>"
"You can disable this warning or edit the list of behaviors that silence this warning under <b>Options -> Project Settings...</b>"
"<br></html></body></p>"
);
QMessageBox msgBox(QMessageBox::Information, "porymap", text, QMessageBox::Close, this);
QPushButton *settings = msgBox.addButton("Open Settings...", QMessageBox::ActionRole);
msgBox.setDefaultButton(QMessageBox::Close);
msgBox.setTextFormat(Qt::RichText);
msgBox.setInformativeText(informative);
msgBox.exec();
if (msgBox.clickedButton() == settings)
this->openProjectSettingsEditor(ProjectSettingsEditor::eventsTab);
}
void MainWindow::on_actionCustom_Scripts_triggered() {
if (!this->customScriptsEditor)
initCustomScriptsEditor();
@ -2794,6 +2837,31 @@ void MainWindow::on_horizontalSlider_MetatileZoom_valueChanged(int value) {
redrawMetatileSelection();
}
void MainWindow::on_horizontalSlider_CollisionZoom_valueChanged(int value) {
porymapConfig.setCollisionZoom(value);
double scale = pow(3.0, static_cast<double>(value - 30) / 30.0);
QTransform transform;
transform.scale(scale, scale);
QSize size(editor->movement_permissions_selector_item->pixmap().width(),
editor->movement_permissions_selector_item->pixmap().height());
size *= scale;
ui->graphicsView_Collision->setResizeAnchor(QGraphicsView::NoAnchor);
ui->graphicsView_Collision->setTransform(transform);
ui->graphicsView_Collision->setFixedSize(size.width() + 2, size.height() + 2);
}
void MainWindow::on_spinBox_SelectedCollision_valueChanged(int collision) {
if (this->editor && this->editor->movement_permissions_selector_item)
this->editor->movement_permissions_selector_item->select(collision, ui->spinBox_SelectedElevation->value());
}
void MainWindow::on_spinBox_SelectedElevation_valueChanged(int elevation) {
if (this->editor && this->editor->movement_permissions_selector_item)
this->editor->movement_permissions_selector_item->select(ui->spinBox_SelectedCollision->value(), elevation);
}
void MainWindow::on_actionRegion_Map_Editor_triggered() {
if (!this->regionMapEditor) {
if (!initRegionMapEditor()) {
@ -2838,8 +2906,8 @@ void MainWindow::closeSupplementaryWindows() {
delete this->mapImageExporter;
delete this->newMapPrompt;
delete this->shortcutsEditor;
delete this->projectSettingsEditor;
delete this->customScriptsEditor;
if (this->projectSettingsEditor) this->projectSettingsEditor->closeQuietly();
}
void MainWindow::closeEvent(QCloseEvent *event) {

View File

@ -28,7 +28,6 @@ using OrderedJsonDoc = poryjson::JsonDoc;
int Project::num_tiles_primary = 512;
int Project::num_tiles_total = 1024;
int Project::num_metatiles_primary = 512;
int Project::num_metatiles_total = 1024;
int Project::num_pals_primary = 6;
int Project::num_pals_total = 13;
int Project::max_map_data_size = 10240; // 0x2800
@ -290,24 +289,22 @@ bool Project::loadMapData(Map* map) {
}
map->events[Event::Group::Heal].clear();
const QString mapPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_map_prefix);
for (auto it = healLocations.begin(); it != healLocations.end(); it++) {
HealLocation loc = *it;
//if TRUE map is flyable / has healing location
if (loc.mapName == QString(mapNamesToMapConstants.value(map->name)).remove(0,4)) {
if (loc.mapName == Map::mapConstantFromName(map->name, false)) {
HealLocationEvent *heal = new HealLocationEvent();
heal->setMap(map);
heal->setX(loc.x);
heal->setY(loc.y);
heal->setElevation(3);
heal->setElevation(projectConfig.getDefaultElevation());
heal->setLocationName(loc.mapName);
heal->setIdName(loc.idName);
heal->setIndex(loc.index);
// TODO: what is this
// heal->put("destination_map_name", mapConstantsToMapNames.value(map->name));
if (projectConfig.getHealLocationRespawnDataEnabled()) {
heal->setRespawnMap(mapConstantsToMapNames.value(QString("MAP_" + loc.respawnMap)));
heal->setRespawnMap(mapConstantsToMapNames.value(QString(mapPrefix + loc.respawnMap)));
heal->setRespawnNPC(loc.respawnNPC);
}
map->events[Event::Group::Heal].append(heal);
@ -631,9 +628,8 @@ void Project::saveWildMonData() {
OrderedJson::object wildEncountersObject;
OrderedJson::array wildEncounterGroups;
// gWildMonHeaders label is not mutable
OrderedJson::object monHeadersObject;
monHeadersObject["label"] = "gWildMonHeaders";
monHeadersObject["label"] = projectConfig.getIdentifier(ProjectIdentifier::symbol_wild_encounters);
monHeadersObject["for_maps"] = true;
OrderedJson::array fieldsInfoArray;
@ -752,6 +748,12 @@ void Project::saveHealLocations(Map *map) {
this->saveHealLocationsConstants();
}
QString Project::getHealLocationsTableName() {
if (projectConfig.getHealLocationRespawnDataEnabled())
return projectConfig.getIdentifier(ProjectIdentifier::symbol_spawn_points);
return projectConfig.getIdentifier(ProjectIdentifier::symbol_heal_locations);
}
// Saves heal location maps/coords/respawn data in root + /src/data/heal_locations.h
void Project::saveHealLocationsData(Map *map) {
// Update heal locations from map
@ -775,20 +777,19 @@ void Project::saveHealLocationsData(Map *map) {
// Create the definition text for each data table
bool respawnEnabled = projectConfig.getHealLocationRespawnDataEnabled();
QString arrayName = respawnEnabled ? "sSpawnPoints" : "sHealLocations";
const QString qualifiers = QString(healLocationDataQualifiers.isStatic ? "static " : "")
+ QString(healLocationDataQualifiers.isConst ? "const " : "");
QString locationTableText = QString("%1struct HealLocation %2[] =\n{\n").arg(qualifiers).arg(arrayName);
QString locationTableText = QString("%1struct HealLocation %2[] =\n{\n").arg(qualifiers).arg(this->getHealLocationsTableName());
QString respawnMapTableText, respawnNPCTableText;
if (respawnEnabled) {
respawnMapTableText = QString("\n%1u16 sWhiteoutRespawnHealCenterMapIdxs[][2] =\n{\n").arg(qualifiers);
respawnNPCTableText = QString("\n%1u8 sWhiteoutRespawnHealerNpcIds[] =\n{\n").arg(qualifiers);
respawnMapTableText = QString("\n%1u16 %2[][2] =\n{\n").arg(qualifiers).arg(projectConfig.getIdentifier(ProjectIdentifier::symbol_spawn_maps));
respawnNPCTableText = QString("\n%1u8 %2[] =\n{\n").arg(qualifiers).arg(projectConfig.getIdentifier(ProjectIdentifier::symbol_spawn_npcs));
}
// Populate the data tables with the heal location data
int i = 0;
const QString emptyMapName = "UNDEFINED"; // TODO: Use a project-wide constant here?
const QString emptyMapName = projectConfig.getIdentifier(ProjectIdentifier::define_map_empty);
for (auto hl : this->healLocations) {
// Add numbered suffix for duplicate constants
if (healLocationsDupes.keys().contains(hl.idName)) {
@ -898,7 +899,7 @@ void Project::updateTilesetMetatileLabels(Tileset *tileset) {
}
// Given a map of define names to define values, returns a formatted list of #defines
QString Project::buildMetatileLabelsText(const QMap<QString, int> defines) {
QString Project::buildMetatileLabelsText(const QMap<QString, uint16_t> defines) {
QStringList labels = defines.keys();
// Setup for pretty formatting.
@ -1095,7 +1096,7 @@ void Project::setNewMapBlockdata(Map *map) {
map->layout->blockdata.clear();
int width = map->getWidth();
int height = map->getHeight();
Block block(projectConfig.getNewMapMetatileId(), 0, projectConfig.getNewMapElevation());
Block block(projectConfig.getDefaultMetatileId(), projectConfig.getDefaultCollision(), projectConfig.getDefaultElevation());
for (int i = 0; i < width * height; i++) {
map->layout->blockdata.append(block);
}
@ -1506,16 +1507,22 @@ bool Project::readTilesetMetatileLabels() {
QString metatileLabelsFilename = projectConfig.getFilePath(ProjectFilePath::constants_metatile_labels);
fileWatcher.addPath(root + "/" + metatileLabelsFilename);
QMap<QString, int> defines = parser.readCDefines(metatileLabelsFilename, QStringList() << "METATILE_");
const QStringList prefixes = {QString("\\b%1").arg(projectConfig.getIdentifier(ProjectIdentifier::define_metatile_label_prefix))};
QMap<QString, int> defines = parser.readCDefinesByPrefix(metatileLabelsFilename, prefixes);
for (QString label : defines.keys()) {
uint32_t metatileId = static_cast<uint32_t>(defines[label]);
if (metatileId > Block::maxValue) {
metatileId &= Block::maxValue;
logWarn(QString("Value of metatile label '%1' truncated to %2").arg(label).arg(Metatile::getMetatileIdString(metatileId)));
}
QString tilesetName = findMetatileLabelsTileset(label);
if (!tilesetName.isEmpty()) {
metatileLabelsMap[tilesetName][label] = defines[label];
metatileLabelsMap[tilesetName][label] = metatileId;
} else {
// This #define name does not match any existing tileset.
// Save it separately to be outputted later.
unusedMetatileLabels[label] = defines[label];
unusedMetatileLabels[label] = metatileId;
}
}
@ -1527,7 +1534,7 @@ void Project::loadTilesetMetatileLabels(Tileset* tileset) {
// Reverse map for faster lookup by metatile id
for (QString labelName : metatileLabelsMap[tileset->name].keys()) {
int metatileId = metatileLabelsMap[tileset->name][labelName];
auto metatileId = metatileLabelsMap[tileset->name][labelName];
tileset->metatileLabels[metatileId] = labelName.replace(metatileLabelPrefix, "");
}
}
@ -1719,8 +1726,9 @@ bool Project::readMapGroups() {
}
}
mapConstantsToMapNames.insert(DYNAMIC_MAP_CONSTANT, DYNAMIC_MAP_NAME);
mapNamesToMapConstants.insert(DYNAMIC_MAP_NAME, DYNAMIC_MAP_CONSTANT);
const QString defineName = this->getDynamicMapDefineName();
mapConstantsToMapNames.insert(defineName, DYNAMIC_MAP_NAME);
mapNamesToMapConstants.insert(DYNAMIC_MAP_NAME, defineName);
maps.append(DYNAMIC_MAP_NAME);
groupNames = groups;
@ -1768,15 +1776,6 @@ QString Project::getNewMapName() {
return newMapName;
}
QStringList Project::getVisibilities() {
// TODO
QStringList names;
for (int i = 0; i < 16; i++) {
names.append(QString("%1").arg(i));
}
return names;
}
Project::DataQualifiers Project::getDataQualifiers(QString text, QString label) {
Project::DataQualifiers qualifiers;
@ -1871,71 +1870,40 @@ bool Project::readTilesetLabels() {
return success;
}
bool Project::readTilesetProperties() {
QStringList definePrefixes;
definePrefixes << "\\bNUM_";
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_fieldmap);
bool Project::readFieldmapProperties() {
const QString numTilesPrimaryName = projectConfig.getIdentifier(ProjectIdentifier::define_tiles_primary);
const QString numTilesTotalName = projectConfig.getIdentifier(ProjectIdentifier::define_tiles_total);
const QString numMetatilesPrimaryName = projectConfig.getIdentifier(ProjectIdentifier::define_metatiles_primary);
const QString numPalsPrimaryName = projectConfig.getIdentifier(ProjectIdentifier::define_pals_primary);
const QString numPalsTotalName = projectConfig.getIdentifier(ProjectIdentifier::define_pals_total);
const QString maxMapSizeName = projectConfig.getIdentifier(ProjectIdentifier::define_map_size);
const QStringList names = {
numTilesPrimaryName,
numTilesTotalName,
numMetatilesPrimaryName,
numPalsPrimaryName,
numPalsTotalName,
maxMapSizeName,
};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_fieldmap);
fileWatcher.addPath(root + "/" + filename);
QMap<QString, int> defines = parser.readCDefines(filename, definePrefixes);
const QMap<QString, int> defines = parser.readCDefinesByName(filename, names);
auto it = defines.find("NUM_TILES_IN_PRIMARY");
if (it != defines.end()) {
Project::num_tiles_primary = it.value();
}
else {
logWarn(QString("Value for tileset property 'NUM_TILES_IN_PRIMARY' not found. Using default (%1) instead.")
.arg(Project::num_tiles_primary));
}
it = defines.find("NUM_TILES_TOTAL");
if (it != defines.end()) {
Project::num_tiles_total = it.value();
}
else {
logWarn(QString("Value for tileset property 'NUM_TILES_TOTAL' not found. Using default (%1) instead.")
.arg(Project::num_tiles_total));
}
it = defines.find("NUM_METATILES_IN_PRIMARY");
if (it != defines.end()) {
Project::num_metatiles_primary = it.value();
}
else {
logWarn(QString("Value for tileset property 'NUM_METATILES_IN_PRIMARY' not found. Using default (%1) instead.")
.arg(Project::num_metatiles_primary));
}
it = defines.find("NUM_METATILES_TOTAL");
if (it != defines.end()) {
Project::num_metatiles_total = it.value();
}
else {
logWarn(QString("Value for tileset property 'NUM_METATILES_TOTAL' not found. Using default (%1) instead.")
.arg(Project::num_metatiles_total));
}
it = defines.find("NUM_PALS_IN_PRIMARY");
if (it != defines.end()) {
Project::num_pals_primary = it.value();
}
else {
logWarn(QString("Value for tileset property 'NUM_PALS_IN_PRIMARY' not found. Using default (%1) instead.")
.arg(Project::num_pals_primary));
}
it = defines.find("NUM_PALS_TOTAL");
if (it != defines.end()) {
Project::num_pals_total = it.value();
}
else {
logWarn(QString("Value for tileset property 'NUM_PALS_TOTAL' not found. Using default (%1) instead.")
.arg(Project::num_pals_total));
}
return true;
}
auto loadDefine = [defines](const QString name, int * dest) {
auto it = defines.find(name);
if (it != defines.end()) {
*dest = it.value();
} else {
logWarn(QString("Value for tileset property '%1' not found. Using default (%2) instead.").arg(name).arg(*dest));
}
};
loadDefine(numTilesPrimaryName, &Project::num_tiles_primary);
loadDefine(numTilesTotalName, &Project::num_tiles_total);
loadDefine(numMetatilesPrimaryName, &Project::num_metatiles_primary);
loadDefine(numPalsPrimaryName, &Project::num_pals_primary);
loadDefine(numPalsTotalName, &Project::num_pals_total);
bool Project::readMaxMapDataSize() {
QStringList definePrefixes;
definePrefixes << "\\bMAX_";
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_fieldmap); // already in fileWatcher from readTilesetProperties
QMap<QString, int> defines = parser.readCDefines(filename, definePrefixes);
auto it = defines.find("MAX_MAP_DATA_SIZE");
auto it = defines.find(maxMapSizeName);
if (it != defines.end()) {
int min = getMapDataSize(1, 1);
if (it.value() >= min) {
@ -1943,16 +1911,111 @@ bool Project::readMaxMapDataSize() {
calculateDefaultMapSize();
} else {
// must be large enough to support a 1x1 map
logWarn(QString("Value for map property 'MAX_MAP_DATA_SIZE' is %1, must be at least %2. Using default (%3) instead.")
logWarn(QString("Value for map property '%1' is %2, must be at least %3. Using default (%4) instead.")
.arg(maxMapSizeName)
.arg(it.value())
.arg(min)
.arg(Project::max_map_data_size));
}
}
else {
logWarn(QString("Value for map property 'MAX_MAP_DATA_SIZE' not found. Using default (%1) instead.")
logWarn(QString("Value for map property '%1' not found. Using default (%2) instead.")
.arg(maxMapSizeName)
.arg(Project::max_map_data_size));
}
return true;
}
// Read data masks for Blocks and metatile attributes.
bool Project::readFieldmapMasks() {
const QString metatileIdMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_metatile);
const QString collisionMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_collision);
const QString elevationMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_elevation);
const QString behaviorMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_behavior);
const QString layerTypeMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_layer);
const QStringList searchNames = {
metatileIdMaskName,
collisionMaskName,
elevationMaskName,
behaviorMaskName,
layerTypeMaskName,
};
QString globalFieldmap = projectConfig.getFilePath(ProjectFilePath::global_fieldmap);
fileWatcher.addPath(root + "/" + globalFieldmap);
QMap<QString, int> defines = parser.readCDefinesByName(globalFieldmap, searchNames);
// These mask values are accessible via the settings editor for users who don't have these defines.
// If users do have the defines we disable them in the settings editor and direct them to their project files.
// Record the names we read so we know later which settings to disable.
const QStringList defineNames = defines.keys();
this->disabledSettingsNames = QSet<QString>(defineNames.constBegin(), defineNames.constEnd());
// Avoid repeatedly writing the config file
projectConfig.setSaveDisabled(true);
// Read Block masks
auto it = defines.find(metatileIdMaskName);
if (it != defines.end())
projectConfig.setBlockMetatileIdMask(static_cast<uint16_t>(it.value()));
it = defines.find(collisionMaskName);
if (it != defines.end())
projectConfig.setBlockCollisionMask(static_cast<uint16_t>(it.value()));
it = defines.find(elevationMaskName);
if (it != defines.end())
projectConfig.setBlockElevationMask(static_cast<uint16_t>(it.value()));
// Read RSE metatile attribute masks
it = defines.find(behaviorMaskName);
if (it != defines.end())
projectConfig.setMetatileBehaviorMask(static_cast<uint32_t>(it.value()));
it = defines.find(layerTypeMaskName);
if (it != defines.end())
projectConfig.setMetatileLayerTypeMask(static_cast<uint32_t>(it.value()));
// pokefirered keeps its attribute masks in a separate table, parse this too.
const QString attrTableName = projectConfig.getIdentifier(ProjectIdentifier::symbol_attribute_table);
const QString srcFieldmap = projectConfig.getFilePath(ProjectFilePath::fieldmap);
const QMap<QString, QString> attrTable = parser.readNamedIndexCArray(srcFieldmap, attrTableName);
if (!attrTable.isEmpty()) {
const QString behaviorTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_behavior);
const QString layerTypeTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_layer);
const QString encounterTypeTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_encounter);
const QString terrainTypeTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_terrain);
fileWatcher.addPath(root + "/" + srcFieldmap);
bool ok;
// Read terrain type mask
uint32_t mask = attrTable.value(terrainTypeTableName).toUInt(&ok, 0);
if (ok) {
projectConfig.setMetatileTerrainTypeMask(mask);
this->disabledSettingsNames.insert(terrainTypeTableName);
}
// Read encounter type mask
mask = attrTable.value(encounterTypeTableName).toUInt(&ok, 0);
if (ok) {
projectConfig.setMetatileEncounterTypeMask(mask);
this->disabledSettingsNames.insert(encounterTypeTableName);
}
// If we haven't already parsed behavior and layer type then try those too
if (!this->disabledSettingsNames.contains(behaviorMaskName)) {
// Read behavior mask
mask = attrTable.value(behaviorTableName).toUInt(&ok, 0);
if (ok) {
projectConfig.setMetatileBehaviorMask(mask);
this->disabledSettingsNames.insert(behaviorTableName);
}
}
if (!this->disabledSettingsNames.contains(layerTypeMaskName)) {
// Read layer type mask
mask = attrTable.value(layerTypeTableName).toUInt(&ok, 0);
if (ok) {
projectConfig.setMetatileLayerTypeMask(mask);
this->disabledSettingsNames.insert(layerTypeTableName);
}
}
}
projectConfig.setSaveDisabled(false);
return true;
}
@ -1960,10 +2023,10 @@ bool Project::readRegionMapSections() {
this->mapSectionNameToValue.clear();
this->mapSectionValueToName.clear();
QStringList prefixes = (QStringList() << "\\bMAPSEC_");
const QStringList prefixes = {QString("\\b%1").arg(projectConfig.getIdentifier(ProjectIdentifier::define_map_section_prefix))};
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_region_map_sections);
fileWatcher.addPath(root + "/" + filename);
this->mapSectionNameToValue = parser.readCDefines(filename, prefixes);
this->mapSectionNameToValue = parser.readCDefinesByPrefix(filename, prefixes);
if (this->mapSectionNameToValue.isEmpty()) {
logError(QString("Failed to read region map sections from %1.").arg(filename));
return false;
@ -1978,14 +2041,18 @@ bool Project::readRegionMapSections() {
// Read the constants to preserve any "unused" heal locations when writing the file later
bool Project::readHealLocationConstants() {
this->healLocationNameToValue.clear();
QStringList prefixes{ "\\bSPAWN_", "\\bHEAL_LOCATION_" };
const QStringList prefixes = {
QString("\\b%1").arg(projectConfig.getIdentifier(ProjectIdentifier::define_heal_locations_prefix)),
QString("\\b%1").arg(projectConfig.getIdentifier(ProjectIdentifier::define_spawn_prefix))
};
QString constantsFilename = projectConfig.getFilePath(ProjectFilePath::constants_heal_locations);
fileWatcher.addPath(root + "/" + constantsFilename);
this->healLocationNameToValue = parser.readCDefines(constantsFilename, prefixes);
this->healLocationNameToValue = parser.readCDefinesByPrefix(constantsFilename, prefixes);
// No need to check if empty, not finding any heal location constants is ok
return true;
}
// TODO: Simplify using the new C struct parsing functions (and indexed array parsing functions)
bool Project::readHealLocations() {
this->healLocationDataQualifiers = {};
this->healLocations.clear();
@ -2004,11 +2071,12 @@ bool Project::readHealLocations() {
bool respawnEnabled = projectConfig.getHealLocationRespawnDataEnabled();
// Get data qualifiers for the location data table
QString tableName = respawnEnabled ? "sSpawnPoints" : "sHealLocations";
this->healLocationDataQualifiers = this->getDataQualifiers(text, tableName);
this->healLocationDataQualifiers = this->getDataQualifiers(text, this->getHealLocationsTableName());
// Create regex pattern for the constants (ex: "SPAWN_PALLET_TOWN" or "HEAL_LOCATION_PETALBURG_CITY")
static const QRegularExpression constantsExpr("(SPAWN|HEAL_LOCATION)_[A-Za-z0-9_]+");
const QString spawnPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_spawn_prefix);
const QString healLocPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_heal_locations_prefix);
const QRegularExpression constantsExpr(QString("\\b(%1|%2)[A-Za-z0-9_]+").arg(spawnPrefix).arg(healLocPrefix));
// Find all the unique heal location constants used in the data tables.
// Porymap doesn't care whether or not a constant appeared in the heal locations constants file.
@ -2068,10 +2136,10 @@ bool Project::readHealLocations() {
}
bool Project::readItemNames() {
QStringList prefixes("\\bITEM_(?!(B_)?USE_)"); // Exclude ITEM_USE_ and ITEM_B_USE_ constants
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_items);
const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_items)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_items);
fileWatcher.addPath(root + "/" + filename);
itemNames = parser.readCDefinesSorted(filename, prefixes);
itemNames = parser.readCDefineNames(filename, prefixes);
if (itemNames.isEmpty()) {
logError(QString("Failed to read item constants from %1").arg(filename));
return false;
@ -2080,28 +2148,22 @@ bool Project::readItemNames() {
}
bool Project::readFlagNames() {
// First read MAX_TRAINERS_COUNT, used to skip over trainer flags
// If this fails flags may simply be out of order, no need to check for success
QString opponentsFilename = projectConfig.getFilePath(ProjectFilePath::constants_opponents);
fileWatcher.addPath(root + "/" + opponentsFilename);
QMap<QString, int> maxTrainers = parser.readCDefines(opponentsFilename, QStringList() << "\\bMAX_");
// Parse flags
QStringList prefixes("\\bFLAG_");
QString flagsFilename = projectConfig.getFilePath(ProjectFilePath::constants_flags);
fileWatcher.addPath(root + "/" + flagsFilename);
flagNames = parser.readCDefinesSorted(flagsFilename, prefixes, maxTrainers);
const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_flags)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_flags);
fileWatcher.addPath(root + "/" + filename);
flagNames = parser.readCDefineNames(filename, prefixes);
if (flagNames.isEmpty()) {
logError(QString("Failed to read flag constants from %1").arg(flagsFilename));
logError(QString("Failed to read flag constants from %1").arg(filename));
return false;
}
return true;
}
bool Project::readVarNames() {
QStringList prefixes("\\bVAR_");
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_vars);
const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_vars)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_vars);
fileWatcher.addPath(root + "/" + filename);
varNames = parser.readCDefinesSorted(filename, prefixes);
varNames = parser.readCDefineNames(filename, prefixes);
if (varNames.isEmpty()) {
logError(QString("Failed to read var constants from %1").arg(filename));
return false;
@ -2110,10 +2172,10 @@ bool Project::readVarNames() {
}
bool Project::readMovementTypes() {
QStringList prefixes("\\bMOVEMENT_TYPE_");
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_obj_event_movement);
const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_movement_types)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_obj_event_movement);
fileWatcher.addPath(root + "/" + filename);
movementTypes = parser.readCDefinesSorted(filename, prefixes);
movementTypes = parser.readCDefineNames(filename, prefixes);
if (movementTypes.isEmpty()) {
logError(QString("Failed to read movement type constants from %1").arg(filename));
return false;
@ -2124,7 +2186,7 @@ bool Project::readMovementTypes() {
bool Project::readInitialFacingDirections() {
QString filename = projectConfig.getFilePath(ProjectFilePath::initial_facing_table);
fileWatcher.addPath(root + "/" + filename);
facingDirections = parser.readNamedIndexCArray(filename, "gInitialMovementTypeFacingDirections");
facingDirections = parser.readNamedIndexCArray(filename, projectConfig.getIdentifier(ProjectIdentifier::symbol_facing_directions));
if (facingDirections.isEmpty()) {
logError(QString("Failed to read initial movement type facing directions from %1").arg(filename));
return false;
@ -2133,10 +2195,10 @@ bool Project::readInitialFacingDirections() {
}
bool Project::readMapTypes() {
QStringList prefixes("\\bMAP_TYPE_");
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_map_types);
const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_map_types)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_map_types);
fileWatcher.addPath(root + "/" + filename);
mapTypes = parser.readCDefinesSorted(filename, prefixes);
mapTypes = parser.readCDefineNames(filename, prefixes);
if (mapTypes.isEmpty()) {
logError(QString("Failed to read map type constants from %1").arg(filename));
return false;
@ -2145,10 +2207,10 @@ bool Project::readMapTypes() {
}
bool Project::readMapBattleScenes() {
QStringList prefixes("\\bMAP_BATTLE_SCENE_");
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_map_types);
const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_battle_scenes)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_map_types);
fileWatcher.addPath(root + "/" + filename);
mapBattleScenes = parser.readCDefinesSorted(filename, prefixes);
mapBattleScenes = parser.readCDefineNames(filename, prefixes);
if (mapBattleScenes.isEmpty()) {
logError(QString("Failed to read map battle scene constants from %1").arg(filename));
return false;
@ -2157,10 +2219,10 @@ bool Project::readMapBattleScenes() {
}
bool Project::readWeatherNames() {
QStringList prefixes("\\bWEATHER_");
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_weather);
const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_weather)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_weather);
fileWatcher.addPath(root + "/" + filename);
weatherNames = parser.readCDefinesSorted(filename, prefixes);
weatherNames = parser.readCDefineNames(filename, prefixes);
if (weatherNames.isEmpty()) {
logError(QString("Failed to read weather constants from %1").arg(filename));
return false;
@ -2172,10 +2234,10 @@ bool Project::readCoordEventWeatherNames() {
if (!projectConfig.getEventWeatherTriggerEnabled())
return true;
QStringList prefixes("\\bCOORD_EVENT_WEATHER_");
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_weather);
const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_coord_event_weather)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_weather);
fileWatcher.addPath(root + "/" + filename);
coordEventWeatherNames = parser.readCDefinesSorted(filename, prefixes);
coordEventWeatherNames = parser.readCDefineNames(filename, prefixes);
if (coordEventWeatherNames.isEmpty()) {
logWarn(QString("Failed to read coord event weather constants from %1. Disabling Weather Trigger events.").arg(filename));
projectConfig.setEventWeatherTriggerEnabled(false);
@ -2187,10 +2249,10 @@ bool Project::readSecretBaseIds() {
if (!projectConfig.getEventSecretBaseEnabled())
return true;
QStringList prefixes("\\bSECRET_BASE_[A-Za-z0-9_]*_[0-9]+");
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_secret_bases);
const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_secret_bases)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_secret_bases);
fileWatcher.addPath(root + "/" + filename);
secretBaseIds = parser.readCDefinesSorted(filename, prefixes);
secretBaseIds = parser.readCDefineNames(filename, prefixes);
if (secretBaseIds.isEmpty()) {
logWarn(QString("Failed to read secret base id constants from '%1'. Disabling Secret Base events.").arg(filename));
projectConfig.setEventSecretBaseEnabled(false);
@ -2199,10 +2261,10 @@ bool Project::readSecretBaseIds() {
}
bool Project::readBgEventFacingDirections() {
QStringList prefixes("\\bBG_EVENT_PLAYER_FACING_");
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_event_bg);
const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_sign_facing_directions)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_event_bg);
fileWatcher.addPath(root + "/" + filename);
bgEventFacingDirections = parser.readCDefinesSorted(filename, prefixes);
bgEventFacingDirections = parser.readCDefineNames(filename, prefixes);
if (bgEventFacingDirections.isEmpty()) {
logError(QString("Failed to read bg event facing direction constants from %1").arg(filename));
return false;
@ -2211,10 +2273,10 @@ bool Project::readBgEventFacingDirections() {
}
bool Project::readTrainerTypes() {
QStringList prefixes("\\bTRAINER_TYPE_");
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_trainer_types);
const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_trainer_types)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_trainer_types);
fileWatcher.addPath(root + "/" + filename);
trainerTypes = parser.readCDefinesSorted(filename, prefixes);
trainerTypes = parser.readCDefineNames(filename, prefixes);
if (trainerTypes.isEmpty()) {
logError(QString("Failed to read trainer type constants from %1").arg(filename));
return false;
@ -2226,40 +2288,47 @@ bool Project::readMetatileBehaviors() {
this->metatileBehaviorMap.clear();
this->metatileBehaviorMapInverse.clear();
QStringList prefixes("\\bMB_");
const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_behaviors)};
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_metatile_behaviors);
fileWatcher.addPath(root + "/" + filename);
this->metatileBehaviorMap = parser.readCDefines(filename, prefixes);
if (this->metatileBehaviorMap.isEmpty()) {
logError(QString("Failed to read metatile behaviors from %1.").arg(filename));
return false;
QMap<QString, int> defines = parser.readCDefinesByPrefix(filename, prefixes);
if (defines.isEmpty()) {
// Not having any metatile behavior names is ok (their values will be displayed instead).
// If the user's metatiles can have nonzero values then warn them, as they likely want names.
if (projectConfig.getMetatileBehaviorMask())
logWarn(QString("Failed to read metatile behaviors from %1.").arg(filename));
return true;
}
for (QString defineName : this->metatileBehaviorMap.keys()) {
this->metatileBehaviorMapInverse.insert(this->metatileBehaviorMap[defineName], defineName);
for (auto i = defines.cbegin(), end = defines.cend(); i != end; i++) {
uint32_t value = static_cast<uint32_t>(i.value());
this->metatileBehaviorMap.insert(i.key(), value);
this->metatileBehaviorMapInverse.insert(value, i.key());
}
return true;
}
bool Project::readSongNames() {
QStringList songDefinePrefixes{ "\\bSE_", "\\bMUS_" };
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_songs);
const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_music)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_songs);
fileWatcher.addPath(root + "/" + filename);
QMap<QString, int> songDefines = parser.readCDefines(filename, songDefinePrefixes);
this->songNames = songDefines.keys();
this->defaultSong = this->songNames.value(0, "MUS_DUMMY");
this->songNames = parser.readCDefineNames(filename, prefixes);
if (this->songNames.isEmpty()) {
logError(QString("Failed to read song names from %1.").arg(filename));
return false;
}
this->defaultSong = this->songNames.value(0);
// Song names don't have a very useful order (esp. if we include SE_* values), so sort them alphabetically.
this->songNames.sort();
return true;
}
bool Project::readObjEventGfxConstants() {
QStringList objEventGfxPrefixes("\\bOBJ_EVENT_GFX_");
const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_obj_event_gfx)};
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_obj_events);
fileWatcher.addPath(root + "/" + filename);
this->gfxDefines = parser.readCDefines(filename, objEventGfxPrefixes);
this->gfxDefines = parser.readCDefinesByPrefix(filename, prefixes);
if (this->gfxDefines.isEmpty()) {
logError(QString("Failed to read object event graphics constants from %1.").arg(filename));
return false;
@ -2270,30 +2339,34 @@ bool Project::readObjEventGfxConstants() {
bool Project::readMiscellaneousConstants() {
miscConstants.clear();
if (userConfig.getEncounterJsonActive()) {
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_pokemon);
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_pokemon);
const QString minLevelName = projectConfig.getIdentifier(ProjectIdentifier::define_min_level);
const QString maxLevelName = projectConfig.getIdentifier(ProjectIdentifier::define_max_level);
fileWatcher.addPath(root + "/" + filename);
QMap<QString, int> pokemonDefines = parser.readCDefines(filename, { "MIN_", "MAX_" });
miscConstants.insert("max_level_define", pokemonDefines.value("MAX_LEVEL") > pokemonDefines.value("MIN_LEVEL") ? pokemonDefines.value("MAX_LEVEL") : 100);
miscConstants.insert("min_level_define", pokemonDefines.value("MIN_LEVEL") < pokemonDefines.value("MAX_LEVEL") ? pokemonDefines.value("MIN_LEVEL") : 1);
QMap<QString, int> pokemonDefines = parser.readCDefinesByName(filename, {minLevelName, maxLevelName});
miscConstants.insert("max_level_define", pokemonDefines.value(maxLevelName) > pokemonDefines.value(minLevelName) ? pokemonDefines.value(maxLevelName) : 100);
miscConstants.insert("min_level_define", pokemonDefines.value(minLevelName) < pokemonDefines.value(maxLevelName) ? pokemonDefines.value(minLevelName) : 1);
}
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_global);
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_global);
const QString maxObjectEventsName = projectConfig.getIdentifier(ProjectIdentifier::define_obj_event_count);
fileWatcher.addPath(root + "/" + filename);
QStringList definePrefixes("\\bOBJECT_");
QMap<QString, int> defines = parser.readCDefines(filename, definePrefixes);
QMap<QString, int> defines = parser.readCDefinesByName(filename, {maxObjectEventsName});
auto it = defines.find("OBJECT_EVENT_TEMPLATES_COUNT");
auto it = defines.find(maxObjectEventsName);
if (it != defines.end()) {
if (it.value() > 0) {
Project::max_object_events = it.value();
} else {
logWarn(QString("Value for 'OBJECT_EVENT_TEMPLATES_COUNT' is %1, must be greater than 0. Using default (%2) instead.")
logWarn(QString("Value for '%1' is %2, must be greater than 0. Using default (%3) instead.")
.arg(maxObjectEventsName)
.arg(it.value())
.arg(Project::max_object_events));
}
}
else {
logWarn(QString("Value for 'OBJECT_EVENT_TEMPLATES_COUNT' not found. Using default (%1) instead.")
logWarn(QString("Value for '%1' not found. Using default (%2) instead.")
.arg(maxObjectEventsName)
.arg(Project::max_object_events));
}
@ -2400,7 +2473,9 @@ bool Project::readEventGraphics() {
<< root + "/" + projectConfig.getFilePath(ProjectFilePath::data_obj_event_pic_tables)
<< root + "/" + projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx));
QMap<QString, QString> pointerHash = parser.readNamedIndexCArray(projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx_pointers), "gObjectEventGraphicsInfoPointers");
const QString pointersFilepath = projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx_pointers);
const QString pointersName = projectConfig.getIdentifier(ProjectIdentifier::symbol_obj_event_gfx_pointers);
QMap<QString, QString> pointerHash = parser.readNamedIndexCArray(pointersFilepath, pointersName);
qDeleteAll(eventGraphicsMap);
eventGraphicsMap.clear();
@ -2408,7 +2483,7 @@ bool Project::readEventGraphics() {
// The positions of each of the required members for the gfx info struct.
// For backwards compatibility if the struct doesn't use initializers.
const auto gfxInfoMemberMap = QHash<int, QString>{
static const auto gfxInfoMemberMap = QHash<int, QString>{
{8, "inanimate"},
{11, "oam"},
{12, "subspriteTables"},
@ -2470,17 +2545,90 @@ bool Project::readEventGraphics() {
}
bool Project::readSpeciesIconPaths() {
speciesToIconPath.clear();
QString srcfilename = projectConfig.getFilePath(ProjectFilePath::pokemon_icon_table);
QString incfilename = projectConfig.getFilePath(ProjectFilePath::data_pokemon_gfx);
this->speciesToIconPath.clear();
// Read map of species constants to icon names
const QString srcfilename = projectConfig.getFilePath(ProjectFilePath::pokemon_icon_table);
fileWatcher.addPath(root + "/" + srcfilename);
const QString tableName = projectConfig.getIdentifier(ProjectIdentifier::symbol_pokemon_icon_table);
const QMap<QString, QString> monIconNames = parser.readNamedIndexCArray(srcfilename, tableName);
// Read map of icon names to filepaths
const QString incfilename = projectConfig.getFilePath(ProjectFilePath::data_pokemon_gfx);
fileWatcher.addPath(root + "/" + incfilename);
QMap<QString, QString> monIconNames = parser.readNamedIndexCArray(srcfilename, "gMonIconTable");
QMap<QString, QString> iconIncbins = parser.readCIncbinMulti(incfilename);
for (QString species : monIconNames.keys()) {
QString path = iconIncbins[monIconNames.value(species)];
speciesToIconPath.insert(species, root + "/" + path.replace("4bpp", "png"));
const QMap<QString, QString> iconIncbins = parser.readCIncbinMulti(incfilename);
// Read species constants. If this fails we can get them from the icon table (but we shouldn't rely on it).
const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_species)};
const QString constantsFilename = projectConfig.getFilePath(ProjectFilePath::constants_species);
fileWatcher.addPath(root + "/" + constantsFilename);
QStringList speciesNames = parser.readCDefineNames(constantsFilename, prefixes);
if (speciesNames.isEmpty())
speciesNames = monIconNames.keys();
// For each species, use the information gathered above to find the icon image.
bool missingIcons = false;
for (auto species : speciesNames) {
QString path = QString();
if (monIconNames.contains(species) && iconIncbins.contains(monIconNames.value(species))) {
// We have the icon filepath from the icon table
path = QString("%1/%2").arg(root).arg(this->fixGraphicPath(iconIncbins[monIconNames.value(species)]));
} else {
// Failed to read icon filepath from the icon table, check filepaths where icons are normally located.
// Try to use the icon name (if we have it) to determine the directory, then try the species name.
// The name permuting is overkill, but it's making up for some of the fragility in the way we find icon paths.
QStringList possibleDirNames;
if (monIconNames.contains(species)) {
// Ex: For 'gMonIcon_QuestionMark' try 'question_mark'
static const QRegularExpression re("([a-z])([A-Z0-9])");
QString iconName = monIconNames.value(species);
iconName = iconName.mid(iconName.indexOf("_") + 1); // jump past prefix ('gMonIcon')
possibleDirNames.append(iconName.replace(re, "\\1_\\2").toLower());
}
// Ex: For 'SPECIES_FOO_BAR_BAZ' try 'foo_bar_baz'
possibleDirNames.append(species.mid(8).toLower());
// Permute paths with underscores.
// Ex: Try 'foo_bar/baz', 'foo/bar_baz', 'foobarbaz', 'foo_bar', and 'foo'
QStringList permutedNames;
for (auto dir : possibleDirNames) {
if (!dir.contains("_")) continue;
for (int i = dir.indexOf("_"); i > -1; i = dir.indexOf("_", i + 1)) {
QString temp = dir;
permutedNames.prepend(temp.replace(i, 1, "/"));
permutedNames.append(dir.left(i)); // Prepend the others so the most generic name ('foo') ends up last
}
permutedNames.prepend(dir.remove("_"));
}
possibleDirNames.append(permutedNames);
possibleDirNames.removeDuplicates();
for (auto dir : possibleDirNames) {
if (dir.isEmpty()) continue;
const QString stdPath = QString("%1/%2%3/icon.png")
.arg(root)
.arg(projectConfig.getFilePath(ProjectFilePath::pokemon_gfx))
.arg(dir);
if (QFile::exists(stdPath)) {
// Icon found at a normal filepath
path = stdPath;
break;
}
}
if (path.isEmpty() && projectConfig.getPokemonIconPath(species).isEmpty()) {
// Failed to find icon, this species will use a placeholder icon.
logWarn(QString("Failed to find Pokémon icon for '%1'").arg(species));
missingIcons = true;
}
}
this->speciesToIconPath.insert(species, path);
}
// Logging this alongside every warning (if there are multiple) is obnoxious, just do it once at the end.
if (missingIcons) logInfo("Pokémon icon filepaths can be specified under 'Options->Project Settings'");
return true;
}
@ -2509,7 +2657,7 @@ int Project::getNumMetatilesPrimary()
int Project::getNumMetatilesTotal()
{
return Project::num_metatiles_total;
return Block::getMaxMetatileId() + 1;
}
int Project::getNumPalettesPrimary()
@ -2563,7 +2711,8 @@ bool Project::calculateDefaultMapSize(){
// x^2 + 29x + (210 - max), then complete the square and simplify
default_map_size = qFloor((qSqrt(4 * getMaxMapDataSize() + 1) - 29) / 2);
} else {
logError(QString("'MAX_MAP_DATA_SIZE' of %1 is too small to support a 1x1 map. Must be at least %2.")
logError(QString("'%1' of %2 is too small to support a 1x1 map. Must be at least %3.")
.arg(projectConfig.getIdentifier(ProjectIdentifier::define_map_size))
.arg(max)
.arg(getMapDataSize(1, 1)));
return false;
@ -2576,7 +2725,41 @@ int Project::getMaxObjectEvents()
return Project::max_object_events;
}
QString Project::getDynamicMapDefineName() {
const QString prefix = projectConfig.getIdentifier(ProjectIdentifier::define_map_prefix);
return prefix + projectConfig.getIdentifier(ProjectIdentifier::define_map_dynamic);
}
void Project::setImportExportPath(QString filename)
{
this->importExportPath = QFileInfo(filename).absolutePath();
}
// The values of some config fields can limit the values of other config fields
// (for example, metatile attributes size limits the metatile attribute masks).
// Others depend on information in the project (for example the default metatile ID
// can be limited by fieldmap defines)
// Once we've read data from the project files we can adjust these accordingly.
void Project::applyParsedLimits() {
// Avoid repeatedly writing the config file
projectConfig.setSaveDisabled(true);
uint32_t maxMask = Metatile::getMaxAttributesMask();
projectConfig.setMetatileBehaviorMask(projectConfig.getMetatileBehaviorMask() & maxMask);
projectConfig.setMetatileTerrainTypeMask(projectConfig.getMetatileTerrainTypeMask() & maxMask);
projectConfig.setMetatileEncounterTypeMask(projectConfig.getMetatileEncounterTypeMask() & maxMask);
projectConfig.setMetatileLayerTypeMask(projectConfig.getMetatileLayerTypeMask() & maxMask);
Block::setLayout();
Metatile::setLayout(this);
Project::num_metatiles_primary = qMin(Project::num_metatiles_primary, Block::getMaxMetatileId() + 1);
projectConfig.setDefaultMetatileId(qMin(projectConfig.getDefaultMetatileId(), Block::getMaxMetatileId()));
projectConfig.setDefaultElevation(qMin(projectConfig.getDefaultElevation(), Block::getMaxElevation()));
projectConfig.setDefaultCollision(qMin(projectConfig.getDefaultCollision(), Block::getMaxCollision()));
projectConfig.setCollisionSheetHeight(qMin(projectConfig.getCollisionSheetHeight(), Block::getMaxElevation() + 1));
projectConfig.setCollisionSheetWidth(qMin(projectConfig.getCollisionSheetWidth(), Block::getMaxCollision() + 1));
projectConfig.setSaveDisabled(false);
projectConfig.save();
}

View File

@ -99,7 +99,7 @@ int MainWindow::getMetatileId(int x, int y) {
if (!this->editor->map->getBlock(x, y, &block)) {
return 0;
}
return block.metatileId;
return block.metatileId();
}
void MainWindow::setMetatileId(int x, int y, int metatileId, bool forceRedraw, bool commitChanges) {
@ -109,7 +109,7 @@ void MainWindow::setMetatileId(int x, int y, int metatileId, bool forceRedraw, b
if (!this->editor->map->getBlock(x, y, &block)) {
return;
}
this->editor->map->setBlock(x, y, Block(metatileId, block.collision, block.elevation));
this->editor->map->setBlock(x, y, Block(metatileId, block.collision(), block.elevation()));
this->tryCommitMapChanges(commitChanges);
this->tryRedrawMapArea(forceRedraw);
}
@ -121,7 +121,7 @@ int MainWindow::getCollision(int x, int y) {
if (!this->editor->map->getBlock(x, y, &block)) {
return 0;
}
return block.collision;
return block.collision();
}
void MainWindow::setCollision(int x, int y, int collision, bool forceRedraw, bool commitChanges) {
@ -131,7 +131,7 @@ void MainWindow::setCollision(int x, int y, int collision, bool forceRedraw, boo
if (!this->editor->map->getBlock(x, y, &block)) {
return;
}
this->editor->map->setBlock(x, y, Block(block.metatileId, collision, block.elevation));
this->editor->map->setBlock(x, y, Block(block.metatileId(), collision, block.elevation()));
this->tryCommitMapChanges(commitChanges);
this->tryRedrawMapArea(forceRedraw);
}
@ -143,7 +143,7 @@ int MainWindow::getElevation(int x, int y) {
if (!this->editor->map->getBlock(x, y, &block)) {
return 0;
}
return block.elevation;
return block.elevation();
}
void MainWindow::setElevation(int x, int y, int elevation, bool forceRedraw, bool commitChanges) {
@ -153,7 +153,7 @@ void MainWindow::setElevation(int x, int y, int elevation, bool forceRedraw, boo
if (!this->editor->map->getBlock(x, y, &block)) {
return;
}
this->editor->map->setBlock(x, y, Block(block.metatileId, block.collision, elevation));
this->editor->map->setBlock(x, y, Block(block.metatileId(), block.collision(), elevation));
this->tryCommitMapChanges(commitChanges);
this->tryRedrawMapArea(forceRedraw);
}
@ -625,7 +625,7 @@ int MainWindow::getMetatileLayerType(int metatileId) {
Metatile * metatile = this->getMetatile(metatileId);
if (!metatile)
return -1;
return metatile->layerType;
return metatile->layerType();
}
void MainWindow::setMetatileLayerType(int metatileId, int layerType) {
@ -640,7 +640,7 @@ int MainWindow::getMetatileEncounterType(int metatileId) {
Metatile * metatile = this->getMetatile(metatileId);
if (!metatile)
return -1;
return metatile->encounterType;
return metatile->encounterType();
}
void MainWindow::setMetatileEncounterType(int metatileId, int encounterType) {
@ -655,7 +655,7 @@ int MainWindow::getMetatileTerrainType(int metatileId) {
Metatile * metatile = this->getMetatile(metatileId);
if (!metatile)
return -1;
return metatile->terrainType;
return metatile->terrainType();
}
void MainWindow::setMetatileTerrainType(int metatileId, int terrainType) {
@ -670,7 +670,7 @@ int MainWindow::getMetatileBehavior(int metatileId) {
Metatile * metatile = this->getMetatile(metatileId);
if (!metatile)
return -1;
return metatile->behavior;
return metatile->behavior();
}
void MainWindow::setMetatileBehavior(int metatileId, int behavior) {

View File

@ -312,9 +312,9 @@ void Scripting::cb_BorderVisibilityToggled(bool visible) {
QJSValue Scripting::fromBlock(Block block) {
QJSValue obj = instance->engine->newObject();
obj.setProperty("metatileId", block.metatileId);
obj.setProperty("collision", block.collision);
obj.setProperty("elevation", block.elevation);
obj.setProperty("metatileId", block.metatileId());
obj.setProperty("collision", block.collision());
obj.setProperty("elevation", block.elevation());
obj.setProperty("rawValue", block.rawValue());
return obj;
}

View File

@ -75,8 +75,8 @@ void CollisionPixmapItem::paint(QGraphicsSceneMouseEvent *event) {
Block block;
if (map->getBlock(pos.x(), pos.y(), &block)) {
block.collision = this->movementPermissionsSelector->getSelectedCollision();
block.elevation = this->movementPermissionsSelector->getSelectedElevation();
block.setCollision(this->selectedCollision->value());
block.setElevation(this->selectedElevation->value());
map->setBlock(pos.x(), pos.y(), block, true);
}
@ -93,8 +93,8 @@ void CollisionPixmapItem::floodFill(QGraphicsSceneMouseEvent *event) {
Blockdata oldCollision = map->layout->blockdata;
QPoint pos = Metatile::coordFromPixmapCoord(event->pos());
uint16_t collision = this->movementPermissionsSelector->getSelectedCollision();
uint16_t elevation = this->movementPermissionsSelector->getSelectedElevation();
uint16_t collision = this->selectedCollision->value();
uint16_t elevation = this->selectedElevation->value();
map->floodFillCollisionElevation(pos.x(), pos.y(), collision, elevation);
if (map->layout->blockdata != oldCollision) {
@ -109,8 +109,8 @@ void CollisionPixmapItem::magicFill(QGraphicsSceneMouseEvent *event) {
} else if (map) {
Blockdata oldCollision = map->layout->blockdata;
QPoint pos = Metatile::coordFromPixmapCoord(event->pos());
uint16_t collision = this->movementPermissionsSelector->getSelectedCollision();
uint16_t elevation = this->movementPermissionsSelector->getSelectedElevation();
uint16_t collision = this->selectedCollision->value();
uint16_t elevation = this->selectedElevation->value();
map->magicFillCollisionElevation(pos.x(), pos.y(), collision, elevation);
if (map->layout->blockdata != oldCollision) {
@ -121,10 +121,7 @@ void CollisionPixmapItem::magicFill(QGraphicsSceneMouseEvent *event) {
void CollisionPixmapItem::pick(QGraphicsSceneMouseEvent *event) {
QPoint pos = Metatile::coordFromPixmapCoord(event->pos());
Block block;
if (map->getBlock(pos.x(), pos.y(), &block)) {
this->movementPermissionsSelector->select(block.collision, block.elevation);
}
this->updateSelection(pos);
}
void CollisionPixmapItem::updateMovementPermissionSelection(QGraphicsSceneMouseEvent *event) {
@ -135,9 +132,14 @@ void CollisionPixmapItem::updateMovementPermissionSelection(QGraphicsSceneMouseE
if (pos.x() >= map->getWidth()) pos.setX(map->getWidth() - 1);
if (pos.y() < 0) pos.setY(0);
if (pos.y() >= map->getHeight()) pos.setY(map->getHeight() - 1);
this->updateSelection(pos);
}
void CollisionPixmapItem::updateSelection(QPoint pos) {
Block block;
if (map->getBlock(pos.x(), pos.y(), &block)) {
this->movementPermissionsSelector->select(block.collision, block.elevation);
const QSignalBlocker blocker(this->selectedCollision); // We only need a signal for changing one of them, not both
this->selectedCollision->setValue(block.collision());
this->selectedElevation->setValue(block.elevation());
}
}

View File

@ -17,6 +17,7 @@ void DraggablePixmapItem::updatePosition() {
} else {
setZValue(event->getY());
}
editor->updateWarpEventWarning(event);
}
void DraggablePixmapItem::emitPositionChanged() {
@ -93,9 +94,10 @@ void DraggablePixmapItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *) {
emit editor->warpEventDoubleClicked(clone->getTargetMap(), clone->getTargetID(), Event::Group::Object);
}
else if (eventType == Event::Type::SecretBase) {
const QString mapPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_map_prefix);
SecretBaseEvent *base = dynamic_cast<SecretBaseEvent *>(this->event);
QString baseId = base->getBaseID();
QString destMap = editor->project->mapConstantsToMapNames.value("MAP_" + baseId.left(baseId.lastIndexOf("_")));
QString destMap = editor->project->mapConstantsToMapNames.value(mapPrefix + baseId.left(baseId.lastIndexOf("_")));
emit editor->warpEventDoubleClicked(destMap, 0, Event::Group::Warp);
}
}

View File

@ -16,9 +16,24 @@ void SpeciesComboDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
QPixmap pm;
if (!QPixmapCache::find(species, &pm)) {
QImage img(this->project->speciesToIconPath.value(species));
img.setColor(0, qRgba(0, 0, 0, 0));
pm = QPixmap::fromImage(img);
// Prefer path from config. If not present, use the path parsed from project files
QString path = projectConfig.getPokemonIconPath(species);
if (path.isEmpty()) {
path = this->project->speciesToIconPath.value(species);
} else {
QFileInfo info(path);
if (info.isRelative())
path = QDir::cleanPath(projectConfig.getProjectDir() + QDir::separator() + path);
}
QImage img(path);
if (img.isNull()) {
// No icon for this species, use placeholder
pm = QPixmap(":images/pokemon_icon_placeholder.png");
} else {
img.setColor(0, qRgba(0, 0, 0, 0));
pm = QPixmap::fromImage(img);
}
QPixmapCache::insert(species, pm);
}
QPixmap monIcon = pm.copy(0, 0, 32, 32);

View File

@ -3,9 +3,6 @@
#include "editcommands.h"
#include "draggablepixmapitem.h"
#include "project.h"
#include "editor.h"
#include <limits>
using std::numeric_limits;
@ -109,7 +106,7 @@ void EventFrame::initCustomAttributesTable() {
this->layout_contents->addWidget(customAttributes);
}
void EventFrame::connectSignals() {
void EventFrame::connectSignals(MainWindow *) {
this->connected = true;
this->spinner_x->disconnect();
@ -258,10 +255,10 @@ void ObjectFrame::setup() {
EventFrame::initCustomAttributesTable();
}
void ObjectFrame::connectSignals() {
void ObjectFrame::connectSignals(MainWindow *window) {
if (this->connected) return;
EventFrame::connectSignals();
EventFrame::connectSignals(window);
// sprite update
this->combo_sprite->disconnect();
@ -418,10 +415,10 @@ void CloneObjectFrame::setup() {
EventFrame::initCustomAttributesTable();
}
void CloneObjectFrame::connectSignals() {
void CloneObjectFrame::connectSignals(MainWindow *window) {
if (this->connected) return;
EventFrame::connectSignals();
EventFrame::connectSignals(window);
// update icon displayed in frame with target
connect(this->clone->getPixmapItem(), &DraggablePixmapItem::spriteChanged, this->label_icon, &QLabel::setPixmap);
@ -472,8 +469,6 @@ void CloneObjectFrame::populate(Project *project) {
this->combo_target_map->addItems(project->mapNames);
}
void WarpFrame::setup() {
EventFrame::setup();
@ -493,14 +488,26 @@ void WarpFrame::setup() {
l_form_dest_warp->addRow("Destination Warp", this->combo_dest_warp);
this->layout_contents->addLayout(l_form_dest_warp);
// warning
static const QString warningText = "Warning:\n"
"This warp event is not positioned on a metatile with a warp behavior.\n"
"Click this warning for more details.";
QVBoxLayout *l_vbox_warning = new QVBoxLayout();
this->warning = new QPushButton(warningText, this);
this->warning->setFlat(true);
this->warning->setStyleSheet("color: red; text-align: left");
this->warning->setVisible(false);
l_vbox_warning->addWidget(this->warning);
this->layout_contents->addLayout(l_vbox_warning);
// custom attributes
EventFrame::initCustomAttributesTable();
}
void WarpFrame::connectSignals() {
void WarpFrame::connectSignals(MainWindow *window) {
if (this->connected) return;
EventFrame::connectSignals();
EventFrame::connectSignals(window);
// dest map
this->combo_dest_map->disconnect();
@ -515,6 +522,10 @@ void WarpFrame::connectSignals() {
this->warp->setDestinationWarpID(text);
this->warp->modify();
});
// warning
this->warning->disconnect();
connect(this->warning, &QPushButton::clicked, window, &MainWindow::onWarpBehaviorWarningClicked);
}
void WarpFrame::initialize() {
@ -572,10 +583,10 @@ void TriggerFrame::setup() {
EventFrame::initCustomAttributesTable();
}
void TriggerFrame::connectSignals() {
void TriggerFrame::connectSignals(MainWindow *window) {
if (this->connected) return;
EventFrame::connectSignals();
EventFrame::connectSignals(window);
// label
this->combo_script->disconnect();
@ -657,10 +668,10 @@ void WeatherTriggerFrame::setup() {
EventFrame::initCustomAttributesTable();
}
void WeatherTriggerFrame::connectSignals() {
void WeatherTriggerFrame::connectSignals(MainWindow *window) {
if (this->connected) return;
EventFrame::connectSignals();
EventFrame::connectSignals(window);
// weather
this->combo_weather->disconnect();
@ -716,10 +727,10 @@ void SignFrame::setup() {
EventFrame::initCustomAttributesTable();
}
void SignFrame::connectSignals() {
void SignFrame::connectSignals(MainWindow *window) {
if (this->connected) return;
EventFrame::connectSignals();
EventFrame::connectSignals(window);
// facing dir
this->combo_facing_dir->disconnect();
@ -818,10 +829,10 @@ void HiddenItemFrame::setup() {
EventFrame::initCustomAttributesTable();
}
void HiddenItemFrame::connectSignals() {
void HiddenItemFrame::connectSignals(MainWindow *window) {
if (this->connected) return;
EventFrame::connectSignals();
EventFrame::connectSignals(window);
// item
this->combo_item->disconnect();
@ -909,10 +920,10 @@ void SecretBaseFrame::setup() {
EventFrame::initCustomAttributesTable();
}
void SecretBaseFrame::connectSignals() {
void SecretBaseFrame::connectSignals(MainWindow *window) {
if (this->connected) return;
EventFrame::connectSignals();
EventFrame::connectSignals(window);
this->combo_base_id->disconnect();
connect(this->combo_base_id, &QComboBox::currentTextChanged, [this](const QString &text) {
@ -973,10 +984,10 @@ void HealLocationFrame::setup() {
EventFrame::initCustomAttributesTable();
}
void HealLocationFrame::connectSignals() {
void HealLocationFrame::connectSignals(MainWindow *window) {
if (this->connected) return;
EventFrame::connectSignals();
EventFrame::connectSignals(window);
if (projectConfig.getHealLocationRespawnDataEnabled()) {
this->combo_respawn_map->disconnect();

View File

@ -1,17 +1,16 @@
#include "config.h"
#include "imageproviders.h"
#include "log.h"
#include "editor.h"
#include <QPainter>
QImage getCollisionMetatileImage(Block block) {
return getCollisionMetatileImage(block.collision, block.elevation);
return getCollisionMetatileImage(block.collision(), block.elevation());
}
QImage getCollisionMetatileImage(int collision, int elevation) {
static const QImage collisionImage(":/images/collisions.png");
int x = (collision != 0) * 16;
int y = elevation * 16;
return collisionImage.copy(x, y, 16, 16);
const QImage * image = Editor::collisionIcons.at(collision).at(elevation);
return image ? *image : QImage();
}
QImage getMetatileImage(
@ -51,7 +50,7 @@ QImage getMetatileImage(
QPainter metatile_painter(&metatile_image);
bool isTripleLayerMetatile = projectConfig.getTripleLayerMetatilesEnabled();
const int numLayers = 3; // When rendering, metatiles always have 3 layers
int layerType = metatile->layerType;
uint32_t layerType = metatile->layerType();
for (int layer = 0; layer < numLayers; layer++)
for (int y = 0; y < 2; y++)
for (int x = 0; x < 2; x++) {

View File

@ -129,11 +129,11 @@ void MapPixmapItem::paintNormal(int x, int y, bool fromScriptCall) {
MetatileSelectionItem item = selection.metatileItems.at(index);
if (!item.enabled)
continue;
block.metatileId = item.metatileId;
block.setMetatileId(item.metatileId);
if (selection.hasCollision && selection.collisionItems.length() == selection.metatileItems.length()) {
CollisionSelectionItem collisionItem = selection.collisionItems.at(index);
block.collision = collisionItem.collision;
block.elevation = collisionItem.elevation;
block.setCollision(collisionItem.collision);
block.setElevation(collisionItem.elevation);
}
map->setBlock(actualX, actualY, block, !fromScriptCall);
}
@ -195,13 +195,13 @@ void MapPixmapItem::paintSmartPath(int x, int y, bool fromScriptCall) {
return;
// Shift to the middle tile of the smart path selection.
uint16_t openTile = selection.metatileItems.at(4).metatileId;
uint16_t openTileCollision = 0;
uint16_t openTileElevation = 0;
uint16_t openMetatileId = selection.metatileItems.at(4).metatileId;
uint16_t openCollision = 0;
uint16_t openElevation = 0;
bool setCollisions = false;
if (selection.hasCollision && selection.collisionItems.length() == selection.metatileItems.length()) {
openTileCollision = selection.collisionItems.at(4).collision;
openTileElevation = selection.collisionItems.at(4).elevation;
openCollision = selection.collisionItems.at(4).collision;
openElevation = selection.collisionItems.at(4).elevation;
setCollisions = true;
}
@ -217,10 +217,10 @@ void MapPixmapItem::paintSmartPath(int x, int y, bool fromScriptCall) {
int actualY = j + y;
Block block;
if (map->getBlock(actualX, actualY, &block)) {
block.metatileId = openTile;
block.setMetatileId(openMetatileId);
if (setCollisions) {
block.collision = openTileCollision;
block.elevation = openTileElevation;
block.setCollision(openCollision);
block.setElevation(openElevation);
}
map->setBlock(actualX, actualY, block, !fromScriptCall);
}
@ -240,7 +240,7 @@ void MapPixmapItem::paintSmartPath(int x, int y, bool fromScriptCall) {
int actualX = i + x;
int actualY = j + y;
Block block;
if (!map->getBlock(actualX, actualY, &block) || !isSmartPathTile(selection.metatileItems, block.metatileId)) {
if (!map->getBlock(actualX, actualY, &block) || !isSmartPathTile(selection.metatileItems, block.metatileId())) {
continue;
}
@ -251,20 +251,20 @@ void MapPixmapItem::paintSmartPath(int x, int y, bool fromScriptCall) {
Block left;
// Get marching squares value, to determine which tile to use.
if (map->getBlock(actualX, actualY - 1, &top) && isSmartPathTile(selection.metatileItems, top.metatileId))
if (map->getBlock(actualX, actualY - 1, &top) && isSmartPathTile(selection.metatileItems, top.metatileId()))
id += 1;
if (map->getBlock(actualX + 1, actualY, &right) && isSmartPathTile(selection.metatileItems, right.metatileId))
if (map->getBlock(actualX + 1, actualY, &right) && isSmartPathTile(selection.metatileItems, right.metatileId()))
id += 2;
if (map->getBlock(actualX, actualY + 1, &bottom) && isSmartPathTile(selection.metatileItems, bottom.metatileId))
if (map->getBlock(actualX, actualY + 1, &bottom) && isSmartPathTile(selection.metatileItems, bottom.metatileId()))
id += 4;
if (map->getBlock(actualX - 1, actualY, &left) && isSmartPathTile(selection.metatileItems, left.metatileId))
if (map->getBlock(actualX - 1, actualY, &left) && isSmartPathTile(selection.metatileItems, left.metatileId()))
id += 8;
block.metatileId = selection.metatileItems.at(smartPathTable[id]).metatileId;
block.setMetatileId(selection.metatileItems.at(smartPathTable[id]).metatileId);
if (setCollisions) {
CollisionSelectionItem collisionItem = selection.collisionItems.at(smartPathTable[id]);
block.collision = collisionItem.collision;
block.elevation = collisionItem.elevation;
block.setCollision(collisionItem.collision);
block.setElevation(collisionItem.elevation);
}
map->setBlock(actualX, actualY, block, !fromScriptCall);
}
@ -326,7 +326,7 @@ void MapPixmapItem::updateMetatileSelection(QGraphicsSceneMouseEvent *event) {
selection.append(QPoint(pos.x(), pos.y()));
Block block;
if (map->getBlock(pos.x(), pos.y(), &block)) {
this->metatileSelector->selectFromMap(block.metatileId, block.collision, block.elevation);
this->metatileSelector->selectFromMap(block.metatileId(), block.collision(), block.elevation());
}
} else if (event->type() == QEvent::GraphicsSceneMouseMove) {
int x1 = selection_origin.x();
@ -349,12 +349,12 @@ void MapPixmapItem::updateMetatileSelection(QGraphicsSceneMouseEvent *event) {
int y = point.y();
Block block;
if (map->getBlock(x, y, &block)) {
metatiles.append(block.metatileId);
metatiles.append(block.metatileId());
}
int blockIndex = y * map->getWidth() + x;
block = map->layout->blockdata.at(blockIndex);
auto collision = block.collision;
auto elevation = block.elevation;
auto collision = block.collision();
auto elevation = block.elevation();
collisions.append(QPair<uint16_t, uint16_t>(collision, elevation));
}
@ -371,7 +371,7 @@ void MapPixmapItem::floodFill(QGraphicsSceneMouseEvent *event) {
Block block;
MetatileSelection selection = this->metatileSelector->getMetatileSelection();
int metatileId = selection.metatileItems.first().metatileId;
if (selection.metatileItems.count() > 1 || (map->getBlock(pos.x(), pos.y(), &block) && block.metatileId != metatileId)) {
if (selection.metatileItems.count() > 1 || (map->getBlock(pos.x(), pos.y(), &block) && block.metatileId() != metatileId)) {
bool smartPathsEnabled = event->modifiers() & Qt::ShiftModifier;
if ((this->settings->smartPathsEnabled || smartPathsEnabled) && selection.dimensions.x() == 3 && selection.dimensions.y() == 3)
this->floodFillSmartPath(pos.x(), pos.y());
@ -413,17 +413,17 @@ void MapPixmapItem::magicFill(
bool fromScriptCall) {
Block block;
if (map->getBlock(initialX, initialY, &block)) {
if (selectedMetatiles.length() == 1 && selectedMetatiles.at(0).metatileId == block.metatileId) {
if (selectedMetatiles.length() == 1 && selectedMetatiles.at(0).metatileId == block.metatileId()) {
return;
}
Blockdata oldMetatiles = !fromScriptCall ? map->layout->blockdata : Blockdata();
bool setCollisions = selectedCollisions.length() == selectedMetatiles.length();
uint16_t metatileId = block.metatileId;
uint16_t metatileId = block.metatileId();
for (int y = 0; y < map->getHeight(); y++) {
for (int x = 0; x < map->getWidth(); x++) {
if (map->getBlock(x, y, &block) && block.metatileId == metatileId) {
if (map->getBlock(x, y, &block) && block.metatileId() == metatileId) {
int xDiff = x - initialX;
int yDiff = y - initialY;
int i = xDiff % selectionDimensions.x();
@ -432,11 +432,11 @@ void MapPixmapItem::magicFill(
if (j < 0) j = selectionDimensions.y() + j;
int index = j * selectionDimensions.x() + i;
if (selectedMetatiles.at(index).enabled) {
block.metatileId = selectedMetatiles.at(index).metatileId;
block.setMetatileId(selectedMetatiles.at(index).metatileId);
if (setCollisions) {
CollisionSelectionItem item = selectedCollisions.at(index);
block.collision = item.collision;
block.elevation = item.elevation;
block.setCollision(item.collision);
block.setElevation(item.elevation);
}
map->setBlock(x, y, block, !fromScriptCall);
}
@ -492,29 +492,29 @@ void MapPixmapItem::floodFill(
if (j < 0) j = selectionDimensions.y() + j;
int index = j * selectionDimensions.x() + i;
uint16_t metatileId = selectedMetatiles.at(index).metatileId;
uint16_t old_metatileId = block.metatileId;
uint16_t old_metatileId = block.metatileId();
if (selectedMetatiles.at(index).enabled && (selectedMetatiles.count() != 1 || old_metatileId != metatileId)) {
block.metatileId = metatileId;
block.setMetatileId(metatileId);
if (setCollisions) {
CollisionSelectionItem item = selectedCollisions.at(index);
block.collision = item.collision;
block.elevation = item.elevation;
block.setCollision(item.collision);
block.setElevation(item.elevation);
}
map->setBlock(x, y, block, !fromScriptCall);
}
if (!visited.contains(x + 1 + y * map->getWidth()) && map->getBlock(x + 1, y, &block) && block.metatileId == old_metatileId) {
if (!visited.contains(x + 1 + y * map->getWidth()) && map->getBlock(x + 1, y, &block) && block.metatileId() == old_metatileId) {
todo.append(QPoint(x + 1, y));
visited.insert(x + 1 + y * map->getWidth());
}
if (!visited.contains(x - 1 + y * map->getWidth()) && map->getBlock(x - 1, y, &block) && block.metatileId == old_metatileId) {
if (!visited.contains(x - 1 + y * map->getWidth()) && map->getBlock(x - 1, y, &block) && block.metatileId() == old_metatileId) {
todo.append(QPoint(x - 1, y));
visited.insert(x - 1 + y * map->getWidth());
}
if (!visited.contains(x + (y + 1) * map->getWidth()) && map->getBlock(x, y + 1, &block) && block.metatileId == old_metatileId) {
if (!visited.contains(x + (y + 1) * map->getWidth()) && map->getBlock(x, y + 1, &block) && block.metatileId() == old_metatileId) {
todo.append(QPoint(x, y + 1));
visited.insert(x + (y + 1) * map->getWidth());
}
if (!visited.contains(x + (y - 1) * map->getWidth()) && map->getBlock(x, y - 1, &block) && block.metatileId == old_metatileId) {
if (!visited.contains(x + (y - 1) * map->getWidth()) && map->getBlock(x, y - 1, &block) && block.metatileId() == old_metatileId) {
todo.append(QPoint(x, y - 1));
visited.insert(x + (y - 1) * map->getWidth());
}
@ -531,14 +531,14 @@ void MapPixmapItem::floodFillSmartPath(int initialX, int initialY, bool fromScri
return;
// Shift to the middle tile of the smart path selection.
uint16_t openTile = selection.metatileItems.at(4).metatileId;
uint16_t openTileCollision = 0;
uint16_t openTileElevation = 0;
uint16_t openMetatileId = selection.metatileItems.at(4).metatileId;
uint16_t openCollision = 0;
uint16_t openElevation = 0;
bool setCollisions = false;
if (selection.hasCollision && selection.collisionItems.length() == selection.metatileItems.length()) {
CollisionSelectionItem item = selection.collisionItems.at(4);
openTileCollision = item.collision;
openTileElevation = item.elevation;
openCollision = item.collision;
openElevation = item.elevation;
setCollisions = true;
}
@ -556,27 +556,27 @@ void MapPixmapItem::floodFillSmartPath(int initialX, int initialY, bool fromScri
continue;
}
uint16_t old_metatileId = block.metatileId;
if (old_metatileId == openTile) {
uint16_t old_metatileId = block.metatileId();
if (old_metatileId == openMetatileId) {
continue;
}
block.metatileId = openTile;
block.setMetatileId(openMetatileId);
if (setCollisions) {
block.collision = openTileCollision;
block.elevation = openTileElevation;
block.setCollision(openCollision);
block.setElevation(openElevation);
}
map->setBlock(x, y, block, !fromScriptCall);
if (map->getBlock(x + 1, y, &block) && block.metatileId == old_metatileId) {
if (map->getBlock(x + 1, y, &block) && block.metatileId() == old_metatileId) {
todo.append(QPoint(x + 1, y));
}
if (map->getBlock(x - 1, y, &block) && block.metatileId == old_metatileId) {
if (map->getBlock(x - 1, y, &block) && block.metatileId() == old_metatileId) {
todo.append(QPoint(x - 1, y));
}
if (map->getBlock(x, y + 1, &block) && block.metatileId == old_metatileId) {
if (map->getBlock(x, y + 1, &block) && block.metatileId() == old_metatileId) {
todo.append(QPoint(x, y + 1));
}
if (map->getBlock(x, y - 1, &block) && block.metatileId == old_metatileId) {
if (map->getBlock(x, y - 1, &block) && block.metatileId() == old_metatileId) {
todo.append(QPoint(x, y - 1));
}
}
@ -602,37 +602,37 @@ void MapPixmapItem::floodFillSmartPath(int initialX, int initialY, bool fromScri
Block left;
// Get marching squares value, to determine which tile to use.
if (map->getBlock(x, y - 1, &top) && isSmartPathTile(selection.metatileItems, top.metatileId))
if (map->getBlock(x, y - 1, &top) && isSmartPathTile(selection.metatileItems, top.metatileId()))
id += 1;
if (map->getBlock(x + 1, y, &right) && isSmartPathTile(selection.metatileItems, right.metatileId))
if (map->getBlock(x + 1, y, &right) && isSmartPathTile(selection.metatileItems, right.metatileId()))
id += 2;
if (map->getBlock(x, y + 1, &bottom) && isSmartPathTile(selection.metatileItems, bottom.metatileId))
if (map->getBlock(x, y + 1, &bottom) && isSmartPathTile(selection.metatileItems, bottom.metatileId()))
id += 4;
if (map->getBlock(x - 1, y, &left) && isSmartPathTile(selection.metatileItems, left.metatileId))
if (map->getBlock(x - 1, y, &left) && isSmartPathTile(selection.metatileItems, left.metatileId()))
id += 8;
block.metatileId = selection.metatileItems.at(smartPathTable[id]).metatileId;
block.setMetatileId(selection.metatileItems.at(smartPathTable[id]).metatileId);
if (setCollisions) {
CollisionSelectionItem item = selection.collisionItems.at(smartPathTable[id]);
block.collision = item.collision;
block.elevation = item.elevation;
block.setCollision(item.collision);
block.setElevation(item.elevation);
}
map->setBlock(x, y, block, !fromScriptCall);
// Visit neighbors if they are smart-path tiles, and don't revisit any.
if (!visited.contains(x + 1 + y * map->getWidth()) && map->getBlock(x + 1, y, &block) && isSmartPathTile(selection.metatileItems, block.metatileId)) {
if (!visited.contains(x + 1 + y * map->getWidth()) && map->getBlock(x + 1, y, &block) && isSmartPathTile(selection.metatileItems, block.metatileId())) {
todo.append(QPoint(x + 1, y));
visited.insert(x + 1 + y * map->getWidth());
}
if (!visited.contains(x - 1 + y * map->getWidth()) && map->getBlock(x - 1, y, &block) && isSmartPathTile(selection.metatileItems, block.metatileId)) {
if (!visited.contains(x - 1 + y * map->getWidth()) && map->getBlock(x - 1, y, &block) && isSmartPathTile(selection.metatileItems, block.metatileId())) {
todo.append(QPoint(x - 1, y));
visited.insert(x - 1 + y * map->getWidth());
}
if (!visited.contains(x + (y + 1) * map->getWidth()) && map->getBlock(x, y + 1, &block) && isSmartPathTile(selection.metatileItems, block.metatileId)) {
if (!visited.contains(x + (y + 1) * map->getWidth()) && map->getBlock(x, y + 1, &block) && isSmartPathTile(selection.metatileItems, block.metatileId())) {
todo.append(QPoint(x, y + 1));
visited.insert(x + (y + 1) * map->getWidth());
}
if (!visited.contains(x + (y - 1) * map->getWidth()) && map->getBlock(x, y - 1, &block) && isSmartPathTile(selection.metatileItems, block.metatileId)) {
if (!visited.contains(x + (y - 1) * map->getWidth()) && map->getBlock(x, y - 1, &block) && isSmartPathTile(selection.metatileItems, block.metatileId())) {
todo.append(QPoint(x, y - 1));
visited.insert(x + (y - 1) * map->getWidth());
}
@ -647,7 +647,7 @@ void MapPixmapItem::pick(QGraphicsSceneMouseEvent *event) {
QPoint pos = Metatile::coordFromPixmapCoord(event->pos());
Block block;
if (map->getBlock(pos.x(), pos.y(), &block)) {
this->metatileSelector->selectFromMap(block.metatileId, block.collision, block.elevation);
this->metatileSelector->selectFromMap(block.metatileId(), block.collision(), block.elevation());
}
}

View File

@ -4,7 +4,7 @@
#include <QPainter>
void MetatileLayersItem::draw() {
const QList<QPoint> tileCoords = QList<QPoint>{
static const QList<QPoint> tileCoords = QList<QPoint>{
QPoint(0, 0),
QPoint(16, 0),
QPoint(0, 16),
@ -19,8 +19,11 @@ void MetatileLayersItem::draw() {
QPoint(80, 16),
};
QPixmap pixmap(projectConfig.getNumLayersInMetatile() * 32, 32);
const int numLayers = projectConfig.getNumLayersInMetatile();
QPixmap pixmap(numLayers * 32, 32);
QPainter painter(&pixmap);
// Draw tile images
int numTiles = projectConfig.getNumTilesInMetatile();
for (int i = 0; i < numTiles; i++) {
Tile tile = this->metatile->tiles.at(i);
@ -29,6 +32,14 @@ void MetatileLayersItem::draw() {
.scaled(16, 16);
painter.drawImage(tileCoords.at(i), tileImage);
}
if (this->showGrid) {
// Draw grid
painter.setPen(Qt::white);
for (int i = 1; i < numLayers; i++) {
int x = i * 32;
painter.drawLine(x, 0, x, 32);
}
}
this->setPixmap(pixmap);
}

View File

@ -1,12 +1,19 @@
#include "movementpermissionsselector.h"
#include <QPainter>
const int MovementPermissionsSelector::CellWidth = 32;
const int MovementPermissionsSelector::CellHeight = 32;
void MovementPermissionsSelector::draw() {
QPixmap pixmap(":/images/collisions.png");
this->setPixmap(pixmap.scaled(64, 512));
this->setPixmap(this->basePixmap);
this->drawSelection();
}
void MovementPermissionsSelector::setBasePixmap(QPixmap pixmap) {
this->basePixmap = pixmap;
this->draw();
}
uint16_t MovementPermissionsSelector::getSelectedCollision() {
return static_cast<uint16_t>(this->selectionInitialX);
}
@ -16,7 +23,7 @@ uint16_t MovementPermissionsSelector::getSelectedElevation() {
}
void MovementPermissionsSelector::select(uint16_t collision, uint16_t elevation) {
SelectablePixmapItem::select(collision != 0, elevation, 0, 0);
SelectablePixmapItem::select(collision, elevation, 0, 0);
}
void MovementPermissionsSelector::hoverMoveEvent(QGraphicsSceneHoverEvent *event) {

View File

@ -175,7 +175,7 @@ void NewMapPopup::setDefaultSettings(Project *project) {
settings.secondaryTilesetLabel = project->getDefaultSecondaryTilesetLabel();
settings.type = project->mapTypes.at(0);
settings.location = project->mapSectionValueToName.values().at(0);
settings.song = project->songNames.at(0);
settings.song = project->defaultSong;
settings.canFlyTo = false;
settings.showLocationName = true;
settings.allowRunning = false;
@ -258,9 +258,9 @@ void NewMapPopup::on_pushButton_NewMap_Accept_clicked() {
newMap->location = this->ui->comboBox_NewMap_Location->currentText();
newMap->song = this->ui->comboBox_NewMap_Song->currentText();
newMap->requiresFlash = false;
newMap->weather = this->project->weatherNames.value(0, "WEATHER_NONE");
newMap->weather = this->project->weatherNames.value(0);
newMap->show_location = this->ui->checkBox_NewMap_Show_Location->isChecked();
newMap->battle_scene = this->project->mapBattleScenes.value(0, "MAP_BATTLE_SCENE_NORMAL");
newMap->battle_scene = this->project->mapBattleScenes.value(0);
if (this->existingLayout) {
layout = this->project->mapLayouts.value(this->layoutId);

View File

@ -38,7 +38,7 @@ void NewTilesetDialog::SecondaryChanged(){
void NewTilesetDialog::NameOrSecondaryChanged() {
this->friendlyName = this->ui->nameLineEdit->text();
this->fullSymbolName = "gTileset_" + this->friendlyName;
this->fullSymbolName = projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix) + this->friendlyName;
this->ui->symbolNameLineEdit->setText(this->fullSymbolName);
this->path = Tileset::getExpectedDir(this->fullSymbolName, this->isSecondary);
this->ui->pathLineEdit->setText(this->path);

View File

@ -56,3 +56,8 @@ void NoScrollComboBox::setNumberItem(int value)
{
this->setItem(this->findData(value), QString::number(value));
}
void NoScrollComboBox::setHexItem(uint32_t value)
{
this->setItem(this->findData(value), "0x" + QString::number(value, 16).toUpper());
}

View File

@ -1,5 +1,4 @@
#include "projectsettingseditor.h"
#include "ui_projectsettingseditor.h"
#include "config.h"
#include "noscrollcombobox.h"
#include "prefab.h"
@ -9,8 +8,12 @@
/*
Editor for the settings in a user's porymap.project.cfg file (and 'use_encounter_json' in porymap.user.cfg).
Disabling the warp behavior warning is actually part of porymap.cfg, but it's on this window because the
related settings are here (and project-specific).
*/
const int ProjectSettingsEditor::eventsTab = 3;
ProjectSettingsEditor::ProjectSettingsEditor(QWidget *parent, Project *project) :
QMainWindow(parent),
ui(new Ui::ProjectSettingsEditor),
@ -21,6 +24,7 @@ ProjectSettingsEditor::ProjectSettingsEditor(QWidget *parent, Project *project)
setAttribute(Qt::WA_DeleteOnClose);
this->initUi();
this->createProjectPathsTable();
this->createProjectIdentifiersTable();
this->connectSignals();
this->refresh();
this->restoreWindowState();
@ -33,10 +37,10 @@ ProjectSettingsEditor::~ProjectSettingsEditor()
void ProjectSettingsEditor::connectSignals() {
connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &ProjectSettingsEditor::dialogButtonClicked);
connect(ui->button_ChoosePrefabs, &QAbstractButton::clicked, this, &ProjectSettingsEditor::choosePrefabsFileClicked);
connect(ui->button_ImportDefaultPrefabs, &QAbstractButton::clicked, this, &ProjectSettingsEditor::importDefaultPrefabsClicked);
connect(ui->comboBox_BaseGameVersion, &QComboBox::currentTextChanged, this, &ProjectSettingsEditor::promptRestoreDefaults);
connect(ui->comboBox_AttributesSize, &QComboBox::currentTextChanged, this, &ProjectSettingsEditor::updateAttributeLimits);
connect(ui->comboBox_IconSpecies, &QComboBox::currentTextChanged, this, &ProjectSettingsEditor::updatePokemonIconPath);
connect(ui->checkBox_EnableCustomBorderSize, &QCheckBox::stateChanged, [this](int state) {
bool customSize = (state == Qt::Checked);
// When switching between the spin boxes or line edit for border metatiles we set
@ -44,10 +48,35 @@ void ProjectSettingsEditor::connectSignals() {
this->setBorderMetatileIds(customSize, this->getBorderMetatileIds(!customSize));
this->setBorderMetatilesUi(customSize);
});
connect(ui->button_AddWarpBehavior, &QAbstractButton::clicked, [this](bool) { this->updateWarpBehaviorsList(true); });
connect(ui->button_RemoveWarpBehavior, &QAbstractButton::clicked, [this](bool) { this->updateWarpBehaviorsList(false); });
// Connect file selection buttons
connect(ui->button_ChoosePrefabs, &QAbstractButton::clicked, [this](bool) { this->choosePrefabsFile(); });
connect(ui->button_CollisionGraphics, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_CollisionGraphics); });
connect(ui->button_ObjectsIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_ObjectsIcon); });
connect(ui->button_WarpsIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_WarpsIcon); });
connect(ui->button_TriggersIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_TriggersIcon); });
connect(ui->button_BGsIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_BGsIcon); });
connect(ui->button_HealspotsIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_HealspotsIcon); });
connect(ui->button_PokemonIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_PokemonIcon); });
// Display a warning if a mask value overlaps with another mask in its group.
connect(ui->spinBox_MetatileIdMask, &UIntSpinBox::textChanged, this, &ProjectSettingsEditor::updateBlockMaskOverlapWarning);
connect(ui->spinBox_CollisionMask, &UIntSpinBox::textChanged, this, &ProjectSettingsEditor::updateBlockMaskOverlapWarning);
connect(ui->spinBox_ElevationMask, &UIntSpinBox::textChanged, this, &ProjectSettingsEditor::updateBlockMaskOverlapWarning);
connect(ui->spinBox_BehaviorMask, &UIntSpinBox::textChanged, this, &ProjectSettingsEditor::updateAttributeMaskOverlapWarning);
connect(ui->spinBox_LayerTypeMask, &UIntSpinBox::textChanged, this, &ProjectSettingsEditor::updateAttributeMaskOverlapWarning);
connect(ui->spinBox_EncounterTypeMask, &UIntSpinBox::textChanged, this, &ProjectSettingsEditor::updateAttributeMaskOverlapWarning);
connect(ui->spinBox_TerrainTypeMask, &UIntSpinBox::textChanged, this, &ProjectSettingsEditor::updateAttributeMaskOverlapWarning);
// Record that there are unsaved changes if any of the settings are modified
for (auto combo : ui->centralwidget->findChildren<NoScrollComboBox *>())
connect(combo, &QComboBox::currentTextChanged, this, &ProjectSettingsEditor::markEdited);
for (auto combo : ui->centralwidget->findChildren<NoScrollComboBox *>()){
// Changes to these two combo boxes are just for info display, don't mark as unsaved
if (combo != ui->comboBox_IconSpecies && combo != ui->comboBox_WarpBehaviors)
connect(combo, &QComboBox::currentTextChanged, this, &ProjectSettingsEditor::markEdited);
}
for (auto checkBox : ui->centralwidget->findChildren<QCheckBox *>())
connect(checkBox, &QCheckBox::stateChanged, this, &ProjectSettingsEditor::markEdited);
for (auto lineEdit : ui->centralwidget->findChildren<QLineEdit *>())
@ -66,30 +95,105 @@ void ProjectSettingsEditor::markEdited() {
void ProjectSettingsEditor::initUi() {
// Populate combo boxes
if (project) ui->comboBox_DefaultPrimaryTileset->addItems(project->primaryTilesetLabels);
if (project) ui->comboBox_DefaultSecondaryTileset->addItems(project->secondaryTilesetLabels);
if (project) {
ui->comboBox_DefaultPrimaryTileset->addItems(project->primaryTilesetLabels);
ui->comboBox_DefaultSecondaryTileset->addItems(project->secondaryTilesetLabels);
ui->comboBox_IconSpecies->addItems(project->speciesToIconPath.keys());
ui->comboBox_WarpBehaviors->addItems(project->metatileBehaviorMap.keys());
}
ui->comboBox_BaseGameVersion->addItems(ProjectConfig::versionStrings);
ui->comboBox_AttributesSize->addItems({"1", "2", "4"});
// Validate that the border metatiles text is a comma-separated list of metatile values
const QString regex_Hex = "(0[xX])?[A-Fa-f0-9]+";
static const QRegularExpression expression(QString("^(%1,)*%1$").arg(regex_Hex)); // Comma-separated list of hex values
QRegularExpressionValidator *validator = new QRegularExpressionValidator(expression);
ui->lineEdit_BorderMetatiles->setValidator(validator);
static const QString regex_Hex = "(0[xX])?[A-Fa-f0-9]+";
static const QRegularExpression expression_HexList(QString("^(%1,)*%1$").arg(regex_Hex)); // Comma-separated list of hex values
QRegularExpressionValidator *validator_HexList = new QRegularExpressionValidator(expression_HexList);
ui->lineEdit_BorderMetatiles->setValidator(validator_HexList);
this->setBorderMetatilesUi(projectConfig.getUseCustomBorderSize());
int maxMetatileId = Project::getNumMetatilesTotal() - 1;
// Validate that the text added to the warp behavior list could be a valid define
// (we don't care whether it actually is a metatile behavior define)
static const QRegularExpression expression_Word("^[A-Za-z0-9_]*$");
QRegularExpressionValidator *validator_Word = new QRegularExpressionValidator(expression_Word);
ui->comboBox_WarpBehaviors->setValidator(validator_Word);
ui->textEdit_WarpBehaviors->setTextColor(Qt::gray);
// Set spin box limits
uint16_t maxMetatileId = Block::getMaxMetatileId();
ui->spinBox_FillMetatile->setMaximum(maxMetatileId);
ui->spinBox_BorderMetatile1->setMaximum(maxMetatileId);
ui->spinBox_BorderMetatile2->setMaximum(maxMetatileId);
ui->spinBox_BorderMetatile3->setMaximum(maxMetatileId);
ui->spinBox_BorderMetatile4->setMaximum(maxMetatileId);
ui->spinBox_Elevation->setMaximum(15);
ui->spinBox_Elevation->setMaximum(Block::getMaxElevation());
ui->spinBox_Collision->setMaximum(Block::getMaxCollision());
ui->spinBox_MaxElevation->setMaximum(Block::getMaxElevation());
ui->spinBox_MaxCollision->setMaximum(Block::getMaxCollision());
ui->spinBox_MetatileIdMask->setMaximum(Block::maxValue);
ui->spinBox_CollisionMask->setMaximum(Block::maxValue);
ui->spinBox_ElevationMask->setMaximum(Block::maxValue);
// Some settings can be determined by constants in the project.
// We reflect that here by disabling their UI elements.
if (project) {
const QString maskFilepath = projectConfig.getFilePath(ProjectFilePath::global_fieldmap);
const QString attrTableFilepath = projectConfig.getFilePath(ProjectFilePath::fieldmap);
const QString metatileIdMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_metatile);
const QString collisionMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_collision);
const QString elevationMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_elevation);
const QString behaviorMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_behavior);
const QString layerTypeMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_layer);
const QString behaviorTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_behavior);
const QString layerTypeTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_layer);
const QString encounterTypeTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_encounter);
const QString terrainTypeTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_terrain);
const QString attrTableName = projectConfig.getIdentifier(ProjectIdentifier::symbol_attribute_table);
// Block masks
if (project->disabledSettingsNames.contains(metatileIdMaskName))
this->disableParsedSetting(ui->spinBox_MetatileIdMask, metatileIdMaskName, maskFilepath);
if (project->disabledSettingsNames.contains(collisionMaskName))
this->disableParsedSetting(ui->spinBox_CollisionMask, collisionMaskName, maskFilepath);
if (project->disabledSettingsNames.contains(elevationMaskName))
this->disableParsedSetting(ui->spinBox_ElevationMask, elevationMaskName, maskFilepath);
// Behavior mask
if (project->disabledSettingsNames.contains(behaviorMaskName))
this->disableParsedSetting(ui->spinBox_BehaviorMask, behaviorMaskName, maskFilepath);
else if (project->disabledSettingsNames.contains(behaviorTableName))
this->disableParsedSetting(ui->spinBox_BehaviorMask, attrTableName, attrTableFilepath);
// Layer type mask
if (project->disabledSettingsNames.contains(layerTypeMaskName))
this->disableParsedSetting(ui->spinBox_LayerTypeMask, layerTypeMaskName, maskFilepath);
else if (project->disabledSettingsNames.contains(layerTypeTableName))
this->disableParsedSetting(ui->spinBox_LayerTypeMask, attrTableName, attrTableFilepath);
// Encounter and terrain type masks
if (project->disabledSettingsNames.contains(encounterTypeTableName))
this->disableParsedSetting(ui->spinBox_EncounterTypeMask, attrTableName, attrTableFilepath);
if (project->disabledSettingsNames.contains(terrainTypeTableName))
this->disableParsedSetting(ui->spinBox_TerrainTypeMask, attrTableName, attrTableFilepath);
}
}
void ProjectSettingsEditor::disableParsedSetting(QWidget * widget, const QString &name, const QString &filepath) {
widget->setEnabled(false);
widget->setToolTip(QString("This value has been read from '%1' in %2").arg(name).arg(filepath));
}
// Remember the current settings tab for future sessions
void ProjectSettingsEditor::on_mainTabs_tabBarClicked(int index) {
porymapConfig.setProjectSettingsTab(index);
}
void ProjectSettingsEditor::setTab(int index) {
ui->mainTabs->setCurrentIndex(index);
porymapConfig.setProjectSettingsTab(index);
}
void ProjectSettingsEditor::setBorderMetatilesUi(bool customSize) {
ui->widget_DefaultSizeBorderMetatiles->setVisible(!customSize);
ui->widget_CustomSizeBorderMetatiles->setVisible(customSize);
ui->stackedWidget_BorderMetatiles->setCurrentIndex(customSize ? 0 : 1);
}
void ProjectSettingsEditor::setBorderMetatileIds(bool customSize, QList<uint16_t> metatileIds) {
@ -121,8 +225,49 @@ QList<uint16_t> ProjectSettingsEditor::getBorderMetatileIds(bool customSize) {
return metatileIds;
}
// Show/hide warning for overlapping mask values. These are technically ok, but probably not intended.
// Additionally, Porymap will not properly reflect that the values are linked.
void ProjectSettingsEditor::updateMaskOverlapWarning(QLabel * warning, QList<UIntSpinBox*> masks) {
// Find any overlapping masks
QMap<int, bool> overlapping;
for (int i = 0; i < masks.length(); i++)
for (int j = i + 1; j < masks.length(); j++) {
if (masks.at(i)->value() & masks.at(j)->value())
overlapping[i] = overlapping[j] = true;
}
// It'de nice if we could style this as a persistent red border around the line edit for any
// overlapping masks. As it is editing the border undesirably modifies the arrow buttons.
// This stylesheet will just highlight the currently selected line edit, which is fine enough.
static const QString styleSheet = "QAbstractSpinBox { selection-background-color: rgba(255, 0, 0, 25%) }";
// Update warning display
if (warning) warning->setHidden(overlapping.isEmpty());
for (int i = 0; i < masks.length(); i++)
masks.at(i)->setStyleSheet(overlapping.contains(i) ? styleSheet : "");
}
void ProjectSettingsEditor::updateBlockMaskOverlapWarning() {
const auto masks = QList<UIntSpinBox*>{
ui->spinBox_MetatileIdMask,
ui->spinBox_CollisionMask,
ui->spinBox_ElevationMask,
};
this->updateMaskOverlapWarning(ui->label_OverlapWarningBlocks, masks);
}
void ProjectSettingsEditor::updateAttributeMaskOverlapWarning() {
const auto masks = QList<UIntSpinBox*>{
ui->spinBox_BehaviorMask,
ui->spinBox_LayerTypeMask,
ui->spinBox_EncounterTypeMask,
ui->spinBox_TerrainTypeMask,
};
this->updateMaskOverlapWarning(ui->label_OverlapWarningMetatiles, masks);
}
void ProjectSettingsEditor::updateAttributeLimits(const QString &attrSize) {
QMap<QString, uint32_t> limits {
static const QMap<QString, uint32_t> limits {
{"1", 0xFF},
{"2", 0xFFFF},
{"4", 0xFFFFFFFF},
@ -134,40 +279,117 @@ void ProjectSettingsEditor::updateAttributeLimits(const QString &attrSize) {
ui->spinBox_TerrainTypeMask->setMaximum(max);
}
void ProjectSettingsEditor::createProjectPathsTable() {
auto pathPairs = ProjectConfig::defaultPaths.values();
for (auto pathPair : pathPairs) {
// Name of the path
// Only one icon path is displayed at a time, so we need to keep track of the rest,
// and update the path edit when the user changes the selected species.
// The existing icon path map in ProjectConfig is left alone to allow unsaved changes.
void ProjectSettingsEditor::updatePokemonIconPath(const QString &newSpecies) {
if (!project) return;
// If user was editing a path for a valid species, record filepath text before we wipe it.
if (!this->prevIconSpecies.isEmpty() && this->project->speciesToIconPath.contains(this->prevIconSpecies))
this->editedPokemonIconPaths[this->prevIconSpecies] = ui->lineEdit_PokemonIcon->text();
QString editedPath = this->editedPokemonIconPaths.value(newSpecies);
QString defaultPath = this->project->speciesToIconPath.value(newSpecies);
ui->lineEdit_PokemonIcon->setText(this->stripProjectDir(editedPath));
ui->lineEdit_PokemonIcon->setPlaceholderText(this->stripProjectDir(defaultPath));
this->prevIconSpecies = newSpecies;
}
QStringList ProjectSettingsEditor::getWarpBehaviorsList() {
return ui->textEdit_WarpBehaviors->toPlainText().split("\n");
}
void ProjectSettingsEditor::setWarpBehaviorsList(QStringList list) {
list.removeDuplicates();
list.sort();
ui->textEdit_WarpBehaviors->setText(list.join("\n"));
}
void ProjectSettingsEditor::updateWarpBehaviorsList(bool adding) {
QString input = ui->comboBox_WarpBehaviors->currentText();
if (input.isEmpty())
return;
// Check if input was a value string for a named behavior
bool ok;
uint32_t value = input.toUInt(&ok, 0);
if (ok && project->metatileBehaviorMapInverse.contains(value))
input = project->metatileBehaviorMapInverse.value(value);
if (!project->metatileBehaviorMap.contains(input))
return;
QStringList list = this->getWarpBehaviorsList();
int pos = list.indexOf(input);
if (adding && pos < 0) {
// Add text to list
list.prepend(input);
} else if (!adding && pos >= 0) {
// Remove text from list
list.removeAt(pos);
} else {
// Trying to add text already in list,
// or trying to remove text not in list
return;
}
this->setWarpBehaviorsList(list);
this->hasUnsavedChanges = true;
}
// Dynamically populate the tabs for project files and identifiers
void ProjectSettingsEditor::createConfigTextTable(const QList<QPair<QString, QString>> configPairs, bool filesTab) {
for (auto pair : configPairs) {
const QString idName = pair.first;
const QString defaultText = pair.second;
auto name = new QLabel();
name->setAlignment(Qt::AlignBottom);
name->setText(pathPair.first);
name->setText(idName);
// Filepath line edit
auto lineEdit = new QLineEdit();
lineEdit->setObjectName(pathPair.first); // Used when saving the paths
lineEdit->setPlaceholderText(pathPair.second);
lineEdit->setObjectName(idName); // Used when saving
lineEdit->setPlaceholderText(defaultText);
lineEdit->setClearButtonEnabled(true);
// "Choose file" button
auto button = new QToolButton();
button->setIcon(QIcon(":/icons/folder.ico"));
connect(button, &QAbstractButton::clicked, [this, lineEdit](bool) {
const QString path = this->chooseProjectFile(lineEdit->placeholderText());
if (!path.isEmpty()) {
lineEdit->setText(path);
this->markEdited();
}
});
// Add to list
auto editArea = new QWidget();
auto layout = new QHBoxLayout(editArea);
layout->addWidget(lineEdit);
layout->addWidget(button);
ui->layout_ProjectPaths->addRow(name, editArea);
if (filesTab) {
// "Choose file" button
auto button = new QToolButton();
button->setIcon(QIcon(":/icons/folder.ico"));
connect(button, &QAbstractButton::clicked, [this, lineEdit](bool) {
const QString path = this->chooseProjectFile(lineEdit->placeholderText());
if (!path.isEmpty()) {
lineEdit->setText(path);
this->markEdited();
}
});
layout->addWidget(button);
ui->layout_ProjectPaths->addRow(name, editArea);
} else {
ui->layout_Identifiers->addRow(name, editArea);
}
}
}
void ProjectSettingsEditor::createProjectPathsTable() {
auto pairs = ProjectConfig::defaultPaths.values();
this->createConfigTextTable(pairs, true);
}
void ProjectSettingsEditor::createProjectIdentifiersTable() {
auto pairs = ProjectConfig::defaultIdentifiers.values();
this->createConfigTextTable(pairs, false);
}
QString ProjectSettingsEditor::chooseProjectFile(const QString &defaultFilepath) {
const QString startDir = this->baseDir + defaultFilepath;
@ -208,6 +430,10 @@ void ProjectSettingsEditor::refresh() {
ui->comboBox_AttributesSize->setTextItem(QString::number(projectConfig.getMetatileAttributesSize()));
this->updateAttributeLimits(ui->comboBox_AttributesSize->currentText());
this->prevIconSpecies = QString();
this->editedPokemonIconPaths = projectConfig.getPokemonIconPaths();
this->updatePokemonIconPath(ui->comboBox_IconSpecies->currentText());
// Set check box states
ui->checkBox_UsePoryscript->setChecked(projectConfig.getUsePoryScript());
ui->checkBox_ShowWildEncounterTables->setChecked(userConfig.getEncounterJsonActive());
@ -224,14 +450,21 @@ void ProjectSettingsEditor::refresh() {
ui->checkBox_EnableCustomBorderSize->setChecked(projectConfig.getUseCustomBorderSize());
ui->checkBox_OutputCallback->setChecked(projectConfig.getTilesetsHaveCallback());
ui->checkBox_OutputIsCompressed->setChecked(projectConfig.getTilesetsHaveIsCompressed());
ui->checkBox_DisableWarning->setChecked(porymapConfig.getWarpBehaviorWarningDisabled());
// Set spin box values
ui->spinBox_Elevation->setValue(projectConfig.getNewMapElevation());
ui->spinBox_FillMetatile->setValue(projectConfig.getNewMapMetatileId());
ui->spinBox_BehaviorMask->setValue(projectConfig.getMetatileBehaviorMask());
ui->spinBox_EncounterTypeMask->setValue(projectConfig.getMetatileEncounterTypeMask());
ui->spinBox_LayerTypeMask->setValue(projectConfig.getMetatileLayerTypeMask());
ui->spinBox_TerrainTypeMask->setValue(projectConfig.getMetatileTerrainTypeMask());
ui->spinBox_Elevation->setValue(projectConfig.getDefaultElevation());
ui->spinBox_Collision->setValue(projectConfig.getDefaultCollision());
ui->spinBox_FillMetatile->setValue(projectConfig.getDefaultMetatileId());
ui->spinBox_MaxElevation->setValue(projectConfig.getCollisionSheetHeight() - 1);
ui->spinBox_MaxCollision->setValue(projectConfig.getCollisionSheetWidth() - 1);
ui->spinBox_BehaviorMask->setValue(projectConfig.getMetatileBehaviorMask() & ui->spinBox_BehaviorMask->maximum());
ui->spinBox_EncounterTypeMask->setValue(projectConfig.getMetatileEncounterTypeMask() & ui->spinBox_EncounterTypeMask->maximum());
ui->spinBox_LayerTypeMask->setValue(projectConfig.getMetatileLayerTypeMask() & ui->spinBox_LayerTypeMask->maximum());
ui->spinBox_TerrainTypeMask->setValue(projectConfig.getMetatileTerrainTypeMask() & ui->spinBox_TerrainTypeMask->maximum());
ui->spinBox_MetatileIdMask->setValue(projectConfig.getBlockMetatileIdMask() & ui->spinBox_MetatileIdMask->maximum());
ui->spinBox_CollisionMask->setValue(projectConfig.getBlockCollisionMask() & ui->spinBox_CollisionMask->maximum());
ui->spinBox_ElevationMask->setValue(projectConfig.getBlockElevationMask() & ui->spinBox_ElevationMask->maximum());
// Set (and sync) border metatile IDs
auto metatileIds = projectConfig.getNewMapBorderMetatileIds();
@ -240,8 +473,25 @@ void ProjectSettingsEditor::refresh() {
// Set line edit texts
ui->lineEdit_PrefabsPath->setText(projectConfig.getPrefabFilepath());
ui->lineEdit_CollisionGraphics->setText(projectConfig.getCollisionSheetPath());
ui->lineEdit_ObjectsIcon->setText(projectConfig.getEventIconPath(Event::Group::Object));
ui->lineEdit_WarpsIcon->setText(projectConfig.getEventIconPath(Event::Group::Warp));
ui->lineEdit_TriggersIcon->setText(projectConfig.getEventIconPath(Event::Group::Coord));
ui->lineEdit_BGsIcon->setText(projectConfig.getEventIconPath(Event::Group::Bg));
ui->lineEdit_HealspotsIcon->setText(projectConfig.getEventIconPath(Event::Group::Heal));
for (auto lineEdit : ui->scrollAreaContents_ProjectPaths->findChildren<QLineEdit*>())
lineEdit->setText(projectConfig.getFilePath(lineEdit->objectName(), true));
lineEdit->setText(projectConfig.getCustomFilePath(lineEdit->objectName()));
for (auto lineEdit : ui->scrollAreaContents_Identifiers->findChildren<QLineEdit*>())
lineEdit->setText(projectConfig.getCustomIdentifier(lineEdit->objectName()));
// Set warp behaviors
auto behaviorValues = projectConfig.getWarpBehaviors();
QStringList behaviorNames;
for (auto value : behaviorValues) {
if (project->metatileBehaviorMapInverse.contains(value))
behaviorNames.append(project->metatileBehaviorMapInverse.value(value));
}
this->setWarpBehaviorsList(behaviorNames);
this->refreshing = false; // Allow signals
}
@ -253,10 +503,13 @@ void ProjectSettingsEditor::save() {
// Prevent a call to save() for each of the config settings
projectConfig.setSaveDisabled(true);
// Save combo box settings
projectConfig.setDefaultPrimaryTileset(ui->comboBox_DefaultPrimaryTileset->currentText());
projectConfig.setDefaultSecondaryTileset(ui->comboBox_DefaultSecondaryTileset->currentText());
projectConfig.setBaseGameVersion(projectConfig.stringToBaseGameVersion(ui->comboBox_BaseGameVersion->currentText()));
projectConfig.setMetatileAttributesSize(ui->comboBox_AttributesSize->currentText().toInt());
// Save check box settings
projectConfig.setUsePoryScript(ui->checkBox_UsePoryscript->isChecked());
userConfig.setEncounterJsonActive(ui->checkBox_ShowWildEncounterTables->isChecked());
projectConfig.setCreateMapTextFileEnabled(ui->checkBox_CreateTextFile->isChecked());
@ -272,17 +525,52 @@ void ProjectSettingsEditor::save() {
projectConfig.setUseCustomBorderSize(ui->checkBox_EnableCustomBorderSize->isChecked());
projectConfig.setTilesetsHaveCallback(ui->checkBox_OutputCallback->isChecked());
projectConfig.setTilesetsHaveIsCompressed(ui->checkBox_OutputIsCompressed->isChecked());
projectConfig.setNewMapElevation(ui->spinBox_Elevation->value());
projectConfig.setNewMapMetatileId(ui->spinBox_FillMetatile->value());
porymapConfig.setWarpBehaviorWarningDisabled(ui->checkBox_DisableWarning->isChecked());
// Save spin box settings
projectConfig.setDefaultElevation(ui->spinBox_Elevation->value());
projectConfig.setDefaultCollision(ui->spinBox_Collision->value());
projectConfig.setDefaultMetatileId(ui->spinBox_FillMetatile->value());
projectConfig.setCollisionSheetHeight(ui->spinBox_MaxElevation->value() + 1);
projectConfig.setCollisionSheetWidth(ui->spinBox_MaxCollision->value() + 1);
projectConfig.setMetatileBehaviorMask(ui->spinBox_BehaviorMask->value());
projectConfig.setMetatileTerrainTypeMask(ui->spinBox_TerrainTypeMask->value());
projectConfig.setMetatileEncounterTypeMask(ui->spinBox_EncounterTypeMask->value());
projectConfig.setMetatileLayerTypeMask(ui->spinBox_LayerTypeMask->value());
projectConfig.setBlockMetatileIdMask(ui->spinBox_MetatileIdMask->value());
projectConfig.setBlockCollisionMask(ui->spinBox_CollisionMask->value());
projectConfig.setBlockElevationMask(ui->spinBox_ElevationMask->value());
// Save line edit settings
projectConfig.setPrefabFilepath(ui->lineEdit_PrefabsPath->text());
projectConfig.setCollisionSheetPath(ui->lineEdit_CollisionGraphics->text());
projectConfig.setEventIconPath(Event::Group::Object, ui->lineEdit_ObjectsIcon->text());
projectConfig.setEventIconPath(Event::Group::Warp, ui->lineEdit_WarpsIcon->text());
projectConfig.setEventIconPath(Event::Group::Coord, ui->lineEdit_TriggersIcon->text());
projectConfig.setEventIconPath(Event::Group::Bg, ui->lineEdit_BGsIcon->text());
projectConfig.setEventIconPath(Event::Group::Heal, ui->lineEdit_HealspotsIcon->text());
for (auto lineEdit : ui->scrollAreaContents_ProjectPaths->findChildren<QLineEdit*>())
projectConfig.setFilePath(lineEdit->objectName(), lineEdit->text());
for (auto lineEdit : ui->scrollAreaContents_Identifiers->findChildren<QLineEdit*>())
projectConfig.setIdentifier(lineEdit->objectName(), lineEdit->text());
// Save warp behaviors
QStringList behaviorNames = this->getWarpBehaviorsList();
QSet<uint32_t> behaviorValues;
for (auto name : behaviorNames)
behaviorValues.insert(project->metatileBehaviorMap.value(name));
projectConfig.setWarpBehaviors(behaviorValues);
// Save border metatile IDs
projectConfig.setNewMapBorderMetatileIds(this->getBorderMetatileIds(ui->checkBox_EnableCustomBorderSize->isChecked()));
// Save pokemon icon paths
const QString species = ui->comboBox_IconSpecies->currentText();
if (this->project->speciesToIconPath.contains(species))
this->editedPokemonIconPaths.insert(species, ui->lineEdit_PokemonIcon->text());
for (auto i = this->editedPokemonIconPaths.cbegin(), end = this->editedPokemonIconPaths.cend(); i != end; i++)
projectConfig.setPokemonIconPath(i.key(), i.value());
projectConfig.setSaveDisabled(false);
projectConfig.save();
this->hasUnsavedChanges = false;
@ -293,25 +581,32 @@ void ProjectSettingsEditor::save() {
}
// Pick a file to use as the new prefabs file path
void ProjectSettingsEditor::choosePrefabsFileClicked(bool) {
QString startPath = this->project->importExportPath;
QFileInfo fileInfo(ui->lineEdit_PrefabsPath->text());
if (fileInfo.exists() && fileInfo.isFile() && fileInfo.suffix() == "json") {
// Current setting is a valid JSON file. Start the file dialog there
startPath = fileInfo.dir().absolutePath();
}
QString filepath = QFileDialog::getOpenFileName(this, "Choose Prefabs File", startPath, "JSON Files (*.json)");
void ProjectSettingsEditor::choosePrefabsFile() {
this->chooseFile(ui->lineEdit_PrefabsPath, "Choose Prefabs File", "JSON Files (*.json)");
}
void ProjectSettingsEditor::chooseImageFile(QLineEdit * filepathEdit) {
this->chooseFile(filepathEdit, "Choose Image File", "Images (*.png *.jpg)");
}
void ProjectSettingsEditor::chooseFile(QLineEdit * filepathEdit, const QString &description, const QString &extensions) {
QString filepath = QFileDialog::getOpenFileName(this, description, this->project->importExportPath, extensions);
if (filepath.isEmpty())
return;
this->project->setImportExportPath(filepath);
// Display relative path if this file is in the project folder
if (filepath.startsWith(this->baseDir))
filepath.remove(0, this->baseDir.length());
ui->lineEdit_PrefabsPath->setText(filepath);
if (filepathEdit)
filepathEdit->setText(this->stripProjectDir(filepath));
this->hasUnsavedChanges = true;
}
// Display relative path if this file is in the project folder
QString ProjectSettingsEditor::stripProjectDir(QString s) {
if (s.startsWith(this->baseDir))
s.remove(0, this->baseDir.length());
return s;
}
void ProjectSettingsEditor::importDefaultPrefabsClicked(bool) {
// If the prompt is accepted the prefabs file will be created and its filepath will be saved in the config.
// No need to set hasUnsavedChanges here.
@ -371,18 +666,24 @@ void ProjectSettingsEditor::dialogButtonClicked(QAbstractButton *button) {
if (buttonRole == QDialogButtonBox::AcceptRole) {
// "OK" button
this->save();
close();
this->close();
} else if (buttonRole == QDialogButtonBox::RejectRole) {
// "Cancel" button
if (!this->promptSaveChanges())
return;
close();
this->close();
} else if (buttonRole == QDialogButtonBox::ResetRole) {
// "Restore Defaults" button
this->promptRestoreDefaults();
}
}
// Close event triggered by a project reload. User doesn't need any prompts, just close the window.
void ProjectSettingsEditor::closeQuietly() {
// Turn off flags that trigger prompts
this->hasUnsavedChanges = false;
this->projectNeedsReload = false;
this->close();
}
void ProjectSettingsEditor::closeEvent(QCloseEvent* event) {
if (!this->promptSaveChanges()) {
event->ignore();
@ -395,9 +696,9 @@ void ProjectSettingsEditor::closeEvent(QCloseEvent* event) {
);
if (this->projectNeedsReload) {
if (this->prompt("Settings changed, reload project to apply changes?") == QMessageBox::Yes){
// Reloading the project will destroy this window, no other work should happen after this signal is emitted
// Note: Declining this prompt with changes that need a reload may cause problems
if (this->prompt("Settings saved, reload project to apply changes?") == QMessageBox::Yes)
emit this->reloadProject();
}
}
QMainWindow::closeEvent(event);
}

View File

@ -796,14 +796,17 @@ void RegionMapEditor::displayRegionMapEntryOptions() {
void RegionMapEditor::updateRegionMapEntryOptions(QString section) {
if (!this->region_map->layoutEnabled()) return;
bool enabled = ((section != "MAPSEC_NONE") && (section != "MAPSEC_COUNT")) && (this->region_map_entries.contains(section));
bool isSpecialSection = (section == this->region_map->default_map_section
|| section == this->region_map->count_map_section);
bool enabled = (!isSpecialSection && this->region_map_entries.contains(section));
this->ui->lineEdit_RM_MapName->setEnabled(enabled);
this->ui->spinBox_RM_Entry_x->setEnabled(enabled);
this->ui->spinBox_RM_Entry_y->setEnabled(enabled);
this->ui->spinBox_RM_Entry_width->setEnabled(enabled);
this->ui->spinBox_RM_Entry_height->setEnabled(enabled);
this->ui->pushButton_entryActivate->setEnabled(section != "MAPSEC_NONE" && section != "MAPSEC_COUNT");
this->ui->pushButton_entryActivate->setEnabled(!isSpecialSection);
this->ui->pushButton_entryActivate->setText(enabled ? "Remove" : "Add");
this->ui->lineEdit_RM_MapName->blockSignals(true);
@ -831,7 +834,7 @@ void RegionMapEditor::updateRegionMapEntryOptions(QString section) {
void RegionMapEditor::on_pushButton_entryActivate_clicked() {
QString section = this->ui->comboBox_RM_Entry_MapSection->currentText();
if (section == "MAPSEC_NONE") return;
if (section == this->region_map->default_map_section) return;
if (this->region_map_entries.contains(section)) {
// disable
@ -1297,7 +1300,7 @@ void RegionMapEditor::on_action_RegionMap_ClearLayout_triggered() {
QMessageBox::StandardButton result = QMessageBox::question(
this,
"WARNING",
"This action will reset the entire map layout to MAPSEC_NONE, continue?",
QString("This action will reset the entire map layout to %1, continue?").arg(this->region_map->default_map_section),
QMessageBox::Yes | QMessageBox::Cancel,
QMessageBox::Yes
);

View File

@ -8,7 +8,7 @@ void RegionMapEntriesPixmapItem::draw() {
int entry_x, entry_y, entry_w, entry_h;
if (!entry.valid || entry.name == "MAPSEC_NONE") {
if (!entry.valid || entry.name == region_map->default_map_section) {
entry_x = entry_y = 0;
entry_w = entry_h = 1;
} else {

View File

@ -22,6 +22,7 @@ void SelectablePixmapItem::select(int x, int y, int width, int height)
this->selectionOffsetX = qMax(0, qMin(width, this->maxSelectionWidth));
this->selectionOffsetY = qMax(0, qMin(height, this->maxSelectionHeight));
this->draw();
emit this->selectionChanged(x, y, width, height);
}
void SelectablePixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
@ -76,6 +77,7 @@ void SelectablePixmapItem::updateSelection(int x, int y)
}
this->draw();
emit this->selectionChanged(x, y, width, height);
}
QPoint SelectablePixmapItem::getCellPos(QPointF pos)

View File

@ -113,7 +113,7 @@ void TilesetEditor::initUi() {
void TilesetEditor::setAttributesUi() {
// Behavior
if (Metatile::getBehaviorMask()) {
if (projectConfig.getMetatileBehaviorMask()) {
for (int num : project->metatileBehaviorMapInverse.keys()) {
this->ui->comboBox_metatileBehaviors->addItem(project->metatileBehaviorMapInverse[num], num);
}
@ -124,7 +124,7 @@ void TilesetEditor::setAttributesUi() {
}
// Terrain Type
if (Metatile::getTerrainTypeMask()) {
if (projectConfig.getMetatileTerrainTypeMask()) {
this->ui->comboBox_terrainType->addItem("Normal", TERRAIN_NONE);
this->ui->comboBox_terrainType->addItem("Grass", TERRAIN_GRASS);
this->ui->comboBox_terrainType->addItem("Water", TERRAIN_WATER);
@ -137,7 +137,7 @@ void TilesetEditor::setAttributesUi() {
}
// Encounter Type
if (Metatile::getEncounterTypeMask()) {
if (projectConfig.getMetatileEncounterTypeMask()) {
this->ui->comboBox_encounterType->addItem("None", ENCOUNTER_NONE);
this->ui->comboBox_encounterType->addItem("Land", ENCOUNTER_LAND);
this->ui->comboBox_encounterType->addItem("Water", ENCOUNTER_WATER);
@ -155,7 +155,7 @@ void TilesetEditor::setAttributesUi() {
this->ui->comboBox_layerType->addItem("Split - Bottom/Top", METATILE_LAYER_BOTTOM_TOP);
this->ui->comboBox_layerType->setEditable(false);
this->ui->comboBox_layerType->setMinimumContentsLength(0);
if (!Metatile::getLayerTypeMask()) {
if (!projectConfig.getMetatileLayerTypeMask()) {
// User doesn't have triple layer metatiles, but has no layer type attribute.
// Porymap is still using the layer type value to render metatiles, and with
// no mask set every metatile will be "Middle/Top", so just display the combo
@ -186,6 +186,10 @@ void TilesetEditor::initMetatileSelector()
connect(this->metatileSelector, &TilesetEditorMetatileSelector::selectedMetatileChanged,
this, &TilesetEditor::onSelectedMetatileChanged);
bool showGrid = porymapConfig.getShowTilesetEditorMetatileGrid();
this->ui->actionMetatile_Grid->setChecked(showGrid);
this->metatileSelector->showGrid = showGrid;
this->metatilesScene = new QGraphicsScene;
this->metatilesScene->addItem(this->metatileSelector);
this->metatileSelector->draw();
@ -202,6 +206,10 @@ void TilesetEditor::initMetatileLayersItem() {
connect(this->metatileLayersItem, &MetatileLayersItem::selectedTilesChanged,
this, &TilesetEditor::onMetatileLayerSelectionChanged);
bool showGrid = porymapConfig.getShowTilesetEditorLayerGrid();
this->ui->actionLayer_Grid->setChecked(showGrid);
this->metatileLayersItem->showGrid = showGrid;
this->metatileLayersScene = new QGraphicsScene;
this->metatileLayersScene->addItem(this->metatileLayersItem);
this->ui->graphicsView_metatileLayers->setScene(this->metatileLayersScene);
@ -373,10 +381,10 @@ void TilesetEditor::onSelectedMetatileChanged(uint16_t metatileId) {
this->ui->lineEdit_metatileLabel->setText(labels.owned);
this->ui->lineEdit_metatileLabel->setPlaceholderText(labels.shared);
this->ui->comboBox_metatileBehaviors->setNumberItem(this->metatile->behavior);
this->ui->comboBox_layerType->setNumberItem(this->metatile->layerType);
this->ui->comboBox_encounterType->setNumberItem(this->metatile->encounterType);
this->ui->comboBox_terrainType->setNumberItem(this->metatile->terrainType);
this->ui->comboBox_metatileBehaviors->setHexItem(this->metatile->behavior());
this->ui->comboBox_layerType->setHexItem(this->metatile->layerType());
this->ui->comboBox_encounterType->setHexItem(this->metatile->encounterType());
this->ui->comboBox_terrainType->setHexItem(this->metatile->terrainType());
}
void TilesetEditor::onHoveredTileChanged(uint16_t tile) {
@ -493,19 +501,19 @@ void TilesetEditor::on_checkBox_yFlip_stateChanged(int checked)
void TilesetEditor::on_comboBox_metatileBehaviors_currentTextChanged(const QString &metatileBehavior)
{
if (this->metatile) {
int behavior;
uint32_t behavior;
if (project->metatileBehaviorMap.contains(metatileBehavior)) {
behavior = project->metatileBehaviorMap[metatileBehavior];
} else {
// Check if user has entered a number value instead
bool ok;
behavior = metatileBehavior.toInt(&ok, 0);
behavior = metatileBehavior.toUInt(&ok, 0);
if (!ok) return;
}
// This function can also be called when the user selects
// a different metatile. Stop this from being considered a change.
if (this->metatile->behavior == static_cast<uint32_t>(behavior))
if (this->metatile->behavior() == behavior)
return;
Metatile *prevMetatile = new Metatile(*this->metatile);
@ -1036,6 +1044,18 @@ void TilesetEditor::on_actionShow_UnusedTiles_toggled(bool checked) {
this->tileSelector->draw();
}
void TilesetEditor::on_actionMetatile_Grid_triggered(bool checked) {
this->metatileSelector->showGrid = checked;
this->metatileSelector->draw();
porymapConfig.setShowTilesetEditorMetatileGrid(checked);
}
void TilesetEditor::on_actionLayer_Grid_triggered(bool checked) {
this->metatileLayersItem->showGrid = checked;
this->metatileLayersItem->draw();
porymapConfig.setShowTilesetEditorLayerGrid(checked);
}
void TilesetEditor::countMetatileUsage() {
// do not double count
metatileSelector->usedMetatiles.fill(0);
@ -1057,7 +1077,7 @@ void TilesetEditor::countMetatileUsage() {
// for each block in the layout, mark in the vector that it is used
for (int i = 0; i < layout->blockdata.length(); i++) {
uint16_t metatileId = layout->blockdata.at(i).metatileId;
uint16_t metatileId = layout->blockdata.at(i).metatileId();
if (metatileId < this->project->getNumMetatilesPrimary()) {
if (usesPrimary) metatileSelector->usedMetatiles[metatileId]++;
} else {
@ -1066,7 +1086,7 @@ void TilesetEditor::countMetatileUsage() {
}
for (int i = 0; i < layout->border.length(); i++) {
uint16_t metatileId = layout->border.at(i).metatileId;
uint16_t metatileId = layout->border.at(i).metatileId();
if (metatileId < this->project->getNumMetatilesPrimary()) {
if (usesPrimary) metatileSelector->usedMetatiles[metatileId]++;
} else {

View File

@ -12,6 +12,19 @@ TilesetEditorMetatileSelector::TilesetEditorMetatileSelector(Tileset *primaryTil
this->usedMetatiles.resize(Project::getNumMetatilesTotal());
}
int TilesetEditorMetatileSelector::numRows(int numMetatiles) {
int numMetatilesHigh = numMetatiles / this->numMetatilesWide;
if (numMetatiles % this->numMetatilesWide != 0) {
// Round up height for incomplete last row
numMetatilesHigh++;
}
return numMetatilesHigh;
}
int TilesetEditorMetatileSelector::numRows() {
return this->numRows(this->primaryTileset->metatiles.length() + this->secondaryTileset->metatiles.length());
}
QImage TilesetEditorMetatileSelector::buildAllMetatilesImage() {
return this->buildImage(0, this->primaryTileset->metatiles.length() + this->secondaryTileset->metatiles.length());
}
@ -25,11 +38,7 @@ QImage TilesetEditorMetatileSelector::buildSecondaryMetatilesImage() {
}
QImage TilesetEditorMetatileSelector::buildImage(int metatileIdStart, int numMetatiles) {
int numMetatilesHigh = numMetatiles / this->numMetatilesWide;
if (numMetatiles % this->numMetatilesWide != 0) {
// Round up height for incomplete last row
numMetatilesHigh++;
}
int numMetatilesHigh = this->numRows(numMetatiles);
int numPrimary = this->primaryTileset->metatiles.length();
int maxPrimary = Project::getNumMetatilesPrimary();
bool includesPrimary = metatileIdStart < maxPrimary;
@ -158,6 +167,22 @@ QPoint TilesetEditorMetatileSelector::getMetatileIdCoordsOnWidget(uint16_t metat
}
void TilesetEditorMetatileSelector::drawFilters() {
if (this->showGrid) {
QPixmap pixmap = this->pixmap();
QPainter painter(&pixmap);
const int numColumns = this->numMetatilesWide;
const int numRows = this->numRows();
for (int column = 1; column < numColumns; column++) {
int x = column * 32;
painter.drawLine(x, 0, x, numRows * 32);
}
for (int row = 1; row < numRows; row++) {
int y = row * 32;
painter.drawLine(0, y, numColumns * 32, y);
}
painter.end();
this->setPixmap(pixmap);
}
if (selectorShowUnused) {
drawUnused();
}

View File

@ -18,6 +18,7 @@ UIntSpinBox::UIntSpinBox(QWidget *parent)
};
void UIntSpinBox::setValue(uint32_t val) {
val = qMax(m_minimum, qMin(m_maximum, val));
if (m_value != val) {
m_value = val;
emit valueChanged(m_value);
@ -103,29 +104,37 @@ void UIntSpinBox::onEditFinished() {
QString input = this->lineEdit()->text();
auto state = this->validate(input, pos);
if (state == QValidator::Invalid)
return;
auto newValue = m_value;
if (state == QValidator::Acceptable) {
// Valid input
m_value = this->valueFromText(input);
newValue = this->valueFromText(input);
} else if (state == QValidator::Intermediate) {
// User has deleted all the number text.
// If they did this by selecting all text and then hitting delete
// make sure to put the cursor back in front of the prefix.
m_value = m_minimum;
newValue = m_minimum;
this->lineEdit()->setCursorPosition(m_prefix.length());
}
if (newValue != m_value) {
m_value = newValue;
emit this->valueChanged(m_value);
}
emit this->textChanged(input);
}
void UIntSpinBox::stepBy(int steps)
{
auto new_value = m_value;
if (steps < 0 && new_value + steps > new_value) {
new_value = 0;
} else if (steps > 0 && new_value + steps < new_value) {
new_value = UINT_MAX;
void UIntSpinBox::stepBy(int steps) {
auto newValue = m_value;
if (steps < 0 && newValue + steps > newValue) {
newValue = 0;
} else if (steps > 0 && newValue + steps < newValue) {
newValue = UINT_MAX;
} else {
new_value += steps;
newValue += steps;
}
this->setValue(new_value);
this->setValue(newValue);
}
QString UIntSpinBox::stripped(QString input) const {
@ -143,8 +152,9 @@ QValidator::State UIntSpinBox::validate(QString &input, int &pos) const {
if (copy.isEmpty())
return QValidator::Intermediate;
// Editing the prefix (if not deleting all text) is not allowed
if (pos < m_prefix.length())
// Editing the prefix (if not deleting all text) is not allowed.
// Nor is editing beyond the maximum value's character limit.
if (pos < m_prefix.length() || pos > (m_numChars + m_prefix.length()))
return QValidator::Invalid;
bool ok;