Path: blob/main/Natives/JavaGUIViewController.m
589 views
#import "customcontrols/ControlLayout.h"1#import "customcontrols/CustomControlsUtils.h"2#import "JavaGUIViewController.h"3#import "JavaLauncher.h"4#import "LauncherPreferences.h"5#import "PLLogOutputView.h"6#import "TrackedTextField.h"7#import "UnzipKit.h"8#import "ios_uikit_bridge.h"9#include "glfw_keycodes.h"10#include "utils.h"1112#define SPECIALBTN_LOGOUTPUT -1001314static BOOL shouldHitEnterAfterWindowShown;15static SurfaceView* surfaceView;1617static jclass class_CTCAndroidInput;18static jmethodID method_ReceiveInput;1920void AWTInputBridge_nativeSendData(int type, int i1, int i2, int i3, int i4) {21if (!runtimeJNIEnvPtr) {22return;23}2425if (!method_ReceiveInput) {26class_CTCAndroidInput = (*runtimeJNIEnvPtr)->FindClass(runtimeJNIEnvPtr, "net/java/openjdk/cacio/ctc/CTCAndroidInput");27if ((*runtimeJNIEnvPtr)->ExceptionCheck(runtimeJNIEnvPtr) == JNI_TRUE) {28(*runtimeJNIEnvPtr)->ExceptionClear(runtimeJNIEnvPtr);29class_CTCAndroidInput = (*runtimeJNIEnvPtr)->FindClass(runtimeJNIEnvPtr, "com/github/caciocavallosilano/cacio/ctc/CTCAndroidInput");30}31assert(class_CTCAndroidInput != NULL);32method_ReceiveInput = (*runtimeJNIEnvPtr)->GetStaticMethodID(runtimeJNIEnvPtr, class_CTCAndroidInput, "receiveData", "(IIIII)V");33assert(method_ReceiveInput != NULL);34}3536(*runtimeJNIEnvPtr)->CallStaticVoidMethod(37runtimeJNIEnvPtr,38class_CTCAndroidInput,39method_ReceiveInput,40type, i1, i2, i3, i441);42}4344void AWTInputBridge_sendChar(jchar keychar) {45AWTInputBridge_nativeSendData(EVENT_TYPE_CHAR, (unsigned int)keychar, 0, 0, 0);46}4748void AWTInputBridge_sendKey(int keycode) {49// TODO: iOS -> AWT keycode mapping50AWTInputBridge_nativeSendData(EVENT_TYPE_KEY, ' ', keycode, 1, 0);51AWTInputBridge_nativeSendData(EVENT_TYPE_KEY, ' ', keycode, 0, 0);52}5354@interface SurfaceView() {55JNIEnv *surfaceJNIEnv;56jclass class_CTCScreen;57jmethodID method_GetRGB;58int *rgbArray;59}60@property(nonatomic) CGColorSpaceRef colorSpace;61@end6263@implementation SurfaceView64- (void)refreshBuffer {65if (!runtimeJavaVMPtr) {66// JVM is not ready yet67return;68} else if (!surfaceJNIEnv) {69// Obtain JNIEnvs70(*runtimeJavaVMPtr)->AttachCurrentThread(runtimeJavaVMPtr, &surfaceJNIEnv, NULL);71assert(surfaceJNIEnv);72dispatch_async(dispatch_get_main_queue(), ^{73(*runtimeJavaVMPtr)->AttachCurrentThread(runtimeJavaVMPtr, &runtimeJNIEnvPtr, NULL);74assert(runtimeJNIEnvPtr);75});7677// Obtain CTCScreen.getCurrentScreenRGB()78class_CTCScreen = (*surfaceJNIEnv)->FindClass(surfaceJNIEnv, "net/java/openjdk/cacio/ctc/CTCScreen");79if ((*surfaceJNIEnv)->ExceptionCheck(surfaceJNIEnv) == JNI_TRUE) {80(*surfaceJNIEnv)->ExceptionClear(surfaceJNIEnv);81class_CTCScreen = (*surfaceJNIEnv)->FindClass(surfaceJNIEnv, "com/github/caciocavallosilano/cacio/ctc/CTCScreen");82}83assert(class_CTCScreen != NULL);84method_GetRGB = (*surfaceJNIEnv)->GetStaticMethodID(surfaceJNIEnv, class_CTCScreen, "getCurrentScreenRGB", "()[I");85assert(method_GetRGB != NULL);86rgbArray = calloc(4, (size_t) (windowWidth * windowHeight));87}8889jintArray jreRgbArray = (jintArray) (*surfaceJNIEnv)->CallStaticObjectMethod(90surfaceJNIEnv,91class_CTCScreen,92method_GetRGB93);94if (!jreRgbArray) {95return;96}97int *tmpArray = (*surfaceJNIEnv)->GetIntArrayElements(surfaceJNIEnv, jreRgbArray, 0);98memcpy(rgbArray, tmpArray, windowWidth * windowHeight * 4);99(*surfaceJNIEnv)->ReleaseIntArrayElements(surfaceJNIEnv, jreRgbArray, tmpArray, JNI_ABORT);100dispatch_async(dispatch_get_main_queue(), ^{101[surfaceView displayLayer];102});103104// Wait until something renders at the middle105if (shouldHitEnterAfterWindowShown && rgbArray[windowWidth/2 + windowWidth*windowHeight/2] != 0) {106shouldHitEnterAfterWindowShown = NO;107dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 200 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^(void){108// Auto hit Enter to install immediately109AWTInputBridge_sendKey('\n');110});111}112}113114- (void)displayLayer {115CGDataProviderRef bitmapProvider = CGDataProviderCreateWithData(NULL, rgbArray, windowWidth * windowHeight * 4, NULL);116CGImageRef bitmap = CGImageCreate(windowWidth, windowHeight, 8, 32, 4 * windowWidth, _colorSpace, kCGImageAlphaFirst | kCGBitmapByteOrder32Little, bitmapProvider, NULL, FALSE, kCGRenderingIntentDefault);117118self.layer.contents = (__bridge id) bitmap;119CGImageRelease(bitmap);120CGDataProviderRelease(bitmapProvider);121// CGColorSpaceRelease(colorSpace);122}123124- (id)initWithFrame:(CGRect)frame {125self = [super initWithFrame:frame];126self.layer.opaque = YES;127self.colorSpace = CGColorSpaceCreateDeviceRGB();128return self;129}130@end131132@interface ScrollableSurfaceView<UIScrollViewDelegate> : UIScrollView133@property CGRect clickRange, virtualMouseFrame;134@property(nonatomic) UIImageView* mousePointerView;135@property BOOL shouldTriggerClick;136@end137138@implementation ScrollableSurfaceView139140- (instancetype)initWithFrame:(CGRect)frame {141surfaceView = [[SurfaceView alloc] initWithFrame:frame];142self = [super initWithFrame:frame];143[self addSubview:surfaceView];144self.delegate = (id)self;145146self.virtualMouseFrame = CGRectMake(frame.size.width / 2, frame.size.height / 2, 18, 27);147self.mousePointerView = [[UIImageView alloc] initWithFrame:self.virtualMouseFrame];148self.mousePointerView.hidden = !virtualMouseEnabled;149self.mousePointerView.image = [UIImage imageNamed:@"MousePointer"];150[surfaceView addSubview:self.mousePointerView];151152return self;153}154155- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {156[super touchesBegan:touches withEvent:event];157CGPoint location = [touches.anyObject locationInView:self];158self.clickRange = CGRectMake(location.x - 2, location.y - 2, 5, 5);159self.shouldTriggerClick = YES;160}161162- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {163[super touchesMoved:touches withEvent:event];164UITouch *touchEvent = touches.anyObject;165CGPoint location = [touchEvent locationInView:self];166if (self.shouldTriggerClick && !CGRectContainsPoint(self.clickRange, location)) {167self.shouldTriggerClick = NO;168}169170if (virtualMouseEnabled) {171CGPoint prevLocation = [touchEvent previousLocationInView:self];172// Calculate delta173location.x = (location.x - prevLocation.x) / self.zoomScale;174location.y = (location.y - prevLocation.y) / self.zoomScale;175// Update cursor's origin176_virtualMouseFrame.origin.x = clamp(self.virtualMouseFrame.origin.x + location.x, 0, self.frame.size.width * self.zoomScale);177_virtualMouseFrame.origin.y = clamp(self.virtualMouseFrame.origin.y + location.y, 0, self.frame.size.height * self.zoomScale);178self.mousePointerView.frame = self.virtualMouseFrame;179location = self.virtualMouseFrame.origin;180181CGPoint minimumContentOffset = CGPointMake(-self.contentInset.left, -self.contentInset.top);182CGPoint maximumContentOffset = CGPointMake(183MAX(minimumContentOffset.x, self.contentSize.width + self.contentInset.right - self.frame.size.width),184MAX(minimumContentOffset.y, self.contentSize.height + self.contentInset.bottom - self.frame.size.height));185// Focus scroll view's content area on virtual mouse186self.contentOffset = CGPointMake(187clamp(self.virtualMouseFrame.origin.x * self.zoomScale - self.center.x, minimumContentOffset.x, maximumContentOffset.x),188clamp(self.virtualMouseFrame.origin.y * self.zoomScale - self.center.y, minimumContentOffset.y, maximumContentOffset.y));189}190191// Send cursor position to AWT192CGFloat screenScale = UIScreen.mainScreen.scale * getPrefFloat(@"video.resolution") / 100.0;193AWTInputBridge_nativeSendData(EVENT_TYPE_CURSOR_POS, (int)(location.x * screenScale), (int)(location.y * screenScale), 0, 0);194}195196- (void)scrollViewDidZoom:(UIScrollView *)scrollView {197if (virtualMouseEnabled) {198// Keep virtual mouse in the middle of screen while zooming199_virtualMouseFrame.origin.x = (self.contentOffset.x + self.center.x) / self.zoomScale;200_virtualMouseFrame.origin.y = (self.contentOffset.y + self.center.y) / self.zoomScale;201self.mousePointerView.frame = self.virtualMouseFrame;202// Send cursor position to AWT203CGFloat screenScale = UIScreen.mainScreen.scale * getPrefFloat(@"video.resolution") / 100.0;204AWTInputBridge_nativeSendData(EVENT_TYPE_CURSOR_POS, (int)(_virtualMouseFrame.origin.x * screenScale), (int)(_virtualMouseFrame.origin.y * screenScale), 0, 0);205}206}207208- (UIView *)viewForZoomingInScrollView:(UIScrollView *)view {209return surfaceView;210}211212@end213214@interface JavaGUIViewController ()<UIGestureRecognizerDelegate, UITextFieldDelegate>215216@property(nonatomic) TrackedTextField* inputTextField;217@property(nonatomic) ControlLayout* ctrlView;218@property(nonatomic) PLLogOutputView* logOutputView;219@property(nonatomic) ScrollableSurfaceView* surfaceScrollView;220221@end222223@implementation JavaGUIViewController224225- (void)viewDidLoad {226[super viewDidLoad];227self.view.backgroundColor = UIColor.blackColor;228[self.navigationController setNavigationBarHidden:YES animated:NO];229[self setNeedsUpdateOfScreenEdgesDeferringSystemGestures];230[self setNeedsUpdateOfHomeIndicatorAutoHidden];231virtualMouseEnabled = getPrefBool(@"control.virtmouse_enable");232233CGRect screenBounds = self.view.bounds;234CGFloat screenScale = UIScreen.mainScreen.scale * getPrefFloat(@"video.resolution") / 100.0;235windowWidth = roundf(screenBounds.size.width * screenScale);236windowHeight = roundf(screenBounds.size.height * screenScale);237// Resolution should not be odd238if ((windowWidth % 2) != 0) {239--windowWidth;240}241if ((windowHeight % 2) != 0) {242--windowHeight;243}244245self.surfaceScrollView = [[ScrollableSurfaceView alloc] initWithFrame:self.view.frame];246self.surfaceScrollView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;247self.surfaceScrollView.minimumZoomScale = 1;248self.surfaceScrollView.maximumZoomScale = 5;249self.surfaceScrollView.scrollEnabled = NO;250[self.view addSubview:self.surfaceScrollView];251252self.inputTextField = [[TrackedTextField alloc] initWithFrame:CGRectMake(0, -32.0, self.view.frame.size.width, 30.0)];253self.inputTextField.backgroundColor = UIColor.secondarySystemBackgroundColor;254self.inputTextField.delegate = self;255self.inputTextField.font = [UIFont fontWithName:@"Menlo-Regular" size:20];256self.inputTextField.clearsOnBeginEditing = YES;257self.inputTextField.textAlignment = NSTextAlignmentCenter;258self.inputTextField.sendChar = ^(jchar keychar){259AWTInputBridge_sendChar(keychar);260};261self.inputTextField.sendKey = ^(int key, int scancode, int action, int mods) {262if (action == 0) return;263switch (key) {264case GLFW_KEY_BACKSPACE:265AWTInputBridge_sendKey('\b'); // VK_BACK_SPACE266break;267case GLFW_KEY_ENTER:268AWTInputBridge_sendKey('\n'); // VK_ENTER;269break;270case GLFW_KEY_DPAD_LEFT:271AWTInputBridge_sendKey(0xE2); // VK_KP_LEFT;272break;273case GLFW_KEY_DPAD_RIGHT:274AWTInputBridge_sendKey(0xE3); // VK_KP_RIGHT;275break;276}277};278[self.view addSubview:self.inputTextField];279280UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc]281initWithTarget:self action:@selector(surfaceOnClick:)];282tapGesture.delegate = self;283tapGesture.numberOfTapsRequired = 1;284tapGesture.numberOfTouchesRequired = 1;285tapGesture.cancelsTouchesInView = NO;286[surfaceView addGestureRecognizer:tapGesture];287288// Borrowing custom controls, might be useful later (full-blown jar launcher with control support?)289self.ctrlView = [[ControlLayout alloc] initWithFrame:UIEdgeInsetsInsetRect(self.view.frame, self.view.safeAreaInsets)];290[self.view addSubview:self.ctrlView];291[self loadCustomControls];292293self.logOutputView = [[PLLogOutputView alloc] initWithFrame:self.view.frame];294self.logOutputView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;295[self.view addSubview:self.logOutputView];296297setenv("POJAV_SKIP_JNI_GLFW", "1", 1);298299// Register the display loop300dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{301CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:surfaceView selector:@selector(refreshBuffer)];302if (@available(iOS 15.0, tvOS 15.0, *)) {303if(getPrefBool(@"video.max_framerate")) {304displayLink.preferredFrameRateRange = CAFrameRateRangeMake(30, 120, 120);305} else {306displayLink.preferredFrameRateRange = CAFrameRateRangeMake(30, 60, 60);307}308}309[displayLink addToRunLoop:NSRunLoop.currentRunLoop forMode:NSRunLoopCommonModes];310[NSRunLoop.currentRunLoop run];311});312313314dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{315launchJVM(nil, self.filepath, windowWidth, windowHeight, _requiredJavaVersion);316_requiredJavaVersion = 0;317});318}319320- (void)loadCustomControls {321NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];322dict[@"version"] = @(4);323dict[@"scaledAt"] = @(100);324dict[@"mControlDataList"] = [[NSMutableArray alloc] init];325//dict[@"mDrawerDataList"] = [[NSMutableArray alloc] init];326[dict[@"mControlDataList"] addObject:createButton(@"Keyboard",327(int[]){SPECIALBTN_KEYBOARD,0,0,0},328@"${margin}", @"${margin}",329BTN_RECT330)];331[dict[@"mControlDataList"] addObject:createButton(localize(@"game.menu.log_output", nil),332(int[]){SPECIALBTN_LOGOUTPUT,0,0,0},333@"${right} - ${margin}", @"${margin}",334BTN_RECT335)];336[dict[@"mControlDataList"] addObject:createButton(@"Mouse",337(int[]){SPECIALBTN_VIRTUALMOUSE,0,0,0},338@"${right} - ${margin}", @"${margin} * 2 + ${height}",339BTN_RECT340)];341[dict[@"mControlDataList"] addObject:createButton(@"PRI",342(int[]){SPECIALBTN_MOUSEPRI,0,0,0},343@"${margin}", @"${bottom} - ${margin}",344BTN_RECT345)];346[dict[@"mControlDataList"] addObject:createButton(@"SEC",347(int[]){SPECIALBTN_MOUSESEC,0,0,0},348@"${margin} * 2 + ${width}", @"${bottom} - ${margin}",349BTN_RECT350)];351[self.ctrlView loadControlLayout:dict];352353// Implement a subset of custom controls functionalites enough for few buttons354for (ControlButton *button in self.ctrlView.subviews) {355[button addTarget:self action:@selector(executebtn_down:) forControlEvents:UIControlEventTouchDown];356[button addTarget:self action:@selector(executebtn_up:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchUpOutside];357}358}359360@synthesize requiredJavaVersion = _requiredJavaVersion;361- (int)requiredJavaVersion {362if (_requiredJavaVersion) {363return _requiredJavaVersion;364}365366NSError *error;367UZKArchive *archive = [[UZKArchive alloc] initWithPath:self.filepath error:&error];368if (error) {369[self showErrorMessage:error.localizedDescription];370return _requiredJavaVersion = 0;371}372373NSData *manifestData = [archive extractDataFromFile:@"META-INF/MANIFEST.MF" error:&error];374if (error) {375[self showErrorMessage:error.localizedDescription];376return _requiredJavaVersion = 0;377}378379NSString *manifestStr = [[NSString alloc] initWithData:manifestData encoding:NSUTF8StringEncoding];380NSArray *manifestLines = [manifestStr componentsSeparatedByCharactersInSet:NSCharacterSet.newlineCharacterSet];381NSString *mainClass;382for (NSString *line in manifestLines) {383if ([line hasPrefix:@"Main-Class: "]) {384mainClass = [line substringFromIndex:12];385break;386}387}388if (!mainClass) {389[self showErrorMessage:[NSString stringWithFormat:390localize(@"java.error.missing_main_class", nil), self.filepath.lastPathComponent]];391return _requiredJavaVersion = 0;392}393mainClass = [NSString stringWithFormat:@"%@.class",394[mainClass stringByReplacingOccurrencesOfString:@"." withString:@"/"]];395396NSData *mainClassData = [archive extractDataFromFile:mainClass error:&error];397if (error) {398[self showErrorMessage:error.localizedDescription];399return _requiredJavaVersion = 0;400}401402uint32_t magic = OSSwapConstInt32(*(uint32_t*)mainClassData.bytes);403if (magic != 0xCAFEBABE) {404[self showErrorMessage:[NSString stringWithFormat:@"Invalid magic number: 0x%x", magic]];405return _requiredJavaVersion = 0;406}407408uint16_t *version = (uint16_t *)(mainClassData.bytes+sizeof(magic));409uint16_t minorVer = OSSwapConstInt16(version[0]);410uint16_t majorVer = OSSwapConstInt16(version[1]);411NSLog(@"[ModInstaller] Main class version: %u.%u", majorVer, minorVer);412413return _requiredJavaVersion = MAX(2, majorVer - 44);414}415416- (void)showErrorMessage:(NSString *)message {417surfaceView = nil;418showDialog(localize(@"Error", nil), message);419}420421- (void)setHitEnterAfterWindowShown:(BOOL)hitEnter {422shouldHitEnterAfterWindowShown = hitEnter;423}424425- (void)executebtn:(ControlButton *)sender withAction:(int)action {426int held = action == ACTION_DOWN;427for (int i = 0; i < 4; i++) {428int keycode = ((NSNumber *)sender.properties[@"keycodes"][i]).intValue;429if (keycode < 0) {430switch (keycode) {431case SPECIALBTN_KEYBOARD:432if (held) return;433[self toggleSoftKeyboard];434break;435436case SPECIALBTN_MOUSEPRI:437AWTInputBridge_nativeSendData(EVENT_TYPE_MOUSE_BUTTON, BUTTON1_DOWN_MASK, held, 0, 0);438break;439440case SPECIALBTN_MOUSEMID:441AWTInputBridge_nativeSendData(EVENT_TYPE_MOUSE_BUTTON, BUTTON2_DOWN_MASK, held, 0, 0);442break;443444case SPECIALBTN_MOUSESEC:445AWTInputBridge_nativeSendData(EVENT_TYPE_MOUSE_BUTTON, BUTTON3_DOWN_MASK, held, 0, 0);446break;447448case SPECIALBTN_VIRTUALMOUSE:449if (held) break;450virtualMouseEnabled = !virtualMouseEnabled;451self.surfaceScrollView.mousePointerView.hidden = !virtualMouseEnabled;452setPrefBool(@"control.virtmouse_enable", virtualMouseEnabled);453break;454455case SPECIALBTN_LOGOUTPUT:456if (held) break;457[self.logOutputView actionToggleLogOutput];458break;459460default:461NSLog(@"Warning: button %@ sent unknown special keycode: %d", sender.titleLabel.text, keycode);462break;463}464} else if (keycode > 0) {465// unimplemented466}467}468}469470- (void)executebtn_down:(ControlButton *)button {471[self executebtn:button withAction:ACTION_DOWN];472}473474- (void)executebtn_up:(ControlButton *)button {475[self executebtn:button withAction:ACTION_UP];476}477478- (void)surfaceOnClick:(UITapGestureRecognizer *)sender {479if (!self.surfaceScrollView.shouldTriggerClick) return;480if (sender.state == UIGestureRecognizerStateRecognized) {481CGFloat screenScale = UIScreen.mainScreen.scale * getPrefFloat(@"video.resolution") / 100.0;482CGPoint location = virtualMouseEnabled ?483self.surfaceScrollView.virtualMouseFrame.origin:484[sender locationInView:sender.view];485CGFloat x = location.x * screenScale;486CGFloat y = location.y * screenScale;487AWTInputBridge_nativeSendData(EVENT_TYPE_CURSOR_POS, (int)x, (int)y, 0, 0);488AWTInputBridge_nativeSendData(EVENT_TYPE_MOUSE_BUTTON, BUTTON1_DOWN_MASK, 1, 0, 0);489AWTInputBridge_nativeSendData(EVENT_TYPE_MOUSE_BUTTON, BUTTON1_DOWN_MASK, 0, 0, 0);490}491}492493- (BOOL)textFieldShouldReturn:(UITextField *)textField {494self.inputTextField.sendKey(GLFW_KEY_ENTER, 0, 1, 0);495//self.inputTextField.sendKey(GLFW_KEY_ENTER, 0, 0, 0);496textField.text = @"";497return YES;498}499500501- (void)toggleSoftKeyboard {502if (self.inputTextField.isFirstResponder) {503[self.inputTextField resignFirstResponder];504} else {505[self.inputTextField becomeFirstResponder];506}507}508509- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator510{511[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {512self.ctrlView.frame = UIEdgeInsetsInsetRect(self.view.frame, self.view.safeAreaInsets);513[self.ctrlView.subviews makeObjectsPerformSelector:@selector(update)];514} completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {515self.surfaceScrollView.virtualMouseFrame = self.surfaceScrollView.mousePointerView.frame;516}];517[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];518}519520- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures {521return UIRectEdgeBottom;522}523524- (BOOL)prefersHomeIndicatorAutoHidden {525return NO;526}527528- (BOOL)prefersStatusBarHidden {529return YES;530}531532@end533534535