mirror of
https://github.com/ClassiCube/ClassiCube.git
synced 2025-09-15 10:35:11 -04:00
Merge pull request #838 from UnknownShadow200/web_interop
Web: Move all nasty JavaScript-in-C code to a separate dedicated file
This commit is contained in:
commit
9c2b28ca88
11
src/Game.c
11
src/Game.c
@ -510,16 +510,9 @@ void Game_TakeScreenshot(void) {
|
||||
String_Format3(&filename, "-%p2-%p2-%p2.png", &now.hour, &now.minute, &now.second);
|
||||
|
||||
#ifdef CC_BUILD_WEB
|
||||
extern void interop_TakeScreenshot(const char* path);
|
||||
Platform_EncodeUtf8(str, &filename);
|
||||
EM_ASM_({
|
||||
var name = UTF8ToString($0);
|
||||
var canvas = Module['canvas'];
|
||||
if (canvas.toBlob) {
|
||||
canvas.toBlob(function(blob) { Module.saveBlob(blob, name); });
|
||||
} else if (canvas.msToBlob) {
|
||||
Module.saveBlob(canvas.msToBlob(), name);
|
||||
}
|
||||
}, str);
|
||||
interop_TakeScreenshot(str);
|
||||
#elif CC_BUILD_MINFILES
|
||||
/* no screenshots for these systems */
|
||||
#else
|
||||
|
40
src/Http.c
40
src/Http.c
@ -314,6 +314,8 @@ static void Http_SetRequestHeaders(struct HttpRequest* req) {
|
||||
#ifdef CC_BUILD_WEB
|
||||
#include <emscripten/emscripten.h>
|
||||
#include "Errors.h"
|
||||
extern void interop_DownloadAsync(const char* url, int method);
|
||||
extern int interop_IsHttpsOnly(void);
|
||||
|
||||
cc_bool Http_DescribeError(cc_result res, cc_string* dst) { return false; }
|
||||
/* web browsers do caching already, so don't need last modified/etags */
|
||||
@ -346,46 +348,12 @@ static void Http_DownloadAsync(struct HttpRequest* req) {
|
||||
String_InitArray(url, urlBuffer);
|
||||
Http_BeginRequest(req, &url);
|
||||
Platform_EncodeUtf8(urlStr, &url);
|
||||
|
||||
/* onFinished = FUNC(data, len, status) */
|
||||
/* onProgress = FUNC(read, total) */
|
||||
EM_ASM_({
|
||||
var url = UTF8ToString($0);
|
||||
var reqMethod = $1 == 1 ? 'HEAD' : 'GET';
|
||||
var onFinished = Module["_Http_OnFinishedAsync"];
|
||||
var onProgress = Module["_Http_OnUpdateProgress"];
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open(reqMethod, url);
|
||||
xhr.responseType = 'arraybuffer';
|
||||
|
||||
var getContentLength = function(e) {
|
||||
if (e.total) return e.total;
|
||||
|
||||
try {
|
||||
var len = xhr.getResponseHeader('Content-Length');
|
||||
return parseInt(len, 10);
|
||||
} catch (ex) { return 0; }
|
||||
};
|
||||
|
||||
xhr.onload = function(e) {
|
||||
var src = new Uint8Array(xhr.response);
|
||||
var len = src.byteLength;
|
||||
var data = _malloc(len);
|
||||
HEAPU8.set(src, data);
|
||||
onFinished(data, len || getContentLength(e), xhr.status);
|
||||
};
|
||||
xhr.onerror = function(e) { onFinished(0, 0, xhr.status); };
|
||||
xhr.ontimeout = function(e) { onFinished(0, 0, xhr.status); };
|
||||
xhr.onprogress = function(e) { onProgress(e.loaded, e.total); };
|
||||
|
||||
try { xhr.send(); } catch (e) { onFinished(0, 0, 0); }
|
||||
}, urlStr, req->requestType);
|
||||
interop_DownloadAsync(urlStr, req->requestType);
|
||||
}
|
||||
|
||||
static void Http_WorkerInit(void) {
|
||||
/* If this webpage is https://, browsers deny any http:// downloading */
|
||||
httpsOnly = EM_ASM_INT_V({ return location.protocol === 'https:'; });
|
||||
httpsOnly = interop_IsHttpsOnly();
|
||||
}
|
||||
static void Http_WorkerStart(void) { }
|
||||
static void Http_WorkerStop(void) { }
|
||||
|
29
src/Menus.c
29
src/Menus.c
@ -1320,7 +1320,7 @@ static void SaveLevelScreen_RemoveOverwrites(struct SaveLevelScreen* s) {
|
||||
}
|
||||
|
||||
#ifdef CC_BUILD_WEB
|
||||
#include <emscripten.h>
|
||||
extern int interop_DownloadMap(const char* path, const char* filename);
|
||||
static void DownloadMap(const cc_string* path) {
|
||||
char strPath[NATIVE_STR_LEN];
|
||||
char strFile[NATIVE_STR_LEN];
|
||||
@ -1333,21 +1333,8 @@ static void DownloadMap(const cc_string* path) {
|
||||
file.length = String_LastIndexOf(&file, '.');
|
||||
String_AppendConst(&file, ".cw");
|
||||
Platform_EncodeUtf8(strFile, &file);
|
||||
|
||||
res = EM_ASM_({
|
||||
try {
|
||||
var name = UTF8ToString($0);
|
||||
var data = FS.readFile(name);
|
||||
var blob = new Blob([data], { type: 'application/octet-stream' });
|
||||
Module.saveBlob(blob, UTF8ToString($1));
|
||||
FS.unlink(name);
|
||||
return 0;
|
||||
} catch (e) {
|
||||
if (!(e instanceof FS.ErrnoError)) abort(e);
|
||||
return -e.errno;
|
||||
}
|
||||
}, strPath, strFile);
|
||||
|
||||
|
||||
res = interop_DownloadMap(strPath, strFile);
|
||||
if (res) {
|
||||
Logger_SysWarn2(res, "Downloading map", &file);
|
||||
} else {
|
||||
@ -1594,18 +1581,12 @@ static void TexturePackScreen_LoadEntries(struct ListScreen* s) {
|
||||
}
|
||||
|
||||
#ifdef CC_BUILD_WEB
|
||||
#include <emscripten.h>
|
||||
extern void interop_UploadTexPack(const char* path);
|
||||
static void TexturePackScreen_UploadCallback(const cc_string* path) {
|
||||
char str[NATIVE_STR_LEN];
|
||||
Platform_EncodeUtf8(str, path);
|
||||
|
||||
/* Move from temp into texpacks folder */
|
||||
/* TODO: This is pretty awful and should be rewritten */
|
||||
EM_ASM_({
|
||||
var name = UTF8ToString($0);;
|
||||
var data = FS.readFile(name);
|
||||
FS.writeFile('/texpacks/' + name.substring(1), data);
|
||||
}, str);
|
||||
interop_UploadTexPack(str);
|
||||
TexturePackScreen_Show();
|
||||
TexturePack_SetDefault(path);
|
||||
TexturePack_ExtractCurrent(true);
|
||||
|
@ -566,9 +566,10 @@ cc_result File_Close(cc_file file) {
|
||||
#ifndef CC_BUILD_WEB
|
||||
return close(file) == -1 ? errno : 0;
|
||||
#else
|
||||
int ret = close(file) == -1 ? errno : 0;
|
||||
EM_ASM( FS.syncfs(false, function(err) { if (err) console.log(err); }); );
|
||||
return ret;
|
||||
extern void interop_SyncFS(void);
|
||||
int res = close(file) == -1 ? errno : 0;
|
||||
interop_SyncFS();
|
||||
return res;
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -1098,11 +1099,11 @@ cc_result Process_StartOpen(const cc_string* args) {
|
||||
cc_result Process_StartGame(const cc_string* args) { return ERR_NOT_SUPPORTED; }
|
||||
void Process_Exit(cc_result code) { exit(code); }
|
||||
|
||||
extern int interop_OpenTab(const char* url);
|
||||
cc_result Process_StartOpen(const cc_string* args) {
|
||||
char str[NATIVE_STR_LEN];
|
||||
Platform_EncodeUtf8(str, args);
|
||||
EM_ASM_({ window.open(UTF8ToString($0)); }, str);
|
||||
return 0;
|
||||
return interop_OpenTab(str);
|
||||
}
|
||||
#elif defined CC_BUILD_ANDROID
|
||||
static char gameArgsBuffer[512];
|
||||
@ -1730,35 +1731,18 @@ void Platform_Init(void) {
|
||||
Platform_InitSpecific();
|
||||
}
|
||||
#elif defined CC_BUILD_WEB
|
||||
extern void interop_InitModule(void);
|
||||
extern void interop_GetIndexedDBError(char* buffer);
|
||||
void Platform_Init(void) {
|
||||
char tmp[64+1] = { 0 };
|
||||
EM_ASM( Module['websocket']['subprotocol'] = 'ClassiCube'; );
|
||||
interop_InitModule();
|
||||
|
||||
/* Check if an error occurred when pre-loading IndexedDB */
|
||||
EM_ASM_({ if (window.cc_idbErr) stringToUTF8(window.cc_idbErr, $0, 64); }, tmp);
|
||||
|
||||
EM_ASM({
|
||||
Module.saveBlob = function(blob, name) {
|
||||
if (window.navigator.msSaveBlob) {
|
||||
window.navigator.msSaveBlob(blob, name); return;
|
||||
}
|
||||
var url = window.URL.createObjectURL(blob);
|
||||
var elem = document.createElement('a');
|
||||
|
||||
elem.href = url;
|
||||
elem.download = name;
|
||||
elem.style.display = 'none';
|
||||
|
||||
document.body.appendChild(elem);
|
||||
elem.click();
|
||||
document.body.removeChild(elem);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
});
|
||||
|
||||
interop_GetIndexedDBError(tmp);
|
||||
if (!tmp[0]) return;
|
||||
|
||||
Chat_Add1("&cError preloading IndexedDB: %c", tmp);
|
||||
Chat_AddRaw("&cPreviously saved settings/maps will be lost");
|
||||
|
||||
/* NOTE: You must pre-load IndexedDB before main() */
|
||||
/* (because pre-loading only works asynchronously) */
|
||||
/* If you don't, you'll get errors later trying to sync local to remote */
|
||||
|
258
src/Window.c
258
src/Window.c
@ -2746,17 +2746,19 @@ void Window_FreeFramebuffer(struct Bitmap* bmp) {
|
||||
#include <emscripten/html5.h>
|
||||
#include <emscripten/key_codes.h>
|
||||
#include "Game.h"
|
||||
static cc_bool keyboardOpen, needResize;
|
||||
extern int interop_CanvasWidth(void);
|
||||
extern int interop_CanvasHeight(void);
|
||||
extern int interop_ScreenWidth(void);
|
||||
extern int interop_ScreenHeight(void);
|
||||
|
||||
static cc_bool keyboardOpen, needResize;
|
||||
static int RawDpiScale(int x) { return (int)(x * emscripten_get_device_pixel_ratio()); }
|
||||
static int GetCanvasWidth(void) { return EM_ASM_INT_V({ return Module['canvas'].width }); }
|
||||
static int GetCanvasHeight(void) { return EM_ASM_INT_V({ return Module['canvas'].height }); }
|
||||
static int GetScreenWidth(void) { return RawDpiScale(EM_ASM_INT_V({ return screen.width; })); }
|
||||
static int GetScreenHeight(void) { return RawDpiScale(EM_ASM_INT_V({ return screen.height; })); }
|
||||
static int GetScreenWidth(void) { return RawDpiScale(interop_ScreenWidth()); }
|
||||
static int GetScreenHeight(void) { return RawDpiScale(interop_ScreenHeight()); }
|
||||
|
||||
static void UpdateWindowBounds(void) {
|
||||
int width = GetCanvasWidth();
|
||||
int height = GetCanvasHeight();
|
||||
int width = interop_CanvasWidth();
|
||||
int height = interop_CanvasHeight();
|
||||
if (width == WindowInfo.Width && height == WindowInfo.Height) return;
|
||||
|
||||
WindowInfo.Width = width;
|
||||
@ -2830,15 +2832,8 @@ static EM_BOOL OnMouseMove(int type, const EmscriptenMouseEvent* ev, void* data)
|
||||
return true;
|
||||
}
|
||||
|
||||
/* TODO: Also query mouse coordinates globally and reuse adjustXY here */
|
||||
/* Adjust from document coordinates to element coordinates */
|
||||
static void AdjustXY(int* x, int* y) {
|
||||
EM_ASM_({
|
||||
var canvasRect = Module['canvas'].getBoundingClientRect();
|
||||
HEAP32[$0 >> 2] = HEAP32[$0 >> 2] - canvasRect.left;
|
||||
HEAP32[$1 >> 2] = HEAP32[$1 >> 2] - canvasRect.top;
|
||||
}, x, y);
|
||||
}
|
||||
/* TODO: Also query mouse coordinates globally (in OnMouseMove) and reuse interop_AdjustXY here */
|
||||
extern void interop_AdjustXY(int* x, int* y);
|
||||
|
||||
static EM_BOOL OnTouchStart(int type, const EmscriptenTouchEvent* ev, void* data) {
|
||||
const EmscriptenTouchPoint* t;
|
||||
@ -2848,7 +2843,7 @@ static EM_BOOL OnTouchStart(int type, const EmscriptenTouchEvent* ev, void* data
|
||||
if (!t->isChanged) continue;
|
||||
x = t->targetX; y = t->targetY;
|
||||
|
||||
AdjustXY( &x, &y);
|
||||
interop_AdjustXY(&x, &y);
|
||||
RescaleXY(&x, &y);
|
||||
Input_AddTouch(t->identifier, x, y);
|
||||
}
|
||||
@ -2865,7 +2860,7 @@ static EM_BOOL OnTouchMove(int type, const EmscriptenTouchEvent* ev, void* data)
|
||||
if (!t->isChanged) continue;
|
||||
x = t->targetX; y = t->targetY;
|
||||
|
||||
AdjustXY( &x, &y);
|
||||
interop_AdjustXY(&x, &y);
|
||||
RescaleXY(&x, &y);
|
||||
Input_UpdateTouch(t->identifier, x, y);
|
||||
}
|
||||
@ -2882,7 +2877,7 @@ static EM_BOOL OnTouchEnd(int type, const EmscriptenTouchEvent* ev, void* data)
|
||||
if (!t->isChanged) continue;
|
||||
x = t->targetX; y = t->targetY;
|
||||
|
||||
AdjustXY( &x, &y);
|
||||
interop_AdjustXY(&x, &y);
|
||||
RescaleXY(&x, &y);
|
||||
Input_RemoveTouch(t->identifier, x, y);
|
||||
}
|
||||
@ -3087,6 +3082,10 @@ static void UnhookEvents(void) {
|
||||
emscripten_set_touchcancel_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, 0, NULL);
|
||||
}
|
||||
|
||||
extern int interop_IsAndroid(void);
|
||||
extern int interop_IsIOS(void);
|
||||
extern void interop_AddClipboardListeners(void);
|
||||
extern void interop_ForceTouchPageLayout(void);
|
||||
void Window_Init(void) {
|
||||
int is_ios, droid;
|
||||
DisplayInfo.Width = GetScreenWidth();
|
||||
@ -3095,40 +3094,10 @@ void Window_Init(void) {
|
||||
|
||||
DisplayInfo.ScaleX = emscripten_get_device_pixel_ratio();
|
||||
DisplayInfo.ScaleY = DisplayInfo.ScaleX;
|
||||
interop_AddClipboardListeners();
|
||||
|
||||
/* Copy text, but only if user isn't selecting something else on the webpage */
|
||||
/* (don't check window.clipboardData here, that's handled in Clipboard_SetText instead) */
|
||||
EM_ASM(window.addEventListener('copy',
|
||||
function(e) {
|
||||
if (window.getSelection && window.getSelection().toString()) return;
|
||||
ccall('Window_RequestClipboardText', 'void');
|
||||
if (!window.cc_copyText) return;
|
||||
|
||||
if (e.clipboardData) {
|
||||
e.clipboardData.setData('text/plain', window.cc_copyText);
|
||||
e.preventDefault();
|
||||
}
|
||||
window.cc_copyText = null;
|
||||
});
|
||||
);
|
||||
|
||||
/* Paste text (window.clipboardData is handled in Clipboard_GetText instead) */
|
||||
EM_ASM(window.addEventListener('paste',
|
||||
function(e) {
|
||||
if (e.clipboardData) {
|
||||
var contents = e.clipboardData.getData('text/plain');
|
||||
ccall('Window_GotClipboardText', 'void', ['string'], [contents]);
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
droid = EM_ASM_INT_V({ return /Android/i.test(navigator.userAgent); });
|
||||
/* iOS 13 on iPad doesn't identify itself as iPad by default anymore */
|
||||
/* https://stackoverflow.com/questions/57765958/how-to-detect-ipad-and-ipad-os-version-in-ios-13-and-up */
|
||||
is_ios = EM_ASM_INT_V({
|
||||
return /iPhone|iPad|iPod/i.test(navigator.userAgent) ||
|
||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints && navigator.maxTouchPoints > 2);
|
||||
});
|
||||
droid = interop_IsAndroid();
|
||||
is_ios = interop_IsIOS();
|
||||
Input_SetTouchMode(is_ios || droid);
|
||||
|
||||
/* iOS shifts the whole webpage up when opening chat, which causes problems */
|
||||
@ -3139,38 +3108,25 @@ void Window_Init(void) {
|
||||
|
||||
/* Let the webpage know it needs to force a mobile layout */
|
||||
if (!Input_TouchMode) return;
|
||||
EM_ASM( if (typeof(forceTouchLayout) === 'function') forceTouchLayout(); );
|
||||
interop_ForceTouchPageLayout();
|
||||
}
|
||||
|
||||
extern void interop_InitContainer(void);
|
||||
void Window_Create(int width, int height) {
|
||||
WindowInfo.Exists = true;
|
||||
WindowInfo.Focused = true;
|
||||
HookEvents();
|
||||
/* Let the webpage decide on initial bounds */
|
||||
WindowInfo.Width = GetCanvasWidth();
|
||||
WindowInfo.Height = GetCanvasHeight();
|
||||
|
||||
/* Create wrapper div if necessary */
|
||||
EM_ASM({
|
||||
var agent = navigator.userAgent;
|
||||
var canvas = Module['canvas'];
|
||||
window.cc_container = document.body;
|
||||
|
||||
if (/Android/i.test(agent) && /Chrome/i.test(agent)) {
|
||||
var wrapper = document.createElement("div");
|
||||
wrapper.id = 'canvas_wrapper';
|
||||
|
||||
canvas.parentNode.insertBefore(wrapper, canvas);
|
||||
wrapper.appendChild(canvas);
|
||||
window.cc_container = wrapper;
|
||||
}
|
||||
});
|
||||
WindowInfo.Width = interop_CanvasWidth();
|
||||
WindowInfo.Height = interop_CanvasHeight();
|
||||
interop_InitContainer();
|
||||
}
|
||||
|
||||
extern void interop_SetPageTitle(const char* title);
|
||||
void Window_SetTitle(const cc_string* title) {
|
||||
char str[NATIVE_STR_LEN];
|
||||
Platform_EncodeUtf8(str, title);
|
||||
EM_ASM_({ document.title = UTF8ToString($0); }, str);
|
||||
interop_SetPageTitle(str);
|
||||
}
|
||||
|
||||
static char pasteBuffer[512];
|
||||
@ -3189,31 +3145,20 @@ EMSCRIPTEN_KEEPALIVE void Window_GotClipboardText(char* src) {
|
||||
Event_RaiseInput(&InputEvents.Down, INPUT_CLIPBOARD_PASTE, 0);
|
||||
}
|
||||
|
||||
extern void interop_TryGetClipboardText(void);
|
||||
void Clipboard_GetText(cc_string* value) {
|
||||
/* For IE11, use window.clipboardData to get the clipboard */
|
||||
EM_ASM_({
|
||||
if (window.clipboardData) {
|
||||
var contents = window.clipboardData.getData('Text');
|
||||
ccall('Window_StoreClipboardText', 'void', ['string'], [contents]);
|
||||
}
|
||||
});
|
||||
/* Window_StoreClipboardText may or may not be called by this */
|
||||
interop_TryGetClipboardText();
|
||||
|
||||
String_Copy(value, &pasteStr);
|
||||
pasteStr.length = 0;
|
||||
}
|
||||
|
||||
extern void interop_TrySetClipboardText(const char* text);
|
||||
void Clipboard_SetText(const cc_string* value) {
|
||||
char str[NATIVE_STR_LEN];
|
||||
Platform_EncodeUtf8(str, value);
|
||||
|
||||
/* For IE11, use window.clipboardData to set the clipboard */
|
||||
/* For other browsers, instead use the window.copy events */
|
||||
EM_ASM_({
|
||||
if (window.clipboardData) {
|
||||
if (window.getSelection && window.getSelection().toString()) return;
|
||||
window.clipboardData.setData('Text', UTF8ToString($0));
|
||||
} else {
|
||||
window.cc_copyText = UTF8ToString($0);
|
||||
}
|
||||
}, str);
|
||||
interop_TrySetClipboardText(str);
|
||||
}
|
||||
|
||||
void Window_Show(void) { }
|
||||
@ -3224,6 +3169,8 @@ int Window_GetWindowState(void) {
|
||||
return status.isFullscreen ? WINDOW_STATE_FULLSCREEN : WINDOW_STATE_NORMAL;
|
||||
}
|
||||
|
||||
extern int interop_GetContainerID(void);
|
||||
extern void interop_EnterFullscreen(void);
|
||||
cc_result Window_EnterFullscreen(void) {
|
||||
EmscriptenFullscreenStrategy strategy;
|
||||
const char* target;
|
||||
@ -3235,28 +3182,15 @@ cc_result Window_EnterFullscreen(void) {
|
||||
strategy.canvasResizedCallback = OnCanvasResize;
|
||||
strategy.canvasResizedCallbackUserData = NULL;
|
||||
|
||||
/* For chrome on android, need to make container div fullscreen instead */
|
||||
res = EM_ASM_INT_V({ return document.getElementById('canvas_wrapper') ? 1 : 0; });
|
||||
/* TODO: Return container element ID instead of hardcoding here */
|
||||
res = interop_GetContainerID();
|
||||
target = res ? "canvas_wrapper" : "#canvas";
|
||||
|
||||
res = emscripten_request_fullscreen_strategy(target, 1, &strategy);
|
||||
if (res == EMSCRIPTEN_RESULT_NOT_SUPPORTED) res = ERR_NOT_SUPPORTED;
|
||||
if (res) return res;
|
||||
|
||||
/* emscripten sets css size to screen's base width/height, */
|
||||
/* except that becomes wrong when device rotates. */
|
||||
/* Better to just set CSS width/height to always be 100% */
|
||||
EM_ASM({
|
||||
var canvas = Module['canvas'];
|
||||
canvas.style.width = '100%';
|
||||
canvas.style.height = '100%';
|
||||
});
|
||||
|
||||
/* By default, pressing Escape will immediately exit fullscreen - which is */
|
||||
/* quite annoying given that it is also the Menu key. Some browsers allow */
|
||||
/* 'locking' the Escape key, so that you have to hold down Escape to exit. */
|
||||
/* NOTE: This ONLY works when the webpage is a https:// one */
|
||||
EM_ASM({ try { navigator.keyboard.lock(["Escape"]); } catch (ex) { } });
|
||||
interop_EnterFullscreen();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -3286,6 +3220,7 @@ void Window_Close(void) {
|
||||
UnhookEvents();
|
||||
}
|
||||
|
||||
extern void interop_RequestCanvasResize(void);
|
||||
void Window_ProcessEvents(void) {
|
||||
if (!needResize) return;
|
||||
needResize = false;
|
||||
@ -3294,7 +3229,8 @@ void Window_ProcessEvents(void) {
|
||||
if (Window_GetWindowState() == WINDOW_STATE_FULLSCREEN) {
|
||||
SetFullscreenBounds();
|
||||
} else {
|
||||
EM_ASM( if (typeof(resizeGameCanvas) === 'function') resizeGameCanvas(); );
|
||||
/* Webpage can adjust canvas size if it wants to */
|
||||
interop_RequestCanvasResize();
|
||||
}
|
||||
UpdateWindowBounds();
|
||||
}
|
||||
@ -3304,16 +3240,14 @@ static void Cursor_GetRawPos(int* x, int* y) { *x = 0; *y = 0; }
|
||||
/* Not allowed to move cursor from javascript */
|
||||
void Cursor_SetPosition(int x, int y) { }
|
||||
|
||||
extern void interop_SetCursorVisible(int visible);
|
||||
static void Cursor_DoSetVisible(cc_bool visible) {
|
||||
if (visible) {
|
||||
EM_ASM(Module['canvas'].style['cursor'] = 'default'; );
|
||||
} else {
|
||||
EM_ASM(Module['canvas'].style['cursor'] = 'none'; );
|
||||
}
|
||||
interop_SetCursorVisible(visible);
|
||||
}
|
||||
|
||||
static void ShowDialogCore(const char* title, const char* msg) {
|
||||
EM_ASM_({ alert(UTF8ToString($0) + "\n\n" + UTF8ToString($1)); }, title, msg);
|
||||
extern void interop_ShowDialog(const char* title, const char* msg);
|
||||
static void ShowDialogCore(const char* title, const char* msg) {
|
||||
interop_ShowDialog(title, msg);
|
||||
}
|
||||
|
||||
static OpenFileDialogCallback uploadCallback;
|
||||
@ -3326,39 +3260,11 @@ EMSCRIPTEN_KEEPALIVE void Window_OnFileUploaded(const char* src) {
|
||||
uploadCallback = NULL;
|
||||
}
|
||||
|
||||
extern void interop_OpenFileDialog(const char* filter);
|
||||
cc_result Window_OpenFileDialog(const char* filter, OpenFileDialogCallback callback) {
|
||||
uploadCallback = callback;
|
||||
EM_ASM_({
|
||||
var elem = window.cc_uploadElem;
|
||||
if (!elem) {
|
||||
elem = document.createElement('input');
|
||||
elem.setAttribute('type', 'file');
|
||||
elem.setAttribute('style', 'display: none');
|
||||
elem.accept = UTF8ToString($0);
|
||||
|
||||
elem.addEventListener('change',
|
||||
function(ev) {
|
||||
var files = ev.target.files;
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var reader = new FileReader();
|
||||
var name = files[i].name;
|
||||
|
||||
reader.onload = function(e) {
|
||||
var data = new Uint8Array(e.target.result);
|
||||
FS.createDataFile('/', name, data, true, true, true);
|
||||
ccall('Window_OnFileUploaded', 'void', ['string'], ['/' + name]);
|
||||
FS.unlink('/' + name);
|
||||
};
|
||||
reader.readAsArrayBuffer(files[i]);
|
||||
}
|
||||
window.cc_container.removeChild(window.cc_uploadElem);
|
||||
window.cc_uploadElem = null;
|
||||
}, false);
|
||||
window.cc_uploadElem = elem;
|
||||
window.cc_container.appendChild(elem);
|
||||
}
|
||||
elem.click();
|
||||
}, filter);
|
||||
/* Calls Window_OnFileUploaded on success */
|
||||
interop_OpenFileDialog(filter);
|
||||
return ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
@ -3366,6 +3272,10 @@ void Window_AllocFramebuffer(struct Bitmap* bmp) { }
|
||||
void Window_DrawFramebuffer(Rect2D r) { }
|
||||
void Window_FreeFramebuffer(struct Bitmap* bmp) { }
|
||||
|
||||
extern void interop_OpenKeyboard(const char* text, int type, const char* placeholder);
|
||||
extern void interop_SetKeyboardText(const char* text);
|
||||
extern void interop_CloseKeyboard(void);
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void Window_OnTextChanged(const char* src) {
|
||||
cc_string str; char buffer[800];
|
||||
String_InitArray(str, buffer);
|
||||
@ -3378,64 +3288,24 @@ void Window_OpenKeyboard(const struct OpenKeyboardArgs* args) {
|
||||
char str[NATIVE_STR_LEN];
|
||||
keyboardOpen = true;
|
||||
if (!Input_TouchMode) return;
|
||||
|
||||
Platform_EncodeUtf8(str, args->text);
|
||||
Platform_LogConst("OPEN SESAME");
|
||||
|
||||
EM_ASM_({
|
||||
var elem = window.cc_inputElem;
|
||||
if (!elem) {
|
||||
if ($1 == 1) {
|
||||
elem = document.createElement('input');
|
||||
elem.setAttribute('inputmode', 'decimal');
|
||||
} else {
|
||||
elem = document.createElement('textarea');
|
||||
}
|
||||
elem.setAttribute('style', 'position:absolute; left:0; bottom:0; margin: 0px; width: 100%');
|
||||
elem.setAttribute('placeholder', UTF8ToString($2));
|
||||
elem.value = UTF8ToString($0);
|
||||
|
||||
elem.addEventListener('input',
|
||||
function(ev) {
|
||||
ccall('Window_OnTextChanged', 'void', ['string'], [ev.target.value]);
|
||||
}, false);
|
||||
window.cc_inputElem = elem;
|
||||
|
||||
window.cc_divElem = document.createElement('div');
|
||||
window.cc_divElem.setAttribute('style', 'position:absolute; left:0; top:0; width:100%; height:100%; background-color: black; opacity:0.4; resize:none; pointer-events:none;');
|
||||
|
||||
window.cc_container.appendChild(window.cc_divElem);
|
||||
window.cc_container.appendChild(elem);
|
||||
}
|
||||
elem.focus();
|
||||
elem.click();
|
||||
}, str, args->type, args->placeholder);
|
||||
interop_OpenKeyboard(str, args->type, args->placeholder);
|
||||
}
|
||||
|
||||
void Window_SetKeyboardText(const cc_string* text) {
|
||||
char str[NATIVE_STR_LEN];
|
||||
if (!Input_TouchMode) return;
|
||||
|
||||
Platform_EncodeUtf8(str, text);
|
||||
|
||||
EM_ASM_({
|
||||
if (!window.cc_inputElem) return;
|
||||
var str = UTF8ToString($0);
|
||||
|
||||
if (str == window.cc_inputElem.value) return;
|
||||
window.cc_inputElem.value = str;
|
||||
}, str);
|
||||
interop_SetKeyboardText(str);
|
||||
}
|
||||
|
||||
void Window_CloseKeyboard(void) {
|
||||
keyboardOpen = false;
|
||||
if (!Input_TouchMode) return;
|
||||
|
||||
EM_ASM({
|
||||
if (!window.cc_inputElem) return;
|
||||
window.cc_container.removeChild(window.cc_divElem);
|
||||
window.cc_container.removeChild(window.cc_inputElem);
|
||||
window.cc_divElem = null;
|
||||
window.cc_inputElem = null;
|
||||
});
|
||||
interop_CloseKeyboard();
|
||||
}
|
||||
|
||||
void Window_EnableRawMouse(void) {
|
||||
@ -4497,15 +4367,11 @@ void GLContext_SetFpsLimit(cc_bool vsync, float minFrameMs) {
|
||||
}
|
||||
}
|
||||
|
||||
extern void interop_GetGpuRenderer(char* buffer, int len);
|
||||
void GLContext_GetApiInfo(cc_string* info) {
|
||||
char buffer[NATIVE_STR_LEN];
|
||||
int len;
|
||||
|
||||
EM_ASM_({
|
||||
var dbg = GLctx.getExtension('WEBGL_debug_renderer_info');
|
||||
var str = dbg ? GLctx.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : "";
|
||||
stringToUTF8(str, $0, $1);
|
||||
}, buffer, NATIVE_STR_LEN);
|
||||
interop_GetGpuRenderer(buffer, NATIVE_STR_LEN);
|
||||
|
||||
len = String_CalcLen(buffer, NATIVE_STR_LEN);
|
||||
if (!len) return;
|
||||
|
379
src/interop_web.c
Normal file
379
src/interop_web.c
Normal file
@ -0,0 +1,379 @@
|
||||
#include "Core.h"
|
||||
|
||||
#ifdef CC_BUILD_WEB
|
||||
#include <emscripten/emscripten.h>
|
||||
|
||||
void interop_InitModule(void) {
|
||||
EM_ASM({
|
||||
Module['websocket']['subprotocol'] = 'ClassiCube';
|
||||
|
||||
Module.saveBlob = function(blob, name) {
|
||||
if (window.navigator.msSaveBlob) {
|
||||
window.navigator.msSaveBlob(blob, name); return;
|
||||
}
|
||||
var url = window.URL.createObjectURL(blob);
|
||||
var elem = document.createElement('a');
|
||||
|
||||
elem.href = url;
|
||||
elem.download = name;
|
||||
elem.style.display = 'none';
|
||||
|
||||
document.body.appendChild(elem);
|
||||
elem.click();
|
||||
document.body.removeChild(elem);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*########################################################################################################################*
|
||||
*-----------------------------------------------------------Game----------------------------------------------------------*
|
||||
*#########################################################################################################################*/
|
||||
void interop_TakeScreenshot(const char* path) {
|
||||
EM_ASM_({
|
||||
var name = UTF8ToString($0);
|
||||
var canvas = Module['canvas'];
|
||||
if (canvas.toBlob) {
|
||||
canvas.toBlob(function(blob) { Module.saveBlob(blob, name); });
|
||||
} else if (canvas.msToBlob) {
|
||||
Module.saveBlob(canvas.msToBlob(), name);
|
||||
}
|
||||
}, path);
|
||||
}
|
||||
|
||||
|
||||
/*########################################################################################################################*
|
||||
*-----------------------------------------------------------Http----------------------------------------------------------*
|
||||
*#########################################################################################################################*/
|
||||
void interop_DownloadAsync(const char* urlStr, int method) {
|
||||
/* onFinished = FUNC(data, len, status) */
|
||||
/* onProgress = FUNC(read, total) */
|
||||
EM_ASM_({
|
||||
var url = UTF8ToString($0);
|
||||
var reqMethod = $1 == 1 ? 'HEAD' : 'GET';
|
||||
var onFinished = Module["_Http_OnFinishedAsync"];
|
||||
var onProgress = Module["_Http_OnUpdateProgress"];
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open(reqMethod, url);
|
||||
xhr.responseType = 'arraybuffer';
|
||||
|
||||
var getContentLength = function(e) {
|
||||
if (e.total) return e.total;
|
||||
|
||||
try {
|
||||
var len = xhr.getResponseHeader('Content-Length');
|
||||
return parseInt(len, 10);
|
||||
} catch (ex) { return 0; }
|
||||
};
|
||||
|
||||
xhr.onload = function(e) {
|
||||
var src = new Uint8Array(xhr.response);
|
||||
var len = src.byteLength;
|
||||
var data = _malloc(len);
|
||||
HEAPU8.set(src, data);
|
||||
onFinished(data, len || getContentLength(e), xhr.status);
|
||||
};
|
||||
xhr.onerror = function(e) { onFinished(0, 0, xhr.status); };
|
||||
xhr.ontimeout = function(e) { onFinished(0, 0, xhr.status); };
|
||||
xhr.onprogress = function(e) { onProgress(e.loaded, e.total); };
|
||||
|
||||
try { xhr.send(); } catch (e) { onFinished(0, 0, 0); }
|
||||
}, urlStr, method);
|
||||
}
|
||||
|
||||
int interop_IsHttpsOnly(void) {
|
||||
/* If this webpage is https://, browsers deny any http:// downloading */
|
||||
return EM_ASM_INT_V({ return location.protocol === 'https:'; });
|
||||
}
|
||||
|
||||
|
||||
/*########################################################################################################################*
|
||||
*-----------------------------------------------------------Menu----------------------------------------------------------*
|
||||
*#########################################################################################################################*/
|
||||
int interop_DownloadMap(const char* path, const char* filename) {
|
||||
return EM_ASM_({
|
||||
try {
|
||||
var name = UTF8ToString($0);
|
||||
var data = FS.readFile(name);
|
||||
var blob = new Blob([data], { type: 'application/octet-stream' });
|
||||
Module.saveBlob(blob, UTF8ToString($1));
|
||||
FS.unlink(name);
|
||||
return 0;
|
||||
} catch (e) {
|
||||
if (!(e instanceof FS.ErrnoError)) abort(e);
|
||||
return -e.errno;
|
||||
}
|
||||
}, path, filename);
|
||||
}
|
||||
|
||||
void interop_UploadTexPack(const char* path) {
|
||||
/* Move from temp into texpacks folder */
|
||||
/* TODO: This is pretty awful and should be rewritten */
|
||||
EM_ASM_({
|
||||
var name = UTF8ToString($0);
|
||||
var data = FS.readFile(name);
|
||||
FS.writeFile('/texpacks/' + name.substring(1), data);
|
||||
}, path);
|
||||
}
|
||||
|
||||
|
||||
/*########################################################################################################################*
|
||||
*---------------------------------------------------------Platform--------------------------------------------------------*
|
||||
*#########################################################################################################################*/
|
||||
void interop_GetIndexedDBError(char* buffer) {
|
||||
EM_ASM_({ if (window.cc_idbErr) stringToUTF8(window.cc_idbErr, $0, 64); }, buffer);
|
||||
}
|
||||
|
||||
void interop_SyncFS(void) {
|
||||
EM_ASM( FS.syncfs(false, function(err) { if (err) console.log(err); }); );
|
||||
}
|
||||
|
||||
int interop_OpenTab(const char* url) {
|
||||
EM_ASM_({ window.open(UTF8ToString($0)); }, url);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/*########################################################################################################################*
|
||||
*----------------------------------------------------------Window---------------------------------------------------------*
|
||||
*#########################################################################################################################*/
|
||||
int interop_CanvasWidth(void) { return EM_ASM_INT_V({ return Module['canvas'].width }); }
|
||||
int interop_CanvasHeight(void) { return EM_ASM_INT_V({ return Module['canvas'].height }); }
|
||||
int interop_ScreenWidth(void) { return EM_ASM_INT_V({ return screen.width; }); }
|
||||
int interop_ScreenHeight(void) { return EM_ASM_INT_V({ return screen.height; }); }
|
||||
|
||||
int interop_IsAndroid(void) {
|
||||
return EM_ASM_INT_V({ return /Android/i.test(navigator.userAgent); });
|
||||
}
|
||||
int interop_IsIOS(void) {
|
||||
/* iOS 13 on iPad doesn't identify itself as iPad by default anymore */
|
||||
/* https://stackoverflow.com/questions/57765958/how-to-detect-ipad-and-ipad-os-version-in-ios-13-and-up */
|
||||
return EM_ASM_INT_V({
|
||||
return /iPhone|iPad|iPod/i.test(navigator.userAgent) ||
|
||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints && navigator.maxTouchPoints > 2);
|
||||
});
|
||||
}
|
||||
|
||||
void interop_InitContainer(void) {
|
||||
/* Create wrapper div if necessary */
|
||||
EM_ASM({
|
||||
var agent = navigator.userAgent;
|
||||
var canvas = Module['canvas'];
|
||||
window.cc_container = document.body;
|
||||
|
||||
if (/Android/i.test(agent) && /Chrome/i.test(agent)) {
|
||||
var wrapper = document.createElement("div");
|
||||
wrapper.id = 'canvas_wrapper';
|
||||
|
||||
canvas.parentNode.insertBefore(wrapper, canvas);
|
||||
wrapper.appendChild(canvas);
|
||||
window.cc_container = wrapper;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
int interop_GetContainerID(void) {
|
||||
/* For chrome on android, need to make container div fullscreen instead */
|
||||
return EM_ASM_INT_V({ return document.getElementById('canvas_wrapper') ? 1 : 0; });
|
||||
}
|
||||
|
||||
void interop_ForceTouchPageLayout(void) {
|
||||
EM_ASM( if (typeof(forceTouchLayout) === 'function') forceTouchLayout(); );
|
||||
}
|
||||
|
||||
void interop_SetPageTitle(const char* title) {
|
||||
EM_ASM_({ document.title = UTF8ToString($0); }, title);
|
||||
}
|
||||
|
||||
void interop_AddClipboardListeners(void) {
|
||||
/* Copy text, but only if user isn't selecting something else on the webpage */
|
||||
/* (don't check window.clipboardData here, that's handled in interop_TrySetClipboardText instead) */
|
||||
EM_ASM(window.addEventListener('copy',
|
||||
function(e) {
|
||||
if (window.getSelection && window.getSelection().toString()) return;
|
||||
ccall('Window_RequestClipboardText', 'void');
|
||||
if (!window.cc_copyText) return;
|
||||
|
||||
if (e.clipboardData) {
|
||||
e.clipboardData.setData('text/plain', window.cc_copyText);
|
||||
e.preventDefault();
|
||||
}
|
||||
window.cc_copyText = null;
|
||||
});
|
||||
);
|
||||
|
||||
/* Paste text (window.clipboardData is handled in interop_TryGetClipboardText instead) */
|
||||
EM_ASM(window.addEventListener('paste',
|
||||
function(e) {
|
||||
if (e.clipboardData) {
|
||||
var contents = e.clipboardData.getData('text/plain');
|
||||
ccall('Window_GotClipboardText', 'void', ['string'], [contents]);
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
void interop_TryGetClipboardText(void) {
|
||||
/* For IE11, use window.clipboardData to get the clipboard */
|
||||
EM_ASM_({
|
||||
if (window.clipboardData) {
|
||||
var contents = window.clipboardData.getData('Text');
|
||||
ccall('Window_StoreClipboardText', 'void', ['string'], [contents]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void interop_TrySetClipboardText(const char* text) {
|
||||
/* For IE11, use window.clipboardData to set the clipboard */
|
||||
/* For other browsers, instead use the window.copy events */
|
||||
EM_ASM_({
|
||||
if (window.clipboardData) {
|
||||
if (window.getSelection && window.getSelection().toString()) return;
|
||||
window.clipboardData.setData('Text', UTF8ToString($0));
|
||||
} else {
|
||||
window.cc_copyText = UTF8ToString($0);
|
||||
}
|
||||
}, text);
|
||||
}
|
||||
|
||||
void interop_EnterFullscreen(void) {
|
||||
/* emscripten sets css size to screen's base width/height, */
|
||||
/* except that becomes wrong when device rotates. */
|
||||
/* Better to just set CSS width/height to always be 100% */
|
||||
EM_ASM({
|
||||
var canvas = Module['canvas'];
|
||||
canvas.style.width = '100%';
|
||||
canvas.style.height = '100%';
|
||||
});
|
||||
|
||||
/* By default, pressing Escape will immediately exit fullscreen - which is */
|
||||
/* quite annoying given that it is also the Menu key. Some browsers allow */
|
||||
/* 'locking' the Escape key, so that you have to hold down Escape to exit. */
|
||||
/* NOTE: This ONLY works when the webpage is a https:// one */
|
||||
EM_ASM({ try { navigator.keyboard.lock(["Escape"]); } catch (ex) { } });
|
||||
}
|
||||
|
||||
/* Adjust from document coordinates to element coordinates */
|
||||
void interop_AdjustXY(int* x, int* y) {
|
||||
EM_ASM_({
|
||||
var canvasRect = Module['canvas'].getBoundingClientRect();
|
||||
HEAP32[$0 >> 2] = HEAP32[$0 >> 2] - canvasRect.left;
|
||||
HEAP32[$1 >> 2] = HEAP32[$1 >> 2] - canvasRect.top;
|
||||
}, x, y);
|
||||
}
|
||||
|
||||
void interop_RequestCanvasResize(void) {
|
||||
EM_ASM( if (typeof(resizeGameCanvas) === 'function') resizeGameCanvas(); );
|
||||
}
|
||||
|
||||
void interop_SetCursorVisible(int visible) {
|
||||
if (visible) {
|
||||
EM_ASM(Module['canvas'].style['cursor'] = 'default'; );
|
||||
} else {
|
||||
EM_ASM(Module['canvas'].style['cursor'] = 'none'; );
|
||||
}
|
||||
}
|
||||
|
||||
void interop_ShowDialog(const char* title, const char* msg) {
|
||||
EM_ASM_({ alert(UTF8ToString($0) + "\n\n" + UTF8ToString($1)); }, title, msg);
|
||||
}
|
||||
|
||||
void interop_OpenKeyboard(const char* text, int type, const char* placeholder) {
|
||||
EM_ASM_({
|
||||
var elem = window.cc_inputElem;
|
||||
if (!elem) {
|
||||
if ($1 == 1) {
|
||||
elem = document.createElement('input');
|
||||
elem.setAttribute('inputmode', 'decimal');
|
||||
} else {
|
||||
elem = document.createElement('textarea');
|
||||
}
|
||||
elem.setAttribute('style', 'position:absolute; left:0; bottom:0; margin: 0px; width: 100%');
|
||||
elem.setAttribute('placeholder', UTF8ToString($2));
|
||||
elem.value = UTF8ToString($0);
|
||||
|
||||
elem.addEventListener('input',
|
||||
function(ev) {
|
||||
ccall('Window_OnTextChanged', 'void', ['string'], [ev.target.value]);
|
||||
}, false);
|
||||
window.cc_inputElem = elem;
|
||||
|
||||
window.cc_divElem = document.createElement('div');
|
||||
window.cc_divElem.setAttribute('style', 'position:absolute; left:0; top:0; width:100%; height:100%; background-color: black; opacity:0.4; resize:none; pointer-events:none;');
|
||||
|
||||
window.cc_container.appendChild(window.cc_divElem);
|
||||
window.cc_container.appendChild(elem);
|
||||
}
|
||||
elem.focus();
|
||||
elem.click();
|
||||
}, text, type, placeholder);
|
||||
}
|
||||
|
||||
void interop_SetKeyboardText(const char* text) {
|
||||
EM_ASM_({
|
||||
if (!window.cc_inputElem) return;
|
||||
var str = UTF8ToString($0);
|
||||
|
||||
if (str == window.cc_inputElem.value) return;
|
||||
window.cc_inputElem.value = str;
|
||||
}, text);
|
||||
}
|
||||
|
||||
void interop_CloseKeyboard(void) {
|
||||
EM_ASM({
|
||||
if (!window.cc_inputElem) return;
|
||||
window.cc_container.removeChild(window.cc_divElem);
|
||||
window.cc_container.removeChild(window.cc_inputElem);
|
||||
window.cc_divElem = null;
|
||||
window.cc_inputElem = null;
|
||||
});
|
||||
}
|
||||
|
||||
void interop_OpenFileDialog(const char* filter) {
|
||||
EM_ASM_({
|
||||
var elem = window.cc_uploadElem;
|
||||
if (!elem) {
|
||||
elem = document.createElement('input');
|
||||
elem.setAttribute('type', 'file');
|
||||
elem.setAttribute('style', 'display: none');
|
||||
elem.accept = UTF8ToString($0);
|
||||
|
||||
elem.addEventListener('change',
|
||||
function(ev) {
|
||||
var files = ev.target.files;
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var reader = new FileReader();
|
||||
var name = files[i].name;
|
||||
|
||||
reader.onload = function(e) {
|
||||
var data = new Uint8Array(e.target.result);
|
||||
FS.createDataFile('/', name, data, true, true, true);
|
||||
ccall('Window_OnFileUploaded', 'void', ['string'], ['/' + name]);
|
||||
FS.unlink('/' + name);
|
||||
};
|
||||
reader.readAsArrayBuffer(files[i]);
|
||||
}
|
||||
window.cc_container.removeChild(window.cc_uploadElem);
|
||||
window.cc_uploadElem = null;
|
||||
}, false);
|
||||
window.cc_uploadElem = elem;
|
||||
window.cc_container.appendChild(elem);
|
||||
}
|
||||
elem.click();
|
||||
}, filter);
|
||||
}
|
||||
|
||||
|
||||
/*########################################################################################################################*
|
||||
*--------------------------------------------------------GLContext--------------------------------------------------------*
|
||||
*#########################################################################################################################*/
|
||||
void interop_GetGpuRenderer(char* buffer, int len) {
|
||||
EM_ASM_({
|
||||
var dbg = GLctx.getExtension('WEBGL_debug_renderer_info');
|
||||
var str = dbg ? GLctx.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : "";
|
||||
stringToUTF8(str, $0, $1);
|
||||
}, buffer, len);
|
||||
}
|
||||
#endif
|
Loading…
x
Reference in New Issue
Block a user