asusoda / Corundum

This is a new plugin-based server-side modding A.P.I. for Minecraft. [WIP]
MIT License
7 stars 1 forks source link

Type creation methods #18

Closed Variadicism-zz closed 9 years ago

Variadicism-zz commented 9 years ago

First, some small notes about this post:

I've been having trouble coming up with a good way to allow the creation of typed objects (like Entities) through types. For example, it's easy and convenient for plugin authors and for us to allow the creation of, say, a Wither Skeleton using new WitherSkeleton(), but then what if someone wants to create a new random Mob? It would be fantastic if they could just write MobType.values()[random].create(). (I already figured out that we can't do new Mob(MobType.values()[random]).) However, I'm having trouble finding a nice way to program this create() method in the A.P.I.

The way I see it, there are two options:

public static final NonLivingEntityType<?> ENDER_CRYSTAL = new NonLivingEntityType<>(200) {
    public EnderCrystal create() {
         return new EnderCrystal();
    }
};

This would be much easier on the computer and eliminate complexity code-wise and it would mean that the create() methods' return type could be specific without casting, but it would also mean a lot more code.

So, opinions on which option is better? Any ideas better than both of these? I'd love to hear them!

ElementsModder commented 9 years ago

I think implementing the create() method individually, because as you say it DOES eliminate complexity.

Niadevv commented 9 years ago

I remember seeing the individual create() method for each entity in some code somewhere, probably in the reengineering entities branch as master's not really updated much any more, so I assume we're going with that one. The reason for my inactivity, btw, is so that I don't make it harder to merge the reengineering entities branch. Also, distractions, like the holidays, me getting into one of those free games I found on the internet, and me attempting to make my own game and messing with Gradle (it now SOMETIMES works for me, instead of never). In fact, I've done very little programming these past few days :P.

nmacholl commented 9 years ago

I don't know if I entirely understand this but...

From a modding perspective, if I wanted to extend a type class, wouldn't individual create() methods be more intelligible for plugin developers than having those methods wrapped up with the entity classes? I think it woudl also be unintuitive if you were to create an arrow and instead of that create() returning an arrow object you got a projectile. For someone that is derp at JAVA that would confuse me.

Variadicism-zz commented 9 years ago

I found a better way to implement solution 1 (mainly the second point, which was my biggest problem with that potential solution): we can parameterize the type classes so that all of their methods return the most appropriate type without casting. You can see how I'm doing this in the reengineering_entities branch. We could use that to call the appropriate constructor through Reflection. (See the create() method I implemented in CreatableType on that branch.)

The one true annoyance that I can't seem to get around with this parameterization solution is that since these type classes self-parameterize (i.e. the first parameter is the same as the class that it's in), each instance class would have to have its own type class so that it could reference itself for the self-parameterization. For example, for the PlayerEntity class, the type class parameter would be a LivingEntityType<[itself], EntityPlayer, PlayerEntity>, but the "[itself]" would have to be replaced with itself, which contains an "[itself]" that would have to be replaced with itself which contains a... well, you get the point. So, instead, I had to make a PlayerEntityType class that extends LivingEntityType<PlayerEntityType, EntityPlayer, PlayerEntity>.

@ElementsModder It's true that it eliminates complexity, but on the other hand, there are hundreds of BlockTypes, ItemTypes, and EntityTypes and that third parameter, the one that relates the type class (e.g. ProjectileType) to its instance class (e.g. Projectile), could be made to do all that work for us.

@Niadel I did try putting in some individual create() methods before, but that was when I realized that this was going to make lots and lots of redundant code as well as tons and tons of anonymous classes. (Each create() implemented like that would create a new anonymous class.) Redundant code means it's a lot harder to change and a lot easier to make copy-and-paste or similar errors.

By the way, don't worry about the merging too much. I know we've had a couple merge issues in the past, but we can get it to work. Besides, if you did want to make changes, you could always just make a new branch off master and make the changes there.

@nmacholl Well, for one thing, we do not want them extending those type classes. Those type classes are supposed to represent all the types in Minecraft and since we can't make more (since this is server-side only), it would be dangerous to go doing that. I've actually been trying to find ways of preventing people from extending those.

Also, with the new parameterization solution I came up with, even if we did let them extend those type classes, the solution 1 create() method would actually be easier for them because they don't have to do anything except specify the instance class they want to use with a nullary constructor; create() would do the rest for them.

Niadevv commented 9 years ago

Oh cool, I forgot about branches :P. Here's to hoping I don't accidentally make a unnecessary repo clone in a folder like last time I tried to do a branch. Turns out I needed to do git checkout and not just straight up push to a different branch. And by the way, the final modifier on a class makes it unextendable, just in case you've forgotten (not attempting to sound condescending, if that's the word, by the way. Unfortunately, some sentences sound condescending even thought it's not the author's intention).

