Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Feature: Mod Browser #231

Draft
wants to merge 28 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e16f313
Adding ModSource interface
Mar 21, 2021
eaeddc8
Adding github resource
Mar 23, 2021
fd635b5
Adding mod data
Mar 23, 2021
1911f1d
Adding local file system source
Mar 23, 2021
65b4f29
Adding temporary mod manager
Mar 23, 2021
e96fc9b
Cutting down implementation
Apr 5, 2021
10e6394
Renaming GitHub mod source file
Apr 5, 2021
42bf39a
Fixing indentation
Apr 5, 2021
c8344c4
Simplifying enabled check
Apr 5, 2021
60f634b
Merge pull request #3 from PythooonUser/feature/mod-source
Apr 5, 2021
728be54
Initial work on mod.io mod source
joshuaskelly Apr 15, 2021
11fa086
Extending LocalFileSystemModSource
joshuaskelly Apr 15, 2021
4d36f71
Removing unused header
joshuaskelly Apr 15, 2021
33d9c9a
Only download if not present on disk
joshuaskelly Apr 15, 2021
d74e055
:lipstick:
joshuaskelly Apr 15, 2021
8bbbed2
Adding ZipUtils to extract contents of zip files
joshuaskelly Apr 29, 2021
a49e6f6
Don't write directories as files
joshuaskelly Apr 29, 2021
55584f4
Restructuring how modio content is arranged
joshuaskelly Apr 29, 2021
64be8eb
Making allModsRoute transient
joshuaskelly Apr 29, 2021
81b695b
Create a modInfo.json in the mod root if neccesary
joshuaskelly Apr 30, 2021
54c74dc
Adding mods directories to gitignore
joshuaskelly Apr 30, 2021
45ce036
Removing RestEndpoint class
joshuaskelly Apr 30, 2021
f8ae0db
Removing unused import
joshuaskelly Apr 30, 2021
d1a997d
Adding progress to DownloadListener
joshuaskelly Apr 30, 2021
ff9fdd2
Merge pull request #4 from joshuaskelly/feature/mod-source-modio
May 1, 2021
57e6190
Adding WIP
May 4, 2021
925e695
Adding unique ID to mod info
May 5, 2021
613c84a
Merge pull request #5 from PythooonUser/feature/mod-source-github
May 5, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ save/
Dungeoneer/save/
Dungeoneer/assets/packaged_files.txt
DungeoneerDesktop/save
**/mods/
**/.mods/

*.iml
.idea/*
Expand Down
52 changes: 42 additions & 10 deletions Dungeoneer/src/com/interrupt/dungeoneer/game/ModManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ArrayMap;
import com.badlogic.gdx.utils.Json;
import com.interrupt.api.steam.SteamApi;
import com.interrupt.api.steam.workshop.WorkshopModData;
import com.interrupt.dungeoneer.entities.Door;
import com.interrupt.dungeoneer.entities.Entity;
Expand All @@ -14,6 +12,10 @@
import com.interrupt.dungeoneer.gfx.TextureAtlas;
import com.interrupt.dungeoneer.gfx.animation.lerp3d.LerpedAnimationManager;
import com.interrupt.dungeoneer.gfx.shaders.ShaderData;
import com.interrupt.dungeoneer.modding.InternalFileSystemModSource;
import com.interrupt.dungeoneer.modding.ModInfo;
import com.interrupt.dungeoneer.modding.ModSource;
import com.interrupt.dungeoneer.modding.SteamWorkshopModSource;
import com.interrupt.dungeoneer.scripting.ScriptingApi;
import com.interrupt.managers.*;
import com.interrupt.utils.JsonUtil;
Expand All @@ -22,6 +24,11 @@
import java.util.HashMap;

public class ModManager {
/** Array of sources to look for mod content. */
private Array<ModSource> sources = new Array<>();

/** Array of default sources to look for mod content. */
private transient Array<ModSource> defaultSources = new Array<>();

private transient Array<String> allMods = new Array<String>();

Expand Down Expand Up @@ -56,11 +63,23 @@ public static void setScriptingApi(ScriptingApi newScriptingApi) {
}

