From eeef6c485aef9cb47bf69bff996b6107df6e586b Mon Sep 17 00:00:00 2001 From: Jonathan G Rennison Date: Thu, 27 Jan 2022 23:41:32 +0000 Subject: [PATCH] Initial implementation of NewGRF feature ID mapping --- docs/newgrf-additions.html | 48 ++++++++++++++ src/lang/english.txt | 1 + src/newgrf.cpp | 124 ++++++++++++++++++++++++++++++++++++- src/newgrf.h | 42 +++++++++++++ src/newgrf_extension.cpp | 7 +++ 5 files changed, 219 insertions(+), 3 deletions(-) diff --git a/docs/newgrf-additions.html b/docs/newgrf-additions.html index e8b84a972f..10ef583aad 100644 --- a/docs/newgrf-additions.html +++ b/docs/newgrf-additions.html @@ -555,5 +555,53 @@

Road waypoint graphics (mappable type: road_waypoints)

This is 4 sprites, in the same order and format as the drive-through bus or truck road stop graphics.

This is indicated by the feature name: action5_road_waypoints, version 1

+
+

Action 14 - Feature ID Mapping

+

See Action 14 Specification and Feature Specifications for background information.

+

The feature ID mapping mechanism has the feature name: feature_id_mapping, this document describes version 1.

+

Users of this mechanism SHOULD at minimum test for the presence of the feature above or test variable 8D, below.

+

Feature ID Mapping: C "FIDM"

+

Each FIDM chunk (type C) describes an individual feature ID mapping.
+ Sub-chunks within each FIDM chunk may appear in any order, however except where otherwise noted each sub-chunk SHOULD only appear ONCE within an individual FIDM chunk.

+

Feature ID mapping can be safely used on implementations which do not implement the feature ID mapping mechanism if all sprites which use mapped feature IDs are skipped if one or more of: +

+ Unknown Action 14 blocks are ignored, and do not need to be skipped.

+

After this mapping, the mapped feature ID value can used in all sprites which expect a feature ID (including Action 0, Action 1, Action 2, Action 3, Action 4, Action D, Action 14).

+

Note that implementations which are not aware of a particular feature ID and which do not implement the feature ID mapping mechanism will automatically skip sprites with unknown feature IDs.
+ However: if/when the specification and/or the implementation assigns the chosen feature ID to an unrelated feature in future, sprites using that feature will not be skipped.
+ This means that the GRF has the potential to inexplicably fail in the distant future if the sprites are not correctly skipped.
+ Do not rely on the behaviour where unknown feature IDs are skipped.

+

Property Name: C "FIDM" -> T "NAME"

+

Within an FIDM chunk, the NAME text (type T) field contains the name of the feature to map. The value of the language ID byte is ignored.

+

Property ID: C "FIDM" -> B "FTID"

+

Within an FTID chunk, the FTID binary (type B) field contains the feature ID to allocate to the named feature. This is 1 byte.
+ Note that the remapped feature ID can only be used after this FIDM chunk in the GRF. It is not applied retrospectively to sprites earlier in the GRF file, or earlier parts of the current Action 14 sprite.
+ It is possible to override existing feature IDs, however this use is STRONGLY NOT RECOMMENDED due to a high potential for confusion when your code is inspected by someone else.
+ At the time of writing known existing feature IDs include the values: 0x00 - 0x13 (inclusive) and 0x48.

+

Success Indicator Global Variable 0x8D Bit: C "FIDM" -> B "SETT"

+

This behaves identically to the C "A0PM" -> B "SETT" case, above

+

Fallback Mode: C "FIDM" -> B "FLBK"

+

This behaves identically to the C "A0PM" -> B "FLBK" case, above

+

Example NFO:

