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:

  1. Improve the output of the merging operation significantly, supporting many more edge cases and handling merges with less complete mapping sets.
  2. Allow configuring (to some degree) the merging process to control some aspects of the default merge process.
  3. 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 a right 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:

  1. Duplicate mappings are possible
  2. 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:

  1. MappingSetMerger
  2. MappingSetMergerHandler

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.