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

[UI] Add importer cata warning #4250

Merged
merged 2 commits into from
May 7, 2024
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
126 changes: 76 additions & 50 deletions ui/core/components/importers.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
import { JsonObject } from '@protobuf-ts/runtime';

import { IndividualSimUI } from '../individual_sim_ui';
import { SimUI } from '../sim_ui';
import { TypedEvent } from '../typed_event';
import {
Class,
EquipmentSpec,
ItemSlot,
Glyphs,
ItemSpec,
Profession,
Race,
Spec,
} from '../proto/common';
import { Class, EquipmentSpec, Glyphs, ItemSlot, ItemSpec, Profession, Race, Spec } from '../proto/common';
import { IndividualSimSettings } from '../proto/ui';
import { Database } from '../proto_utils/database';
import { classNames, nameToClass, nameToRace, nameToProfession } from '../proto_utils/names';
import { classNames, nameToClass, nameToProfession, nameToRace } from '../proto_utils/names';
import { SimSettingCategories } from '../sim';
import { SimUI } from '../sim_ui';
import { classGlyphsConfig, talentSpellIdsToTalentString } from '../talents/factory';
import { GlyphConfig } from '../talents/glyphs_picker';
import { BaseModal } from './base_modal';
import { TypedEvent } from '../typed_event';
import { buf2hex, getEnumValues } from '../utils';
import { JsonObject } from '@protobuf-ts/runtime';
import { SimSettingCategories } from '../sim';
import { BaseModal } from './base_modal';

declare var pako: any;
declare let pako: any;

