Skip to content

Commit

Permalink
Game metadata extractor and some fixes
Browse files Browse the repository at this point in the history
- App not closing on back pressed (AlberInputListener.java)
- Extract SMDH from rom.
- Extract metadata from SMDH
  • Loading branch information
GabrielBRDeveloper committed Dec 21, 2023
1 parent 9ca7e88 commit c0960dc
Show file tree
Hide file tree
Showing 13 changed files with 370 additions and 16 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ if(ENABLE_VULKAN)
endif()

if(ANDROID)
set(HEADER_FILES ${HEADER_FILES} include/jni_driver.hpp)
set(ALL_SOURCES ${ALL_SOURCES} src/jni_driver.cpp)
endif()

Expand Down
7 changes: 7 additions & 0 deletions include/jni_driver.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#include <vector>
#include "helpers.hpp"

class Pandroid {
public:
static void onSmdhLoaded(const std::vector<u8> &smdh);
};
9 changes: 9 additions & 0 deletions src/core/loader/ncch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
#include "loader/ncch.hpp"
#include "memory.hpp"

#ifdef __ANDROID__
#include "jni_driver.hpp"
#endif

#include <iostream>

bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSInfo &info) {
Expand Down Expand Up @@ -255,6 +259,11 @@ bool NCCH::parseSMDH(const std::vector<u8>& smdh) {
return false;
}


#ifdef __ANDROID__
Pandroid::onSmdhLoaded(smdh);
#endif

// Bitmask showing which regions are allowed.
// https://www.3dbrew.org/wiki/SMDH#Region_Lockout
const u32 regionMasks = *(u32*)&smdh[0x2018];
Expand Down
46 changes: 45 additions & 1 deletion src/jni_driver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@
#include "renderer_gl/renderer_gl.hpp"
#include "services/hid.hpp"

#include "jni_driver.hpp"

std::unique_ptr<Emulator> emulator = nullptr;
HIDService* hidService = nullptr;
RendererGL* renderer = nullptr;
bool romLoaded = false;
JavaVM* jvm = nullptr;
const char* alberClass = "com/panda3ds/pandroid/AlberDriver";

#define AlberFunction(type, name) JNIEXPORT type JNICALL Java_com_panda3ds_pandroid_AlberDriver_##name

Expand All @@ -20,7 +24,47 @@ void throwException(JNIEnv* env, const char* message) {
env->ThrowNew(exceptionClass, message);
}

JNIEnv* jniEnv(){
JNIEnv* env;
auto status = jvm->GetEnv((void **)&env, JNI_VERSION_1_6);
if(status == JNI_EDETACHED){
jvm->AttachCurrentThread(&env, nullptr);
} else if(status != JNI_OK){
throw std::runtime_error("Failed to obtain JNIEnv from JVM!!");
}
return env;
}


void Pandroid::onSmdhLoaded(const std::vector<u8> &smdh){
JNIEnv* env = jniEnv();
int size = smdh.size();

jbyteArray result = env->NewByteArray(size);

jbyte buffer[size];

for(int i = 0; i < size; i++){
buffer[i] = (jbyte) smdh[i];
}
env->SetByteArrayRegion(result, 0, size, buffer);


auto clazz = env->FindClass(alberClass);
auto method = env->GetStaticMethodID(clazz, "OnSmdhLoaded", "([B)V");

env->CallStaticVoidMethod(clazz, method, result);

env->DeleteLocalRef(result);
}


extern "C" {

AlberFunction(void, Setup)(JNIEnv* env, jobject obj) {
env->GetJavaVM(&jvm);
}

AlberFunction(void, Initialize)(JNIEnv* env, jobject obj) {
emulator = std::make_unique<Emulator>();

Expand Down Expand Up @@ -73,4 +117,4 @@ AlberFunction(void, SetCirclepadAxis)(JNIEnv* env, jobject obj, jint x, jint y)
}
}

#undef AlberFunction
#undef AlberFunction
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
package com.panda3ds.pandroid;

import android.util.Log;

import com.panda3ds.pandroid.data.SMDH;
import com.panda3ds.pandroid.data.game.GameMetadata;
import com.panda3ds.pandroid.utils.Constants;
import com.panda3ds.pandroid.utils.GameUtils;

public class AlberDriver {
AlberDriver() { super(); }

public static native void Setup();
public static native void Initialize();
public static native void RunFrame(int fbo);
public static native boolean HasRomLoaded();
Expand All @@ -15,5 +23,13 @@ public class AlberDriver {
public static native void TouchScreenUp();
public static native void TouchScreenDown(int x, int y);

public static void OnSmdhLoaded(byte[] buffer) {
Log.i(Constants.LOG_TAG, "Loaded rom smdh");
SMDH smdh = new SMDH(buffer);
GameMetadata game = GameUtils.getCurrentGame();
GameUtils.removeGame(game);
GameUtils.addGame(GameMetadata.applySMDH(game, smdh));
}

static { System.loadLibrary("Alber"); }
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

public class GameActivity extends BaseActivity {

private final AlberInputListener inputListener = new AlberInputListener();
private final AlberInputListener inputListener = new AlberInputListener(this);

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import android.app.Application;
import android.content.Context;

import com.panda3ds.pandroid.AlberDriver;
import com.panda3ds.pandroid.data.config.GlobalConfig;
import com.panda3ds.pandroid.input.InputMap;
import com.panda3ds.pandroid.utils.GameUtils;
Expand All @@ -17,6 +18,7 @@ public void onCreate() {
GlobalConfig.initialize();
GameUtils.initialize();
InputMap.initialize();
AlberDriver.Setup();
}

public static Context getAppContext() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
package com.panda3ds.pandroid.app.game;

import android.app.Activity;
import android.view.KeyEvent;

import com.panda3ds.pandroid.AlberDriver;
import com.panda3ds.pandroid.input.InputEvent;
import com.panda3ds.pandroid.input.InputMap;
import com.panda3ds.pandroid.input.KeyName;
import com.panda3ds.pandroid.lang.Function;
import com.panda3ds.pandroid.math.Vector2;

import java.util.Objects;

public class AlberInputListener implements Function<InputEvent> {
private final Activity activity;
public AlberInputListener(Activity activity){
this.activity = activity;
}

private final Vector2 axis = new Vector2(0.0f, 0.0f);

@Override
public void run(InputEvent event) {
KeyName key = InputMap.relative(event.getName());

if (Objects.equals(event.getName(), "KEYCODE_BACK")){
activity.onBackPressed();
return;
}

if (key == KeyName.NULL)
return;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public void onActivityResult(Uri result) {
String uri = result.toString();
if (GameUtils.findByRomPath(uri) == null) {
FileUtils.makeUriPermanent(uri, FileUtils.MODE_READ);
GameMetadata game = new GameMetadata(FileUtils.getName(uri).split("\\.")[0], uri, "Unknown");
GameMetadata game = new GameMetadata(uri, FileUtils.getName(uri).split("\\.")[0],"Unknown");
GameUtils.addGame(game);
GameUtils.launch(requireActivity(), game);
}
Expand Down
167 changes: 167 additions & 0 deletions src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/SMDH.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package com.panda3ds.pandroid.data;

import android.graphics.Bitmap;

import com.panda3ds.pandroid.data.game.GameRegion;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;

public class SMDH {
private static final int ICON_SIZE = 48;
private static final int META_OFFSET = 0x8 ;
private static final int META_REGION_OFFSET = 0x2018;
private static final int IMAGE_OFFSET = 0x24C0;

private int metaIndex = 1;
private final ByteBuffer smdh;
private final String[] title = new String[12];
private final String[] publisher = new String[12];
private final int[] icon;

private final GameRegion region;

public SMDH(byte[] source){
smdh = ByteBuffer.allocate(source.length);
smdh.position(0);
smdh.put(source);
smdh.position(0);

region = parseRegion();
icon = parseIcon();
parseMeta();
}

private GameRegion parseRegion(){
GameRegion region;
smdh.position(META_REGION_OFFSET);

int regionMasks = smdh.get() & 0xFF;

final boolean japan = (regionMasks & 0x1) != 0;
final boolean northAmerica = (regionMasks & 0x2) != 0;
final boolean europe = (regionMasks & 0x4) != 0;
final boolean australia = (regionMasks & 0x8) != 0;
final boolean china = (regionMasks & 0x10) != 0;
final boolean korea = (regionMasks & 0x20) != 0;

final boolean taiwan = (regionMasks & 0x40) != 0;
if (northAmerica) {
region = GameRegion.NorthAmerican;
} else if (europe) {
region = GameRegion.Europe;
} else if (australia) {
region = GameRegion.Australia;
} else if (japan) {
region = GameRegion.Japan;
metaIndex = 0;
} else if (korea) {
metaIndex = 7;
region = GameRegion.Korean;
} else if (china) {
metaIndex = 6;
region = GameRegion.China;
} else if (taiwan) {
metaIndex = 6;
region = GameRegion.Taiwan;
} else {
region = GameRegion.None;
}

return region;
}

private void parseMeta(){

for (int i = 0; i < 12; i++){
smdh.position(META_OFFSET + (512*i) + 0x80);
byte[] data = new byte[0x100];
smdh.get(data);
title[i] = convertString(data).replaceAll("\n", " ");
}

for (int i = 0; i < 12; i++){
smdh.position(META_OFFSET + (512 * i) + 0x180);
byte[] data = new byte[0x80];
smdh.get(data);
publisher[i] = convertString(data);
}
}

private int[] parseIcon() {
int[] icon = new int[ICON_SIZE*ICON_SIZE];
smdh.position(0);

for (int x = 0; x < ICON_SIZE; x++) {
for (int y = 0; y < ICON_SIZE; y++) {
int curseY = y & ~7;
int curseX = x & ~7;

int i = mortonInterleave(x, y);
int offset = (i + (curseX * 8)) * 2;

offset = offset + curseY * 48 * 2;

smdh.position(offset + IMAGE_OFFSET);

int bit1 = smdh.get() & 0xFF;
int bit2 = smdh.get() & 0xFF;

int pixel = bit1 + (bit2 << 8);

int r = (((pixel & 0xF800) >> 11) << 3);
int g = (((pixel & 0x7E0) >> 5) << 2);
int b = (((pixel & 0x1F)) << 3);

//Convert to ARGB8888
icon[x + 48 * y] = 255 << 24 | (r & 255) << 16 | (g & 255) << 8 | (b & 255);
}
}
return icon;
}


public GameRegion getRegion() {
return region;
}

public Bitmap getBitmapIcon(){
Bitmap bitmap = Bitmap.createBitmap(ICON_SIZE, ICON_SIZE, Bitmap.Config.RGB_565);
bitmap.setPixels(icon,0,ICON_SIZE,0,0,ICON_SIZE,ICON_SIZE);
return bitmap;
}

public int[] getIcon() {
return icon;
}

public String getTitle(){
return title[metaIndex];
}

public String getPublisher(){
return publisher[metaIndex];
}

// SMDH stores string in UTF-16LE format
private static String convertString(byte[] buffer){
try {
return new String(buffer,0, buffer.length, StandardCharsets.UTF_16LE)
.replaceAll("\0","");
} catch (Exception e){
return "";
}
}

// u and v are the UVs of the relevant texel
// Texture data is stored interleaved in Morton order, ie in a Z - order curve as shown here
// https://en.wikipedia.org/wiki/Z-order_curve
// Textures are split into 8x8 tiles.This function returns the in - tile offset depending on the u & v of the texel
// The in - tile offset is the sum of 2 offsets, one depending on the value of u % 8 and the other on the value of y % 8
// As documented in this picture https ://en.wikipedia.org/wiki/File:Moser%E2%80%93de_Bruijn_addition.svg
private static int mortonInterleave(int u, int v) {
int[] xlut = {0, 1, 4, 5, 16, 17, 20, 21};
int[] ylut = {0, 2, 8, 10, 32, 34, 40, 42};
return xlut[u % 8] + ylut[v % 8];
}
}
Loading

0 comments on commit c0960dc

Please sign in to comment.