Variadicism-zz commented 9 years ago

@Niadel Ha ha. Yeah. Good luck with the branches.

Also, I do remember the final modifier. However, this would only work on the classes furthest down in the hierarchy; we can't make parent classes like MobType final because our own classes extend it. Also, if we use this parameterization technique that I've been trying to work out, the classes that would be at the bottom of the hierarchy would be the ones specific to each individual Entity class, which are hidden from the A.P.I. by default visibility modifiers anyway because their only use is as an implementation detail. The point is that most or all of those type classes can't use the final modifier to stop people from extending them.

Variadicism-zz commented 9 years ago

Okay. I think I have finally found the best arrangement for everything typed. It's a little (extremely) complicated internally, but it's fast, needs no Reflection, and has minimal repetition. I'm going to explain it all here. Please feel free to post questions or comments and whatnot.

There are three parts to this system: 1) instance classes, type classes, and type enumeration interfaces.

These three parts will fit into the system one of two ways depending on whether they are parent classes (classes that are higher up on the hierarchy from which other types will inherit certain properties, e.g. Projectile) or base classes (classes that represent the most specific types, e.g. Snowball).

Instance classes

Instance classes are classes that represent individual instances of different types, e.g. Projectile or Skeleton classes. Below is an example of a header of LivingEntity, which is a parent instance class that represents any living entity (really just players and mobs):

public abstract class LivingEntity<S extends LivingEntity<S, MC, T>, MC extends EntityLivingBase, T extends LivingEntity.LivingEntityType<T, MC, S>> extends Entity<S, MC, T>

In this header, the class is declared as public and abstract and parameterized with three types:

public class PlayerEntity extends LivingEntity<PlayerEntity, EntityPlayer, PlayerEntity.PlayerType>

Type classes

Type classes represent the different types of Entities, Blocks, or Items in Minecraft, e.g. ProjectileType or SnowballType. The type classes should be located INSIDE the classes that they represent; for example, MobType is inside Mob and NonLivingEntityType is inside NonLivingEntity. Below is an example of the header of the LivingEntityType class, which is a parent type class that represents the type of any Entity that is living:

public static class LivingEntityType<S extends LivingEntityType<S, MC, I>, MC extends EntityLivingBase, I extends LivingEntity<I, MC, S>> extends EntityType<S, MC, I>
            implements LivingEntityTypes

In this header, there are three parameters:

protected LivingEntityType(int id, int data) {
    super(id, data);

    addValueAs(LivingEntityType.class);
}
// pseudo-enum utilities
public static LivingEntityType getByID(int id) {
    return getByID(LivingEntityType.class, id);
}

public static LivingEntityType getByID(int id, int data) {
    return getByID(LivingEntityType.class, id, data);
}

public static LivingEntityType[] values() {
    return values(LivingEntityType.class);
}
public static final SnowballType extends ProjectileType<SnowballType, EntitySnowball, Snowball>

Type enumeration interfaces

Type enumeration interfaces are the classes that list the different potential types of any given grouping like enums. These interfaces include public static final variables that simply point to the instances of the different types that they include. Like type classes, these should appear inside their corresponding instance classes. Type classes implement their corresponding type enumeration interfaces so that they can appear like enums; however, the type enumeration interfaces can extend each other so that they can be grouped together without having to repeat all of the type declarations. For example, ProjectileType, VehicleType, and DropType are all subclasses of NonLivingEntityType, so the list of NonLivingEntityTypes should include all the types from ProjectileType, VehicleType, DropType, and more, and having them in separate interfaces allows 1) extending multiple interfaces so it can include the elements from more than one other type and 2) allows us to reverse the order of the inheritance so that less specific types have MORE elements, not less. Here's an example with the NonLivingEntityTypes type enumeration interface header:

public static interface NonLivingEntityTypes extends ProjectileTypes, VehicleTypes, DropTypes, BlockEntityTypes, HangingEntityTypes

Note that it has to be public for other interfaces higher up in the hierarchy to see it. It contains only the NonLivingEntityTypes that do not fit into any of the interfaces it extends; all those other values come from the interfaces that it extends.

There are no base type enumeration interfaces because they would be pointless; they would just include one public static final pointer to one type, e.g. public static final SnowballType SNOWBALL = SnowballType.INSTANCE;.

Questions, comments, concerns, etc... \/\/\/ BELOW! (Man, that took forever and a year to write.)

Variadicism-zz commented 9 years ago

So... anything? Questions? Comments? Opinions?

Variadicism-zz commented 9 years ago

I have to make a small modification to this setup.

