Java-Gaming.org Hi !
Featured games (90)
games approved by the League of Dukes
Games in Showcase (741)
Games in Android Showcase (225)
games submitted by our members
Games in WIP (823)
games currently in development
News: Read the Java Gaming Resources, or peek at the official Java tutorials
 
    Home     Help   Search   Login   Register   
Pages: 1 [2] 3
  ignore  |  Print  
  SilenceEngine - A 2D/3D Game Engine  (Read 28900 times)
0 Members and 1 Guest are viewing this topic.
Offline SHC
« Reply #30 - Posted 2014-11-25 17:05:14 »

Now added Transformations!! All you have to do is to create an instance of
Transform
class and apply that to the batcher. Here's a crappy gif that I've recorded (747 KB)

Click to Play


All you have to do is manipulate the Transform object and before rendering, apply to the batcher with
batcher.applyTransform(transform)
and the changes apply instantly. More coming soon, I'm already working on the Camera, so stay tuned!!

Offline Gibbo3771

JGO Kernel


Medals: 128
Projects: 5
Exp: 1 year


Currently inactive on forums :(


« Reply #31 - Posted 2014-11-25 21:43:43 »

Nice, how are you handling transforms?

Are you just using standard Vector code or matrix transforms? I am new to Matrices, waiting patiently for the updated repo Cheesy.

"This code works flawlessly first time and exactly how I wanted it"
Said no programmer ever
Offline SHC
« Reply #32 - Posted 2014-11-26 03:38:08 »

Batcher is the place where all the rendering takes place in SilenceEngine. It has a Transform object whose matrix is passed to the shaders internally. Everytime you apply a transform, the internal transformation matrix will get multiplied, allowing the changes for that frame. That's how it works.

And on the end of every frame, the internal transform will be reset, to an identity matrix. Also whenever you apply a transform, the previous contents gets flushed before transformation, meaning that the transform will apply to the vertices that were batched after the application. If you want to change the transform, you have to end the batch, and re-start batching with the new transform.

The transform and batcher classes are located in the
com.shc.silenceengine.graphics
package, take a look at them to get an understanding.

Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline SHC
« Reply #33 - Posted 2014-11-27 09:23:44 »

Update: Added Cameras! I've written two Camera classes
OrthoCam
and
PerspCam
for orthographic and perspective cameras. Both the cameras are very basic in behaviour, though. The
OrthoCam
should be used to render 2D stuff, and can be used to render HUDs in the games.

The
PerspCam
provides you a method called
setPositionAndLookAt(Vector3, Vector3)
which can be used to position the Camera anywhere on the scene, and make it look at a point. This is particularly useful in FPS style games, you position the camera behind the player, and make it look at the player, making it simple and easy.

Here's how to use the Cameras, you have to pass the camera to the batcher in the begin method. You can also pass in a transform along with the camera, and also, it is possible to set the camera by using the
applyCamera(ICamera)
method of the batcher. Here's the screenshot of the CameraTest.



The triangle in the top-left corner is rendered using
OrthoCam
while the one in the center is rendered using
PerspCam
and a rotating Transform. I think I should document the classes now..

Offline SHC
« Reply #34 - Posted 2014-11-28 14:34:26 »

TrueTypeFonts!! Thanks to NegativeZero for sharing some of his code to load from AWT Font. To render the font, you have to use ortho camera. This is because in PerspCam, the coordinates will be -1 to 1, instead of 0 to width and 0 to height.



As usual, here's some code!

1  
2  
3  
4  
5  
6  
7  
8  
// Draw some text: Warning, requires OrthoCam
batcher.begin(cam);
{
    font.drawString(batcher, "Hello World!!", 10, 10);
    font.drawString(batcher, "Colored Text!!", 10, 10 + font.getHeight(), Color.RED);
    font.drawString(batcher, "Multi line\nText!!", 10, 10 + 2 * font.getHeight(), Color.GOLD);
}
batcher.end();

Stay tuned for more!! Coming up are entity and world system for 2D.

Offline gouessej
« Reply #35 - Posted 2014-11-28 15:27:17 »

TrueTypeFonts!! Thanks to NegativeZero for sharing some of his code to load from AWT Font.
trollwarrior might have something else to suggest. There are a few AWT-free solutions to loads TrueType fonts. If you use AWT, forget once for all Android support.

Julien Gouesse | Personal blog | Website | Jogamp
Offline SHC
« Reply #36 - Posted 2014-11-28 16:29:04 »

trollwarrior might have something else to suggest. There are a few AWT-free solutions to loads TrueType fonts. If you use AWT, forget once for all Android support.

Thanks for the info, I'll contact him now. BTW, I really had no plans for Android, I'm only thinking of GWT for now.

Offline matanui159

JGO Coder


Medals: 11
Projects: 1
Exp: 10-12 months


Aww... So cute...


« Reply #37 - Posted 2014-12-07 11:32:23 »

I was justing needing a bit of help with my game engine so I had a quick look around at yours on github if that is alright... It is really amazing, you did a good job...

Just one question tho... In Display.setFullscreen it seems like you are recreating the window and the OpenGL context... Does this mean that the OpenGL context will restart? So will all textures, VBOs, etc. need to be redone? Or are you hiding some sort of trick and somehow keeping the same context? persecutioncomplex

It is just that I am trying to change to fullscreen in game while keeping the same context.

Thx in advance...

Is it sad that I still get a fright when the computer beeps at me...
Offline SHC
« Reply #38 - Posted 2014-12-07 16:05:29 »

@matanui159

Sorry, accidental medal, was browsing through mobile. To answer your question, I'm only creating one context: Every window I create shares that context, because it is the only way to implement fullscreen switching in GLFW.

Offline matanui159

JGO Coder


Medals: 11
Projects: 1
Exp: 10-12 months


Aww... So cute...


« Reply #39 - Posted 2014-12-07 23:01:30 »

But every time you create a window, you call GLContext.createFromCurrent()
Doesn't this make a new OpenGL context?

Is it sad that I still get a fright when the computer beeps at me...
Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline Opiop
« Reply #40 - Posted 2014-12-08 03:31:09 »

But every time you create a window, you call GLContext.createFromCurrent()
Doesn't this make a new OpenGL context?
I don't know if that is engine specific code, or GLFW code, but the name implies that the OP is copying the current context, and creating a new context. Basically just a copy of the original context. Don't take my word for it, though, because I don't entirely know. It's just my best inference.
Offline SHC
« Reply #41 - Posted 2014-12-08 03:41:05 »

The call to
GLContext.createFromCurrent()
changes the context used by LWJGL's GL** class functions to the context from the active glfwContext.

Offline matanui159

JGO Coder


Medals: 11
Projects: 1
Exp: 10-12 months


Aww... So cute...


« Reply #42 - Posted 2014-12-08 03:57:14 »

I tried it myself and I was having troubles until I realised that although all images and that are still kept I still have to re-enable stuff like GL_TEXTURE_2D, GL_BLEND, etc. and set everything up.

EDIT: thx for the help, your engine is amazing btw.

Is it sad that I still get a fright when the computer beeps at me...
Offline SHC
« Reply #43 - Posted 2014-12-11 16:40:52 »

Just did a large amount of changes into the engine. The changes include a lot of improvements to the Batcher, and a whole package full of classes that let you use OpenGL just like another object of a Java class, most of them are encapsulated (Not sure if I can use this term in the meaning of wrapping)

The Batcher brings the good old Immediate Mode rendering to VBOs and VAOs, you just add the geometry to be streamed using
vertex()
,
color()
and
texCoord()
methods of the batcher. Only one thing is that the begin mode is FIXED as triangles, as changing that will also increase the size of Mesh class. Here's a small example.

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
batcher.begin();
{
    batcher.vertex(0, 0.5f);
    batcher.color(Color.CORN_FLOWER_BLUE);

    batcher.vertex(-0.5f, -0.5f);
    batcher.color(Color.INDIAN_RED);

    batcher.vertex(0.5f, -0.5f);
    batcher.color(Color.OLIVE_DRAB);

    batcher.vertex(1, 1, 0);
    batcher.vertex(1, -1, 0);
    batcher.vertex(0.5f, 0, 1);
}
batcher.end();

Produces two triangles, one colored and one pure white on the screen. The next major change is done to the shader classes. I've separated the
ShaderProgram
class into two separate
Shader
and
Program
classes, which would give a lot of comfortability to the user when implementing custom shaders.

Another change is that every frame, Game class will call the
setupUniforms()
public method in the Program class, which can be used to set the uniforms, allowing the user to change the names of the uniforms in his shaders. Here is the code in the
DefaultProgram
class that sets the uniforms.

1  
2  
3  
4  
5  
6  
7  
public void setupUniforms()
{
    setUniform("tex", Texture.getActiveUnit());
    setUniform("mTransform", Game.getBatcher().getTransform().getMatrix());
    setUniform("camProj", BaseCamera.projection);
    setUniform("camView", BaseCamera.view);
}

I think you got it. Also you do not need to worry about state changes, the OpenGL state functions are only called when the state really changes. These classes keeps track of current object that is bound, for example, you have
Texture.CURRENT
and
Program.CURRENT
. This allows for less state changing.

Another major change is the introduction of Development mode, which is true by default. You are expected to turn it off when you are distributing, because it turns off basic OpenGL error checking, usually done after every OpenGL function is called, thus saving you a few extra frames. To turn it off, place this code in the class that extends from
Game
class.

1  
2  
3  
4  
static
{
    development = false;
}

This is all for now guys, see ya' soon with much more updates  Wink

Offline SHC
« Reply #44 - Posted 2014-12-13 09:31:48 »

Hey friends, say hello to SilenceEngine's Scene Graph!!

Click to Play


The Scene Graph is a way of managing the relationship between objects in the form of a tree, i.e., a Parent-Child relation ship. In SilenceEngine, this SceneGraph is implemented in two classes
Scene
and
SceneNode
and are part of the
com.shc.silenceengine.scene
package. The Scene class is the root node, you just make an instance of it. The
SceneNode
is just a normal class, and every object you wish to place in the scene should extend from it.

The Scene's life style starts with calling
Scene.init()
function. This clears any existing children and resets the world transform. Then you add children. This is the code from the above test.

1  
2  
3  
4  
5  
6  
7  
8  
9  
Scene scene = new Scene();
SceneObject root = new SceneObject(new Vector2(0, 0), Color.RED);
{
    root.addChild(new SceneObject(new Vector2(-0.5f, 0.5f), Color.GREEN));
    root.addChild(new SceneObject(new Vector2(0.5f, 0.5f), Color.BLUE));
    root.addChild(new SceneObject(new Vector2(0, -0.5f), Color.SILVER));
}
scene.addChild(root);
scene.init();

Though I called the first object as root, the Scene is actually the root. I also added some more nodes to the node I just created, and added that to the Scene. The
SceneObject
class is just a small class that extends the
SceneNode
which takes some parameters such as position and a color.

Then you just update the whole scene and render it using the static functions of the Scene class. The advantage of this is that if you transform the parent, then the children will also get transformed. Regarding transformations, every
SceneNode
contains two transformations,
transform
and
localTransform
. The difference is that the transform also contains the transformations of the parent, it is the one you apply to the batcher when drawing. The localTransform will only contain the transformation of that node, and is what you use to transform the node.

1  
2  
3  
4  
5  
6  
// In update code
getLocalTransform().reset().rotate(Vector3.AXIS_Z, rotation)
                           .translate(new Vector3(position.getX(), position.getY(), z));

// In render code
batcher.applyTransform(getTransform());

This is what makes the Scene Graph. If you want to remove a child node, call the
destroy()
function on it, as calling the remove directly causes the next object to lost a frame. This, combined with some fun with trigonometry, generated the above SceneTest.

That's all for now, stay tuned for more!!

Offline trollwarrior1
« Reply #45 - Posted 2014-12-13 09:40:01 »

Why is the Scene static?
Offline SHC
« Reply #46 - Posted 2014-12-13 09:44:21 »

Why is the Scene static?

I don't see any point in having two scenes at a moment. You can simply clear the previous scene and re-initialize it with new nodes. Basically, I use the scene as a level, containing a lot of objects. In this case, I don't think making it non-static can be of any other benefit, so I made it static.

Offline 65K
« Reply #47 - Posted 2014-12-13 10:42:39 »

A debug window would need its own scene, for instance.
And if you do not need multiple scenes, static lifecycle methods prevent your users from easy modifications and takes away flexibility for no benefit.

Lethal Running - a RPG about a deadly game show held in a futuristic dysoptian society.
Offline gouessej
« Reply #48 - Posted 2014-12-13 11:34:16 »

In this case, I don't think making it non-static can be of any other benefit, so I made it static.
In my humble opinion, you should do the opposite, you should have a strong reason to make it static to do it, otherwise just don't do that. You can't imagine how the developers are going to use your engine, keep it as flexible as possible except when a constraint is useful.

Julien Gouesse | Personal blog | Website | Jogamp
Offline SHC
« Reply #49 - Posted 2014-12-13 12:03:30 »

You're right, I didn't get that thought. I've made it non-static now. Thanks for saying that to me.

Offline SHC
« Reply #50 - Posted 2014-12-14 13:27:39 »

This is just another update, I've implemented 2D geometry classes
Polygon
,
Rectangle
and
Circle
in SilenceEngine. They can be rotated at any time, and can be tested for intersections. The collision detection is based on the SAT (Separating Axis Theorem). Anyway, here's a crappy gif that I recorded.

Click to Play


Unlike the similar classes, I've kept the position and vertices seperately in them, so the vertices will only regenerate when they are rotated. Next up, I'm planning to implement Scene colliders to automatically test collisions between objects in the scene.

Offline KudoDEV

JGO Ninja


Medals: 79
Exp: 6 years


Game Dev Hobbyist


« Reply #51 - Posted 2014-12-14 15:32:37 »

Keep up the good work! Make sure as you progress to keep things just as simple as it is now. If this starts getting overly complicated and over engineered it will have no use.

Great work nonetheless!

Offline SHC
« Reply #52 - Posted 2014-12-16 10:04:21 »

Hello everyone, this is time to introduce
SceneCollider2D
interface and the two colliders that implement this interface. Basically in SilenceEngine, a SceneCollider is an object that takes care of collisions between entities in a scene automatically for you.

SceneCollider2D
interface specifies how a collider should be implemented for 2D entities, that is, they only check between collisions for classes that extend
Entity2D
class. As specified in the name, these colliders need the scene to check collisions in. What you basically do is this.

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
// Create the scene
scene = new Scene();

... // Add entities to the scene

// Create the collider
collider = new GridSceneCollider(width, height, cellWidth, cellHeight); // OR
collider = new QuadTreeSceneCollider(width, height);

// Set the scene to the collider
collider.setScene(scene);

This is how you create the colliders. The next thing you have to do is to specify for which entities the collisions should be detected. This is done with the
register
method of the colliders like this.

1  
2  
3  
4  
collider.register(Player.class, Floor.class);
collider.register(Player.class, Coin.class );
collider.register(Player.class, Enemy.class);
collider.register(Enemy.class,  Floor.class);

The class you pass to the register must be a Entity2D or extend from it. Only for these registered classes, the collisions will be checked, that too, in the same order that you register. In the above example, the collisions will be checked for every Player with every Floor, but they will only be notified to the
collision
method in the Player.

To check for the collisions and to update the scene, do the following.

1  
2  
scene.update(delta);
collider.checkCollisions();

It's that simple! And to be notified of collisions, the entity class should override the collision method provided by the Entity2D class. This is all for now guys, stay tuned for more!

Offline Destructor

Junior Devvie


Medals: 1
Exp: 2 years



« Reply #53 - Posted 2014-12-16 21:07:41 »

Nice engine and updates coming just rapidly Cheesy keep it going Wink
Offline SHC
« Reply #54 - Posted 2014-12-17 17:15:31 »

Hello friends, here's the update for today, the
ResourceLoader
, the way of loading resources in SilenceEngine.



One of the main features of this
ResourceLoader
is that it does not multithread loading, it loads the resources in the same thread that the game is running. And it is really simple to use too. Here's the sample code from the
ResourceLoaderTest
to get you started.

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
ResourceLoader loader = ResourceLoader.getInstance();

int fontID1 = loader.defineFont("Times New Roman", TrueTypeFont.STYLE_NORMAL, 24);
int fontID2 = loader.defineFont("Comic Sans MS", TrueTypeFont.STYLE_NORMAL, 24);
int textureID = loader.defineTexture("resources/texture2.png");

loader.startLoading();

texture = loader.getTexture(textureID);
font1   = loader.getFont(fontID1);
font2   = loader.getFont(fontID2);

It basically follows a very simple three step process that you can simply integrate into any existing game skeleton. You first define the texture and/or fonts which gives you an ID for that resource and call the
startLoading
method on the loader, which starts loading the files in the current thread, displaying a progress bar to show you the progress.

Once loading has been completed, you simply assign your variables to the loaded resources using the IDs that were generated when you defined them, and the game starts playing. Once you don't need those resources, you can simply call
ResourceLoader.getInstance().dispose()
to dispose all the resources that are loaded by the loader.

You can change your logo displayed there by calling the
ResourceLoader.getInstance().setLogo("resources/logo.png")
or whatever, but it is recommended to keep the SilenceEngine logo if you want to show some love!  Roll Eyes

By the way, you can use the
defineFont()
method in two ways, it can be used to load from a face name, or from a TTF file. If you passed it a resource name, it will load from the resource, or it will load from the family name.

Offline SHC
« Reply #55 - Posted 2014-12-19 12:57:45 »

This is just a small update, but a big one actually. SilenceEngine now supports overhanging glyphs in TrueTypeFont.



This update also fixed the issue of texture blending that occured due to the usage of depth testing in OrthoCam.

Offline SHC
« Reply #56 - Posted 2014-12-27 12:58:34 »

Hello friends, I made a new game to test SilenceEngine, this is a 2D Top-Down Shooter, and also my entry for TAFSJ (The Actually Finish Something Jam).

Here is a huge crappy GIF (10 MB)

Click to Play


Download JAR (3.5 MB) ( Requires Java8 )
Source Code (GitHub)

Offline SHC
« Reply #57 - Posted 2014-12-28 09:14:53 »

This is a feature introduction after a long time, now I present you the Audio API of SilenceEngine. I have wrapped up OpenAL into Java classes in
com.shc.silenceengine.audio.openal
package allowing you use OpenAL directly in your code.



I also created a
Sound
class to simply load and play the sound files. Currently I only had
WaveReader
class that can read WAV, MIDI, AI, AIFF files, but I'm looking into JOrbis to be able to load Ogg vorbis audio as well.

The
Sound
class allows you to easily load and play the sounds, all you need to do is to call the constructor with a file name (there is also a constructor for InputStream) and use the methods play, pause, stop, etc., to control the audio.

1  
2  
3  
music = new Sound("resources/music.wav");
music.setLooping(true);
music.play();

You can also use the
ResourceLoader
to load the sounds by using the
defineSound()
and
getSound()
methods of the ResourceLoader. Here is a short example on using it.

1  
2  
3  
4  
ResourceLoader loader = ResourceLoader.getInstance();
int musicID = loader.defineSound("resources/music.wav");
loader.startLoading();
music = loader.getSound(musicID);

That's all for today friends, thanks for stopping by and taking a look at my SilenceEngine.

EDIT: Just added
OggReader
class to read from OGG files. JOrbis and JOgg are awesome!!

Offline SHC
« Reply #58 - Posted 2015-01-05 05:39:21 »

I'm happy to announce that SilenceEngine now reached version 0.0.1, the version information of SilenceEngine follows the three number versioning system, major, minor, alpha/bugfix.

UPDATE ON 14th JANUARY 2015

After a long gap, here is the news about the new collision detection system in 2D. Everything related to collision is present in the
com.shc.silenceengine.collision
package. This package is again sub-divided into
colliders
and
broadphase
.

In SilenceEngine, a collider is an object that checks for collisions among registered types of entities in a scene. The broadphase, is an algorithm that reduces the amount of collision tests that need to be performed. I provide three implementations of broadphase, namely
Grid
,
QuadTree
and
DynamicTree2D
. Each of these implementations implement the
IBroadPhaseResolver2D
interface.

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
public interface IBroadphaseResolver2D
{
    public void clear();

    public void insert(Entity2D e);

    public void remove(Entity2D e);

    public List<Entity2D> retrieve(Rectangle rect);

    public default List<Entity2D> retrieve(Entity2D e)
    {
        return retrieve(e.getBounds());
    }
}

For every of these broadphase implementation, there is a collider implemented. All you have to do, is to create an instance of that collider and set the scene to check for collisions. Here is an example for each of these implementations.

GridSceneCollider

The GridSceneCollider uses the Grid as the broadphase implementation. This is a boundary based broadphase, meaning that you should specify an area rectangle which is then divided into grid cells or buckets. In my opinion, use this when you are having small scenes which are in fixed space.

1  
2  
3  
4  
5  
Scene scene = // Create the scene here
scene.init();

SceneCollider2D collider = new GridSceneCollider(sceneWidth, sceneHeight, cellWidth, cellHeight);
collider.setScene(scene);

All the parameters passed here are self explanatory, and are in pixels. The Scene class doesn't give them for you, so tweak those values to make it efficient.

QuadTreeSceneCollider

The QuadTreeSceneCollider uses the QuadTree as the broadphase algorithm to reduce the number of collision checks. Unlike the Grid, this QuadTree is more memory efficient (due to less number of ArrayLists of objects stored) and works well for large maps too. Here is some example code.

1  
2  
3  
4  
5  
Scene scene = // Create the scene here
scene.init();

SceneCollider2D collider = new QuadTreeSceneCollider(sceneWidth, sceneHeight);
collider.setScene(scene);

This collider requires only two parameters, scene width and scene height, in pixels. It is better to use this collider instead of GridSceneCollider, because of it's less memory usage.

DynamicSceneCollider2D

This DynamicSceneCollider2D uses the 2D Dynamic AABB Tree as it's broadphase implementation. The advantage of this collider is that it is borderless, it has no constraints. I have implemented this by following a tutorial called Game Physics: Broadphase Dynamic AABB Tree written by Allen Chou. Here is some code as usual.

1  
2  
3  
4  
5  
Scene scene = // Create the scene here
scene.init();

SceneCollider2D collider = new DynamicSceneCollider2D();
collider.setScene(scene);

This collider is the best of all the colliders in SilenceEngine in my opinion, and I recommend to use this if you are not sure why to use others. The best part, is this collider doesn't require any parameters, and is also very fast. Thanks to @Phased for helping me to spot a bug that occurred when I'm translating pointers in C++ to Java references when following Allen's tutorial.

That's all for now folks! Stay tuned for more information!!

Offline BlueChaos
« Reply #59 - Posted 2015-04-01 15:33:38 »

Great work on this! Can't wait to see more updates.  Wink
Pages: 1 [2] 3
  ignore  |  Print  
 
 

 
xxMrPHDxx (13 views)
2017-11-21 16:21:00

xxMrPHDxx (10 views)
2017-11-21 16:14:31

xxMrPHDxx (10 views)
2017-11-21 16:10:57

Ecumene (113 views)
2017-09-30 02:57:34

theagentd (148 views)
2017-09-26 18:23:31

cybrmynd (246 views)
2017-08-02 12:28:51

cybrmynd (242 views)
2017-08-02 12:19:43

cybrmynd (242 views)
2017-08-02 12:18:09

Sralse (257 views)
2017-07-25 17:13:48

Archive (875 views)
2017-04-27 17:45:51
List of Learning Resources
by elect
2017-03-13 14:05:44

List of Learning Resources
by elect
2017-03-13 14:04:45

SF/X Libraries
by philfrei
2017-03-02 08:45:19

SF/X Libraries
by philfrei
2017-03-02 08:44:05

SF/X Libraries
by SkyAphid
2017-03-02 06:38:56

SF/X Libraries
by SkyAphid
2017-03-02 06:38:32

SF/X Libraries
by SkyAphid
2017-03-02 06:38:05

SF/X Libraries
by SkyAphid
2017-03-02 06:37:51
java-gaming.org is not responsible for the content posted by its members, including references to external websites, and other references that may or may not have a relation with our primarily gaming and game production oriented community. inquiries and complaints can be sent via email to the info‑account of the company managing the website of java‑gaming.org
Powered by MySQL Powered by PHP Powered by SMF 1.1.18 | SMF © 2013, Simple Machines | Managed by Enhanced Four Valid XHTML 1.0! Valid CSS!