1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-13 02:01:51 +00:00

Add helper state from yorg3

This commit is contained in:
Jasper Meggitt 2020-06-02 10:48:30 -07:00
parent 87139872f7
commit 91ff34c4c5
14 changed files with 503 additions and 6 deletions

View File

@ -45,6 +45,8 @@ module.exports = ({ watch = false, standalone = false }) => {
G_BUILD_COMMIT_HASH: JSON.stringify(utils.getRevision()),
G_BUILD_VERSION: JSON.stringify(utils.getVersion()),
G_ALL_UI_IMAGES: JSON.stringify(utils.getAllResourceImages()),
// TODO: Get API endpoint from tobspr
G_API_ENDPOINT: JSON.stringify(lzString.compressToEncodedURIComponent("http://localhost:8000")),
}),
new CircularDependencyPlugin({

View File

@ -38,6 +38,8 @@ module.exports = ({
G_BUILD_COMMIT_HASH: JSON.stringify(utils.getRevision()),
G_BUILD_VERSION: JSON.stringify(utils.getVersion()),
G_ALL_UI_IMAGES: JSON.stringify(utils.getAllResourceImages()),
// TODO: Get API endpoint from tobspr
G_API_ENDPOINT: JSON.stringify(lzString.compressToEncodedURIComponent("https://api.shapez.io/v1")),
};
return {

View File

@ -28,6 +28,7 @@
@import "states/about";
@import "states/mobile_warning";
@import "states/changelog";
@import "states/mods";
@import "ingame_hud/buildings_toolbar";
@import "ingame_hud/building_placer";

109
src/css/states/mods.scss Normal file
View File

@ -0,0 +1,109 @@
#state_ModsState {
.mainContent {
pointer-events: all;
}
.devHint {
font-size: calc(13px*var(--ui-scale));
line-height: calc(17px*var(--ui-scale));
font-weight: 400;
letter-spacing: .04em;
color: #aaa;
.moddingDocsLink {
pointer-events: all;
cursor: pointer;
color: #26c6da;
}
}
.mod {
padding: calc(14px*var(--ui-scale));
display: grid;
grid-template-columns: auto auto 1fr auto;
grid-gap: calc(5px*var(--ui-scale));
grid-column-gap: calc(15px*var(--ui-scale));
.title {
font-size: calc(19px*var(--ui-scale));
line-height: calc(21px*var(--ui-scale));
font-weight: 400;
grid-column: 1 / 4;
grid-row: 1 / 2;
letter-spacing: .04em;
.version {
font-size: calc(13px*var(--ui-scale));
line-height: calc(17px*var(--ui-scale));
font-weight: 400;
letter-spacing: .04em;
color: #aaa;
}
}
.author {
grid-column: 1 / 2;
grid-row: 2 / 3;
color: #aaa;
}
.website {
font-size: calc(13px*var(--ui-scale));
line-height: calc(17px*var(--ui-scale));
font-weight: 400;
grid-column: 2 / 3;
grid-row: 2 / 3;
pointer-events: all;
cursor: pointer;
color: #26c6da;
}
.description {
grid-column: 1 / 5;
grid-row: 3 / 4;
border-top: 1px solid;
border-top-width: calc(1px*var(--ui-scale));
padding-top: calc(5px*var(--ui-scale));
}
.installedText, button.installMod, button.uninstallMod {
grid-column: 4 / 5;
grid-row: 1 / 3;
align-self: start;
}
.author, .gameChangingHint {
font-size: calc(13px*var(--ui-scale));
line-height: calc(17px*var(--ui-scale));
font-weight: 400;
letter-spacing: .04em;
}
.gameChangingHint {
grid-column: 1 / 5;
grid-row: 4 / 5;
color: #ef5072;
border-top: 1px solid;
border-top-width: calc(1px*var(--ui-scale));
padding-top: calc(5px*var(--ui-scale));
}
.installed {
opacity: .5;
}
.installCount, .installedText {
font-size: calc(13px*var(--ui-scale));
line-height: calc(17px*var(--ui-scale));
font-weight: 400;
letter-spacing: .04em;
color: #aaa;
}
.installedText {
text-transform: uppercase;
justify-self: end;
}
}
}

View File

@ -38,6 +38,7 @@ import { PreloadState } from "./states/preload";
import { SettingsState } from "./states/settings";
import { ModsState } from "./states/mods";
import { ModManager } from "./core/mod_manager";
import { BackendAPI } from "./core/backend";
const logger = createLogger("application");
@ -72,6 +73,7 @@ export class Application {
this.savegameMgr = new SavegameManager(this);
this.inputMgr = new InputDistributor(this);
this.backgroundResourceLoader = new BackgroundResourcesLoader(this);
this.api = new BackendAPI(this);
this.modManager = new ModManager(this);
// Platform dependent stuff

193
src/js/core/backend.js Normal file
View File

@ -0,0 +1,193 @@
/* typehints:start */
import { Application } from "../application";
/* typehints:end */
import { JSON_stringify } from "./builtins";
import { globalError, globalLog, globalWarn } from "./logging";
import { decodeHashedString } from "./sensitive_utils.encrypt";
import { globalConfig } from "./config";
import { BACKEND_ERRORS } from "./backend_errors";
import { RequestChannel } from "./request_channel";
export class BackendAPI {
constructor(app) {
/** @type {Application} */
this.app = app;
this.url = decodeHashedString(G_API_ENDPOINT);
// For testing
if (G_IS_DEV && window.location.hostname !== "localhost") {
this.url = "http://172.0.0.1:8000";
}
this.runningRequest = null;
this.requestChannel = new RequestChannel();
}
/**
* Retrieves the list of mods
*
* Route: /mods/gallery
* @returns {Promise<Array<any>>}
*/
fetchModGallery() {
return this.performRequest("GET", "/mods/gallery").then(res => {
if (!res.mods) {
throw BACKEND_ERRORS.badResponse;
}
return res.mods;
});
}
/**
* Retrieves the list of mods
*
* Route: /mods/track-download/:id
* @returns {Promise<Array<any>>}
*/
trackModDownload(id) {
return this.performRequest("POST", "/mods/track-download/" + id);
}
/**
* Formats an endpoint like '/user/profile' to a full url
* @param {string} endpoint
*/
getEndpointUrl(endpoint) {
assertAlways(endpoint.startsWith("/"), "Endpoint must start with '/'");
return this.url + endpoint;
}
/**
* Internal fetch helper
* @param {string} method
* @param {string} endpoint
* @param {Object.<string, any>} parameters
*/
internalFetch(method, endpoint, parameters) {
if (G_IS_DEV && globalConfig.debug.alwaysOffline) {
return Promise.reject("offline");
}
return (
fetch(this.getEndpointUrl(endpoint), {
method: method,
mode: "cors",
cache: "no-cache",
referrer: "no-referrer",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"X-App-Version": G_BUILD_VERSION,
},
credentials: "omit",
body: method === "POST" ? JSON_stringify(parameters) : undefined,
})
// Catch network errors / bad status codes
.catch(err => {
globalLog(this, "Network error:", err);
throw BACKEND_ERRORS.networkError;
})
// Check if the response was good
.then(response => {
if (!response.ok) {
return response.json().then(
data => {
if (data.error) {
globalWarn(this, "API request error:", data);
throw data.error;
}
globalWarn(this, "API response has no error payload:", data);
throw BACKEND_ERRORS.unknownError;
},
err => {
globalError(this, "API bad json:", err);
throw BACKEND_ERRORS.badResponse;
}
);
}
return Promise.resolve(response);
})
// JSON parsing
.then(response => {
try {
return response.json();
} catch (err) {
globalError(this, "API response json parsing error:", err);
throw BACKEND_ERRORS.badResponse;
}
})
// Check if error flag is set
.then(data => {
if (data.error) {
globalWarn(this, "API sent error:", data.error);
const str = new String(data.error);
// @ts-ignore
str.originalError = data;
throw str;
}
return data;
})
);
}
/**
* Performs a request
* @param {string} method
* @param {string} endpoint
* @param {Object.<string, any>=} parameters
*/
performRequest(method, endpoint, parameters = null) {
if (this.runningRequest) {
globalWarn(
this,
"Request to",
endpoint,
"queried while request to",
this.runningRequest,
"did not finish yet!"
);
}
assertAlways(method === "POST" || method === "GET", "Invalid mode");
globalLog(this, "🔗 " + method + " " + endpoint);
this.runningRequest = endpoint;
return this.requestChannel.watch(
new Promise((resolve, reject) => {
let timedOut = false;
let timeout = setTimeout(() => {
globalWarn("api", "Request to", endpoint, "timed out");
timedOut = true;
this.runningRequest = null;
reject(BACKEND_ERRORS.networkError);
}, 30000);
const request = this.internalFetch(method, endpoint, parameters);
request.then(
res => {
if (timedOut) {
globalWarn("api", "Request finished but already timed out");
} else {
this.runningRequest = null;
clearTimeout(timeout);
resolve(res);
}
},
err => {
if (timedOut) {
globalWarn("api", "Request finished with error but already timed out");
} else {
this.runningRequest = null;
clearTimeout(timeout);
reject(err);
}
}
);
})
);
}
}

View File

@ -0,0 +1,37 @@
import { T } from "../translations";
/**
* Translates error codes like "invalid-auth-key" into their translations (E.g. "Invalid Auth Key").
* If the code is not known or an arbitrary string, returns the code
* @param {string} err The error code
* @returns {string} Translated code
*/
export function tryTranslateBackendError(err) {
// TODO: Add errors to base-en.yaml
// return "<div class='backendError'>" + (T.backend.errors[err] || err) + "</div>";
return "<div class='backendError'> ☹" + err + "</div>";
}
export const BACKEND_ERRORS = {
// Frontend errors
networkError: "network-error",
offline: "offline",
badResponse: "bad-response",
rateLimited: "rate-limited",
// Backend errors
unsupportedVersion: "unsupported-version",
unknownError: "unknown-error",
unauthenticated: "unauthenticated",
invalidRequestSchema: "invalid-request-schema",
internalServerError: "internal-server-error",
databaseError: "database-error",
notFound: "not-found",
serverOverloaded: "server-overloaded",
// Binary checksum stuff
failedToDecompress: "failed-to-decompress",
checksumMissing: "checksum-missing",
checksumMismatch: "checksum-mismatch",
failedToParse: "failed-to-parse",
};

View File

@ -109,6 +109,7 @@ export const globalConfig = {
// instantBelts: true,
// instantProcessors: true,
// instantMiners: true,
// alwaysOffline: true,
// renderForTrailer: true,
/* dev:end */

View File

@ -1,6 +1,6 @@
import { ReadWriteProxy } from "./read_write_proxy";
import { ExplainedResult } from "./explained_result";
import { globalError, globalLog, globalWarn } from "./logging";
import { globalError, globalWarn } from "./logging";
import { ModApi } from "./mod_api";
import { queryParamOptions } from "./query_parameters";
@ -272,10 +272,13 @@ export class ModManager extends ReadWriteProxy {
this.currentData.mods.push(mod);
// TODO: Check if this use of trackUiClick is okay.
// Track download in the background
return this.writeAsync()
.then(() => this.app.analytics.trackUiClick("mod/" + id))
.then(() =>
this.app.api
.trackModDownload(id)
.catch(err => globalWarn(this, "Failed to track mod download:", err))
)
.then(() => (this.needsRestart = true))
.then(() => null);
}

1
src/js/globals.d.ts vendored
View File

@ -7,6 +7,7 @@ declare function assertAlways(condition: boolean | object | string, ...errorMess
declare const abstract: void;
declare const G_API_ENDPOINT: string;
declare const G_APP_ENVIRONMENT: string;
declare const G_HAVE_ASSERT: boolean;
declare const G_BUILD_TIME: number;

View File

@ -37,10 +37,10 @@ export class ModsState extends TextualGameState {
</span>
<strong class="category_label">${T.mods.installedMods}</strong>
<strong class="categoryLabel">${T.mods.installedMods}</strong>
<div class="installedMods"></div>
<strong class="category_label">${T.mods.modsBrowser}</strong>
<strong class="categoryLabel">${T.mods.modsBrowser}</strong>
<div class="modGallery"></div>
`;
}

View File

@ -1,5 +1,5 @@
import { GameState } from "../core/game_state";
import { createLogger } from "../core/logging";
import { createLogger, globalError } from "../core/logging";
import { findNiceValue, waitNextFrame } from "../core/utils";
import { cachebust } from "../core/cachebust";
import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper";
@ -173,6 +173,21 @@ export class PreloadState extends GameState {
});
})
.then(() => this.setStatus("Initializing mods"))
.then(() => {
return this.app.modManager.initialize().catch(err => {
globalError(this, "Failed to initialize mods:", err);
return new Promise(resolve => {
const { ok } = this.dialogs.showWarning(
T.global.error,
T.dialogs.modLoadFailDialog.content + "<br><br><b>" + err + "</b>",
["ok:good"]
);
ok.add(resolve);
});
});
})
.then(() => this.setStatus("Downloading resources"))
.then(() => {
return this.app.backgroundResourceLoader.getPromiseForBareGame();

View File

@ -0,0 +1,121 @@
import { TextualGameState } from "../core/textual_game_state";
import { T } from "../translations";
import { Savegame } from "../savegame/savegame";
export class SyncSavegameModsState extends TextualGameState {
constructor() {
super("SyncSavegameModsState");
}
getMainContentHTML() {
return `
<div class="comparisonDiv">
<span class="desc">${T.syncSavegameMods.desc}</span>
<div class="modsComparison">
<div class="savegameMods modsGroup">
<strong>${T.syncSavegameMods.savegameMods}</strong>
<div class="modsList">
<span>Mymod@1.0.0</span>
</div>
</div>
<div class="installedMods modsGroup">
<strong>${T.syncSavegameMods.yourMods}</strong>
<div class="modsList">
<span>Mymod@1.0.1</span>
</div>
</div>
</div>
<div class="buttonDiv">
<button class="styledButton cancel">${T.dialogs.buttons.cancel}</button>
<button class="styledButton syncMods">${T.syncSavegameMods.syncMods}</button>
</div>
</div>
`;
}
getShowDiamonds() {
return false;
}
onEnter(payload) {
/** @type {Savegame} */
const savegame = payload.savegame;
const nextStateId = payload.nextStateId;
const nextStatePayload = payload.nextStatePayload;
this.targetMods = savegame.getInstalledMods();
if (this.app.modManager.needsRestart) {
this.containerElement.classList.add("loading");
const { cancel, restart } = this.dialogs.showWarning(
T.syncSavegameMods.dialog.needs_restart.title,
T.syncSavegameMods.dialog.needs_restart.desc,
this.app.platformWrapper.getSupportsRestart()
? ["cancel:bad", "restart:misc"]
: ["cancel:bad"]
);
cancel.add(() => this.moveToState("MainMenuState"));
if (restart) {
restart.add(() => this.app.platformWrapper.performRestart());
}
return;
}
const renderMods = (mods, targetId) => {
const targetElement = this.containerElement.querySelector(targetId);
const filteredMods = mods.filter(mod => mod.is_game_changing);
if (filteredMods.length > 0) {
targetElement.innerHTML = filteredMods
.map(m => "<strong>" + m.name + "@" + m.version + "</strong>")
.join("");
} else {
targetElement.innerHTML = `<span class='noMods'>${T.syncSavegameMods.no_game_changing_mods}</span>`;
}
};
renderMods(this.app.modManager.getMods(), ".installedMods .modsList");
renderMods(savegame.getInstalledMods(), ".savegameMods .modsList");
this.trackClicks(this.htmlElement.querySelector("button.cancel"), this.onBackButton);
this.trackClicks(this.htmlElement.querySelector("button.syncMods"), this.doSyncMods);
}
doSyncMods() {
const closeLoading = this.dialogs.showLoadingDialog();
this.app.modManager.syncModsFromSavegame(this.targetMods).then(
() => {
closeLoading();
const { cancel, restart } = this.dialogs.showWarning(
T.syncSavegameMods.dialog_synced.title,
T.syncSavegameMods.dialog_synced.desc,
this.app.platformWrapper.getSupportsRestart()
? ["cancel:bad", "restart:misc"]
: ["cancel:bad"]
);
cancel.add(this.onBackButton, this);
if (restart) {
restart.add(() => this.app.platformWrapper.performRestart());
}
},
err => {
closeLoading();
const { ok } = this.dialogs.showWarning(T.global.error, err);
ok.add(this.onBackButton, this);
}
);
}
onBackButton() {
this.moveToState("SingleplayerOverviewState");
}
}

View File

@ -97,6 +97,12 @@ mods:
title: Mod Removed
desc: The mod has successfully beeen removed and will get unloaded after a restart.
syncSavegameMods:
desc: You need
savegameMods: Savegame Mods
yourMods: Loaded Mods
syncMods: Sync Mods
demoBanners:
# This is the "advertisement" shown in the main menu and other various places
@ -243,6 +249,10 @@ dialogs:
title: New Marker
desc: Give it a meaningful name
modLoadFailDialog:
content: >-
Failed to initialize mods:
ingame:
# This is shown in the top left corner and displays useful keybindings in
# every situation