This adds SPDX license headers to all source code files, following the REUSE[1] specification. This does not include any asset files (such as generated JSON files, or textures). While REUSE does support doing so with ".license" files, for now we define these licences using the .reuse/dep5 file. [1]: https://reuse.software/
8.4 KiB
Architecture
CC: Tweaked has a rather complex project layout, as there's several use-cases we want to support (multiple mod loaders, usable outside of Minecraft). As such, it can be tricky to understand how the code is structured and how the various sub-projects interact. This document provides a high-level overview of the entire mod.
Project Outline
CC: Tweaked is split into 4 primary modules (core
, common
, fabric
, forge
). These themselves are then split into
a public API (i.e core-api
) and the actual implementation (i.e. core
).
-
core
: This contains the core "computer" part of ComputerCraft, such as the Lua VM, filesystem and builtin APIs. This is also where the Lua ROM is located (projects/core/src/main/resources/data/computercraft/lua
). Notably this project does not depend on Minecraft, making it possible to use it in emulators and other tooling. -
common
: This contains all non mod-loader-specific Minecraft code. This is where computers, turtles and peripherals are defined (and everything else Minecraft-related!).This project is separates client code into its own separate source set (suitably named
client
). This helps us ensure that server code can never reference client-only code (such as LWJGL). -
forge
andfabric
: These contain any mod-loader specific code.
When we need to call loader-specific code from our own code (for instance, sending network messages or firing
loader-specific events), we use a PlatformHelper
interface (defined in
projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java
). This abstracts over most
loader-specific code we need to use, and is then implemented by each mod-loader-specific project. The concrete
implementation is then loaded with Java's ServiceLoader
, in a design based on jaredlll08's
multi-loader template. We use a similar system for communicating between the API and its
implementation.
flowchart LR
subgraph Common
platform(PlatformHelper)
impl[AbstractComputerCraftAPI]
end
subgraph API
api(ComputerCraft API) --> impl
end
subgraph Forge[Forge]
platform --> forgePlatform[PlatformHelperImpl]
impl -.-> forgeImpl[ComputerCraftAPIImpl]
end
subgraph Fabric
platform --> fabricPlatform[PlatformHelperImpl]
impl -.-> fabricImpl[ComputerCraftAPIImpl]
end
Note the PlatformHelper
is only used when calling from our code into loader-specific code. While we use this to fire
events, we do not use it to subscribe to events. For that we just subscribe to the events in the loader-specific
project, and then dispatch to the common CommonHooks
(for shared code) and ClientHooks
(for client-specific code).
You may notice there's a couple of other, smaller modules in the codebase. These you can probably ignore, but are worth mentioning:
-
lints
: This defines an ErrorProne plugin which adds a couple of compile-time checks to our code. This is what enforces that no client-specific code is used inside themain
source set (and a couple of other things!). -
web
: This contains the additional tooling for building the documentation website, such as support for rendering recipes -
buildSrc
(in the base directory, not inprojects/
): This contains any build logic shared between modules. For instance,cc-tweaked.java-convention.gradle.kts
sets up the defaults for Java that we use across the whole project.
Note
The Forge and Fabric modules (and their API counterparts) depend on the common modules. However, in order to correctly process mixins we need to compile the common code along with the Forge/Fabric code. This leads to a slightly strange build process:
- In your IDE, Forge/Fabric depend on the common as normal.
- When building via Gradle, the common code is compiled alongside Forge/Fabric.
You shouldn't need to worry about this - it should all be set up automatically - but hopefully explains a little bit why our Gradle scripts are slightly odd!
Testing
CC: Tweaked has a small (though growing!) test suite to ensure various features behave correctly. Most tests are written in Java using JUnit, though we also make use of jqwik for property testing.
Test Fixtures
Some projects define an additional testFixtures
folder alongside their main test
code (i.e.
projects/core/src/testFixtures
). This source set contains test-related code which might be consumed in dependent
projects. For instance, core's test fixtures defines additional Hamcrest matchers, which are used in both core
and
common
's test suite.
Test fixtures may also define Test Interfaces. This is a pattern for writing tests to ensure that an implementation
obeys its interface's contract. For instance, we might have a ListContract
test, which asserts an abstract list
behaves as expected:
interface ListContract<T extends List<Integer>> {
T newList();
@Test
default void testAddInsert() {
var list = newList();
assertTrue(list.add(123));
assertTrue(list.contains(123));
}
}
We can then use this interface to create tests for a specific implementation:
class ArrayListTest implements ListContract<ArrayList<Integer>> {
@Override public ArrayList<Integer> newList() { return new ArrayList<>(); }
}
This is especially useful when testing PlatformHelper
and other mod loader abstractions.
Lua tests
While the majority of CC: Tweaked is written in Java, a significant portion of the code is written in Lua. As such, it's also useful to test that.
This is done by starting a Lua VM with all of ComputerCraft's APIs loaded, then starting a custom test framework
(mcfly.lua
). This test framework discovers tests and sends them back to the Java side. These are turned into JUnit
tests which are then in turn run on the computer again. This allows the tests to integrate with existing Java testing
tooling (for instance, XML test reports and IDE integration).
There's a slightly more detailed description of the process at ComputerTestDelegate.java
.
Game tests
CC: Tweaked also runs several tests in-game using Minecraft's gametest framework. These work by starting a Minecraft server and then, for each test, spawning a structure and then interacting with the blocks inside the structure, asserting they behave as expected.
Unlike most of our other tests, these are written in Kotlin. We make extensive use of extension methods to augment vanilla's own test classes, which helps give a more consistent feel to the API.
Each test works by defining a sequence of steps. Each step can either run an action (thenExecute
), sleep for a period
(thenIdle
) or sleep until a condition is met (thenWaitUntil
).
fun Some_test(context: GameTestHelper) = context.sequence {
thenExecute { context.setBlock(BlockPos(2, 2, 2), Blocks.AIR) }
thenIdle(4)
thenExecute { context.assertBlockHas(lamp, RedstoneLampBlock.LIT, false, "Lamp should not be lit") }
}
Some tests need to use Lua APIs from a computer, such as when testing turtle.dig
. In order to do this, we install
a custom "Lua" runtime (see ManagedComputers.kt
) which actually runs Java functions. Tests can then enqueue a function
to run on a particular computer and then wait for it to finish.
While the internals of this is quite complex, it ends up being a much nicer workflow than writing parts of the test in Lua. It also ends up being much more efficient, which is important when running a dozen tests at once!