Initial implementation of NewGRF feature ID mapping

pull/363/head
Jonathan G Rennison 2 years ago
parent ef3916928c
commit eeef6c485a

@ -555,5 +555,53 @@
<h4 id="road_waypoints">Road waypoint graphics (mappable type: road_waypoints)</h4>
<p>This is 4 sprites, in the same order and format as the drive-through bus or truck road stop graphics.</p>
<p>This is indicated by the feature name: <font face="monospace">action5_road_waypoints</font>, version 1</p>
<br />
<h3 id="feature-id-mapping">Action 14 - Feature ID Mapping</h3>
<p>See <a href="https://newgrf-specs.tt-wiki.net/wiki/Action14">Action 14 Specification</a> and <a href="https://newgrf-specs.tt-wiki.net/wiki/Features">Feature Specifications</a> for background information.</p>
<p>The feature ID mapping mechanism has the feature name: <font face="monospace">feature_id_mapping</font>, this document describes version 1.</p>
<p>Users of this mechanism SHOULD at minimum test for the presence of the feature above or test variable 8D, below.</p>
<h4 id="FIDM">Feature ID Mapping: C "FIDM"</h4>
<p>Each FIDM chunk (type C) describes an individual feature ID mapping.<br />
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.</p>
<p>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:
<ul>
<li>The global variable 0x8D is checked to determine whether the feature ID mapping operation was successful.</li>
<li>The feature name <font face="monospace">feature_id_mapping</font> is checked for.</li>
</ul>
Unknown Action 14 blocks are ignored, and do not need to be skipped.</p>
<p>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).</p>
<p><b>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.<br />
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.<br />
This means that the GRF has the potential to inexplicably fail in the distant future if the sprites are not correctly skipped.<br />
Do not rely on the behaviour where unknown feature IDs are skipped.</b></p>
<h4 id="FIDM-NAME">Property Name: C "FIDM" -> T "NAME"</h4>
<p>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.</p>
<h4 id="FIDM-FTID">Property ID: C "FIDM" -> B "FTID"</h4>
<p>Within an FTID chunk, the FTID binary (type B) field contains the feature ID to allocate to the named feature. This is 1 byte.<br />
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.<br />
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.<br />
At the time of writing known existing feature IDs include the values: 0x00 - 0x13 (inclusive) and 0x48.</p>
<h4 id="FIDM-SETT">Success Indicator Global Variable 0x8D Bit: C "FIDM" -> B "SETT"</h4>
<p>This behaves identically to the <a href="#A0PM-SETT">C "A0PM" -> B "SETT"</a> case, above</p>
<h4 id="FIDM-FLBK">Fallback Mode: C "FIDM" -> B "FLBK"</h4>
<p>This behaves identically to the <a href="#A0PM-FLBK">C "A0PM" -> B "FLBK"</a> case, above</p>
<h4 id="FIDM-example">Example NFO:</h4>
<pre>
// 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
</pre>
</body>
</html>

@ -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})

@ -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),

@ -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<uint8, GRFFeatureMapRemapEntry> 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<GRFVariableMapEntry> 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();

@ -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"),

Loading…
Cancel
Save