Skip to content
Open
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
61 changes: 40 additions & 21 deletions src/logic/Inventory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { OptionDefs, TypedOptions } from '../permalink/SettingsTypes';
import _ from 'lodash';
import { TypedOptions } from '../permalink/SettingsTypes';
import { BitVector } from './bitlogic/BitVector';
import { itemName, Logic } from './Logic';
import {
Expand Down Expand Up @@ -91,7 +92,12 @@ export function isItem(id: string): id is InventoryItem {
* Returns a BitVector containing all the expressions that should be visible in the tooltips
* and not recursively expanded (items and various item-like requirements).
*/
export function getTooltipOpaqueBits(logic: Logic, options: OptionDefs, settings: TypedOptions, expertMode: boolean, consideredTricks: Set<string>) {
export function getTooltipOpaqueBits(
logic: Logic,
settings: TypedOptions,
expertMode: boolean,
consideredTricks: Set<string>,
) {
const items = new BitVector();
const set = (id: string) => {
const bit = logic.itemBits[id];
Expand All @@ -102,28 +108,31 @@ export function getTooltipOpaqueBits(logic: Logic, options: OptionDefs, settings
}
};

for (const option of options) {
for (const [req, condition] of Object.entries(logic.optionConditions)) {
if (
option.type === 'multichoice' &&
(option.command === 'enabled-tricks-glitched' ||
option.command === 'enabled-tricks-bitless')
condition.type === 'query' &&
condition.op === 'in' &&
(condition.option === 'enabled-tricks-glitched' ||
condition.option === 'enabled-tricks-bitless')
) {
const vals = option.choices;
for (const opt of vals) {
const considered =
settings[option.command].includes(opt) ||
(expertMode &&
(!consideredTricks.size || consideredTricks.has(opt)));
if (considered) {
set(`${opt} Trick`);
}
const value = condition.value as string;
const considered =
settings[condition.option].includes(value) ||
(expertMode &&
(!consideredTricks.size || consideredTricks.has(value)));
if (considered) {
set(req);
}
}
}

// All actual inventory items are shown in the tooltips
for (const [item, count] of Object.entries(itemMaxes)) {
if (count === undefined || item === 'Sailcloth' || item === 'Tumbleweed') {
if (
count === undefined ||
item === 'Sailcloth' ||
item === 'Tumbleweed'
) {
continue;
}
if (item === sothItemReplacement) {
Expand Down Expand Up @@ -151,14 +160,24 @@ export function getTooltipOpaqueBits(logic: Logic, options: OptionDefs, settings
set(cubeItem);
}

// No point in revealing that the math behind 80 crystals is 13*5+15
for (const amt of [5, 10, 30, 40, 50, 70, 80]) {
set(`\\${amt} Gratitude Crystals`);
if (_.isEmpty(logic.counterThresholds)) {
// No point in revealing that the math behind 80 crystals is 13*5+15
for (const amt of [5, 10, 30, 40, 50, 70, 80]) {
set(`\\${amt} Gratitude Crystals`);
}
} else {
for (const counter of Object.keys(logic.counterThresholds)) {
set(counter);
}
}

if (settings['gondo-upgrades'] === false) {
set('\\Skyloft\\Central Skyloft\\Bazaar\\Gondo\'s Upgrades\\Upgrade to Quick Beetle');
set('\\Skyloft\\Central Skyloft\\Bazaar\\Gondo\'s Upgrades\\Upgrade to Tough Beetle');
set(
"\\Skyloft\\Central Skyloft\\Bazaar\\Gondo's Upgrades\\Upgrade to Quick Beetle",
);
set(
"\\Skyloft\\Central Skyloft\\Bazaar\\Gondo's Upgrades\\Upgrade to Tough Beetle",
);
}

return items;
Expand Down
110 changes: 110 additions & 0 deletions src/logic/LegacySupport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Sometimes the rando adds things to the dump that were
* previously hardcoded in the tracker. This file contains
* that data so that we can essentially migrate older but still
* supported releases to the new format and reduce the number
* of special cases we have to handle in core tracker code.
*/

import { MultiChoiceOption, OptionDefs, TypedOptions } from '../permalink/SettingsTypes';
import { dungeonNames } from './Locations';
import { Logic } from './Logic';
import { OptionQuery, RawLogic } from './UpstreamTypes';

const m = <K extends keyof TypedOptions>(
option: K,
op: OptionQuery['op'],
value: Exclude<TypedOptions[K], undefined>,
negation = false,
): OptionQuery => {
return {
type: 'query',
op,
option,
value,
negation,
};
};

const legacyHardcodedOptions: RawLogic['options'] = {
'Open Thunderhead option': m('open-thunderhead', 'eq', 'Open'),
'Open ET option': m('open-et', 'eq', true),
'Open LMF option': m('open-lmf', 'eq', 'Open'),
'LMF Nodes On option': m('open-lmf', 'eq', 'Main Node'),
'Open Lake Floria option': m('open-lake-floria', 'eq', 'Open'),
'Talk to Yerbal option': m('open-lake-floria', 'eq', 'Talk to Yerbal'),
'Vanilla Lake Floria option': m('open-lake-floria', 'eq', 'Vanilla'),
'Randomized Beedle option': m(
'shopsanity',
'eq',
'Vanilla',
/* negation */ true,
),
'Gondo Upgrades On option': m('gondo-upgrades', 'eq', false),
'No BiT crashes': m('bit-patches', 'eq', 'Fix BiT Crashes'),
'Nonlethal Hot Cave': m('damage-multiplier', 'lt', 12),
'Upgraded Skyward Strike option': m('upgraded-skyward-strike', 'eq', true),
'FS Lava Flow option': m('fs-lava-flow', 'eq', true),
};

/**
* Get Query conditions (settings value or required dungeon tests, tricks)
*/
export function getLegacyOptionConditions(options: OptionDefs) {
const conditions = { ...legacyHardcodedOptions };
const enabledTricksOption = options.find(
(v) => v.command === 'enabled-tricks-bitless',
);
const enabledTricksGlitchedOption = options.find(
(v) => v.command === 'enabled-tricks-glitched',
);

const handleTricks = (option: MultiChoiceOption) => {
for (const choice of option.choices) {
conditions[`${choice} Trick`] = {
type: 'query',
option: option.command,
op: 'in',
value: choice,
negation: false,
};
}
};

// https://github.com/NindyBK/ssrnppbuild/pull/1
if (options.some((o) => o.command === 'open-shortcuts')) {
conditions['Open Dungeon Shortcuts option'] = m('open-shortcuts', 'eq', 'All Dungeons');
conditions['Open Unrequired Shortcuts option'] = m('open-shortcuts', 'eq', 'Unrequired Dungeons Only');
conditions['Default Dungeon Behavior option'] = m('open-shortcuts', 'eq', 'None');

for (const dungeon of dungeonNames) {
conditions[`${dungeon} Required`] = { type: 'req_dungeon', dungeon, negation: false };
conditions[`${dungeon} Unrequired`] = { type: 'req_dungeon', dungeon, negation: true };
}
}

if (enabledTricksOption?.type === 'multichoice') {
handleTricks(enabledTricksOption);
}
if (enabledTricksGlitchedOption?.type === 'multichoice') {
handleTricks(enabledTricksGlitchedOption);
}

return conditions;
}

const gotOpeningReq = 'GoT Opening Requirement';
const gotRaisingReq = 'GoT Raising Requirement';
const hordeDoorReq = 'Horde Door Requirement';
const impaSongCheck = '\\Faron\\Sealed Grounds\\Sealed Temple\\Song from Impa';
const completeTriforceReq = '\\Complete Triforce';

export function getLegacyWellKnownRequirements(): Logic['wellKnownRequirements'] {
return {
open_got: gotOpeningReq,
raise_got: gotRaisingReq,
horde_door: hordeDoorReq,
impa_song_check: impaSongCheck,
complete_triforce: completeTriforceReq,
};
}
58 changes: 51 additions & 7 deletions src/logic/Logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
TimeOfDay,
RawEntrance,
RawExit,
Counter,
} from './UpstreamTypes';
import {
cubeCheckToCubeCollected,
Expand All @@ -22,10 +23,13 @@ import {
} from './bitlogic/BitLogic';
import {
booleanExprToLogicalExpr,
parseCounterThreshold,
parseExpression,
} from './booleanlogic/ExpressionParse';
import { dungeonNames } from './Locations';
import { LogicBuilder } from './LogicBuilder';
import { OptionDefs } from '../permalink/SettingsTypes';
import { getLegacyOptionConditions, getLegacyWellKnownRequirements } from './LegacySupport';

export interface Logic {
numRequirements: number;
Expand All @@ -47,7 +51,16 @@ export interface Logic {
hintRegions: string[];
checksByHintRegion: Record<string, string[]>;
exitsByHintRegion: Record<string, string[]>;
dungeonCompletionRequirements: { [dungeon: string]: string }
dungeonCompletionRequirements: { [dungeon: string]: string };
optionConditions: Exclude<RawLogic['options'], undefined>;
wellKnownRequirements: Exclude<RawLogic['well_known_requirements'], undefined>;
counters: Record<string, Counter> | undefined;
counterThresholds: Record<string, CounterThreshold>;
}

export interface CounterThreshold {
item: string;
count: number;
}

export interface LogicalCheck {
Expand Down Expand Up @@ -311,7 +324,7 @@ function preprocessItems(raw: string[]): {

const checkAreaPlaceholder = 'filled-in-later';

export function parseLogic(raw: RawLogic): Logic {
export function parseLogic(raw: RawLogic, options: OptionDefs): Logic {
const start = performance.now();

const { newItems, impliedBy, implies } = preprocessItems(
Expand All @@ -323,6 +336,8 @@ export function parseLogic(raw: RawLogic): Logic {
...Object.values(dungeonCompletionItems),
];

const counterThresholds: Logic['counterThresholds'] = {};

// Pessimistically, all items are opaque
const opaqueItems = new BitVector();
for (let i = 0; i < rawItems.length; i++) {
Expand Down Expand Up @@ -647,7 +662,19 @@ export function parseLogic(raw: RawLogic): Logic {
? location
: `${area.id}\\${location}`;

const expr = parseExpr(locationRequirementExpression);
let expr: LogicalExpression | undefined = undefined;
if (area.abstract) {
// only abstract areas can contain counters
const counter = parseCounterThreshold(locationRequirementExpression);
if (counter) {
expr = LogicalExpression.false();
counterThresholds[locationId] = counter;
}
}

if (!expr) {
expr = parseExpr(locationRequirementExpression);
}

const check: LogicalCheck | undefined = checks[locationId];
const isPrimaryLocation = check && !location.startsWith('\\');
Expand Down Expand Up @@ -831,9 +858,14 @@ export function parseLogic(raw: RawLogic): Logic {
return acc;
}, {});

const wellKnownReqs = {
...getLegacyWellKnownRequirements(),
...raw.well_known_requirements,
};

// Now map our area graph to BitLogic
const newBuilder = new LogicBuilder(rawItems, itemLookup, staticRequirements);
mapAreaToBitLogic(newBuilder, areaGraph, opaqueItems);
mapAreaToBitLogic(newBuilder, areaGraph, opaqueItems, counterThresholds, Object.values(wellKnownReqs));


// check for orphaned locations. This again should probably not be in here
Expand Down Expand Up @@ -905,17 +937,23 @@ export function parseLogic(raw: RawLogic): Logic {
exitsByHintRegion,
dungeonCompletionRequirements: raw.dungeon_completion_requirements,
areaGraph,
optionConditions: raw.options ?? getLegacyOptionConditions(options),
wellKnownRequirements: wellKnownReqs,
counters: raw.counters,
counterThresholds,
};
}

function mapAreaToBitLogic(
b: LogicBuilder,
areaGraph: AreaGraph,
opaqueItems: BitVector,
counters: Logic['counterThresholds'],
wellKnown: string[],
area = areaGraph.rootArea,
) {
for (const subArea of Object.values(area.subAreas)) {
mapAreaToBitLogic(b, areaGraph, opaqueItems, subArea);
mapAreaToBitLogic(b, areaGraph, opaqueItems, counters, wellKnown, subArea);
}

if (area.canSleep) {
Expand All @@ -930,10 +968,16 @@ function mapAreaToBitLogic(
case 'mapExit':
{
const locName = location.id;
// Hack: We keep these virtual locations opaque...

if (
// Hack: We keep these virtual locations opaque...
!locName.endsWith('Gratitude Crystals') &&
!locName.includes("\\Gondo's Upgrades\\Upgrade to")
!locName.includes("\\Gondo's Upgrades\\Upgrade to") &&
// Counters are populated at runtime
!counters[location.id] &&
// Some requirements are populated at runtime
// FIXME these should be identified via "Unknown"
!wellKnown.includes(location.id)
) {
opaqueItems.clearBit(b.bit(locName));
}
Expand Down
Loading