jOOR - Fluent Reflection in Java jOOR is a very simple fluent API that gives access to your Java Class structures in a more intuitive way. The JDK's reflection APIs are hard and verbose to use. Other languages have much simpler constructs to access type meta information at runtime. Let us make Java reflection better.

Related tags

Introspection jOOR
Overview

Overview

jOOR stands for jOOR Object Oriented Reflection. It is a simple wrapper for the java.lang.reflect package.

jOOR's name is inspired by jOOQ, a fluent API for SQL building and execution.

Dependencies

None!

Download

For use with Java 9+

<dependency>
  <groupId>org.jooq</groupId>
  <artifactId>joor</artifactId>
  <version>0.9.13</version>
</dependency>

For use with Java 8+

<dependency>
  <groupId>org.jooq</groupId>
  <artifactId>joor-java-8</artifactId>
  <version>0.9.13</version>
</dependency>

For use with Java 6+

<dependency>
  <groupId>org.jooq</groupId>
  <artifactId>joor-java-6</artifactId>
  <version>0.9.13</version>
</dependency>

Simple example

// All examples assume the following static import:
import static org.joor.Reflect.*;

String world = onClass("java.lang.String") // Like Class.forName()
                .create("Hello World")     // Call most specific matching constructor
                .call("substring", 6)      // Call most specific matching substring() method
                .call("toString")          // Call toString()
                .get();                    // Get the wrapped object, in this case a String

Proxy abstraction

jOOR also gives access to the java.lang.reflect.Proxy API in a simple way:

public interface StringProxy {
  String substring(int beginIndex);
}

String substring = onClass("java.lang.String")
                    .create("Hello World")
                    .as(StringProxy.class) // Create a proxy for the wrapped object
                    .substring(6);         // Call a proxy method

Runtime compilation of Java code

jOOR has an optional dependency on the java.compiler module and simplifies access to javax.tools.JavaCompiler through the following API:

Supplier<String> supplier = Reflect.compile(
    "com.example.HelloWorld",
    "package com.example;\n" +
    "class HelloWorld implements java.util.function.Supplier<String> {\n" +
    "    public String get() {\n" +
    "        return \"Hello World!\";\n" +
    "    }\n" +
    "}\n").create().get();

// Prints "Hello World!"
System.out.println(supplier.get());

Comparison with standard java.lang.reflect

jOOR code:

Employee[] employees = on(department).call("getEmployees").get();

for (Employee employee : employees) {
  Street street = on(employee).call("getAddress").call("getStreet").get();
  System.out.println(street);
}

The same example with normal reflection in Java:

try {
  Method m1 = department.getClass().getMethod("getEmployees");
  Employee[] employees = (Employee[]) m1.invoke(department);

  for (Employee employee : employees) {
    Method m2 = employee.getClass().getMethod("getAddress");
    Address address = (Address) m2.invoke(employee);

    Method m3 = address.getClass().getMethod("getStreet");
    Street street = (Street) m3.invoke(address);

    System.out.println(street);
  }
}

// There are many checked exceptions that you are likely to ignore anyway 
catch (Exception ignore) {

  // ... or maybe just wrap in your preferred runtime exception:
  throw new RuntimeException(e);
}

Similar projects

Everyday Java reflection with a fluent interface:

Reflection modelled as XPath (quite interesting!)

