Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge 2.7.8 to Preview #2524

Merged
merged 15 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ jobs:
rm -fR ./dist/mac
rm -fR ./dist/mac-arm64

- uses: actions/upload-artifact@v3.1.2
- uses: actions/upload-artifact@v4
with:
path: dist
name: Dist-${{ matrix.packCmd }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ jobs:
ESIGNER_PASSWORD: ${{ secrets.ESIGNER_PASSWORD }}
ESIGNER_TOTP_SECRET: ${{ secrets.ESIGNER_TOTP_SECRET }}

- uses: actions/upload-artifact@v3.1.2
- uses: actions/upload-artifact@v4
with:
path: dist
name: Dist-${{ matrix.packCmd }}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "NineChronicles",
"productName": "Nine Chronicles",
"version": "2.7.6",
"version": "2.7.7",
"description": "Game Launcher for Nine Chronicles",
"author": "Planetarium <[email protected]>",
"license": "GPL-3.0",
Expand Down
1 change: 0 additions & 1 deletion src/interfaces/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export interface IConfig {
TrayOnClose: boolean;
Planet: string;
PlanetRegistryUrl: string;
PlayerUpdateRetryCount: number;
PatrolRewardServiceUrl: string;
MeadPledgePortalUrl: string;
SeasonPassServiceUrl: string;
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type RpcEndpoints = {
"world-boss.rest"?: string[];
"patrol-reward.gql"?: string[];
"guild.rest"?: string[];
"arena.gql"?: string[];
"arena.rest"?: string[];
};

export type Planet = {
Expand Down
99 changes: 77 additions & 22 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ let gameNode: ChildProcessWithoutNullStreams | null = null;
const client = new NTPClient("time.google.com", 123, { timeout: 5000 });

let registry: Planet[];
let accessiblePlanets: Planet[];
let remoteNode: NodeInfo;
let geoBlock: { ip: string; country: string; isWhitelist?: boolean };

Expand All @@ -76,6 +77,10 @@ const updateOptions: IUpdateOptions = {
downloadStarted: quitAllProcesses,
};

/**
* NTP Check to adapt on thai calendar system
* https://snack.planetarium.dev/kor/2020/02/thai-in-2562/
*/
client
.syncTime()
.then((time) => {
Expand All @@ -94,6 +99,10 @@ client
console.error(error);
});

/**
* Prevent launcher run concurrently and manage deep link event when running launcher already exists
* To prevent other conditional logic making deep link behavior irregular this should be called as early as possible.
*/
if (!app.requestSingleInstanceLock()) {
app.quit();
} else {
Expand All @@ -108,6 +117,12 @@ if (!app.requestSingleInstanceLock()) {

cleanUp();

/** Install rosetta on AArch64
* native ARM64 launcher complicates distribution and build (both game and launcher),
* only x86 binary is served for both game and launcher.
* Rosetta installation is crucial step since 'missing rosetta error' is silent on both main and renderer,
* which makes very hard to debug.
*/
if (process.platform === "darwin" && process.arch == "arm64") {
exec("/usr/bin/arch -arch x86_64 uname -m", (error) => {
if (error) {
Expand All @@ -129,6 +144,7 @@ if (!app.requestSingleInstanceLock()) {

async function initializeConfig() {
try {
// Start of config.json fetch flow, can be mutated safely until finalization
const res = await axios(REMOTE_CONFIG_URL);
const remoteConfig: IConfig = res.data;

Expand All @@ -151,19 +167,26 @@ async function initializeConfig() {
const data = await fetch(remoteConfig.PlanetRegistryUrl);

registry = await data.json();

/** Planet Registry Failsafe
* if registry not exists or failed to fetch, throw 'parse failure'
* if registry fetched but the format is invalid, throw 'registry empty'
* if registry fetched correctly but matching entry with ID in config.json not exists, use first planet available from parsed data.
*/
if (registry === undefined) throw Error("Failed to parse registry.");
if (!Array.isArray(registry) || registry.length <= 0) {
throw Error("Registry is empty or invalid. No planets found.");
}
accessiblePlanets = await filterAccessiblePlanets(registry);

const planet =
registry.find((v) => v.id === remoteConfig.Planet) ??
accessiblePlanets.find((v) => v.id === remoteConfig.Planet) ??
(() => {
console.log(
"No matching PlanetID found in registry. Using the first planet.",
);
remoteConfig.Planet = registry[0].id;
return registry[0];
remoteConfig.Planet = accessiblePlanets[0].id;
return accessiblePlanets[0];
})();

remoteNode = await initializeNode(planet.rpcEndpoints, true);
Expand All @@ -178,15 +201,11 @@ async function initializeConfig() {
return;
}

// Replace config
console.log("Replace config with remote config:", remoteConfig);
remoteConfig.Locale = getConfig("Locale");
remoteConfig.PlayerUpdateRetryCount = getConfig(
"PlayerUpdateRetryCount",
0,
);
remoteConfig.TrayOnClose = getConfig("TrayOnClose", true);

// config finalized at this point
configStore.store = remoteConfig;
console.log("Initialize config complete");
} catch (error) {
Expand All @@ -201,6 +220,7 @@ async function initializeConfig() {
async function initializeApp() {
console.log("initializeApp");

// set default protocol to OS, so that launcher can be executed via protocol even if launcher is off.
const isProtocolSet = app.setAsDefaultProtocolClient(
"ninechronicles-launcher",
process.execPath,
Expand All @@ -211,15 +231,18 @@ async function initializeApp() {
console.log("isProtocolSet", isProtocolSet);

app.on("ready", async () => {
// electron-remote initialization.
// As this impose security considerations, we should remove this ASAP.
remoteInitialize();

// Renderer is initialized at this very moment.
win = await createV2Window();
await initGeoBlocking();

process.on("uncaughtException", async (error) => {
if (error.message.includes("system error -86")) {
console.error("System error -86 error occurred:", error);

// system error -86 : unknown arch, missing rosetta, failed to execute x86 program.
if (win) {
await dialog
.showMessageBox(win, {
Expand Down Expand Up @@ -251,8 +274,8 @@ async function initializeApp() {
setV2Quitting(!getConfig("TrayOnClose"));

if (useUpdate) {
appUpdaterInstance = new AppUpdater(win, baseUrl, updateOptions);
initCheckForUpdateWorker(win, appUpdaterInstance);
appUpdaterInstance = new AppUpdater(win, baseUrl, updateOptions); // Launcher Updater
initCheckForUpdateWorker(win, appUpdaterInstance); // Game Updater
}

webEnable(win.webContents);
Expand Down Expand Up @@ -285,18 +308,11 @@ function initializeIpc() {
}

if (utils.getExecutePath() === "PLAYER_UPDATE") {
configStore.set(
// Update Retry Counter
"PlayerUpdateRetryCount",
configStore.get("PlayerUpdateRetryCount") + 1,
);
return manualPlayerUpdate();
}

const node = utils.execute(utils.getExecutePath(), info.args);
if (node !== null) {
configStore.set("PlayerUpdateRetryCount", 0);
}

node.on("close", (code) => {
// Code 21: ERROR_NOT_READY
if (code === 21) {
Expand All @@ -320,7 +336,6 @@ function initializeIpc() {
ipcMain.handle("execute launcher update", async (event) => {
if (appUpdaterInstance === null) throw Error("appUpdaterInstance is null");
setV2Quitting(true);
configStore.set("PlayerUpdateRetryCount", 0);
await appUpdaterInstance.execute();
});

Expand Down Expand Up @@ -367,13 +382,16 @@ function initializeIpc() {
});

ipcMain.handle("get-planetary-info", async () => {
while (!registry || !remoteNode) {
// Synchronously wait until registry / remote node initialized
// This should return, otherwise entry point of renderer will stuck in white screen.
while (!registry || !remoteNode || !accessiblePlanets) {
await utils.sleep(100);
}
return [registry, remoteNode];
return [registry, remoteNode, accessiblePlanets];
});

ipcMain.handle("check-geoblock", async () => {
// synchronously wait until 'await initGeoBlocking();' finished
while (!geoBlock) {
await utils.sleep(100);
}
Expand Down Expand Up @@ -483,6 +501,7 @@ function initCheckForUpdateWorker(
throw new NotSupportedPlatformError(process.platform);
}

// Fork separated update checker worker process
const checkForUpdateWorker = fork(
path.join(__dirname, "./checkForUpdateWorker.js"),
[],
Expand Down Expand Up @@ -552,6 +571,8 @@ async function initGeoBlocking() {
return geoBlock.country;
} catch (error) {
console.error("Failed to fetch geo data:", error);
// Fallback to latest result stored in renderer-side local storage.
// defaults to the most strict condition if both remote and local value not exists.
win?.webContents
.executeJavaScript('localStorage.getItem("country")')
.then((result) => {
Expand All @@ -562,3 +583,37 @@ async function initGeoBlocking() {
});
}
}

async function filterAccessiblePlanets(planets: Planet[]): Promise<Planet[]> {
const accessiblePlanets: Planet[] = [];

for (const planet of planets) {
const endpoints = Object.values(planet.rpcEndpoints["headless.gql"]).flat();
// GraphQL 쿼리 정의
const query = `
query {
nodeStatus {
bootstrapEnded
}
}
`;
// 모든 endpoint에 대해 병렬로 요청을 보냅니다.
const requests = endpoints.map((endpoint) =>
axios.post(endpoint, { query }).then(
(response) => response.status === 200,
() => false, // 요청 실패 시 false 반환
),
);

try {
const results = await Promise.all(requests);
if (results.some((isAccessible) => isAccessible)) {
accessiblePlanets.push(planet);
}
} catch (error) {
console.error(`Error checking endpoints for planet ${planet.id}:`, error);
}
}

return accessiblePlanets;
}
8 changes: 0 additions & 8 deletions src/main/update/player-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,6 @@ export async function performPlayerUpdate(
return;
}

if (get("PlayerUpdateRetryCount", 0) > 3) {
console.error("[ERROR] Player Update Failed 3 Times.");
win.webContents.send("go to error page", "player", {
url: "reinstall",
});
return;
}

try {
lockfile.lockSync(lockfilePath);
console.log(
Expand Down
15 changes: 13 additions & 2 deletions src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,22 @@ import { NodeInfo } from "src/config";
function App() {
const { planetary, account, game } = useStore();
const client = useApolloClient();

/** Asynchronous Invoke in useEffect
* As ipcRenderer.invoke() is async we're not guaranteed to receive IPC result on time
*
* Also even if we use .then() to force synchronous flow useEffect() won't wait.
* But we need these to render login page.
* hence we render null until all three initialized;
* Planetary, GQL client, AccountStore
*
* It could be better if we can have react suspense here.
*/
useEffect(() => {
ipcRenderer
.invoke("get-planetary-info")
.then((info: [Planet[], NodeInfo]) => {
planetary.init(info[0], info[1]);
.then((info: [Planet[], NodeInfo, Planet[]]) => {
planetary.init(info[0], info[1], info[2]);
});
ipcRenderer
.invoke("check-geoblock")
Expand Down
8 changes: 7 additions & 1 deletion src/renderer/views/LoginView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,13 @@ function LoginView() {
label="Planet"
>
{planetary.registry.map((entry) => (
<SelectOption key={entry.id} value={entry.id}>
<SelectOption
key={entry.id}
value={entry.id}
disabled={
!planetary.accessiblePlanets.some((p) => p.id === entry.id)
}
>
{entry.name}
</SelectOption>
))}
Expand Down
8 changes: 7 additions & 1 deletion src/renderer/views/RegisterView/GetPatronView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,13 @@ function GetPatronView() {
label="Planet"
>
{planetary.registry.map((entry) => (
<SelectOption key={entry.id} value={entry.id}>
<SelectOption
key={entry.id}
value={entry.id}
disabled={
!planetary.accessiblePlanets.some((p) => p.id === entry.id)
}
>
{entry.name}
</SelectOption>
))}
Expand Down
7 changes: 5 additions & 2 deletions src/stores/planetary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ export default class PlanetaryStore {
}

@action
public init(registry: Planet[], node: NodeInfo) {
public init(registry: Planet[], node: NodeInfo, accessiblePlanets: Planet[]) {
this.registry = registry;
this.accessiblePlanets = accessiblePlanets;
this.setPlanet(get("Planet", "0x000000000000"));
this.setNode(node);
}
Expand All @@ -25,6 +26,8 @@ export default class PlanetaryStore {
registry!: Planet[];
@observable
planet!: Planet;
@observable
accessiblePlanets!: Planet[];

@action
public setRegistry(registry: Planet[]) {
Expand Down Expand Up @@ -67,7 +70,7 @@ export default class PlanetaryStore {
const planet = this.getPlanetById(planetID);
if (planet === undefined) {
console.error("No matching planet ID found, Using Default.");
this.planet = this.registry[0];
this.planet = this.accessiblePlanets[0];
} else this.planet = planet;
this.updateConfigToPlanet();
}
Expand Down
Loading