@2003scape/rsc-client
Version:
runescape classic web client
910 lines (719 loc) • 25.7 kB
JavaScript
const BZLib = require('./bzlib');
const Color = require('./lib/graphics/color');
const Font = require('./lib/graphics/font');
const Graphics = require('./lib/graphics/graphics');
const Socket = require('./lib/net/socket');
const Surface = require('./surface');
const TGA = require('tga-js');
const Utility = require('./utility');
const keycodes = require('./lib/keycodes');
const version = require('./version');
const sleep = require('sleep-promise');
const CHAR_MAP =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!"\243$%^&' +
"*()-_=+[{]};:'@#~,<.>/?\\| ";
const FONTS = [
'h11p.jf',
'h12b.jf',
'h12p.jf',
'h13b.jf',
'h14b.jf',
'h16b.jf',
'h20b.jf',
'h24b.jf'
];
// using width: 0 bugs out on chrome
const INPUT_STYLES = {
position: 'absolute',
display: 'none',
padding: 0,
border: 0,
outline: 0,
opacity: 0,
fontFamily: 'sans'
};
function getMousePosition(canvas, e) {
const boundingRect = canvas.getBoundingClientRect();
const scaleX = canvas.width / boundingRect.width;
const scaleY = canvas.height / boundingRect.height;
return {
x: ((e.clientX - boundingRect.left) * scaleX) | 0,
y: ((e.clientY - boundingRect.top) * scaleY) | 0
};
}
class GameShell {
constructor(container) {
this._container = container;
this._container.style.position = 'relative';
this._canvas = document.createElement('canvas');
this._canvas.style.width = '100%';
this._canvas.style.height = '100%';
this._container.appendChild(this._canvas);
this._graphics = new Graphics(this._canvas);
this.options = {
middleClickCamera: false,
mouseWheel: false,
resetCompass: false,
zoomCamera: false,
showRoofs: true,
remainingExperience: false,
totalExperience: false,
wordFilter: true,
accountManagement: true,
fpsCounter: false,
retryLoginOnDisconnect: true,
mobile: false
};
this.middleButtonDown = false;
this.mouseScrollDelta = 0;
this.mouseActionTimeout = 0;
this.logoHeaderText = null;
this.mouseX = 0;
this.mouseY = 0;
this.mouseButtonDown = 0;
this.lastMouseButtonDown = 0;
this.timings = [];
this.resetTimings();
this.stopTimeout = 0;
this.interlaceTimer = 0;
this.loadingProgressPercent = 0;
this.imageLogo = null;
this.graphics = null;
this.appletWidth = 512;
this.appletHeight = 346;
this.targetFPS = 20;
this.maxDrawTime = 1000;
this.loadingStep = 1;
this.hasRefererLogoNotUsed = false;
this.loadingProgessText = 'Loading';
this.fontTimesRoman15 = new Font('Times New Roman', 0, 15);
this.fontHelvetica13b = new Font('Helvetica', Font.BOLD, 13);
this.fontHelvetica12 = new Font('Helvetica', 0, 12);
this.keyLeft = false;
this.keyRight = false;
this.keyUp = false;
this.keyDown = false;
this.keySpace = false;
this.keyHome = false;
this.keyPgUp = false;
this.keyPgDown = false;
this.ctrl = false;
this.threadSleep = 1;
this.interlace = false;
this.inputTextCurrent = '';
this.inputTextFinal = '';
this.inputPMCurrent = '';
this.inputPMFinal = '';
}
async startApplication(width, height, title) {
window.document.title = title;
this._canvas.tabIndex = 0;
this._canvas.width = width;
this._canvas.height = height;
this._container.style.width = `${width}px`;
this._container.style.height = `${height}px`;
console.log('Started application');
this.appletWidth = width;
this.appletHeight = height;
if (this.options.mobile) {
this._canvas.addEventListener('touchstart', (e) => {
//e.preventDefault();
console.log('touchstart');
if (e.touches.length === 1) {
clearTimeout(this.rightClickTimeout);
this.rightClickTimeout = setTimeout(() => {
this.disableEndClick = true;
e = {
button: 2,
clientX: e.touches[0].clientX,
clientY: e.touches[0].clientY
};
this.mousePressed(e);
this.mouseReleased(e);
}, 500);
} else {
// scroll
}
});
this._canvas.addEventListener('touchmove', (e) => {
//e.preventDefault();
console.log('touchmoving');
if (!this.touchMoving) {
clearTimeout(this.rightClickTimeout);
this.mousePressed({
button: 1,
clientX: e.touches[0].clientX,
clientY: e.touches[0].clientY
});
this.touchMoving = true;
} else {
this.mouseMoved({
clientX: e.touches[0].clientX,
clientY: e.touches[0].clientY
});
}
});
this._canvas.addEventListener('touchend', (e) => {
e.preventDefault();
console.log('touchend');
if (this.disableEndClick) {
this.disableEndClick = false;
return;
}
clearTimeout(this.rightClickTimeout);
e = {
button: this.touchMoving ? 1 : 0,
clientX: e.changedTouches[0].clientX,
clientY: e.changedTouches[0].clientY
};
if (this.touchMoving) {
this.touchMoving = false;
} else {
this.mousePressed(e);
}
this.mouseReleased(e);
});
} else {
this._canvas.addEventListener(
'mousedown',
this.mousePressed.bind(this)
);
window.addEventListener('mousemove', this.mouseMoved.bind(this));
window.addEventListener('mouseup', this.mouseReleased.bind(this));
}
//window.addEventListener('mouseout', this.mouseOut.bind(this));
this._canvas.addEventListener('wheel', this.mouseWheel.bind(this));
// prevent right clicks
this._canvas.addEventListener('contextmenu', (e) => {
e.preventDefault();
return false;
});
this._canvas.addEventListener('keydown', this.keyPressed.bind(this));
this._canvas.addEventListener('keyup', this.keyReleased.bind(this));
//window.addEventListener('beforeunload', () => this.onClosing());
if (this.options.mobile) {
this.toggleKeyboard = false;
// we can't re-use the mobileInput for passwords since browsers
// turn off autocomplete once it's switched back to text
this.mobileInput = document.createElement('input');
Object.assign(this.mobileInput.style, INPUT_STYLES);
this.mobilePassword = document.createElement('input');
this.mobilePassword.type = 'password';
Object.assign(this.mobilePassword.style, INPUT_STYLES);
this.mobileInput.addEventListener(
'keydown',
this.mobileKeyDown.bind(this)
);
this.mobilePassword.addEventListener(
'keydown',
this.mobileKeyDown.bind(this)
);
this.mobileInput.addEventListener(
'blur',
this.closeKeyboard.bind(this)
);
this.mobilePassword.addEventListener(
'blur',
this.closeKeyboard.bind(this)
);
this._container.appendChild(this.mobileInput);
this._container.appendChild(this.mobilePassword);
}
this.loadingStep = 1;
await this.run();
}
closeKeyboard() {
clearInterval(this.keyboardUpdateInterval);
this.mobileInputEl.style.display = 'none';
this._canvas.focus();
}
openKeyboard(type = 'text', text, maxLength, style) {
this.mobileInputCaret = -1;
this.lastMobileInput = text;
this.toggleKeyboard = true;
this.mobileInputEl =
type === 'password' ? this.mobilePassword : this.mobileInput;
this.mobileInputEl.value = text;
this.mobileInputEl.maxLength = maxLength;
this.mobileInputEl.style.display = 'block';
for (const [name, value] of Object.entries(style)) {
this.mobileInputEl.style[name] = value;
}
this.keyboardUpdateInterval = setInterval(() => {
this.mobileKeyboardUpdate();
}, 125);
}
mobileKeyDown(e) {
if (e.keyCode === keycodes.ENTER) {
this.closeKeyboard();
this.keyPressed(e);
}
}
mobileKeyboardUpdate() {
this.mobileInputCaret = this.mobileInputEl.selectionStart;
const newInput = this.mobileInputEl.value;
if (newInput === this.lastMobileInput) {
return;
}
for (let i = 0; i < this.lastMobileInput.length; i += 1) {
this.keyPressed({ keyCode: keycodes.BACKSPACE });
}
for (let i = 0; i < newInput.length; i += 1) {
this.keyPressed({ key: newInput[i] });
}
this.lastMobileInput = newInput;
}
keyPressed(e) {
const code = e.keyCode;
let charCode =
e.key && e.key.length === 1 ? e.key.charCodeAt(0) : 65535;
if (e.preventDefault) {
e.preventDefault();
}
if ([8, 10, 13, 9].includes(code)) {
charCode = code;
}
this.handleKeyPress(charCode);
if (code === keycodes.LEFT_ARROW) {
this.keyLeft = true;
} else if (code === keycodes.RIGHT_ARROW) {
this.keyRight = true;
} else if (code === keycodes.UP_ARROW) {
this.keyUp = true;
} else if (code === keycodes.DOWN_ARROW) {
this.keyDown = true;
} else if (code === keycodes.SPACE) {
this.keySpace = true;
} else if (code === keycodes.F1) {
this.interlace = !this.interlace;
} else if (code === keycodes.HOME) {
this.keyHome = true;
} else if (code === keycodes.PAGE_UP) {
this.keyPgUp = true;
} else if (code === keycodes.PAGE_DOWN) {
this.keyPgDown = true;
} else if (code === keycodes.CTRL) {
this.ctrl = true;
}
let foundText = false;
for (let i = 0; i < CHAR_MAP.length; i++) {
if (CHAR_MAP.charCodeAt(i) === charCode) {
foundText = true;
break;
}
}
if (foundText) {
if (this.inputTextCurrent.length < 20) {
this.inputTextCurrent += String.fromCharCode(charCode);
}
if (this.inputPMCurrent.length < 80) {
this.inputPMCurrent += String.fromCharCode(charCode);
}
}
if (code === keycodes.ENTER) {
this.inputTextFinal = this.inputTextCurrent;
this.inputPMFinal = this.inputPMCurrent;
} else if (code === keycodes.BACKSPACE) {
if (this.inputTextCurrent.length > 0) {
this.inputTextCurrent = this.inputTextCurrent.substring(
0,
this.inputTextCurrent.length - 1
);
}
if (this.inputPMCurrent.length > 0) {
this.inputPMCurrent = this.inputPMCurrent.substring(
0,
this.inputPMCurrent.length - 1
);
}
}
if (this.options.mobile) {
// inputs can only be focused when a user performs an action,
// and we set toggleKeyboard to true after an event has occured
setTimeout(() => {
if (!this.toggleKeyboard) {
return;
}
this.toggleKeyboard = false;
this.mobileInputEl.focus();
}, 125);
}
}
keyReleased(e) {
e.preventDefault();
const code = e.keyCode;
if (code === keycodes.LEFT_ARROW) {
this.keyLeft = false;
} else if (code === keycodes.RIGHT_ARROW) {
this.keyRight = false;
} else if (code === keycodes.UP_ARROW) {
this.keyUp = false;
} else if (code === keycodes.DOWN_ARROW) {
this.keyDown = false;
} else if (code === keycodes.SPACE) {
this.keySpace = false;
} else if (code === keycodes.HOME) {
this.keyHome = false;
} else if (code === keycodes.PAGE_UP) {
this.keyPgUp = false;
} else if (code === keycodes.PAGE_DOWN) {
this.keyPgDown = false;
} else if (code === keycodes.CTRL) {
this.ctrl = false;
}
}
mouseMoved(e) {
const { x, y } = getMousePosition(this._canvas, e);
this.mouseX = x;
this.mouseY = y;
this.mouseActionTimeout = 0;
}
mouseReleased(e) {
const { x, y } = getMousePosition(this._canvas, e);
this.mouseX = x;
this.mouseY = y;
this.mouseButtonDown = 0;
if (e.button === 1) {
this.middleButtonDown = false;
}
}
mouseOut(e) {
const { x, y } = getMousePosition(this._canvas, e);
this.mouseX = x;
this.mouseY = y;
this.mouseButtonDown = 0;
this.middleButtonDown = false;
}
mousePressed(e) {
if (e.button === 1 && e.preventDefault) {
e.preventDefault();
}
if (this.options.mobile) {
// inputs can only be focused when a user performs an action,
// and we set toggleKeyboard to true after an event has occured
setTimeout(() => {
if (!this.toggleKeyboard) {
return;
}
this.toggleKeyboard = false;
this.mobileInputEl.focus();
}, 125);
}
const { x, y } = getMousePosition(this._canvas, e);
this.mouseX = x;
this.mouseY = y;
if (this.options.middleClickCamera && e.button === 1) {
this.middleButtonDown = true;
this.originRotation = this.cameraRotation;
this.originMouseX = this.mouseX;
return;
}
if (e.metaKey || e.button === 2) {
this.mouseButtonDown = 2;
} else {
this.mouseButtonDown = 1;
}
this.lastMouseButtonDown = this.mouseButtonDown;
this.mouseActionTimeout = 0;
this.handleMouseDown(this.mouseButtonDown, x, y);
}
mouseWheel(e) {
if (
!this.options.mouseWheel ||
document.activeElement !== this._canvas
) {
return;
}
e.preventDefault();
if (e.deltaMode === 0) {
// deltaMode === 0 means deltaY/deltaY is given in pixels (chrome)
this.mouseScrollDelta = Math.floor(e.deltaY / 14);
} else if (e.deltaMode === 1) {
// deltaMode === 1 means deltaY/deltaY is given in lines (firefox)
this.mouseScrollDelta = Math.floor(e.deltaY);
}
return false;
}
setTargetFps(i) {
this.targetFPS = 1000 / i;
}
resetTimings() {
for (let i = 0; i < 10; i += 1) {
this.timings[i] = 0;
}
}
start() {
if (this.stopTimeout >= 0) {
this.stopTimeout = 0;
}
}
stop() {
if (this.stopTimeout >= 0) {
this.stopTimeout = 4000 / this.targetFPS;
}
}
async run() {
if (this.loadingStep === 1) {
this.loadingStep = 2;
this.graphics = this.getGraphics();
await this.loadJagex();
this.drawLoadingScreen(0, 'Loading...');
await this.startGame();
this.loadingStep = 0;
}
let i = 0;
let j = 256;
let delay = 1;
let i1 = 0;
for (let j1 = 0; j1 < 10; j1++) {
this.timings[j1] = Date.now();
}
while (this.stopTimeout >= 0) {
if (this.stopTimeout > 0) {
this.stopTimeout--;
if (this.stopTimeout === 0) {
this.onClosing();
return;
}
}
const k1 = j;
const lastDelay = delay;
j = 300;
delay = 1;
const time = Date.now();
if (this.timings[i] === 0) {
j = k1;
delay = lastDelay;
} else if (time > this.timings[i]) {
j = ((2560 * this.targetFPS) / (time - this.timings[i])) | 0;
}
if (j < 25) {
j = 25;
}
if (j > 256) {
j = 256;
delay = (this.targetFPS - (time - this.timings[i]) / 10) | 0;
if (delay < this.threadSleep) {
delay = this.threadSleep;
}
}
await sleep(delay);
this.timings[i] = time;
i = (i + 1) % 10;
if (delay > 1) {
for (let j2 = 0; j2 < 10; j2++) {
if (this.timings[j2] !== 0) {
this.timings[j2] += delay;
}
}
}
let k2 = 0;
while (i1 < 256) {
await this.handleInputs();
i1 += j;
if (++k2 > this.maxDrawTime) {
i1 = 0;
this.interlaceTimer += 6;
if (this.interlaceTimer > 25) {
this.interlaceTimer = 0;
this.interlace = true;
}
break;
}
}
this.interlaceTimer--;
i1 &= 0xff;
this.draw();
// calculate fps
this.fps = (1000 * j) / (this.targetFPS * 256);
this.mouseScrollDelta = 0;
}
}
async loadJagex() {
this.graphics.setColor(Color.black);
this.graphics.fillRect(0, 0, this.appletWidth, this.appletHeight);
const jagexJag = await this.readDataFile(
'jagex.jag',
'Jagex library',
0
);
if (jagexJag) {
const logoTga = Utility.loadData('logo.tga', 0, jagexJag);
this.imageLogo = this.parseTGA(logoTga);
}
const fontsJag = await this.readDataFile(
`fonts${version.FONTS}.jag`,
'Game fonts',
5
);
if (jagexJag) {
for (let i = 0; i < FONTS.length; i += 1) {
const fontName = FONTS[i];
Surface.createFont(Utility.loadData(fontName, 0, fontsJag), i);
}
}
}
drawLoadingScreen(percent, text) {
let x = ((this.appletWidth - 281) / 2) | 0;
let y = ((this.appletHeight - 148) / 2) | 0;
this.graphics.setColor(Color.black);
this.graphics.fillRect(0, 0, this.appletWidth, this.appletHeight);
if (!this.hasRefererLogoNotUsed) {
this.graphics.drawImage(this.imageLogo, x, y /*, this*/);
}
x += 2;
y += 90;
this.loadingProgressPercent = percent;
this.loadingProgessText = text;
this.graphics.setColor(new Color(132, 132, 132));
if (this.hasRefererLogoNotUsed) {
this.graphics.setColor(new Color(220, 0, 0));
}
this.graphics.drawRect(x - 2, y - 2, 280, 23);
this.graphics.fillRect(x, y, ((277 * percent) / 100) | 0, 20);
this.graphics.setColor(new Color(198, 198, 198));
if (this.hasRefererLogoNotUsed) {
this.graphics.setColor(new Color(255, 255, 255));
}
this.drawString(
this.graphics,
text,
this.fontTimesRoman15,
x + 138,
y + 10
);
if (!this.hasRefererLogoNotUsed) {
this.drawString(
this.graphics,
'Created by JAGeX - visit www.jagex.com',
this.fontHelvetica13b,
x + 138,
y + 30
);
this.drawString(
this.graphics,
'\u00a92001-2002 Andrew Gower and Jagex Ltd',
this.fontHelvetica13b,
x + 138,
y + 44
);
} else {
this.graphics.setColor(new Color(132, 132, 152));
this.drawString(
this.graphics,
'\u00a92001-2002 Andrew Gower and Jagex Ltd',
this.fontHelvetica12,
x + 138,
this.appletHeight - 20
);
}
// not sure where this would have been used. maybe to indicate a
// special client?
if (this.logoHeaderText) {
this.graphics.setColor(Color.white);
this.drawString(
this.graphics,
this.logoHeaderText,
this.fontHelvetica13b,
x + 138,
y - 120
);
}
}
showLoadingProgress(percent, text) {
const x = (((this.appletWidth - 281) / 2) | 0) + 2;
const y = (((this.appletHeight - 148) / 2) | 0) + 90;
this.loadingProgressPercent = percent;
this.loadingProgessText = text;
this.graphics.setColor(new Color(132, 132, 132));
if (this.hasRefererLogoNotUsed) {
this.graphics.setColor(new Color(220, 0, 0));
}
const progressWidth = ((277 * percent) / 100) | 0;
this.graphics.fillRect(x, y, progressWidth, 20);
this.graphics.setColor(Color.black);
this.graphics.fillRect(x + progressWidth, y, 277 - progressWidth, 20);
this.graphics.setColor(new Color(198, 198, 198));
if (this.hasRefererLogoNotUsed) {
this.graphics.setColor(new Color(255, 255, 255));
}
this.drawString(
this.graphics,
text,
this.fontTimesRoman15,
x + 138,
y + 10
);
}
drawString(graphics, string, font, x, y) {
graphics.setFont(font);
const { width, height } = graphics.ctx.measureText(string);
graphics.drawString(
string,
x - ((width / 2) | 0),
y + ((height / 4) | 0)
);
}
parseTGA(tgaBuffer) {
const tgaImage = new TGA();
tgaImage.load(new Uint8Array(tgaBuffer.buffer));
const canvas = tgaImage.getCanvas();
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
return imageData;
}
async readDataFile(file, description, percent) {
file = `./data204/${file}`;
this.showLoadingProgress(percent, `Loading ${description} - 0%`);
const fileDownloadStream = Utility.openFile(file);
const header = new Int8Array(6);
await fileDownloadStream.readFully(header, 0, 6);
const archiveSize =
((header[0] & 0xff) << 16) +
((header[1] & 0xff) << 8) +
(header[2] & 0xff);
const archiveSizeCompressed =
((header[3] & 0xff) << 16) +
((header[4] & 0xff) << 8) +
(header[5] & 0xff);
this.showLoadingProgress(percent, `Loading ${description} - 5%`);
let read = 0;
const archiveData = new Int8Array(archiveSizeCompressed);
while (read < archiveSizeCompressed) {
let length = archiveSizeCompressed - read;
if (length > 1000) {
length = 1000;
}
await fileDownloadStream.readFully(archiveData, read, length);
read += length;
this.showLoadingProgress(
percent,
`Loading ${description} - ` +
((5 + (read * 95) / archiveSizeCompressed) | 0) +
'%'
);
}
this.showLoadingProgress(percent, `Unpacking ${description}`);
if (archiveSizeCompressed !== archiveSize) {
const decompressed = new Int8Array(archiveSize);
BZLib.decompress(
decompressed,
archiveSize,
archiveData,
archiveSizeCompressed,
0
);
return decompressed;
}
return archiveData;
}
getGraphics() {
return this._graphics;
}
async createSocket(server, port) {
const socket = new Socket(server, port);
await socket.connect();
return socket;
}
}
module.exports = GameShell;