Review pass: the room-specific bugs a shared FloorView was hiding
Three independent reviewers checked each new venue against its brief. No blockers, all three rooms genuinely distinct — but two of the findings were mine, in the file the layout authors were told not to touch. FloorView was still drawing two things at Voltage's coordinates: - The scan beams pivoted on a hardcoded (38,23) — Voltage's mirror ball. In The Royal that swept two beams across the empty middle of the pub, twenty-five tiles from that venue's dance floor. Now derived from the layout's own discoball, and a venue without one gets no beams, because the beams ARE the ball's light. - The resident DJ stood at a hardcoded (55.5, 22.5) — Voltage's booth, which in The Royal is the middle of the bistro. Now placed from the venue's own gear and stepped clear of `posts.decks`, so a player on a DJ shift is never standing inside him. The beat-bob had the same constant baked into it. Elevate's staffDj prop is dropped as a consequence: with a resident DJ derived per venue it would have made two DJs on the plinth, and three once the player took the shift. New invariant: a staff prop may not stand on a post tile. Elevate's bartender was on the exact tile the bar shift teleports the player to, so the shift would have been played from inside her. Caught by the rule, then fixed. `mirror` and `graffiti` kinds added. Elevate was standing BOTH its mirrors up as `poster3` (the author raised it as a contract request and stubbed it honestly), which would have hung the same torn gig poster in a marble bathroom and called it a mirror. ROOM's toilets get the graffiti its brief always asked for. The Royal: TAB screens moved onto the wall they belong on rather than floating mid-carpet, the trough given its own fluoro (the one fixture that room is known for was rendering unlit), the beer garden's dark middle band lit — 288 tiles on two lights left the gate everyone walks through in the dark — the out-of-order sign moved off the tile the queue stands on, and two comments corrected to describe what the code actually does. Also hardened the farm client: `_req` raised SystemExit, which is a BaseException, so `except Exception` in the batch's worker threads did not catch it and one stray 401 during a poll killed a 43-asset run after a single asset. It now raises a normal error and retries transient 401/429/5xx with backoff. Gate: lint clean, tsc clean, floor suite 271 passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
4caa901df2
commit
22bd43857c
BIN
public/props/bistroTable.png
Normal file
BIN
public/props/bistroTable.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 417 B |
@ -3,6 +3,7 @@
|
||||
"arcade",
|
||||
"barFridge",
|
||||
"barShelf",
|
||||
"bistroTable",
|
||||
"boothLamp",
|
||||
"boothTable",
|
||||
"buttBin",
|
||||
|
||||
@ -66,6 +66,7 @@ export class FloorView {
|
||||
private readonly scene: Phaser.Scene;
|
||||
private readonly map: VenueMap;
|
||||
private readonly props: readonly PropDef[];
|
||||
private readonly posts: FloorLayout['posts'];
|
||||
private readonly lights: readonly ZoneLight[];
|
||||
private readonly palette: FloorLayout['palette'];
|
||||
|
||||
@ -95,6 +96,8 @@ export class FloorView {
|
||||
private readonly beams: Phaser.GameObjects.Image[] = [];
|
||||
private beamAngle = 0;
|
||||
private dj: Phaser.GameObjects.Image | null = null;
|
||||
/** Where the resident DJ stands, so the beat bob has something to bob from. */
|
||||
private djBaseY = 0;
|
||||
private djBob = 0;
|
||||
// Render-only shove displacement by patron id. The sim owns agent.x/agent.y;
|
||||
// syncAgents adds this on top so a shoving pair jitters without the crowd
|
||||
@ -112,6 +115,7 @@ export class FloorView {
|
||||
// the old shape imported one PROPS and one LIGHTS, so every venue in the
|
||||
// ladder was the same room with different difficulty numbers on it.
|
||||
this.props = layout.props;
|
||||
this.posts = layout.posts;
|
||||
this.lights = layout.lights;
|
||||
this.palette = layout.palette;
|
||||
const worldW = map.width * TILE;
|
||||
@ -256,9 +260,17 @@ export class FloorView {
|
||||
canvas.refresh();
|
||||
}
|
||||
}
|
||||
// Pivot on the disco ball (PROPS: 'ball' at 37,22 with a 2x2 footprint).
|
||||
const cx = 38 * TILE;
|
||||
const cy = 23 * TILE;
|
||||
// Pivot on THIS venue's mirror ball, not on Voltage's.
|
||||
//
|
||||
// This was `38 * TILE, 23 * TILE` — hardcoded to Voltage's 'ball' prop at
|
||||
// (37,22). Correct while there was one room; with four it swept two beams
|
||||
// across the empty middle of The Royal's carpet, twenty-five tiles from
|
||||
// that pub's actual dance floor. A venue with no ball gets no beams, which
|
||||
// is right: the beams are the ball's light, and ROOM does not own one.
|
||||
const ball = this.props.find((p) => p.kind === 'discoball');
|
||||
if (!ball) return;
|
||||
const cx = (ball.tx + ball.tw / 2) * TILE;
|
||||
const cy = (ball.ty + ball.th / 2) * TILE;
|
||||
for (let i = 0; i < 2; i++) {
|
||||
this.beams.push(
|
||||
this.scene.add
|
||||
@ -316,9 +328,36 @@ export class FloorView {
|
||||
canvas.refresh();
|
||||
}
|
||||
}
|
||||
// Behind the decks (PROPS puts the gear on the booth's west edge at tx 53;
|
||||
// the DJ stands one tile east of it, centred on the mixer row).
|
||||
this.dj = this.scene.add.image(55.5 * TILE, 22.5 * TILE, key).setDepth(D_TILES + 2);
|
||||
// Stand the resident DJ in THIS venue's gear, not in Voltage's booth.
|
||||
//
|
||||
// Was a flat (55.5, 22.5) — Voltage's booth. In The Royal that is the
|
||||
// middle of the bistro, so the club's DJ spent the night among the chicken
|
||||
// parmis. The gear is wherever the layout put it, so the DJ is derived from
|
||||
// it; a venue with no decks at all gets no resident DJ.
|
||||
const gear = this.props.filter(
|
||||
(p) => p.kind === 'deck' || p.kind === 'cdj' || p.kind === 'mixer' || p.kind === 'djPlinth',
|
||||
);
|
||||
if (gear.length === 0) return;
|
||||
let cx = 0;
|
||||
let cy = 0;
|
||||
for (const g of gear) {
|
||||
cx += g.tx + g.tw / 2;
|
||||
cy += g.ty + g.th / 2;
|
||||
}
|
||||
cx /= gear.length;
|
||||
cy /= gear.length;
|
||||
// Step clear of the tile the PLAYER is planted on for a DJ shift, so a
|
||||
// player working the decks is never standing inside the resident DJ.
|
||||
const post = this.posts['decks'];
|
||||
if (post) {
|
||||
const dx = cx - (post.tx + 0.5);
|
||||
const dy = cy - (post.ty + 0.5);
|
||||
const len = Math.hypot(dx, dy) || 1;
|
||||
cx += (dx / len) * 1.5;
|
||||
cy += (dy / len) * 1.5;
|
||||
}
|
||||
this.djBaseY = cy * TILE;
|
||||
this.dj = this.scene.add.image(cx * TILE, this.djBaseY, key).setDepth(D_TILES + 2);
|
||||
}
|
||||
|
||||
/** Call from the scene's beat:tick handler — the dance floor breathes in time. */
|
||||
@ -347,7 +386,7 @@ export class FloorView {
|
||||
});
|
||||
if (this.dj && this.djBob > 0) {
|
||||
this.djBob = Math.max(0, this.djBob - dtMs / 200);
|
||||
this.dj.setY(22.5 * TILE - 2 * this.djBob);
|
||||
this.dj.setY(this.djBaseY - 2 * this.djBob);
|
||||
}
|
||||
const t = this.scene.time.now;
|
||||
for (const f of this.flickerLights) {
|
||||
@ -675,6 +714,8 @@ const PROP_SIZES: Record<PropKind, [number, number]> = {
|
||||
cdj: [32, 32], rollerDoor: [64, 32], ruleBoard: [32, 32],
|
||||
// Staff figures (set dressing, all venues)
|
||||
staffBartender: [16, 32], staffDj: [16, 32], staffGlassie: [16, 32], staffSecurity: [16, 32],
|
||||
// Wall surfaces
|
||||
mirror: [32, 16], graffiti: [32, 16],
|
||||
};
|
||||
|
||||
function bakePropPlaceholder(scene: Phaser.Scene, kind: PropKind): string {
|
||||
|
||||
@ -152,10 +152,10 @@ const SKYLINE: PropEmissive = { colour: 0xd8a848, radius: 56 };
|
||||
const PROPS: readonly PropDef[] = Object.freeze([
|
||||
// --- lift lobby ------------------------------------------------------------
|
||||
P('liftDoors', 'liftDoor', 0, 22, 1, 3),
|
||||
// No 'mirror' PropKind exists and the union is frozen after Phase 0, so both
|
||||
// of this venue's mirrors are flat wall plates borrowed from the poster slot.
|
||||
// TODO(contract): a `mirror` kind would let these stop lying.
|
||||
P('lobbyMirror', 'poster3', 1, 19, 2, 1),
|
||||
// A real 'mirror' kind now exists (it was raised as a contract request and
|
||||
// granted in the same session) — these were both `poster3`, which would have
|
||||
// hung the same torn gig poster in a marble lift lobby and called it a mirror.
|
||||
P('lobbyMirror', 'mirror', 1, 19, 2, 1),
|
||||
P('lobbyArt', 'poster1', 1, 26, 2, 1),
|
||||
P('podium', 'stampPodium', 11, 23, 1, 1),
|
||||
P('ropeL1', 'ropePost', 5, 21, 1, 1),
|
||||
@ -177,7 +177,10 @@ const PROPS: readonly PropDef[] = Object.freeze([
|
||||
P('fridge', 'barFridge', 33, 8, 2, 2, { colour: 0x60c0e0, radius: 16 }),
|
||||
P('dish', 'dishwasher', 20, 8, 2, 1, { colour: 0x50c060, radius: 20 }),
|
||||
P('rack', 'glassRack', 36, 9, 2, 1),
|
||||
P('bartender', 'staffBartender', 25, BAR_LANE_Y, 1, 1),
|
||||
// West end of the service lane, NOT on `posts.taps` (25,9) — that is the tile
|
||||
// the bar shift plants the player on, and a staff sprite there means playing
|
||||
// the whole shift from inside the bartender.
|
||||
P('bartender', 'staffBartender', 20, BAR_LANE_Y, 1, 1),
|
||||
|
||||
// --- DJ plinth -------------------------------------------------------------
|
||||
P('plinth', 'djPlinth', 29, 16, 7, 2),
|
||||
@ -186,7 +189,10 @@ const PROPS: readonly PropDef[] = Object.freeze([
|
||||
P('deckB', 'deck', 33, 16, 2, 2),
|
||||
P('monitor', 'monitor', 29, 17, 1, 1),
|
||||
P('djLamp', 'boothLamp', 35, 16, 1, 1, { colour: 0x3060c0, radius: 38, pulse: 'flicker' }),
|
||||
P('theDj', 'staffDj', 34, 15, 1, 1),
|
||||
// No staffDj prop here on purpose. FloorView draws a RESIDENT DJ in every
|
||||
// venue, positioned from that venue's gear and stepped clear of `posts.decks`
|
||||
// — so a staffDj prop on the plinth would put two DJs on it before the player
|
||||
// takes a DJ shift, and three afterwards.
|
||||
|
||||
// --- dance floor -----------------------------------------------------------
|
||||
P('spkNW', 'speaker', 25, 20, 1, 2),
|
||||
@ -211,7 +217,7 @@ const PROPS: readonly PropDef[] = Object.freeze([
|
||||
|
||||
// --- toilets ---------------------------------------------------------------
|
||||
P('basin', 'marbleSink', 48, 15, 5, 1),
|
||||
P('looMirror', 'poster3', 48, 14, 5, 1),
|
||||
P('looMirror', 'mirror', 48, 14, 5, 1),
|
||||
P('cubDoorA', 'cubicleDoor', 48, 11, 1, 1),
|
||||
P('cubDoorB', 'cubicleDoor', 52, 11, 1, 1),
|
||||
P('dryer', 'handDryer', 55, 13, 1, 1),
|
||||
|
||||
@ -223,6 +223,12 @@ const ROOM_PROPS: readonly PropDef[] = Object.freeze([
|
||||
|
||||
// ---- dunnies ---------------------------------------------------------------
|
||||
P('basin', 'brokenBasin', 58, 9, 2, 1),
|
||||
// "Graffiti on every surface" is in this venue's brief (docs/VENUES.md §5) and
|
||||
// had no kind to render with until one was added. No mirror in here — that is
|
||||
// the joke, and it is why the tags are the only thing to read.
|
||||
P('tagsN', 'graffiti', 58, 3, 2, 1),
|
||||
P('tagsW', 'graffiti', 58, 12, 2, 1),
|
||||
P('tagsE', 'graffiti', 70, 8, 2, 1),
|
||||
// Only the first cubicle still has a door on it. The other two tell their
|
||||
// story with what's on the floor outside them instead.
|
||||
P('cubDoor', 'cubicleDoor', 60, 6, 1, 1),
|
||||
|
||||
@ -29,9 +29,9 @@ import type {
|
||||
TileKind, TilePoint, VenueMap, WorldPoint, ZoneLight,
|
||||
} from '../venueMap';
|
||||
|
||||
// Two cubicles, not four — it is a pub, not a club. They share a wall column so
|
||||
// there is no useless one-tile slot between them, and CUBICLE_Y sits flush under
|
||||
// the dunny's north wall.
|
||||
// Two cubicles, not four — it is a pub, not a club. They abut directly (cols
|
||||
// 65-68 and 69-72) so there is no useless one-tile slot between them, and
|
||||
// CUBICLE_Y sits flush under the dunny's north wall.
|
||||
const CUBICLE_X = [65, 69] as const;
|
||||
const CUBICLE_Y = 17;
|
||||
|
||||
@ -117,7 +117,7 @@ function buildTiles(): TileKind[] {
|
||||
set(29, 33, 'smokeDoor');
|
||||
|
||||
// The only neon in the building: two beer signs in the front windows, the TAB
|
||||
// sign over the pokie room door, and whatever is left of the one by the decks.
|
||||
// sign over the pokie room door, and the jukebox's own tube in the far corner.
|
||||
for (const tx of [40, 52]) set(tx, 0, 'neon');
|
||||
set(66, 12, 'neon');
|
||||
set(MAP_W - 1, 37, 'neon');
|
||||
@ -214,15 +214,22 @@ const PROPS: readonly PropDef[] = Object.freeze([
|
||||
P('pokieF', 'pokie', 63, 6, 2, 2, POKIE_GLOW),
|
||||
P('tabA', 'tabBoard', 69, 1, 4, 2, { colour: 0x50b0c0, radius: 18 }),
|
||||
P('tabB', 'tabBoard', 74, 1, 4, 2, { colour: 0x50b0c0, radius: 18 }),
|
||||
P('tabRace', 'tvSport', 66, 9, 3, 2, { colour: 0x6090c0, radius: 18 }),
|
||||
// On the pokie room's south wall (row 12), not floating at row 9 — a bank of
|
||||
// betting screens standing in the middle of the carpet reads as an accident.
|
||||
P('tabRace', 'tvSport', 66, 11, 3, 1, { colour: 0x6090c0, radius: 18 }),
|
||||
|
||||
// dunnies — the trough, the one basin, and the cubicle that has been "getting
|
||||
// looked at" since the Blues won a series
|
||||
P('trough', 'trough', 74, 17, 4, 1),
|
||||
// The trough carries its own fluoro: 'looN' sits over the cubicles and its
|
||||
// spill dies well short of this alcove, so the one fixture the room is
|
||||
// actually known for was rendering in the dark.
|
||||
P('trough', 'trough', 74, 17, 4, 1, { colour: 0x50c060, radius: 26, pulse: 'flicker' }),
|
||||
P('basin', 'sinkRow', 70, 27, 1, 1),
|
||||
P('cubDoorA', 'cubicleDoor', 66, 20, 1, 1),
|
||||
P('cubDoorB', 'cubicleDoor', 70, 20, 1, 1),
|
||||
P('outOfOrder', 'wetFloor', 70, 21, 1, 1),
|
||||
// One tile west of stall s2's door (70,21): that tile is both the door and
|
||||
// the toilet anchor, so a sign placed on it is a sign the queue stands on.
|
||||
P('outOfOrder', 'wetFloor', 69, 21, 1, 1),
|
||||
P('mopBucket', 'mopBucket', 71, 22, 1, 1),
|
||||
P('dryer', 'handDryer', 77, 26, 1, 1),
|
||||
|
||||
@ -305,6 +312,12 @@ const LIGHTS: readonly ZoneLight[] = Object.freeze([
|
||||
L('dj', 55, 36, 0x3060c0, 36),
|
||||
L('yardW', 20, 38, 0x6080a0, 48),
|
||||
L('yardE', 37, 38, 0x6080a0, 48),
|
||||
// The garden is 288 tiles — ten times Voltage's yard — and on two lights its
|
||||
// whole middle band went dark, including the gate everyone comes through.
|
||||
L('yardGate', 29, 36, 0x6080a0, 42),
|
||||
L('yardMid', 28, 40, 0x6080a0, 40),
|
||||
L('yardFarW', 15, 40, 0x6080a0, 36),
|
||||
L('yardFarE', 43, 39, 0x6080a0, 36),
|
||||
// Entry keeps the house pink: navigate-by-glow is a rule across all four
|
||||
// venues (SPACES.md §1), so the way out has to read the same everywhere.
|
||||
L('entry', 3, 14, 0xd03470, 34),
|
||||
|
||||
@ -226,7 +226,11 @@ export type PropKind =
|
||||
// dollPlan draws the exact items the dress code convicts on, so a
|
||||
// pre-rendered patron would make the convictions invisible (FABLE-SOLO-23).
|
||||
// A bartender who never moves carries no mechanical weight, so she can be art.
|
||||
| 'staffBartender' | 'staffDj' | 'staffGlassie' | 'staffSecurity';
|
||||
| 'staffBartender' | 'staffDj' | 'staffGlassie' | 'staffSecurity'
|
||||
// Wall surfaces. Added after review: Elevate was standing BOTH its mirrors up
|
||||
// as `poster3`, so the lift lobby and the marble bathroom would have rendered
|
||||
// the same torn gig poster and called it a mirror.
|
||||
| 'mirror' | 'graffiti';
|
||||
|
||||
export interface PropEmissive {
|
||||
colour: number;
|
||||
|
||||
@ -104,7 +104,7 @@ describe('ROOM layout', () => {
|
||||
});
|
||||
|
||||
it('is darker than Voltage: fewer lights, tighter radii, sodium over the dock', () => {
|
||||
// Voltage runs 19 zone lights with dance pools at radius 26. If ROOM ever
|
||||
// Voltage runs 20 zone lights with dance pools at radius 26. If ROOM ever
|
||||
// catches up with that, the torch has stopped being the mechanic.
|
||||
expect(ROOM.lights.length).toBeLessThanOrEqual(14);
|
||||
for (const l of ROOM.lights) expect(l.radius, `light '${l.id}' is too big for ROOM`).toBeLessThanOrEqual(40);
|
||||
|
||||
@ -174,6 +174,23 @@ export function assertLayout(
|
||||
expect(p.tx + p.tw, `${layout.id}: prop '${p.id}' overhangs the east edge`).toBeLessThanOrEqual(MAP_W);
|
||||
expect(p.ty + p.th, `${layout.id}: prop '${p.id}' overhangs the south edge`).toBeLessThanOrEqual(MAP_H);
|
||||
}
|
||||
// A staff figure is set dressing; a POST is the tile the game teleports the
|
||||
// player onto for a shift. Put them on the same tile and the player spends
|
||||
// the whole shift standing inside the bartender — which reads as a rendering
|
||||
// glitch, not as a colleague.
|
||||
const postTiles = new Set(Object.values(posts).map((p) => key(p.tx, p.ty)));
|
||||
for (const p of props) {
|
||||
if (!p.kind.startsWith('staff')) continue;
|
||||
for (let ty = p.ty; ty < p.ty + p.th; ty++) {
|
||||
for (let tx = p.tx; tx < p.tx + p.tw; tx++) {
|
||||
expect(
|
||||
postTiles.has(key(tx, ty)),
|
||||
`${layout.id}: staff prop '${p.id}' stands on a post tile (${tx},${ty})`,
|
||||
).toBe(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lightIds = new Set<string>();
|
||||
for (const l of lights) {
|
||||
expect(lightIds.has(l.id), `${layout.id}: duplicate light id '${l.id}'`).toBe(false);
|
||||
|
||||
@ -164,6 +164,14 @@ ROOM = [
|
||||
"cryptic house rules, chipped frame"),
|
||||
]
|
||||
|
||||
# ---- Wall surfaces (flat by nature — flux draws these directly) ---------------
|
||||
WALLS = [
|
||||
F("mirror", (32, 16), 4480,
|
||||
"a large wall mirror in a plain frame reflecting dim room lights"),
|
||||
F("graffiti", (32, 16), 4481,
|
||||
"a concrete toilet wall completely covered in layered marker tags and scrawl"),
|
||||
]
|
||||
|
||||
# ---- Staff figures (set dressing props, never patrons) ------------------------
|
||||
STAFF = [
|
||||
M("staffBartender", (16, 32), 4460, 76,
|
||||
@ -192,7 +200,7 @@ STREETS = [
|
||||
gen=(2048, 576), quantise=False, keep_bg=True),
|
||||
]
|
||||
|
||||
ALL: list[Asset] = [*ROYAL, *ELEVATE, *ROOM, *STAFF, *STREETS]
|
||||
ALL: list[Asset] = [*ROYAL, *ELEVATE, *ROOM, *WALLS, *STAFF, *STREETS]
|
||||
|
||||
BY_KIND = {a.kind: a for a in ALL}
|
||||
|
||||
|
||||
@ -45,19 +45,46 @@ def _scrub(raw: bytes) -> object:
|
||||
return json.loads("".join(c if c >= " " or c == "\t" else " " for c in text))
|
||||
|
||||
|
||||
class MBError(RuntimeError):
|
||||
"""A farm call failed. A normal Exception ON PURPOSE.
|
||||
|
||||
This used to raise SystemExit, which is a BaseException — so when the batch
|
||||
generator started running three assets in worker threads, `except Exception`
|
||||
did not catch it and a single transient blip took the whole run down instead
|
||||
of costing one prop. That happened: a stray 401 during a poll killed a
|
||||
43-asset batch after one asset.
|
||||
"""
|
||||
|
||||
|
||||
# A poll every few seconds across three concurrent assets occasionally comes
|
||||
# back 401 or 5xx from a farm that is perfectly healthy a second later. Retrying
|
||||
# is right for everything except a token that is genuinely wrong — and that
|
||||
# fails on the very first call, long before a batch is in flight.
|
||||
_RETRY_STATUS = {401, 429, 500, 502, 503, 504}
|
||||
|
||||
|
||||
def _req(path: str, data: bytes | None = None, headers: dict[str, str] | None = None,
|
||||
raw: bool = False, timeout: int = 180) -> object:
|
||||
head = {"Authorization": f"Bearer {creds.get('MB_TOKEN')}"}
|
||||
head.update(headers or {})
|
||||
req = urllib.request.Request(HOST + path, data=data, headers=head)
|
||||
try:
|
||||
body = urllib.request.urlopen(req, timeout=timeout).read()
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode(errors="ignore")[:400]
|
||||
raise SystemExit(f"MODELBEAST HTTP {exc.code} on {path}: {detail}") from None
|
||||
except urllib.error.URLError as exc:
|
||||
raise SystemExit(f"MODELBEAST unreachable at {HOST}: {exc.reason}") from None
|
||||
return body if raw else _scrub(body)
|
||||
raw: bool = False, timeout: int = 180, attempts: int = 4) -> object:
|
||||
delay = 2.0
|
||||
last = ""
|
||||
for attempt in range(1, attempts + 1):
|
||||
head = {"Authorization": f"Bearer {creds.get('MB_TOKEN')}"}
|
||||
head.update(headers or {})
|
||||
req = urllib.request.Request(HOST + path, data=data, headers=head)
|
||||
try:
|
||||
body = urllib.request.urlopen(req, timeout=timeout).read()
|
||||
return body if raw else _scrub(body)
|
||||
except urllib.error.HTTPError as exc:
|
||||
last = f"HTTP {exc.code} on {path}: {exc.read().decode(errors='ignore')[:300]}"
|
||||
if exc.code not in _RETRY_STATUS or attempt == attempts:
|
||||
raise MBError(f"MODELBEAST {last}") from None
|
||||
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
||||
last = f"unreachable at {HOST}: {exc}"
|
||||
if attempt == attempts:
|
||||
raise MBError(f"MODELBEAST {last}") from None
|
||||
time.sleep(delay)
|
||||
delay *= 2
|
||||
raise MBError(f"MODELBEAST {last}")
|
||||
|
||||
|
||||
def ops() -> list[str]:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user