+
+// Map feature "sample_feature" to feature id 0xB0, and set bit 6 of global variable 0x8D if successful
+-1 * -1 14
+	"C" "FIDM"
+		"T" "NAME" 00 "sample_feature" 00
+		"B" "FTID" \w1 B0
+		"B" "SETT" \w1 6
+		00
+	00
+....
+// Skip 3 sprites if bit 6 of global variable 0x8D is not set (indicating that the sample_feature feature is NOT present)
+-1 * -1  07 8D 01 \70 06 03
+// Use action 1 to define 1 sample_feature spriteset of 2 views
+-1 * -1 01 B0 01 02
+-1  sprites/sample.png 546 8 09 23 33 -26 0
+-1  sprites/sample.png 594 8 09 23 33 -5 0
+	
diff --git a/src/lang/english.txt b/src/lang/english.txt index 2e5430d08d..7111015e22 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -4224,6 +4224,7 @@ STR_NEWGRF_ERROR_TOO_MANY_NEWGRFS_LOADED :Too many NewGRF STR_NEWGRF_ERROR_STATIC_GRF_CAUSES_DESYNC :Loading {1:RAW_STRING} as static NewGRF with {RAW_STRING} could cause desyncs STR_NEWGRF_ERROR_UNEXPECTED_SPRITE :Unexpected sprite (sprite {3:NUM}) STR_NEWGRF_ERROR_UNKNOWN_PROPERTY :Unknown Action 0 property {4:HEX} (sprite {3:NUM}) +STR_NEWGRF_ERROR_UNIMPLEMETED_MAPPED_FEATURE_ID :Unimplemented remapped feature ID: name: {2:RAW_STRING}, mapped to: {5:HEX} (sprite {3:NUM}) STR_NEWGRF_ERROR_UNIMPLEMETED_MAPPED_PROPERTY :Unimplemented remapped Action 0 property feature: {4:HEX}, name: {2:RAW_STRING}, mapped to: {5:HEX} (sprite {3:NUM}) STR_NEWGRF_ERROR_UNIMPLEMETED_MAPPED_ACTION5_TYPE :Unimplemented remapped Action 5 type: name: {2:RAW_STRING}, mapped to: {4:HEX} (sprite {3:NUM}) STR_NEWGRF_ERROR_INVALID_ID :Attempt to use invalid ID (sprite {3:NUM}) diff --git a/src/newgrf.cpp b/src/newgrf.cpp index 387813d56c..ef4eff5236 100644 --- a/src/newgrf.cpp +++ b/src/newgrf.cpp @@ -4932,6 +4932,24 @@ static bool HandleChangeInfoResult(const char *caller, ChangeInfoResult cir, Grf static GrfSpecFeatureRef ReadFeature(uint8 raw_byte, bool allow_48 = false) { + if (unlikely(HasBit(_cur.grffile->ctrl_flags, GFCF_HAVE_FEATURE_ID_REMAP))) { + const GRFFeatureMapRemapSet &remap = _cur.grffile->feature_id_remaps; + if (remap.remapped_ids[raw_byte]) { + auto iter = remap.mapping.find(raw_byte); + const GRFFeatureMapRemapEntry &def = iter->second; + if (def.feature == GSF_ERROR_ON_USE) { + grfmsg(0, "Error: Unimplemented mapped feature: %s, mapped to: %02X", def.name, raw_byte); + GRFError *error = DisableGrf(STR_NEWGRF_ERROR_UNIMPLEMETED_MAPPED_FEATURE_ID); + error->data = stredup(def.name); + error->param_value[1] = GSF_INVALID; + error->param_value[2] = raw_byte; + } else if (def.feature == GSF_INVALID) { + grfmsg(2, "Ignoring unimplemented mapped feature: %s, mapped to: %02X", def.name, raw_byte); + } + return { def.feature, raw_byte }; + } + } + GrfSpecFeature feature; if (raw_byte >= GSF_REAL_FEATURE_END && !(allow_48 && raw_byte == 0x48)) { feature = GSF_INVALID; @@ -4971,6 +4989,15 @@ const char *GetFeatureString(GrfSpecFeatureRef feature) if (feature.id < GSF_END) { seprintf(buffer, lastof(buffer), "0x%02X (%s)", feature.raw_byte, _feature_names[feature.id]); } else { + if (unlikely(HasBit(_cur.grffile->ctrl_flags, GFCF_HAVE_FEATURE_ID_REMAP))) { + const GRFFeatureMapRemapSet &remap = _cur.grffile->feature_id_remaps; + if (remap.remapped_ids[feature.raw_byte]) { + auto iter = remap.mapping.find(feature.raw_byte); + const GRFFeatureMapRemapEntry &def = iter->second; + seprintf(buffer, lastof(buffer), "0x%02X (%s)", feature.raw_byte, def.name); + return buffer; + } + } seprintf(buffer, lastof(buffer), "0x%02X", feature.raw_byte); } return buffer; @@ -4978,7 +5005,16 @@ const char *GetFeatureString(GrfSpecFeatureRef feature) const char *GetFeatureString(GrfSpecFeature feature) { - return GetFeatureString(GrfSpecFeatureRef{ feature, feature }); + uint8 raw_byte = feature; + if (feature >= GSF_REAL_FEATURE_END) { + for (const auto &entry : _cur.grffile->feature_id_remaps.mapping) { + if (entry.second.feature == feature) { + raw_byte = entry.second.raw_id; + break; + } + } + } + return GetFeatureString(GrfSpecFeatureRef{ feature, raw_byte }); } struct GRFFilePropertyDescriptor { @@ -5059,7 +5095,7 @@ static void FeatureChangeInfo(ByteReader *buf) uint engine = buf->ReadExtendedByte(); if (feature >= GSF_END) { - grfmsg(1, "FeatureChangeInfo: Unsupported feature %s skipping", GetFeatureString(feature)); + grfmsg(1, "FeatureChangeInfo: Unsupported feature %s skipping", GetFeatureString(feature_ref)); return; } @@ -5194,7 +5230,7 @@ static void NewSpriteSet(ByteReader *buf) if (feature >= GSF_END) { _cur.skip_sprites = num_sets * num_ents; - grfmsg(1, "NewSpriteSet: Unsupported feature %s, skipping %d sprites", GetFeatureString(feature), _cur.skip_sprites); + grfmsg(1, "NewSpriteSet: Unsupported feature %s, skipping %d sprites", GetFeatureString(feature_ref), _cur.skip_sprites); return; } @@ -8758,6 +8794,53 @@ struct GRFPropertyMapAction { this->output_mask = 0; } + void ExecuteFeatureIDRemapping() + { + if (this->prop_id < 0) { + grfmsg(2, "Action 14 %s remapping: no feature ID defined, doing nothing", this->descriptor); + return; + } + if (this->name.empty()) { + grfmsg(2, "Action 14 %s remapping: no name defined, doing nothing", this->descriptor); + return; + } + SetBit(_cur.grffile->ctrl_flags, GFCF_HAVE_FEATURE_ID_REMAP); + bool success = false; + const char *str = this->name.c_str(); + extern const GRFFeatureMapDefinition _grf_remappable_features[]; + for (const GRFFeatureMapDefinition *info = _grf_remappable_features; info->name != nullptr; info++) { + if (strcmp(info->name, str) == 0) { + GRFFeatureMapRemapEntry &entry = _cur.grffile->feature_id_remaps.Entry(this->prop_id); + entry.name = info->name; + entry.feature = info->feature; + entry.raw_id = this->prop_id; + success = true; + break; + } + } + if (this->ttd_ver_var_bit > 0) { + SB(_cur.grffile->var8D_overlay, this->ttd_ver_var_bit, 1, success ? 1 : 0); + } + if (!success) { + if (this->fallback_mode == GPMFM_ERROR_ON_DEFINITION) { + grfmsg(0, "Error: Unimplemented mapped %s: %s, mapped to: 0x%02X", this->descriptor, str, this->prop_id); + GRFError *error = DisableGrf(STR_NEWGRF_ERROR_UNIMPLEMETED_MAPPED_FEATURE_ID); + error->data = stredup(str); + error->param_value[1] = GSF_INVALID; + error->param_value[2] = this->prop_id; + } else { + const char *str_store = stredup(str); + grfmsg(2, "Unimplemented mapped %s: %s, mapped to: %X, %s on use", + this->descriptor, str, this->prop_id, (this->fallback_mode == GPMFM_IGNORE) ? "ignoring" : "error"); + _cur.grffile->remap_unknown_property_names.emplace_back(str_store); + GRFFeatureMapRemapEntry &entry = _cur.grffile->feature_id_remaps.Entry(this->prop_id); + entry.name = str_store; + entry.feature = (this->fallback_mode == GPMFM_IGNORE) ? GSF_INVALID : GSF_ERROR_ON_USE; + entry.raw_id = this->prop_id; + } + } + } + void ExecutePropertyRemapping() { if (this->feature == GSF_INVALID) { @@ -8925,6 +9008,19 @@ static bool ChangePropertyRemapPropertyId(size_t len, ByteReader *buf) return true; } +/** Callback function for ->'FTID' to set the feature ID to which this feature is being mapped. */ +static bool ChangePropertyRemapFeatureId(size_t len, ByteReader *buf) +{ + GRFPropertyMapAction &action = _current_grf_property_map_action; + if (len != 1) { + grfmsg(2, "Action 14 %s mapping: expected 1 byte for '%s'->'FTID' but got " PRINTF_SIZE ", ignoring this field", action.descriptor, action.tag_name, len); + buf->Skip(len); + } else { + action.prop_id = buf->ReadByte(); + } + return true; +} + /** Callback function for ->'TYPE' to set the property ID to which this item is being mapped. */ static bool ChangePropertyRemapTypeId(size_t len, ByteReader *buf) { @@ -9050,6 +9146,26 @@ static bool ChangePropertyRemapSetOutputParam(size_t len, ByteReader *buf) return true; } +/** Action14 tags for the FIDM node */ +AllowedSubtags _tags_fidm[] = { + AllowedSubtags('NAME', ChangePropertyRemapName), + AllowedSubtags('FTID', ChangePropertyRemapFeatureId), + AllowedSubtags('FLBK', ChangePropertyRemapSetFallbackMode), + AllowedSubtags('SETT', ChangePropertyRemapSetTTDVerVarBit), + AllowedSubtags() +}; + +/** + * Callback function for 'FIDM' (feature ID mapping) + */ +static bool HandleFeatureIDMap(ByteReader *buf) +{ + _current_grf_property_map_action.Reset("FIDM", "feature"); + HandleNodes(buf, _tags_fidm); + _current_grf_property_map_action.ExecuteFeatureIDRemapping(); + return true; +} + /** Action14 tags for the A0PM node */ AllowedSubtags _tags_a0pm[] = { AllowedSubtags('NAME', ChangePropertyRemapName), @@ -9119,6 +9235,7 @@ static bool HandleAction5TypeMap(ByteReader *buf) AllowedSubtags _tags_root_static[] = { AllowedSubtags('INFO', _tags_info), AllowedSubtags('FTST', SkipInfoChunk), + AllowedSubtags('FIDM', SkipInfoChunk), AllowedSubtags('A0PM', SkipInfoChunk), AllowedSubtags('A2VM', SkipInfoChunk), AllowedSubtags('A5TM', SkipInfoChunk), @@ -9129,6 +9246,7 @@ AllowedSubtags _tags_root_static[] = { AllowedSubtags _tags_root_feature_tests[] = { AllowedSubtags('INFO', SkipInfoChunk), AllowedSubtags('FTST', HandleFeatureTestInfo), + AllowedSubtags('FIDM', HandleFeatureIDMap), AllowedSubtags('A0PM', HandleAction0PropertyMap), AllowedSubtags('A2VM', HandleAction2VariableMap), AllowedSubtags('A5TM', HandleAction5TypeMap), diff --git a/src/newgrf.h b/src/newgrf.h index 042bc68295..7c77d80f48 100644 --- a/src/newgrf.h +++ b/src/newgrf.h @@ -95,6 +95,7 @@ enum GrfSpecFeature { GSF_FAKE_STATION_STRUCT, ///< Fake station struct GrfSpecFeature for NewGRF debugging GSF_FAKE_END, ///< End of the fake features + GSF_ERROR_ON_USE = 0xFE, ///< An invalid value which generates an immediate error on mapping GSF_INVALID = 0xFF, ///< An invalid spec feature }; @@ -114,6 +115,39 @@ enum GRFPropertyMapFallbackMode { GPMFM_END, }; +struct GRFFeatureMapDefinition { + const char *name; // nullptr indicates the end of the list + GrfSpecFeature feature; + + /** Create empty object used to identify the end of a list. */ + GRFFeatureMapDefinition() : + name(nullptr), + feature((GrfSpecFeature)0) + {} + + GRFFeatureMapDefinition(GrfSpecFeature feature, const char *name) : + name(name), + feature(feature) + {} +}; + +struct GRFFeatureMapRemapEntry { + const char *name = nullptr; + GrfSpecFeature feature = (GrfSpecFeature)0; + uint8 raw_id = 0; +}; + +struct GRFFeatureMapRemapSet { + std::bitset<256> remapped_ids; + btree::btree_map mapping; + + GRFFeatureMapRemapEntry &Entry(uint8 raw_id) + { + this->remapped_ids.set(raw_id); + return this->mapping[raw_id]; + } +}; + struct GRFPropertyMapDefinition { const char *name; // nullptr indicates the end of the list int id; @@ -246,6 +280,11 @@ enum NewSignalAction3ID { NSA3ID_CUSTOM_SIGNALS = 0, ///< Action 3 ID for custom signal sprites }; +/** GRFFile control flags. */ +enum GRFFileCtrlFlags { + GFCF_HAVE_FEATURE_ID_REMAP = 0, ///< This GRF has one or more feature ID mappings +}; + /** Dynamic data of a loaded NewGRF */ struct GRFFile : ZeroedMemoryAllocator { char *filename; @@ -263,6 +302,7 @@ struct GRFFile : ZeroedMemoryAllocator { struct AirportSpec **airportspec; struct AirportTileSpec **airtspec; + GRFFeatureMapRemapSet feature_id_remaps; GRFFilePropertyRemapSet action0_property_remaps[GSF_END]; Action5TypeRemapSet action5_type_remaps; std::vector grf_variable_remaps; @@ -302,6 +342,8 @@ struct GRFFile : ZeroedMemoryAllocator { byte new_signal_ctrl_flags; ///< Ctrl flags for new signals byte new_signal_extra_aspects; ///< Number of extra aspects for new signals + byte ctrl_flags; ///< General GRF control flags + GRFFile(const struct GRFConfig *config); ~GRFFile(); diff --git a/src/newgrf_extension.cpp b/src/newgrf_extension.cpp index 94c518f300..c87a728c99 100644 --- a/src/newgrf_extension.cpp +++ b/src/newgrf_extension.cpp @@ -19,6 +19,7 @@ extern const GRFFeatureInfo _grf_feature_list[] = { GRFFeatureInfo("feature_test", 1), GRFFeatureInfo("property_mapping", 1), GRFFeatureInfo("variable_mapping", 1), + GRFFeatureInfo("feature_id_mapping", 1), GRFFeatureInfo("action5_type_id_mapping", 1), GRFFeatureInfo("action0_station_prop1B", 1), GRFFeatureInfo("action0_station_disallowed_bridge_pillars", 1), @@ -51,6 +52,12 @@ extern const GRFFeatureInfo _grf_feature_list[] = { GRFFeatureInfo(), }; +/** Action14 remappable feature list */ +extern const GRFFeatureMapDefinition _grf_remappable_features[] = { + GRFFeatureMapDefinition(), +}; + + /** Action14 Action0 remappable property list */ extern const GRFPropertyMapDefinition _grf_action0_remappable_properties[] = { GRFPropertyMapDefinition(GSF_STATIONS, A0RPI_STATION_MIN_BRIDGE_HEIGHT, "station_min_bridge_height"),