export abstract class Importer extends BaseModal {
protected readonly textElem: HTMLTextAreaElement;
Expand All @@ -39,13 +31,16 @@ export abstract class Importer extends BaseModal {
<textarea spellCheck="false" class="importer-textarea form-control"></textarea>
`;
this.footer!.innerHTML = `
${this.includeFile ? `
${
this.includeFile
? `
<label for="${uploadInputId}" class="importer-button btn btn-primary upload-button me-2">
<i class="fas fa-file-arrow-up"></i>
Upload File
</label>
<input type="file" id="${uploadInputId}" class="importer-upload-input d-none" hidden>
` : ''
`
: ''
}
<button class="importer-button btn btn-primary import-button">
<i class="fa fa-download"></i>
Expand All @@ -65,18 +60,27 @@ export abstract class Importer extends BaseModal {
}

this.importButton = this.rootElem.getElementsByClassName('import-button')[0] as HTMLButtonElement;
this.importButton.addEventListener('click', event => {
this.importButton.addEventListener('click', async () => {
try {
this.onImport(this.textElem.value || '');
} catch (error) {
alert('Import error: ' + error);
await this.onImport(this.textElem.value || '');
} catch (error: any) {
alert(`Import error:
${error?.message}`);
}
});
}

abstract onImport(data: string): void

protected async finishIndividualImport<SpecType extends Spec>(simUI: IndividualSimUI<SpecType>, charClass: Class, race: Race, equipmentSpec: EquipmentSpec, talentsStr: string, glyphs: Glyphs | null, professions: Array<Profession>): Promise<void> {
abstract onImport(data: string): Promise<void>;

protected async finishIndividualImport<SpecType extends Spec>(
simUI: IndividualSimUI<SpecType>,
charClass: Class,
race: Race,
equipmentSpec: EquipmentSpec,
talentsStr: string,
glyphs: Glyphs | null,
professions: Array<Profession>,
): Promise<void> {
const playerClass = simUI.player.getClass();
if (charClass != playerClass) {
throw new Error(`Wrong Class! Expected ${classNames.get(playerClass)} but found ${classNames.get(charClass)}!`);
Expand All @@ -103,10 +107,10 @@ export abstract class Importer extends BaseModal {
simUI.player.setTalentsString(eventID, talentsStr);
}
if (glyphs) {
simUI.player.setGlyphs(eventID, glyphs)
simUI.player.setGlyphs(eventID, glyphs);
}
if (professions.length > 0) {
simUI.player.setProfessions(eventID, professions)
simUI.player.setProfessions(eventID, professions);
}
});

Expand All @@ -115,21 +119,22 @@ export abstract class Importer extends BaseModal {
if (missingItems.length == 0 && missingEnchants.length == 0) {
alert('Import successful!');
} else {
alert('Import successful, but the following IDs were not found in the sim database:' +
(missingItems.length == 0 ? '' : '\n\nItems: ' + missingItems.join(', ')) +
(missingEnchants.length == 0 ? '' : '\n\nEnchants: ' + missingEnchants.join(', ')));
alert(
'Import successful, but the following IDs were not found in the sim database:' +
(missingItems.length == 0 ? '' : '\n\nItems: ' + missingItems.join(', ')) +
(missingEnchants.length == 0 ? '' : '\n\nEnchants: ' + missingEnchants.join(', ')),
);
}
}
}

interface UrlParseData {
settings: IndividualSimSettings,
categories: Array<SimSettingCategories>,
settings: IndividualSimSettings;
categories: Array<SimSettingCategories>;
}

// For now this just holds static helpers to match the exporter, so it doesn't extend Importer.
export class IndividualLinkImporter {

// Exclude UISettings by default, since most users don't intend to export those.
static readonly DEFAULT_CATEGORIES = getEnumValues(SimSettingCategories).filter(c => c != SimSettingCategories.UISettings) as Array<SimSettingCategories>;

Expand All @@ -148,7 +153,7 @@ export class IndividualLinkImporter {
return map;
})();

static tryParseUrlLocation(location: Location): UrlParseData|null {
static tryParseUrlLocation(location: Location): UrlParseData | null {
let hash = location.hash;
if (hash.length <= 1) {
return null;
Expand All @@ -170,8 +175,7 @@ export class IndividualLinkImporter {
if (urlParams.has(IndividualLinkImporter.CATEGORY_PARAM)) {
const categoryChars = urlParams.get(IndividualLinkImporter.CATEGORY_PARAM)!.split('');
exportCategories = categoryChars
.map(char => [...IndividualLinkImporter.CATEGORY_KEYS.entries()]
.find(e => e[1] == char))
.map(char => [...IndividualLinkImporter.CATEGORY_KEYS.entries()].find(e => e[1] == char))
.filter(e => e)
.map(e => e![0]);
}
Expand Down Expand Up @@ -230,10 +234,18 @@ export class Individual80UImporter<SpecType extends Spec> extends Importer {
`;
}

onImport(data: string) {
async onImport(data: string) {
const importJson = JSON.parse(data);

// Parse all the settings.
const charLevel = importJson?.character?.level;
const hasReforge = importJson?.items?.some((item: any) => !!item?.reforge);
const hasMastery = importJson?.stats?.masteryRating > 0;

if ((charLevel && charLevel > 80) || hasReforge || hasMastery) {
throwCataError();
}

const charClass = nameToClass((importJson?.character?.gameClass as string) || '');
if (charClass == Class.ClassUnknown) {
throw new Error('Could not parse Class!');
Expand All @@ -250,9 +262,9 @@ export class Individual80UImporter<SpecType extends Spec> extends Importer {
talentsStr = talentSpellIdsToTalentString(charClass, talentIds);
}

let equipmentSpec = EquipmentSpec.create();
const equipmentSpec = EquipmentSpec.create();
(importJson.items as Array<any>).forEach(itemJson => {
let itemSpec = ItemSpec.create();
const itemSpec = ItemSpec.create();
itemSpec.id = itemJson.id;
if (itemJson.enchant?.id) {
itemSpec.enchant = itemJson.enchant.id;
Expand Down Expand Up @@ -288,10 +300,15 @@ export class IndividualWowheadGearPlannerImporter<SpecType extends Spec> extends
`;
}

onImport(url: string) {
async onImport(url: string) {
const isCataUrl = url.includes('wowhead.com/cata');
if (isCataUrl) {
throwCataError();
}

const match = url.match(/www\.wowhead\.com\/wotlk\/gear-planner\/([a-z\-]+)\/([a-z\-]+)\/([a-zA-Z0-9_\-]+)/);
if (!match) {
throw new Error(`Invalid WCL URL ${url}, must look like "https://www.wowhead.com/wotlk/gear-planner/CLASS/RACE/XXXX"`);
throw new Error(`Invalid WowHead URL ${url}, must look like "https://www.wowhead.com/wotlk/gear-planner/CLASS/RACE/XXXX"`);
}

// Parse all the settings.
Expand All @@ -307,7 +324,7 @@ export class IndividualWowheadGearPlannerImporter<SpecType extends Spec> extends

const base64Data = match[3].replaceAll('_', '/').replaceAll('-', '+');
//console.log('Base64: ' + base64Data);
const data = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0))
const data = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0));
//console.log('Hex: ' + buf2hex(data));

// Binary schema
Expand Down Expand Up @@ -337,19 +354,19 @@ export class IndividualWowheadGearPlannerImporter<SpecType extends Spec> extends
// First byte in glyphs section seems to always be 0x30
cur = 1;
let hasGlyphs = false;
const d = "0123456789abcdefghjkmnpqrstvwxyz";
const d = '0123456789abcdefghjkmnpqrstvwxyz';
const glyphStr = String.fromCharCode(...glyphBytes);
const glyphIds = [0, 0, 0, 0, 0, 0];
while (cur < glyphBytes.length) {

// First byte for each glyph is 0x3z, where z is the glyph position.
// 0, 1, 2 are major glyphs, 3, 4, 5 are minor glyphs.
const glyphPosition = d.indexOf(glyphStr[cur]);
cur++;

// For some reason, wowhead uses the spell IDs for the glyphs and
// applies a ridiculous hashing scheme.
const spellId = 0 +
const spellId =
0 +
(d.indexOf(glyphStr[cur + 0]) << 15) +
(d.indexOf(glyphStr[cur + 1]) << 10) +
(d.indexOf(glyphStr[cur + 2]) << 5) +
Expand Down Expand Up @@ -390,7 +407,7 @@ export class IndividualWowheadGearPlannerImporter<SpecType extends Spec> extends
cur++;

const numGems = (gearBytes[cur] & 0b11100000) >> 5;
const highid = (gearBytes[cur] & 0b00011111);
const highid = gearBytes[cur] & 0b00011111;
cur++;

itemSpec.id = (highid << 16) + (gearBytes[cur] << 8) + gearBytes[cur + 1];
Expand All @@ -407,7 +424,7 @@ export class IndividualWowheadGearPlannerImporter<SpecType extends Spec> extends

for (let gemIdx = 0; gemIdx < numGems; gemIdx++) {
const gemPosition = (gearBytes[cur] & 0b11100000) >> 5;
const highgemid = (gearBytes[cur] & 0b00011111);
const highgemid = gearBytes[cur] & 0b00011111;
cur++;

const gemId = (highgemid << 16) + (gearBytes[cur] << 8) + gearBytes[cur + 1];
Expand Down Expand Up @@ -488,13 +505,17 @@ export class IndividualAddonImporter<SpecType extends Spec> extends Importer {
throw new Error('Could not parse Race!');
}

const professions = (importJson['professions'] as Array<{ name: string, level: number }>).map(profData => nameToProfession(profData.name));
const professions = (importJson['professions'] as Array<{ name: string; level: number }>).map(profData => nameToProfession(profData.name));
professions.forEach((prof, i) => {
if (prof == Profession.ProfessionUnknown) {
throw new Error(`Could not parse profession '${importJson['professions'][i]}'`);
}
});

if (importJson['glyphs']['prime']) {
throwCataError();
}

const talentsStr = (importJson['talents'] as string) || '';
const glyphsConfig = classGlyphsConfig[charClass];

Expand Down Expand Up @@ -524,12 +545,17 @@ export class IndividualAddonImporter<SpecType extends Spec> extends Importer {
}
}

const throwCataError = () => {
throw new Error(`WowSims does not support the Cata Pre-patch.
Please use: https://wowsims.github.io/cata/ instead`);
};

function glyphNameToID(glyphName: string, glyphsConfig: Record<number, GlyphConfig>): number {
if (!glyphName) {
return 0;
}

for (let glyphIDStr in glyphsConfig) {
for (const glyphIDStr in glyphsConfig) {
if (glyphsConfig[glyphIDStr].name == glyphName) {
return parseInt(glyphIDStr);
}
Expand Down
Loading
Loading