Skip to content
This repository has been archived by the owner on Mar 5, 2021. It is now read-only.

Commit

Permalink
Support new server-side build system
Browse files Browse the repository at this point in the history
  • Loading branch information
elisee committed Jul 1, 2016
1 parent 55659cb commit 766c721
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 109 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -1,5 +1,6 @@
**/node_modules
player/**/*.js
server/**/*.js

public/index.js
public/plugins.json
Expand Down
150 changes: 55 additions & 95 deletions player/src/index.ts
@@ -1,33 +1,23 @@
/// <reference path="../../../../typings/tsd.d.ts" />
/// <reference path="../../../../SupClient/typings/SupApp.d.ts" />

import * as async from "async";
import * as querystring from "querystring";
import supFetch from "../../../../SupClient/src/fetch";
import * as dummy_fs from "fs";
import * as dummy_path from "path";
import * as dummy_http from "http";

let isApp = window.navigator.userAgent.indexOf("Electron") !== -1;
let nodeRequire: NodeRequire;

let electron: Electron.ElectronMainAndRenderer;
let fs: typeof dummy_fs;
let path: typeof dummy_path;
let http: typeof dummy_http;

if (isApp) {
nodeRequire = (top as any).global.require;
electron = nodeRequire("electron");
fs = nodeRequire("fs");
path = nodeRequire("path");
http = nodeRequire("http");
}
import * as path from "path";

const isApp = window.navigator.userAgent.indexOf("Electron") !== -1;

let statusElt = document.querySelector(".status") as HTMLDivElement;
const statusElt = document.querySelector(".status") as HTMLDivElement;
let tempFolderPath: string;

let qs = querystring.parse(window.location.search.slice(1));
let buildPath = (qs.project != null) ? `/builds/${qs.project}/${qs.build}/` : "./";
const qs = querystring.parse(window.location.search.slice(1));
const buildPath = (qs.project != null) ? `/builds/${qs.project}/${qs.build}/` : "./";

document.addEventListener("keydown", (event) => {
// F12
if (event.keyCode === 123) SupApp.getCurrentWindow().webContents.toggleDevTools();
});