Type classes can no longer implement their related type enumeration interface, e.g. ProjectileType cannot be allowed to implement ProjectileTypes. I was hoping we could do this so that people could use ProjectileType.ARROW, but sadly, because lower-level classes like FireballType inherit from ProjectileType, if FireballType implements FireballTypes and ProjectileType implements ProjectileTypes, since ProjectileTypes extends FireballTypes, all the things declared in FireballTypes will be declared twice in ProjectileType, causing ambiguity compiler errors.

Long story short: type classes cannot implement type enumeration interfaces and plugin authors will have to use ProjectileTypes.ARROW instead of ProjectileType.ARROW. That's not to bad; it's the difference of one "s" and I couldn't find a way to hide those interfaces from the A.P.I. anyway.

Niadevv commented 9 years ago

It's a complicated setup indeed (seriously, not once have I ever seen a type with 3 generic parameters), but seeing as I understood the story of a game with an allegedly extremely complex and confusing story perfectly well, I should have no problem understanding this. Fortunately, this shouldn't concern the end plugin author, but as chances are there are going to be some versions of Corundum that implement mod compatibility, someone's going to have to understand it. However, the number of people doing this will probably be tiny. You probably might want to link that specification of the entity system in the docs for Corundum.

And by the way, if you wanted to hide the interfaces, you could make the fields package local (removing the access identifier, most likely currently public). Of course, that'd mean you'd need to have all (or some) of the Type classes in the same package as the Types classes, but seeing as the Type classes are being hidden anyways, it shouldn't matter too much. Assuming I understood the system, that is.

Variadicism-zz commented 9 years ago

@Niadel Yeah, it did get pretty darn complicated, but as you said, all the parameterization and other complicated stuff will be hidden from the plugin authors; all they'll have to know if they can use instance classes to get individual Entities, type classes to get an Entity's type, and type enumeration interfaces to get the different kinds of types. that seems pretty straightforward to me. The only part of that that might be slightly difficult is letting everyone know that they should ignore those generic type parameters and use raw types. We can put that in all the Javadocs and tutorial videos and all that, though.

We could give the type enumeration interfaces default visibility, but then we would have to have all those Entity classes in one big package. We could do that, and that's what Bukkit ended up doing with a lot of their stuff to hide things without getting too complicated. Still, there's no need for that any more since now those interfaces should be visible to plugin authors and I'm glad for that because having all those Entities -- or Items and Blocks as we'll have to do later -- in one package would just be ugly. Still, if we come across a similar problem later, your idea is one to keep in mind.

By the way, I haven't really figured out yet if it matters or not, but since BlockTypes and ItemTypes and BiomeTypes and other types have equivalent Minecraft objects (unlike EntityTypes), we may need a fourth type parameter for those classes for the type of Minecraft object they represent. We can figure that out when we come to it.

Variadicism-zz commented 9 years ago

I think this should help in understanding:

In Eclipse, you can make "templates" which allow you to use a keyword to create blocks of code with only a few pieces to fill in, which is really helpful because Java can be so redundant. I have two templates here that I'm using in my coding: one is for Entity classes: one is for parent classes and the other is for base classes. Remember that since type classes and type enumeration interfaces are both inside instance classes, these templates include all three.

If you want to use these in Eclipse, you can go to Window >> Preferences >> Java >> Editor >> Templates, create a new template, and plug this stuff into it. Ignore the "newName" things in some of the tags; that's just to tell Eclipse what it is that we're filling in.

For parent classes

For the parent class template, you only need to put in two things: the instance class's name (${class_name}, which will also be used to name the type class and type enumeration interface by appending "Type" and "Types", respectively) and the Minecraft object class that best represents this type (MC_type).

/** 
 * 
 * @param <S>
 *            is a self-parameterization; this type should be the same type as this class.
 * @param <MC>
 *            determines the type of Minecraft Entity <tt>Object</tt> that this class represents.
 * @param <T>
 *            determines the type of {@link EntityType} that represents the type of this class. */
public abstract class ${class_name:newName}<S extends ${class_name}<S, MC, T>, MC extends ${MC_type:newName}, T extends ${class_name}.${class_name}Type<T, MC, S>> extends ${parent_class}<S, MC, T> {
    protected ${class_name}(MC entityMC) {
        super(entityMC);
    }

    public static interface ${class_name}Types {
        // TODO
    }

    public abstract static class ${class_name}Type<S extends ${class_name}Type<S, MC, I>, MC extends ${MC_type}, I extends ${class_name}<I, MC, S>> extends ${parent_class}Type<S, MC, I> {
        protected ${class_name}Type(int id, int data) {
            super(id, data);

            addValueAs(${class_name}Type.class);
        }

        // abstract utilities

        // overridden utilities

        // pseudo-enum utilities
        @SuppressWarnings("rawtypes")
        public static ${class_name}Type getByID(int id) {
            return getByID(${class_name}Type.class, id);
        }

        @SuppressWarnings("rawtypes")
        public static ${class_name}Type getByID(int id, int data) {
            return getByID(${class_name}Type.class, id, data);
        }

