This is a very simple Java tool for games and other apps to manage resources.
The current version is 1.1
. This version is compatible with Java 17 and above.
The artifact can be installed from my Maven repository.
// Kotlin DSL:
repositories {
// Add my repository
maven { url = uri("https://maven.shadew.net/") }
}
dependencies {
// Add the artifact
implementation("dev.runefox:rms:1.1")
}
// Groovy DSL:
repositories {
// Add my repository
maven { url "https://maven.shadew.net/" }
}
dependencies {
// Add the artifact
implementation "dev.runefox:rms:1.1"
}
<repositories>
<!-- Add my repository -->
<repository>
<id>Runefox Maven</id>
<url>https://maven.shadew.net/</url>
</repository>
</repositories>
<dependencies>
<!-- Add the artifact -->
<dependency>
<groupId>dev.runefox</groupId>
<artifactId>rms</artifactId>
<version>1.1</version>
</dependency>
</dependencies>
There are 3 core components to the system:
ResourceManager
Resource
ResourceType
You need at the very least a ResourceManager
, which is a concrete class you can simply instantiate:
ResourceManager mgr = new ResourceManager(path, "namespace");
There are 2 main things you need to create the resource manager:
- A
Path
, to the root directory that contains all your resources. - A "standard namespace", which is the main namespace of your application. This is usually one identifier, like
rms
.
There is a special static method on ResourceManager
that allows you to get a Path
for a resource folder on the class path. This method uses the ClassLoader.getResource
method, which needs a resource file in order to work. So, to get the resources from your jar file, you need to define some empty file in the root directory of your resources folder, and call ResourceManager.classpath("/.resources_root")
if it's named .resources_root
. The returned path is then the parent directory of that file, possibly inside of a jar file.
ResourceManager mgr = new ResourceManager(ResourceManager.classpath(".resources_root"), "namespace");
Whenever you're done with your resources, or need to reload all of them, call ResourceManager.dispose
.
The next step is to create a Resource
implementation. Resource
is an interface you can implement and it just has one method: dispose
. This method is called by the ResourceManager
upon ResourceManager.dispose
. Usually, this method does nothing, but if your resource acquires some native resources (like off-heap memory) that must be cleaned up, then this is the place to do that. This is a resource that just contains a string:
public class ContentResource implements Resource {
private final String content;
public ContentResource(String content) {
this.content = content;
}
public String content() {
return content;
}
@Override
public void dispose() {
// Not applicable
}
}
Once you've got a Resource
implementation, you have to define a way this resource is loaded. The ResourceManager
assumes this resource to be in a certain folder, so all you have to do is locate it inside of that folder, read the files that must be read and turn them into your resource. Typically, the resource is in a file with the same name as the name of the resource. Resources may have path names, so you can group similar resources of the same type into a subfolder.
The loading of resources from the filesystem is done by a ResourceType
. The ResourceType
interface has two methods: load
and createFallback
. First load
is called to locate and load the resource, and if that fails, createFallback
should be able to create some kind of fallback in case the resource fails loading. For above example we could create the following resource type:
public class ContentResourceType implements ResourceType<ContentResource> {
@Override
public ContentResource load(ResourceManager res, String ns, String name, Path directory) throws Exception {
// The `directory` parameter already accounts for the namespace, so you don't need to incorporate the
// namespace into locating the resource.
try (BufferedReader in = Files.newBufferedReader(directory.resolve(name + ".txt"), StandardCharsets.UTF_8)) {
StringWriter writer = new StringWriter();
in.transferTo(writer);
return new ContentResource(writer.toString());
}
}
@Override
public ContentResource createFallback(ResourceManager res, String ns, String name) {
// The fallback should be created from memory, it should not load from a file.
return new ContentResource("[Failed loading.]");
}
}
You may, however, encounter resources made of text in this specific structure quite often. There are 3 common interfaces that extend ResourceType
which ease the loading of single-file resources (which are resources of just one file which has the name of the resource):
FileResourceType
is a type for loading binary files: it provides you with anInputStream
that streams the contents of the file.TextResourceType
is a type for loading text files: it provides you with aReader
that reads the contents of the file, by default in UTF-8.StringResourceType
is an extension onTextResourceType
that reads the entire file into aString
, which it then provides to you.
Above example could benefit from a StringResourceType
:
public class ContentResourceType implements StringResourceType<ContentResource> {
@Override
public ContentResource load(ResourceManager res, String ns, String name, String content) {
return new ContentResource(content);
}
@Override
public String extension() {
return "txt";
}
@Override
public ContentResource createFallback(ResourceManager res, String ns, String name) {
return new ContentResource("[Failed loading.]");
}
}
StringResourceType
does the exact Reader.transferTo(StringWriter)
operation to provide us the string content. We don't need to do this ourselves.
Whereas you will have multiple instances of your Resource
implementation, you only have one matching ResourceType
. So typically you'd create public static final
field somewhere with its instance because you still need it later to retrieve resources:
public static final ContentResourceType INSTANCE = new ContentResourceType();
When you got a ResourceManager
, Resource
and ResourceType
, the next thing is loading the resources. First you will have to tell your ResourceManager
about your ResourceType
. This is so it can cache resources properly. You do this using the register
method:
// ...create resource manager here...
mgr.register(ContentResourceType.INSTANCE, "content");
You pass a directory name, which is the directory in which the resources are. The resource is then located in <root path>/<namespace>/<type directory>/
, where:
<root path>
is the path given when creating the resource manager<namespace>
is the namespace of the resource<type directory>
is the directory name given when registering a resource type to the resource manager
You can now retrieve a resource using ResourceManager.get
:
ContentResource myContent = mgr.get(ContentResourceType.INSTANCE, "namespace", "my_content");
// This will load <root path>/namespace/content/my_content.txt
// You can leave out the namespace, it will fall back on the namespace given when creating the ResourceManager
ContentResource myContent = mgr.get(ContentResourceType.INSTANCE, "my_content");
In some occasion, you may want to reload all the resources in your application. This means all the Resource
instances change. You will have to re-obtain all the resources yourself, which can be quite annoying to do. That is, unless you use Handle
s. A Handle
is a reference to a resource. It will store the resource for quick access but it's controlled by the resource manager and reset upon dispose()
. Instead of calling ResourceManager.get
, you can call ResourceManager.handle
in exactly the same way to get a handle instead. You can then store this handle anywhere in your app without ever having to think about reloading it again. Another advantage of using handles is that they're lazily loaded: it will load the resource only when you request it using Handle.get
. A handle is simply just a glorified Supplier
.
private final Handle<ContentResource> myContent = mgr.handle(ContentResourceType.INSTANCE, "my_content");
...
ContentResource instance = myContent.get();
Now you can call dispose
at any time to simply unload all resources. RMS will invalidate all handles and the next time you call get
on any of them, it will reload the resource. In fact, you can call dispose
and give a new root path, and the resource will load as usual from the new path.
Below is a list of libraries that load files in certain formats, with an artifact that belongs to it. All artifacts have the same version as the main project and all artifacts depend on the main project with the same version (in the list referred to with [rms-version]
).
- JSON: use
dev.runefox:rms-json:[rms-version]
.
In the future I may add more supported formats through my libraries or third-party libraries.
Licensed under the LGPLv3 license. For the full LGPL+GPLv3 license, see LICENSE
.
Copyright (C) 2023 Samū
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.