From dc758d4310b5fe525443cd3884a2b8a9f0f714bb Mon Sep 17 00:00:00 2001 From: Algent Date: Fri, 14 Aug 2026 22:03:04 +0200 Subject: [PATCH] test: characterize MicroMaterialRegistry Freezes the registry lifecycle before the port: setupIDMap sorts the type map by name into the id array, ids are the index into it, materialID and materialName round trip, remapName resolves on both getMaterial(String) and materialID, getMissingId points at the registered placeholder rather than falling back to zero, ids round trip through the shared IDWriter, and a highlight renderer that claims the highlight short-circuits the default client renderer. Also freezes the two members IMicroMaterial supplies. A Java class cannot inherit them today, so they are observed through MissingMicroMaterial, which overrides only isTransparent and loadIcons. Two limits are deliberate and recorded in the test. The registry is global mutable state, so both test classes guard their registrations and assert only relative facts. The error paths are not covered at all: they call the microblock logger, which is null until MicroblockProxy.preInit runs. Adds a frozen Scala 2.11.5 consumer compiled against the reference dev jar at 52ff8ff, reading MODULE$ and the raw scala.Tuple2 array. 73 plain-JVM tests, all passing against the untouched Scala. --- src/test/fixtures/README.md | 12 + .../ReferenceScalaMicroMaterialConsumer.scala | 21 ++ ...terialRegistryBinaryCompatibilityTest.java | 75 ++++++ ...oMaterialRegistryCharacterizationTest.java | 242 ++++++++++++++++++ ...erenceScalaMicroMaterialConsumer.class.b64 | 42 +++ 5 files changed, 392 insertions(+) create mode 100644 src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMicroMaterialConsumer.scala create mode 100644 src/test/java/codechicken/microblock/MicroMaterialRegistryBinaryCompatibilityTest.java create mode 100644 src/test/java/codechicken/microblock/MicroMaterialRegistryCharacterizationTest.java create mode 100644 src/test/resources/compat/ReferenceScalaMicroMaterialConsumer.class.b64 diff --git a/src/test/fixtures/README.md b/src/test/fixtures/README.md index b32b766..92e7cd3 100644 --- a/src/test/fixtures/README.md +++ b/src/test/fixtures/README.md @@ -112,3 +112,15 @@ Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the referenc (SHA-256 `e931927e83134f57197aba082345ea96909291469d01a4f146836418a76bb3d1`). Class-file SHA-256 is `a03e8eb8566ae830829d59a379c67f355519bc82de24265baabd84720f068ce1`, stored as `src/test/resources/compat/ReferenceScalaSaw.class.b64`. + +### `ReferenceScalaMicroMaterialConsumer` + +`scala/codechicken/multipart/compat/ReferenceScalaMicroMaterialConsumer.scala` reads +`MicroMaterialRegistry$.MODULE$` and calls the instance methods ProjRed links against (`getMaterial(int)`, +`materialID`, `materialName`), plus the `scala.Tuple2` array `getIdMap` that extrautilities links against. It reads +`_1` and `_2` off the raw tuples rather than going through accessors, so the array element type is exercised too. + +Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the reference dev jar built at `52ff8ff` +(SHA-256 `122d89cf0a070cfd2325bbbcf4bb4550d6130bb7da1ffba1a3b2e2efea6ec7e5`). Class-file SHA-256 is +`e66be14e6422277cc9567acf8e96a26e419abf5ec6582085c8c6e32cc60aa599`, stored as +`src/test/resources/compat/ReferenceScalaMicroMaterialConsumer.class.b64`. diff --git a/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMicroMaterialConsumer.scala b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMicroMaterialConsumer.scala new file mode 100644 index 0000000..344e25f --- /dev/null +++ b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMicroMaterialConsumer.scala @@ -0,0 +1,21 @@ +package codechicken.multipart.compat + +import codechicken.microblock.MicroMaterialRegistry +import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial + +/** Compiled against the reference dev jar. Reads MicroMaterialRegistry$.MODULE$ and calls the instance methods ProjRed + * links against, plus the Tuple2-array getIdMap that extrautilities links against. + */ +class ReferenceScalaMicroMaterialConsumer { + def materialAt(id: Int): IMicroMaterial = MicroMaterialRegistry.getMaterial(id) + + def idOf(name: String): Int = MicroMaterialRegistry.materialID(name) + + def nameOf(id: Int): String = MicroMaterialRegistry.materialName(id) + + /** Exercises the raw scala.Tuple2 array descriptor rather than the accessors. */ + def idMapEntry(id: Int): String = { + val entry = MicroMaterialRegistry.getIdMap.apply(id) + entry._1 + "=" + (entry._2 eq materialAt(id)) + } +} diff --git a/src/test/java/codechicken/microblock/MicroMaterialRegistryBinaryCompatibilityTest.java b/src/test/java/codechicken/microblock/MicroMaterialRegistryBinaryCompatibilityTest.java new file mode 100644 index 0000000..e7336a4 --- /dev/null +++ b/src/test/java/codechicken/microblock/MicroMaterialRegistryBinaryCompatibilityTest.java @@ -0,0 +1,75 @@ +package codechicken.microblock; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; +import java.util.Scanner; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial; + +/** + * Loads a Scala class compiled against the reference dev jar. It reads MicroMaterialRegistry$.MODULE$ and calls the + * instance methods ProjRed links against, plus the scala.Tuple2 array getIdMap that extrautilities links against. + * + * Registration is global, so this registers its own uniquely named material and only asserts facts about it. + */ +class MicroMaterialRegistryBinaryCompatibilityTest { + + private static final String NAME = "test:binarycompat"; + + @BeforeAll + static void register() { + // The other test class may already have registered the placeholder; re-registering would hit the error path. + if (MicroMaterialRegistry.getMaterial(MissingMicroMaterial.key()) == null) { + MicroMaterialRegistry.registerMaterial(MissingMicroMaterial$.MODULE$, MissingMicroMaterial.key()); + } + if (MicroMaterialRegistry.getMaterial(NAME) == null) { + MicroMaterialRegistry.registerMaterial(MissingMicroMaterial$.MODULE$, NAME); + } + MicroMaterialRegistry$.MODULE$.setupIDMap(); + } + + @Test + void referenceScalaConsumerStillLinksAgainstTheSingletonAndIdMap() throws Exception { + Class fixtureClass = new FixtureClassLoader(MicroMaterialRegistry.class.getClassLoader()) + .define(loadFixture()); + Object fixture = fixtureClass.getDeclaredConstructor().newInstance(); + + int id = (Integer) fixtureClass.getMethod("idOf", String.class).invoke(fixture, NAME); + assertEquals(NAME, fixtureClass.getMethod("nameOf", int.class).invoke(fixture, id)); + + Object material = fixtureClass.getMethod("materialAt", int.class).invoke(fixture, id); + assertSame(MicroMaterialRegistry.getMaterial(id), material); + assertSame(MicroMaterialRegistry.getMaterial(id), (IMicroMaterial) material); + + // Reads the raw scala.Tuple2 array rather than the accessors. + assertEquals(NAME + "=true", fixtureClass.getMethod("idMapEntry", int.class).invoke(fixture, id)); + } + + private static byte[] loadFixture() { + InputStream input = Objects.requireNonNull( + MicroMaterialRegistryBinaryCompatibilityTest.class + .getResourceAsStream("/compat/ReferenceScalaMicroMaterialConsumer.class.b64")); + try (Scanner scanner = new Scanner(input, StandardCharsets.US_ASCII.name()).useDelimiter("\\A")) { + return Base64.getMimeDecoder().decode(scanner.next()); + } + } + + private static final class FixtureClassLoader extends ClassLoader { + + private FixtureClassLoader(ClassLoader parent) { + super(parent); + } + + private Class define(byte[] bytecode) { + return defineClass(null, bytecode, 0, bytecode.length); + } + } +} diff --git a/src/test/java/codechicken/microblock/MicroMaterialRegistryCharacterizationTest.java b/src/test/java/codechicken/microblock/MicroMaterialRegistryCharacterizationTest.java new file mode 100644 index 0000000..6bae44f --- /dev/null +++ b/src/test/java/codechicken/microblock/MicroMaterialRegistryCharacterizationTest.java @@ -0,0 +1,242 @@ +package codechicken.microblock; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.lang.reflect.Proxy; + +import net.minecraft.block.Block.SoundType; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.IIcon; +import net.minecraft.util.MovingObjectPosition; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutput; +import codechicken.lib.data.MCDataOutputWrapper; +import codechicken.lib.vec.Cuboid6; +import codechicken.lib.vec.Vector3; +import codechicken.microblock.MicroMaterialRegistry.IMicroHighlightRenderer; +import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial; + +/** + * The registry is global mutable state, so everything is registered once in {@link #registerAndFreezeIdMap()} and the + * cases below only read it. Error paths are not covered: they call the microblock logger, which is null until + * MicroblockProxy.preInit runs, so they cannot execute headless. + */ +class MicroMaterialRegistryCharacterizationTest { + + private static final TestMaterial STONE = new TestMaterial(false, 3); + private static final TestMaterial GLASS = new TestMaterial(true, 1); + + @BeforeAll + static void registerAndFreezeIdMap() { + // Registration is global and re-registering hits the logger-backed error path, which cannot run headless. + if (MicroMaterialRegistry.getMaterial(MissingMicroMaterial.key()) == null) { + MicroMaterialRegistry.registerMaterial(MissingMicroMaterial$.MODULE$, MissingMicroMaterial.key()); + } + MicroMaterialRegistry.registerMaterial(STONE, "test:stone"); + MicroMaterialRegistry.registerMaterial(GLASS, "test:glass"); + MicroMaterialRegistry.remapName("test:oldglass", "test:glass"); + // setupIDMap is package private in Scala, so it has no static forwarder and must go through the singleton. + MicroMaterialRegistry$.MODULE$.setupIDMap(); + } + + @Test + void idMapIsSortedByNameAndDrivesBothLookups() { + String[] names = new String[MicroMaterialRegistry.getIdMap().length]; + for (int i = 0; i < names.length; i++) { + names[i] = MicroMaterialRegistry.getIdMap()[i]._1(); + } + + String[] sorted = names.clone(); + java.util.Arrays.sort(sorted); + assertArrayEquals(sorted, names); + + for (int i = 0; i < names.length; i++) { + assertEquals(i, MicroMaterialRegistry.materialID(names[i])); + assertEquals(names[i], MicroMaterialRegistry.materialName(i)); + assertSame(MicroMaterialRegistry.getIdMap()[i]._2(), MicroMaterialRegistry.getMaterial(i)); + } + } + + @Test + void getMaterialByNameResolvesRemapsAndReturnsNullWhenUnknown() { + assertSame(GLASS, MicroMaterialRegistry.getMaterial("test:glass")); + assertSame(GLASS, MicroMaterialRegistry.getMaterial("test:oldglass")); + assertNull(MicroMaterialRegistry.getMaterial("test:nosuchmaterial")); + } + + @Test + void materialIdResolvesRemappedNames() { + assertEquals(MicroMaterialRegistry.materialID("test:glass"), MicroMaterialRegistry.materialID("test:oldglass")); + } + + @Test + void missingIdPointsAtTheMissingMaterialRatherThanZero() { + assertEquals( + MicroMaterialRegistry.materialID(MissingMicroMaterial.key()), + MicroMaterialRegistry.getMissingId()); + assertSame( + MissingMicroMaterial$.MODULE$, + MicroMaterialRegistry.getMaterial(MicroMaterialRegistry.getMissingId())); + } + + @Test + void materialIdsRoundTripThroughTheIdWriter() throws Exception { + for (int id = 0; id < MicroMaterialRegistry.getIdMap().length; id++) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + MCDataOutput output = new MCDataOutputWrapper(new DataOutputStream(bytes)); + MicroMaterialRegistry.writeMaterialID(output, id); + + byte[] encoded = bytes.toByteArray(); + assertEquals(1, encoded.length, "A small id map must still use the byte carrier"); + assertEquals(id, MicroMaterialRegistry.readMaterialID(input(encoded))); + } + } + + @Test + void highlightRendererShortCircuitsTheDefaultRenderer() { + RecordingRenderer accepting = new RecordingRenderer(true); + MicroMaterialRegistry.registerHighlightRenderer(accepting); + + // A renderer that claims the highlight stops the default client renderer being reached at all. + assertTrue(MicroMaterialRegistry.renderHighlight(null, null, null, 2, 0)); + assertEquals(1, accepting.calls); + } + + /** + * MissingMicroMaterial overrides only isTransparent and loadIcons, so it inherits canRenderInPass and isSolid from + * the trait. A Java class cannot inherit them today, so this is the only way to observe the supplied values. + */ + @Test + void materialInterfaceSuppliesRenderPassAndSolidity() { + assertTrue(MissingMicroMaterial.canRenderInPass(0)); + assertFalse(MissingMicroMaterial.canRenderInPass(1)); + assertFalse(MissingMicroMaterial.canRenderInPass(-1)); + + assertFalse(MissingMicroMaterial.isTransparent()); + assertTrue(MissingMicroMaterial.isSolid()); + } + + private static MCDataInput input(byte[] bytes) { + DataInputStream data = new DataInputStream(new ByteArrayInputStream(bytes)); + return (MCDataInput) Proxy.newProxyInstance( + MCDataInput.class.getClassLoader(), + new Class[] { MCDataInput.class }, + (proxy, method, arguments) -> { + switch (method.getName()) { + case "readUByte": + return (short) data.readUnsignedByte(); + case "readUShort": + return data.readUnsignedShort(); + case "readInt": + return data.readInt(); + default: + throw new AssertionError("Unexpected read method: " + method.getName()); + } + }); + } + + private static final class RecordingRenderer implements IMicroHighlightRenderer { + + private final boolean handled; + private int calls; + + private RecordingRenderer(boolean handled) { + this.handled = handled; + } + + @Override + public boolean renderHighlight(EntityPlayer player, MovingObjectPosition hit, CommonMicroClass mcrClass, + int size, int material) { + calls++; + return handled; + } + } + + /** Mirrors the forwarders Scala emits, so this file compiles against the trait and against the Java interface. */ + private static final class TestMaterial implements IMicroMaterial { + + private final boolean transparent; + private final int cutterStrength; + + private TestMaterial(boolean transparent, int cutterStrength) { + this.transparent = transparent; + this.cutterStrength = cutterStrength; + } + + @Override + public IIcon getBreakingIcon(int side) { + return null; + } + + @Override + public void loadIcons() {} + + @Override + public void renderMicroFace(Vector3 pos, int pass, Cuboid6 bounds) {} + + @Override + public boolean canRenderInPass(int pass) { + return pass == 0; + } + + @Override + public boolean isTransparent() { + return transparent; + } + + @Override + public int getLightValue() { + return 0; + } + + @Override + public float getStrength(EntityPlayer player) { + return 1f; + } + + @Override + public String getLocalizedName() { + return "Test Material"; + } + + @Override + public ItemStack getItem() { + return null; + } + + @Override + public int getCutterStrength() { + return cutterStrength; + } + + @Override + public SoundType getSound() { + return null; + } + + @Override + public boolean isSolid() { + return !isTransparent(); + } + + @Override + public float explosionResistance(Entity entity) { + return 1f; + } + } +} diff --git a/src/test/resources/compat/ReferenceScalaMicroMaterialConsumer.class.b64 b/src/test/resources/compat/ReferenceScalaMicroMaterialConsumer.class.b64 new file mode 100644 index 0000000..3dd7191 --- /dev/null +++ b/src/test/resources/compat/ReferenceScalaMicroMaterialConsumer.class.b64 @@ -0,0 +1,42 @@ +yv66vgAAADQAXAEAQGNvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFN +aWNyb01hdGVyaWFsQ29uc3VtZXIHAAEBABBqYXZhL2xhbmcvT2JqZWN0BwADAQApUmVmZXJlbmNl +U2NhbGFNaWNyb01hdGVyaWFsQ29uc3VtZXIuc2NhbGEBAB5Mc2NhbGEvcmVmbGVjdC9TY2FsYVNp +Z25hdHVyZTsBAAVieXRlcwEB5AYBBTNBIQECARMJGSMrGjRmZRZ0Ny1aKmRDMgxXKlsyc182C0cv +GjpqQzIcdU5cOnZbFhQoQkECBQMZGXcuHDliaSoRUUFCAQpbVmRHLxs5YmVSVBFhQgEMRz4kV20Z +NWpHLixnbgEBFAUBUQFDQQYPGwVhISJBBwILTRwXDVwxCgU9YSFBQiFvc0orZw1DAxIBEQUhIwEE +PVM6TEdPEAsCJ0ERQQNBBwIFISlhAwFDAS8FUVEuGTtmZSYMRy4ROxUFYUEDQ0ENJh0JUSJFBAIc +QTkRQWRIBwI7KRFhBEMBB3lJfHcOHiAKAx1JISEJBAIVNUw3TV04Y1k+HDcuAwIkSQUpUipbMnNf +NgtHLxo6akMyFFZtWjV0aUpMKEJBEQcTCTFzRQEIShsmHCdvXCdiaRYUGC4ZNwsFDSIDIkIVFgEE +URNBQTVlIQlZMSYDAi0ZCRkRShw7CQs5AkERQRgCCSUkd0oaCwNVQUJRIU0XQQJJCkFBXDFuS0IR +MUcOCAMXUUohIQ4HAg1BE1gNWjNnEwk5BEgBBFRpSkxnbhoGA2sxQVFBDwEFAm0KYUFcMW5LPjNH +QwEaPREVSRMIMQErERVxBAEiAUADKUlHLVQxcQs6JCgvHwsDZQFDUSFLH0ECKQIBAAptYXRlcmlh +bEF0AQBAKEkpTGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9NYXRlcmlhbFJlZ2lzdHJ5JElN +aWNyb01hdGVyaWFsOwEALWNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9NYXRlcmlhbFJlZ2lz +dHJ5JAcACwEAB01PRFVMRSQBAC9MY29kZWNoaWNrZW4vbWljcm9ibG9jay9NaWNyb01hdGVyaWFs +UmVnaXN0cnkkOwwADQAOCQAMAA8BAAtnZXRNYXRlcmlhbAwAEQAKCgAMABIBAAR0aGlzAQBCTGNv +ZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFNaWNyb01hdGVyaWFsQ29u +c3VtZXI7AQACaWQBAAFJAQAEaWRPZgEAFShMamF2YS9sYW5nL1N0cmluZzspSQEACm1hdGVyaWFs +SUQMABoAGQoADAAbAQAEbmFtZQEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABm5hbWVPZgEAFShJKUxq +YXZhL2xhbmcvU3RyaW5nOwEADG1hdGVyaWFsTmFtZQwAIQAgCgAMACIBAAppZE1hcEVudHJ5AQAI +Z2V0SWRNYXABABEoKVtMc2NhbGEvVHVwbGUyOwwAJQAmCgAMACcBACZzY2FsYS9jb2xsZWN0aW9u +L211dGFibGUvU3RyaW5nQnVpbGRlcgcAKQEABjxpbml0PgEAAygpVgwAKwAsCgAqAC0BAAxzY2Fs +YS9UdXBsZTIHAC8BAAJfMQEAFCgpTGphdmEvbGFuZy9PYmplY3Q7DAAxADIKADAAMwEAEGphdmEv +bGFuZy9TdHJpbmcHADUBAAZhcHBlbmQBADwoTGphdmEvbGFuZy9PYmplY3Q7KUxzY2FsYS9jb2xs +ZWN0aW9uL211dGFibGUvU3RyaW5nQnVpbGRlcjsMADcAOAoAKgA5AQABPQgAOwEAAl8yDAA9ADIK +ADAAPgwACQAKCgACAEABABpzY2FsYS9ydW50aW1lL0JveGVzUnVuVGltZQcAQgEADGJveFRvQm9v +bGVhbgEAFihaKUxqYXZhL2xhbmcvQm9vbGVhbjsMAEQARQoAQwBGAQAIdG9TdHJpbmcBABQoKUxq +YXZhL2xhbmcvU3RyaW5nOwwASABJCgAqAEoBAAVlbnRyeQEADkxzY2FsYS9UdXBsZTI7CgAEAC0B +ADtjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01pY3JvTWF0ZXJpYWxSZWdpc3RyeSRJTWljcm9NYXRl +cmlhbAcATwEALGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9NYXRlcmlhbFJlZ2lzdHJ5BwBR +AQAOSU1pY3JvTWF0ZXJpYWwBAARDb2RlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAPTGluZU51bWJl +clRhYmxlAQANU3RhY2tNYXBUYWJsZQEAClNvdXJjZUZpbGUBAAxJbm5lckNsYXNzZXMBABlSdW50 +aW1lVmlzaWJsZUFubm90YXRpb25zAQAIU2NhbGFTaWcAIQACAAQAAAAAAAUAAQAJAAoAAQBUAAAA +PAACAAIAAAAIsgAQG7YAE7AAAAACAFUAAAAWAAIAAAAIABQAFQAAAAAACAAWABcAAQBWAAAABgAB +AAAACgABABgAGQABAFQAAAA8AAIAAgAAAAiyABArtgAcrAAAAAIAVQAAABYAAgAAAAgAFAAVAAAA +AAAIAB0AHgABAFYAAAAGAAEAAAAMAAEAHwAgAAEAVAAAADwAAgACAAAACLIAEBu2ACOwAAAAAgBV +AAAAFgACAAAACAAUABUAAAAAAAgAFgAXAAEAVgAAAAYAAQAAAA4AAQAkACAAAQBUAAAApwAEAAMA +AAA6sgAQtgAoGzJNuwAqWbcALiy2ADTAADa2ADoSPLYAOiy2AD8qG7YAQaYABwSnAAQDuABHtgA6 +tgBLsAAAAAMAVQAAACAAAwAAADoAFAAVAAAAAAA6ABYAFwABAAkAMABMAE0AAgBWAAAACgACAAAA +EgAJABMAVwAAACUAAv8ALwADBwACAQcAMAABBwAq/wAAAAMHAAIBBwAwAAIHACoBAAEAKwAsAAEA +VAAAAC8AAQABAAAABSq3AE6xAAAAAgBVAAAADAABAAAABQAUABUAAABWAAAABgABAAAAFQAEAFgA +AAACAAUAWQAAAAoAAQBQAFIAUwYJAFoAAAALAAEABgABAAdzAAgAWwAAAAMFAAA=