Lorenz 0.5.4
Lorenz 0.5.4 includes yet-more bug fixes, and a new (and vastly more powerful) mapping merging API.
The ProGuard mapping reader now also reads field types.
The Bombe dependency has been bumped to 0.3.4, resolving a NullPointerException
that would occur when using the ClassLoaderClassProvider
.
Merging
Lorenz now has a new mappings merging API which aims at fulfilling 3 main goals:
- Improve the output of the merging operation significantly, supporting many more edge cases and handling merges with less complete mapping sets.
- Allow configuring (to some degree) the merging process to control some aspects of the default merge process.
- Provide an API which is extensible and modifiable at multiple levels to enable custom merge situations.
There are a limited number of possible edge cases one would expect to run into when merging, so we laid them all out and made decisions on how best to handle them in the default case. Due to the extensible API users can override and change any of the default behavioe as necessary. The default behavior is laid out as follows:
Left | Right | Output | Note |
---|---|---|---|
A -> B |
B -> C |
A -> C |
Typical case, easiest to handle |
A -> B |
Missing | A -> B |
Standalone mappings get copied |
Missing | B -> C |
B -> C |
Standalone mappings get copied |
A -> B |
X -> Y |
A -> B and X -> Y |
This is no different than the 2 above cases with missing mappings on each side. This is just meant to be a further example that if two unrelated mappings are present, a standard merger won’t know how to handle them other than copying both. |
A -> B |
A -> C |
A -> C |
By default the right mapping is considered the “most up to date” mappings, so in the case where both mapping sets provide mappings for the same obfuscated name, the right mapping is used in the default implementation. |
A -> B (types) |
B -> B (types) and A -> C (names) |
A -> B (types) and A -> C (names) |
This is an example of a special case situation where the left mapping only maps types, then the second mapping set only maps members, but from the expectation that the first mapping set was already applied. The default implementation should handle this case correctly. |
How to read the above chart: Each mapping operation is made up of a
left
side and aright
side. The chart represents this with the directional arrow->
. Merge operations in the table apply to any individual mapping, not the whole mapping set. That is, a field name mapping merging with another field name, or a class name merging with another class name.Handling merges with less complete mappings sets are what is intended by the last row.
Directionality
It may seem a natural goal for merging mapping sets is for the merge operation to be commutative - in other words, when
merging A -> B
and B -> C
you would get the same result as when merging C -> B
and B -> A
(except, reversed).
This is true for most cases, but for some special cases - most notably duplicate mappings - this property just can’t be handled in any reasonable way. In these situations where a decision has to be made regarding whether to keep one mapping or another (and neither being a better option by itself) we use the position of the mapping as the heuristic for which one to keep.
When merging A -> B
and B -> C
the names you get in the output are C
. Because of this, the right
side of a merge
is considered the more correct set of names (since it is the output). If the merge operation were to be reversed A
would be preferred instead. Because of this it is important to consider which direction works best for you merge.
Other Scenarios
There’s also 2 other factors necessary to consider when merging mapping sets that we’ll go over:
- Duplicate mappings are possible
- Field mappings can have type signature
Handling Duplicates
A duplicate mapping is when both mapping sets define a mapping for the same obfuscated member. For example, if you were
to merge the following mappings, they would be considered duplicated because they both share com.example.Example
as
the obfuscated name.
com.example.Example -> com.example.OtherExample1
com.example.Example -> com.example.OtherExample2
For the default merge implementation right
mappings take priority for all duplicate merge scenarios. So in this case
the mapping which would be present in the output mapping set would be:
com.example.Example -> com.example.OtherExample2
Handling Field Types
Field mappings can have types, but often don’t. Because Java doesn’t allow duplicate field names visible in any class it’s not always necessary to explicitly list the type of the field. The complication comes when merging mapping sets where one mapping set does and the other doesn’t contain field types. In this scenario looking up the field from one mapping set to another in the default case by simply using the field signature would fail, as the type descriptor is missing in one of the mapping sets.
The new merge system provides a configuration option for whether to handle this case. The default options is LOOSE
-
that is, first check for the signature, but also check for just the field name and use it if the field
signature can’t be found. This can be switched to STRICT
where all field mappings must match signatures exactly.
Merging Code and API
The new merging system is made up of 2 main classes:
The JavaDocs for each class go into considerable detail on exactly what each class is and how it’s used, but here’s a quick rundown:
MappingSetMerger
is the entry point for the merge operation. It’s an interface with a default implementation which
can be retrieved with MappingSetMerger.create()
. It does the job of walking the class hierarchy of both mapping sets
and finding the members of each side that should be merged together. It decides which members to include based on the
MergeConfig
provided in the call to create()
(or just the default config if none was provided) and determines which individual
case each merge is.
MappingSetMerger
is available with an overridable default implementation in case it’s necessary to modify how the
above cases are decided individually. It is designed to let a user override individual methods in order to change said
logic when necessary, but the general idea is that in most cases you’ll never need to modify the default implementation
of MappingSetMerger
.
MappingSetMerger
doesn’t handle actually merging the mappings, all it does is recognize which type of merge each merge
is, and delegates the handling of that merge to MappingSetMergerHandler
. This handler class has individual methods for
each mapping case, and is a fully-default interface. This means you can implement MappingSetMergerHandler
and override
only the mapping cases you want to modify for your custom merge operation. The merge cases are all described by the
method names (and explained in further detail in the JavaDoc), and look like:
FieldMapping mergeFieldMappings(
final FieldMapping left,
final FieldMapping strictRight,
final FieldMapping looseRight,
final ClassMapping<?, ?> target,
final MergeContext context
);
FieldMapping mergeDuplicateFieldMappings(
final FieldMapping left,
final FieldMapping strictRightDuplicate,
final FieldMapping looseRightDuplicate,
final FieldMapping strictRightContinuation,
final FieldMapping looseRightContinuation,
final ClassMapping<?, ?> target,
final MergeContext context
);
FieldMapping addLeftFieldMapping(final FieldMapping left, final ClassMapping<?, ?> target, final MergeContext context);
FieldMapping addRightFieldMapping(final FieldMapping right, final ClassMapping<?, ?> target, final MergeContext context);
If you only need some custom logic when the right
mapping set provides a mapping that the left
mapping set doesn’t
provide, then you could just override addRightFieldMapping
with your custom logic and leave everything else default.
You wouldn’t need to worry about walking the hierarchy or copying of the other members or handling any other special
cases.
This separation of concerns between determining what kind of merge each case is and actually handling the merge operation for each merge case hopefully will allow you to easily customize mapping merges for whatever custom situation you have.
Existing Code
Each of the existing merge()
methods on each of the mapping classes have all been updated to use this new merge
system. The methods haven’t been removed and are still binary compatible with any existing code you have which may be
using it. The output of the merge may differ however, as the logic has been considerably modified as described above.
One Last Thing
The default implementation of MappingSetMerger
merges top-level classes in parallel. This is okay because
MappingSetMergerHandler
is a stateless class, and MappingSetMerger
is completely thread-safe. Due to this merge time
fell by as much as half of the previous merge time when testing against Minecraft mapping sets.