package com.antest1.kcanotify.h5; import android.app.AlertDialog; import android.app.Service; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.database.ContentObserver; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Vibrator; import android.provider.Settings; import android.support.annotation.Nullable; import android.util.DisplayMetrics; import android.util.Log; import android.util.LruCache; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; import com.bilibili.boxing.Boxing; import com.bilibili.boxing.BoxingMediaLoader; import com.bilibili.boxing.model.config.BoxingConfig; import com.bilibili.boxing.model.entity.BaseMedia; import com.google.webp.libwebp; import org.jetbrains.annotations.NotNull; import org.json.JSONArray; import org.json.JSONObject; import org.xwalk.core.XWalkActivity; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; import andhook.lib.AndHook; import customview.ConfirmDialog; import master.flame.danmaku.controller.DrawHandler; import master.flame.danmaku.danmaku.model.BaseDanmaku; import master.flame.danmaku.danmaku.model.DanmakuTimer; import master.flame.danmaku.danmaku.model.IDanmakus; import master.flame.danmaku.danmaku.model.IDisplayer; import master.flame.danmaku.danmaku.model.android.DanmakuContext; import master.flame.danmaku.danmaku.model.android.Danmakus; import master.flame.danmaku.danmaku.parser.BaseDanmakuParser; import master.flame.danmaku.ui.widget.DanmakuView; import okhttp3.Authenticator; import okhttp3.Credentials; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; import okhttp3.Route; public abstract class GameBaseActivity extends XWalkActivity { private final static String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36"; private static final String[] SERVER_IP = new String[]{"203.104.209.71", "203.104.209.87", "125.6.184.215", "203.104.209.183", "203.104.209.150", "203.104.209.134", "203.104.209.167", "203.104.248.135", "125.6.189.7", "125.6.189.39", "125.6.189.71", "125.6.189.103", "125.6.189.135", "125.6.189.167", "125.6.189.215", "125.6.189.247", "203.104.209.23", "203.104.209.39", "203.104.209.55", "203.104.209.102"}; private GameViewBroadcastReceiver gameViewBroadcastReceiver = new GameViewBroadcastReceiver(); private GameBaseActivity.RotationObserver rotationObserver; public SharedPreferences prefs = null; private boolean chatDanmuku; private HashMap<String, String> serverMap; private boolean battleResultVibrate; private boolean changeTouchEventPrefs = false; protected GameView gameView; protected ProgressBar progressBar1; private TextView fpsCounter; private TextView subtitleTextView; private StrokeTextView subtitleStrokeTextView; private ImageView chatImageView; private ImageView chatNewMsgImageView; private String imageSize; private boolean showDanmaku; private DanmakuView danmakuView; private DanmakuContext danmakuContext; private BaseDanmakuParser parser = new BaseDanmakuParser() { @Override protected IDanmakus parse() { return new Danmakus(); } }; private ChatDialogUtils dialogUtils; private OkHttpClient client = null; private JSONObject jsonObj = null; private File cacheJsonFile = null; protected boolean clearCookie; protected boolean voicePlay; protected boolean changeCookie; public HashMap<String, String> voiceCookieMap; public HashMap<String, String> dmmCookieMap; boolean changeTouchEvent = false; private boolean subTitleEnable; private Handler subtitleHandler; private Runnable dismissSubTitle; private boolean chatService; private String nickName; private boolean modEnable; private ExecutorService mExecutor; private boolean proxyEnable; private String proxyIP; private boolean pngToWebp; private int quality; LruCache<String, byte[]> mLruCache; public native String stringFromJNI(); public native int nativeInit(int version, boolean proxyEnable, String ip); @Override protected void onXWalkReady() { } public void setProgressBarProgress(int newProgress) { ProgressBar progressBar = (ProgressBar)findViewById(R.id.progressBar1); if (newProgress == 100) { progressBar.setVisibility(View.GONE); } else { progressBar.setVisibility(View.VISIBLE); progressBar.setProgress(newProgress); } } abstract int getLayoutResID(); @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // For some system changes not captured by android:configChanges, // an instant is re-created but the system is still keeping/using resources of the old instant for a while // In this case, we do not finish the old view to avoid a crash if(KcaApplication.gameActivity != null && KcaApplication.gameActivity.getClass() != this.getClass()){ // Remove last game activity only after changing important settings (DMM/OOI, Crosswalk/WebView) KcaApplication.gameActivity.finish(); } int cacheSize = 150 * 1024 * 1024; // 150MiB mLruCache = new LruCache<String, byte[]>(cacheSize) { @Override protected int sizeOf(String key, byte[] value) { return value.length; } }; KcaApplication.gameActivity = this; rotationObserver = new GameBaseActivity.RotationObserver(new Handler()); prefs = getSharedPreferences("pref", Context.MODE_PRIVATE); mExecutor = Executors.newScheduledThreadPool(5); subTitleEnable = prefs.getBoolean("voice_sub_title", false); if(subTitleEnable) { //语言字幕初始化 new Thread(new Runnable() { @Override public void run() { SubTitleUtils.initVoiceMap(); SubTitleUtils.initSubTitle(); } }).start(); } chatService = prefs.getBoolean("chat_service", false); chatDanmuku = prefs.getBoolean("chat_danmuku", false); if(chatService){ serverMap = SubTitleUtils.initServiceHost(); } battleResultVibrate = prefs.getBoolean("battle_result_vibrate", true); pngToWebp = prefs.getBoolean("png_to_webp", true); quality = Integer.parseInt(prefs.getString("png_to_webp_quality", "80")); if(pngToWebp){ System.loadLibrary("webp"); } //proxy init proxyEnable = prefs.getBoolean("host_proxy_enable", false); proxyIP = prefs.getString("host_proxy_address", "167.179.91.86"); if(proxyEnable) { AndHook.ensureNativeLibraryLoaded(null); System.loadLibrary("xhook"); System.loadLibrary("native-lib"); Log.e("KCAV", "onCreate: " + "native hook result-->" + (nativeInit(Build.VERSION.SDK_INT, proxyEnable, proxyIP) == 0)); } setContentView(getLayoutResID()); boolean hardSpeed = prefs.getBoolean("hardware_accelerated", true); if(hardSpeed) { getWindow().setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); } boolean keepScreenOn = prefs.getBoolean("keep_screen_on", false); if(keepScreenOn){ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } changeTouchEventPrefs = prefs.getBoolean("change_touch_event", true); getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); final View decorView = GameBaseActivity.this.getWindow().getDecorView(); decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() { @Override public void onSystemUiVisibilityChange(int visibility) { if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); } } }); imageSize = getIntent().getStringExtra("imageSize"); if(imageSize == null){ imageSize = "100"; } gameView = (GameView) findViewById(R.id.webView1); gameView.assignActivity(this); fpsCounter = (TextView) findViewById(R.id.fps_counter); subtitleTextView = findViewById(R.id.subtitle_textview); subtitleStrokeTextView = findViewById(R.id.subtitle_textview_stroke); chatImageView = findViewById(R.id.chat_image_view); chatNewMsgImageView = findViewById(R.id.chat_new_msg_image_view); chatImageView.setImageAlpha(50); initDanMuKu(); initCache(); resetWebView(false); OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); if(prefs.getBoolean("ooi_host_auth", false)) { clientBuilder.authenticator(new Authenticator() { @Override public Request authenticate(Route route, Response response) throws IOException { String credential = Credentials.basic(prefs.getString("ooi_host_auth_name", ""), prefs.getString("ooi_host_auth_pwd", "")); return response.request().newBuilder().header("Authorization", credential).build(); } }); } client = clientBuilder.build(); clearCookie = prefs.getBoolean("clear_cookie_start", false); voicePlay = prefs.getBoolean("voice_play", false); changeCookie = prefs.getBoolean("change_cookie_start", false); initCookieData(); //增加魔改开关 modEnable = prefs.getBoolean("mod_enable", true); subtitleHandler = new Handler(); dismissSubTitle = new Runnable() { @Override public void run() { runOnUiThread(new Runnable() { @Override public void run() { subtitleTextView.setText(""); subtitleStrokeTextView.setText(""); } }); } }; initChat(); registerReceiver(gameViewBroadcastReceiver, new IntentFilter("com.antest1.kcanotify.h5.webview_reload")); registerReceiver(gameViewBroadcastReceiver, new IntentFilter("com.antest1.kcanotify.h5.webview_finish")); } private void updateLanguage(Context context, Locale locale) { Resources resources = context.getResources(); Configuration config = resources.getConfiguration(); DisplayMetrics dm = resources.getDisplayMetrics(); config.setLocale(locale); context.createConfigurationContext(config); resources.updateConfiguration(config, dm); } @Override protected void onResume() { rotationObserver.startObserver(); updateLanguage(this, Locale.JAPAN); setScreenOrientation(); if (danmakuView != null && danmakuView.isPrepared() && danmakuView.isPaused()) { danmakuView.resume(); } super.onResume(); } @Override protected void onPause() { mLruCache.evictAll(); rotationObserver.stopObserver(); if (danmakuView != null && danmakuView.isPrepared()) { danmakuView.pause(); } super.onPause(); } @Override protected void onStop() { if (!prefs.getBoolean("background_play", true)){ gameView.pauseGame(); } super.onStop(); } @Override protected void onStart() { if (!prefs.getBoolean("background_play", true)) { gameView.resumeGame(); } super.onStart(); } //点击返回上一页面而不是退出浏览器 @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { startActivity(new Intent(this, MainActivity.class)); return true; } return super.onKeyDown(keyCode, event); } //销毁Webview @Override protected void onDestroy() { if (gameView != null) { gameView.destroy(); gameView = null; } unregisterReceiver(gameViewBroadcastReceiver); showDanmaku = false; if (danmakuView != null) { danmakuView.release(); danmakuView = null; } super.onDestroy(); } /** * danmuku init */ public void initDanMuKu(){ //弹幕 // 设置最大显示行数 HashMap<Integer, Integer> maxLinesPair = new HashMap<Integer, Integer>(); maxLinesPair.put(BaseDanmaku.TYPE_SCROLL_RL, 10); // 滚动弹幕最大显示10行 // 设置是否禁止重叠 HashMap<Integer, Boolean> overlappingEnablePair = new HashMap<Integer, Boolean>(); overlappingEnablePair.put(BaseDanmaku.TYPE_SCROLL_RL, true); overlappingEnablePair.put(BaseDanmaku.TYPE_FIX_TOP, true); danmakuContext = DanmakuContext.create(); danmakuContext.setDanmakuStyle(IDisplayer.DANMAKU_STYLE_STROKEN, 5).setDuplicateMergingEnabled(false) .setMaximumLines(maxLinesPair) .preventOverlapping(overlappingEnablePair).setDanmakuMargin(20); danmakuView = findViewById(R.id.danmaku_view); danmakuView.enableDanmakuDrawingCache(true); danmakuView.setCallback(new DrawHandler.Callback() { @Override public void prepared() { showDanmaku = true; danmakuView.start(); } @Override public void updateTimer(DanmakuTimer timer) { } @Override public void danmakuShown(BaseDanmaku danmaku) { } @Override public void drawingFinished() { } }); danmakuView.prepare(parser, danmakuContext); } /** * cache init */ public void initCache(){ try { String cacheJsonPathStr = Environment.getExternalStorageDirectory() + "/KanCollCache/cache.json"; String nomedia = Environment.getExternalStorageDirectory() + "/KanCollCache/.nomedia"; cacheJsonFile = new File(cacheJsonPathStr); if (!cacheJsonFile.getParentFile().exists()) { cacheJsonFile.getParentFile().mkdirs(); } if (!cacheJsonFile.exists()) { cacheJsonFile.createNewFile(); } File nomediaFile = new File(nomedia); if(!nomediaFile.exists()){ nomediaFile.createNewFile(); } byte[] bytes = readFileToBytes(cacheJsonFile); String cacheJson = new String(bytes, "UTF-8"); if (cacheJson != null && !cacheJson.equals("")) { jsonObj = new JSONObject(cacheJson); } else { jsonObj = new JSONObject(); } } catch (Exception e){ Log.e("KCVA", e.getMessage()); } } /** * cookie init */ private void initCookieData(){ // TODO: depend to the connection type voiceCookieMap = new HashMap<>(); int vol = voicePlay ? 50 : 0; // Add volume cookies for DMM for (String serverIp : SERVER_IP) { voiceCookieMap.put("vol_bgm=" + vol + "; domain=" + serverIp + "; path=/kcs2", "http://" + serverIp); voiceCookieMap.put("vol_se=" + vol + "; domain=" + serverIp + "; path=/kcs2", "http://" + serverIp); voiceCookieMap.put("vol_voice=" + vol + "; domain=" + serverIp + "; path=/kcs2", "http://" + serverIp); } // Add volume cookies for OOI String hostName = prefs.getString("ooi_host_name", "ooi.moe"); if(hostName.equals("")) hostName = "ooi.moe"; String fullHostName = hostName; if (!fullHostName.startsWith("http")) { // XWalkCookieManager requires full URL instead of just domain fullHostName = "https://" + fullHostName; } voiceCookieMap.put("vol_bgm=" + vol + "; domain=" + hostName + "; path=/kcs2", fullHostName); voiceCookieMap.put("vol_se=" + vol + "; domain=" + hostName + "; path=/kcs2", fullHostName); voiceCookieMap.put("vol_voice=" + vol + "; domain=" + hostName + "; path=/kcs2", fullHostName); dmmCookieMap = new HashMap<>(); dmmCookieMap.put("cklg=welcome;expires=Sun, 09 Feb 2029 09:00:09 GMT;domain=.dmm.com;path=/", "http://www.dmm.com"); dmmCookieMap.put("cklg=welcome;expires=Sun, 09 Feb 2029 09:00:09 GMT;domain=.dmm.com;path=/netgame/", "http://www.dmm.com"); dmmCookieMap.put("cklg=welcome;expires=Sun, 09 Feb 2029 09:00:09 GMT;domain=.dmm.com;path=/netgame_s/", "http://www.dmm.com"); dmmCookieMap.put("ckcy=1;expires=Sun, 09 Feb 2029 09:00:09 GMT;domain=.dmm.com;path=/", "http://www.dmm.com"); dmmCookieMap.put("ckcy=1;expires=Sun, 09 Feb 2029 09:00:09 GMT;domain=.dmm.com;path=/netgame/", "http://www.dmm.com"); dmmCookieMap.put("ckcy=1;expires=Sun, 09 Feb 2029 09:00:09 GMT;domain=.dmm.com;path=/netgame_s/", "http://www.dmm.com"); dmmCookieMap.put("ckcy=1;expires=Sun, 09 Feb 2029 09:00:09 GMT;domain=osapi.dmm.com;path=/", "http://www.dmm.com"); dmmCookieMap.put("ckcy=1;expires=Sun, 09 Feb 2029 09:00:09 GMT;domain=203.104.209.7;path=/", "http://www.dmm.com"); dmmCookieMap.put("ckcy=1;expires=Sun, 09 Feb 2029 09:00:09 GMT;domain=www.dmm.com;path=/netgame/", "http://www.dmm.com"); dmmCookieMap.put("ckcy=1;expires=Sun, 09 Feb 2029 09:00:09 GMT;domain=log-netgame.dmm.com;path=/", "http://www.dmm.com"); } public void initChat(){ if(chatService) { BoxingMediaLoader.getInstance().init(new BoxingPicassoLoader()); BoxingConfig singleImgConfig = new BoxingConfig(BoxingConfig.Mode.SINGLE_IMG).withMediaPlaceHolderRes(R.drawable.ic_boxing_default_image); dialogUtils = new ChatDialogUtils(this, new ChatListener() { @Override public void onMessage(ChatMsgObject msgObject) { if(msgObject.getMsgType() == ChatMsgObject.MsgTypeCont.MSG_TEXT) { if(chatDanmuku) { addDanmaku(msgObject.getMsg(), false); } } if(!dialogUtils.isShow()) { chatNewMsgImageView.setVisibility(View.VISIBLE); chatNewMsgImageView.setImageAlpha(255); chatImageView.setImageAlpha(255); new Handler().postDelayed(new Runnable() { @Override public void run() { runOnUiThread(new Runnable() { @Override public void run() { chatImageView.setImageAlpha(50); chatNewMsgImageView.setImageAlpha(50); } }); } }, 5000); } } @Override public void onSelectMsg(){ Boxing.of(singleImgConfig).withIntent(GameBaseActivity.this, LandScapeBoxingActivity.class).start(GameBaseActivity.this, 2); } }, imageSize); chatImageView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { chatNewMsgImageView.setVisibility(View.GONE); dialogUtils.showLeftChat(nickName); } }); } else { chatImageView.setVisibility(View.GONE); } } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == 2) { if(data != null){ List<BaseMedia> medias = Boxing.getResult(data); if(medias.size() > 0) { ConfirmDialog confirmDialog = new ConfirmDialog(this, new ConfirmDialog.Callback() { @Override public void callback(int position) { if(position == 1) { dialogUtils.sendImage(medias.get(0).getPath()); } } }); confirmDialog.setContent("请确认是否发送该图片?"); confirmDialog.show(); } } } super.onActivityReenter(resultCode, data); } @Override public void onMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration newConfig) { super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig); if(isInMultiWindowMode) { RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) chatImageView.getLayoutParams(); lp.setMargins(0, dip2px(20), 0, 0); chatImageView.setLayoutParams(lp); RelativeLayout.LayoutParams lp1 = (RelativeLayout.LayoutParams) chatNewMsgImageView.getLayoutParams(); lp1.setMargins(dip2px(10), dip2px(15), 0, 0); chatNewMsgImageView.setLayoutParams(lp1); } else { RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) chatImageView.getLayoutParams(); lp.setMargins(0, 0, 0, 0); chatImageView.setLayoutParams(lp); RelativeLayout.LayoutParams lp1 = (RelativeLayout.LayoutParams) chatNewMsgImageView.getLayoutParams(); lp1.setMargins(dip2px(20), dip2px(-5), 0, 0); chatNewMsgImageView.setLayoutParams(lp1); } resetWebView(true); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); } //观察屏幕旋转设置变化,类似于注册动态广播监听变化机制 private class RotationObserver extends ContentObserver { ContentResolver mResolver; public RotationObserver(Handler handler) { super(handler); mResolver = getContentResolver(); // TODO Auto-generated constructor stub } //屏幕旋转设置改变时调用 @Override public void onChange(boolean selfChange) { // TODO Auto-generated method stub super.onChange(selfChange); //更新按钮状态 setScreenOrientation(); } public void startObserver() { mResolver.registerContentObserver(Settings.System .getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, this); } public void stopObserver() { mResolver.unregisterContentObserver(this); } } private class GameViewBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals("com.antest1.kcanotify.h5.webview_reload")) gameView.reloadGame(); if (intent.getAction().equals("com.antest1.kcanotify.h5.webview_finish")) GameBaseActivity.this.finish(); } } public Object[] interceptRequest(Uri uri, String requestMethod, Map<String, String> requestHeader){ if ("POST".equals(requestMethod)){ return null; } String path = uri.getPath(); final long startTime = System.nanoTime(); Log.d("KCVA", "requesting uri:" + uri); if(battleResultVibrate && path != null && (path.contains("battle_result_main.png"))){ Vibrator vib = (Vibrator) GameBaseActivity.this.getSystemService(Service.VIBRATOR_SERVICE); vib.vibrate(200); } // Handle pixi.min.js. Need to patch but not cache if(path != null && path.contains("pixi.")) { ResponseBody serverResponse = requestServer(uri.toString(), requestHeader); if (serverResponse != null) { try{ String patchedPixi = injectPixi(serverResponse.string()); byte[] bytes = patchedPixi.getBytes(); return createResponseObject(path, String.valueOf(bytes.length), new ByteArrayInputStream(bytes)); } catch (Exception ex) { return null; } } return null; } if(uri.toString().startsWith("http://203.104.209.7/")){ try { String url = uri.toString(); //Reverse Proxy if(proxyEnable){ url = url.replace("203.104.209.7", proxyIP); } ResponseBody serverResponse = requestServer(url, requestHeader); byte[] respByte = null; if(uri.toString().contains("/kcscontents/css/style.css")){ String newRespStr = injectInspection(serverResponse.string()); respByte = newRespStr.getBytes(); } else { respByte = serverResponse.bytes(); } return createResponseObject(path, String.valueOf(respByte.length), new ByteArrayInputStream(respByte)); } catch (Exception e){ e.printStackTrace(); } } if(uri.toString().startsWith("https://cdnjs.cloudflare.com") && prefs.getBoolean("change_cdn", true)){ try { String url = uri.toString(); url = url.replace("cdnjs.cloudflare.com/ajax/libs", "cdn.bootcss.com"); ResponseBody serverResponse = requestServer(url, requestHeader); byte[] respByte = serverResponse.bytes(); return createResponseObject(path, String.valueOf(respByte.length), new ByteArrayInputStream(respByte)); }catch (Exception e){ e.printStackTrace(); } } if(uri.toString().startsWith("https://code.createjs.com/tweenjs-0.6.2.min.js") && prefs.getBoolean("change_cdn", true)){ try { String url = uri.toString(); url = url.replace("https://code.createjs.com/tweenjs-0.6.2.min.js", "https://cdn.bootcss.com/tweenjs/0.6.2/tweenjs.min.js"); ResponseBody serverResponse = requestServer(url, requestHeader); byte[] respByte = serverResponse.bytes(); return createResponseObject(path, String.valueOf(respByte.length), new ByteArrayInputStream(respByte)); }catch (Exception e){ e.printStackTrace(); } } if ("GET".equals(requestMethod) && path != null && (path.startsWith("/kcs2/") || path.startsWith("/kcs/"))) { if(path.contains("version.json") || path.contains("index.php")){ return null; } try { //获取字幕 String pattern = "/kcs/sound/(.*?)/(.*?).mp3"; boolean isMatch = Pattern.matches(pattern, path); if(isMatch && subTitleEnable){ String subTitle = SubTitleUtils.getSubTitle(path); subtitleHandler.removeCallbacks(dismissSubTitle); runOnUiThread(new Runnable() { @Override public void run() { subtitleTextView.setText(subTitle); subtitleStrokeTextView.setText(subTitle); } }); subtitleHandler.postDelayed(dismissSubTitle, 15000); } String version = "0"; if (uri.getQueryParameter("version") != null && !uri.getQueryParameter("version").equals("")) { version = uri.getQueryParameter("version"); } String currentVersion = null; if (jsonObj.has(path)) { currentVersion = jsonObj.getString(path); } //add read mod file if(path.contains(".png") && modEnable && (prefs.getBoolean("mod_ui_enable", true) || path.contains("ship/"))) { String modFilePath = Environment.getExternalStorageDirectory() + "/KanCollCache/mod" + path; File modFile = new File(modFilePath); if (modFile.exists()) { //从魔改目录直接返回客户端 long length = modFile.length(); InputStream inputStream = new FileInputStream(modFile); Object[] response = createResponseObject(path, String.valueOf(length), inputStream); return response; } } String filePath = Environment.getExternalStorageDirectory() + "/KanCollCache" + path; File tmp = new File(filePath); Log.d("KCVA", "Local cache version:" + currentVersion + " ;server version: " + version + "us"); //版本不一致则删除老的cache /*if (!version.equals(currentVersion)) { tmp.delete(); }*/ if (version.equals(currentVersion) && tmp.exists()) { //从缓存直接返回客户端,不请求服务器 long length = 0; InputStream inputStream; byte[] fileContent = mLruCache.get(path);//读取缓存 if(fileContent == null){ if(path.endsWith("png") && pngToWebp) { String webpUrlPath = path.replace("png", "webp"); String webpPath = Environment.getExternalStorageDirectory() + "/KanCollCache/webp/" + quality + "/" + webpUrlPath; File webpTmp = new File(webpPath); if(webpTmp.exists()) { fileContent = readFileToBytes(new File(webpPath)); } else { fileContent = readFileToBytes(tmp); final byte[] saveRespByte = fileContent; mExecutor.execute(() -> { try { byte[] webpData = bitmapToWebp(saveRespByte); saveFile(webpPath, webpData); mLruCache.put(path, webpData); } catch (Exception e) { e.printStackTrace(); } }); } } else { fileContent = readFileToBytes(tmp); } if(path.contains("/kcs2/js/main.js")) { // Inject code after caching, so it wont require re-downloading main.js after switching touch mode if (prefs.getBoolean("show_fps_counter", false)) { fileContent = injectFpsUpdater(fileContent); } fileContent = injectTickerTimingMode(fileContent); if (changeTouchEventPrefs && prefs.getBoolean("change_webview", false)) { // TODO: it should be handled by gameView fileContent = injectTouchLogic(fileContent); } fileContent = injectHookAjax(fileContent); } mLruCache.put(path, fileContent); } length = fileContent.length; inputStream = new ByteArrayInputStream(fileContent); Object[] response = createResponseObject(path, String.valueOf(length), inputStream); final long duration = System.nanoTime() - startTime; Log.d("KCVA", "Local cache uri:" + uri + " after " + duration/1000 + "us"); return response; } else { /*boolean needModify = path.contains("/kcs2/js/main.js") || path.contains("/gadget_html5/js/kcs_inspection.js") || path.contains("ooi_moe_t.png"); if (needModify) {*/ // Download the modify the result immediately ResponseBody serverResponse = requestServer(uri.toString(), requestHeader); if(serverResponse != null){ byte[] respByte = null; if(path.contains("/kcs2/js/main.js")){ String newRespStr = serverResponse.string() + "!function(t){function r(i){if(n[i])return n[i].exports;var e=n[i]={exports:{},id:i,loaded:!1};return t[i].call(e.exports,e,e.exports,r),e.loaded=!0,e.exports}var n={};return r.m=t,r.c=n,r.p=\"\",r(0)}([function(t,r,n){n(1)(window)},function(t,r){t.exports=function(t){t.hookAjax=function(t){function r(r){return function(){var n=this.hasOwnProperty(r+\"_\")?this[r+\"_\"]:this.xhr[r],i=(t[r]||{}).getter;return i&&i(n,this)||n}}function n(r){return function(n){var i=this.xhr,e=this,o=t[r];if(\"function\"==typeof o)i[r]=function(){t[r](e)||n.apply(i,arguments)};else{var h=(o||{}).setter;n=h&&h(n,e)||n;try{i[r]=n}catch(t){this[r+\"_\"]=n}}}}function i(r){return function(){var n=[].slice.call(arguments);if(!t[r]||!t[r].call(this,n,this.xhr))return this.xhr[r].apply(this.xhr,n)}}return window._ahrealxhr=window._ahrealxhr||XMLHttpRequest,XMLHttpRequest=function(){this.xhr=new window._ahrealxhr;for(var t in this.xhr){var e=\"\";try{e=typeof this.xhr[t]}catch(t){}\"function\"===e?this[t]=i(t):Object.defineProperty(this,t,{get:r(t),set:n(t)})}},window._ahrealxhr},t.unHookAjax=function(){window._ahrealxhr&&(XMLHttpRequest=window._ahrealxhr),window._ahrealxhr=void 0},t.default=t}}]);"; respByte = newRespStr.getBytes(); } else { respByte = serverResponse.bytes(); } //save file final String saveVersion = version; final byte[] saveRespByte = respByte; mExecutor.execute(() -> { try { saveFile(Environment.getExternalStorageDirectory() + "/KanCollCache" + path, saveRespByte); if(path.endsWith("png") && pngToWebp){ byte[] webpData = bitmapToWebp(saveRespByte); String webpUrlPath = path.replace("png", "webp"); String webpPath = Environment.getExternalStorageDirectory() + "/KanCollCache/webp/" + quality + "/" + webpUrlPath; saveFile(webpPath, webpData); mLruCache.put(path, webpData); } synchronized (jsonObj) { jsonObj.put(path, saveVersion); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(cacheJsonFile, false), "UTF-8")); writer.write(jsonObj.toString()); writer.flush(); writer.close(); } } catch (Exception e){ e.printStackTrace(); } }); // Inject code after caching, so it wont require re-downloading main.js after switching touch mode if(path.contains("/kcs2/js/main.js")) { if (prefs.getBoolean("show_fps_counter", false)) { respByte = injectFpsUpdater(respByte); } respByte = injectTickerTimingMode(respByte); if (changeTouchEventPrefs && prefs.getBoolean("change_webview", false)) { respByte = injectTouchLogic(respByte); } respByte = injectHookAjax(respByte); } mLruCache.put(path, respByte); return createResponseObject(path, String.valueOf(respByte.length), new ByteArrayInputStream(respByte)); } else { return null; } /*} else { // No need to modify the resource // Do it in an async way Object[] ob = downloadAndCacheAsync(path, uri, requestHeader, version); Log.d("KCVA", "started asyncDL:" + uri); return ob; }*/ } } catch (Exception e) { e.printStackTrace(); return null;//异常情况,直接访问网络资源 } } return null; } public String buildModJson() { String result = null; try { String pathDirStr = "/KanCollCache" + File.separator + "mod"; File modDirfile = new File(Environment.getExternalStorageDirectory(), pathDirStr); if (!modDirfile.exists()) { return null; } JSONArray jsonModShipArr = new JSONArray(); File[] subFiles = modDirfile.listFiles(); for(File f : subFiles){ if(f.getName().endsWith("json")){ jsonModShipArr.put(new String(readFileToBytes(f))); } } /*JSONObject shipObject = new JSONObject(oriData); JSONArray jsonShipArr = shipObject.getJSONObject("api_data").getJSONArray("api_mst_shipgraph"); for (int i = 0; i < jsonModShipArr.length(); i++) { JSONObject modShipJsonObj = jsonModShipArr.getJSONObject(i); for (int j = 0; j < jsonShipArr.length(); j++) { JSONObject shipJsonObj = jsonShipArr.getJSONObject(j); if (shipJsonObj.getInt("api_id") == modShipJsonObj.getInt("api_id")) { jsonShipArr.put(j, modShipJsonObj); break; } } }*/ result = jsonModShipArr.toString(); } catch (Exception e){ e.printStackTrace(); } return result; } private Object[] downloadAndCacheAsync(String path, Uri uri, Map<String, String> headerHeader, String version) { // In old chromium, shouldInterceptRequest() is called synchronously in the JS main thread // But after that the downloading of the content is async (not blocking UI) // Therefore we cannot create the okhttp request before returning a response header // which blocks the main JS thread and causes all animation freezing. // Instead, we send request afterward and block the read() operation only // Blocking read() does not affect the main JS thread, so there is no more lag final CountDownLatch haveData = new CountDownLatch(1); final AtomicReference<InputStream> inputStreamRef = new AtomicReference<>(); new Thread() { @Override public void run() { Request.Builder builder = new Request.Builder().url(uri.toString()); for(Map.Entry<String, String> keySet : headerHeader.entrySet()){ builder.addHeader(keySet.getKey(), keySet.getValue()); } Request serverRequest = builder.build(); try { Response response = client.newCall(serverRequest).execute(); if(response != null && response.isSuccessful()){ ResponseBody body = response.body(); if (body != null) { inputStreamRef.set(body.byteStream()); haveData.countDown(); } } else { Log.d("KCVA", "Download failed:" + uri); // Return a failing stream immediately // So that the thread reading the stream doesn't need to wait 30sec time out inputStreamRef.set(new InputStream() { @Override public int read() throws IOException { throw new IOException("Error :"); } }); haveData.countDown(); } } catch (Exception e) { Log.d("KCVA", "Download error!:" + uri); // Return a failing stream immediately // So that the thread reading the stream doesn't need to wait 30sec time out inputStreamRef.set(new InputStream() { @Override public int read() throws IOException { throw new IOException("Error :"); } }); haveData.countDown(); } } }.start(); return createResponseObject(path, null, createResourceInputSteam(path, uri, version, haveData, inputStreamRef, headerHeader)); } @NotNull private InputStream createResourceInputSteam(String path, Uri uri, String version, CountDownLatch haveData, AtomicReference<InputStream> inputStreamRef, Map<String, String> headerHeader) { return new InputStream() { FileOutputStream outputStream = null; BufferedOutputStream bufferedOutputStream = null; boolean ableToWrite = true; File tmpFile = null; int pos = 0; @Override public int read() throws IOException { // Open a tmp buffered file stream to start writing if (outputStream == null && ableToWrite) { Log.e("KCA", "Save LocalRes:" + path); openCache(); } // Wait the okhttp request try { if (!haveData.await(90, TimeUnit.SECONDS)){ // Waited so long and don't have any data // Instead of throwing exception, end the DL and pretend it is finished Log.d("KCVA", "Timeout to wait for OKHTTP:" + uri); closeFileStream(); ableToWrite = false; return -1; } } catch (InterruptedException e) { // Unable to wait the data // handle error and close streams ASAP // Instead of throwing exception, end the DL and pretend it is finished e.printStackTrace(); Log.d("KCVA", "Interrupted before the data:" + uri); closeFileStream(); ableToWrite = false; return -1; } // Read next data int nextData = forcedRead(inputStreamRef); pos++; if (nextData > -1) { // Have new data, try to write into file if (ableToWrite) { try { // Keep writing to the file if not yet failed bufferedOutputStream.write(nextData); } catch (Exception e) { ableToWrite = false; closeFileStream(); } } } else { // Close the file stream if (ableToWrite) { Log.d("KCVA", "END OF DOWNLOAD:" + uri); try { bufferedOutputStream.flush(); outputStream.close(); bufferedOutputStream.close(); // If the returned data is -2 or -3 // The resource is not complete // Do not rename the cache if (nextData == -1) { // Rename the file, (e.g. from xxx.png.tmp to xxx.png) // Since renaming is an atomic operation, // We can guarantee the final cache is never corrupted // If anything goes wrong, xxx.png will not be updated // Next usage of xxx.png will re-download it from internet again File to = new File(Environment.getExternalStorageDirectory(),"/KanCollCache" + path); if (tmpFile.renameTo(to)) { // If renaming is done, update the cache json in a new thread // Returning the last byte will not be blocked // Worst case is just one more un-cached request // But we saved a lot of smoothness new Thread() { @Override public void run() { synchronized (jsonObj) { try{ jsonObj.put(path, version); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(cacheJsonFile, false), "UTF-8")); writer.write(jsonObj.toString()); writer.close(); } catch (Exception e3) { e3.printStackTrace(); } } } }.start(); } } } catch (Exception e) { Log.e("KCA", e.getMessage()); } finally { // Whatever happened, the stream is closed and the last bit is sent // Sometimes the EOS is sent twice, prevent updating the cache twice ableToWrite = false; closeFileStream(); } } } // Do not return out custom code to the chromium // Which does not know if (nextData < -1) { nextData = -1; } return nextData; } // It is forced to read the next byte of input stream // If the stream fails to read, it will block and retry until: // 1. successful recovered with a new request; or // 2. user don't want to retry anymore; or // 3. interrupted by system // If it is recovered, the inputStream ref is changed to point to a recovered download // It returns the final output byte, or -2 if user don't want to retry anymore, or // -3 if there is an interrupt private int forcedRead(AtomicReference<InputStream> inputStreamRef) { final AtomicReference<Boolean> cancelled = new AtomicReference<>(false); final AtomicReference<Integer> numberOfRetry = new AtomicReference<>(0); while (true) { try { // First possible exit: successful read return inputStreamRef.get().read(); } catch (Exception ex) { // Failed to read from the stream } final CountDownLatch retryReady = new CountDownLatch(1); runOnUiThread(() -> { DialogInterface.OnClickListener dialogClickListener = (dialog, which) -> { switch (which){ case DialogInterface.BUTTON_POSITIVE: // User allow retry recovery // InputStream may be changed // We can proceed to next iteration retryReady.countDown(); break; case DialogInterface.BUTTON_NEGATIVE: // User give up and it is ok to stop loading cancelled.set(true); retryReady.countDown(); break; } dialog.dismiss(); }; AlertDialog.Builder builder = new AlertDialog.Builder(GameBaseActivity.this); // TODO: i18n builder.setTitle("游戏资源下载失败") .setMessage(path + "下载失败\n" + "是否重试?已重试次数: " + numberOfRetry + "\n" + (path.contains(".js") ? "跳过此文档可能引致游戏卡死!!" : (path.contains(".png") ? "跳过此图片可能引致画面异常" : "跳过此资源的后果未明!")) ) .setPositiveButton("重试", dialogClickListener) .setNegativeButton("跳过", dialogClickListener) .setCancelable(false).show(); }); try { // Wait for the user choice retryReady.await(); numberOfRetry.set(numberOfRetry.get() + 1); } catch (InterruptedException e) { // Possible exit: system interrupt while okhttp is trying return -3; } if (cancelled.get()) { // Possible exit: successful read return -2; } else { // User want to retry // Create new request and send it out try { Request.Builder builder = new Request.Builder().url(uri.toString()); for(Map.Entry<String, String> keySet : headerHeader.entrySet()){ builder.addHeader(keySet.getKey(), keySet.getValue()); } Request serverRequest = builder.build(); Response response = client.newCall(serverRequest).execute(); if(response != null && response.isSuccessful()){ ResponseBody body = response.body(); if (body != null) { InputStream is = body.byteStream(); if (pos == is.skip(pos)) { // Successfully recover the connection // Will continue at where error occurs // But it doesn't mean it can read next byte // Need to try it in next iteration inputStreamRef.set(is); } } } } catch (Exception e) { // Okhttp failed e.printStackTrace(); } } } } private void openCache() { tmpFile = new File(Environment.getExternalStorageDirectory(),"/KanCollCache" + path + ".tmp"); try { if (!tmpFile.getParentFile().exists()) { tmpFile.getParentFile().mkdirs(); } if(!tmpFile.exists()) { tmpFile.createNewFile(); } outputStream = new FileOutputStream(tmpFile); bufferedOutputStream = new BufferedOutputStream(outputStream, 4096); } catch(Exception e){ ableToWrite = false; closeFileStream(); } } private void closeFileStream() { if (outputStream != null) { try { outputStream.close(); } catch (IOException e2) { e2.printStackTrace(); } } if (bufferedOutputStream != null) { try { bufferedOutputStream.close(); } catch (Exception e2) { e2.printStackTrace(); } } } }; } public Object[] createResponseObject(String path, String size, InputStream is){ String mimeType = null; if (path.endsWith("mp3")) { mimeType = "audio/mpeg"; } else if (path.endsWith("png")) { mimeType = "image/png"; } else if (path.endsWith("json")) { mimeType = "application/json"; } else if (path.endsWith("js")) { mimeType = "application/javascript"; } else if (path.endsWith("css")) { mimeType = "text/css"; } else { mimeType = "text/html"; } Map<String, String> map = new HashMap<>(); map.put("Connection", "keep-alive"); map.put("Server", "KanCollCache"); if (size != null && !size.isEmpty()) { map.put("Content-Length", size); } map.put("Content-Type", mimeType); map.put("Cache-Control", "public"); return new Object[]{mimeType, null, 200, "OK", map, is}; } private ResponseBody requestServer(String url, Map<String, String> headerHeader){ Request.Builder builder = new Request.Builder().url(url).removeHeader("User-Agent").addHeader("User-Agent",USER_AGENT); for(Map.Entry<String, String> keySet : headerHeader.entrySet()){ builder.addHeader(keySet.getKey(), keySet.getValue()); } Request serverRequest = builder.build(); Response response = null; try { response = client.newCall(serverRequest).execute(); } catch (IOException e) { e.printStackTrace(); } if(response != null && response.isSuccessful()){ return response.body(); } else { return null; } } public void jsToJava(String requestUrl, String param, String respData){ try { URL url = new URL(requestUrl); KcaVpnData.renderToHander(url.getPath(), param, respData); if(url.getPath().contains("/kcsapi/api_start2/getData") && subTitleEnable){ SubTitleUtils.initShipGraph(respData); } if(url.getPath().contains("/kcsapi/api_port/port") && chatService){ String host = url.getHost(); String serName = serverMap.get(host); nickName = new JSONObject(respData.substring(7)).getJSONObject("api_data").getJSONObject("api_basic").getString("api_nickname") + (serName != null ? " - " + serName : ""); } } catch (Exception e) { e.printStackTrace(); } } public boolean dispatchTouchEvent(MotionEvent event) { if (changeTouchEventPrefs) { gameView.handleTouch(event); } return super.dispatchTouchEvent(event); } private void setScreenOrientation() { try { int screenchange = Settings.System.getInt(getContentResolver(), Settings.System.ACCELEROMETER_ROTATION); //是否开启自动旋转设置 1 开启 0 关闭 if (screenchange == 1){ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); }else { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); } } catch (Settings.SettingNotFoundException e) { e.printStackTrace(); } } public void resetWebView(boolean changeMutiWindow){ WindowManager manager = this.getWindowManager(); DisplayMetrics outMetrics = new DisplayMetrics(); manager.getDefaultDisplay().getMetrics(outMetrics); int width = outMetrics.widthPixels; int height = outMetrics.heightPixels; if(changeMutiWindow){ if(width < height) { int tmp = width; width = height; height = tmp; } else { int tmp = width; width = height; height = tmp / 2; } } double weightScale = width / 1200.0; double heightScale = height / 720.0; if(weightScale < heightScale){ gameView.setLayoutParams(width, (int)(720 * weightScale)); } else { gameView.setLayoutParams((int)(1200 * heightScale), height); } } private byte[] readFileToBytes(File file){ byte[] bytes = new byte[0]; if(!file.exists()){ return bytes; } BufferedInputStream buf = null; try { int fileSize = (int) file.length(); bytes = new byte[fileSize]; buf = new BufferedInputStream(new FileInputStream(file)); buf.read(bytes, 0, fileSize); } catch (IOException e) { e.printStackTrace(); } finally { try { if(buf != null){ buf.close(); } } catch (IOException e) { e.printStackTrace(); } } return bytes; } private void saveFile(String path, byte[] fileContent){ Log.e("KCA", "Save LocalRes:" + path); File file = new File(path); FileOutputStream outputStream = null; BufferedOutputStream bufferedOutputStream = null; try { if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } if(!file.exists()) { file.createNewFile(); } outputStream = new FileOutputStream(file); bufferedOutputStream = new BufferedOutputStream(outputStream); bufferedOutputStream.write(fileContent); bufferedOutputStream.flush(); } catch(Exception e){ Log.e("KCA", e.getMessage()); } finally { if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (bufferedOutputStream != null) { try { bufferedOutputStream.close(); } catch (Exception e2) { e2.printStackTrace(); } } } } private void addDanmaku(String content, boolean withBorder) { if(danmakuView == null){ return; } BaseDanmaku danmaku = danmakuContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL); if(content.length() > 7) { danmaku = danmakuContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_FIX_TOP); } danmaku.text = content; danmaku.padding = 5; danmaku.textSize = sp2px(14); danmaku.textColor = Color.WHITE; danmaku.textShadowColor = Color.BLACK; danmaku.setTime(danmakuView.getCurrentTime()); if (withBorder) { danmaku.borderColor = Color.GREEN; } danmakuView.addDanmaku(danmaku); } public int sp2px(float spValue) { final float fontScale = getResources().getDisplayMetrics().scaledDensity; return (int) (spValue * fontScale + 0.5f); } public int dip2px(float dpValue) { final float scale = getResources().getDisplayMetrics().density; return (int) (dpValue * scale + 0.5f); } // Inject code to change the spritesheet loading method of Pixi.js private String injectPixi(String pixi) { // Patch Pixi.js to solve the random black sprite issue // // Cause of issue: // 1. First of all, KC loads and unload image resources very often (for every scene change) // 2. Unlike java, openGL does not do GC or compact GPU memory. So there is more and more fragmentation // 3. When loading a new sprite sheet, Pixi creates a BaseTexture to hold the full image into GPU memory as one // 4. The full image of a sprite sheet can be as big as 3129*3047 in KC, eating up 36MB vram (after decode) // 5. The GPU failed to allocate a continuous gpu memory space. The sprites can't be loaded // // The idea of the solution is to split and crop the full images into sprite size before loading into GPU // Instead of sharing a huge BaseTexture, each sprite has its own smaller BaseTexture // Those smaller textures can fill into smaller free memory blocks so now we don't care fragmentation // // P.S. this patch uses more resource because canvas objects are used to split and cache the textures // canvas object has more overhead than img object (original method) in chromium // Load the full image in an img object instead of a BaseTexture resource, so it does not loaded into GPU pixi = pixi.replace("this.add(r,s,o,function(r){if(r.error)return void e(r.error);var n=new a.Spritesheet(r.texture.baseTexture,t.data,t.url);n.parse(function(){t.spritesheet=n,t.textures=n.textures,e()})})", "var image=new Image();" + "image.onerror=function(){var ee='Fail to download image: '+image.src;console.error(ee);e(ee);};" + "image.onload=function(){" + "var n=new a.Spritesheet(null,t.data,t.url);" + "n.image=image;" + "n.parse(function(){t.spritesheet=n,t.textures=n.textures,e()});" + "};" + "if(this.defaultQueryString){" + // Add back the version number "var hash=(/(#[\\w-]+)?$/).exec(s)[0];" + "s=s.substr(0,s.length-hash.length);" + "if(s.indexOf('?')!==-1){" + "s+='&'+this.defaultQueryString;" + "}else{" + "s+='?'+this.defaultQueryString;" + "}" + "s+=hash;" + "}"+ "image.src=s;"); // Assume scale is 1 pixi = pixi.replace("i=this.baseTexture.sourceScale;", "i=1;"); // Since each split frame has their own texture, the x and y values are 0 now pixi = pixi.replace("h=a.rotated?new o.Rectangle(Math.floor(u.x*i)/this.resolution,Math.floor(u.y*i)/this.resolution,Math.floor(u.h*i)/this.resolution,Math.floor(u.w*i)/this.resolution):new o.Rectangle(Math.floor(u.x*i)/this.resolution,Math.floor(u.y*i)/this.resolution,Math.floor(u.w*i)/this.resolution,Math.floor(u.h*i)/this.resolution),", "h=a.rotated?new o.Rectangle(0,0,Math.floor(u.h*i)/this.resolution,Math.floor(u.w*i)/this.resolution):new o.Rectangle(0,0,Math.floor(u.w*i)/this.resolution,Math.floor(u.h*i)/this.resolution),"); // When creating a sprite frame, create a canvas object to crop the part from the huge sshared BaseTexture // And then use the canvas as the BaseTexture pixi = pixi.replace(",this.textures[s]=new o.Texture(this.baseTexture,h,d,l,a.rotated?2:0,a.anchor),o.Texture.addToCache(this.textures[s],s)}r++}},", ";var tmpCanvas = document.createElement('canvas');" + "tmpCanvas.width = u.w; tmpCanvas.height = u.h;" + "tmpCanvas.getContext('2d').drawImage(this.image, u.x, u.y, u.w, u.h, 0, 0, u.w, u.h);" + "var bt = new PIXI.BaseTexture(tmpCanvas);" + "this.textures[s]=new o.Texture(bt,h,d,l,a.rotated?2:0,a.anchor),o.Texture.addToCache(this.textures[s],s)}r++};" + "this.image.onload=null,this.image.onerror=null,this.image=null" + // Also destroy the baseTexture after the loop "},"); // As the shared baseTexture is already destroyed in the parser, // Don't need to destroy it again in destroy() // Instead, destroy the baseTexture of each split texture pixi = pixi.replace("t.prototype.destroy=function(){var t=arguments.length>0&&void 0!==arguments[0]&&arguments[0];for(var e in this.textures)this.textures[e].destroy();this._frames=null,this._frameKeys=null,this.data=null,this.textures=null,t&&this.baseTexture.destroy(),this.baseTexture=null}", "t.prototype.destroy=function(){var t=arguments.length>0&&void 0!==arguments[0]&&arguments[0];for(var e in this.textures)this.textures[e].destroy(true);this._frames=null,this._frameKeys=null,this.data=null,this.textures=null}"); return pixi; } private String injectInspection(String inspection) { return inspection + "\ndiv#spacing_top {\n" + " height: 0px;\n" + " display: none;\n" + "}\n" + "body{background: #000;}"; } private byte[] injectTouchLogic(byte[] mainJs){ // Convert byte[] to String String s = new String(mainJs, StandardCharsets.UTF_8); // Replace the mouseout and mouseover event name to custom name s = s.replace("over:n.pointer?\"pointerover\":\"mouseover\"", "over:\"touchover\""); s = s.replace("out:n.pointer?\"pointerout\":\"mouseout\"", "out:\"touchout\""); // Add code patch inspired by https://github.com/pixijs/pixi.js/issues/616 // Only trigger touchout when there is another object start touchover s += "function patchInteractionManager () {\n" + " var proto = PIXI.interaction.InteractionManager.prototype;\n" + "\n" + " function extendMethod (method, extFn) {\n" + " var old = proto[method];\n" + " proto[method] = function () {\n" + " old.call(this, ...arguments);\n" + " extFn.call(this, ...arguments);\n" + " };\n" + " }\n" + "\n" + " extendMethod('onTouchMove', function () {\n" + " this.didMove = true;\n" + " });\n" + "\n" + " proto.update = mobileUpdate;\n" + "\n" + " function mobileUpdate(deltaTime) {\n" + " if (!this.interactionDOMElement) {\n" + " return;\n" + " }\n" + " if(this.didMove) {\n" + " this.didMove = false;\n" + " return;\n" + " }\n" + " if (this.eventData.data && (this.eventData.type == 'touchmove' || this.eventData.type == 'touchstart')) {\n" + " window.__eventData = this.eventData;\n" + " this.processInteractive(this.eventData, this.renderer._lastObjectRendered, this.processTouchOverOut, true);\n" + " }\n" + " }\n" + "\n" + " extendMethod('processTouchMove', function(displayObject, hit) {\n" + " this.processTouchOverOut('processTouchMove', displayObject, hit);\n" + " });\n" + " extendMethod('processTouchStart', function(displayObject, hit) {\n" + " this.processTouchOverOut('processTouchStart', displayObject, hit);\n" + " });\n" + "\n" + " proto.processTouchOverOut = function (interactionEvent, displayObject, hit) {\n" + " if(hit) {\n" + " if(!displayObject.__over) {\n" + " displayObject.__over = true;\n" + " proto.dispatchEvent( displayObject, 'touchover', window.__eventData);\n" + " }\n" + " } else {\n" + " if(displayObject.__over && interactionEvent.target != displayObject) {\n" + " displayObject.__over = false;\n" + " proto.dispatchEvent( displayObject, 'touchout', window.__eventData);\n" + " }\n" + " }\n" + " };\n" + "}\n" + "patchInteractionManager();"; // Convert back to bytes return s.getBytes(); } private byte[] injectFpsUpdater(byte[] mainJs){ // Convert byte[] to String String s = new String(mainJs, StandardCharsets.UTF_8); // Add code patch inspired by https://github.com/pixijs/pixi.js/issues/616 s += "const times = [];\n" + "var lastTimeFps = 0;\n" + "function refreshLoop() {\n" + " window.requestAnimationFrame(() => {\n" + " const now = performance.now();\n" + " while (times.length > 0 && times[0] <= now - 1000) {\n" + " times.shift();\n" + " }\n" + " times.push(now);\n" + " if (lastTimeFps != times.length) {\n" + " lastTimeFps = times.length;\n" + " window.fpsUpdater.update(times.length);\n" + " }\n" + " refreshLoop();\n" + " });\n" + "}\n" + "\n" + "refreshLoop();"; // Convert back to bytes return s.getBytes(); } private byte[] injectTickerTimingMode(byte[] mainJs){ // Convert byte[] to String String s = new String(mainJs, StandardCharsets.UTF_8); // Replace the ticker timing mode to keep animation smooth even with a lot of events (i.e. touch taps) s = s.replaceAll("(createjs\\[[\\s\\S]{10,20}\\]\\[[\\s\\S]{10,20}\\])=createjs\\[[\\s\\S]{10,20}\\]\\[[\\s\\S]{10,20}\\]", "$1=createjs.Ticker.RAF"); /*s = s.replace("GC_MAX_CHECK_COUNT=180", "GC_MAX_CHECK_COUNT=180"); s = s.replace("PIXI.settings.GC_MAX_IDLE=360", "" + "PIXI.settings.GC_MAX_IDLE=360," + "PIXI.settings.MIPMAP_TEXTURES=false"); // Save mem if an image is power of 2*/ // Convert back to bytes return s.getBytes(); } private byte[] injectHookAjax(byte[] respByte){ String modJson = "var shipModJson = [];"; if(modEnable){ modJson = "var shipModJson = " + buildModJson() + ";"; } String modRead = "function changeMod(v,xhr){if(shipModJson!=null&&xhr.xhr.responseURL.indexOf(\"/kcsapi/api_start2/getData\")!=-1){var getData=JSON.parse(v.substring(7));var jsonShipArr=getData.api_data.api_mst_shipgraph;for(var i=0;i<shipModJson.length;i++){var modJson=shipModJson[i];for(var j=0;j<jsonShipArr.length;j++){var getDataShip=jsonShipArr[j];if(JSON.parse(modJson).api_id==getDataShip.api_id){jsonShipArr[j]=JSON.parse(modJson);}}}\n" + "getData.api_data.api_mst_shipgraph=jsonShipArr;v=\"svdata=\"+JSON.stringify(getData);}\n" + "return v;};hookAjax({onreadystatechange:function(xhr){var contentType=xhr.getResponseHeader(\"content-type\")||\"\";if(contentType.toLocaleLowerCase().indexOf(\"text/plain\")!==-1&&xhr.readyState==4&&xhr.status==200){window.androidJs.JsToJavaInterface(xhr.xhr.responseURL,xhr.xhr.requestParam,xhr.responseText);}},send:function(arg,xhr){xhr.requestParam=arg[0];},responseText:{getter:changeMod},response:{getter:changeMod}});"; respByte = (new String(respByte) + modJson + modRead).getBytes(); return respByte; } public void updateFpsCounter(String newFps) { runOnUiThread(new Runnable(){ public void run(){ fpsCounter.setText(newFps); } }); } private byte[] bitmapToWebp(byte[] fileBytes) { Bitmap bitmap = BitmapFactory.decodeByteArray(fileBytes, 0, fileBytes.length); int height = bitmap.getHeight(); int width = bitmap.getWidth(); int stride = width * 4; int[] bitmapPixels = new int[width * height]; bitmap.getPixels(bitmapPixels, 0, width, 0, 0, width, height); byte[] bgra = new byte[width * height * 4]; for(int in = 0, out = 0; out<bgra.length; in++, out += 4){ bgra[out] = (byte) bitmapPixels[in]; bgra[out + 1] = (byte) (bitmapPixels[in] >> 8); bgra[out + 2] = (byte) (bitmapPixels[in] >> 16); bgra[out + 3] = (byte) (bitmapPixels[in] >> 24); } byte[] encoded = libwebp.WebPEncodeBGRA(bgra, width, height, stride, quality); return encoded; } }