private void loadModsEnabledList() {
defaultSources.clear();
sources.clear();

// Add default mod sources.
defaultSources.add(new InternalFileSystemModSource("mods"));
defaultSources.add(new SteamWorkshopModSource());

try {
FileHandle progressionFile = Game.getFile(Options.getOptionsDir() + "modslist.dat");
if(progressionFile.exists()) {
ModManager loaded = JsonUtil.fromJson(ModManager.class, progressionFile);
sources = loaded.sources;
modsEnabled = loaded.modsEnabled;

for (ModSource source : sources) {
source.init();
}
}
} catch (Exception e) {
Gdx.app.error("DelverMods", e.getMessage());
Expand Down Expand Up @@ -89,13 +108,13 @@ private void findMods() {
// add the default search paths
allMods.add(".");

FileHandle fh = Game.getInternal("mods");
for(FileHandle h : fh.list()) {
if(h.isDirectory()) allMods.add("mods/" + h.name());
for (ModSource source : defaultSources) {
allMods.addAll(source.getInstalledMods());
}

// add any mods subscribed in Steam Workshop
allMods.addAll(SteamApi.api.getWorkshopFolders());
for (ModSource source : sources) {
allMods.addAll(source.getInstalledMods());
}
}

private void filterMods() {
Expand Down Expand Up @@ -418,11 +437,11 @@ public GenTheme loadTheme(String filename) {
}

public boolean checkIfModIsEnabled(String mod) {
if(modsEnabled == null)
if (modsEnabled == null) {
return true;
}

Boolean enabled = modsEnabled.get(mod);
return enabled == null || enabled;
return modsEnabled.getOrDefault(mod, true);
}

// Call refresh after changing this!
Expand Down Expand Up @@ -467,11 +486,24 @@ public WorkshopModData getDataForMod(String modPath) {
}
}

public ModInfo getModInfo(String path) {
try {
return JsonUtil.fromJson(ModInfo.class, new FileHandle(path).child("modInfo.json"));
}
catch (Exception ignored) {
return null;
}
}

public String getModName(String modFolder) {
if(Game.modManager != null) {
WorkshopModData data = getDataForMod(modFolder);
if(data != null && data.title != null && !data.title.isEmpty())
return data.title;

ModInfo modInfo = getModInfo(modFolder);
if (modInfo != null && modInfo.name != null && !modInfo.name.isEmpty())
return modInfo.name;
}

FileHandle path = new FileHandle(modFolder);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.interrupt.dungeoneer.modding;

import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Array;

public abstract class AbstractFileSystemModSource implements ModSource {
/** The root file path to look for mod content. */
protected String root;

protected AbstractFileSystemModSource() {
}

protected AbstractFileSystemModSource(String root) {
this.root = root;
}

protected abstract FileHandle getRootHandle();

@Override
public Array<String> getInstalledMods() {
Array<String> mods = new Array<>();
FileHandle rootHandle = getRootHandle();

if (!rootHandle.isDirectory()) {
return mods;
}

for (FileHandle childHandle : rootHandle.list()) {
if (childHandle.isDirectory()) {
mods.add(childHandle.path());
}
}

return mods;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.interrupt.dungeoneer.modding;

import java.util.HashMap;
import java.util.UUID;

import com.badlogic.gdx.files.FileHandle;
import com.interrupt.dungeoneer.game.Game;
import com.interrupt.dungeoneer.net.DownloadListenerAdapter;
import com.interrupt.utils.DownloadUtils;
import com.interrupt.utils.JsonUtil;
import com.interrupt.utils.ZipUtils;

public class GitHubRepoModSource extends LocalFileSystemModSource {
private String username;
private String repository;
private boolean hasMultipleMods;

private transient String downloadsPath;
private transient String modsPath;

@Override
public void init() {
if (root == null || root.isEmpty()) {
root = ".mods/github/";
}

if (!root.endsWith("/"))
root += "/";

downloadsPath = root + ".downloads/";
modsPath = root + "mods/";

refresh();
}

@Override
protected FileHandle getRootHandle() {
return Game.getFile(modsPath);
}

public void refresh() {
HashMap<String, String> header = new HashMap<>();
header.put("Accept", "application/vnd.github.v3+json");

String url = "https://api.github.com/repos/" + this.username + "/" + this.repository + "/zipball";
String filename = this.username + "-" + this.repository + ".zip";
String filepath = downloadsPath + filename;

FileHandle file = Game.getFile(filepath);
if (!file.exists()) {
DownloadUtils.downloadFile(url, filepath, header, new DownloadListenerAdapter() {
@Override
public void completed(FileHandle file) {
// Extract contents to temporary path.
FileHandle path = Game.getFile(downloadsPath + "/" + username + "-" + repository + "/");
ZipUtils.extract(file, path);

// Move into actual repository root.
FileHandle[] innerRoot = path.list();
path = innerRoot[0];

if (hasMultipleMods) {
for (FileHandle root : path.list()) {
if (!root.isDirectory())
continue;

FileHandle modInfoPath = root.child("modInfo.json");
if (!modInfoPath.exists()) {
ModInfo modInfo = new ModInfo();
modInfo.id = UUID.randomUUID().getMostSignificantBits() & Long.MAX_VALUE;
modInfo.name = root.nameWithoutExtension();

JsonUtil.toJson(modInfo, modInfoPath);
}

FileHandle destination = new FileHandle(modsPath + "/" + username + "-" + repository + "-" + root.nameWithoutExtension() + "/");
destination.mkdirs();
for (FileHandle item : root.list()) {
item.copyTo(destination);
}
}
} else {
FileHandle modInfoPath = path.child("modInfo.json");
if (!modInfoPath.exists()) {
ModInfo modInfo = new ModInfo();
modInfo.id = 0;
modInfo.name = username + "-" + repository;

JsonUtil.toJson(modInfo, modInfoPath);
}

FileHandle destination = new FileHandle(modsPath + "/" + username + "-" + repository + "/");
destination.mkdirs();
for (FileHandle item : path.list()) {
item.copyTo(destination);
}
}
}
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.interrupt.dungeoneer.modding;

import com.badlogic.gdx.files.FileHandle;
import com.interrupt.dungeoneer.game.Game;

public class InternalFileSystemModSource extends AbstractFileSystemModSource {
public InternalFileSystemModSource(String root) {
super(root);
}

@Override
protected FileHandle getRootHandle() {
return Game.getInternal(root);
}

@Override
public void init() {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.interrupt.dungeoneer.modding;

import com.badlogic.gdx.files.FileHandle;
import com.interrupt.dungeoneer.game.Game;

public class LocalFileSystemModSource extends AbstractFileSystemModSource {
@Override
protected FileHandle getRootHandle() {
return Game.getFile(root);
}

@Override
public void init() {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.interrupt.dungeoneer.modding;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Net;
import com.badlogic.gdx.Net.HttpRequest;
import com.badlogic.gdx.net.HttpRequestBuilder;
import com.badlogic.gdx.files.FileHandle;
import com.interrupt.dungeoneer.game.Game;
import com.interrupt.dungeoneer.modding.modio.*;
import com.interrupt.dungeoneer.net.DownloadListenerAdapter;
import com.interrupt.dungeoneer.net.ObjectResponseListener;
import com.interrupt.utils.DownloadUtils;
import com.interrupt.utils.JsonUtil;
import com.interrupt.utils.ZipUtils;

public class ModIOModSource extends LocalFileSystemModSource {
private int gameID;
private String apiKey;

private transient String downloadsPath;
private transient String modsPath;

@Override
public void init() {
if (root == null || root.isEmpty()) {
root = ".mods/modio/";
}

if (!root.endsWith("/")) root += "/";

downloadsPath = root + ".downloads/";
modsPath = root + "mods/";

refresh();
}

@Override
protected FileHandle getRootHandle() {
return Game.getFile(modsPath);
}

public void refresh() {
HttpRequestBuilder requestBuilder = new HttpRequestBuilder();
HttpRequest request = requestBuilder
.newRequest()
.method(Net.HttpMethods.GET)
.header("User-Agent", "delverengine/" + Game.VERSION)
.url("https://api.test.mod.io/v1/games/" + gameID + "/mods")
.content("api_key=" + apiKey)
.build();

Gdx.net.sendHttpRequest(request, new ObjectResponseListener<GetMods>(GetMods.class) {
@Override
public void handleObjectResponse(GetMods mods) {
for (ModObject mod : mods.data) {
String url = mod.modfile.download.binary_url;
String filename = mod.modfile.filename;
String filepath = downloadsPath + filename;

FileHandle file = Game.getFile(filepath);
if (!file.exists()) {
DownloadUtils.downloadFile(
url,
filepath,
null,
new DownloadListenerAdapter() {
@Override
public void completed(FileHandle file) {
FileHandle path = Game.getFile(modsPath + "/" + mod.id + "/");
ZipUtils.extract(file, path);

FileHandle modInfoPath = path.child("modInfo.json");
if (!modInfoPath.exists()) {
ModInfo modInfo = new ModInfo();
modInfo.id = mod.id;
modInfo.name = mod.name;

JsonUtil.toJson(modInfo, modInfoPath);
}
}
}
);
}
}
}
});
}
}
6 changes: 6 additions & 0 deletions Dungeoneer/src/com/interrupt/dungeoneer/modding/ModInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.interrupt.dungeoneer.modding;

public class ModInfo {
public long id;
public String name;
}
Loading