layout: restructure into src/ tests/ android/ scripts/ tools/

- extension/src/{port,platform,codepage} -> src/; native_render -> src/host
  (+ dxt, shaders/); libgr2 -> src/gr2; extension/third_party -> third_party
- extension/tests -> tests/port, libgr2/tests -> tests/gr2
- android-native -> android (build.sh, push-client.sh moved in)
- script -> scripts; tools/40250 -> tools/server; oracle -> tools/granny-oracle;
  perf tools -> tools/perf
- all build trees under build/ (native, release, android, port-gate)
- xrender:: CMake aliases -> mt::; port-map ledger impl paths rewritten

No code changes. ctest 15/15, port_gate macos+android PASS, port_map check 0 errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-29 19:08:19 +09:00
co-authored by Claude Opus 5.5
parent 70710477cf
commit a46093104c
2817 changed files with 13728 additions and 13744 deletions
+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x400003" android:required="true" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<application android:label="@string/app_name"
android:allowBackup="false"
android:appCategory="game"
android:isGame="true"
android:extractNativeLibs="true"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:hardwareAccelerated="true"
android:usesCleartextTraffic="true">
<activity android:name=".MainActivity"
android:label="@string/app_name"
android:alwaysRetainTaskState="true"
android:launchMode="singleInstance"
android:screenOrientation="sensorLandscape"
android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
android:preferMinimalPostProcessing="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,293 @@
package org.metin2port.client;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.os.BatteryManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Display;
import android.view.DisplayCutout;
import android.view.HapticFeedbackConstants;
import android.view.RoundedCorner;
import android.view.Choreographer;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import org.libsdl.app.SDLActivity;
/**
* Runs src/host's main() (libmain.so). Command-line arguments come from the launch intent,
* e.g. adb shell am start -n org.metin2port.client/.MainActivity \
* --es args "--live-server 192.168.21.203:11000:13000 --login-screen"
* A launcher start (no arguments) opens the login screen against DEFAULT_ARGS' server, so the
* installed app is played by hand on the real server. Any explicit args replace DEFAULT_ARGS; args
* without --live-server (e.g. --es args "--fake-mobs 64") run the in-process fake-server smoke flow.
*/
public class MainActivity extends SDLActivity {
// --perf-log: src/host/perf_log.h writes perf-*.csv under files/perf (tools/perf/pull_perf_logs.sh).
private static final String DEFAULT_ARGS =
"--live-server 192.168.21.203:11000:13000 --login-screen --perf-log --show-fps";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
applyImmersiveMode();
// --refresh-rate N pins the display mode for a test run; otherwise src/host switches it with
// the frame-rate setting (setDisplayRefreshRate).
final float pinned = requestedRefreshRate(launchArgs());
if (pinned > 0)
applyRefreshRate(pinned);
}
private String launchArgs() {
Intent intent = getIntent();
String args = intent != null ? intent.getStringExtra("args") : null;
return args == null || args.trim().isEmpty() ? DEFAULT_ARGS : args;
}
// --refresh-rate N (0 = system default); -1 when absent.
private static float requestedRefreshRate(String args) {
final String[] tokens = args.trim().split("\\s+");
for (int i = 0; i + 1 < tokens.length; ++i) {
if (tokens[i].equals("--refresh-rate")) {
try {
return Float.parseFloat(tokens[i + 1]);
} catch (NumberFormatException e) {
return -1;
}
}
}
return -1;
}
/**
* Asks for the display mode with the current resolution and the refresh rate closest to `wanted`
* (the highest one when wanted < 0). The vendor can still override it; currentRefreshRate() reports
* what the display actually runs at.
*/
private int applyRefreshRate(float wanted) {
if (wanted == 0 || Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
return 0;
final Display display = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ? getDisplay()
: getWindowManager().getDefaultDisplay();
if (display == null)
return 0;
final Display.Mode current = display.getMode();
Display.Mode best = null;
for (Display.Mode mode : display.getSupportedModes()) {
if (mode.getPhysicalWidth() != current.getPhysicalWidth() ||
mode.getPhysicalHeight() != current.getPhysicalHeight())
continue;
if (best == null) {
best = mode;
continue;
}
final float rate = mode.getRefreshRate();
final boolean better = wanted < 0 ? rate > best.getRefreshRate()
: Math.abs(rate - wanted) < Math.abs(best.getRefreshRate() - wanted);
if (better)
best = mode;
}
if (best == null)
return 0;
final WindowManager.LayoutParams params = getWindow().getAttributes();
params.preferredDisplayModeId = best.getModeId();
getWindow().setAttributes(params);
Log.i("Metin2Args", "display mode " + best.getModeId() + " " + best.getPhysicalWidth() + "x" +
best.getPhysicalHeight() + " @ " + best.getRefreshRate() + " Hz");
return Math.round(best.getRefreshRate());
}
// The app-vsync rate, measured with a Choreographer callback. It differs from the panel's refresh rate
// when the system renders the app at a divisor of it (ColorOS drops a non-game app to 60 fps after
// 2.5 s without a touch while the panel stays at 120 Hz). Started by the first currentRenderRate().
private volatile float renderRate = -1f;
private boolean renderRateStarted = false;
// Called from native code (android_perf.h), off the UI thread.
public float currentRenderRate() {
synchronized (this) {
if (!renderRateStarted) {
renderRateStarted = true;
runOnUiThread(() -> Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
long windowStart = 0;
int frames = 0;
@Override
public void doFrame(long frameTimeNanos) {
if (windowStart == 0) {
windowStart = frameTimeNanos;
} else if (++frames >= 30) {
renderRate = frames * 1e9f / (frameTimeNanos - windowStart);
windowStart = frameTimeNanos;
frames = 0;
}
Choreographer.getInstance().postFrameCallback(this);
}
}));
}
}
return renderRate;
}
// Called from native code (android_perf.h): battery temperature in C, -1 if unknown.
public float batteryTemperature() {
final Intent battery = registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
final int tenths = battery != null ? battery.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -10) : -10;
return tenths / 10f;
}
// Called from native code (android_perf.h): the display mode whose refresh rate is closest to `hz`
// (the highest one when hz < 0), for the frame-rate setting. The panel then need not scan out at 120 Hz
// for a 60 fps game.
public void setDisplayRefreshRate(float hz) {
runOnUiThread(() -> applyRefreshRate(hz));
}
// Called from native code (android_perf.h): the highest refresh rate of the current resolution.
public float maxRefreshRate() {
final Display display = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ? getDisplay()
: getWindowManager().getDefaultDisplay();
if (display == null)
return -1f;
float best = display.getRefreshRate();
final Display.Mode current = display.getMode();
for (Display.Mode mode : display.getSupportedModes())
if (mode.getPhysicalWidth() == current.getPhysicalWidth() &&
mode.getPhysicalHeight() == current.getPhysicalHeight())
best = Math.max(best, mode.getRefreshRate());
return best;
}
// Called from native code (android_perf.h): BATTERY_PROPERTY_CURRENT_NOW as reported -- uA per the
// docs, mA on some vendors; the sign convention varies too (android_perf::battery_power sorts both
// out). 0 if unknown.
public float batteryCurrentRaw() {
final BatteryManager manager = (BatteryManager) getSystemService(BATTERY_SERVICE);
if (manager == null)
return 0f;
final int raw = manager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW);
return raw == Integer.MIN_VALUE ? 0f : raw;
}
// Called from native code (android_perf.h): battery voltage in mV, -1 if unknown.
public float batteryVoltage() {
final Intent battery = registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
return battery != null ? battery.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1) : -1f;
}
// Called from native code (android_perf.h): 1 while a charger is connected (the current then no
// longer measures the game's draw), 0 otherwise.
public float batteryPlugged() {
final Intent battery = registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
return battery != null && battery.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0 ? 1f : 0f;
}
// Called from native code (android_perf.h): the rate the display runs at now.
public float currentRefreshRate() {
final Display display = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ? getDisplay()
: getWindowManager().getDefaultDisplay();
return display != null ? display.getRefreshRate() : -1f;
}
@Override
protected void onResume() {
super.onResume();
applyImmersiveMode();
}
// Called from native code (touch_controller long press).
public void performHaptic() {
runOnUiThread(() -> getWindow().getDecorView().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS));
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus)
applyImmersiveMode();
}
private void applyImmersiveMode() {
final View decor = getWindow().getDecorView();
decor.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
getWindow().setDecorFitsSystemWindows(false);
final WindowInsetsController controller = getWindow().getInsetsController();
if (controller != null) {
controller.hide(WindowInsets.Type.systemBars());
controller.setSystemBarsBehavior(
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
}
}
}
@Override
protected String[] getLibraries() {
return new String[] { "SDL3", "main" };
}
@Override
protected String[] getArguments() {
Intent intent = getIntent();
String args = intent != null ? intent.getStringExtra("args") : null;
Log.i("Metin2Args", "native args: " + args);
if (args == null || args.trim().isEmpty())
args = DEFAULT_ARGS;
if (!args.contains("--safe-inset-px"))
args = args.trim() + " --safe-inset-px " + safeInsetPx();
// Test phase: FPS readout in the top-left corner. Drop this line for release builds.
if (!args.contains("--show-fps"))
args = args.trim() + " --show-fps";
return args.trim().split("\\s+");
}
/**
* Pixels the game UI keeps clear at the left and right edges in landscape: the largest rounded
* screen corner (a window hugging a corner is clipped until it is a full radius in) and any
* display cutout. The 3D view still fills the screen.
*/
private int safeInsetPx() {
int inset = 0;
final Display display = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ? getDisplay()
: getWindowManager().getDefaultDisplay();
if (display == null)
return 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
final int[] positions = { RoundedCorner.POSITION_TOP_LEFT, RoundedCorner.POSITION_TOP_RIGHT,
RoundedCorner.POSITION_BOTTOM_LEFT, RoundedCorner.POSITION_BOTTOM_RIGHT };
for (int position : positions) {
final RoundedCorner corner = display.getRoundedCorner(position);
if (corner != null)
inset = Math.max(inset, corner.getRadius());
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// The activity may not be landscape yet: any side's cutout can end up left or right.
final DisplayCutout cutout = display.getCutout();
if (cutout != null)
inset = Math.max(inset, Math.max(Math.max(cutout.getSafeInsetLeft(), cutout.getSafeInsetRight()),
Math.max(cutout.getSafeInsetTop(), cutout.getSafeInsetBottom())));
}
Log.i("Metin2Args", "safe inset px: " + inset);
return inset;
}
@Override
public void setOrientationBis(int w, int h, boolean resizable, String hint) {
// Enforce landscape-only on mobile devices (sensorLandscape allows flipping 180 deg but never portrait).
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
}
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Metin2</string>
</resources>