        @SuppressWarnings("rawtypes")
        public static ${class_name}Type[] values() {
            return values(${class_name}Type.class);
        }
    }

    // type utilities

    // instance utilities
}

For base classes

Base classes take the same things as parent classes plus two more: the I.D. and data value of the object (${id} and ${data} respectively; you can see them in the type class constructor).

public class ${class_name:newName} extends ${parent_class:newName}<${class_name}, ${MC_type:newName}, ${class_name}.${class_name}Type> {
    public ${class_name}() {
        super(new ${MC_type}(null));
    }

    protected ${class_name}(${MC_type} entityMC) {
        super(entityMC);
    }

    protected static class ${class_name}Type extends ${parent_class}Type<${class_name}Type, ${MC_type}, ${class_name}> {
        public static final ${class_name}Type TYPE = new ${class_name}Type();

        private ${class_name}Type() {
            super(${id:newName}, ${data:newName});
        }

        // overridden utilities
    }

    // instance utilities

    // overridden utilities
    @Override
    public ${class_name}Type getType() {
        return ${class_name}Type.TYPE;
    }
}
Variadicism-zz commented 9 years ago

I have been busy on the reengineering_Entities branch... well, reengineering the Entities. Along the way, I noticed a couple problems or needs for certain things, so I had to make some changes to the templates for the base and parent classes. Most of them are minor; the important ones are adding ? type parameters to type classes when they're used as return types (because apparently raw types are not the same as ?-parameterized types) and adding the fromMC() method to allow us to much more easily get a Corundum Entity from a Minecraft Entity. I pasted the updated templates below.

Also, today is my first day back at school, so I won't have quite as much time to work on this, but I'll still keep going at it and dedicating as much time as I can.

For parent classes

/** TODO
 * 
 * @param <S>
 *            is a self-parameterization; this type should be the same type as this class.
 * @param <MC>
 *            determines the type of Minecraft Entity <tt>Object</tt> that this class represents.
 * @param <T>
 *            determines the type of {@link EntityType} that represents the type of this class. */
public abstract class ${primary_type_name}<S extends ${primary_type_name}<S, MC, T>, MC extends ${MC_type:newName}, T extends ${primary_type_name}.${primary_type_name}Type<T, MC, S>> extends ${parent_class}<S, MC, T> {
    protected ${primary_type_name}(MC entityMC) {
        super(entityMC);
    }

    public static interface ${primary_type_name}Types {
        // TODO
    }

    public abstract static class ${primary_type_name}Type<S extends ${primary_type_name}Type<S, MC, I>, MC extends ${MC_type}, I extends ${primary_type_name}<I, MC, S>> extends ${parent_class}Type<S, MC, I> {
        protected ${primary_type_name}Type(int id, int data) {
            super(id, data);

            addValueAs(${primary_type_name}Type.class);
        }

        // abstract utilities

        // overridden utilities

        // pseudo-enum utilities
        public static ${primary_type_name}Type<?, ?, ?> getByID(int id) {
            return getByID(${primary_type_name}Type.class, id);
        }

        public static ${primary_type_name}Type<?, ?, ?> getByID(int id, int data) {
            return getByID(${primary_type_name}Type.class, id, data);
        }

        public static ${primary_type_name}Type<?, ?, ?>[] values() {
            return values(${primary_type_name}Type.class);
        }
    }

    // type utilities

    // instance utilities
}

For base classes

public class ${primary_type_name} extends ${parent_class:newName}<${primary_type_name}, ${MC_type:newName}, ${primary_type_name}.${primary_type_name}Type> {
    public ${primary_type_name}() {
        super(new ${MC_type}(null));
    }

    protected ${primary_type_name}(${MC_type} entityMC) {
        super(entityMC);
    }

    protected static class ${primary_type_name}Type extends ${parent_class}Type<${primary_type_name}Type, ${MC_type}, ${primary_type_name}> {
        public static final ${primary_type_name}Type TYPE = new ${primary_type_name}Type();

        private ${primary_type_name}Type() {
            super(${id:newName}, ${data:newName});
        }

        // overridden utilities
        @Override
        public ${primary_type_name} create() {
            return new ${primary_type_name}();
        }

        @Override
        public ${primary_type_name} fromMC(${MC_type} entityMC) {
            return new ${primary_type_name}(entityMC);
        }
    }

    // instance utilities

    // overridden utilities
    @Override
    public ${primary_type_name}Type getType() {
        return ${primary_type_name}Type.TYPE;
    }
}
Variadicism-zz commented 9 years ago

I finished implementing Entities in this way. Since I haven't heard any criticisms or anything, I'm assuming this system is good with everyone and I'm closing this issue. If anyone wants to comment, feel free to reopen this issue.