Skip to content

Commit 00665bb

Browse files
committed
feat(game): requestClientScreenshot support
1 parent f92a4b4 commit 00665bb

File tree

5 files changed

+113
-19
lines changed

5 files changed

+113
-19
lines changed

game/server/bootstrap.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createServer } from './koa-router';
22
import './export';
33
import { eventController } from './event';
4-
import { RequestUploadToken } from './types';
4+
import { RequestUploadToken, createRegularUploadData } from './types';
55
import { UploadStore } from './upload-store';
66

77
export const uploadStore = new UploadStore();
@@ -23,7 +23,7 @@ eventController<RequestUploadToken, string>(
2323
emitNet('screencapture:INTERNAL_uploadComplete', playerSource, JSON.stringify(response), correlationId);
2424
}
2525

26-
const token = uploadStore.addUpload({
26+
const token = uploadStore.addUpload(createRegularUploadData({
2727
callback: uploadCallback,
2828
isRemote: true,
2929
remoteConfig: {
@@ -36,7 +36,7 @@ eventController<RequestUploadToken, string>(
3636
dataType: 'blob',
3737
playerSource: ctx.source,
3838
correlationId: body.correlationId,
39-
});
39+
}));
4040

4141
return send(token);
4242
},

game/server/export.ts

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { uploadStore } from './bootstrap';
2-
import { CallbackFn, CaptureOptions, DataType } from './types';
2+
import { CallbackFn, CaptureOptions, DataType, ScreenshotBasicCallbackFn, createScreenshotBasicUploadData, createRegularUploadData } from './types';
3+
import { exportHandler } from './utils';
34

45
/* global.exports("serverCaptureStream", (source: number) => {
56
const token = router.addStream({
@@ -22,7 +23,7 @@ global.exports(
2223
(source: number, url: string, options: CaptureOptions, callback: CallbackFn, dataType: DataType = 'base64') => {
2324
if (!source) return console.error('source is required for serverCapture');
2425

25-
const token = uploadStore.addUpload({
26+
const token = uploadStore.addUpload(createRegularUploadData({
2627
callback: callback,
2728
isRemote: true,
2829
remoteConfig: {
@@ -31,7 +32,7 @@ global.exports(
3132
},
3233
url,
3334
dataType,
34-
});
35+
}));
3536

3637
emitNet('screencapture:captureScreen', source, token, options, dataType);
3738
},
@@ -49,13 +50,44 @@ global.exports(
4950
encoding: options.encoding ?? 'webp',
5051
};
5152

52-
const token = uploadStore.addUpload({
53+
const token = uploadStore.addUpload(createRegularUploadData({
5354
callback,
5455
isRemote: false,
55-
remoteConfig: opts, // Store options here so we have access to encoding
56+
remoteConfig: opts,
5657
dataType,
57-
});
58+
}));
5859

5960
emitNet('screencapture:captureScreen', source, token, opts, dataType);
6061
},
61-
);
62+
);
63+
64+
// screeenshot-basic backwards compatibility
65+
function requestClientScreenshot(source: number, options: CaptureOptions, callback: ScreenshotBasicCallbackFn) {
66+
if (!source) return console.error('source is required for requestClientScreenshot');
67+
68+
const opts = {
69+
...options,
70+
encoding: options.encoding ?? 'webp',
71+
};
72+
73+
const isBlob = options.fileName ? true : false;
74+
75+
const token = uploadStore.addUpload(createScreenshotBasicUploadData({
76+
callback,
77+
isRemote: false,
78+
remoteConfig: opts,
79+
dataType: isBlob ? 'blob' : 'base64',
80+
}));
81+
82+
emitNet('screencapture:captureScreen', source, token, opts, isBlob ? 'blob' : 'base64');
83+
}
84+
85+
global.exports(
86+
'requestClientScreenshot',
87+
(source: number, options: CaptureOptions, callback: ScreenshotBasicCallbackFn) => {
88+
requestClientScreenshot(source, options, callback);
89+
},
90+
);
91+
exportHandler("requestClientScreenshot", (source: number, options: CaptureOptions, callback: ScreenshotBasicCallbackFn) => {
92+
requestClientScreenshot(source, options, callback);
93+
});

game/server/koa-router.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Koa from 'koa';
22
import Router from '@koa/router';
3+
import { writeFileSync} from 'fs';
34

45
// @ts-ignore - no types
56
import { setHttpCallback } from '@citizenfx/http-wrapper';
@@ -30,7 +31,7 @@ export async function createServer(uploadStore: UploadStore) {
3031
ctx.response.append('Access-Control-Allow-Origin', '*');
3132
ctx.response.append('Access-Control-Allow-Methods', 'GET, POST');
3233

33-
const { callback, dataType, isRemote, remoteConfig, url, playerSource, correlationId } =
34+
const { callback, dataType, isRemote, remoteConfig, url, playerSource, correlationId, screenshotBasicCompatibility } =
3435
uploadStore.getUpload(token);
3536

3637
if (!ctx.files) {
@@ -41,30 +42,55 @@ export async function createServer(uploadStore: UploadStore) {
4142
const file = ctx.file;
4243

4344
try {
44-
// Get encoding from remoteConfig or default to 'webp'
4545
const encoding = remoteConfig?.encoding || 'webp';
46+
// base64 or buffer
4647
const buf = await buffer(dataType, file.buffer, encoding);
4748

4849
if (isRemote) {
4950
const response = await uploadFile(url, remoteConfig, buf, dataType);
5051

51-
// this is only when we return data back to the client
52-
if (playerSource && correlationId) {
53-
callback(response, playerSource, correlationId);
52+
if (screenshotBasicCompatibility) {
53+
(callback as any)(false, response);
5454
} else {
55-
callback(response);
55+
if (playerSource && correlationId) {
56+
(callback as any)(response, playerSource, correlationId);
57+
} else {
58+
(callback as any)(response);
59+
}
5660
}
5761
} else {
58-
callback(buf);
62+
if (screenshotBasicCompatibility) {
63+
// this will be a base64 string
64+
if (remoteConfig?.fileName) {
65+
const filename = saveFileToDisk(remoteConfig.fileName, buf);
66+
(callback as any)(false, filename);
67+
} else {
68+
(callback as any)(false, buf);
69+
}
70+
} else {
71+
(callback as any)(buf);
72+
}
5973
}
6074

6175
ctx.status = 200;
6276
ctx.body = { status: 'success' };
6377
} catch (err) {
6478
if (err instanceof Error) {
79+
if (screenshotBasicCompatibility) {
80+
(callback as any)(err.message, null);
81+
} else {
82+
(callback as any)(err);
83+
}
84+
6585
ctx.status = 500;
6686
ctx.body = { status: 'error', message: err.message };
6787
} else {
88+
if (screenshotBasicCompatibility) {
89+
(callback as any)('An unknown error occurred', null);
90+
} else {
91+
(callback as any)(new Error('An unknown error occurred'));
92+
}
93+
6894
ctx.status = 500;
6995
ctx.body = { status: 'error', message: 'An unknown error occurred' };
7096
}
@@ -195,3 +221,13 @@ function getMimeType(encoding: string): string {
195221
return 'image/webp';
196222
}
197223
}
224+
225+
function saveFileToDisk(fileName: string, data: string | Buffer) {
226+
try {
227+
writeFileSync(fileName, data);
228+
return fileName;
229+
} catch (err) {
230+
console.error('Error saving file to disk:', err);
231+
throw new Error('Error saving file to disk');
232+
}
233+
}

game/server/types.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,30 @@ export type DataType = 'base64' | 'blob';
33
type Encoding = 'webp' | 'jpg' | 'png';
44

55
export interface UploadData {
6-
callback: CallbackFn;
6+
callback: CallbackFn | ScreenshotBasicCallbackFn;
77
isRemote: boolean;
88
remoteConfig: CaptureOptions | null;
99
dataType: DataType;
1010
url?: string;
1111
playerSource?: number;
1212
correlationId?: string;
13+
screenshotBasicCompatibility?: boolean;
14+
}
15+
16+
export function createScreenshotBasicUploadData(params: Omit<UploadData, 'callback' | 'screenshotBasicCompatibility'> & { callback: ScreenshotBasicCallbackFn }): UploadData {
17+
return {
18+
...params,
19+
callback: params.callback,
20+
screenshotBasicCompatibility: true,
21+
};
22+
}
23+
24+
export function createRegularUploadData(params: Omit<UploadData, 'callback' | 'screenshotBasicCompatibility'> & { callback: CallbackFn }): UploadData {
25+
return {
26+
...params,
27+
callback: params.callback,
28+
screenshotBasicCompatibility: false,
29+
};
1330
}
1431

1532
export interface StreamUploadData {
@@ -31,14 +48,17 @@ export interface CaptureOptions {
3148
headers?: HeadersInit;
3249
formField?: string;
3350
filename?: string;
51+
// fuck me, but needed for screenshot-basic stuffies -- delete later :)
52+
fileName?: string;
3453
encoding?: string;
3554
maxWidth?: number;
3655
maxHeight?: number;
3756
}
3857

3958
export type CallbackFn = (data: unknown, _playerSource?: number, correlationId?: string) => void;
59+
export type ScreenshotBasicCallbackFn = (err: string | boolean, data: string) => void;
4060

41-
export interface CallbackData {
61+
export interface CallbackData {
4262
imageData: string | Buffer<ArrayBuffer>;
4363
dataType: string;
4464
}

game/server/utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// https://github.com/overextended/ox_target/blob/main/client/compat/qtarget.lua#L1
2+
export function exportHandler(exportName: string, func: (...args: any[]) => any): void {
3+
AddEventHandler(`__cfx_export_screenshot-basic_${exportName}`, (setCB: (cb: (...args: any[]) => any) => void) => {
4+
setCB(func);
5+
});
6+
}

0 commit comments

Comments
 (0)