function start() {
if (!isApp) {
Expand All @@ -39,21 +29,30 @@ function start() {
document.body.addEventListener("click", (event) => {
if ((event.target as HTMLElement).tagName !== "A") return;
event.preventDefault();
electron.shell.openExternal((event.target as HTMLAnchorElement).href);
SupApp.openLink((event.target as HTMLAnchorElement).href);
});

if (localStorage["supLove2DPath"] == null || !fs.existsSync(localStorage["supLove2DPath"])) {
(document.querySelector(".where-is-love") as HTMLDivElement).hidden = false;
document.querySelector(".where-is-love button").addEventListener("click", onLocateLoveClick);
} else downloadGame();
if (localStorage["supLove2DPath"] == null) {
showLocateLove();
} else {
SupApp.tryFileAccess(localStorage["supLove2DPath"], "execute", (err) => {
if (err != null) { showLocateLove(); return; }
downloadGame();
});
}
}

function showLocateLove() {
(document.querySelector(".where-is-love") as HTMLDivElement).hidden = false;
document.querySelector(".where-is-love button").addEventListener("click", onLocateLoveClick);
}

function onLocateLoveClick(event: Event) {
electron.remote.dialog.showOpenDialog({ properties: ["openFile"] }, (files) => {
if (files == null || files.length === 0) return;
SupApp.chooseFile("execute", (file) => {
if (file == null) return;

(document.querySelector(".where-is-love") as HTMLDivElement).hidden = true;
localStorage["supLove2DPath"] = files[0];
localStorage["supLove2DPath"] = file;
downloadGame();
});
}
Expand All @@ -70,94 +69,55 @@ interface Entry {
children?: any[];
}

function createTempFolder(callback: (err: Error) => any) {
let tmpRoot = nodeRequire("os").tmpdir();

let characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
function randomChar() {
return characters[Math.floor(Math.random() * characters.length)];
}

async.retry(10, (cb: ErrorCallback) => {
let folderName = "sup-love2d-";
for (let i = 0; i < 16; i++) folderName += randomChar();

tempFolderPath = `${tmpRoot}/${folderName}`;
fs.mkdir(tempFolderPath, cb);
}, callback);
}

function downloadGame() {
statusElt.textContent = "Downloading game...";

createTempFolder((err) => {
SupApp.mktmpdir((err, createdFolderPath) => {
if (err != null) {
statusElt.textContent = `Could not create temporary folder: ${err.message}`;
return;
}

supFetch(`${buildPath}project.json`, "json", (err, project) => {
tempFolderPath = createdFolderPath;

supFetch(`${buildPath}files.json`, "json", (err, filesToDownload) => {
if (err != null) {
statusElt.textContent = `Could not load project: ${err.message}`;
statusElt.textContent = `Could not load files list: ${err.message}`;
return;
}

downloadAssets(project);
async.eachSeries(filesToDownload, downloadFile, () => { runGame(); });
});
});
}

function downloadAssets(project: Project) {
let foldersToCreate: string[] = [];
let assetsToLoad: string[] = [];
function downloadFile(filePath: string, callback: ErrorCallback) {
const inputPath = `${window.location.origin}${buildPath}files/${filePath}`;
const outputPath = path.join(tempFolderPath, filePath);

function walk(entry: Entry, parentPath: string) {
let assetPath = (parentPath.length > 0) ? `${parentPath}/${entry.name}` : entry.name;

if (entry.children != null) {
foldersToCreate.push(assetPath);
for (let child of entry.children) walk(child, assetPath);
} else {
assetsToLoad.push(assetPath);
}
}

for (let asset of project.assets) walk(asset, "");
async.eachSeries(foldersToCreate, createAssetFolder, () => {
async.eachSeries(assetsToLoad, downloadAsset, () => { runGame(); });
SupApp.mkdirp(path.dirname(outputPath), () => {
supFetch(inputPath, "arraybuffer", (err, data) => {
SupApp.writeFile(outputPath, Buffer.from(data), (err) => {
callback(null);
});
});
});
}

function createAssetFolder(folderPath: string, callback: ErrorCallback) {
let outputPath = path.join(tempFolderPath, folderPath);
fs.mkdir(outputPath, callback);
}

function downloadAsset(assetPath: string, callback: ErrorCallback) {
let inputPath = `${window.location.origin}${buildPath}assets/${assetPath}`;
let outputPath = path.join(tempFolderPath, assetPath);

http.get(inputPath, (response) => {
let localFile = fs.createWriteStream(outputPath);
localFile.on("finish", () => { callback(null); });
response.pipe(localFile);
}).on("error", callback);
}

function runGame() {
statusElt.textContent = "Running LÖVE...";
let childProcess = (top as any).global.require("child_process");
let loveProcess = childProcess.spawn(localStorage["supLove2DPath"], [ tempFolderPath ]);
electron.remote.getCurrentWindow().hide();

let failed = false;
loveProcess.on("error", (err: Error) => {
failed = true;
statusElt.textContent = `Could not start LÖVE: ${err.message}`;
localStorage.removeItem("supLove2DPath");
return;

SupApp.spawnChildProcess(localStorage["supLove2DPath"], [ tempFolderPath ], (err, loveProcess) => {
SupApp.getCurrentWindow().hide();

let failed = false;
loveProcess.on("error", (err: Error) => {
failed = true;
statusElt.textContent = `Could not start LÖVE: ${err.message}`;
localStorage.removeItem("supLove2DPath");
});
loveProcess.on("exit", () => { if (!failed) window.close(); });
});
loveProcess.on("exit", () => { if (!failed) window.close(); });
}

start();
20 changes: 12 additions & 8 deletions plugins/default/blob/data/BlobAsset.ts
Expand Up @@ -79,18 +79,22 @@ export default class BlobAsset extends SupCore.Data.Base.Asset {
});
}

publish(buildPath: string, callback: (err: Error) => any) {
if (this.pub.buffer == null) { callback (null); return; }
serverExport(buildPath: string, callback: (err: Error, writtenFiles: string[]) => void) {
if (this.pub.buffer == null) { callback (null, []); return; }

let pathFromId = this.server.data.entries.getPathFromId(this.id);
if (pathFromId.lastIndexOf(".") <= pathFromId.lastIndexOf("/")) {
let filePath = this.server.data.entries.getPathFromId(this.id);
if (filePath.lastIndexOf(".") <= filePath.lastIndexOf("/")) {
// No dots in the name, try adding a default extension
pathFromId += `.${defaultExtensions[this.pub.mediaType]}`;
filePath += `.${defaultExtensions[this.pub.mediaType]}`;
}

let outputPath = `${buildPath}/assets/${pathFromId}`;
let parentPath = outputPath.slice(0, outputPath.lastIndexOf("/"));
mkdirp(parentPath, () => { fs.writeFile(outputPath, this.pub.buffer, callback); });
const outputPath = `${buildPath}/files/${filePath}`;
mkdirp(path.dirname(outputPath), () => {
fs.writeFile(outputPath, this.pub.buffer, (err) => {
if (err != null) callback(err, null);
callback(null, [ filePath ]);
});
});
}

server_upload(client: any, mediaType: string, buffer: Buffer, callback: UploadCallback) {
Expand Down
17 changes: 11 additions & 6 deletions plugins/default/lua/data/LuaAsset.ts
Expand Up @@ -89,14 +89,19 @@ export default class LuaAsset extends SupCore.Data.Base.Asset {
});
}

publish(buildPath: string, callback: (err: Error) => any) {
serverExport(buildPath: string, callback: (err: Error, writtenFiles: string[]) => void) {
let pathFromId = this.server.data.entries.getPathFromId(this.id);
if (pathFromId.lastIndexOf(".lua") === pathFromId.length - 4) pathFromId = pathFromId.slice(0, -4);
let outputPath = `${buildPath}/assets/${pathFromId}.lua`;
let parentPath = outputPath.slice(0, outputPath.lastIndexOf("/"));

let text = this.pub.text;
mkdirp(parentPath, () => { fs.writeFile(outputPath, text, callback); });
const filePath = `${pathFromId}.lua`;
const outputPath = `${buildPath}/files/${filePath}`;

const text = this.pub.text;
mkdirp(path.dirname(outputPath), () => {
fs.writeFile(outputPath, text, (err) => {
if (err != null) { callback(err, null); return; }
callback(null, [ filePath ]);
});
});
}

server_editText(client: any, operationData: OperationData, revisionIndex: number, callback: EditTextCallback) {
Expand Down
23 changes: 23 additions & 0 deletions server/gulpfile.js
@@ -0,0 +1,23 @@
"use strict";

const gulp = require("gulp");

// TypeScript
const ts = require("gulp-typescript");
const tsProject = ts.createProject("./tsconfig.json");
const tslint = require("gulp-tslint");

gulp.task("typescript", function() {
let failed = false;
const tsResult = tsProject.src()
.pipe(tslint())
.pipe(tslint.report("prose", { emitError: true }))
.on("error", (err) => { throw err; })
.pipe(ts(tsProject))
.on("error", () => { failed = true; })
.on("end", () => { if (failed) throw new Error("There were TypeScript errors."); });
return tsResult.js.pipe(gulp.dest("./"));
});

// All
gulp.task("default", [ "typescript" ]);
1 change: 1 addition & 0 deletions server/index.d.ts
@@ -0,0 +1 @@
/// <reference path="../../../SupCore/SupCore.d.ts" />
37 changes: 37 additions & 0 deletions server/index.ts
@@ -0,0 +1,37 @@
import * as fs from "fs";
import * as async from "async";

interface ServerExportableAsset extends SupCore.Data.Base.Asset {
serverExport: (folderPath: string, callback: (err: Error, writtenFiles: string[]) => void) => void;
}

SupCore.system.serverBuild = (server: ProjectServer, buildPath: string, callback: (err: string) => void) => {
fs.mkdirSync(`${buildPath}/files`);

const assetIdsToExport: string[] = [];
server.data.entries.walk((entry: SupCore.Data.EntryNode, parent: SupCore.Data.EntryNode) => {
if (entry.type != null && server.system.data.assetClasses[entry.type].prototype.serverExport != null) assetIdsToExport.push(entry.id);
});

let files: string[] = [];

async.each(assetIdsToExport, (assetId, cb) => {
server.data.assets.acquire(assetId, null, (err: Error, asset: ServerExportableAsset) => {
asset.serverExport(buildPath, (err, writtenFiles) => {
server.data.assets.release(assetId, null);

files = files.concat(writtenFiles);
cb();
});
});
}, (err) => {
if (err != null) { callback("Could not export all assets"); return; }

const json = JSON.stringify(files, null, 2);
fs.writeFile(`${buildPath}/files.json`, json, { encoding: "utf8" }, (err) => {
if (err != null) { callback("Could not save files.json"); return; }

callback(null);
});
});
};
7 changes: 7 additions & 0 deletions server/tsconfig.json
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"noImplicitAny": true
}
}

0 comments on commit 766c721

Please sign in to comment.