Comments
  • Dynamic compilation with provided ClassLoader

    Dynamic compilation with provided ClassLoader

    Hi, is it possible to create overloaded Compile.compile(String, String, ClassLoader) and Reflect.compile(String, String, ClassLoader)?

    I use Apache CXF to create dynamic SOAP web service client, but it loads the classes on a new ClassLoader instance, so I can't reference them on my class compiled by Reflect.compile(String, String).

    I may try submit a PR later.

    P: Medium T: Support Request R: Answered 
    opened by tioricardo 14
  • Support Java 8 default methods on interface proxies

    Support Java 8 default methods on interface proxies

    I use joor 0.9.6, doesn't support default method.

    interface A{
     default void a(){}
    }
    
    // Will throw no such method
    Reflect.on(new Object()).as(A.class).a()
    
    P: Medium R: Fixed T: Enhancement 
    opened by wenerme 10
  • InvocationTargetException when compiling a class extending a class in the same file

    InvocationTargetException when compiling a class extending a class in the same file

    Expected behavior and actual behavior:

    I got an InvocationTargetException for the compliation of a simple class which extends another class, which is in the same file:

    java.lang.reflect.InvocationTargetException
    org.joor.ReflectException: java.lang.reflect.InvocationTargetException
    	at org.joor.Reflect.on(Reflect.java:914)
    	at org.joor.Reflect.call(Reflect.java:583)
    	at org.joor.Compile.lambda$compile$0(Compile.java:112)
    	at org.joor.Compile$ClassFileManager.loadAndReturnMainClass(Compile.java:251)
    	at org.joor.Compile.compile(Compile.java:111)
    	at org.joor.Reflect.compile(Reflect.java:104)
    	at org.joor.Reflect.compile(Reflect.java:79)
           ... some more
    Caused by: java.lang.reflect.InvocationTargetException
    	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    	at java.lang.reflect.Method.invoke(Method.java:498)
    	at org.joor.Reflect.on(Reflect.java:910)
    	... 90 more
    Caused by: java.lang.NoClassDefFoundError: sample/AnotherSampleClass
    	at java.lang.ClassLoader.defineClass1(Native Method)
    	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
    	at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
    	... 95 more
    Caused by: java.lang.ClassNotFoundException: sample.AnotherSampleClass
    	at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
    	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
    	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
    	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
    

    I want to test an annotation processor, which detects the superclass of an annotated class. The superclass must be in the same package as the annotated class, which means I cannot simply extend an existing class like Object or Exception or the like.

    Steps to reproduce the problem:

    Reflect.compile( "sample.SampleClass", "package sample; public class SampleClass extends AnotherSampleClass{} class AnotherSampleClass{}");

    My real test is actually flaky, i.e. it passes sometimes but I got also the Exception. The example above is a minimal example but seems to fail always.

    Versions:

    • jOOR: 0.9.14
    • Java: 8
    P: Medium R: Fixed T: Defect 
    opened by muehmar 9
  • Reflect.compile(name,content) import static org.joor.Reflect.* gives compilation error, and import org.joor.Reflect that the compiler tells me to use throw exception

    Reflect.compile(name,content) import static org.joor.Reflect.* gives compilation error, and import org.joor.Reflect that the compiler tells me to use throw exception

    Expected behavior and actual behavior:

    print hellow world to console

    Steps to reproduce the problem:

    I follower your instructions, added Dependency and imported static org.joor.Reflect.*, but I got a compilation error. so I used the suggestion eclipse gave me to import org.joor.Reflect. compile method didn't excised so I changed the dependency version to 0.9.12, and I run my code.

    Versions:

    0.9.12

    • jOOR:
    • Java:

    Error message:

    Exception in thread "Thread-1" org.joor.ReflectException: Error while compiling com.example.HelloWorld at org.joor.Compile.compile(Compile.java:156) at org.joor.Reflect.compile(Reflect.java:102) at org.joor.Reflect.compile(Reflect.java:77) at server.Server.compileFile(Server.java:397) at server.OurThreadClass.handleRequest(OurThreadClass.java:215) at server.OurThreadClass.handleConnection(OurThreadClass.java:111) at server.OurThreadClass.run(OurThreadClass.java:72) Caused by: java.lang.NullPointerException at org.joor.Compile.compile(Compile.java:64) ... 6 more

    P: Medium R: Worksforme T: Defect 
    opened by michaelm43 9
  • Fields and methods from superclasses are invisible

    Fields and methods from superclasses are invisible

    To finding non-public fields or methods you use code like

        private Method exactMethod(String name, Class<?>[] types) throws NoSuchMethodException {
            final Class<?> type = type();
    
            // first priority: find a public method with exact signature match in class hierarchy
            try {
                return type.getMethod(name, types);
            }
    
            // second priority: find a private method with exact signature match on declaring class
            catch (NoSuchMethodException e) {
                return type.getDeclaredMethod(name, types);
            }
        }
    

    It's incorrect, it looks for fields/methods only in type(), doesn't work if the field/method is in some superclass.

    P: Medium R: Fixed T: Enhancement 
    opened by iirekm 9
  • first version of wrapped by String path . easy way to get / set Object

    first version of wrapped by String path . easy way to get / set Object

    hi i added full new branch of wrapped object by String path . for ex : if i have object from a class i and i need array first index value b/arr/[0] this will send to me value

    class a {
          b  b; 
        class b {
                String[] arr = {"some value"};
          }
    }
    

    not only that i make some observer list so any one can re call same path with different object very fast

    P: Medium R: Invalid T: Enhancement 
    opened by AlhassanReda 8
  • Fail to set private static final variable

    Fail to set private static final variable

    Hello,

    First of all, thank you for the library, it's really useful :)

    I found an issue when I want to reflect on a private final static variable (the worst case ^^), Joor throws a: org.joor.ReflectException: java.lang.IllegalAccessException: Can not set static final boolean field com.ogoutay.robomocki3.BuildConfig.DEBUG to java.lang.Boolean

    If I use my own Kotlin extension on top of your library, it works well:

    Reflect.setFinalStatic(name: String, newValue: Any) {
        val field = this.get<Class<*>>().getDeclaredField(name)
    
        field.isAccessible = true
        val modifiersField = Field::class.java.getDeclaredField("modifiers")
        modifiersField.isAccessible = true
        modifiersField.setInt(field, field.modifiers and Modifier.FINAL.inv())
    
        field.set(null, newValue)
    }
    

    Do you know where the problem could come from?

    Thank you in advance

    Cheers,

    Olivier

    P: Medium R: Worksforme T: Defect 
    opened by oliveeyay 8
  • Is there anyway to set JOOR to create a test directory under \target ?

    Is there anyway to set JOOR to create a test directory under \target ?

    I'm trying to create an annotation processor class and started to use JOOR to test it.

        @Test
        public void ensureAnnotationProcessorCreatesResourceBundleClassAndPropertiesWithMultipleLocales() throws Exception {
            MessageBundleGeneratorAnnotationProcessor processor = new MessageBundleGeneratorAnnotationProcessor();
            Reflect.compile(
                    "br.com.c8tech.javalib.apt.i18n.SourceResourceBundleWithTwoMethods",
                    "package br.com.c8tech.javalib.apt.i18n; "
                            + "@MessageBundle "
                            + "public interface SourceResourceBundleWithTwoMethods {"
                            + "@Message(value=\" worked {0} ! \", locale=\"pt-BR\" )"
                            + " public String m1(String pZero);" 
                            + "@Message(\" worked {0} {1}! \")"
                            + " public String m2(String pZero, String pOne);" 
                            + "}",
                            new CompileOptions().options("-source", "8")
                            .processors(processor))
            .type();
            assertTrue(processor.isProcessed());
        }
    

    In the class under test I'm creating a resource file:

    FileObject f = processingEnv.getFiler().createResource(
                        StandardLocation.SOURCE_OUTPUT, "", resourceName);
                
    

    I'm running the test inside Eclipse IDE and I was expecting that the resulting files was being created inside a specific test directory under project's \target directory, but it is being created inside my annotation processor project's directory instead.

    Am I missing something ?

    P: Medium T: Support Request R: Answered 
    opened by cvgaviao 7
  • java.lang.ClassCastException

    java.lang.ClassCastException

    Expected behavior and actual behavior:

    To run.

    {"provider":"HappyPlugin","name":"TimeViewer","commandName":"time","info":"Shows the time","code":["package io.github.xavierdd1st.commands;","import java.time.Clock;","import java.time.ZoneId;","public class TimeViewer extends Command {","public void command() {","Clock clock \u003d Clock.system(ZoneId.systemDefault());","CommandParser.setOutput( clock.toString() );","}","}"]}
    Exception in thread "main" java.lang.ClassCastException: io.github.xavierdd1st.commands.TimeViewer cannot be cast to java.util.function.Supplier
    	at io.github.xavierdd1st.commands.CommandParser.parseCode(CommandParser.java:23)
    	at io.github.xavierdd1st.commands.CommandHandler.setup(CommandHandler.java:16)
    	at io.github.xavierdd1st.Main.<init>(Main.java:21)
    	at io.github.xavierdd1st.Main.main(Main.java:35)```
    ### Steps to reproduce the problem:
    Run code;
    https://github.com/XavierDD1st/TBUA
    ### Versions:
    
    - jOOR: 0.9.9
    - Java: 8
    
    P: Low R: Wontfix T: Support Request 
    opened by ghost 7
  • Support setting values on final fields

    Support setting values on final fields

    When using jOOR to access private static classes or private final fields I get an access exception. Therefore I have two questions

    1. Why doesn't jOOR automatically try to modify the accessibility?
    2. How to modify the accessibility manually? I know of the method Reflect.accessible but I don't know how to get field references without using java.lang.reflect
    P: Medium R: Fixed T: Enhancement 
    opened by ooxi 7
  • Reflect on(Class<?> type, Object object)  become public?

    Reflect on(Class type, Object object) become public?

    Expected behavior and actual behavior:

    when i want to use joor on a String object to read its field. i found a question.

    when i write on("abc"), 2 method match.

    1. on(String)
    2. on(Object o);

    the second is which i want to use.

    Steps to reproduce the problem:

          String abc = "abc";
             Object o = abc;  
            Map<java.lang.String, Reflect> fields = on(o).fields();
            System.out.println("fields = " + fields);
    
    

    if i don't use Object to hold string, always call the on(String) overload due to single dispatch.

    Versions:

    • jOOR 0.9.7
    • Java: 8
    P: Medium T: Support Request R: Answered 
    opened by afk6 6
  • Add CompileOptions.warningHandling() to specify how compile time warnings should be handled

    Add CompileOptions.warningHandling() to specify how compile time warnings should be handled

    Currently, when there are compile time warnings, we're throwing a ReflectException. For example, if an annotation processor declares supporting Java 11, but we're processing things with Java 15, there's this exception here:

    org.joor.ReflectException: Compilation error: warning: Supported source version 'RELEASE_11' from annotation processor 'org.joor.test.CompileOptionsTest$AProcessor' less than -source '15'
    

    That shouldn't be an exception, but just a log message. We should have:

    • [ ] CompileOptions.warningHandling() as a way to set the warning level
    • [ ] WarningHandling.ERROR to treat warnings as errors
    • [ ] WarningHandling.LOG to log warnings on System.out
    • [ ] WarningHandling.IGNORE to silently ignore warnings
    P: Medium T: Enhancement 
    opened by lukaseder 1
  • Multi compile java9

    Multi compile java9

    Here is a first cut prototype for compiling multiple files in one go, which allows these classes to be dependent on each other.

    The code can surely be polished, and I was only focusing on making the Compile to work for multiple files, and not as much on a nice end user API.

    I tried to avoid changing in existing files. I only added a compileUnit method to Reflect which gives access to do the multiple compiler.

    We can likely find another API to avoid any changes to existing, if desired.

    We need this for Apache Camel so I would like to help more if @lukaseder would consider accepting this function in his excellent library. (cc @lburgazzoli)

    P: Medium T: Enhancement 
    opened by davsclaus 3
  • Working with Obfuscation - Duplicate Names

    Working with Obfuscation - Duplicate Names

    Versions:

    • jOOR: for Java 8+
    • Java: 8

    Reflection is actually helpful most of the time, even with obfuscation, but there is a problem while working against obfuscation. Some obfuscators make member names duplicate so we can't directly access it just by giving the name and maybe arguments. As an example, this is allowed at both compile time and runtime:

    public void foo(String str) {
        System.out.println(str);
    }
    
    public void foo() {
        System.out.println("Legit");
    }
    
    public void random() {
        this.foo();
        this.foo("Legit");
    }
    

    This example is not allowed at compile time but allowed at runtime:

    public int duplicate() {
        return 0;
    }
    
    public String duplicate() {
        return "Duplicate";
    }
    
    public void random() {
        int i = this.duplicate();         //INVOKEVIRTUAL randomPackage/randomClass.duplicate()I
        String str = this.duplicate();    //INVOKEVIRTUAL randomPackage/randomClass.duplicate()Ljava/lang/String;
    }
    

    There should be some additional methods that considers return types to fully recognize method signatures (name and description) to solve this problem.

    By the way I am not talking about runtime compilation, this issue is about calling duplicate methods

    P: Medium T: Enhancement 
    opened by MizzMaster 3
  • [#5] Add MethodOverloadTests for each module

    [#5] Add MethodOverloadTests for each module

    I tested these three test cases. They works fine, but if any fails, try clean and run again. (Maybe we should migrate to JUnit5 and annount with @RepeatTest)

    My environment:

    IntelliJ IDEA 2020.3.1
    Build #IU-203.6682.168, built on December 29, 2020
    Runtime version: 11.0.9.1+11-b1145.63 amd64
    VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.
    Windows 10 10.0
    

    ref: #5

    ps: There is something wrong with jOOR-java-6 and jOOR-java-8's existing test cases.

    P: Medium T: Enhancement 
    opened by DevDengChao 1
  • Reflect - Calling the same method over and over to be quicker

    Reflect - Calling the same method over and over to be quicker

    Expected behavior and actual behavior:

    This is an enhancement request.

    In Apache Camel in our new camel-joor component we use jOOR as an expression language in the Camel DSL that allows our end users to use some small scripting code that with the help from jOOR can be pre-compiled to Java. We wrap the end user code into a single class with a single method with the same method signature.

    And for runtime we then call that same method over and over again.

    I used a profiler (YourKit) and noticed that jOOR spend some time to compute which method to call. And then noticed I was able to get hold of the class type, and with Java reflection I could find the method (that jOOR would also find) and then keep that method reference for reuse and so its much faster.

    The "improvement" in the camel-joor is located around this place https://github.com/apache/camel/blob/master/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorExpression.java#L89

    I wonder if there could be some API in jOOR that allows end users to do something like this, where you can tell jOOR I want a reference to calling a method with this name and these types, and then keep that reference for calling over and over again.

    The profiler shows its significant improvement in terms of reduced object allocations etc. I have some screenshots I will paste to this ticket.

    Steps to reproduce the problem:

    Versions:

    • jOOR: 0.9.13
    • Java: 11
    opened by davsclaus 4
  • Allow for compiling several classes in one go

    Allow for compiling several classes in one go

    We can currently compile only a single class using the Reflect.compile() API. It may be desireable to compile several classes in one go through some auxiliary API

    A suggestion was made here: https://github.com/jOOQ/jOOR/issues/100#issuecomment-615296925

    P: Medium T: Enhancement 
    opened by lukaseder 2
Owner
jOOQ Object Oriented Querying
jOOQ Object Oriented Querying
Java runtime metadata analysis

Released org.reflections:reflections:0.9.12 - with support for Java 8 Reflections library has over 2.5 million downloads per month from Maven Central,

null 4.4k Dec 29, 2022
An uber-fast parallelized Java classpath scanner and module scanner.

ClassGraph ClassGraph is an uber-fast parallelized classpath scanner and module scanner for Java, Scala, Kotlin and other JVM languages. ClassGraph wo

classgraph 2.4k Dec 29, 2022
Tink is a multi-language, cross-platform, open source library that provides cryptographic APIs that are secure, easy to use correctly, and hard(er) to misuse.

Tink A multi-language, cross-platform library that provides cryptographic APIs that are secure, easy to use correctly, and hard(er) to misuse. Ubuntu

Google 12.9k Jan 3, 2023
In the Developer - Platform of EdgeGallery, we have provided a lot of useful APIs, in this project, try to simulates APIs of the competence center to help develoers test API request and response online.

api-emulator api-emulator模块,为EdgeGallery提供了基本能力的模拟api,开发者可以调用该模拟器提供的api,不需要真实部署就可以查看平台已有的能力。目前该api-emulator集成了两种平台能力:位置服务和人脸识别能力。 平台能力简介 位置服务 提供用户位置,E

EdgeGallery 21 Dec 25, 2021
Simpler, better and faster Java bean mapping framework

Orika ! NEW We are pleased to announce the release of Orika 1.5.4 ! This version is available on Maven central repository What? Orika is a Java Bean m

null 1.2k Jan 6, 2023
Java-Programs---For-Practice is one of the Java Programming Practice Series By Shaikh Minhaj ( minhaj-313 ). This Series will help you to level up your Programming Skills. This Java Programs are very much helpful for Beginners.

Java-Programs---For-Practice is one of the Java Programming Practice Series By Shaikh Minhaj ( minhaj-313 ). This Series will help you to level up your Programming Skills. This Java Programs are very much helpful for Beginners. If You Have any doubt or query you can ask me here or you can also ask me on My LinkedIn Profile

Shaikh Minhaj 3 Nov 8, 2022
dOOv (Domain Object Oriented Validation) a fluent API for type-safe bean validation and mapping

dOOv (Domain Object Oriented Validation) dOOv is a fluent API for typesafe domain model validation and mapping. It uses annotations, code generation a

dOOv 77 Nov 20, 2022
dOOv (Domain Object Oriented Validation) a fluent API for type-safe bean validation and mapping

dOOv (Domain Object Oriented Validation) dOOv is a fluent API for typesafe domain model validation and mapping. It uses annotations, code generation a

dOOv 77 Nov 20, 2022
You want to go to a cafe but don't know where to go. Let cafe hub support you. Ok let's go

cafe-hub You want to go to a cafe but don't know where to go. Let cafe hub support you. Ok let's go Architecture: Domain Driven Design (DDD) LDM Insta

Khoa 1 Nov 12, 2022
Manages server status and gives log of status information. front end - angular js, backend- sbring boot, DB-MySQL

ServerManagerApplication / | | / | | ( ___ _ __ __ __ ___ _ __ | \ / | __ _ _ __ __ _ __ _ ___ _ __ __ \ / _ \ | '| \ \ / / / _ \ | '| | |/| | / | | '

null 1 Jan 6, 2022
Log sourcing is method of trying to map all the ERROR and WARN logs you have in your system in a cost effective way.

log-sourcing Log sourcing is method of trying to map all the ERROR and WARN logs you have in your system in a cost effective way. The basic idea is th

Shimon Magal 12 Apr 19, 2021
Generates and keeps up-to-date your Spring Boot applications' Let's Encrypt or other ACME compliant SSL certificates.

Generates and keeps up-to-date your Spring Boot applications' Let's Encrypt or other ACME compliant SSL certificates. Pure Java in a single file of library code. An automated embedded alternative to Certbot and docker-sidecars. No JVM restart is needed on certificate update.

Valentyn Berezin 12 Nov 18, 2022
Eclipse Collections is a collections framework for Java with optimized data structures and a rich, functional and fluent API.

English | 中文 | Deutsch | Español | Ελληνικά | Français | 日本語 | Norsk (bokmål) | Português-Brasil | Русский | हिंदी Eclipse Collections is a comprehens

Eclipse Foundation 2.1k Jan 5, 2023
Eclipse Collections is a collections framework for Java with optimized data structures and a rich, functional and fluent API.

English | 中文 | Deutsch | Español | Ελληνικά | Français | 日本語 | Norsk (bokmål) | Português-Brasil | Русский | हिंदी Eclipse Collections is a comprehens

Eclipse Foundation 2.1k Dec 29, 2022
This mod gives the option to server admins to disable chat reporting, in a non-intrusive way

Simply No Report This mod gives the option to server admins to disable chat reporting, in a non-intrusive way. It is disabled by default to let everyo

Amber Bertucci 17 Aug 20, 2022
GreenMail is an open source, intuitive and easy-to-use test suite of email servers for testing purposes.

GreenMail GreenMail is an open source, intuitive and easy-to-use test suite of email servers for testing purposes. Supports SMTP, POP3, IMAP with SSL

null 529 Dec 28, 2022
this repo is probs gonna die cuz idk very much java but i will update it when i learn how to actually DO SHIT

pastegod.cc shitty rename of zihasz client base rn but as i learn java i will paste-i mean add modules ;) (23/9/2021) why is everyone starring and wat

null 9 Dec 9, 2022
This mod makes the clouds look much better.

Fabulous Clouds is a 1.17 Fabric mod that makes minecraft's clouds look much better. It isn't going to be ported to Forge or be backported. Fabulous C

Nuclear Chaos 17 Oct 28, 2022