ONI Agent Bridge - initial mod implementation
This commit is contained in:
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
node_modules/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.DS_Store
|
||||
.env
|
||||
*.dll
|
||||
*.pdb
|
||||
*.log
|
||||
mod/bin/
|
||||
mod/obj/
|
||||
mod/Output/
|
||||
.vscode/
|
||||
.idea/
|
||||
venv/
|
||||
.venv/
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
.python-version
|
||||
BIN
ONIModTutorial-main.zip
Normal file
BIN
ONIModTutorial-main.zip
Normal file
Binary file not shown.
50
ONIModTutorial/ONIModTutorial-main/.github/workflows/deploy.yml
vendored
Normal file
50
ONIModTutorial/ONIModTutorial-main/.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
name: Deploy VitePress site to Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build with VitePress
|
||||
|
||||
run: npx vitepress build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: .vitepress/dist
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
28
ONIModTutorial/ONIModTutorial-main/.github/workflows/pr-check.yml
vendored
Normal file
28
ONIModTutorial/ONIModTutorial-main/.github/workflows/pr-check.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
name: PR Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
|
||||
- name: Install deps
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
12
ONIModTutorial/ONIModTutorial-main/.gitignore
vendored
Normal file
12
ONIModTutorial/ONIModTutorial-main/.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# 排除依赖库
|
||||
node_modules/
|
||||
|
||||
# 排除编译输出
|
||||
dist/
|
||||
.temp/
|
||||
.cache/
|
||||
|
||||
# 排除本地环境变量和日志
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
61
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/_metadata.json
vendored
Normal file
61
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/_metadata.json
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
{
|
||||
"hash": "5368ccde",
|
||||
"configHash": "2733785a",
|
||||
"lockfileHash": "3ae99b52",
|
||||
"browserHash": "26eaea40",
|
||||
"optimized": {
|
||||
"vue": {
|
||||
"src": "../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
|
||||
"file": "vue.js",
|
||||
"fileHash": "605c659d",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > @vue/devtools-api": {
|
||||
"src": "../../../node_modules/@vue/devtools-api/dist/index.js",
|
||||
"file": "vitepress___@vue_devtools-api.js",
|
||||
"fileHash": "3cacbefb",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > @vueuse/core": {
|
||||
"src": "../../../node_modules/@vueuse/core/index.mjs",
|
||||
"file": "vitepress___@vueuse_core.js",
|
||||
"fileHash": "770ea158",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > @vueuse/integrations/useFocusTrap": {
|
||||
"src": "../../../node_modules/@vueuse/integrations/useFocusTrap.mjs",
|
||||
"file": "vitepress___@vueuse_integrations_useFocusTrap.js",
|
||||
"fileHash": "b915752a",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > mark.js/src/vanilla.js": {
|
||||
"src": "../../../node_modules/mark.js/src/vanilla.js",
|
||||
"file": "vitepress___mark__js_src_vanilla__js.js",
|
||||
"fileHash": "38aa2e8f",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > minisearch": {
|
||||
"src": "../../../node_modules/minisearch/dist/es/index.js",
|
||||
"file": "vitepress___minisearch.js",
|
||||
"fileHash": "f5e205a1",
|
||||
"needsInterop": false
|
||||
},
|
||||
"rss-parser": {
|
||||
"src": "../../../node_modules/rss-parser/index.js",
|
||||
"file": "rss-parser.js",
|
||||
"fileHash": "c4c7877b",
|
||||
"needsInterop": true
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
"chunk-LJKO4TMH": {
|
||||
"file": "chunk-LJKO4TMH.js"
|
||||
},
|
||||
"chunk-QE257C5J": {
|
||||
"file": "chunk-QE257C5J.js"
|
||||
},
|
||||
"chunk-BUSYA2B4": {
|
||||
"file": "chunk-BUSYA2B4.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/chunk-BUSYA2B4.js
vendored
Normal file
9
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/chunk-BUSYA2B4.js
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __commonJS = (cb, mod) => function __require() {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
};
|
||||
|
||||
export {
|
||||
__commonJS
|
||||
};
|
||||
//# sourceMappingURL=chunk-BUSYA2B4.js.map
|
||||
7
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/chunk-BUSYA2B4.js.map
vendored
Normal file
7
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/chunk-BUSYA2B4.js.map
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
9719
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/chunk-LJKO4TMH.js
vendored
Normal file
9719
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/chunk-LJKO4TMH.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/chunk-LJKO4TMH.js.map
vendored
Normal file
7
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/chunk-LJKO4TMH.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
12824
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/chunk-QE257C5J.js
vendored
Normal file
12824
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/chunk-QE257C5J.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/chunk-QE257C5J.js.map
vendored
Normal file
7
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/chunk-QE257C5J.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/package.json
vendored
Normal file
3
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/package.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
6788
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/rss-parser.js
vendored
Normal file
6788
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/rss-parser.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/rss-parser.js.map
vendored
Normal file
7
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/rss-parser.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4507
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/vitepress___@vue_devtools-api.js
vendored
Normal file
4507
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/vitepress___@vue_devtools-api.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
584
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/vitepress___@vueuse_core.js
vendored
Normal file
584
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/vitepress___@vueuse_core.js
vendored
Normal file
@ -0,0 +1,584 @@
|
||||
import {
|
||||
DefaultMagicKeysAliasMap,
|
||||
StorageSerializers,
|
||||
TransitionPresets,
|
||||
assert,
|
||||
breakpointsAntDesign,
|
||||
breakpointsBootstrapV5,
|
||||
breakpointsElement,
|
||||
breakpointsMasterCss,
|
||||
breakpointsPrimeFlex,
|
||||
breakpointsQuasar,
|
||||
breakpointsSematic,
|
||||
breakpointsTailwind,
|
||||
breakpointsVuetify,
|
||||
breakpointsVuetifyV2,
|
||||
breakpointsVuetifyV3,
|
||||
bypassFilter,
|
||||
camelize,
|
||||
clamp,
|
||||
cloneFnJSON,
|
||||
computedAsync,
|
||||
computedEager,
|
||||
computedInject,
|
||||
computedWithControl,
|
||||
containsProp,
|
||||
controlledRef,
|
||||
createEventHook,
|
||||
createFetch,
|
||||
createFilterWrapper,
|
||||
createGlobalState,
|
||||
createInjectionState,
|
||||
createRef,
|
||||
createReusableTemplate,
|
||||
createSharedComposable,
|
||||
createSingletonPromise,
|
||||
createTemplatePromise,
|
||||
createUnrefFn,
|
||||
customStorageEventName,
|
||||
debounceFilter,
|
||||
defaultDocument,
|
||||
defaultLocation,
|
||||
defaultNavigator,
|
||||
defaultWindow,
|
||||
executeTransition,
|
||||
extendRef,
|
||||
formatDate,
|
||||
formatTimeAgo,
|
||||
get,
|
||||
getLifeCycleTarget,
|
||||
getSSRHandler,
|
||||
hasOwn,
|
||||
hyphenate,
|
||||
identity,
|
||||
increaseWithUnit,
|
||||
injectLocal,
|
||||
invoke,
|
||||
isClient,
|
||||
isDef,
|
||||
isDefined,
|
||||
isIOS,
|
||||
isObject,
|
||||
isWorker,
|
||||
makeDestructurable,
|
||||
mapGamepadToXbox360Controller,
|
||||
noop,
|
||||
normalizeDate,
|
||||
notNullish,
|
||||
now,
|
||||
objectEntries,
|
||||
objectOmit,
|
||||
objectPick,
|
||||
onClickOutside,
|
||||
onElementRemoval,
|
||||
onKeyDown,
|
||||
onKeyPressed,
|
||||
onKeyStroke,
|
||||
onKeyUp,
|
||||
onLongPress,
|
||||
onStartTyping,
|
||||
pausableFilter,
|
||||
promiseTimeout,
|
||||
provideLocal,
|
||||
provideSSRWidth,
|
||||
pxValue,
|
||||
rand,
|
||||
reactify,
|
||||
reactifyObject,
|
||||
reactiveComputed,
|
||||
reactiveOmit,
|
||||
reactivePick,
|
||||
refAutoReset,
|
||||
refDebounced,
|
||||
refDefault,
|
||||
refThrottled,
|
||||
refWithControl,
|
||||
resolveRef,
|
||||
resolveUnref,
|
||||
set,
|
||||
setSSRHandler,
|
||||
syncRef,
|
||||
syncRefs,
|
||||
templateRef,
|
||||
throttleFilter,
|
||||
timestamp,
|
||||
toArray,
|
||||
toReactive,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
tryOnBeforeMount,
|
||||
tryOnBeforeUnmount,
|
||||
tryOnMounted,
|
||||
tryOnScopeDispose,
|
||||
tryOnUnmounted,
|
||||
unrefElement,
|
||||
until,
|
||||
useActiveElement,
|
||||
useAnimate,
|
||||
useArrayDifference,
|
||||
useArrayEvery,
|
||||
useArrayFilter,
|
||||
useArrayFind,
|
||||
useArrayFindIndex,
|
||||
useArrayFindLast,
|
||||
useArrayIncludes,
|
||||
useArrayJoin,
|
||||
useArrayMap,
|
||||
useArrayReduce,
|
||||
useArraySome,
|
||||
useArrayUnique,
|
||||
useAsyncQueue,
|
||||
useAsyncState,
|
||||
useBase64,
|
||||
useBattery,
|
||||
useBluetooth,
|
||||
useBreakpoints,
|
||||
useBroadcastChannel,
|
||||
useBrowserLocation,
|
||||
useCached,
|
||||
useClipboard,
|
||||
useClipboardItems,
|
||||
useCloned,
|
||||
useColorMode,
|
||||
useConfirmDialog,
|
||||
useCountdown,
|
||||
useCounter,
|
||||
useCssVar,
|
||||
useCurrentElement,
|
||||
useCycleList,
|
||||
useDark,
|
||||
useDateFormat,
|
||||
useDebounceFn,
|
||||
useDebouncedRefHistory,
|
||||
useDeviceMotion,
|
||||
useDeviceOrientation,
|
||||
useDevicePixelRatio,
|
||||
useDevicesList,
|
||||
useDisplayMedia,
|
||||
useDocumentVisibility,
|
||||
useDraggable,
|
||||
useDropZone,
|
||||
useElementBounding,
|
||||
useElementByPoint,
|
||||
useElementHover,
|
||||
useElementSize,
|
||||
useElementVisibility,
|
||||
useEventBus,
|
||||
useEventListener,
|
||||
useEventSource,
|
||||
useEyeDropper,
|
||||
useFavicon,
|
||||
useFetch,
|
||||
useFileDialog,
|
||||
useFileSystemAccess,
|
||||
useFocus,
|
||||
useFocusWithin,
|
||||
useFps,
|
||||
useFullscreen,
|
||||
useGamepad,
|
||||
useGeolocation,
|
||||
useIdle,
|
||||
useImage,
|
||||
useInfiniteScroll,
|
||||
useIntersectionObserver,
|
||||
useInterval,
|
||||
useIntervalFn,
|
||||
useKeyModifier,
|
||||
useLastChanged,
|
||||
useLocalStorage,
|
||||
useMagicKeys,
|
||||
useManualRefHistory,
|
||||
useMediaControls,
|
||||
useMediaQuery,
|
||||
useMemoize,
|
||||
useMemory,
|
||||
useMounted,
|
||||
useMouse,
|
||||
useMouseInElement,
|
||||
useMousePressed,
|
||||
useMutationObserver,
|
||||
useNavigatorLanguage,
|
||||
useNetwork,
|
||||
useNow,
|
||||
useObjectUrl,
|
||||
useOffsetPagination,
|
||||
useOnline,
|
||||
usePageLeave,
|
||||
useParallax,
|
||||
useParentElement,
|
||||
usePerformanceObserver,
|
||||
usePermission,
|
||||
usePointer,
|
||||
usePointerLock,
|
||||
usePointerSwipe,
|
||||
usePreferredColorScheme,
|
||||
usePreferredContrast,
|
||||
usePreferredDark,
|
||||
usePreferredLanguages,
|
||||
usePreferredReducedMotion,
|
||||
usePreferredReducedTransparency,
|
||||
usePrevious,
|
||||
useRafFn,
|
||||
useRefHistory,
|
||||
useResizeObserver,
|
||||
useSSRWidth,
|
||||
useScreenOrientation,
|
||||
useScreenSafeArea,
|
||||
useScriptTag,
|
||||
useScroll,
|
||||
useScrollLock,
|
||||
useSessionStorage,
|
||||
useShare,
|
||||
useSorted,
|
||||
useSpeechRecognition,
|
||||
useSpeechSynthesis,
|
||||
useStepper,
|
||||
useStorage,
|
||||
useStorageAsync,
|
||||
useStyleTag,
|
||||
useSupported,
|
||||
useSwipe,
|
||||
useTemplateRefsList,
|
||||
useTextDirection,
|
||||
useTextSelection,
|
||||
useTextareaAutosize,
|
||||
useThrottleFn,
|
||||
useThrottledRefHistory,
|
||||
useTimeAgo,
|
||||
useTimeout,
|
||||
useTimeoutFn,
|
||||
useTimeoutPoll,
|
||||
useTimestamp,
|
||||
useTitle,
|
||||
useToNumber,
|
||||
useToString,
|
||||
useToggle,
|
||||
useTransition,
|
||||
useUrlSearchParams,
|
||||
useUserMedia,
|
||||
useVModel,
|
||||
useVModels,
|
||||
useVibrate,
|
||||
useVirtualList,
|
||||
useWakeLock,
|
||||
useWebNotification,
|
||||
useWebSocket,
|
||||
useWebWorker,
|
||||
useWebWorkerFn,
|
||||
useWindowFocus,
|
||||
useWindowScroll,
|
||||
useWindowSize,
|
||||
watchArray,
|
||||
watchAtMost,
|
||||
watchDebounced,
|
||||
watchDeep,
|
||||
watchIgnorable,
|
||||
watchImmediate,
|
||||
watchOnce,
|
||||
watchPausable,
|
||||
watchThrottled,
|
||||
watchTriggerable,
|
||||
watchWithFilter,
|
||||
whenever
|
||||
} from "./chunk-LJKO4TMH.js";
|
||||
import "./chunk-QE257C5J.js";
|
||||
import "./chunk-BUSYA2B4.js";
|
||||
export {
|
||||
DefaultMagicKeysAliasMap,
|
||||
StorageSerializers,
|
||||
TransitionPresets,
|
||||
assert,
|
||||
computedAsync as asyncComputed,
|
||||
refAutoReset as autoResetRef,
|
||||
breakpointsAntDesign,
|
||||
breakpointsBootstrapV5,
|
||||
breakpointsElement,
|
||||
breakpointsMasterCss,
|
||||
breakpointsPrimeFlex,
|
||||
breakpointsQuasar,
|
||||
breakpointsSematic,
|
||||
breakpointsTailwind,
|
||||
breakpointsVuetify,
|
||||
breakpointsVuetifyV2,
|
||||
breakpointsVuetifyV3,
|
||||
bypassFilter,
|
||||
camelize,
|
||||
clamp,
|
||||
cloneFnJSON,
|
||||
computedAsync,
|
||||
computedEager,
|
||||
computedInject,
|
||||
computedWithControl,
|
||||
containsProp,
|
||||
computedWithControl as controlledComputed,
|
||||
controlledRef,
|
||||
createEventHook,
|
||||
createFetch,
|
||||
createFilterWrapper,
|
||||
createGlobalState,
|
||||
createInjectionState,
|
||||
reactify as createReactiveFn,
|
||||
createRef,
|
||||
createReusableTemplate,
|
||||
createSharedComposable,
|
||||
createSingletonPromise,
|
||||
createTemplatePromise,
|
||||
createUnrefFn,
|
||||
customStorageEventName,
|
||||
debounceFilter,
|
||||
refDebounced as debouncedRef,
|
||||
watchDebounced as debouncedWatch,
|
||||
defaultDocument,
|
||||
defaultLocation,
|
||||
defaultNavigator,
|
||||
defaultWindow,
|
||||
computedEager as eagerComputed,
|
||||
executeTransition,
|
||||
extendRef,
|
||||
formatDate,
|
||||
formatTimeAgo,
|
||||
get,
|
||||
getLifeCycleTarget,
|
||||
getSSRHandler,
|
||||
hasOwn,
|
||||
hyphenate,
|
||||
identity,
|
||||
watchIgnorable as ignorableWatch,
|
||||
increaseWithUnit,
|
||||
injectLocal,
|
||||
invoke,
|
||||
isClient,
|
||||
isDef,
|
||||
isDefined,
|
||||
isIOS,
|
||||
isObject,
|
||||
isWorker,
|
||||
makeDestructurable,
|
||||
mapGamepadToXbox360Controller,
|
||||
noop,
|
||||
normalizeDate,
|
||||
notNullish,
|
||||
now,
|
||||
objectEntries,
|
||||
objectOmit,
|
||||
objectPick,
|
||||
onClickOutside,
|
||||
onElementRemoval,
|
||||
onKeyDown,
|
||||
onKeyPressed,
|
||||
onKeyStroke,
|
||||
onKeyUp,
|
||||
onLongPress,
|
||||
onStartTyping,
|
||||
pausableFilter,
|
||||
watchPausable as pausableWatch,
|
||||
promiseTimeout,
|
||||
provideLocal,
|
||||
provideSSRWidth,
|
||||
pxValue,
|
||||
rand,
|
||||
reactify,
|
||||
reactifyObject,
|
||||
reactiveComputed,
|
||||
reactiveOmit,
|
||||
reactivePick,
|
||||
refAutoReset,
|
||||
refDebounced,
|
||||
refDefault,
|
||||
refThrottled,
|
||||
refWithControl,
|
||||
resolveRef,
|
||||
resolveUnref,
|
||||
set,
|
||||
setSSRHandler,
|
||||
syncRef,
|
||||
syncRefs,
|
||||
templateRef,
|
||||
throttleFilter,
|
||||
refThrottled as throttledRef,
|
||||
watchThrottled as throttledWatch,
|
||||
timestamp,
|
||||
toArray,
|
||||
toReactive,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
tryOnBeforeMount,
|
||||
tryOnBeforeUnmount,
|
||||
tryOnMounted,
|
||||
tryOnScopeDispose,
|
||||
tryOnUnmounted,
|
||||
unrefElement,
|
||||
until,
|
||||
useActiveElement,
|
||||
useAnimate,
|
||||
useArrayDifference,
|
||||
useArrayEvery,
|
||||
useArrayFilter,
|
||||
useArrayFind,
|
||||
useArrayFindIndex,
|
||||
useArrayFindLast,
|
||||
useArrayIncludes,
|
||||
useArrayJoin,
|
||||
useArrayMap,
|
||||
useArrayReduce,
|
||||
useArraySome,
|
||||
useArrayUnique,
|
||||
useAsyncQueue,
|
||||
useAsyncState,
|
||||
useBase64,
|
||||
useBattery,
|
||||
useBluetooth,
|
||||
useBreakpoints,
|
||||
useBroadcastChannel,
|
||||
useBrowserLocation,
|
||||
useCached,
|
||||
useClipboard,
|
||||
useClipboardItems,
|
||||
useCloned,
|
||||
useColorMode,
|
||||
useConfirmDialog,
|
||||
useCountdown,
|
||||
useCounter,
|
||||
useCssVar,
|
||||
useCurrentElement,
|
||||
useCycleList,
|
||||
useDark,
|
||||
useDateFormat,
|
||||
refDebounced as useDebounce,
|
||||
useDebounceFn,
|
||||
useDebouncedRefHistory,
|
||||
useDeviceMotion,
|
||||
useDeviceOrientation,
|
||||
useDevicePixelRatio,
|
||||
useDevicesList,
|
||||
useDisplayMedia,
|
||||
useDocumentVisibility,
|
||||
useDraggable,
|
||||
useDropZone,
|
||||
useElementBounding,
|
||||
useElementByPoint,
|
||||
useElementHover,
|
||||
useElementSize,
|
||||
useElementVisibility,
|
||||
useEventBus,
|
||||
useEventListener,
|
||||
useEventSource,
|
||||
useEyeDropper,
|
||||
useFavicon,
|
||||
useFetch,
|
||||
useFileDialog,
|
||||
useFileSystemAccess,
|
||||
useFocus,
|
||||
useFocusWithin,
|
||||
useFps,
|
||||
useFullscreen,
|
||||
useGamepad,
|
||||
useGeolocation,
|
||||
useIdle,
|
||||
useImage,
|
||||
useInfiniteScroll,
|
||||
useIntersectionObserver,
|
||||
useInterval,
|
||||
useIntervalFn,
|
||||
useKeyModifier,
|
||||
useLastChanged,
|
||||
useLocalStorage,
|
||||
useMagicKeys,
|
||||
useManualRefHistory,
|
||||
useMediaControls,
|
||||
useMediaQuery,
|
||||
useMemoize,
|
||||
useMemory,
|
||||
useMounted,
|
||||
useMouse,
|
||||
useMouseInElement,
|
||||
useMousePressed,
|
||||
useMutationObserver,
|
||||
useNavigatorLanguage,
|
||||
useNetwork,
|
||||
useNow,
|
||||
useObjectUrl,
|
||||
useOffsetPagination,
|
||||
useOnline,
|
||||
usePageLeave,
|
||||
useParallax,
|
||||
useParentElement,
|
||||
usePerformanceObserver,
|
||||
usePermission,
|
||||
usePointer,
|
||||
usePointerLock,
|
||||
usePointerSwipe,
|
||||
usePreferredColorScheme,
|
||||
usePreferredContrast,
|
||||
usePreferredDark,
|
||||
usePreferredLanguages,
|
||||
usePreferredReducedMotion,
|
||||
usePreferredReducedTransparency,
|
||||
usePrevious,
|
||||
useRafFn,
|
||||
useRefHistory,
|
||||
useResizeObserver,
|
||||
useSSRWidth,
|
||||
useScreenOrientation,
|
||||
useScreenSafeArea,
|
||||
useScriptTag,
|
||||
useScroll,
|
||||
useScrollLock,
|
||||
useSessionStorage,
|
||||
useShare,
|
||||
useSorted,
|
||||
useSpeechRecognition,
|
||||
useSpeechSynthesis,
|
||||
useStepper,
|
||||
useStorage,
|
||||
useStorageAsync,
|
||||
useStyleTag,
|
||||
useSupported,
|
||||
useSwipe,
|
||||
useTemplateRefsList,
|
||||
useTextDirection,
|
||||
useTextSelection,
|
||||
useTextareaAutosize,
|
||||
refThrottled as useThrottle,
|
||||
useThrottleFn,
|
||||
useThrottledRefHistory,
|
||||
useTimeAgo,
|
||||
useTimeout,
|
||||
useTimeoutFn,
|
||||
useTimeoutPoll,
|
||||
useTimestamp,
|
||||
useTitle,
|
||||
useToNumber,
|
||||
useToString,
|
||||
useToggle,
|
||||
useTransition,
|
||||
useUrlSearchParams,
|
||||
useUserMedia,
|
||||
useVModel,
|
||||
useVModels,
|
||||
useVibrate,
|
||||
useVirtualList,
|
||||
useWakeLock,
|
||||
useWebNotification,
|
||||
useWebSocket,
|
||||
useWebWorker,
|
||||
useWebWorkerFn,
|
||||
useWindowFocus,
|
||||
useWindowScroll,
|
||||
useWindowSize,
|
||||
watchArray,
|
||||
watchAtMost,
|
||||
watchDebounced,
|
||||
watchDeep,
|
||||
watchIgnorable,
|
||||
watchImmediate,
|
||||
watchOnce,
|
||||
watchPausable,
|
||||
watchThrottled,
|
||||
watchTriggerable,
|
||||
watchWithFilter,
|
||||
whenever
|
||||
};
|
||||
//# sourceMappingURL=vitepress___@vueuse_core.js.map
|
||||
7
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/vitepress___@vueuse_core.js.map
vendored
Normal file
7
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/vitepress___@vueuse_core.js.map
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
1667
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js
vendored
Normal file
1667
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
1815
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/vitepress___minisearch.js
vendored
Normal file
1815
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/vitepress___minisearch.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/vitepress___minisearch.js.map
vendored
Normal file
7
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/vitepress___minisearch.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
348
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/vue.js
vendored
Normal file
348
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/vue.js
vendored
Normal file
@ -0,0 +1,348 @@
|
||||
import {
|
||||
BaseTransition,
|
||||
BaseTransitionPropsValidators,
|
||||
Comment,
|
||||
DeprecationTypes,
|
||||
EffectScope,
|
||||
ErrorCodes,
|
||||
ErrorTypeStrings,
|
||||
Fragment,
|
||||
KeepAlive,
|
||||
ReactiveEffect,
|
||||
Static,
|
||||
Suspense,
|
||||
Teleport,
|
||||
Text,
|
||||
TrackOpTypes,
|
||||
Transition,
|
||||
TransitionGroup,
|
||||
TriggerOpTypes,
|
||||
VueElement,
|
||||
assertNumber,
|
||||
callWithAsyncErrorHandling,
|
||||
callWithErrorHandling,
|
||||
camelize,
|
||||
capitalize,
|
||||
cloneVNode,
|
||||
compatUtils,
|
||||
compile,
|
||||
computed,
|
||||
createApp,
|
||||
createBaseVNode,
|
||||
createBlock,
|
||||
createCommentVNode,
|
||||
createElementBlock,
|
||||
createHydrationRenderer,
|
||||
createPropsRestProxy,
|
||||
createRenderer,
|
||||
createSSRApp,
|
||||
createSlots,
|
||||
createStaticVNode,
|
||||
createTextVNode,
|
||||
createVNode,
|
||||
customRef,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
defineCustomElement,
|
||||
defineEmits,
|
||||
defineExpose,
|
||||
defineModel,
|
||||
defineOptions,
|
||||
defineProps,
|
||||
defineSSRCustomElement,
|
||||
defineSlots,
|
||||
devtools,
|
||||
effect,
|
||||
effectScope,
|
||||
getCurrentInstance,
|
||||
getCurrentScope,
|
||||
getCurrentWatcher,
|
||||
getTransitionRawChildren,
|
||||
guardReactiveProps,
|
||||
h,
|
||||
handleError,
|
||||
hasInjectionContext,
|
||||
hydrate,
|
||||
hydrateOnIdle,
|
||||
hydrateOnInteraction,
|
||||
hydrateOnMediaQuery,
|
||||
hydrateOnVisible,
|
||||
initCustomFormatter,
|
||||
initDirectivesForSSR,
|
||||
inject,
|
||||
isMemoSame,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
isRef,
|
||||
isRuntimeOnly,
|
||||
isShallow,
|
||||
isVNode,
|
||||
markRaw,
|
||||
mergeDefaults,
|
||||
mergeModels,
|
||||
mergeProps,
|
||||
nextTick,
|
||||
nodeOps,
|
||||
normalizeClass,
|
||||
normalizeProps,
|
||||
normalizeStyle,
|
||||
onActivated,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onBeforeUpdate,
|
||||
onDeactivated,
|
||||
onErrorCaptured,
|
||||
onMounted,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
onScopeDispose,
|
||||
onServerPrefetch,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
onWatcherCleanup,
|
||||
openBlock,
|
||||
patchProp,
|
||||
popScopeId,
|
||||
provide,
|
||||
proxyRefs,
|
||||
pushScopeId,
|
||||
queuePostFlushCb,
|
||||
reactive,
|
||||
readonly,
|
||||
ref,
|
||||
registerRuntimeCompiler,
|
||||
render,
|
||||
renderList,
|
||||
renderSlot,
|
||||
resolveComponent,
|
||||
resolveDirective,
|
||||
resolveDynamicComponent,
|
||||
resolveFilter,
|
||||
resolveTransitionHooks,
|
||||
setBlockTracking,
|
||||
setDevtoolsHook,
|
||||
setTransitionHooks,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
ssrContextKey,
|
||||
ssrUtils,
|
||||
stop,
|
||||
toDisplayString,
|
||||
toHandlerKey,
|
||||
toHandlers,
|
||||
toRaw,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
transformVNodeArgs,
|
||||
triggerRef,
|
||||
unref,
|
||||
useAttrs,
|
||||
useCssModule,
|
||||
useCssVars,
|
||||
useHost,
|
||||
useId,
|
||||
useModel,
|
||||
useSSRContext,
|
||||
useShadowRoot,
|
||||
useSlots,
|
||||
useTemplateRef,
|
||||
useTransitionState,
|
||||
vModelCheckbox,
|
||||
vModelDynamic,
|
||||
vModelRadio,
|
||||
vModelSelect,
|
||||
vModelText,
|
||||
vShow,
|
||||
version,
|
||||
warn,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
withAsyncContext,
|
||||
withCtx,
|
||||
withDefaults,
|
||||
withDirectives,
|
||||
withKeys,
|
||||
withMemo,
|
||||
withModifiers,
|
||||
withScopeId
|
||||
} from "./chunk-QE257C5J.js";
|
||||
import "./chunk-BUSYA2B4.js";
|
||||
export {
|
||||
BaseTransition,
|
||||
BaseTransitionPropsValidators,
|
||||
Comment,
|
||||
DeprecationTypes,
|
||||
EffectScope,
|
||||
ErrorCodes,
|
||||
ErrorTypeStrings,
|
||||
Fragment,
|
||||
KeepAlive,
|
||||
ReactiveEffect,
|
||||
Static,
|
||||
Suspense,
|
||||
Teleport,
|
||||
Text,
|
||||
TrackOpTypes,
|
||||
Transition,
|
||||
TransitionGroup,
|
||||
TriggerOpTypes,
|
||||
VueElement,
|
||||
assertNumber,
|
||||
callWithAsyncErrorHandling,
|
||||
callWithErrorHandling,
|
||||
camelize,
|
||||
capitalize,
|
||||
cloneVNode,
|
||||
compatUtils,
|
||||
compile,
|
||||
computed,
|
||||
createApp,
|
||||
createBlock,
|
||||
createCommentVNode,
|
||||
createElementBlock,
|
||||
createBaseVNode as createElementVNode,
|
||||
createHydrationRenderer,
|
||||
createPropsRestProxy,
|
||||
createRenderer,
|
||||
createSSRApp,
|
||||
createSlots,
|
||||
createStaticVNode,
|
||||
createTextVNode,
|
||||
createVNode,
|
||||
customRef,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
defineCustomElement,
|
||||
defineEmits,
|
||||
defineExpose,
|
||||
defineModel,
|
||||
defineOptions,
|
||||
defineProps,
|
||||
defineSSRCustomElement,
|
||||
defineSlots,
|
||||
devtools,
|
||||
effect,
|
||||
effectScope,
|
||||
getCurrentInstance,
|
||||
getCurrentScope,
|
||||
getCurrentWatcher,
|
||||
getTransitionRawChildren,
|
||||
guardReactiveProps,
|
||||
h,
|
||||
handleError,
|
||||
hasInjectionContext,
|
||||
hydrate,
|
||||
hydrateOnIdle,
|
||||
hydrateOnInteraction,
|
||||
hydrateOnMediaQuery,
|
||||
hydrateOnVisible,
|
||||
initCustomFormatter,
|
||||
initDirectivesForSSR,
|
||||
inject,
|
||||
isMemoSame,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
isRef,
|
||||
isRuntimeOnly,
|
||||
isShallow,
|
||||
isVNode,
|
||||
markRaw,
|
||||
mergeDefaults,
|
||||
mergeModels,
|
||||
mergeProps,
|
||||
nextTick,
|
||||
nodeOps,
|
||||
normalizeClass,
|
||||
normalizeProps,
|
||||
normalizeStyle,
|
||||
onActivated,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onBeforeUpdate,
|
||||
onDeactivated,
|
||||
onErrorCaptured,
|
||||
onMounted,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
onScopeDispose,
|
||||
onServerPrefetch,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
onWatcherCleanup,
|
||||
openBlock,
|
||||
patchProp,
|
||||
popScopeId,
|
||||
provide,
|
||||
proxyRefs,
|
||||
pushScopeId,
|
||||
queuePostFlushCb,
|
||||
reactive,
|
||||
readonly,
|
||||
ref,
|
||||
registerRuntimeCompiler,
|
||||
render,
|
||||
renderList,
|
||||
renderSlot,
|
||||
resolveComponent,
|
||||
resolveDirective,
|
||||
resolveDynamicComponent,
|
||||
resolveFilter,
|
||||
resolveTransitionHooks,
|
||||
setBlockTracking,
|
||||
setDevtoolsHook,
|
||||
setTransitionHooks,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
ssrContextKey,
|
||||
ssrUtils,
|
||||
stop,
|
||||
toDisplayString,
|
||||
toHandlerKey,
|
||||
toHandlers,
|
||||
toRaw,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
transformVNodeArgs,
|
||||
triggerRef,
|
||||
unref,
|
||||
useAttrs,
|
||||
useCssModule,
|
||||
useCssVars,
|
||||
useHost,
|
||||
useId,
|
||||
useModel,
|
||||
useSSRContext,
|
||||
useShadowRoot,
|
||||
useSlots,
|
||||
useTemplateRef,
|
||||
useTransitionState,
|
||||
vModelCheckbox,
|
||||
vModelDynamic,
|
||||
vModelRadio,
|
||||
vModelSelect,
|
||||
vModelText,
|
||||
vShow,
|
||||
version,
|
||||
warn,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
withAsyncContext,
|
||||
withCtx,
|
||||
withDefaults,
|
||||
withDirectives,
|
||||
withKeys,
|
||||
withMemo,
|
||||
withModifiers,
|
||||
withScopeId
|
||||
};
|
||||
//# sourceMappingURL=vue.js.map
|
||||
7
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/vue.js.map
vendored
Normal file
7
ONIModTutorial/ONIModTutorial-main/.vitepress/cache/deps/vue.js.map
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
131
ONIModTutorial/ONIModTutorial-main/.vitepress/config.mts
Normal file
131
ONIModTutorial/ONIModTutorial-main/.vitepress/config.mts
Normal file
@ -0,0 +1,131 @@
|
||||
import { defineConfig } from 'vitepress'
|
||||
|
||||
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
base: process.env.CF_PAGES ? '/' : '/ONIModTutorial/',
|
||||
title: "ONIModTutorial",
|
||||
description: "缺氧MOD教程",
|
||||
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{ text: '首页', link: '/' },
|
||||
{ text: '教程示例', link: '/markdown-examples' }
|
||||
],
|
||||
|
||||
// 导航栏右侧的社交链接
|
||||
socialLinks: [
|
||||
|
||||
{
|
||||
icon: {
|
||||
|
||||
svg: '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>前往我的 GitHub 仓库</title><path d="M10,0 C15.523,0 20,4.59 20,10.253 C20,14.782 17.138,18.624 13.167,19.981 C12.66,20.082 12.48,19.762 12.48,19.489 C12.48,19.151 12.492,18.047 12.492,16.675 C12.492,15.719 12.172,15.095 11.813,14.777 C14.04,14.523 16.38,13.656 16.38,9.718 C16.38,8.598 15.992,7.684 15.35,6.966 C15.454,6.707 15.797,5.664 15.252,4.252 C15.252,4.252 14.414,3.977 12.505,5.303 C11.706,5.076 10.85,4.962 10,4.958 C9.15,4.962 8.295,5.076 7.497,5.303 C5.586,3.977 4.746,4.252 4.746,4.252 C4.203,5.664 4.546,6.707 4.649,6.966 C4.01,7.684 3.619,8.598 3.619,9.718 C3.619,13.646 5.954,14.526 8.175,14.785 C7.889,15.041 7.63,15.493 7.54,16.156 C6.97,16.418 5.522,16.871 4.63,15.304 C4.63,15.304 4.101,14.319 3.097,14.247 C3.097,14.247 2.122,14.234 3.029,14.87 C3.029,14.87 3.684,15.185 4.139,16.37 C4.139,16.37 4.726,18.2 7.508,17.58 C7.513,18.437 7.522,19.245 7.522,19.489 C7.522,19.76 7.338,20.077 6.839,19.982 C2.865,18.627 0,14.783 0,10.253 C0,4.59 4.478,0 10,0" fill="currentColor"></path></svg>'
|
||||
},
|
||||
link: 'https://github.com/ChiYuKe/ONIModTutorial',
|
||||
ariaLabel: '前往 GitHub 仓库'
|
||||
},
|
||||
|
||||
{
|
||||
icon: {
|
||||
svg: '<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><title>我的Steam创意工坊</title><path d="M18.102 12.129c0-0 0-0 0-0.001 0-1.564 1.268-2.831 2.831-2.831s2.831 1.268 2.831 2.831c0 1.564-1.267 2.831-2.831 2.831-0 0-0 0-0.001 0h0c-0 0-0 0-0.001 0-1.563 0-2.83-1.267-2.83-2.83 0-0 0-0 0-0.001v0zM24.691 12.135c0-2.081-1.687-3.768-3.768-3.768s-3.768 1.687-3.768 3.768c0 2.081 1.687 3.768 3.768 3.768v0c2.080-0.003 3.765-1.688 3.768-3.767v-0zM10.427 23.76l-1.841-0.762c0.524 1.078 1.611 1.808 2.868 1.808 1.317 0 2.448-0.801 2.93-1.943l0.008-0.021c0.155-0.362 0.246-0.784 0.246-1.226 0-1.757-1.424-3.181-3.181-3.181-0.405 0-0.792 0.076-1.148 0.213l0.022-0.007 1.903 0.787c0.852 0.364 1.439 1.196 1.439 2.164 0 1.296-1.051 2.347-2.347 2.347-0.324 0-0.632-0.066-0.913-0.184l0.015 0.006zM15.974 1.004c-7.857 0.001-14.301 6.046-14.938 13.738l-0.004 0.054 8.038 3.322c0.668-0.462 1.495-0.737 2.387-0.737 0.001 0 0.002 0 0.002 0h-0c0.079 0 0.156 0.005 0.235 0.008l3.575-5.176v-0.074c0.003-3.12 2.533-5.648 5.653-5.648 3.122 0 5.653 2.531 5.653 5.653s-2.531 5.653-5.653 5.653h-0.131l-5.094 3.638c0 0.065 0.005 0.131 0.005 0.199 0 0.001 0 0.002 0 0.003 0 2.342-1.899 4.241-4.241 4.241-2.047 0-3.756-1.451-4.153-3.38l-0.005-0.027-5.755-2.383c1.841 6.345 7.601 10.905 14.425 10.905 8.281 0 14.994-6.713 14.994-14.994s-6.713-14.994-14.994-14.994c-0 0-0.001 0-0.001 0h0z" fill="currentColor"></path></svg>'
|
||||
},
|
||||
link: 'https://steamcommunity.com/profiles/76561199064454583/myworkshopfiles/?appid=457140',
|
||||
ariaLabel: '查看我的 Steam 创意工坊'
|
||||
}
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
// 1. 默认侧边栏:显示在根目录及基础章节
|
||||
'/': [
|
||||
{
|
||||
text: '01. 项目概述',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '开发引言', link: '/preface' },
|
||||
{ text: '开发环境配置', link: '/development-environment' },
|
||||
{ text: '游戏资产提取', link: '/asset-extraction' },
|
||||
{ text: '游戏源码分析工具', link: '/dnspy-analysis' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '02. 核心开发',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'C# 基础与补丁语法', link: '/csharp-basics' },
|
||||
{ text: 'Mod 结构', link: '/mod-structure' },
|
||||
{ text: '第一个Mod', link: '/first-mod-tutorial' },
|
||||
{ text: '编译流程扩展', link: '/build-and-debug' },
|
||||
{ text: 'Unity篇', link: '/resource-unity' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '03. 游戏内容扩展',
|
||||
collapsed: true, // 在主站默认折叠,避免太长
|
||||
items: [
|
||||
{ text: '新增建筑', link: '/content/buildings' },
|
||||
{ text: '新增植物', link: '/content/plants' },
|
||||
{ text: '新增物品', link: '/content/items' },
|
||||
{ text: '新增生物', link: '/content/creatures' },
|
||||
{ text: '新增元素', link: '/content/elements' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '04. 发布与维护',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Steam Workshop 上传', link: '/steam-workshop-upload' },
|
||||
{ text: '多版本兼容性处理', link: '/version-compatibility' },
|
||||
{ text: '开源协议与社区规范', link: '/community-guidelines' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '05. 其他',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: '贡献者名单', link: '/contributors' },
|
||||
{ text: '加入贡献', link: '/contribute' },
|
||||
{ text: '新闻公告', link: '/announcements' },
|
||||
{ text: '创意工坊', link: '/workshop-list' }
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
// 2. 内容扩展专用侧边栏
|
||||
'/content/': [
|
||||
{
|
||||
text: '🏗️ 游戏内容扩展',
|
||||
items: [
|
||||
{ text: '← 返回教程主页', link: '/preface' },
|
||||
{ text: '新增建筑 (Buildings)', link: '/content/buildings' },
|
||||
{ text: '新增植物 (Plants)', link: '/content/plants' },
|
||||
{ text: '新增物品 (Items)', link: '/content/items' },
|
||||
{ text: '新增生物 (Creatures)', link: '/content/creatures' },
|
||||
{ text: '新增元素 (Elements)', link: '/content/elements' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
editLink: {
|
||||
pattern: 'https://github.com/ChiYuKe/ONIModTutorial/edit/main/:path',
|
||||
text: '在 GitHub 上编辑此页'
|
||||
},
|
||||
search: {
|
||||
provider: 'local'
|
||||
},
|
||||
lastUpdated: {
|
||||
text: '最后更新于',
|
||||
formatOptions: {
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'medium'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="contributors-container">
|
||||
<div v-for="user in contributors" :key="user.id" class="contributor-card">
|
||||
<a :href="user.html_url" target="_blank" rel="noreferrer">
|
||||
<img :src="user.avatar_url" :alt="user.login" class="avatar" />
|
||||
<span class="username">{{ user.login }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
repo: { type: String, required: true } // 格式: "Owner/RepoName"
|
||||
})
|
||||
|
||||
const contributors = ref([])
|
||||
|
||||
onMounted(async () => {
|
||||
const cacheKey = `contributors-${props.repo}`;
|
||||
const cachedData = localStorage.getItem(cacheKey);
|
||||
const cacheTime = localStorage.getItem(`${cacheKey}-timestamp`);
|
||||
|
||||
// 如果缓存存在且没超过 1 小时,直接用缓存
|
||||
if (cachedData && cacheTime && Date.now() - cacheTime < 3600000) {
|
||||
contributors.value = JSON.parse(cachedData);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.github.com/repos/${props.repo}/contributors`);
|
||||
const data = await response.json();
|
||||
if (Array.isArray(data)) {
|
||||
const users = data.filter(user => user.type === 'User');
|
||||
contributors.value = users;
|
||||
// 存入缓存
|
||||
localStorage.setItem(cacheKey, JSON.stringify(users));
|
||||
localStorage.setItem(`${cacheKey}-timestamp`, Date.now().toString());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取失败', e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.contributors-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 24px;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.contributor-card a {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
transition: transform 0.2s ease;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.contributor-card a:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
object-fit: cover;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
color: var(--vp-c-text-1);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="timeline">
|
||||
<div class="timeline-item" v-for="(item, index) in steps" :key="index">
|
||||
<div class="timeline-dot"></div>
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-date">{{ item.status }}</div>
|
||||
<h4 class="timeline-title">{{ item.title }}</h4>
|
||||
<p class="timeline-desc">{{ item.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const steps = [
|
||||
{ status: '已完成', title: '摸鱼', desc: '了解上班摸鱼写文档的流程。' },
|
||||
{ status: '进行中', title: 'Mod 基础教程', desc: '掌握变量、类与对象,这是制作 Mod 的基石。' },
|
||||
{ status: '计划中', title: 'UI 界面开发', desc: '教你如何在游戏里画出自己的窗口和按钮。' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.timeline { margin: 2rem 0; padding-left: 20px; border-left: 2px solid var(--vp-c-divider); }
|
||||
.timeline-item { position: relative; margin-bottom: 2rem; padding-left: 30px; }
|
||||
.timeline-dot {
|
||||
position: absolute; left: -31px; top: 5px;
|
||||
width: 20px; height: 20px;
|
||||
background: var(--vp-c-brand);
|
||||
border: 4px solid var(--vp-c-bg);
|
||||
border-radius: 50%;
|
||||
}
|
||||
.timeline-date { font-size: 0.8rem; color: var(--vp-c-brand); font-weight: bold; }
|
||||
.timeline-title { margin: 5px 0 !important; color: var(--vp-c-text-1); }
|
||||
.timeline-desc { font-size: 0.9rem; color: var(--vp-c-text-2); margin: 0; }
|
||||
</style>
|
||||
@ -0,0 +1,292 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
|
||||
const articles = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
const APP_ID = '457140'
|
||||
|
||||
// Tooltip 相关状态
|
||||
const tooltip = reactive({
|
||||
visible: false,
|
||||
lines: [],
|
||||
style: {}
|
||||
})
|
||||
|
||||
// 解析 Steam 补丁说明为纯文本行数组
|
||||
function parseSteamDescription(html) {
|
||||
if (!html) return []
|
||||
|
||||
return html
|
||||
.replace(/\r?\n/g, '')
|
||||
.replace(/<li>(.*?)<\/li>/gi, '• $1\n')
|
||||
.replace(/<\/p>|<br\s*\/?>/gi, '\n')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.split('\n')
|
||||
.map(l => l.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
// 显示 Tooltip
|
||||
function showTooltip(e, item) {
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
|
||||
const MAX_LINES = 10
|
||||
const GAP = 12
|
||||
const WIDTH = 700
|
||||
const LINE_HEIGHT = 22
|
||||
const PADDING = 24
|
||||
|
||||
const total = item.parsedLines.length
|
||||
|
||||
if (total > MAX_LINES) {
|
||||
tooltip.lines = [
|
||||
...item.parsedLines
|
||||
.slice(0, MAX_LINES - 1)
|
||||
.map(t => ({ text: t })),
|
||||
{ text: '......', ellipsis: true }
|
||||
]
|
||||
} else {
|
||||
tooltip.lines = item.parsedLines.map(t => ({ text: t }))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const estimatedHeight =
|
||||
tooltip.lines.length * LINE_HEIGHT + PADDING
|
||||
|
||||
let top
|
||||
|
||||
// 优先显示在上方
|
||||
if (rect.top < estimatedHeight + GAP) {
|
||||
top = rect.bottom + GAP
|
||||
} else {
|
||||
top = rect.top - estimatedHeight - GAP
|
||||
}
|
||||
|
||||
tooltip.style = {
|
||||
position: 'fixed',
|
||||
top: `${top}px`,
|
||||
left: `${rect.left}px`,
|
||||
width: `${WIDTH}px`
|
||||
}
|
||||
|
||||
tooltip.visible = true
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
tooltip.visible = false
|
||||
}
|
||||
|
||||
|
||||
// 获取 Steam 新闻数据
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://api.rss2json.com/v1/api.json?rss_url=https%3A%2F%2Fstore.steampowered.com%2Ffeeds%2Fnews%2Fapp%2F${APP_ID}`
|
||||
)
|
||||
const data = await res.json()
|
||||
|
||||
if (data.status === 'ok') {
|
||||
articles.value = data.items.slice(0, 10).map(item => {
|
||||
const imgReg = /<img.*?src=["'](.*?)["']/
|
||||
const match = item.content?.match(imgReg)
|
||||
|
||||
const fallbackImg =
|
||||
`https://cdn.akamai.steamstatic.com/steam/apps/${APP_ID}/header.jpg`
|
||||
|
||||
return {
|
||||
...item,
|
||||
displayImage: (match ? match[1] : item.thumbnail) || fallbackImg,
|
||||
displayDate: item.pubDate.split(' ')[0],
|
||||
parsedLines: parseSteamDescription(item.description)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
<template>
|
||||
<div class="steam-container">
|
||||
<div v-if="loading" class="loading">
|
||||
正在同步 Steam 补丁说明...
|
||||
</div>
|
||||
|
||||
<div v-else class="news-list">
|
||||
<a
|
||||
v-for="item in articles"
|
||||
:key="item.guid"
|
||||
:href="item.link"
|
||||
target="_blank"
|
||||
class="news-card"
|
||||
@mouseenter="e => showTooltip(e, item)"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<div class="news-card-inner">
|
||||
<div class="card-text">
|
||||
<div class="card-header">
|
||||
<span class="tag">游戏更新</span>
|
||||
<span class="date">{{ item.displayDate }}</span>
|
||||
</div>
|
||||
|
||||
<h3 class="title">{{ item.title }}</h3>
|
||||
|
||||
<p class="description">
|
||||
{{ item.parsedLines.join(' ').slice(0, 60) }}...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card-image-box">
|
||||
<img :src="item.displayImage" />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="tooltip.visible"
|
||||
class="global-tooltip"
|
||||
:style="tooltip.style"
|
||||
>
|
||||
<div
|
||||
v-for="(line, i) in tooltip.lines"
|
||||
:key="i"
|
||||
class="tooltip-line"
|
||||
:class="{
|
||||
bullet: line.text.startsWith('•'),
|
||||
ellipsis: line.ellipsis
|
||||
}"
|
||||
>
|
||||
{{ line.text }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.news-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.news-card {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.news-card-inner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background: #2a313d;
|
||||
border: 1px solid #535a66;
|
||||
border-radius: 4px;
|
||||
transition: transform 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.news-card:hover .news-card-inner {
|
||||
background: #363f4c;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.card-text {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
color: #8f98a0;
|
||||
}
|
||||
|
||||
.date {
|
||||
color: #66c0f4;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 6px 0 0;
|
||||
font-size: 1.1rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.85rem;
|
||||
color: #acb2b8;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.card-image-box {
|
||||
width: 200px;
|
||||
height: 112px;
|
||||
margin: 0.75rem;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.card-image-box img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.global-tooltip {
|
||||
background: rgba(18, 22, 28, 0.96);
|
||||
border: 1px solid #3d4450;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
width: 700px;
|
||||
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.6);
|
||||
z-index: 9999999;
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tooltip-line {
|
||||
font-size: 0.8rem;
|
||||
color: #dcdedf;
|
||||
line-height: 1.45;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.tooltip-line.bullet {
|
||||
padding-left: 14px;
|
||||
text-indent: -14px;
|
||||
color: #c7d5e0;
|
||||
}
|
||||
|
||||
.tooltip-line.ellipsis {
|
||||
color: #66c0f4;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
@ -0,0 +1,290 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const mods = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
const STATS_CACHE_TTL = 60 * 60 * 1000 // 60 分钟
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function getCachedStats(id) {
|
||||
const raw = localStorage.getItem('workshop_stats_' + id)
|
||||
if (!raw) return null
|
||||
|
||||
const { time, data } = JSON.parse(raw)
|
||||
if (Date.now() - time > STATS_CACHE_TTL) return null
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
function setCachedStats(id, data) {
|
||||
localStorage.setItem(
|
||||
'workshop_stats_' + id,
|
||||
JSON.stringify({
|
||||
time: Date.now(),
|
||||
data
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchWorkshopStats(detailUrl) {
|
||||
try {
|
||||
const match = detailUrl.match(/id=(\d+)/)
|
||||
if (!match) return null
|
||||
|
||||
const cleanUrl = `https://steamcommunity.com/sharedfiles/filedetails/?id=${match[1]}`
|
||||
|
||||
const cached = getCachedStats(match[1])
|
||||
if (cached) return cached
|
||||
|
||||
const workerUrl =
|
||||
`https://aged-dream-7a55.liukele015.workers.dev/?url=${encodeURIComponent(cleanUrl)}`
|
||||
|
||||
const res = await fetch(workerUrl)
|
||||
if (!res.ok) {
|
||||
console.warn('Worker 返回失败:', res.status, res.statusText)
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
if (!data || !data.stats) {
|
||||
console.warn('Worker 返回数据不正确:', data)
|
||||
return null
|
||||
}
|
||||
|
||||
setCachedStats(match[1], data.stats)
|
||||
return data.stats
|
||||
|
||||
} catch (err) {
|
||||
console.warn('统计获取失败:', detailUrl, err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStats(mod) {
|
||||
if (!mod.link || mod.link === '#') return
|
||||
if (mod.stats || mod.statsLoading) return
|
||||
|
||||
mod.statsLoading = true
|
||||
mod.stats = await fetchWorkshopStats(mod.link)
|
||||
mod.statsLoading = false
|
||||
}
|
||||
|
||||
async function loadStatsSequentially(list) {
|
||||
for (const mod of list) {
|
||||
await loadStats(mod)
|
||||
await sleep(150)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const steamUrl =
|
||||
'https://steamcommunity.com/workshop/browse/' +
|
||||
'?appid=457140' +
|
||||
'&browssemethod=trend' +
|
||||
'§ion=readytouseitems' +
|
||||
'&actualsort=trend' +
|
||||
'&p=1' +
|
||||
'&numperpage=6'
|
||||
|
||||
// 这里使用 Cloudflare做为代理,避免 CORS 问题
|
||||
const proxyUrl =
|
||||
`https://aged-dream-7a55.liukele015.workers.dev/?mode=workshop&url=${encodeURIComponent(steamUrl)}`
|
||||
|
||||
const res = await fetch(proxyUrl)
|
||||
const html = await res.text()
|
||||
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html')
|
||||
const items = doc.querySelectorAll('.workshopItem')
|
||||
|
||||
mods.value = Array.from(items).map(item => {
|
||||
const titleEl = item.querySelector('.workshopItemTitle')
|
||||
const linkEl = item.querySelector('a')
|
||||
const authorEl = item.querySelector('.workshopItemAuthorName a')
|
||||
|
||||
const imgDiv = item.querySelector('.workshopItemImg')
|
||||
const bg = imgDiv?.style?.backgroundImage || ''
|
||||
const bgUrl = bg.match(/url\(["']?(.*?)["']?\)/)?.[1] || null
|
||||
|
||||
const imgPreview = item.querySelector('img.preview_image')
|
||||
const previewUrl = imgPreview?.src || null
|
||||
|
||||
const imgAny = item.querySelector('img')
|
||||
const anyUrl = imgAny?.src || null
|
||||
|
||||
let thumbnail =
|
||||
bgUrl || previewUrl || anyUrl ||
|
||||
'https://via.placeholder.com/200x120?text=No+Image'
|
||||
|
||||
if (thumbnail.startsWith('//')) {
|
||||
thumbnail = 'https:' + thumbnail
|
||||
}
|
||||
|
||||
thumbnail = `https://images.weserv.nl/?url=${encodeURIComponent(thumbnail)}`
|
||||
|
||||
return {
|
||||
title: titleEl?.innerText.trim() ?? '未知模组',
|
||||
link: linkEl?.href ?? '#',
|
||||
author: authorEl?.innerText.trim() ?? '匿名作者',
|
||||
thumbnail,
|
||||
|
||||
stats: null,
|
||||
statsLoading: false
|
||||
}
|
||||
})
|
||||
|
||||
loading.value = false
|
||||
loadStatsSequentially(mods.value)
|
||||
|
||||
} catch (err) {
|
||||
console.error('抓取 Workshop 失败:', err)
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="workshop-container">
|
||||
<div v-if="loading" class="loading-box">
|
||||
正在连接 Steam 创意工坊...
|
||||
</div>
|
||||
|
||||
<div v-else class="mod-grid">
|
||||
<a
|
||||
v-for="mod in mods"
|
||||
:key="mod.link"
|
||||
:href="mod.link"
|
||||
target="_blank"
|
||||
class="mod-card"
|
||||
>
|
||||
<div class="mod-img">
|
||||
<img :src="mod.thumbnail" loading="lazy" />
|
||||
</div>
|
||||
|
||||
<div class="mod-card__body">
|
||||
<div class="mod-card__title">{{ mod.title }}</div>
|
||||
<div class="mod-card__meta">By {{ mod.author }}</div>
|
||||
|
||||
<div class="mod-card__stats">
|
||||
<template v-if="mod.stats">
|
||||
<span>访客 {{ mod.stats.visitors }}</span>
|
||||
<span>订阅 {{ mod.stats.subscribers }}</span>
|
||||
<span>收藏 {{ mod.stats.favorites }}</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="mod.statsLoading">
|
||||
<span>统计加载中…</span>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<span>—</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.workshop-container {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.loading-box {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--vp-c-text-2);
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mod-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.mod-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.mod-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.mod-card {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
text-decoration: none !important;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.mod-card:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: translateY(-4px);
|
||||
background: var(--vp-c-bg-mute);
|
||||
}
|
||||
|
||||
.mod-img {
|
||||
width: 100%;
|
||||
height: 125px;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.mod-img img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.mod-card__body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.mod-card__title {
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.3;
|
||||
height: 2.6em;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.mod-card__meta {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.8rem;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.mod-card__stats {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.mod-card__stats span {
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* VitePress 布局宽度自定义
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* 1. 文档内容的最大宽度 (默认是 1152px) */
|
||||
/* 设置为 1280px 或 1440px 可以让代码块更宽,减少横向滚动条 */
|
||||
--vp-content-max-width: 1280px;
|
||||
|
||||
/* 2. 整个页面的最大宽度 (包括侧边栏和正文) */
|
||||
--vp-layout-max-width: 1600px;
|
||||
|
||||
/* --vp-c-brand-1: #8f2a3b;
|
||||
--vp-c-brand-2: #8f2a3b; */
|
||||
}
|
||||
|
||||
|
||||
.VPHero .name {
|
||||
/* 撤销默认的渐变色背景 */
|
||||
background: none !important;
|
||||
-webkit-background-clip: initial !important;
|
||||
background-clip: initial !important;
|
||||
-webkit-text-fill-color: initial !important;
|
||||
}
|
||||
|
||||
|
||||
/* 强制覆盖首页标题的渐变效果 */
|
||||
.VPHero .name span.color-w {
|
||||
background: none !important;
|
||||
-webkit-text-fill-color: initial !important;
|
||||
color: var(--vp-c-text-1) !important;
|
||||
}
|
||||
|
||||
.VPHero .name span.color-r {
|
||||
background: none !important;
|
||||
-webkit-text-fill-color: #ff4d4f !important; /* 红色 */
|
||||
color: #ff4d4f !important;
|
||||
}
|
||||
|
||||
/* 正文里的颜色 */
|
||||
span.color-white {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
span.color-white {
|
||||
color: #ff4d4f !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 如果你希望“代码块”在宽屏下表现更好,可以加上这个微调 */
|
||||
@media (min-width: 1280px) {
|
||||
.VPDoc .container {
|
||||
/* 这里的 margin 会自动平衡左右空间 */
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* 3. (可选) 首页 Hero 部分的宽度调整 */
|
||||
:root {
|
||||
--vp-home-hero-max-width: 1152px;
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
// .vitepress/theme/index.mts
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import './custom.css' // 这里的引入才是有效的
|
||||
// @ts-ignore
|
||||
import Contributors from './components/Contributors.vue'
|
||||
import SteamNews from './components/SteamNews.vue'
|
||||
import LearningTimeline from './components/LearningTimeline.vue'
|
||||
import WorkshopList from './components/WorkshopList.vue'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp({ app }: any) {
|
||||
app.component('Contributors', Contributors)
|
||||
app.component('SteamNews', SteamNews)
|
||||
app.component('LearningTimeline', LearningTimeline)
|
||||
app.component('WorkshopList', WorkshopList)
|
||||
}
|
||||
|
||||
}
|
||||
115
ONIModTutorial/ONIModTutorial-main/README.md
Normal file
115
ONIModTutorial/ONIModTutorial-main/README.md
Normal file
@ -0,0 +1,115 @@
|
||||
<div align="center">
|
||||
<h1>🧩 ONIModTutorial</h1>
|
||||
|
||||
[](https://vitepress.vuejs.org/)
|
||||
|
||||
_📘 《缺氧(Oxygen Not Included)》Mod 开发教程 · 从入门到进阶 · 长期维护_
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 🎯 仓库目标
|
||||
|
||||
- 帮助新人快速入门 ONI Mod 开发
|
||||
- 系统整理缺氧 Mod 常用技术与思路
|
||||
- 记录 Harmony 补丁、反编译、组件分析等实战经验
|
||||
- 作为长期维护的知识库,而不是零散笔记
|
||||
|
||||
---
|
||||
|
||||
## 📚 教程内容概览
|
||||
|
||||
教程内容主要包括(并持续补充中):
|
||||
|
||||
- 缺氧 Mod 开发基础认知
|
||||
- 开发环境搭建
|
||||
- Mod 基本结构与加载流程
|
||||
- C# 基础(面向 ONI Mod)
|
||||
- Harmony 补丁详解与实战
|
||||
- dnSpy / 反编译分析游戏源码
|
||||
- 常见系统解析(建筑、复制人、植物、逻辑、UI 等)
|
||||
- 实用代码示例与踩坑记录
|
||||
|
||||
---
|
||||
|
||||
## 📁 仓库结构说明
|
||||
|
||||
- content/
|
||||
教程正文内容,所有章节均以 `.md` 文件形式存在
|
||||
- preface.md —— 教程引言
|
||||
- csharp-basics.md —— C# 基础(ONI Mod 向)
|
||||
- harmony-reference.md —— Harmony 补丁整理与示例
|
||||
- 其他章节持续增加中…
|
||||
|
||||
- .github/workflows/
|
||||
GitHub Actions 配置(如自动构建文档)
|
||||
|
||||
- vitepress/
|
||||
VitePress 文档站点配置
|
||||
|
||||
- public/
|
||||
文档站点用到的静态资源
|
||||
|
||||
---
|
||||
|
||||
## 🚀 本地测试
|
||||
|
||||
### 直接阅读(推荐)
|
||||
|
||||
- 直接在 GitHub 中按顺序阅读 `content` 目录下的 Markdown 文件即可
|
||||
|
||||
### 本地测试运行文档站点(可选)
|
||||
|
||||
1. 安装 Node.js(推荐使用 v16 及以上长期支持版本【LTS】windows系统确保安装时勾选添加到 PATH)
|
||||
[Node.js下载及安装](https://nodejs.org/zh-cn/download)
|
||||
|
||||
安装完成后,打开终端(如 bash、PowerShell、CMD 等),输入 `node -v` 确认安装成功并正确添加Node到系统环境变量中。
|
||||
|
||||
2. 克隆仓库
|
||||
|
||||
```
|
||||
git clone https://github.com/ChiYuKe/ONIModTutorial.git
|
||||
```
|
||||
|
||||
3. 安装依赖
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
4. 启动本地文档
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
5. 浏览器访问终端提示的本地地址,通常是 `http://localhost:端口号/ONIModTutorial/`
|
||||
```
|
||||
http://localhost:5173/ONIModTutorial/
|
||||
```
|
||||
6. 在浏览器尽情阅读
|
||||
|
||||
---
|
||||
|
||||
## 🧠 适合人群
|
||||
|
||||
✔ 想制作《缺氧》Mod 的玩家
|
||||
✔ 有一定 C# 基础,想深入理解 ONI 内部逻辑
|
||||
✔ 希望系统学习 Harmony Patch 的开发者
|
||||
✔ 已在做 Mod,但想补齐底层理解的人
|
||||
|
||||
---
|
||||
|
||||
## 🤝 贡献说明
|
||||
|
||||
欢迎任何形式的贡献,包括但不限于:
|
||||
|
||||
- 修正教程错误
|
||||
- 补充新的章节
|
||||
- 提供实战案例
|
||||
- 优化结构和表述
|
||||
|
||||
你可以通过 **Issue** 或 **Pull Request** 提交改动。
|
||||
|
||||
---
|
||||
7
ONIModTutorial/ONIModTutorial-main/announcements.md
Normal file
7
ONIModTutorial/ONIModTutorial-main/announcements.md
Normal file
@ -0,0 +1,7 @@
|
||||
::: info 提示
|
||||
下方动态实时同步自 Steam 官方,若图片加载较慢请稍候。
|
||||
:::
|
||||
|
||||
<ClientOnly>
|
||||
<SteamNews />
|
||||
</ClientOnly>
|
||||
3
ONIModTutorial/ONIModTutorial-main/api-examples.md
Normal file
3
ONIModTutorial/ONIModTutorial-main/api-examples.md
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
109
ONIModTutorial/ONIModTutorial-main/asset-extraction.md
Normal file
109
ONIModTutorial/ONIModTutorial-main/asset-extraction.md
Normal file
@ -0,0 +1,109 @@
|
||||
# 🎨 游戏资产提取 (Asset Extraction)
|
||||
|
||||
在进行《缺氧》(Oxygen Not Included)的 Mod 开发时,获取原版资产(贴图、动画、文本)是进行二次创作和保持视觉一致性的基础。本章将详细介绍开发者常用的资产解包与处理工具。
|
||||
|
||||
## 一 . 资源解包
|
||||
|
||||
### 1.1 AssetRipper
|
||||
|
||||
目前处理 Unity 引擎资源最强大的开源工具之一。它不仅能提取静态贴图,还能在一定程度上还原 Unity 项目的资源目录结构。
|
||||
|
||||
* **[📥 官方仓库 (GitHub Releases)](https://github.com/AssetRipper/AssetRipper/releases)**
|
||||
* **适用范围**:UI 图标(Sprite)、建筑贴图(Texture2D)、音频文件(AudioClip)... 。
|
||||
|
||||
#### 🛠️ 提取指南
|
||||
|
||||
* **载入资源:** 运行 `AssetRipper` ,选择游戏的 `OxygenNotIncluded_Data` 文件夹。
|
||||
* **资源导出:** 在视图中,通过 `Export`导出全部资源。
|
||||
|
||||
### 1.2 AssetStudio
|
||||
如果你不需要还原整个项目,只想快速找到某几张特定的贴图、立绘或听一段音频,AssetStudio 是更高效的选择。它支持实时预览,让你在导出前就能看到资源内容。
|
||||
|
||||
* **[📥 官方仓库 (GitHub Releases)](https://github.com/aelurum/AssetStudio/releases)**
|
||||
|
||||
* **适用范围**:快速筛选特定贴图、导出 3D 模型(Mesh)、预览动画片段... 。
|
||||
|
||||
#### 🛠️ 提取指南
|
||||
* **载入资源:** 运行 `AssetStudioModGUI` ,点击菜单栏的 `File` -> `Load Folder` ,选择游戏的 `OxygenNotIncluded_Data` 文件夹。
|
||||
* **资源导出:** 点击 `Filter Type` 勾选你需要的类型(如 `Texture2D` `Sprite` `TexAsset`),最后 `Export` -> `Filtered assets` 导出勾选的类型
|
||||
|
||||
|
||||
|
||||
### 💡 进阶技巧:如何选择工具?
|
||||
| 需求场景 | 推荐工具| 优势 |
|
||||
|----------|----------|----------|
|
||||
| 我想研究游戏的 UI 布局或逻辑 | `AssetRipper` | 能够尽可能还原 `.prefab` 和文件夹结构。 |
|
||||
| 我只想拿到动画纹理资源 | `AssetStudio` | 无需等待漫长的导出过程,导出方便 |
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 二 . 动画处理
|
||||
|
||||
《缺氧》的动态表现基于 Klei 自定义的 **KAnim** 系统,而非 Unity 原生动画。每个动画对象由三个核心文件组成:`图集(*_0.png)`、`结构(*_build.bytes)` 和 `序列(*_anim.bytes)`。
|
||||
|
||||
### 🛠️ 推荐转换工具
|
||||
|
||||
#### kanimal-SE (KAnim转换)
|
||||
**kanimal-SE** 是处理 KAnim 的标准工具,支持将字节流转换为可编辑的 SCML 格式。
|
||||
* **[📥 官方仓库 (kanimal-SE)](https://github.com/skairunner/kanimal-SE)**
|
||||
|
||||
::: code-group
|
||||
```powershell [Windows (PowerShell)]
|
||||
# 解包:将 KAnim 转换为可编辑的 SCML (用于 Spriter 编辑)
|
||||
.\kanimal-cli.exe scml [file_name].png [file_name]_anim.bytes [file_name]_build.bytes
|
||||
|
||||
# 打包:将修改后的 SCML 重新编译为游戏可读的 KAnim
|
||||
.\kanimal-cli.exe kanim [file_name].scml
|
||||
```
|
||||
:::
|
||||
|
||||
|
||||
::: tip 🚀 推荐:KAnim_GUI (可视化操作)
|
||||
**KAnim_GUI** 对 `kanimal-SE` 提供了极简的图形化界面,支持快速解压和重组 KAnim,极大地降低了命令行操作的门槛。需要将 `kanimal-SE(kanimal-cli.exe)` 放入 `KAnim_GUI(KAnimGui.exe)` 同目录下才能正常工作
|
||||
* **[📥 官方仓库 (KAnim_GUI)](https://github.com/ChiYuKe/KAnim_GUI)**
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 三 . 多语言与文本处理
|
||||
|
||||
无论是寻找建筑的 `PrefabID`,还是为 Mod 增加多语言支持,都需要处理 `.po` 格式的翻译文件。
|
||||
|
||||
### 🔍 文本利器:Poedit
|
||||
* **[📥 官方下载 (Poedit 官网)](https://poedit.net/)**
|
||||
|
||||
::: info 实战技巧
|
||||
* **快速溯源**:使用 `Ctrl + F` 搜索游戏内的中文词条(如“电解器”),即可快速定位其对应的 `StringKey`(例如:`STRINGS.BUILDINGS.PREFABS.ELECTROLYZER.NAME`)。
|
||||
* **规范化翻译**:使用 Poedit 维护你的 `zh.po` 文件,可以确保翻译格式完全符合 Klei 的底层加载规范,避免乱码。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 开发准则与约束
|
||||
|
||||
::: danger 法律声明:严禁商业用途
|
||||
所有提取自《缺氧》的游戏资产(包括但不限于原画、音效、动画脚本、底层代码)所有权均归 **Klei Entertainment** 所有,严禁用于任何形式的商业盈利行为。
|
||||
:::
|
||||
|
||||
::: warning 尊重版权
|
||||
* **禁止二次分发**:请勿在公开平台分发未经大幅修改的官方原始资源包。
|
||||
* **致谢声明**:若你的作品基于官方素材修改,请在说明文档中按照惯例标注 `"Credit to Klei Entertainment"`。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
<div align="center" style="margin-top: 3rem; padding: 2rem; border-top: 1px solid var(--vp-c-divider);">
|
||||
<p style="font-size: 1.1rem; font-style: italic; color: var(--vp-c-text-2); margin-bottom: 1.5rem;">
|
||||
“逻辑是生存的第一步,而代码是改写规则的起点。”
|
||||
</p>
|
||||
<a href="dnspy-analysis">
|
||||
<button style="background: var(--vp-c-brand-1); color: white; padding: 10px 24px; border-radius: 20px; font-weight: bold; cursor: pointer; border: none;">
|
||||
游戏源码工具 →
|
||||
</button>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
37
ONIModTutorial/ONIModTutorial-main/community-guidelines.md
Normal file
37
ONIModTutorial/ONIModTutorial-main/community-guidelines.md
Normal file
@ -0,0 +1,37 @@
|
||||
# 开源协议与社区规范
|
||||
|
||||
在开发《缺氧》Mod 并将其发布至 Steam 创意工坊之前,请务必阅读并遵守科雷(Klei)官方发布的通用准则。
|
||||
|
||||
## 官方准则 (Klei Official Guidelines)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **官方原文链接:** [General Mod and & UGC Guidelines](https://support.klei.com/hc/en-us/articles/360029556052-General-Mod-and-UGC-Guidelines)
|
||||
|
||||
### 1. 允许的 Mod 类型
|
||||
* **新实体**:包括自定义美术资产和特殊能力的各色角色。
|
||||
* **平衡调整**:对现有角色、建筑或物品的数值微调。
|
||||
* **逻辑修改**:改变生物行为、世界生成规则或 UI 界面。
|
||||
* **新机制**:为玩家提供全新的游戏挑战或功能扩展。
|
||||
|
||||
### 2. 严禁行为 (Prohibited)
|
||||
为了维护社区环境,以下行为是被严格禁止的,违规可能导致 Mod 被下架:
|
||||
* **侵权与盗用**:未经授权使用他人作品,或包含未授权的版权材料(如其他游戏的素材)。
|
||||
* **破解与绕过**:试图解锁 DLC、皮肤或绕过付费所有权验证。
|
||||
* **代码混淆**:**禁止加密或模糊化代码**。Klei 官方和玩家必须能够审计代码以确保安全性。
|
||||
* **商业化/付费墙**:严禁任何形式的付费 Mod、付费皮肤或“赞助者提前体验”等金钱交易。
|
||||
|
||||
### 3. 开发礼仪 (Etiquette)
|
||||
* **默认不授权**:若原作者未明确说明,一律视为“未获得授权”。
|
||||
* **二次开发**:引用或修改他人作品前请务必联系作者,并注明出处。
|
||||
* **维护义务**:作者没有义务必须更新 Mod,但建议在发布时提供完整的说明和截图。
|
||||
* **关于弃坑**:若原作者未明确禁止且 Mod 已失效,社区成员可以进行兼容性更新并重新上传,但需尊重原作者的移除请求。
|
||||
|
||||
---
|
||||
|
||||
## 开源协议建议
|
||||
|
||||
对于你自己的代码,我建议在 GitHub 上选择一个开源协议(如 **MIT** 或 **GPL-3.0**)。
|
||||
* **MIT**:最宽松,别人可以随意使用你的代码,只需保留你的署名。
|
||||
* **GPL-3.0**:最严格,如果别人用了你的代码,他们的 Mod 也必须开源。
|
||||
|
||||
**尊重他人劳动成果,是社区良性发展的基石。**
|
||||
175
ONIModTutorial/ONIModTutorial-main/content/items.md
Normal file
175
ONIModTutorial/ONIModTutorial-main/content/items.md
Normal file
@ -0,0 +1,175 @@
|
||||
# 🚀 编写第一个 物品
|
||||
|
||||
本章将带你亲手编写一个物品或者说掉落物。
|
||||
|
||||
---
|
||||
|
||||
## 一 . 准备工作
|
||||
|
||||
在开始之前,你需要手动创建一个文件夹,用于存放你开发的 Mod 文件:
|
||||
* **路径**:`%USERPROFILE%\Documents\Klei\OxygenNotIncluded\mods\`
|
||||
* **操作**:在此目录下创建一个名为 **`Dev`** 的文件夹。
|
||||
|
||||
---
|
||||
|
||||
## 二 . 创建项目
|
||||
|
||||
1. 打开 **Visual Studio 2022**。
|
||||
2. 选择 **创建新项目** -> 搜索 **类库**(建议选择支持 **.NET Standard 2.1** 的类库模板)-> 点击下一步。
|
||||
3. **项目名称**:输入 `MyFirstItem`。
|
||||
4. **目标框架**:务必选择 **.NET Standard 2.1**。
|
||||
|
||||
---
|
||||
|
||||
## 三 . 导入游戏核心库 (References)
|
||||
|
||||
你需要把游戏的 `.dll` 文件“介绍”给 Visual Studio,它才能理解游戏代码。
|
||||
|
||||
1. 在右侧 **解决方案资源管理器** 中,当然你的可能在左侧,右键点击 **引用 (References)** -> **添加引用**。
|
||||
2. 点击 **浏览**,进入游戏目录:
|
||||
`...\steamapps\common\OxygenNotIncluded\OxygenNotIncluded_Data\Managed\`
|
||||
3. **选中并添加以下文件:**
|
||||
* `Assembly-CSharp.dll`
|
||||
* `Assembly-CSharp-firstpass.dll`
|
||||
* `0Harmony.dll`
|
||||
* `UnityEngine.dll`
|
||||
* `UnityEngine.CoreModule.dll`
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 四 . 编写代码
|
||||
|
||||
在我们的工程中,创建如下 `.cs` 文件结构:
|
||||
|
||||
``` 文件结构
|
||||
|
||||
MyFirstItem/
|
||||
├── Patch.cs # Harmony补丁入口文件,用来在游戏加载时注册你的Mod逻辑
|
||||
├── MyFirstItemConfig.cs # 物品的定义文件(ID、动画、属性等核心配置)
|
||||
├── STRINGS.cs # 本地化文本(名称、描述、效果说明)
|
||||
└── MyFirstItem.cs # 自定义组件逻辑(功能代码实现)
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### MyFirstItemConfig.cs 的处理
|
||||
|
||||
我们需要先继承 `IEntityConfig`。
|
||||
|
||||
在 Oxygen Not Included 中:
|
||||
|
||||
> `IEntityConfig` 是用来“定义一个实体”的接口。
|
||||
|
||||
简单说:
|
||||
|
||||
> 它告诉游戏:我要注册一个新的实体,请按这些规则创建它。
|
||||
|
||||
---
|
||||
|
||||
### 基础结构
|
||||
|
||||
```csharp
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ONIModTutorialCode
|
||||
{
|
||||
public class MyFirstItemConfig : IEntityConfig
|
||||
{
|
||||
public GameObject CreatePrefab()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void OnPrefabInit(GameObject inst)
|
||||
{
|
||||
// 预制体初始化时调用
|
||||
}
|
||||
|
||||
public void OnSpawn(GameObject inst)
|
||||
{
|
||||
// 实例生成时调用(生成到地图上才会触发)
|
||||
}
|
||||
|
||||
public string[] GetDlcIds()
|
||||
{
|
||||
return null; // 所有 DLC 可用
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 实现 CreatePrefab()
|
||||
|
||||
在 `CreatePrefab()` 中,我们使用 `EntityTemplates.CreateLooseEntity()` 来注册它。
|
||||
|
||||
这里的动画资源我们使用仙水掌的 `cactus_kanim` 中的 `idle_empty` 状态。
|
||||
|
||||
常规掉落物通常使用 `"object"`。
|
||||
|
||||
(这里为什么是 `idle_empty` 而不是 `object`,我们会在后面说明。)
|
||||
|
||||
---
|
||||
|
||||
### 完整代码(可直接复制)
|
||||
|
||||
```csharp
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ONIModTutorialCode
|
||||
{
|
||||
public class MyFirstItemConfig : IEntityConfig
|
||||
{
|
||||
// 物品ID
|
||||
public const string ID = "MYFIRSTITEM";
|
||||
|
||||
public GameObject CreatePrefab()
|
||||
{
|
||||
GameObject gameObject = EntityTemplates.CreateLooseEntity(
|
||||
ID,
|
||||
STRINGS.ITEMS.MYFIRSTITEM.NAME,
|
||||
STRINGS.ITEMS.MYFIRSTITEM.DESC,
|
||||
5f,
|
||||
true,
|
||||
Assets.GetAnim("cactus_kanim"),
|
||||
"idle_empty",
|
||||
Grid.SceneLayer.SolidConduits,
|
||||
EntityTemplates.CollisionShape.RECTANGLE,
|
||||
0.6f,
|
||||
0.6f,
|
||||
true,
|
||||
0,
|
||||
SimHashes.Creature,
|
||||
new List<Tag> { GameTags.IndustrialIngredient }
|
||||
);
|
||||
|
||||
return gameObject;
|
||||
}
|
||||
|
||||
public void OnPrefabInit(GameObject inst)
|
||||
{
|
||||
// 预制体初始化时调用
|
||||
}
|
||||
|
||||
public void OnSpawn(GameObject inst)
|
||||
{
|
||||
// 实例生成时调用
|
||||
}
|
||||
|
||||
public string[] GetDlcIds()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
---
|
||||
|
||||
> 🚧 **To Be Continued...**
|
||||
>
|
||||
> 先写一点,有点累了,睡一觉
|
||||
11
ONIModTutorial/ONIModTutorial-main/contribute.md
Normal file
11
ONIModTutorial/ONIModTutorial-main/contribute.md
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
layout: home
|
||||
hero:
|
||||
name: <span class="color-r">加入贡献</span>
|
||||
text: 共同打造这个教程文档
|
||||
tagline: 无论你是新手还是大佬,我们都欢迎。
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 加入群聊(QQ)
|
||||
link: https://qm.qq.com/q/9IXwoJzOus
|
||||
---
|
||||
10
ONIModTutorial/ONIModTutorial-main/contributors.md
Normal file
10
ONIModTutorial/ONIModTutorial-main/contributors.md
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
editLink: false
|
||||
---
|
||||
|
||||
# 贡献者名单
|
||||
|
||||
感谢所有为本项目做出贡献的小伙伴!
|
||||
|
||||
<Contributors repo="ChiYuKe/ONIModTutorial" />
|
||||
|
||||
182
ONIModTutorial/ONIModTutorial-main/csharp-basics-reference.md
Normal file
182
ONIModTutorial/ONIModTutorial-main/csharp-basics-reference.md
Normal file
@ -0,0 +1,182 @@
|
||||
---
|
||||
sidebar: false
|
||||
---
|
||||
<a href="javascript:history.back()" class="back-button">
|
||||
⬅ 返回
|
||||
</a>
|
||||
|
||||
<style scoped>
|
||||
.back-button {
|
||||
position: fixed;
|
||||
left: 20px;
|
||||
top: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 15px;
|
||||
background-color: var(--vp-c-brand);
|
||||
color: white !important;
|
||||
border-radius: 20px;
|
||||
text-decoration: none !important;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s;
|
||||
z-index: 100;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
transform: scale(1.05);
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
# ⚡ C# 核心语法
|
||||
---
|
||||
这不是一篇“C# 入门”,而是 **专门为《缺氧(ONI)》Mod 开发提炼的最小语法集**。
|
||||
|
||||
> 🎯 阅读目标:
|
||||
>
|
||||
> * 能**直接看懂 ONI 源码**
|
||||
> * 能**独立写 Harmony Patch**
|
||||
> * 知道哪些语法是“必须会”,哪些可以先无视
|
||||
|
||||
---
|
||||
|
||||
## 🧠 总览
|
||||
|
||||
> **ONI Mod 的 C# 本质只有四件事:**
|
||||
>
|
||||
> 1️⃣ 类 + 组件
|
||||
> 2️⃣ static + ref
|
||||
> 3️⃣ 数组 + 泛型
|
||||
> 4️⃣ null 判断
|
||||
|
||||
下面所有内容,都是在为这四点服务。
|
||||
|
||||
---
|
||||
|
||||
## 🧱 一、class 是一切的起点
|
||||
|
||||
### 你在 ONI 里只会写这三种类
|
||||
|
||||
```csharp
|
||||
public class XXXConfig : IBuildingConfig {}
|
||||
public class XXXPatch {}
|
||||
public class XXXComponent : KMonoBehaviour {}
|
||||
```
|
||||
|
||||
结论:
|
||||
|
||||
* **类必须是 `public`**
|
||||
* 不要纠结面向对象
|
||||
|
||||
---
|
||||
|
||||
## 🔧 二、static:为什么 Patch 必须是 static
|
||||
|
||||
### 普通方法(ONI 几乎不用)
|
||||
|
||||
```csharp
|
||||
public void DoSomething() {}
|
||||
```
|
||||
|
||||
### Patch 方法(必须这样)
|
||||
|
||||
```csharp
|
||||
public static void Postfix() {}
|
||||
```
|
||||
|
||||
原因一句话解释:
|
||||
|
||||
> Harmony **不会 new 你的类**,只能调用静态方法。
|
||||
|
||||
👉 **Patch 写错 90% 都是这里的问题**
|
||||
|
||||
---
|
||||
|
||||
## 🧬 三、ref:为什么 ONI Patch 能“改游戏”
|
||||
|
||||
### 没有 ref(改不到)
|
||||
|
||||
```csharp
|
||||
public static void Postfix(BuildingDef def)
|
||||
{
|
||||
def.EnergyConsumptionWhenActive = 1f; // ❌ 无效
|
||||
}
|
||||
```
|
||||
|
||||
### 有 ref(直接改内存)
|
||||
|
||||
```csharp
|
||||
public static void Postfix(ref BuildingDef __result)
|
||||
{
|
||||
__result.EnergyConsumptionWhenActive = 1f; // ✅ 生效
|
||||
}
|
||||
```
|
||||
|
||||
一句话结论:
|
||||
|
||||
> **Harmony 能改游戏,全靠 ref。**
|
||||
|
||||
---
|
||||
|
||||
## 📦 四、组件思维:ONI ≈ Unity
|
||||
|
||||
### 所有功能 = 组件
|
||||
|
||||
```csharp
|
||||
go.AddOrGet<ElementConverter>();
|
||||
go.AddOrGet<Operational>();
|
||||
```
|
||||
|
||||
### AddOrGet 的真实含义
|
||||
|
||||
```text
|
||||
有 → 拿
|
||||
无 → 加
|
||||
```
|
||||
|
||||
> ❗ ONI 世界里
|
||||
>
|
||||
> * **没有组件 = 没有功能**
|
||||
|
||||
---
|
||||
|
||||
## 🔍 五、泛型 `<T>`:不要怕,它只是“占位”
|
||||
|
||||
### 看到下面这两行,直接翻译成中文
|
||||
|
||||
```csharp
|
||||
go.AddOrGet<ElementConverter>();
|
||||
go.GetComponent<KPrefabID>();
|
||||
```
|
||||
|
||||
翻译:
|
||||
|
||||
> “我要 **ElementConverter** 这个组件”
|
||||
|
||||
> `<T>` = 类型占位符,仅此而已
|
||||
|
||||
---
|
||||
|
||||
## 🚧 六、null 判断:ONI Mod 的生命线
|
||||
|
||||
### 正确写法
|
||||
|
||||
```csharp
|
||||
var converter = go.GetComponent<ElementConverter>();
|
||||
if (converter == null)
|
||||
return;
|
||||
```
|
||||
|
||||
原因一句话:
|
||||
|
||||
> Patch 执行顺序不固定,**组件不一定存在**。
|
||||
|
||||
👉 **不写 null 判断 = 随机崩溃**
|
||||
|
||||
---
|
||||
|
||||
|
||||
77
ONIModTutorial/ONIModTutorial-main/csharp-basics.md
Normal file
77
ONIModTutorial/ONIModTutorial-main/csharp-basics.md
Normal file
@ -0,0 +1,77 @@
|
||||
# 🛠️ C# 开发基础(ONI Mod 专用)
|
||||
|
||||
在编写《缺氧(Oxygen Not Included, ONI)》Mod 代码之前,你需要掌握一些 **C# 的核心概念**。本篇内容将**专门围绕 ONI Mod 开发中最常见、最实用的语法与结构**展开,避免泛泛而谈。
|
||||
|
||||
---
|
||||
|
||||
## 📦 一、命名空间与类(Namespace & Class)
|
||||
|
||||
《缺氧》的源码规模非常庞大,因此大量使用 **命名空间(namespace)** 来对功能进行划分。你的 Mod 代码也应当遵循这一习惯,以避免冲突、提升可维护性。
|
||||
|
||||
### ✅ 开始
|
||||
|
||||
::: danger 💡 建议
|
||||
如果不知道什么是 `空间命名(namespace)` `类名(Class)` `方法名(Method)` 是什么的,在这里你就可以止步了,可以花点时间快速了解一下 `C#` ,这会让你后续的开发事半功倍。
|
||||
:::
|
||||
|
||||
* [C# 杂七杂八](./csharp-basics-reference.md)
|
||||
* [Harmony的小整理](./harmony-reference.md)
|
||||
* [Harmony官方文档](https://harmony.pardeike.net/articles/patching.html)。
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 🔧 二、HarmonyPatch:精准定位游戏源码
|
||||
|
||||
在《缺氧》Mod 开发中,`[HarmonyPatch]` 属性(Attribute)就像一个**手术定位仪**。它告诉补丁框架:你需要拦截哪个“类”里的哪个“方法”。
|
||||
|
||||
### 1. 核心语法:三种定位方式
|
||||
|
||||
根据目标方法的复杂程度,你通常会用到以下三种写法:
|
||||
|
||||
| 定位类型 | 代码示例 (Attribute) | 适用场景 |
|
||||
| :--- | :--- | :--- |
|
||||
| **基础定位** | `[HarmonyPatch(typeof(Db), "Initialize")]` | **最常用**。目标类中只有一个同名方法时使用。 |
|
||||
| **重载定位** | `[HarmonyPatch(typeof(Assets), "GetSprite", new Type[] { typeof(HashedString) })]` | 当方法有**多个重载**(名字相同但参数不同)时,必须指定参数类型。 |
|
||||
| **属性定位** | `[HarmonyPatch(typeof(BuildingDef), "IsSolid", MethodType.Getter)]` | 专门用于拦截 C# 的 **属性 (Property)**。需指定是修改 `Getter` 还是 `Setter`。 |
|
||||
|
||||
---
|
||||
### 2. 🔍 实战:如何从 dnSpy 提取 Patch 信息?
|
||||
|
||||
当你使用 **dnSpy** 查看游戏源码 `Assembly-CSharp.dll` 时,请按照以下逻辑进行拆解:
|
||||
|
||||
|
||||
|
||||
* **类名 (Class)**:看代码最顶层的 `public class` 后面跟着的单词。
|
||||
* 源码:`public class GeneratedBuildings`
|
||||
* 对应参数:**`typeof(GeneratedBuildings)`**
|
||||
* **方法名 (Method)**:看括号左边的那个单词。
|
||||
* 源码:`public static void LoadGeneratedBuildings(...)`
|
||||
* 对应参数:**`"LoadGeneratedBuildings"`**
|
||||
* **参数类型 (Parameters)**:看括号里面的内容,如果是[重载](./harmony-reference.md#postfix-Overloading)方法则需要提取。
|
||||
* 源码:`(ICollection<Type> types)`
|
||||
* 对应参数:**`new Type[] { typeof(ICollection<Type>) }`**
|
||||
|
||||
---
|
||||
|
||||
### 3. 📝 标准代码结构模板
|
||||
|
||||
```csharp
|
||||
using HarmonyLib; // 必须引用 Harmony 库
|
||||
using UnityEngine; // 如果用到 Debug.Log 或 GameObject
|
||||
|
||||
namespace MyOniMod
|
||||
{
|
||||
// 1. 定位:我们要改哪个类的哪个方法?
|
||||
[HarmonyPatch(typeof(EntityConfigManager), "LoadGeneratedEntities")]
|
||||
public class MyEntityPatch
|
||||
{
|
||||
// 2. 注入:在原方法执行后(Postfix)做点什么?
|
||||
public static void Postfix()
|
||||
{
|
||||
// 你的逻辑代码
|
||||
Debug.Log("ONI Mod: 游戏实体加载完成!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
# 🛠️ 开发环境搭建
|
||||
|
||||
在编写代码之前,我们需要准备好开发工具,并配置针对《缺氧》的自动编译环境。
|
||||
|
||||
---
|
||||
## 软件准备清单
|
||||
|
||||
你需要安装以下工具。请确保它们已正确安装:
|
||||
|
||||
| 工具 | 作用 | 下载链接 | 推荐版本 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Visual Studio 2026** | 核心开发 IDE | [官方下载地址](https://visualstudio.microsoft.com/zh-hans/vs/community/) | 社区版 (Community) |
|
||||
| **dnSpy** | 逆向源码 / 找代码 | [GitHub 下载页](https://github.com/dnSpy/dnSpy/releases) | 最新版 (win64) |
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ 核心注意事项
|
||||
|
||||
> **💡 关键点:关于 .NET 版本**
|
||||
> 《缺氧》官方现在把 [**.NET Standard 2.1**](https://learn.microsoft.com/zh-cn/dotnet/standard/net-standard) 视为模组开发的更佳起点;与此同时,游戏本体仍保留 Unity 的 **.NET Framework API Compatibility Target** 作为兼容层。
|
||||
>
|
||||
> * **如果不选这个版本**:你的 Mod 在加载时也许会报错,或者更容易遇到引用和兼容性问题。
|
||||
> * **VS 安装提示**:在安装 Visual Studio Installer 时,请务必勾选 **“.NET 桌面开发”** 工作负荷,并确保已安装用于 **.NET Standard 2.1** 的相关开发组件。
|
||||
> * [图文安装教程](./visual-studio-installation.md) <span id="back-point"></span>|
|
||||
24
ONIModTutorial/ONIModTutorial-main/dnspy-analysis.md
Normal file
24
ONIModTutorial/ONIModTutorial-main/dnspy-analysis.md
Normal file
@ -0,0 +1,24 @@
|
||||
# 🎨 游戏代码反编译
|
||||
|
||||
|
||||
|
||||
## 一. 代码反编译
|
||||
|
||||
如果你想了解游戏的逻辑(如建筑如何运作、掉落概率等),你需要反编译游戏的 `.dll` 文件。
|
||||
|
||||
### 🛠️ 工具:dnSpy
|
||||
|
||||
目前社区最推荐的 `.NET` 反编译器和调试器。
|
||||
|
||||
* **[📥 官方仓库 (GitHub Releases)](https://github.com/dnSpyEx/dnSpy/releases)**
|
||||
* **适用范围**:查看 `C#` 代码、搜索特定方法、甚至直接修改并重新编译代码。
|
||||
|
||||
#### 💡 操作步骤
|
||||
|
||||
* 找到游戏的程序集文件:通常位于 `OxygenNotIncluded_Data/Managed/Assembly-CSharp.dll` 。
|
||||
|
||||
* 将该文件拖入 **dnSpy** 。
|
||||
|
||||
* 使用 `Ctrl + Shift + K` 进行全局搜索,快速定位你想研究的类名(如 `ElectrolyzerConfig` )。
|
||||
|
||||
* 或者选择目标 `.dll` 文件,将其导出成 `Visual Studio` 工程文件。
|
||||
137
ONIModTutorial/ONIModTutorial-main/first-mod-tutorial.md
Normal file
137
ONIModTutorial/ONIModTutorial-main/first-mod-tutorial.md
Normal file
@ -0,0 +1,137 @@
|
||||
# 🚀 编写第一个 Mod:节能电解器
|
||||
|
||||
本章将带你亲手编写一个 Mod,把电解器的 120W 功耗改为 1W。
|
||||
|
||||
---
|
||||
|
||||
## 一 . 准备工作
|
||||
|
||||
在开始之前,你需要手动创建一个文件夹,用于存放你开发的 Mod 文件:
|
||||
* **路径**:`%USERPROFILE%\Documents\Klei\OxygenNotIncluded\mods\`
|
||||
* **操作**:在此目录下创建一个名为 **`Dev`** 的文件夹。
|
||||
|
||||
---
|
||||
|
||||
## 二 . 创建项目
|
||||
|
||||
1. 打开 **Visual Studio 2022**。
|
||||
2. 选择 **创建新项目** -> 搜索 **类库**(建议选择支持 **.NET Standard 2.1** 的类库模板)-> 点击下一步。
|
||||
3. **项目名称**:输入 `MyFirstMod`。
|
||||
4. **目标框架**:务必选择 **.NET Standard 2.1**。
|
||||
|
||||
---
|
||||
|
||||
## 三 . 导入游戏核心库 (References)
|
||||
|
||||
你需要把游戏的 `.dll` 文件“介绍”给 Visual Studio,它才能理解游戏代码。
|
||||
|
||||
1. 在右侧 **解决方案资源管理器** 中,当然你的可能在左侧,右键点击 **引用 (References)** -> **添加引用**。
|
||||
2. 点击 **浏览**,进入游戏目录:
|
||||
`...\steamapps\common\OxygenNotIncluded\OxygenNotIncluded_Data\Managed\`
|
||||
3. **选中并添加以下文件:**
|
||||
* `Assembly-CSharp.dll`
|
||||
* `Assembly-CSharp-firstpass.dll`
|
||||
* `0Harmony.dll`
|
||||
* `UnityEngine.dll`
|
||||
* `UnityEngine.CoreModule.dll`
|
||||
|
||||
---
|
||||
|
||||
## 四 . 编写补丁代码
|
||||
|
||||
<!-- <a href="./harmony-replenish#postfix" class="guide-link">
|
||||
🚀 了解 Postfix
|
||||
</a>
|
||||
|
||||
<style scoped>
|
||||
.guide-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 10px 0;
|
||||
padding: 8px 16px;
|
||||
background-color: var(--vp-c-brand-soft); /* 浅色背景 */
|
||||
color: var(--vp-c-brand-1) !important; /* 品牌主色文本 */
|
||||
border: 1px solid var(--vp-c-brand);
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
text-decoration: none !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.guide-link:hover {
|
||||
background-color: var(--vp-c-brand); /* 悬浮变深色 */
|
||||
color: white !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style> -->
|
||||
::: info 进阶指引
|
||||
想要进一步了解补丁?请点击阅读:
|
||||
[了解 Postfix →](./harmony-reference.md#postfix)
|
||||
:::
|
||||
删除项目自动生成的 `Class1.cs` 里的所有内容,粘贴下面的代码:
|
||||
|
||||
```csharp
|
||||
using HarmonyLib;
|
||||
|
||||
namespace MyFirstMod
|
||||
{
|
||||
// 告诉游戏:要在 ElectrolyzerConfig 的 CreateBuildingDef 执行完后动刀
|
||||
[HarmonyPatch(typeof(ElectrolyzerConfig), "CreateBuildingDef")]
|
||||
public class MyFirstPatch
|
||||
{
|
||||
// Postfix 代表后置补丁
|
||||
public static void Postfix(ref BuildingDef __result)
|
||||
{
|
||||
// 将电解器的功耗改为 1 瓦
|
||||
__result.EnergyConsumptionWhenActive = 1f;
|
||||
Debug.Log("电解器的功耗已被修改为 1 瓦");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 📝 调试技巧
|
||||
> 如果 Mod 运行不正常,可以在这里查看日志排查:
|
||||
|
||||
`%USERPROFILE%\AppData\LocalLow\Klei\Oxygen Not Included\player.log`
|
||||
|
||||
---
|
||||
## 🎁 五 . 下载项目模板 (可选)
|
||||
|
||||
前面叽里咕噜没看懂?
|
||||
没关系,我这里提供了一份Mod模板,如果你是按教程来的,那么在这里我将默认你安装了Visual Studio 2026版本。
|
||||
|
||||
### 安装方法:
|
||||
|
||||
* [点击此处下载模板 .zip 文件](/templates/ONIModTemplate.zip)(不要解压)。
|
||||
* 将其放入Visual Studio 模板文件夹:
|
||||
> `%USERPROFILE%\Documents\Visual Studio 18\Templates\ProjectTemplates`
|
||||
<details>
|
||||
<summary>📸 点击展开查看ProjectTemplates目录结构</summary>
|
||||
<br>
|
||||
<div align="center">
|
||||
<img src="/images/first-mod/Project.png" alt="发布界面" width="600" />
|
||||
</div>
|
||||
</details>
|
||||
|
||||
* 重启你的Visual Studio, 创建项目时候你将可以看到:
|
||||
|
||||
<details>
|
||||
<summary>📸 点击展开查看预览图</summary>
|
||||
<br>
|
||||
<div align="center">
|
||||
<img src="/images/first-mod/ModTemplate.png" alt="发布界面" width="600" />
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
||||
## 六 . 编译与部署
|
||||
|
||||
1. **生成 DLL**:点击顶部菜单栏的 **生成 (Build)** -> **生成解决方案**, 或者直接在 **解决方案资源管理器** 当中右键项目然后 **生成 (Build)**。
|
||||
2. **找到文件**:进入项目文件夹的 `bin/Debug` 目录,找到 `MyFirstMod.dll`。
|
||||
3. **放入游戏**:在 `Documents\Klei\OxygenNotIncluded\mods\Dev\` 当中创建一个`MyFirstMod`文件夹,将 `MyFirstMod.dll`放入其中,记住不要少了上一章讲的
|
||||
`mod.yaml` 和 `mod_info.yaml`
|
||||
|
||||
---
|
||||
395
ONIModTutorial/ONIModTutorial-main/harmony-reference.md
Normal file
395
ONIModTutorial/ONIModTutorial-main/harmony-reference.md
Normal file
@ -0,0 +1,395 @@
|
||||
---
|
||||
sidebar: false
|
||||
---
|
||||
<a href="javascript:history.back()" class="back-button">
|
||||
⬅ 返回
|
||||
</a>
|
||||
|
||||
<style scoped>
|
||||
.back-button {
|
||||
position: fixed;
|
||||
left: 20px;
|
||||
top: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 15px;
|
||||
background-color: var(--vp-c-brand);
|
||||
color: white !important;
|
||||
border-radius: 20px;
|
||||
text-decoration: none !important;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s;
|
||||
z-index: 100;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
transform: scale(1.05);
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
# 🧩 Harmony 补丁整理
|
||||
|
||||
> **本篇是 Harmony 在《缺氧(ONI)》Mod 开发中的实战速查表**
|
||||
> 👉 目标:**快速了解以及什么时候该用哪一种 Patch**
|
||||
|
||||
---
|
||||
|
||||
## 📖 阅读指引
|
||||
|
||||
- **基础操作**:了解 `Harmony` 初始化及 `Prefix / Postfix` 的基本用法。
|
||||
- **常用参数**:了解 `__instance / __state / AccessTools` 读写游戏数据。
|
||||
- **特殊场景**:了解 `Transpiler / Finalizer` 处理底层逻辑与异常的方式。
|
||||
|
||||
---
|
||||
|
||||
## 🧠 一、Harmony 是什么?
|
||||
|
||||
Harmony 是一个 **运行时方法注入库**,用于在**不修改源代码**的情况下:
|
||||
|
||||
- 插入逻辑
|
||||
- 拦截原方法
|
||||
- 修改返回值
|
||||
- 重写部分执行流程
|
||||
|
||||
---
|
||||
|
||||
## 🧩 二、Patch 类型总览
|
||||
|
||||
| Patch 类型 | 执行时机 | 常见用途 |
|
||||
|----------|----------|----------|
|
||||
| Prefix | 原方法执行前 | 拦截 / 改参数 / 阻止执行 |
|
||||
| Postfix | 原方法执行后 | 补充逻辑 / 改返回值 |
|
||||
| Transpiler | IL 层 | 改硬编码 / if / 常量 |
|
||||
| Finalizer | 所有逻辑后 | 捕获异常 / 兜底 |
|
||||
---
|
||||
|
||||
## ⚙️ 三、Harmony 初始化
|
||||
|
||||
> 在 ONI 中,继承 UserMod2 后,游戏会自动帮你执行 harmony.PatchAll()。你不需要再手动调用它,除非你有特殊的动态补丁需求。
|
||||
|
||||
|
||||
```csharp
|
||||
using HarmonyLib;
|
||||
using KMod;
|
||||
|
||||
namespace MyMod
|
||||
{
|
||||
public class MyFirstMod : UserMod2
|
||||
{
|
||||
public override void OnLoad(Harmony harmony)
|
||||
{
|
||||
// 1. 必须保留基类调用,它会处理自动 PatchAll
|
||||
base.OnLoad(harmony);
|
||||
|
||||
// 2. 这里通常用来放“全局初始化”逻辑
|
||||
// 比如:打印日志确认 Mod 已加载
|
||||
Debug.Log("MyFirstMod 已成功加载!");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🟦 四、Prefix(执行前拦截)
|
||||
|
||||
`Prefix` 用于在原代码跑起来之前先一步执行。
|
||||
|
||||
### 4.1 基础用法:单纯插入逻辑
|
||||
如果你只想在某个动作发生时顺便做点事(比如记录日志),补丁返回 void 即可。
|
||||
|
||||
```csharp
|
||||
[HarmonyPatch(typeof(Operational), nameof(Operational.SetActive))]
|
||||
public static class Log_Patch
|
||||
{
|
||||
public static void Prefix(bool value)
|
||||
{
|
||||
// 只是看一眼参数,不影响原逻辑执行
|
||||
Debug.Log($"建筑状态尝试切换为: {value}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
### 4.2 进阶用法:干预原逻辑
|
||||
|
||||
如果你想改变游戏的结果,需要注意 bool 返回值和 ref 关键字的作用。
|
||||
|
||||
| 方式 | 代码关键点| 实际效果 |
|
||||
|----------|----------|----------|
|
||||
| 强行篡改 | `ref bool value` | 改指令。 原代码照样跑,但你偷偷换掉了它的参数(比如让关机指令变成开机) |
|
||||
| 彻底拦截 | `return false` | 断后路。 原代码被直接掐断,后面的逻辑(动画、音效、状态改变)全都不再执行 |
|
||||
|
||||
|
||||
**案例对比:**
|
||||
我们依然是在`[HarmonyPatch(typeof(Operational), nameof(Operational.SetActive))]`上打补丁。
|
||||
```csharp
|
||||
|
||||
// 情况 A:篡改参数(原逻辑继续跑,但用你的数)
|
||||
public static void Prefix(ref bool value)
|
||||
{
|
||||
value = true; // 哪怕玩家点了关闭,建筑也会因为参数被改而保持开启
|
||||
}
|
||||
|
||||
// 情况 B:彻底拦截(原逻辑直接罢工)
|
||||
public static bool Prefix(bool value, bool force_ignore)
|
||||
{
|
||||
return false; // 原方法体内的代码一行都不会跑,功能被完全“冻结”
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
<h2 id="postfix">🟩 五、Postfix(执行后补充)</h2>
|
||||
|
||||
`Prefix` 在原方法执行完毕后运行。它是最推荐的补丁方式,因为它不会中断游戏的正常逻辑,兼容性最好。
|
||||
|
||||
### 5.1 实战代码:追加额外逻辑
|
||||
- 作用: 在原逻辑跑完后,顺便执行一些你自己的操作。
|
||||
|
||||
```csharp
|
||||
[HarmonyPatch(typeof(ElectrolyzerConfig), "CreateBuildingDef")]
|
||||
public class ElectrolyzerCreateBuildingDef_Patch
|
||||
{
|
||||
public static void Postfix(ref BuildingDef __result)
|
||||
{
|
||||
// 将电解器的功耗改为 1 瓦
|
||||
__result.EnergyConsumptionWhenActive = 1f;
|
||||
Debug.Log("电解器的功耗已被修改为 1 瓦");
|
||||
}
|
||||
}
|
||||
```
|
||||
- 场景: 初始化后的属性修正、添加自定义的组件、触发额外的通知。
|
||||
- 优势: 无论你的代码写得好不好,原版代码都已经安全跑完了,不会导致核心功能丢失。
|
||||
|
||||
---
|
||||
### 5.2 修改返回值:修改计算结果
|
||||
|
||||
- 作用: 劫持原方法的返回值,让游戏拿到你“调包”后的结果。
|
||||
```csharp
|
||||
[HarmonyPatch(typeof(Overheatable), nameof(Overheatable.OverheatTemperature), MethodType.Getter)]
|
||||
public static class Overheatable_OverheatTemperature_Patch
|
||||
{
|
||||
// ref __result 指向原方法算出来的那个返回值
|
||||
public static void Postfix(ref float __result)
|
||||
{
|
||||
__result = 9999f; // 让建筑永远不会因为温度过高而损坏(强行改掉判定数值)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
| 枚举项 | 怎么认出它(看 ILSpy/dnSpy) | 干嘛的 |
|
||||
| :--- | :--- | :--- |
|
||||
| **Normal** | 方法名后面带个括号,比如 OnSpawn() | 在某个动作执行前后“插一脚” |
|
||||
| **Getter** | 代码里长这样:float Speed { get; } | 用于修改游戏“读取”到的数据 |
|
||||
| **Setter** | 代码里长这样:set { ... } | 用于在游戏尝试修改某个数值时进行拦截 |
|
||||
| **Constructor** | 跟类名长得一模一样的方法 | 在对象刚创建、还没放进世界时,提前修改它的默认字段或初始化逻辑 |
|
||||
| **StaticConstructor** |前面带 static 的构造函数 | 专门用来修改游戏加载时就定死的全局静态常量 |
|
||||
| **Enumerator** | 里面有一堆 yield return 的方法 | 专门对付 ONI 里的异步动作、动画序列或那些不是瞬间完成的任务 |
|
||||
|
||||
|
||||
|
||||
<h3 id="postfix-Overloading">5.3 处理重载</h3>
|
||||
在 ONI 源码中,经常会出现多个方法名相同但参数不同的方法。如果不指定参数类型,Harmony 会报错。
|
||||
|
||||
**案例:拦截 `ModUtil.AddBuildingToPlanScreen`**
|
||||
这个方法在游戏源码中有多个重载版本,我们对比来看:
|
||||
|
||||
```csharp
|
||||
public static class ModUtil
|
||||
{
|
||||
// 重载 A:只有 2 个参数
|
||||
public static void AddBuildingToPlanScreen(HashedString category, string building_id) { ... }
|
||||
|
||||
// 重载 B:有 3 个参数
|
||||
public static void AddBuildingToPlanScreen(HashedString category, string building_id, string subcategoryID) { ... }
|
||||
|
||||
// 重载 c:有 5 个参数
|
||||
public static void AddBuildingToPlanScreen(HashedString category, string building_id, string subcategoryID, string relativeBuildingId, ModUtil.BuildingOrdering ordering = ModUtil.BuildingOrdering.After) { ... }
|
||||
}
|
||||
|
||||
// 【对应的 HarmonyPatch 写法】
|
||||
// 拦截“重载 B”,必须明确写出那 3 个参数的类型:
|
||||
[HarmonyPatch(typeof(ModUtil), "AddBuildingToPlanScreen", new Type[] { typeof(HashedString), typeof(string), typeof(string) })]
|
||||
// 试试举一反三 重载a会是怎么样的?
|
||||
|
||||
// ❌ 错误写法
|
||||
[HarmonyPatch(typeof(ModUtil), "AddBuildingToPlanScreen")]
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🛠️ 总结:Prefix 与 Postfix 怎么选?
|
||||
- 选 Prefix 的唯一理由:你需要在事情发生之前拦住它(比如阻止一个动作,或者在原逻辑使用参数前改掉参数)。
|
||||
|
||||
- 其他情况全选 Postfix:只要不需要拦截原逻辑,就往原逻辑后面接代码。这是保持 Mod 稳定、不坏档的金法则。
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## 🔁 六、状态桥接(__state 传值)
|
||||
有时候你需要在执行之后知道执行之前发生了什么(比如:存货前有多少,存货后剩多少)。`__state` 就是专门用来在 `Prefix` 和 `Postfix` 之间带货的“临时储物柜”。
|
||||
|
||||
|
||||
### 6.1 实战用法:前后状态对比
|
||||
- 作用: 记录一个初始值,等原逻辑跑完后再拿出来算差值。
|
||||
```csharp
|
||||
[HarmonyPatch(typeof(Storage), "Store")]
|
||||
public static class Storage_Monitor_Patch
|
||||
{
|
||||
// 1. 在执行前,把当前帧数塞进 __state 储物柜
|
||||
public static void Prefix(out int __state)
|
||||
{
|
||||
__state = Time.frameCount;
|
||||
}
|
||||
|
||||
// 2. 在执行后,从 __state 储物柜拿出刚才存的帧数
|
||||
public static void Postfix(int __state)
|
||||
{
|
||||
int cost = Time.frameCount - __state;
|
||||
if (cost > 0)
|
||||
{
|
||||
Debug.Log($"这次存货居然花了 {cost} 帧的时间!");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
### 💡 为什么用它?
|
||||
|
||||
* **性能追踪:** 记录方法运行前后的时间差。
|
||||
* **逻辑判定:** 记录执行前的数值(比如生命值),执行后发现数值没变,就触发补救逻辑。
|
||||
* **简洁性:** 它只在这一次补丁调用中有效,跑完就销毁,不会像全局变量那样污染你的代码。
|
||||
|
||||
### 🔑 参数细节说明(避坑)
|
||||
|
||||
* **Prefix 里:** 必须用 out 或者 ref(比如 out int __state),否则存不进去。
|
||||
* **Postfix 里:** 直接用(比如 int __state)即可拿到之前存的值。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔓 七、访问 private 字段(AccessTools与 ___)
|
||||
在 ONI 源码中,很多变量被声明为 `private` 你直接用 `__instance` 变量名 是访问不到的。这时有两种常用的“破门”方法。
|
||||
|
||||
|
||||
### 7.1 方法一:三下划线暗号(最快、最推荐)
|
||||
如果只是想简单读写一个实例变量,Harmony 提供了一个极简写法:在参数名前加 三个下划线。
|
||||
```csharp
|
||||
[HarmonyPatch(typeof(Storage), "OnSpawn")]
|
||||
public static class Storage_Easy_Patch
|
||||
{
|
||||
// ___capacityKg 对应 Storage 类里的私有变量 capacityKg
|
||||
public static void Postfix(Storage __instance, ref float ___capacityKg)
|
||||
{
|
||||
___capacityKg = 9999f; // 直接像改普通变量一样改掉它
|
||||
}
|
||||
}
|
||||
```
|
||||
### 7.2 方法二:使用 AccessTools(性能更好、更强大)
|
||||
当你需要在多个地方频繁访问同一个变量,或者要处理 `static`(静态)私有变量时,建议先提前定义一个“后门”。
|
||||
```csharp
|
||||
using System.Reflection;
|
||||
|
||||
public static class Storage_Hard_Patch
|
||||
{
|
||||
// 1. 先定义后门(只找一次,性能更优)
|
||||
private static readonly FieldInfo CapacityField =
|
||||
AccessTools.Field(typeof(Storage), "capacityKg");
|
||||
|
||||
[HarmonyPatch(typeof(Storage), "OnSpawn")]
|
||||
public static class Patch
|
||||
{
|
||||
public static void Postfix(Storage __instance)
|
||||
{
|
||||
// 2. 暴力写入新值
|
||||
CapacityField.SetValue(__instance, 9999f);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 💡 怎么选?
|
||||
|
||||
* **三下划线 (`___`)**:**日常主力。** 只要能跑通,就优先用它,代码短且不容易写错。
|
||||
* **AccessTools**:**重型工具。**
|
||||
- 专门对付 `static private`(静态私有)变量。
|
||||
- 适合在补丁函数体外面拿取数据。
|
||||
- 如果你需要极致的性能优化,提前定义一个 `static readonly FieldInfo` 会比三下划线快那么一点点。
|
||||
---
|
||||
|
||||
## 🧬 Transpiler(改底层指令)
|
||||
这是 Harmony 中最硬核的补丁。
|
||||
- 它不是在方法前后插话,而是直接冲进原代码内部,把某几行代码“抠掉”或者“掉包”。
|
||||
|
||||
### 8.1 它的作用
|
||||
如果你发现 `Prefix` 和 `Postfix` 都没法改掉某个逻辑,通常是因为那个逻辑是硬编码在方法中间的。
|
||||
|
||||
* **改常量:** 比如代码里写死了一个 `if (speed > 10f)`,你想把 `10f` 改成 `100f`。
|
||||
* **删代码:** 比如你想把原版中某一行烦人的检测逻辑彻底抹除。
|
||||
* **加指令:** 在方法的第 50 行和第 51 行中间塞进你自己的逻辑。
|
||||
|
||||
### 8.2 实战代码:修改硬编码数值
|
||||
- 场景: 游戏原本规定挖矿只掉一半掉落物`0.5f`,我们通过补丁把它改成全掉 `1.0f`。
|
||||
```csharp
|
||||
[HarmonyPatch(typeof(WorldDamage), "OnDigComplete")]
|
||||
public static class WorldDamage_OnDigComplete_Patch
|
||||
{
|
||||
// Transpiler 接收的是一组 IL 指令集
|
||||
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
|
||||
{
|
||||
foreach (var ins in instructions)
|
||||
{
|
||||
// 目标:找到指令是“加载浮点数 0.5”的那一行
|
||||
if (ins.opcode == OpCodes.Ldc_R4 && (float)ins.operand == 0.5f)
|
||||
{
|
||||
// 改成 1.0f
|
||||
ins.operand = 1.0f;
|
||||
}
|
||||
yield return ins;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
### ⚠️ 为什么说要慎用?
|
||||
* **报错难找:** 普通的补丁写错,通常只是功能失效;`Transpiler` 写错,可能会导致游戏直接闪退或整个类崩掉。
|
||||
* **极易冲突:** 如果有两个 Mod 同时对同一个方法的同一行代码动了“手术”,游戏大概率会当场报废。
|
||||
* **门槛高:** 你需要看懂 IL(中间语言) 指令(类似于 `ldarg.0` `call` `stfld` 等),就像在看天书。
|
||||
|
||||
---
|
||||
|
||||
## 🧯 九、Finalizer(异常兜底)
|
||||
|
||||
```csharp
|
||||
[HarmonyPatch(typeof(SomeClass), "SomeMethod")]
|
||||
public static class SomeMethod_Finalizer
|
||||
{
|
||||
public static void Finalizer(Exception __exception)
|
||||
{
|
||||
if (__exception != null)
|
||||
{
|
||||
Debug.LogError(__exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 十、ONI Mod 常见翻车点
|
||||
|
||||
- Patch 方法忘记 `static`
|
||||
- 参数签名与原方法不一致
|
||||
- Prefix `return false` 但没处理返回值
|
||||
- Transpiler 修改逻辑过多
|
||||
|
||||
|
||||
---
|
||||
|
||||
29
ONIModTutorial/ONIModTutorial-main/index.md
Normal file
29
ONIModTutorial/ONIModTutorial-main/index.md
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
# name: "ONIModTutorial"
|
||||
name: <span class="color-w">ONI</span><span class="color-r">Mod</span><span class="color-w">Tutorial</span>
|
||||
text: "缺氧 MOD 教程"
|
||||
tagline: "Talk is cheap. Show me the code."
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 开始阅读
|
||||
link: /preface
|
||||
- theme: alt
|
||||
text: API Examples
|
||||
# link: /api-examples
|
||||
|
||||
features:
|
||||
- title: 开发环境搭建
|
||||
details: 介绍如何配置 Visual Studio 2022 开发环境,以及如何引用游戏核心 DLL 文件。
|
||||
|
||||
- title: 源码反编译与分析
|
||||
details: 使用 dnSpy 等工具查阅游戏底层代码逻辑,学习如何定位 Patch(补丁)注入点。
|
||||
|
||||
- title: 实践与心得
|
||||
details: 分享《缺氧》Mod 开发中的实战案例,涵盖 Harmony 补丁使用及常见问题记录。
|
||||
|
||||
---
|
||||
|
||||
85
ONIModTutorial/ONIModTutorial-main/markdown-examples.md
Normal file
85
ONIModTutorial/ONIModTutorial-main/markdown-examples.md
Normal file
@ -0,0 +1,85 @@
|
||||
# Markdown Extension Examples
|
||||
|
||||
This page demonstrates some of the built-in markdown extensions provided by VitePress.
|
||||
|
||||
## Syntax Highlighting
|
||||
|
||||
VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting:
|
||||
|
||||
**Input**
|
||||
|
||||
````md
|
||||
```js{4}
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
msg: 'Highlighted!'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
**Output**
|
||||
|
||||
```js{4}
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
msg: 'Highlighted!'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Containers
|
||||
|
||||
**Input**
|
||||
|
||||
```md
|
||||
::: info
|
||||
This is an info box.
|
||||
:::
|
||||
|
||||
::: tip
|
||||
This is a tip.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
This is a warning.
|
||||
:::
|
||||
|
||||
::: danger
|
||||
This is a dangerous warning.
|
||||
:::
|
||||
|
||||
::: details
|
||||
This is a details block.
|
||||
:::
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
::: info
|
||||
This is an info box.
|
||||
:::
|
||||
|
||||
::: tip
|
||||
This is a tip.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
This is a warning.
|
||||
:::
|
||||
|
||||
::: danger
|
||||
This is a dangerous warning.
|
||||
:::
|
||||
|
||||
::: details
|
||||
This is a details block.
|
||||
:::
|
||||
|
||||
## More
|
||||
|
||||
Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).
|
||||
49
ONIModTutorial/ONIModTutorial-main/mod-structure.md
Normal file
49
ONIModTutorial/ONIModTutorial-main/mod-structure.md
Normal file
@ -0,0 +1,49 @@
|
||||
# 📂 Mod 目录结构
|
||||
|
||||
一个标准的《缺氧》Mod 目录通常包含以下核心文件。理解这些文件的角色是开发的第一步。
|
||||
|
||||
## 一 . 典型结构总览
|
||||
|
||||
每个模组都使用一个单独文件夹,例如,你的`MyFirstMod`路径应该如下所示:
|
||||
`...\Klei\OxygenNotIncluded\mods\Dev\MyFirstMod\`
|
||||
* `MyFirstMod.dll`
|
||||
* `mod.yaml`
|
||||
* `mod_info.yaml`
|
||||
```text
|
||||
MyFirstMod/
|
||||
├── mod_info.yaml # Mod 的元数据与版本兼容性配置
|
||||
├── mod.yaml # 用于配置 Mod 的名称、描述等静态信息
|
||||
├── MyFirstMod.dll # 编译后的程序集文件(逻辑代码)
|
||||
└── preview.png # Steam 创意工坊显示的预览图
|
||||
|
||||
```
|
||||
## 二 . 配置文件详解
|
||||
::: code-group
|
||||
```yaml [mod_info.yaml 模板]
|
||||
supportedContent: ALL # 支持范围:VANILLA_ID, EXPANSION1_ID, 或 ALL
|
||||
minimumSupportedBuild: 512719 # 运行该 Mod 所需的最低游戏版本号
|
||||
version: "1.0.0" # 你自己的 Mod 版本号
|
||||
```
|
||||
:::
|
||||
|
||||
|
||||
::: code-group
|
||||
```yaml [mod.yaml 模板]
|
||||
title: "MyFirstMod" # 游戏内 Mod 列表显示的名称
|
||||
description: "这是一个示例描述" # 游戏内显示的详细功能介绍
|
||||
staticID: "AuthorName.MyMod" # Mod 的唯一标识符(建议格式:作者名.Mod名)
|
||||
```
|
||||
:::
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 三 . 资源与可选目录 (进阶)
|
||||
当你的 Mod 不再仅仅是简单的逻辑修改,而是涉及新建筑、新美术或多语言支持时,需要建立以下目录结构:
|
||||
* **/translations/:** 用于存放多语言翻译文件(.pot 或 .po)。
|
||||
* **/anim/assets/:** 存放自定义建筑、生物或掉落物的动画资源。
|
||||
* **/assets/:** 额外素材
|
||||
* **/elements/:** 存放自定义元素的配置数据。
|
||||
* **/worldgen/:** 涉及地形生成、生物群落(Biomes)修改时的核心目录。包含 `biomes`、`features` 等子文件夹。
|
||||
2568
ONIModTutorial/ONIModTutorial-main/package-lock.json
generated
Normal file
2568
ONIModTutorial/ONIModTutorial-main/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
ONIModTutorial/ONIModTutorial-main/package.json
Normal file
13
ONIModTutorial/ONIModTutorial-main/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "vitepress dev",
|
||||
"build": "vitepress build",
|
||||
"preview": "vitepress preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitepress": "^1.6.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"rss-parser": "^3.13.0"
|
||||
}
|
||||
}
|
||||
39
ONIModTutorial/ONIModTutorial-main/preface.md
Normal file
39
ONIModTutorial/ONIModTutorial-main/preface.md
Normal file
@ -0,0 +1,39 @@
|
||||
# 🚀 开发引言 (Preface)
|
||||
|
||||
这份文档旨在为 **《缺氧》**(*Oxygen Not Included*)的 Mod 开发者提供一份系统化的技术指南。
|
||||
|
||||
::: info 编写初衷
|
||||
在尝试通过 Mod 扩展游戏玩法的过程中,会遇到许多问题,也会解决一些问题。本项目建立的初衷是:**整理并记录开发过程中的新发现**,让后来的开发者~~少走弯路~~。
|
||||
:::
|
||||
|
||||
### 🛠️ 文档核心价值
|
||||
| 特性 | 描述 |
|
||||
| :--- | :--- |
|
||||
| **源码参考** |内容主要参考当前版本的 `Assembly-CSharp.dll` 逆向结果,尽量确保时效性。 |
|
||||
| **侧重实践** |分享我寻找 **Hook 点** 或修改 **Prefab** 时的思路,希望能提供一些实操参考。 |
|
||||
| **尝试同步** | 由于游戏仍在更新,我会尽力在业余时间同步那些影响较大的逻辑变动。 |
|
||||
|
||||
### 🗺️ 学习路线图
|
||||
|
||||
1. <Badge type="tip" text="STEP 1" /> **环境准备**
|
||||
* 介绍我常用的反编译与开发工具配置。。
|
||||
2. <Badge type="warning" text="STEP 2" /> **基础机制**
|
||||
* 探讨 Harmony 补丁的使用以及对游戏对象结构的理解。
|
||||
3. <Badge type="danger" text="STEP 3" /> **进阶尝试**
|
||||
* 记录从属性修改到实体注入的一些实验性过程。
|
||||
---
|
||||
|
||||
::: danger 💡 学习建议
|
||||
**Mod 开发需要一定的 C# 基础。** 如果你是完全的编程新手,建议先花点时间了解 C# 的基本语法(变量、类、继承、Lambda 表达式),这会让你后续的开发事半功倍。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<p>从零开始,在代码中构建属于你的宇宙。</p>
|
||||
<a href="development-environment">
|
||||
<button style="background: var(--vp-c-brand-1); color: white; padding: 10px 24px; border-radius: 20px; font-weight: bold; cursor: pointer; border: none;">
|
||||
配置开发环境 →
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
98
ONIModTutorial/ONIModTutorial-main/resource-unity.md
Normal file
98
ONIModTutorial/ONIModTutorial-main/resource-unity.md
Normal file
@ -0,0 +1,98 @@
|
||||
# 缺氧Mod开发 - Unity资源使用指南
|
||||
|
||||
缺氧是使用Unity游戏引擎开发的。所以很多机制与资源都与Unity开发是通用的。
|
||||
|
||||
在我们开发缺氧mod的过程中,我们可以巧妙的使用Unity相关的功能来扩展或增强我们对mod的开发能力。
|
||||
|
||||
## 常用Unity资源
|
||||
|
||||
1. Asset Bundle 资源整合包(简称AB包)
|
||||
2. UI界面 - 自定义UI界面,比如sidescreen等
|
||||
3. 动画效果 - 粒子效果、材质制作
|
||||
|
||||
## 详细介绍
|
||||
|
||||
### 1. Asset Bundle包(简称AB包)
|
||||
|
||||
**简介:**
|
||||
Asset Bundle是Unity中一种资源打包格式,可以将游戏中的各种资源(如纹理、模型、预制体等)打包成一个独立的文件。在Mod开发中使用AB包有以下优势:
|
||||
- 减少Mod的文件体积,提高加载效率
|
||||
- 方便管理和更新资源
|
||||
- 支持热更新,无需重新编译Mod
|
||||
- 可以包含复杂的Unity资源,如预制体、动画等
|
||||
|
||||
**最常打包使用的内容:**
|
||||
1. 图片资源(Sprite)
|
||||
2. 预制体(Prefab)
|
||||
- 预制体经常用来制作自定义的UI界面、自定义的UI效果比如粒子效果Effect等,用预制体打包到AB包中
|
||||
|
||||
**使用步骤:**
|
||||
1. **安装Asset Bundle Browser工具**
|
||||
Unity菜单栏 --> Windows --> Package Manager --> + --> Add package from git url -->
|
||||
`https://github.com/Unity-Technologies/AssetBundles-Browser.git`
|
||||
|
||||
2. **打包资源**
|
||||
Unity菜单栏 --> Windows --> Asset Bundle Browser-> Tab: Configure 确认 --> Tab: Build -> Build
|
||||
|
||||
3. **Mod中引用**
|
||||
需要引用 `UnityEngine.AssetBundleModule.dll` 包
|
||||
|
||||
```csharp
|
||||
using UnityEngine;
|
||||
|
||||
// 载入ab包
|
||||
AssetBundle assetBundle = AssetBundle.LoadFromFile(file_path);
|
||||
|
||||
// 加载资源
|
||||
// sprite 图片资源
|
||||
assetBundle.LoadAsset<Sprite>(sprite_name);
|
||||
|
||||
// 挂载时机:[HarmonyPath(typeof(Asset),"OnPrefabInit")]
|
||||
// 资源挂载点:Assets.Sprites
|
||||
|
||||
// prefab 预制体
|
||||
GameObject prefab = assetBundle.LoadAsset<GameObject>("gameobject_name");
|
||||
gameObjectNameInstance = Util.KInstantiate(prefab);
|
||||
```
|
||||
|
||||
### 2. UI界面
|
||||
|
||||
**简介:**
|
||||
在缺氧Mod开发中,自定义UI界面是增强用户体验的重要部分。常见的UI界面包括:
|
||||
- SideScreen:物品或建筑的侧边信息栏
|
||||
- 自定义对话框
|
||||
- 配置面板
|
||||
|
||||
**实现方式:**
|
||||
1. 使用Unity创建UI预制体,打包到AB包中
|
||||
2. 在Mod中加载并实例化这些预制体
|
||||
3. 使用Harmony补丁修改游戏的UI系统,将自定义UI集成到游戏中
|
||||
|
||||
### 3. 动画效果
|
||||
|
||||
**简介:**
|
||||
动画效果可以为Mod添加视觉吸引力,包括:
|
||||
- 粒子效果:如特效、光晕、烟雾等
|
||||
- 材质动画:如发光、变色等效果
|
||||
- 模型动画:如机械运动、角色动作等
|
||||
|
||||
**实现方式:**
|
||||
1. 在Unity中创建动画效果和材质
|
||||
2. 打包到AB包中
|
||||
3. 在Mod中加载并应用这些效果
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **资源管理**:合理组织AB包,避免过大的包体
|
||||
2. **性能优化**:只加载必要的资源,及时卸载不需要的资源
|
||||
3. **版本控制**:为AB包添加版本号,方便更新和兼容性管理
|
||||
4. **错误处理**:添加资源加载失败的处理逻辑
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 确保使用与游戏版本兼容的Unity版本
|
||||
- 避免使用Unity的新特性,确保向后兼容性
|
||||
- 测试不同平台上的资源加载情况
|
||||
- 遵守Klei的Mod开发规范
|
||||
|
||||
通过合理使用Unity资源,你可以创建更加丰富和专业的缺氧Mod,为玩家带来更好的游戏体验。
|
||||
64
ONIModTutorial/ONIModTutorial-main/steam-workshop-upload.md
Normal file
64
ONIModTutorial/ONIModTutorial-main/steam-workshop-upload.md
Normal file
@ -0,0 +1,64 @@
|
||||
# 📤 Steam Workshop 上传指南
|
||||
|
||||
配置好 `mod_info.yaml` 后,你可以使用《缺氧》游戏内置的工具将 Mod 上传至创意工坊。
|
||||
|
||||
---
|
||||
|
||||
## 一 . 上传工具
|
||||
|
||||
<div align="center">
|
||||
<img src="/images/steam-upload/Tool.png" alt="Mod上传工具" width="600" />
|
||||
</div>
|
||||
此工具是缺氧游戏附赠的,在库中可搜索到
|
||||
|
||||
---
|
||||
|
||||
## 二 . 上传已有 Mod
|
||||
|
||||
如果你需要发布的 Mod,请参考下方界面:
|
||||
|
||||
<div align="center">
|
||||
<img src="/images/steam-upload/Tool_1.png" alt="发布界面" width="600" />
|
||||
</div>
|
||||
|
||||
按照以下步骤配置上传信息:
|
||||
|
||||
* <Badge type="danger" text="1" /> **选择项目**:点击此处选择你准备好的 Mod 文件夹。
|
||||
* <Badge type="danger" text="2" /> **路径确认**:确保显示的是整个 Mod 文件夹的完整路径。
|
||||
* <Badge type="danger" text="3" /> **预览图**:选择你 Mod 文件夹中的 `preview.png` 文件(建议尺寸 512x512)。
|
||||
* <Badge type="danger" text="4" /> **Mod 名称**:输入你希望在创意工坊上显示的标题。
|
||||
|
||||
|
||||
|
||||
|
||||
::: warning 注意事项
|
||||
1. **文件夹结构**:上传前请务必确认 `mod_info.yaml` 位于文件夹根目录。
|
||||
2. **预览图限制**:图片大小不得超过 1MB,否则 Steam 可能会上传失败。
|
||||
3. **版本号同步**:记得更新 `mod_info.yaml` 中的 `version` 字段,方便玩家识别更新内容。
|
||||
:::
|
||||
|
||||
## 三 . 创意工坊 图片、标题与描述
|
||||
* <Badge type="danger" text="1" /> **编辑标题与描述**:多国语言版本标题与描述
|
||||
**常用标签
|
||||
```html
|
||||
<!-- 标题 -->
|
||||
[h1][/h1]..[h*][/h*] [url=*][/url] [hr/]
|
||||
<!-- 链接 -->
|
||||
[url=链接]链接[/url] [img=链接]图片[/img]
|
||||
<!-- 分隔符 -->
|
||||
[hr/]
|
||||
<!-- 表格 -->
|
||||
[table]
|
||||
[tr]
|
||||
[td]单元格内容[/td]
|
||||
[tr]
|
||||
[table/]
|
||||
<!-- 列表 -->
|
||||
[list]
|
||||
[*]列表项1[/*]
|
||||
[*]列表项2[/*]
|
||||
[list/]
|
||||
<!-- 换行 -->
|
||||
[br/>
|
||||
```
|
||||
* <Badge type="danger" text="2" /> **添加编辑图片/视频**:图片尺寸比例1920/1080
|
||||
65
ONIModTutorial/ONIModTutorial-main/version-compatibility.md
Normal file
65
ONIModTutorial/ONIModTutorial-main/version-compatibility.md
Normal file
@ -0,0 +1,65 @@
|
||||
# 🔄 多版本兼容性与发布管理
|
||||
|
||||
为了确保 Mod 能够在不同版本的《缺氧》(如本体、DLC、测试版)中正常运行,你需要正确配置 `mod_info.yaml` 和 `archived_versions` 文件夹。
|
||||
|
||||
---
|
||||
|
||||
## 一 . 核心配置文件:mod_info.yaml
|
||||
> [!IMPORTANT]
|
||||
> **官方原文链接:** [Setting up mod_info.yaml and archived_versions](https://forums.kleientertainment.com/forums/topic/158363-setting-up-mod_infoyaml-and-archived_versions/)
|
||||
|
||||
|
||||
每个发布到 Steam Workshop 的 Mod 根目录必须包含 `mod_info.yaml`。它决定了 Mod 的“准入资格”。
|
||||
|
||||
::: code-group
|
||||
```yaml [mod_info.yaml 模板]
|
||||
supportedContent: ALL # 支持范围:VANILLA_ID, EXPANSION1_ID, 或 ALL
|
||||
minimumSupportedBuild: 512719 # 运行该 Mod 所需的最低游戏版本号
|
||||
version: "1.0.0" # 你自己的 Mod 版本号
|
||||
```
|
||||
:::
|
||||
|
||||
## 二 . 文件夹结构:archived_versions
|
||||
|
||||
当你需要同时兼容当前稳定版和之前的旧版本时,不能只靠一个 .dll。你需要使用文件夹分层。
|
||||
|
||||
📂 推荐目录结构
|
||||
::: details 点击查看结构树
|
||||
```yaml
|
||||
MyMod/
|
||||
├── mod_info.yaml # 最新版的信息(通常指向当前正式版)
|
||||
├── mod.yaml # 最新版的基本描述(通常指向当前正式版)
|
||||
├── MyMod.dll # 适用于当前最新正式版的代码
|
||||
└── archived_versions/ # 存档版本文件夹
|
||||
└── 512719/ # 文件夹名 = 适用的最低游戏版本号
|
||||
├── mod_info.yaml # 该版本的独立信息
|
||||
├── mod.yaml # 该版本的基本描述
|
||||
└── MyMod.dll # 该版本的专用代码
|
||||
```
|
||||
:::
|
||||
|
||||
⚙️ 运行逻辑说明
|
||||
* **扫描阶段:** 游戏启动时会优先扫描 `archived_versions` 文件夹。
|
||||
|
||||
* **匹配阶段:** 它会寻找文件夹名(版本号)小于或等于当前游戏版本,且最接近当前版本的文件夹。
|
||||
|
||||
* **回退阶段:** 如果没有匹配的归档文件夹,则运行根目录下的默认文件。
|
||||
|
||||
## 三 . 兼容性开发建议
|
||||
|
||||
::: tip 🛡️ 为什么使用这种结构?
|
||||
|
||||
* **版本隔离(防止“炸档”):** 当游戏大版本更新导致 API 变更时,旧版本用户会自动定向到 `archived_versions` 运行,确保插件不会因环境冲突而崩溃。
|
||||
* **双轨并行(分支维护):** 支持同时为 **测试版 (Testing Branch)** 和 **正式版 (Live Branch)** 维护两套完全独立的 DLL 逻辑,互不干扰。
|
||||
:::
|
||||
|
||||
### 🛠️ 自动化生成事件
|
||||
|
||||
在 Visual Studio 的项目属性中添加 **生成事件 (Post-build event)**,可实现编译后自动归档到对应文件夹:
|
||||
|
||||
```batch
|
||||
# 如果是 Release 配置,自动拷贝到 512719 归档目录
|
||||
if "$(ConfigurationName)" == "Release" (
|
||||
xcopy /Y "$(TargetDir)$(TargetFileName)" "$(ProjectDir)archived_versions\512719\"
|
||||
)
|
||||
```
|
||||
@ -0,0 +1,57 @@
|
||||
---
|
||||
sidebar: false
|
||||
---
|
||||
<a href="./development-environment#back-point" style="
|
||||
position: fixed;
|
||||
left: 20px;
|
||||
top: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 15px;
|
||||
background-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-radius: 20px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s;
|
||||
z-index: 100;
|
||||
" onmouseover="this.style.transform='scale(1.1)'" onmouseout="this.style.transform='scale(1)'">
|
||||
<span style="font-size: 1.2em;">返回</span>
|
||||
</a>
|
||||
|
||||
# 🛠️ Visual Studio 安装细节
|
||||
|
||||
在安装过程中,正确的勾选决定了你是否能顺利编译 Mod。请参考以下图解进行配置:
|
||||
|
||||
### 1. 选择工作负荷 (Workloads)
|
||||
首先,你需要告诉 VS 你要进行桌面程序开发。
|
||||
|
||||
<div align="center">
|
||||
<img src="/images/VSInstallation-details/VSInstall.png" alt="安装" width="600" />
|
||||
</div>
|
||||
|
||||
> **操作:** 勾选 **“.NET 桌面开发”**。这是开发 C# 插件最基础的环境,包含了编译器和必要的库。
|
||||
|
||||
---
|
||||
|
||||
### 2. 精确勾选组件 (Individual Components)
|
||||
这是最关键的一步,很多新手会因为少勾选组件而无法创建合适的项目模板。
|
||||
|
||||
<div align="center">
|
||||
<img src="/images/VSInstallation-details/VSInstall_.png" alt="选择组件" width="600" />
|
||||
</div>
|
||||
|
||||
**必须确保以下项已勾选:**
|
||||
* **.NET 桌面开发相关组件**:用于编写针对《缺氧》的代码。
|
||||
* **.NET Standard 2.1 相关开发支持**:提供项目模板和程序集引用支持。
|
||||
|
||||
---
|
||||
|
||||
### 3. 完成安装
|
||||
确认右下角的安装位置有足够的空间(通常需要几 GB),点击 **“修改”** 或 **“安装”**。安装完成后,建议重启电脑以确保环境变量生效。
|
||||
|
||||
::: tip 下一步
|
||||
安装完成后,你可以回到 [开发环境搭建](./development-environment#back-point) 继续学习如何创建你的第一个 Mod 项目。
|
||||
:::
|
||||
15
ONIModTutorial/ONIModTutorial-main/workshop-list.md
Normal file
15
ONIModTutorial/ONIModTutorial-main/workshop-list.md
Normal file
@ -0,0 +1,15 @@
|
||||
# 玩家社区与扩展 🚀
|
||||
|
||||
这里同步了《缺氧》创意工坊最新的模组动态,以及我们的开发路线图。
|
||||
|
||||
## 创意工坊动态
|
||||
<ClientOnly>
|
||||
<WorkshopList />
|
||||
</ClientOnly>
|
||||
|
||||
---
|
||||
|
||||
## 学习路线图 (Roadmap)
|
||||
<ClientOnly>
|
||||
<LearningTimeline />
|
||||
</ClientOnly>
|
||||
325
README.md
Normal file
325
README.md
Normal file
@ -0,0 +1,325 @@
|
||||
# ONI Agent — 缺氧 AI 助手工具集
|
||||
|
||||
[Oxygen Not Included](https://www.kleientertainment.com/games/oxygen-not-included)(《缺氧》)是一款高难度的太空殖民模拟游戏。**ONI Agent** 为 AI 提供了一套完整的"眼手"系统:92 个 RESTful API 端点覆盖了玩家能做的全部操作(建造/挖掘/研究/管线/自动化/生物管理/覆盖层切换等),截图和视角控制让 AI 看到游戏画面,事件守护进程让 AI 实时感知游戏动态。Mod + Python 工具链 + AI 三层架构,让任何支持 Tool Use 的模型都能像人类一样操作游戏。
|
||||
|
||||
## 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ AI Agent │
|
||||
│ (Claude / GPT / 任何支持 Tool Use 的模型) │
|
||||
└──────┬──────────────────────────┬───────────────┘
|
||||
│ HTTP API (RESTful JSON) │ CLI 工具链
|
||||
▼ ▼
|
||||
┌──────────────┐ ┌────────────────────────────┐
|
||||
│ ONI Mod │ │ Python Tools │
|
||||
│ (C# / │ │ ├── oni_api.py (客户端) │
|
||||
│ Harmony) │ │ ├── oni_analyzer.py (分析) │
|
||||
│ 端口 23876 │ │ └── oni_builder.py (蓝图) │
|
||||
└──────┬───────┘ └──────────┬─────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Oxygen Not Included (游戏) │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 前置条件
|
||||
|
||||
- [Oxygen Not Included](https://store.steampowered.com/app/457140) 游戏
|
||||
- Python 3.8+
|
||||
- Mod 文件部署到游戏 Mod 目录
|
||||
|
||||
### 1. 安装 Mod
|
||||
|
||||
将 `mod/` 目录下的内容(`mod_info.yaml` + `ONIAgentBridge.dll`)放入游戏的 Mod 文件夹:
|
||||
|
||||
```
|
||||
Windows: %USERPROFILE%\Documents\Klei\OxygenNotIncluded\mods\local\ONIAgentBridge\
|
||||
macOS: ~/Library/Application Support/Klei/OxygenNotIncluded/mods/local/ONIAgentBridge/
|
||||
Linux: ~/.config/unity3d/Klei/OxygenNot Included/mods/local/ONIAgentBridge/
|
||||
```
|
||||
|
||||
### 2. 启动游戏
|
||||
|
||||
启动 ONI,在 Mod 菜单中启用 **ONI Agent Bridge**,加载存档。
|
||||
|
||||
### 3. 验证连接
|
||||
|
||||
```bash
|
||||
python3 tools/oni_api.py health
|
||||
```
|
||||
|
||||
预期输出: `{ "status": "ok", "service": "oni-agent-bridge" }`
|
||||
|
||||
### 4. 查看游戏状态
|
||||
|
||||
```bash
|
||||
python3 tools/oni_api.py status
|
||||
python3 tools/oni_analyzer.py
|
||||
```
|
||||
|
||||
## 工具链详解
|
||||
|
||||
### `tools/oni_api.py` — Mod API 客户端
|
||||
|
||||
与游戏 Mod 通信的核心 CLI 工具,支持所有查询和操作。
|
||||
|
||||
```bash
|
||||
# 状态查询
|
||||
python3 tools/oni_api.py status # 游戏总览
|
||||
python3 tools/oni_api.py resources # 全部资源
|
||||
python3 tools/oni_api.py buildings # 全部建筑
|
||||
python3 tools/oni_api.py duplicants # 复制人详情
|
||||
python3 tools/oni_api.py research # 科技进度
|
||||
python3 tools/oni_api.py geysers # 喷泉列表
|
||||
python3 tools/oni_api.py critters # 小动物
|
||||
python3 tools/oni_api.py plants # 植物
|
||||
python3 tools/oni_api.py rooms # 房间
|
||||
|
||||
# 地图/格子数据
|
||||
python3 tools/oni_api.py cell 10 5 # 查看单个格子
|
||||
python3 tools/oni_api.py cells 0 0 20 20 # 查看 20x20 区域
|
||||
python3 tools/oni_api.py slice y 20 0 50 # 扫描第 20 行
|
||||
python3 tools/oni_api.py gas 50 50 30 # 气体分析
|
||||
python3 tools/oni_api.py explore 40 40 30 20 # AI 友好摘要
|
||||
|
||||
# 实体注册表(查询 ID)
|
||||
python3 tools/oni_api.py registry buildings Electrolyzer
|
||||
python3 tools/oni_api.py registry elements Water
|
||||
python3 tools/oni_api.py registry techs
|
||||
|
||||
# 执行操作
|
||||
python3 tools/oni_api.py dig 10 10 8 6 # 挖掘区域
|
||||
python3 tools/oni_api.py build Electrolyzer 15 12 # 建造建筑
|
||||
python3 tools/oni_api.py deconstruct Tile 10 10 # 拆除
|
||||
python3 tools/oni_api.py prioritize 15 12 9 # 优先
|
||||
python3 tools/oni_api.py research_select ImprovedOxygen # 科研
|
||||
python3 tools/oni_api.py mop 10 10 # 清理液体
|
||||
python3 tools/oni_api.py harvest 20 15 # 收获植物
|
||||
|
||||
# 游戏速度控制(AI 操作前必须先暂停)
|
||||
python3 tools/oni_api.py pause "Reason here" # 暂停游戏
|
||||
python3 tools/oni_api.py unpause 1 # 恢复(1x 速度)
|
||||
python3 tools/oni_api.py speed 3 # 直接设速度(不暂停)
|
||||
|
||||
# 批量任务
|
||||
python3 tools/oni_api.py batch docs/batch_example.json # 执行批量建造计划
|
||||
|
||||
# 优先级管理
|
||||
python3 tools/oni_api.py priority_global dig 7 # 全局挖掘优先级设为 7
|
||||
python3 tools/oni_api.py priority_type Electrolyzer 9 # 电解器建造优先级设为 9
|
||||
|
||||
# 管道/电线路径
|
||||
python3 tools/oni_api.py build_pipe_line liquid 42 40 48 40 # 铺设液体管道
|
||||
python3 tools/oni_api.py build_wire_line regular 42 40 48 40 # 铺设电线
|
||||
|
||||
# 高级指令(多个 API 组合)
|
||||
python3 tools/oni_commander.py diagnose # 全面诊断
|
||||
python3 tools/oni_commander.py fix_co2 # 自动处理 CO2
|
||||
python3 tools/oni_commander.py fix_overload # 处理过载电路
|
||||
python3 tools/oni_commander.py emergency_o2 # 紧急制氧
|
||||
python3 tools/oni_commander.py expand_base 40 40 10 8 # 一键拓展房间
|
||||
```
|
||||
|
||||
### `tools/oni_analyzer.py` — 智能分析器
|
||||
|
||||
自动拉取全方位游戏状态,分析六大维度并生成可操作建议。
|
||||
|
||||
```bash
|
||||
python3 tools/oni_analyzer.py
|
||||
```
|
||||
|
||||
分析内容:
|
||||
- 氧气供应状态 → 建议 SPOM 建造时机
|
||||
- 食物储备 → 建议扩建农场/养殖
|
||||
- 电力状况 → 建议新增发电类型
|
||||
- 温度异常 → 建议冷却方案
|
||||
- 水资源 → 建议过滤/收集策略
|
||||
- 科研进度 → 建议下一个研究方向
|
||||
|
||||
### `tools/oni_builder.py` — 蓝图建造器
|
||||
|
||||
预置常用建筑模块,一键部署。
|
||||
|
||||
```bash
|
||||
python3 tools/oni_builder.py list # 列出所有蓝图
|
||||
python3 tools/oni_builder.py build spom 42 42 # 建造 SPOM
|
||||
```
|
||||
|
||||
内置蓝图(7个):
|
||||
|
||||
| 蓝图 | 说明 | 尺寸 |
|
||||
|------|------|------|
|
||||
| `spom` | 标准 SPOM(电解制氧+氢气发电闭环) | 8×6 |
|
||||
| `spom_mini` | 紧凑型 SPOM(前期过渡用) | 5×4 |
|
||||
| `toilet_loop` | 卫生间水循环(厕所→净水器→厕所) | 8×4 |
|
||||
| `ranch_hatch` | 哈奇养殖模块 | 10×6 |
|
||||
| `farm_mealwood` | 浆果农场(6个种植箱+储物箱) | 6×4 |
|
||||
| `cooling` | 蒸汽涡轮冷却模块 | 8×6 |
|
||||
| `bedroom` | 标准卧室(床+梯子床+装饰) | 8×4 |
|
||||
|
||||
### `scripts/event_daemon.py` — 事件守护进程(AI 输入源)
|
||||
|
||||
持续轮询游戏事件并将其注入 AI 输入流。这是 AI 感知游戏状态变化的实时通道。
|
||||
|
||||
```bash
|
||||
python3 scripts/event_daemon.py
|
||||
```
|
||||
|
||||
工作原理:
|
||||
1. 每 5 秒轮询 `GET /api/state/events?since=<seq>` 获取新事件
|
||||
2. 对事件分类(critical / warning / info)
|
||||
3. critical 事件 → 红色告警 + **自动触发全量游戏快照**(周期/窒息/饥饿/压力)
|
||||
4. warning 事件 → 结构化输出给 AI
|
||||
5. 维护滚动事件历史(最多 200 条),AI 可随时查询摘要
|
||||
|
||||
事件类型:
|
||||
- `critical` — 复制人窒息、建筑损坏、电力中断 → 立即触发分析
|
||||
- `warning` — 低氧、食物短缺、高温 → 主动通知给 AI
|
||||
- `action_feedback` — 建造/挖掘等操作的结果反馈
|
||||
- `info` — 常规游戏状态变化
|
||||
|
||||
### 辅助脚本
|
||||
|
||||
```bash
|
||||
bash scripts/auto_repair.sh # 诊断 Mod 连接
|
||||
bash scripts/auto_analyze.sh # 一键健康检查+状态+分析
|
||||
bash scripts/watch.sh 60 # 每 60 秒持续监控
|
||||
bash scripts/setup.sh # 环境初始化
|
||||
python3 scripts/event_daemon.py # 事件守护进程(AI 输入源)
|
||||
```
|
||||
|
||||
## Mod API 完整端点
|
||||
|
||||
### 状态查询 (GET)
|
||||
|
||||
| 端点 | 说明 |
|
||||
|------|------|
|
||||
| `/health` | Mod 存活检测 |
|
||||
| `/api/state/game` | 全局状态(周期/人数/世界尺寸) |
|
||||
| `/api/state/resources` | 资源列表(含物态/分类) |
|
||||
| `/api/state/duplicants` | 复制人(位置/压力/食物/当前任务) |
|
||||
| `/api/state/buildings` | 建筑(位置/是否运行/功耗/分类) |
|
||||
| `/api/state/research` | 科技树(进度/解锁建筑) |
|
||||
| `/api/state/geysers` | 喷泉(位置/状态/排放率) |
|
||||
| `/api/state/alert` | 警报 |
|
||||
| `/api/state/critters` | 小动物(位置/种类/幸福度) |
|
||||
| `/api/state/plants` | 植物(位置/生长进度/是否枯萎) |
|
||||
| `/api/state/rooms` | 房间(类型/格数/建筑数) |
|
||||
| `/api/state/queue` | 任务队列(查看待处理任务) |
|
||||
| `/api/state/events?since=&limit=` | 事件流(AI 轮询增量事件) |
|
||||
| `/api/state/priorities` | 优先级配置(全局/建筑/复制人) |
|
||||
|
||||
### 地图/格子数据 (GET)
|
||||
|
||||
| 端点 | 说明 |
|
||||
|------|------|
|
||||
| `/api/state/cell?x=10&y=5` | 单格详情 |
|
||||
| `/api/state/cells?x=&y=&width=&height=` | 矩形区域批量查询 |
|
||||
| `/api/state/cells/slice?axis=&index=&start=&end=` | 行/列扫描 |
|
||||
| `/api/state/gas?x=&y=&radius=` | 区域气体分布 |
|
||||
|
||||
### 实体注册表 (GET,AI 参考)
|
||||
|
||||
| 端点 | 说明 |
|
||||
|------|------|
|
||||
| `/api/registry/buildings` | 全部建筑定义(尺寸/功耗/材料) |
|
||||
| `/api/registry/elements` | 全部元素定义(比热容/熔沸点/导热) |
|
||||
| `/api/registry/techs` | 全部科技定义(前置/解锁) |
|
||||
| `/api/registry/priorities` | 优先级级别含义对照表 |
|
||||
|
||||
### 操作 (POST)
|
||||
|
||||
| 端点 | 请求体 |
|
||||
|------|--------|
|
||||
| `/api/action/dig` | `{x, y, width, height}` |
|
||||
| `/api/action/build` | `{buildingId, x, y}` |
|
||||
| `/api/action/deconstruct` | `{buildingId, x, y}` |
|
||||
| `/api/action/prioritize` | `{x, y, priority}` |
|
||||
| `/api/action/research` | `{techId}` |
|
||||
| `/api/action/mop` | `{x, y}` |
|
||||
| `/api/action/harvest` | `{x, y}` |
|
||||
| `/api/action/schedule` | `{duplicantId, schedule}` |
|
||||
| `/api/action/wardrobe` | `{duplicantId, equipment}` |
|
||||
| `/api/action/batch` | `{actions: [{type, ...}]}` — 批量执行 |
|
||||
| `/api/action/priority_global` | `{target, priority}` — 全局默认优先级 |
|
||||
| `/api/action/priority_type` | `{buildingType, priority}` — 按建筑类型设优先级 |
|
||||
| `/api/action/pause` | `{reason?}` — 暂停游戏(AI 操作前必须调用) |
|
||||
| `/api/action/unpause` | `{speed?}` — 恢复游戏 |
|
||||
| `/api/action/speed` | `{speed}` — 设置速度 1x/2x/3x |
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
oni-agent/
|
||||
├── README.md # ← 本文件
|
||||
├── config.json # Mod 连接配置(host/port/timeout)
|
||||
├── SKILL.md # AI Agent skill 定义与操作手册
|
||||
│
|
||||
├── mod/ # 游戏 Mod(C# / Harmony)
|
||||
│ ├── mod_info.yaml # Mod 元信息(标题/版本/兼容性)
|
||||
│ └── ONIAgentBridge.cs # HTTP API 服务实现
|
||||
│
|
||||
├── tools/ # Python 工具链
|
||||
│ ├── oni_api.py # API 客户端(20+ 子命令)
|
||||
│ ├── oni_analyzer.py # 状态分析(6维度预警+建议)
|
||||
│ └── oni_builder.py # 蓝图建造(7个预置模块)
|
||||
│
|
||||
├── scripts/ # 辅助脚本
|
||||
│ ├── setup.sh # 环境初始化
|
||||
│ ├── auto_repair.sh # 连接诊断
|
||||
│ ├── auto_analyze.sh # 一键分析
|
||||
│ ├── watch.sh # 持续监控模式
|
||||
│ └── event_daemon.py # 事件守护进程(AI 实时输入源)
|
||||
│
|
||||
├── docs/
|
||||
│ ├── MOD_DEV_GUIDE.md # Mod 开发规范与约束
|
||||
│ ├── AI_KNOWLEDGE_BASE.md # 200+ 建筑/元素/科技 ID 知识库
|
||||
│ └── batch_example.json # 批量任务示例文件
|
||||
│
|
||||
└── skills/
|
||||
└── oni_agent.md # Agent skill 定义
|
||||
```
|
||||
|
||||
## 文档索引
|
||||
|
||||
| 文档 | 目标读者 | 内容 |
|
||||
|------|---------|------|
|
||||
| `SKILL.md` | AI Agent | 坐标系理解、API 用途、场景推理示例、操作表述规范 |
|
||||
| `docs/AI_KNOWLEDGE_BASE.md` | AI Agent | 建筑 ID 对照表、元素属性、科技树、游戏机制参考 |
|
||||
| `docs/MOD_DEV_GUIDE.md` | Mod 开发者 | 通信协议、端点规范、安全约束、检查清单 |
|
||||
| `README.md` | 人类用户 | ← 你在看这个 |
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: Mod 连接不上?**
|
||||
|
||||
运行 `bash scripts/auto_repair.sh` 诊断。确认:
|
||||
1. 游戏已启动并加载存档
|
||||
2. Mod 已在游戏中启用
|
||||
3. 端口 23876 未被占用
|
||||
|
||||
**Q: `buildingId` 从哪查?**
|
||||
|
||||
```bash
|
||||
python3 tools/oni_api.py registry buildings | grep -i electrolyzer
|
||||
```
|
||||
|
||||
或者查看 `docs/AI_KNOWLEDGE_BASE.md`。
|
||||
|
||||
**Q: 怎么知道在哪个坐标建造?**
|
||||
|
||||
使用 `explore` 命令探索区域:
|
||||
|
||||
```bash
|
||||
python3 tools/oni_api.py explore 40 40 30 20
|
||||
```
|
||||
|
||||
它会告诉你该区域有哪些建筑、复制人、元素分布,帮你决策。
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
489
SKILL.md
Normal file
489
SKILL.md
Normal file
@ -0,0 +1,489 @@
|
||||
# Oxygen Not Included (ONI) Agent
|
||||
|
||||
## 职责
|
||||
协助玩家操作和管理游<EFBFBD><EFBFBD>?缺氧"(Oxygen Not Included),提供游戏知识、策略建议,并通过 Mod API 直接操控游戏<E6B8B8><E6888F>?
|
||||
## 工程结构
|
||||
|
||||
```
|
||||
oni-agent/
|
||||
├── config.json # Mod 连接配置
|
||||
├── mod/
|
||||
<EFBFBD><EFBFBD>? ├── mod_info.yaml # Mod 元信<E58583><E4BFA1>?<3F><>? └── ONIAgentBridge.cs # Mod HTTP API 服务 (端口 23876)
|
||||
├── tools/
|
||||
<EFBFBD><EFBFBD>? ├── oni_api.py # Mod API 客户<E5AEA2><E688B7>?<3F><>? ├── oni_analyzer.py # 游戏状态分<E68081><E58886>?<3F><>? └── oni_builder.py # 蓝图建造规<E980A0><E8A784>?├── scripts/
|
||||
<EFBFBD><EFBFBD>? ├── auto_repair.sh # 连接诊断
|
||||
<EFBFBD><EFBFBD>? ├── auto_analyze.sh # 一键分<E994AE><E58886>?<3F><>? ├── watch.sh # 持续监控
|
||||
<EFBFBD><EFBFBD>? └── setup.sh # 环境初始<E5889D><E5A78B>?├── docs/
|
||||
<EFBFBD><EFBFBD>? ├── MOD_DEV_GUIDE.md # Mod 开发指<E58F91><E68C87>?<3F><>? └── AI_KNOWLEDGE_BASE.md # AI 知识<E79FA5><E8AF86>?(ID注册<E6B3A8><E5868C>?语义标签)
|
||||
├── skills/
|
||||
<EFBFBD><EFBFBD>? └── oni_agent.md # Agent skill 定义
|
||||
└── SKILL.md # 本文<E69CAC><E69687>?```
|
||||
|
||||
---
|
||||
|
||||
## 重要概念:理<EFBC9A><E79086>?ONI 的数据模<E68DAE><E6A8A1>?
|
||||
### 1. 坐标<E59D90><E6A087>?
|
||||
ONI 使用二维方格(tile)系统。AI 必须理解坐标系才能正确操作:
|
||||
|
||||
```
|
||||
y <20><>? <20><>? ┌────┬────┬────<E29480><E29480>? <20><>? <20><>?5,5)<29><>?6,5)<29><>?7,5)<29><>? <20><>? ├────┼────┼────<E29480><E29480>? <20><>? <20><>?5,4)<29><>?6,4)<29><>?7,4)<29><>? <20><>?这个格子 (6,4) 包含一个电解器
|
||||
<20><>? ├────┼────┼────<E29480><E29480>? <20><>? <20><>?5,3)<29><>?6,3)<29><>?7,3)<29><>? <20><>? └────┴────┴────<E29480><E29480>? └──────────────────────────<E29480><E29480>?x
|
||||
(0,0)
|
||||
```
|
||||
|
||||
- **原点 (0,0)** 在地<E59CA8><E59CB0>?*左下<E5B7A6><E4B88B>?*
|
||||
- **x <20><>?*向右增加<E5A29E><E58AA0>?*y <20><>?*向上增加
|
||||
- 每个格子 (cell) 有唯一<E594AF><E4B880>?(x, y) 坐标
|
||||
- 建筑占用 w×h 个格子,其坐标是**左下角锚<E8A792><E9949A>?*
|
||||
- 世界大小通过 `/api/state/game` 查询(`gridWidth` x `gridHeight`<60><>?- 典型地图: ~256 x 384 <20><>?
|
||||
### 2. 理解格子状<E5AD90><E78AB6>?
|
||||
每格的数据结构如下(通过 `/api/state/cell?x=&y=` 查询):
|
||||
|
||||
```json
|
||||
{
|
||||
"x": 10, "y": 5,
|
||||
"element": "Oxygen", // 该格包含的元素名<E7B4A0><E5908D>? "elementState": "gas", // solid/liquid/gas/vacuum
|
||||
"massKg": 1.8, // 该格中元素的质量
|
||||
"temperatureC": 23.5, // 温度(摄氏<E69184><E6B08F>?
|
||||
"hasBuilding": true, // 是否有建<E69C89><E5BBBA>? "buildingName": "Electrolyzer",// 建筑名称(如有)
|
||||
"hasDuplicant": false, // 是否有复制人
|
||||
"isVacuum": false, // 是否为真<E4B8BA><E79C9F>? "isSolid": false, // 是否为固<E4B8BA><E59BBA>? "isLiquid": false,
|
||||
"isGas": true,
|
||||
"isVisible": true // 是否已探<E5B7B2><E68EA2>?}
|
||||
```
|
||||
|
||||
### 3. 理解地图区域
|
||||
|
||||
通过 `/api/state/cells?x=&y=&width=&height=` 获取矩形区域的格子数组<E695B0><E7BB84>?
|
||||
通过 `explore <x> <y> <w> <h>` 命令获取 AI 友好的结构化摘要<E69198><E8A681>?- 该区域的建筑列表(带是否可运行)
|
||||
- 该区域的复制人列表(带压<E5B8A6><E58E8B>?当前任务<E4BBBB><E58AA1>?- 元素分布统计
|
||||
- 感兴趣的关键格子
|
||||
|
||||
---
|
||||
|
||||
## 通信方式
|
||||
|
||||
- Mod 在游戏内启动 HTTP 服务,暴<EFBC8C><E69AB4>?RESTful API
|
||||
- 通过 `http://127.0.0.1:PORT` 与游戏通信
|
||||
- 端口<E7ABAF><E58FA3>?`config.json` 中配置(默认 23876<37><36>?
|
||||
---
|
||||
|
||||
## 可用 API 端点
|
||||
|
||||
### 状态查<E68081><E69FA5>?(GET)
|
||||
|
||||
| 端点 | 说明 | 用<><E794A8>?|
|
||||
|------|------|------|
|
||||
| `/api/state/buildable` | 当前科技解锁的建<E79A84><E5BBBA>?| 查看 AI 现在能造什<E980A0><E4BB80>?|
|
||||
| `/api/state/research` | 科技树(进度/解锁的建筑) | 科研规划 |
|
||||
| `/api/state/geysers` | 喷泉(位<EFBC88><E4BD8D>?状<><E78AB6>?排放率) | 资源规划 |
|
||||
| `/api/state/alert` | 警报列表 | 紧急处<E680A5><E5A484>?|
|
||||
| `/api/state/critters` | 小动物(位置/种类/幸福<E5B9B8><E7A68F>?年龄<E5B9B4><E9BE84>?| 养殖管理 |
|
||||
| `/api/state/plants` | 植物(位<EFBC88><E4BD8D>?生长进度/是否枯萎<E69EAF><E8908E>?| 农业管理 |
|
||||
| `/api/state/rooms` | 房间(类<EFBC88><E7B1BB>?格数/建筑数) | 房间判定 |
|
||||
|
||||
### 地图/格子数据 (GET)
|
||||
|
||||
| 端点 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `/api/state/cell?x=10&y=5` | 单格详情 | 查看某个格子是气<E698AF><E6B094>?液体/建筑 |
|
||||
| `/api/state/cells?x=0&y=0&width=10&height=10` | 矩形区域 | 查看 10x10 区域 |
|
||||
| `/api/state/cells/slice?axis=y&index=20&start=0&end=50` | <20><>?列扫<E58897><E689AB>?| 查看<E69FA5><E79C8B>?20 <20><>?|
|
||||
| `/api/state/gas?x=10&y=10&radius=20` | 区域气体分析 | 查看周围气体成分 |
|
||||
|
||||
### 实体注册<E6B3A8><E5868C>?(GET,AI 参考用)
|
||||
|
||||
| 端点 | 说明 |
|
||||
|------|------|
|
||||
| `/api/registry/buildings` | 所有建<E69C89><E5BBBA>?ID 及尺<E58F8A><E5B0BA>?功<><E58A9F>?发热 |
|
||||
| `/api/registry/elements` | 所有元<E69C89><E58583>?ID 及比热容/导热/熔沸<E78694><E6B2B8>?|
|
||||
| `/api/registry/techs` | 所有科技 ID 及前<E58F8A><E5898D>?解锁内容 |
|
||||
|
||||
### 操作 (POST)
|
||||
|
||||
| 端点 | 请求<E8AFB7><E6B182>?| 用<><E794A8>?|
|
||||
|------|--------|------|
|
||||
| `/api/action/toggle` | `{x, y}` | 开关建筑(省电/控制流程<E6B581><E7A88B>?|
|
||||
| `/api/action/set_recipe` | `{x, y, recipeId}` | 设置建筑配方 |
|
||||
| `/api/action/empty` | `{x, y}` | 清空建筑储物 |
|
||||
| `/api/action/cancel_errand` | `{x, y}` | 取消建筑处的任务 |
|
||||
| `/api/action/dig` | `{x, y, width, height}` | 挖掘区域 |
|
||||
| `/api/action/build` | `{buildingId, x, y}` | 建造建<E980A0><E5BBBA>?|
|
||||
| `/api/action/deconstruct` | `{buildingId, x, y}` | 拆除建筑 |
|
||||
| `/api/action/prioritize` | `{x, y, priority}` | 设优先级 |
|
||||
| `/api/action/research` | `{techId}` | 选研究项<E7A9B6><E9A1B9>?|
|
||||
| `/api/action/mop` | `{x, y}` | 清理液体 |
|
||||
| `/api/action/harvest` | `{x, y}` | 收获植物 |
|
||||
|
||||
---
|
||||
|
||||
## AI 如何进行推理和操<E5928C><E6938D>?
|
||||
### 第一步:获取全局上下<E4B88A><E4B88B>?
|
||||
```bash
|
||||
python3 tools/oni_api.py status
|
||||
python3 tools/oni_api.py buildings
|
||||
python3 tools/oni_analyzer.py
|
||||
```
|
||||
|
||||
### 第二步:理解地图
|
||||
|
||||
```bash
|
||||
# 探索基地中心区域(假设基地在 50,50<35><30>?python3 tools/oni_api.py explore 40 40 40 30
|
||||
|
||||
# 检查某个格子的详细信息
|
||||
python3 tools/oni_api.py cell 45 48
|
||||
|
||||
# 查看气体分布
|
||||
python3 tools/oni_api.py gas 50 50 30
|
||||
```
|
||||
|
||||
### 第三步:参考知识库
|
||||
|
||||
```bash
|
||||
# 查找某个建筑<E5BBBA><E7AD91>?ID
|
||||
python3 tools/oni_api.py registry buildings Electrolyzer
|
||||
|
||||
# 查看元素属<E7B4A0><E5B19E>?python3 tools/oni_api.py registry elements Water
|
||||
|
||||
# 查看科技<E7A791><E68A80>?python3 tools/oni_api.py registry techs
|
||||
```
|
||||
|
||||
### 第四步:执行操作
|
||||
|
||||
```bash
|
||||
# 建造电解器
|
||||
python3 tools/oni_api.py build Electrolyzer 45 48
|
||||
|
||||
# 挖掘空间
|
||||
python3 tools/oni_api.py dig 40 45 8 6
|
||||
|
||||
# 选择科研方向
|
||||
python3 tools/oni_api.py research_select ImprovedOxygen
|
||||
|
||||
# 使用蓝图
|
||||
python3 tools/oni_builder.py build spom 42 42
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AI 如何理解常见游戏场景
|
||||
|
||||
### 场景 1:氧气不<E6B094><E4B88D>?
|
||||
**AI 推理过程<E8BF87><E7A88B>?*
|
||||
1. 检<><E6A380>?`/api/state/resources` 中的 O2 <20><>?Algae 存量
|
||||
2. 检<><E6A380>?`/api/state/buildings` 是否有电解器或氧气扩散器
|
||||
3. 检<><E6A380>?`/api/state/cell?x=&y=` 查询基地气体分布
|
||||
4. 如果 Algae < 1t 且无电解<EFBFBD><EFBFBD>?<3F><>?建议建<EFBFBD><EFBFBD>?SPOM
|
||||
5. SPOM 需要:水源 + 电解<EFBFBD><EFBFBD>?+ 气体<EFBFBD><EFBFBD>?+ 氢气发电<EFBFBD><EFBFBD>?+ 气体过滤<EFBFBD><EFBFBD>?6. 通过 `explore` 找到一<EFBFBD><EFBFBD>?8x6 的空<EFBFBD><EFBFBD>?7. 执行 `build Electrolyzer x y` + `build GasPump ...` + `build HydrogenGenerator ...`
|
||||
|
||||
### 场景 2:食物短<E789A9><E79FAD>?
|
||||
**AI 推理过程<EFBFBD><EFBFBD>?*
|
||||
1. 检<EFBFBD><EFBFBD>?Calories < 500,000 kcal <EFBFBD><EFBFBD>?食物预警
|
||||
2. 检查是否有 PlanterBox/FarmTile <EFBFBD><EFBFBD>?ElectricGrill
|
||||
3. 如果没有农场 <EFBFBD><EFBFBD>?建议建<EFBFBD><EFBFBD>?5 <EFBFBD><EFBFBD>?PlanterBox <EFBFBD><EFBFBD>?Mealwood
|
||||
4. Mealwood 不需要灌溉或施肥,只需 Dirt
|
||||
5. 检<EFBFBD><EFBFBD>?Dirt 存量,如果足<EFBFBD><EFBFBD>?<3F><>?执行建<EFBFBD><EFBFBD>?6. 如果有污<EFBFBD><EFBFBD>?<3F><>?建议建<EFBFBD><EFBFBD>?Water Sieve + 厕所水循<EFBFBD><EFBFBD>?
|
||||
### 场景 3:温度过<E5BAA6><E8BF87>?
|
||||
**AI 推理过程<EFBFBD><EFBFBD>?*
|
||||
1. 检查温度数据(通过资源中的 Temperature 或格子数据)
|
||||
2. 查看热源(煤发电机、精炼厂等靠近基地的位置<EFBFBD><EFBFBD>?3. 建议:用隔热门包围热<EFBFBD><EFBFBD>?+ 建造液冷模<EFBFBD><EFBFBD>?4. 液冷模块需要:Aquatuner + SteamTurbine + 导热液体管道
|
||||
|
||||
---
|
||||
|
||||
## 暂停与速度参<E5BAA6><E58F82>?
|
||||
游戏状态中<EFBFBD><EFBFBD>?`isPaused` <EFBFBD><EFBFBD>?`gameSpeed` 字段<EFBFBD><EFBFBD>?
|
||||
```
|
||||
isPaused: true <20><>?是否暂停
|
||||
gameSpeed: 0 <20><>?0=暂停, 1=1x, 2=2x, 3=3x
|
||||
```
|
||||
|
||||
暂停规则已集成在"统一操作协议"的标<EFBFBD><EFBFBD>?SOP 中,详见下节。核心原则:
|
||||
- **所有写操作前必须暂<EFBFBD><EFBFBD>?*(dig/build/deconstruct/batch/pipe/wire<EFBFBD><EFBFBD>?- **只读查询不需要暂<EFBFBD><EFBFBD>?*(status/resources/events<EFBFBD><EFBFBD>?- **操作完成后必须恢<EFBFBD><EFBFBD>?*
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
## AI 统一操作协议
|
||||
|
||||
这是 AI 操作缺氧的标准协议。所有决策和操作必须遵循此协议<EFBFBD><EFBFBD>?
|
||||
### 铁律(必须遵守)
|
||||
|
||||
```
|
||||
铁律 1: 任何时<E4BD95><E697B6>?AI 开始推<E5A78B><E68EA8>?决策<E586B3><E7AD96>?<3F><>?必须先暂停游戏<E6B8B8><E6888F>? 工具在获取游戏状态数据时会自动触发暂停,
|
||||
保证 AI 获取的信息是当前时刻的准确快照<E5BFAB><E785A7>? <20><>?工具自动执行 pause(无需手动调用<E8B083><E794A8>? <20><>?AI 完成所有操作后主动 unpause
|
||||
|
||||
铁律 2: 任何时<E4BD95><E697B6>?AI 需要向用户提问,必须先暂停游戏<E6B8B8><E6888F>? <20><>?工具自动暂停,用户回答后 AI 恢复<E681A2><E5A48D>?unpause
|
||||
|
||||
铁律 3: 任何时<E4BD95><E697B6>?AI 结束回答/退出操作状态,必须确保游戏处于暂停态,
|
||||
除非用户明确要求不暂停<E69A82><E5819C>? <20><>?防止游戏<E6B8B8><E6888F>?AI 不监控时状态恶化(窒息/过载/高温/CO₂)
|
||||
|
||||
铁律 4: 任何时<E4BD95><E697B6>?AI 执行写操作(dig/build/deconstruct/batch/pipe/wire),
|
||||
工具自动确保暂停态<E5819C><E68081>?
|
||||
铁律 5: 重大操作前必须先 save 存档,失败后允许 load 回滚<E59B9E><E6BB9A>?```
|
||||
|
||||
### 核心操作循环
|
||||
|
||||
每次 AI 与游戏交互都必须遵循这个五步循环<E5BEAA><E78EAF>?
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────<EFBFBD><EFBFBD>?<3F><>? 1. 上下文感<EFBFBD><EFBFBD>? <EFBFBD><EFBFBD>?<3F><>? snapshot + diagnose + events <EFBFBD><EFBFBD>?<3F><>? "我现在看到什么?当前状态是什么?发生了什么?" <EFBFBD><EFBFBD>?└──────────────────────┬──────────────────────────────────<E29480><E29480>? <EFBFBD><EFBFBD>?┌─────────────────────────────────────────────────────────<E29480><E29480>?<3F><>? 2. 决策与规<EFBFBD><EFBFBD>? <EFBFBD><EFBFBD>?<3F><>? pause <EFBFBD><EFBFBD>?分析数据 <EFBFBD><EFBFBD>?确定目标 <EFBFBD><EFBFBD>?选择工具 <EFBFBD><EFBFBD>?<3F><>? "基于现状,我需要做什么?用什么工具?在哪个坐标?" <EFBFBD><EFBFBD>?└──────────────────────┬──────────────────────────────────<E29480><E29480>? <EFBFBD><EFBFBD>?┌─────────────────────────────────────────────────────────<E29480><E29480>?<3F><>? 3. 执行前保<EFBFBD><EFBFBD>? <EFBFBD><EFBFBD>?<3F><>? save <EFBFBD><EFBFBD>?camera <EFBFBD><EFBFBD>?cell <EFBFBD><EFBFBD>?snapshot <EFBFBD><EFBFBD>?<3F><>? "先存档,然后把视野移过去,确认坐标正<EFBFBD><EFBFBD>? <EFBFBD><EFBFBD>?└──────────────────────┬──────────────────────────────────<E29480><E29480>? <EFBFBD><EFBFBD>?┌─────────────────────────────────────────────────────────<E29480><E29480>?<3F><>? 4. 执行操作 <EFBFBD><EFBFBD>?<3F><>? dig/build/build_pipe_line/batch <EFBFBD><EFBFBD>?<3F><>? 每一步检查反馈(success/fail + suggestion<EFBFBD><EFBFBD>? <EFBFBD><EFBFBD>?└──────────────────────┬──────────────────────────────────<E29480><E29480>? <EFBFBD><EFBFBD>?┌─────────────────────────────────────────────────────────<E29480><E29480>?<3F><>? 5. 验证与恢<EFBFBD><EFBFBD>? <EFBFBD><EFBFBD>?<3F><>? unpause <EFBFBD><EFBFBD>?snapshot <EFBFBD><EFBFBD>?检查状<EFBFBD><EFBFBD>? <EFBFBD><EFBFBD>?<3F><>? 恶化 <EFBFBD><EFBFBD>?load 回滚 <EFBFBD><EFBFBD>?换方<EFBFBD><EFBFBD>? <EFBFBD><EFBFBD>?└─────────────────────────────────────────────────────────<E29480><E29480>?```
|
||||
|
||||
### 标准操作 SOP
|
||||
|
||||
任何时<EFBFBD><EFBFBD>?AI 执行操作,必须按以下流程<E6B581><E7A88B>?
|
||||
```
|
||||
步骤 0: 暂停
|
||||
python3 tools/oni_api.py pause "操作说明"
|
||||
<20><>?确认 success=true,否则重<E58899><E9878D>?
|
||||
步骤 1: 视觉确认
|
||||
python3 tools/oni_api.py camera <x> <y> <zoom>
|
||||
python3 tools/oni_api.py snapshot
|
||||
<20><>?下载截图,确认目标位置正<E7BDAE><E6ADA3>?
|
||||
步骤 2: 数据确认
|
||||
python3 tools/oni_api.py cell <x> <y>
|
||||
<20><>?hasBuilding=true <20><>?<3F><>?deconstruct 或换位置
|
||||
<20><>?isSolid=true <20><>?<3F><>?dig
|
||||
<20><>?isVacuum=true <20><>?确认原因
|
||||
|
||||
步骤 3: 安全存档
|
||||
python3 tools/oni_api.py save "before_任务<E4BBBB><E58AA1>?
|
||||
<20><>?确认 success=true
|
||||
|
||||
步骤 4: 执行
|
||||
dig / build / build_pipe_line / build_wire_line / batch
|
||||
<20><>?每次检查反馈:
|
||||
success=true <20><>?继续
|
||||
success=false <20><>?<3F><>?suggestion <20><>?调整重试 <20><>?3次失败则 load 回滚
|
||||
|
||||
步骤 5: 验证
|
||||
snapshot <20><>?截图对比
|
||||
resources <20><>?资源变化
|
||||
未改<E69CAA><E694B9>?<3F><>?load 回滚
|
||||
|
||||
步骤 6: 恢复
|
||||
unpause 1
|
||||
```
|
||||
|
||||
### 决策框架
|
||||
|
||||
AI 面对任何问题按此框架思考:
|
||||
|
||||
```
|
||||
【数据收集<EFBFBD><EFBFBD>? status <20><>?diagnose <20><>?power <20><>?co2 <20><>?temp_zones <20><>?resources <20><>?buildings
|
||||
|
||||
【定位分析<EFBFBD><EFBFBD>? cells <x> <y> <w> <h> <20><>?cell <x> <y> <20><>?camera + snapshot
|
||||
|
||||
【方案选择<EFBFBD><EFBFBD>? 紧<><E7B4A7>?<3F><>?emergency_o2 / fix_co2 / fix_overload
|
||||
建<><E5BBBA>?<3F><>?定坐<E5AE9A><E59D90>?<3F><>?build / build_pipe_line / build_wire_line
|
||||
批量 <20><>?batch JSON
|
||||
扩展 <20><>?expand_base
|
||||
检<><E6A380>?<3F><>?diagnose
|
||||
回滚 <20><>?load
|
||||
```
|
||||
|
||||
### 标准方案手册
|
||||
|
||||
#### 方案 A:初期基地(Cycle 1-20<32><30>?
|
||||
```
|
||||
1. pause "Initial base"
|
||||
2. diagnose
|
||||
3. expand_base <中心> <<3C><>? <<3C><>?
|
||||
4. build ResearchStation <坐标>
|
||||
5. build ManualGenerator <坐标>
|
||||
6. build Battery <坐标>
|
||||
7. build OxygenDiffuser <坐标>
|
||||
8. build PlanterBox x5
|
||||
9. build Cot x3 / LadderBed x3
|
||||
10. build Outhouse + WashBasin
|
||||
11. research_select FarmingTech
|
||||
12. research_select PowerRegulation
|
||||
13. unpause 1
|
||||
```
|
||||
|
||||
#### 方案 B:CO2 危机
|
||||
|
||||
```
|
||||
症状:duplicants 窒息,co2 显示大量 CO2
|
||||
1. pause "CO2"
|
||||
2. co2 <20><>?找到最<E588B0><E69C80>?CO2 聚集<E8819A><E99B86>?3. camera <x> <最<><E69C80>?y> 20
|
||||
4. snapshot <20><>?确认地形
|
||||
5. save "before_co2"
|
||||
6. fix_co2 <20><>?自动挖排气管
|
||||
7. snapshot <20><>?确认挖<E8AEA4><E68C96>?8. unpause 1
|
||||
9. 30s <20><>?co2 确认下降
|
||||
```
|
||||
|
||||
#### 方案 C:电力过<E58A9B><E8BF87>?
|
||||
```
|
||||
症状:power 显示 *** OVERLOAD ***
|
||||
1. pause "Power overload"
|
||||
2. power <20><>?哪个电路过载
|
||||
3. diagnose <20><>?资源检<E6BA90><E6A380>?4. save "before_power"
|
||||
5. fix_overload <20><>?修复建议
|
||||
6. 如电力不<E58A9B><E4B88D>?<3F><>?build <发电<E58F91><E794B5>? <20><>?build_wire_line <连接>
|
||||
7. power <20><>?确认过载消失
|
||||
8. unpause 1
|
||||
```
|
||||
|
||||
#### 方案 D:氧气危<E6B094><E58DB1>?
|
||||
```
|
||||
症状:duplicants 显示 oxygen<20%
|
||||
1. pause "O2"
|
||||
2. snapshot <20><>?视觉确认
|
||||
3. resources <20><>?O2/Algae 存量
|
||||
4. buildings <20><>?电解<E794B5><E8A7A3>?扩散<E689A9><E695A3>?5. save "before_o2"
|
||||
6. 分支<E58886><E694AF>? 无设<E697A0><E8AEBE>?<3F><>?build OxygenDiffuser
|
||||
无藻<E697A0><E897BB>?<3F><>?build Electrolyzer + 接水管电<E7AEA1><E794B5>? 设备不工<E4B88D><E5B7A5>?<3F><>?cell 检查供水和供电
|
||||
7. unpause 1
|
||||
8. 30s <20><>?resources <20><>?O2 回升<E59B9E><E58D87>?```
|
||||
|
||||
#### 方案 E:建<EFBC9A><E5BBBA>?SPOM
|
||||
|
||||
```
|
||||
1. pause "SPOM"
|
||||
2. explore <20><>?8x6 空地
|
||||
3. camera <20><>?snapshot 确认
|
||||
4. save "before_spom"
|
||||
5. oni_builder.py build spom <x> <y>
|
||||
6. build_pipe_line liquid <水源> <电解<E794B5><E8A7A3>?
|
||||
7. build_wire_line heavy <发电<E58F91><E794B5>? <电池> cross
|
||||
8. unpause 1
|
||||
```
|
||||
|
||||
#### 方案 F:管道铺设(带交叉处理)
|
||||
|
||||
```
|
||||
需求:液体管道横穿已有气体管道
|
||||
1. pause "Plumbing"
|
||||
2. camera <起点> <终点> 25
|
||||
3. save "before_pipe"
|
||||
4. 横穿段用 cross 模式<E6A8A1><E5BC8F>? build_pipe_line liquid <起点> <终点> cross
|
||||
<20><>?Mod 自动在交叉处放跨接器
|
||||
5. pipes liquid <20><>?确认流动
|
||||
6. unpause 1
|
||||
```
|
||||
|
||||
#### 方案 G:电线布<E7BABF><E5B883>?
|
||||
```
|
||||
需求:为新建筑拉电线到电网
|
||||
1. pause "Wiring"
|
||||
2. camera <建筑> <电源> 25
|
||||
3. power <20><>?查空余容<E4BD99><E5AEB9>?4. save "before_wire"
|
||||
5. 布线(横穿用 cross):
|
||||
build_wire_line regular <建筑> <变压<E58F98><E58E8B>? cross
|
||||
6. unpause 1
|
||||
```
|
||||
|
||||
### 错误恢复
|
||||
|
||||
```
|
||||
操作失败 <20><>?AI 必须读取 error + errorMessage + suggestion
|
||||
|
||||
错误处理表:
|
||||
cell_occupied <20><>?换坐标或 deconstruct
|
||||
cell_solid <20><>?<3F><>?dig
|
||||
cell_occupied_by_dupe<EFBFBD><EFBFBD>?等待
|
||||
material_shortage <20><>?查资<E69FA5><E8B584>?+ 安排生产
|
||||
unknown_building <20><>?registry buildings 查询
|
||||
unknown_tech <20><>?registry techs 查询
|
||||
missing_prerequisites<EFBFBD><EFBFBD>?先研究前置科技
|
||||
invalid_priority <20><>?<3F><>?1-9
|
||||
save_not_found <20><>?saves 列出
|
||||
|
||||
重试 3 次失<E6ACA1><E5A4B1>?<3F><>?load 回滚
|
||||
```
|
||||
|
||||
### 坐标定位方法
|
||||
|
||||
```
|
||||
方法 1: 基于已有建筑偏移
|
||||
buildings <20><>?查已有建筑坐<E7AD91><E59D90>?<3F><>?偏移放置
|
||||
|
||||
方法 2: 基于区域探索
|
||||
explore <x> <y> <w> <h> <20><>?找空<E689BE><E7A9BA>?
|
||||
方法 3: 基于资源位置
|
||||
co2 <20><>?CO2 聚集点下方挖排气<E68E92><E6B094>?
|
||||
方法 4: 基于 cell 验证
|
||||
cell <x> <y> <20><>?确认 isSolid=false + hasBuilding=false + isVisible=true
|
||||
|
||||
AI 习惯:头脑规划坐<E58892><E59D90>?<3F><>?cell 验证 <20><>?确认无误再建<E5868D><E5BBBA>?```
|
||||
|
||||
### 事件自动响应
|
||||
|
||||
event_daemon 运行<E8BF90><E8A18C>?AI 自动响应规则<E8A784><E58899>?
|
||||
```
|
||||
[CRITICAL] 窒息
|
||||
<20><>?diagnose <20><>?执行方案 D 或方<E68896><E696B9>?B
|
||||
|
||||
[CRITICAL] 电力中断
|
||||
<20><>?power <20><>?执行方案 C
|
||||
|
||||
[WARNING] 食物短缺
|
||||
<20><>?Calories<200k <20><>?<3F><>?PlanterBox x5
|
||||
|
||||
[WARNING] 温度过高
|
||||
<20><>?temp_zones <20><>?隔热<E99A94><E783AD>?+ 冷却
|
||||
|
||||
[INFO] 新周<E696B0><E591A8>? <20><>?research <20><>?继续科研
|
||||
<20><>?resources <20><>?安排生产
|
||||
|
||||
AI 不应等待指令——事件本身就是指令<E68C87><E4BBA4>?```
|
||||
|
||||
---
|
||||
|
||||
## AI 如何表达"在哪个格子做什<E5819A><E4BB80>?
|
||||
|
||||
### 定位语法
|
||||
|
||||
AI 在描述操作时应使用以下格式:
|
||||
|
||||
```
|
||||
在坐<EFBFBD><EFBFBD>?(x, y) 建<><E5BBBA>?<buildingId>
|
||||
在区<EFBFBD><EFBFBD>?(x, y, width, height) 进行挖掘
|
||||
<EFBFBD><EFBFBD>?(x1,y1) <20><>?(x2,y2) 铺设管道/电线
|
||||
在格<EFBFBD><EFBFBD>?(x, y) 设置优先级为 <priority>
|
||||
```
|
||||
|
||||
### 坐标查找策略
|
||||
|
||||
<EFBFBD><EFBFBD>?AI 不确定在哪里建造时<E980A0><E697B6>?1. 先用 `explore` 找一个空闲区域(没有建筑和固体阻挡)
|
||||
2. 检查空闲区域的元素和温度是否适合
|
||||
3. <20><>?`cell` 命令确认目标格子状<E5AD90><E78AB6>?4. 然后<E784B6><E5908E>?`dig` 清理空间
|
||||
5. 最后用 `build` 建<><E5BBBA>?
|
||||
### 建筑放置规则
|
||||
|
||||
- 建筑坐标是其**左下<E5B7A6><E4B88B>?*的位<E79A84><E4BD8D>?- 建筑占用<E58DA0><E794A8>?w×h 区域必须全部是空<E698AF><E7A9BA>?- 需要确认目标区域无建筑、无固体自然方块
|
||||
- 气体/液体不会阻挡建筑
|
||||
- 如果建筑需要特定环境(如电解器需要水),AI 需要先检查环<E69FA5><E78EAF>?
|
||||
---
|
||||
|
||||
## 工具列表
|
||||
|
||||
| 工具 | 用<><E794A8>?|
|
||||
|------|------|
|
||||
| `tools/oni_api.py` | Mod API 客户端(状<EFBC88><E78AB6>?格子/注册<E6B3A8><E5868C>?操作/批量/优先<E4BC98><E58588>?事件<E4BA8B><E4BBB6>?|
|
||||
| `tools/oni_analyzer.py` | 自动分析游戏状态、生成预警和建议 |
|
||||
| `tools/oni_builder.py` | 预置蓝图建造(SPOM/农场/养殖等) |
|
||||
| `scripts/auto_repair.sh` | 诊断 Mod 连接问题 |
|
||||
| `scripts/auto_analyze.sh` | 一键健康检<E5BAB7><E6A380>?状<><E78AB6>?分析 |
|
||||
| `scripts/watch.sh [秒]` | 循环监控模式 |
|
||||
| `scripts/setup.sh` | 环境初始化与检<E4B88E><E6A380>?|
|
||||
| `scripts/event_daemon.py` | **事件守护进程** <20><>?持续轮询事件 <20><>?AI 输入<E8BE93><E585A5>?|
|
||||
| `docs/AI_KNOWLEDGE_BASE.md` | 建筑/元素/科技 ID 注册表和游戏机制参<E588B6><E58F82>?|
|
||||
| `docs/batch_example.json` | 批量任务示例文件 |
|
||||
|
||||
---
|
||||
|
||||
## 核心游戏知识
|
||||
|
||||
### 生存优先<E4BC98><E58588>?1. **氧气** <20><>?电解<E794B5><E8A7A3>?> 藻类制氧(前期过渡)
|
||||
2. **食物** <EFBFBD><EFBFBD>?浆果 > 烤肉 > 营养<E890A5><E585BB>?3. **温度控制** <20><>?液冷 + 蒸汽<E892B8><E6B1BD>?4. **电力** <20><>?氢气发电 > 煤炭 > 手动
|
||||
5. **水资源管<E6BA90><E7AEA1>?* <20><>?净水器、污水过<E6B0B4><E8BF87>?
|
||||
### 常用布局
|
||||
- SPOM: 电解制氧 + 氢气发电闭环
|
||||
- 卫生间水循环: 卫生<E58DAB><E7949F>?<3F><>?净水器 <20><>?卫生<E58DAB><E7949F>?- 冷却系统: 液冷 + 蒸汽<E892B8><E6B1BD>?+ 导热<E5AFBC><E783AD>?- Ranch 模块: 养殖哈奇/滑鳞/飞鱼
|
||||
|
||||
### 关键事件预警
|
||||
- 氧气不足 (< 500g/tile) <EFBFBD><EFBFBD>?增加制氧
|
||||
- 温度超标 (> 40°C <20><>?< -10°C) <EFBFBD><EFBFBD>?增加温控
|
||||
- 食物短缺 (< 5 周期余量) <EFBFBD><EFBFBD>?扩大种植/养殖
|
||||
- 电力不足 <EFBFBD><EFBFBD>?增加发电或减少负<EFBFBD><EFBFBD>?- 污水满溢 <EFBFBD><EFBFBD>?增加净<EFBFBD><EFBFBD>?扩大存储
|
||||
5
config.json
Normal file
5
config.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"modHost": "127.0.0.1",
|
||||
"modPort": 23876,
|
||||
"timeout": 10
|
||||
}
|
||||
268
docs/AI_KNOWLEDGE_BASE.md
Normal file
268
docs/AI_KNOWLEDGE_BASE.md
Normal file
@ -0,0 +1,268 @@
|
||||
# ONI Knowledge Base — AI Reference
|
||||
|
||||
This file aggregates all building IDs, element IDs, tech IDs, and their semantic metadata
|
||||
in one structured document. The AI uses this to understand what each entity means and
|
||||
how to reason about the game.
|
||||
|
||||
## Coordinate System
|
||||
|
||||
ONI uses a 2D grid. The origin (0,0) is at the **bottom-left** of the map.
|
||||
- x increases to the right
|
||||
- y increases upward
|
||||
- Each cell is 1x1 tile
|
||||
- Buildings may occupy multiple cells (width x height); the (x,y) is the **bottom-left anchor**
|
||||
- World size varies by asteroid (default ~256x384 cells)
|
||||
|
||||
When the AI wants to reference a location:
|
||||
- Use absolute (x,y) coordinates
|
||||
- For regions: (x, y, width, height)
|
||||
- For buildings: reference the bottom-left cell of the building
|
||||
- For movement: "at (x,y)" or "from (x1,y1) to (x2,y2)"
|
||||
|
||||
## Building Categories
|
||||
|
||||
| Category | Purpose | Examples |
|
||||
|------------|----------------------------------------|---------------------------------------------------|
|
||||
| Base | Structure, storage, doors, ladders | Tile, Ladder, StorageBin, InsulatedTile, Door |
|
||||
| Oxygen | Oxygen production, gas cleaning | Electrolyzer, OxygenDiffuser, Deodorizer |
|
||||
| Power | Power generation, storage, wiring | CoalGenerator, HydrogenGenerator, Battery |
|
||||
| Food | Food production, cooking, ranching | PlanterBox, ElectricGrill, MicrobeMusher, Ranch |
|
||||
| Plumbing | Liquid handling, hygiene | LiquidPump, Lavatory, WaterSiever, Shower |
|
||||
| Ventilation| Gas handling | GasPump, GasFilter, GasVent |
|
||||
| Refinement | Material processing | MetalRefinery, RockCrusher, Kiln, Compost |
|
||||
| Medicine | Health, disease treatment | Apothecary, MassageTable, SickBay |
|
||||
| Furniture | Decor, morale, stress relief | Cot, MessTable, ArcadeCabinet, FlowerPot |
|
||||
| Stations | Research, suit docks, crafting | ResearchStation, SuperComputer, AtmoSuitDock |
|
||||
| Utilities | Temperature management | ThermoAquatuner, SpaceHeater, TempshiftPlate |
|
||||
| Automation | Logic circuits, sensors | AND Gate, AtmoSensor, AutomationWire |
|
||||
| Shipping | Conveyor systems | ConveyorLoader, ConveyorRail, SolidFilter |
|
||||
|
||||
## Key Building IDs (frequently used by AI)
|
||||
|
||||
```
|
||||
# Oxygen production
|
||||
Electrolyzer -> Produces O2 + H2 from water (needs 1kg/s water)
|
||||
OxygenDiffuser -> Produces O2 from algae (early game)
|
||||
Deodorizer -> Converts PollutedOxygen to Oxygen (needs filtration medium)
|
||||
|
||||
# Power
|
||||
ManualGenerator -> 400W, duplicant powered
|
||||
CoalGenerator -> 600W, consumes Coal
|
||||
HydrogenGenerator -> 800W, consumes H2
|
||||
NaturalGasGenerator-> 800W, consumes NaturalGas
|
||||
PetroleumGenerator -> 2000W, consumes Petroleum
|
||||
SteamTurbine -> Extracts heat from steam, produces power
|
||||
SolarPanel -> 380W max, needs light
|
||||
WoodBurner -> 300W, consumes Lumber
|
||||
|
||||
# Power storage
|
||||
Battery -> 10kJ storage, small
|
||||
JumboBattery -> 40kJ storage
|
||||
SmartBattery -> 20kJ storage, automation output
|
||||
|
||||
# Food
|
||||
PlanterBox -> Grows plants, needs dupe delivery
|
||||
FarmTile -> Grows plants with irrigation
|
||||
HydroponicFarm -> Grows plants with automatic irrigation
|
||||
ElectricGrill -> Cooks food (better quality)
|
||||
MicrobeMusher -> Makes basic mush bars from water + dirt
|
||||
GasRange -> Advanced cooking with gas
|
||||
Refrigerator -> Stores food, slows decay
|
||||
|
||||
# Plumbing
|
||||
LiquidPump -> Pumps liquids (240kg/s)
|
||||
Lavatory -> Produces PollutedWater from dupe use
|
||||
WaterSiever -> Filters PollutedWater -> Water
|
||||
Desalinator -> Removes salt from SaltWater/Brine
|
||||
LiquidVent -> Outputs liquid into world
|
||||
|
||||
# Ventilation
|
||||
GasPump -> Pumps gases (500g/s)
|
||||
GasFilter -> Filters specific gas from mixed pipes
|
||||
GasVent -> Outputs gas into world
|
||||
HighPressureGasVent-> 20kg/tile max pressure
|
||||
|
||||
# Refinement
|
||||
MetalRefinery -> Refines metal ores into refined metals
|
||||
RockCrusher -> Crushes rock into sand/power
|
||||
Kiln -> Burns clay -> ceramic
|
||||
GlassForge -> Makes glass from sand
|
||||
Compost -> Converts polluted dirt into dirt
|
||||
|
||||
# Base
|
||||
Tile -> Standard tile, 2 tiles high
|
||||
Ladder -> Allows vertical movement
|
||||
InsulatedTile -> Reduces heat transfer (best insulation)
|
||||
ManualAirlock -> Manual door
|
||||
PneumaticDoor -> Auto door, lets gas pass
|
||||
MechanizedAirlock -> Auto door, seals gas/liquid
|
||||
StorageBin -> Stores solid resources
|
||||
LiquidReservoir -> Stores 5t of liquid
|
||||
GasReservoir -> Stores 150kg of gas
|
||||
|
||||
# Temperature
|
||||
ThermoAquatuner -> Cools liquid piped through it (needs steam room)
|
||||
ThermoRegulator -> Cools gas piped through it
|
||||
SpaceHeater -> Heats area (inefficient)
|
||||
LiquidTepidizer -> Heats liquid (up to 85C)
|
||||
TempshiftPlate -> Distributes heat evenly
|
||||
|
||||
# Stations
|
||||
ResearchStation -> Basic research (needs dirt)
|
||||
SuperComputer -> Advanced research (needs plastic)
|
||||
AtmoSuitDock -> Stores atmo suit
|
||||
GroomingStation -> Grooms critters for happiness/eggs
|
||||
RanchStation -> Ranching skill station
|
||||
FarmStation -> Improves farm yield (needs fertilizer)
|
||||
PowerControlStation-> Improves generator efficiency
|
||||
|
||||
# Medicine
|
||||
MassageTable -> Reduces stress
|
||||
SickBay -> Cures diseases
|
||||
TriageCot -> Heals physical damage
|
||||
Apothecary -> Produces medicine
|
||||
|
||||
# Automation (common items)
|
||||
AtmoSensor -> Senses gas pressure
|
||||
ThermoSensor -> Senses temperature
|
||||
HydroSensor -> Senses liquid pressure
|
||||
GasElementSensor -> Senses specific gas type
|
||||
AND Gate, NOT Gate, FILTER Gate, BUFFER Gate
|
||||
```
|
||||
|
||||
## Key Element IDs (resources)
|
||||
|
||||
```
|
||||
# Critical survival
|
||||
Oxygen -> Breathable gas (needed by dupes)
|
||||
Water -> Essential for farming, electrolysis, life support
|
||||
Dirt -> Used in research (early) and farming
|
||||
Algae -> Consumed by OxygenDiffuser
|
||||
|
||||
# Food chain
|
||||
Calories -> Aggregate food energy for colony
|
||||
MealLice -> Basic food from mealwood plants
|
||||
BristleBerry -> Mid-tier food from bristle blossom
|
||||
Mushroom -> Food from dusk cap (needs CO2 + slime)
|
||||
RawEgg -> Egg for cooking
|
||||
Meat -> From ranching
|
||||
|
||||
# Power chain
|
||||
Coal -> Burned in CoalGenerator
|
||||
Hydrogen -> Burned in HydrogenGenerator
|
||||
NaturalGas -> Burned in NaturalGasGenerator
|
||||
Petroleum -> Burned in PetroleumGenerator
|
||||
CrudeOil -> Can be refined to Petroleum
|
||||
|
||||
# Construction metals
|
||||
CopperOre -> Early building material
|
||||
IronOre -> Mid building material
|
||||
GoldAmalgam -> High corrosion resistance
|
||||
Wolframite -> Very high melting point (tungsten source)
|
||||
Steel -> Strong, high melting point (refined)
|
||||
RefinedIron -> Mid refined metal
|
||||
Plastic -> Advanced material (from polymer press or dreckos)
|
||||
Ceramic -> Best insulation material
|
||||
|
||||
# Water / liquids
|
||||
SaltWater -> Can be desalinated to Water
|
||||
Brine -> Can be desalinated (more salt per water)
|
||||
PollutedWater -> Can be filtered to Water (also used by reed fiber)
|
||||
CrudeOil -> Pumped from oil biome
|
||||
Petroleum -> Refined from oil (cooking or refinery)
|
||||
Lumber -> From arbor trees, burned in wood burner
|
||||
Ethanol -> From lumber, burned in petroleum generator
|
||||
|
||||
# Gas handling
|
||||
PollutedOxygen -> Can be deodorized to Oxygen
|
||||
CarbonDioxide -> Sinks to bottom, used by mushrooms/soda fountain
|
||||
ChlorineGas -> Disinfects, used by balm lily
|
||||
SourGas -> Can be cooled to methane + sulfur
|
||||
|
||||
# Rare / advanced
|
||||
Niobium -> End-game space material
|
||||
Thermium -> Best heat conductor (space material)
|
||||
Isoresin -> Used for insulation (space material)
|
||||
ViscoGel -> Non-mixing liquid (space material)
|
||||
Radium -> Radioactive, spaced out DLC
|
||||
```
|
||||
|
||||
## Research Tech Tree (simplified path)
|
||||
|
||||
```
|
||||
Tier 0 (start): FarmingTech, PowerRegulation, Plumbing, Ventilation
|
||||
Tier 1: FoodPreparation (-> ElectricGrill, MicrobeMusher)
|
||||
InteriorDecor (-> Cot, FlowerPot, various decor)
|
||||
AdvancedPowerRegulation (-> SmartBattery, PowerTransformer)
|
||||
LiquidPiping (-> LiquidPump, WaterSiever, Desalinator)
|
||||
GasPiping (-> GasPump, GasFilter)
|
||||
FineDining (-> GasRange, Refrigerator)
|
||||
Tier 2: ImprovedOxygen (-> Electrolyzer)
|
||||
RefinedObjects (-> MetalRefinery, RockCrusher)
|
||||
TemperatureModulation (-> ThermoAquatuner, ThermoRegulator)
|
||||
Ranching (-> GroomingStation, Incubator)
|
||||
SmartStorage (-> LiquidReservoir, GasReservoir)
|
||||
Automation (-> first automation buildings)
|
||||
Tier 3: HighTemperatureForging (-> Steel)
|
||||
SpaceProgram (-> telescope, rocket platform)
|
||||
CryoFuelPropulsion (-> hydrogen rocket engine)
|
||||
Tier 4: MaterialsScience (-> SuperCoolant, ViscoGel)
|
||||
```
|
||||
|
||||
## Building Size Reference (width x height)
|
||||
|
||||
```
|
||||
1x1: Wire, GasPipe, LiquidPipe, AutomationWire, Switch, Sensor
|
||||
1x2: ManualGenerator, Battery, PlanterBox, FarmTile, RationBox
|
||||
2x1: GasFilter, LiquidFilter, Incubator, Jukebot
|
||||
2x2: CoalGenerator, HydrogenGenerator, OxygenDiffuser, Electrolyzer,
|
||||
GasPump, LiquidPump, AlgaeTerrarium, Deodorizer, WaterSiever,
|
||||
ElectricGrill, MicrobeMusher, RockCrusher, Kiln, Compost,
|
||||
MassageTable, Apothecary, ResearchStation, TextileLoom
|
||||
2x3: MetalRefinery, PolymerPress, OilRefinery, AtmoSuitDock
|
||||
2x4: Telescope, MolecularForge
|
||||
3x1: LadderBed, DisplayShelf
|
||||
3x2: ManualAirlock, PneumaticDoor, MechanizedAirlock, ConveyorLoader,
|
||||
SmartStorageBin, LiquidReservoir, GasReservoir
|
||||
3x3: Shower, Sauna, HotTub
|
||||
3x4: SteamTurbine
|
||||
4x2: SuperComputer, JumboBattery, StorageBin
|
||||
4x4: SolarPanel
|
||||
```
|
||||
|
||||
## Common Game Mechanics (AI reference)
|
||||
|
||||
### Gas/Liquid Physics
|
||||
- Gases **layer by density**: CO2 (sinks) < O2 < PollutedO2 < NaturalGas < H2 (rises)
|
||||
- Liquids **layer by density**: Petroleum < Water/SaltWater/Brine < CrudeOil < Mercury
|
||||
- Pressure limit for gas vents: GasVent = 2kg/tile, HighPressureGasVent = 20kg/tile
|
||||
- Buildings overheat above their overheat temperature (default 75C, some higher)
|
||||
- Insulated Tile reduces heat transfer by ~100x
|
||||
|
||||
### Duplicant Needs
|
||||
- Oxygen: need ~100g/s per dupe
|
||||
- Food: need ~1000kcal/cycle per dupe
|
||||
- Stress: > 50% starts causing problems, > 80% mental breaks
|
||||
- Temperature: comfortable at 18-35C, hypothermia below 10C, hyperthermia above 40C
|
||||
- Morale: affected by decor, food quality, room bonuses
|
||||
|
||||
### Room Types
|
||||
- Latrine: Outhouse + WashBasin = +1 morale
|
||||
- Washroom: Lavatory + Sink = +2 morale
|
||||
- Barracks: Cot/LadderBed + 1 decor = +1 morale
|
||||
- Bedroom: ComfyBed + 1 decor = +2 morale
|
||||
- Mess Hall: MessTable + 1 decor = +3 morale
|
||||
- Great Hall: MessTable + 2+ decor and recreation = +6 morale
|
||||
- Park: 4+ wild/planted plants = +1 morale
|
||||
- Nature Reserve: 12+ wild plants = +6 morale
|
||||
- Stable: GroomingStation + CritterDropOff = ranching room
|
||||
- Ranch: Incubator + feeder (spaced out)
|
||||
|
||||
### SPOM (Self-Powered Oxygen Module)
|
||||
The classic Rodriguez SPOM:
|
||||
- 1 Electrolyzer (consumes 1kg/s water -> 888g/s O2 + 112g/s H2)
|
||||
- 2 GasPumps for O2 extraction
|
||||
- 1 HydrogenGenerator (burns 100g/s H2, produces 800W)
|
||||
- 1 GasFilter to separate H2 from O2
|
||||
- Net power positive (runs on its own hydrogen)
|
||||
- Standard size: 8x6 tiles
|
||||
349
docs/MOD_DEV_GUIDE.md
Normal file
349
docs/MOD_DEV_GUIDE.md
Normal file
@ -0,0 +1,349 @@
|
||||
# ONI Agent Mod 开发指南
|
||||
|
||||
## 概述
|
||||
|
||||
本文档规定了一个与外部 Agent(AI 助手)对接的《缺氧》(Oxygen Not Included) Mod 应遵循的接口规范、约束条件和最佳实践。遵循此规范开发的 Mod 可与 `oni-agent` 工具链无缝协作。
|
||||
|
||||
---
|
||||
|
||||
## 1. 通信协议
|
||||
|
||||
### 1.1 传输层
|
||||
- **协议**: HTTP 1.1
|
||||
- **地址**: `127.0.0.1`(仅本地回环,禁止暴露到外部网络)
|
||||
- **端口**: 由配置文件指定(默认 `23876`)
|
||||
- **编码**: 所有请求和响应均为 UTF-8
|
||||
|
||||
### 1.2 数据格式
|
||||
- 所有请求和响应使用 `application/json`
|
||||
- 响应必须包含有效的 JSON
|
||||
- 错误响应必须包含 `error` 字段
|
||||
|
||||
```json
|
||||
// 成功响应
|
||||
{ "result": "ok", ... }
|
||||
|
||||
// 错误响应
|
||||
{ "error": "error_message" }
|
||||
```
|
||||
|
||||
### 1.3 请求超时
|
||||
- 服务端应在 5 秒内响应
|
||||
- 长时间操作应排队后立即返回 `{ "result": "queued" }`
|
||||
|
||||
---
|
||||
|
||||
## 2. API 端点规范
|
||||
|
||||
### 2.1 健康检查
|
||||
|
||||
```
|
||||
GET /health
|
||||
```
|
||||
|
||||
用于 Agent 探测 Mod 是否存活。必须始终可达,不依赖游戏状态。
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{ "status": "ok", "service": "mod_name" }
|
||||
```
|
||||
|
||||
### 2.2 状态查询端点
|
||||
|
||||
所有状态查询为 `GET` 请求,路径前缀 `/api/state/`。
|
||||
|
||||
| 端点 | 返回内容 | 必需 | 新增字段 |
|
||||
|------|---------|------|---------|
|
||||
| `/api/state/game` | 周期、复制人、世界尺寸 | **是** | `gridWidth`, `gridHeight` |
|
||||
| `/api/state/resources` | 资源列表(含分类/物态) | **是** | `id`, `state`, `category` |
|
||||
| `/api/state/duplicants` | 复制人详情 | **是** | `x`, `y`, `cell`, `currentChore` |
|
||||
| `/api/state/buildings` | 建筑列表 | 推荐 | `cell`, `category`, `powerWatt` |
|
||||
| `/api/state/research` | 科技树(含解锁列表) | 推荐 | `requiredTechs`, `unlockedBuildings` |
|
||||
| `/api/state/geysers` | 喷泉详情 | 可选 | `cell`, `isActive`, `isDormant` |
|
||||
| `/api/state/alert` | 当前警报 | 推荐 | `clickable` |
|
||||
| `/api/state/critters` | 小动物详情 | 可选 | `cell`, `calories` |
|
||||
| `/api/state/plants` | 植物列表 | 可选 | `isGrown`, `progress`, `isWilting` |
|
||||
| `/api/state/rooms` | 房间列表 | 可选 | 类型/格数/建筑/生物/植物 |
|
||||
|
||||
### 2.3 地图/格子数据端点
|
||||
|
||||
这是 AI 理解游戏世界最重要的端点。格子级数据让 AI 知道每个具体位置的状态。
|
||||
|
||||
| 端点 | 说明 | 参数 |
|
||||
|------|------|------|
|
||||
| `GET /api/state/cell` | 单格详情 | `?x=&y=` |
|
||||
| `GET /api/state/cells` | 矩形区域(批量) | `?x=&y=&width=&height=` |
|
||||
| `GET /api/state/cells/slice` | 行或列扫描 | `?axis=x&index=&start=&end=` |
|
||||
| `GET /api/state/gas` | 气体分布分析 | `?x=&y=&radius=` |
|
||||
|
||||
#### `GET /api/state/cell?x=10&y=5`
|
||||
|
||||
```json
|
||||
{
|
||||
"x": 10, "y": 5, "cell": 4523,
|
||||
"element": "Oxygen",
|
||||
"elementId": "Oxygen",
|
||||
"elementState": "gas",
|
||||
"massKg": 1.8,
|
||||
"temperatureC": 23.5,
|
||||
"isSolid": false, "isLiquid": false, "isGas": true,
|
||||
"hasBuilding": true, "buildingName": "GasPump",
|
||||
"hasDuplicant": false, "duplicantName": null,
|
||||
"isVacuum": false, "isVisible": true
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/state/cells?x=0&y=0&width=5&height=5`
|
||||
|
||||
```json
|
||||
{
|
||||
"region": { "x": 0, "y": 0, "width": 5, "height": 5 },
|
||||
"cells": [ /* array of cell objects */ ]
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 实体注册表端点 (AI 参考)
|
||||
|
||||
用于 AI 在运行时查询游戏实体的元数据。所有为 `GET` 请求。
|
||||
|
||||
| 端点 | 返回内容 |
|
||||
|------|---------|
|
||||
| `/api/registry/buildings` | 全部建筑定义(尺寸/功耗/发热/材料) |
|
||||
| `/api/registry/elements` | 全部元素定义(比热容/导热/熔沸点) |
|
||||
| `/api/registry/techs` | 全部科技定义(前置/解锁建筑) |
|
||||
|
||||
#### `GET /api/registry/buildings`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "Electrolyzer",
|
||||
"name": "Electrolyzer",
|
||||
"category": "Oxygen",
|
||||
"width": 2, "height": 2,
|
||||
"powerCost": 120,
|
||||
"heatGeneration": 1.25,
|
||||
"constructionMass": ["IronOre", "IronOre"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 2.5 操作端点
|
||||
|
||||
所有操作为 `POST` 请求,路径前缀 `/api/action/`。
|
||||
|
||||
| 端点 | 作用 | 必需 |
|
||||
|------|------|------|
|
||||
| `/api/action/dig` | 挖掘指定区域 | **是** |
|
||||
| `/api/action/build` | 建造建筑 | **是** |
|
||||
| `/api/action/deconstruct` | 拆除建筑 | 推荐 |
|
||||
| `/api/action/prioritize` | 设置优先级 | 可选 |
|
||||
| `/api/action/research` | 选择研究方向 | 推荐 |
|
||||
| `/api/action/schedule` | 修改复制人日程 | 可选 |
|
||||
| `/api/action/wardrobe` | 修改复制人装备 | 可选 |
|
||||
| `/api/action/mop` | 清理液体 | 推荐 |
|
||||
| `/api/action/harvest` | 收获植物 | 推荐 |
|
||||
| `/api/action/cancel` | 取消操作 | 可选 |
|
||||
|
||||
#### `GET /api/state/game`
|
||||
|
||||
```json
|
||||
{
|
||||
"cycle": 42,
|
||||
"duplicantCount": 6,
|
||||
"worldName": "Terra",
|
||||
"worldSize": 256
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/state/duplicants`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Dup1",
|
||||
"stress": 12.5,
|
||||
"calories": 850000,
|
||||
"stamina": 98.2,
|
||||
"oxygen": 85.0,
|
||||
"diseases": 0,
|
||||
"skillLevels": 3
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### `GET /api/state/resources`
|
||||
|
||||
```json
|
||||
[
|
||||
{ "name": "Oxygen", "tag": "Oxygen", "amount": 12345.6, "unit": "kg" }
|
||||
]
|
||||
```
|
||||
|
||||
#### `GET /api/state/buildings`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Manual Generator",
|
||||
"id": "ManualGenerator",
|
||||
"x": 10,
|
||||
"y": 5,
|
||||
"isOperational": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 2.3 操作端点
|
||||
|
||||
所有操作为 `POST` 请求,路径前缀 `/api/action/`。
|
||||
|
||||
| 端点 | 作用 | 必需 |
|
||||
|------|------|------|
|
||||
| `/api/action/dig` | 挖掘指定区域 | **是** |
|
||||
| `/api/action/build` | 建造建筑 | **是** |
|
||||
| `/api/action/deconstruct` | 拆除建筑 | 推荐 |
|
||||
| `/api/action/prioritize` | 设置优先级 | 可选 |
|
||||
| `/api/action/research` | 选择研究方向 | 推荐 |
|
||||
| `/api/action/schedule` | 修改复制人日程 | 可选 |
|
||||
| `/api/action/wardrobe` | 修改复制人装备 | 可选 |
|
||||
|
||||
#### `POST /api/action/dig`
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{ "x": 10, "y": 5, "width": 8, "height": 6 }
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{ "result": "dig_queued", "x": 10, "y": 5, "width": 8, "height": 6 }
|
||||
```
|
||||
|
||||
#### `POST /api/action/build`
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{ "buildingId": "Electrolyzer", "x": 10, "y": 5, "rotation": null }
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{ "result": "build_queued", "buildingId": "Electrolyzer", "x": 10, "y": 5 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Mod 约束
|
||||
|
||||
### 3.1 安全性
|
||||
1. **仅绑定本地回环地址** `127.0.0.1`,不得监听 `0.0.0.0`
|
||||
2. **不得实现认证/授权**——回环地址默认安全
|
||||
3. **不得执行文件 I/O**(读取 Mod 自带配置除外)
|
||||
4. **输入校验**——所有用户输入必须校验类型和范围
|
||||
|
||||
### 3.2 性能
|
||||
1. 状态查询必须是**只读操作**,不得持有锁
|
||||
2. 资源列表等大数据量接口应考虑分页(未来扩展)
|
||||
3. 建造/挖掘等操作应返回 `queued` 后异步执行
|
||||
4. HTTP 服务器应在**单独线程**运行,不得阻塞游戏主线程
|
||||
|
||||
### 3.3 兼容性
|
||||
1. 使用 `KMod.UserMod2` 基类
|
||||
2. 使用 `Harmony` 进行补丁(如果使用)
|
||||
3. 引用 `UnityEngine` 和 `Assembly-CSharp` 程序集
|
||||
4. 最小支持游戏版本应在 `mod_info.yaml` 中声明(当前推荐 `612000`)
|
||||
|
||||
### 3.4 错误处理
|
||||
```json
|
||||
// Mod 内部错误的通用格式
|
||||
{ "error": "error_type", "details": "human readable message" }
|
||||
|
||||
// 常见错误类型
|
||||
{ "error": "not_found" } // 404 端点不存在
|
||||
{ "error": "invalid_request" } // 请求体解析失败
|
||||
{ "error": "not_implemented" } // 功能尚未实现
|
||||
{ "error": "unknown_building", "buildingId": "..." } // 建筑 ID 不识别
|
||||
{ "error": "unknown_tech", "techId": "..." } // 科技 ID 不识别
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 配置文件规范
|
||||
|
||||
Agent 侧通过 `config.json` 定位 Mod:
|
||||
|
||||
```json
|
||||
{
|
||||
"modHost": "127.0.0.1",
|
||||
"modPort": 23876,
|
||||
"timeout": 10
|
||||
}
|
||||
```
|
||||
|
||||
- `modHost`: 始终为 `127.0.0.1`
|
||||
- `modPort`: 应与 Mod 中监听的端口一致
|
||||
- `timeout`: HTTP 请求超时秒数
|
||||
|
||||
---
|
||||
|
||||
## 5. AI-Friendly 数据设计原则
|
||||
|
||||
### 5.1 数据可读性
|
||||
所有暴露的数据应满足 AI 可直接理解的三个条件:
|
||||
1. **命名语义化** — 使用自然语言字段名(如 `temperatureC` 而非 `tempK`)
|
||||
2. **数据类型合理** — 使用数字而非枚举字符串(温度用 `float` 而非 `string`)
|
||||
3. **上下文完整** — 每个实体包含足够的位置和状态信息(坐标、类别、是否可运行)
|
||||
|
||||
### 5.2 坐标系统
|
||||
- 统一使用 `(x, y)` 整数坐标,对应游戏网格
|
||||
- 原点在左下角:`(0, 0)`
|
||||
- 建筑使用其左下角锚点坐标
|
||||
- 返回数据中同时提供 `cell` 索引(Grid 内部使用)和 `(x, y)` 坐标
|
||||
|
||||
### 5.3 实体分类
|
||||
每个实体(建筑、元素、科技)必须包含分类标签:
|
||||
- 建筑: `category`(Base/Oxygen/Power/Food/...)
|
||||
- 元素: `state`(solid/liquid/gas)+ `category`(metal/water/fuel/...)
|
||||
- 资源: 带 `state` 和 `category` 帮助 AI 推理用途
|
||||
|
||||
### 5.4 AI 推理辅助
|
||||
- `/api/registry/*` 端点提供完整的实体元数据查询
|
||||
- `GET /api/state/gas` 提供区域气体分布统计(AI 无法逐格遍历)
|
||||
- `/api/state/rooms` 提供房间判定结果(AI 无法自行判断房间类型)
|
||||
|
||||
### 5.5 操作设计
|
||||
操作设计遵循以下原则:
|
||||
- **幂等性** — 同个操作重复执行不产生副作用
|
||||
- **非阻塞** — 操作立即返回 `queued`,异步执行
|
||||
- **确定性** — 使用绝对坐标 `(x, y)`,不支持"在某个建筑旁边"这类模糊表述
|
||||
- **输入校验** — 拒绝未知的 `buildingId` 或越界坐标
|
||||
|
||||
## 6. 扩展建议
|
||||
|
||||
### 5.1 添加新端点
|
||||
1. 在 `ProcessRequest` 中添加路由匹配
|
||||
2. 实现对应的处理方法
|
||||
3. 更新本指南和 `SKILL.md`
|
||||
|
||||
### 5.2 蓝图系统
|
||||
预置的建造方案(蓝图)应满足:
|
||||
- 坐标相对于原点,便于偏移
|
||||
- 包含预先挖掘区域
|
||||
- 建筑顺序隐含依赖关系
|
||||
|
||||
### 5.3 事件推送(未来)
|
||||
考虑支持 WebSocket 或 SSE,用于游戏事件实时推送(如警报触发)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 开发检查清单
|
||||
|
||||
- [ ] Mod 继承 `UserMod2`,在 `OnLoad` 中启动 HTTP 服务
|
||||
- [ ] 在 `OnUnload` 中停止 HTTP 服务
|
||||
- [ ] 实现全部必需端点(health, game, resources, duplicants, dig, build)
|
||||
- [ ] 错误响应包含 `error` 字段
|
||||
- [ ] 端口号与 `config.json` 一致
|
||||
- [ ] 仅绑定 `127.0.0.1`
|
||||
- [ ] 使用 `Newtonsoft.Json`(游戏自带,无需额外依赖)
|
||||
- [ ] `mod_info.yaml` 声明了正确的游戏版本
|
||||
40
docs/batch_example.json
Normal file
40
docs/batch_example.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "Build SPOM - Step 1: Dig and Electrolyzer",
|
||||
"actions": [
|
||||
{
|
||||
"type": "dig",
|
||||
"x": 42,
|
||||
"y": 40,
|
||||
"width": 8,
|
||||
"height": 6
|
||||
},
|
||||
{
|
||||
"type": "build",
|
||||
"buildingId": "Electrolyzer",
|
||||
"x": 45,
|
||||
"y": 42
|
||||
},
|
||||
{
|
||||
"type": "build",
|
||||
"buildingId": "GasPump",
|
||||
"x": 43,
|
||||
"y": 42
|
||||
},
|
||||
{
|
||||
"type": "build",
|
||||
"buildingId": "HydrogenGenerator",
|
||||
"x": 45,
|
||||
"y": 40
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"delayMs": 100
|
||||
},
|
||||
{
|
||||
"type": "priority",
|
||||
"x": 45,
|
||||
"y": 42,
|
||||
"priority": 9
|
||||
}
|
||||
]
|
||||
}
|
||||
180
mod/API_REFERENCE.md
Normal file
180
mod/API_REFERENCE.md
Normal file
@ -0,0 +1,180 @@
|
||||
# ONI Mod API Reference (Confirmed via DLL Decompile)
|
||||
## Assembly: Assembly-CSharp.dll (D:\steam\...\Managed\)
|
||||
|
||||
### BuildingDef (extends Def -> ScriptableObject)
|
||||
```
|
||||
FIELD string PrefabID # FROM BASE CLASS Def
|
||||
FIELD int WidthInCells
|
||||
FIELD int HeightInCells
|
||||
FIELD float EnergyConsumptionWhenActive
|
||||
FIELD float ExhaustKilowattsWhenActive
|
||||
FIELD string[] MaterialCategory
|
||||
FIELD float[] Mass
|
||||
FIELD string Name # PROPERTY (Name getter)
|
||||
PROPERTY string Desc, Flavor, Effect
|
||||
```
|
||||
> **Usage**: `def.PrefabID` (NOT `def?.PrefabID` — fields cannot use null-conditional)
|
||||
|
||||
### Building (base of BuildingComplete)
|
||||
```
|
||||
FIELD BuildingDef Def
|
||||
PROPERTY Orientation Orientation
|
||||
METHOD int GetCell()
|
||||
METHOD int GetBottomLeftCell()
|
||||
```
|
||||
> **Usage**: `((BuildingComplete)item).Def.PrefabID` / `building.Def.WidthInCells`
|
||||
> `IsOperational` is NOT on BuildingComplete — use `go.GetComponent<Operational>()?.IsOperational`
|
||||
> `IsOperational` is a PROPERTY (bool), NOT a method: no `()` after it
|
||||
|
||||
### Operational
|
||||
```
|
||||
PROPERTY bool IsOperational # getter
|
||||
PROPERTY bool IsFunctional
|
||||
PROPERTY bool IsActive
|
||||
METHOD void SetFlag(Flag flag, bool value)
|
||||
METHOD bool GetFlag(Flag flag)
|
||||
METHOD void SetActive(bool value, bool force_ignore)
|
||||
```
|
||||
|
||||
### Generator
|
||||
```
|
||||
PROPERTY float WattageRating
|
||||
PROPERTY ushort CircuitID
|
||||
PROPERTY float JoulesAvailable
|
||||
PROPERTY float Capacity
|
||||
PROPERTY bool IsEmpty
|
||||
```
|
||||
> NO `IsPowered` property. Check `JoulesAvailable > 0` instead.
|
||||
|
||||
### ConduitFlow.ConduitContents (STRUCT — value type!)
|
||||
```
|
||||
STRUCT ConduitContents {
|
||||
SimHashes element # FIELD
|
||||
float mass # FIELD
|
||||
float temperature # FIELD
|
||||
int diseaseHash
|
||||
int diseaseCount
|
||||
}
|
||||
```
|
||||
> `flow.GetContents(int cell)` returns `ConduitContents` (always valid since it's a struct)
|
||||
> **Never** null-check with `!= null` — use `contents.mass > 0` instead
|
||||
|
||||
### CircuitManager
|
||||
```
|
||||
METHOD ushort GetCircuitID(int cell)
|
||||
METHOD float GetWattsUsedByCircuit(ushort circuitID)
|
||||
METHOD float GetWattsGeneratedByCircuit(ushort circuitID)
|
||||
METHOD float GetMaxSafeWattageForCircuit(ushort circuitID)
|
||||
METHOD List<Generator> GetGeneratorsOnCircuit(ushort circuitID)
|
||||
METHOD List<IEnergyConsumer> GetConsumersOnCircuit(ushort circuitID)
|
||||
METHOD List<Battery> GetBatteriesOnCircuit(ushort circuitID)
|
||||
```
|
||||
> NO `GetCircuits()` method. Discover circuit IDs via `GetCircuitID(cell)` for each building.
|
||||
|
||||
### SaveLoader
|
||||
```
|
||||
PROPERTY static SaveLoader Instance # getter
|
||||
METHOD string Save(string filename, bool isAutoSave, bool updateSavePointer)
|
||||
METHOD bool Load(string filename)
|
||||
METHOD string GetActiveSaveFilePath()
|
||||
```
|
||||
> All are INSTANCE methods. Usage: `SaveLoader.Instance.Save(...)`
|
||||
|
||||
### Techs (ResourceSet<Tech>)
|
||||
```
|
||||
METHOD Tech TryGetTechForTechItem(string itemId)
|
||||
```
|
||||
> To enumerate: use the base class `ResourceSet<T>` which has `resources` / implements IEnumerable
|
||||
> `Db.Get().Techs` returns the Techs collection. Iterate via `foreach (var tech in Db.Get().Techs.resources)`
|
||||
> or check if Techs implements IEnumerable directly
|
||||
|
||||
### World
|
||||
```
|
||||
PROPERTY static World Instance
|
||||
```
|
||||
> NO `worldName` property. **Fallback**: Use a hardcoded string like "ONI World"
|
||||
|
||||
### Components quirks
|
||||
- `Cmps<T>` implements `IEnumerable` (non-generic) → foreach gives `object`, cast explicitly
|
||||
- `CmpsByWorld<T>` does NOT implement IEnumerable → CANNOT foreach. Access items through its internal API
|
||||
- Workaround: skip `Components.Geysers` or use reflection to access items
|
||||
- `Components.CreatureIdentities` NOT FOUND → use `Components.Brains` instead
|
||||
- `Components.Crops` gives `Crop` objects → access `.gameObject` not cast to GameObject
|
||||
|
||||
### Missing types in this ONI version
|
||||
- ~~`ImmuneSystemMonitor`~~ → NOT FOUND, wrap in try-catch
|
||||
- ~~`GlobalNotificationManager`~~ → NOT FOUND, use `Notifier` if available
|
||||
- ~~`AlertManager`~~ → NOT FOUND
|
||||
- ~~`PriorityScreen.Instance`~~ → NOT FOUND
|
||||
- ~~`AttributeLevels`~~ → NOT FOUND
|
||||
- ~~`GameTags.RawPoultry` / `PreciousStone`~~ → NOT FOUND
|
||||
- ~~`MoraleProvider` / `QualityOfLife` / `HappyMonitor`~~ → NOT FOUND
|
||||
|
||||
### Grid (Stable — confirmed working)
|
||||
```
|
||||
STATIC Element[] Element # grid[cell]
|
||||
STATIC float[] Mass
|
||||
STATIC float[] Temperature
|
||||
STATIC bool[] Solid
|
||||
STATIC bool[] IsVisible
|
||||
STATIC GameObject[,] Objects # [cell, ObjectLayer enum]
|
||||
STATIC int XYToCell(int x, int y)
|
||||
STATIC void CellToXY(int cell, out int x, out int y)
|
||||
STATIC int CellCount (field)
|
||||
STATIC int WidthInCells
|
||||
STATIC int HeightInCells
|
||||
```
|
||||
|
||||
### ObjectLayer (enum values include)
|
||||
- Building, Pickupables, Minion, Plants, Creatures, etc.
|
||||
- NO `Creature` value → check the actual enum
|
||||
|
||||
### Health
|
||||
```
|
||||
PROPERTY float hitPoints # NOT a method
|
||||
PROPERTY float maxHitPoints
|
||||
METHOD bool IsIncapacitated()
|
||||
METHOD void Damage(float amount)
|
||||
```
|
||||
|
||||
### Verified correct patterns
|
||||
```csharp
|
||||
// Buildings
|
||||
foreach (var item in Components.BuildingCompletes) {
|
||||
var b = (BuildingComplete)item;
|
||||
var go = b.gameObject;
|
||||
var def = b.Def; // FIELD on Building base
|
||||
string id = def.PrefabID; // FIELD on Def base
|
||||
int w = def.WidthInCells;
|
||||
bool op = go.GetComponent<Operational>()?.IsOperational ?? false;
|
||||
}
|
||||
|
||||
// Pipes (ConduitContents is struct!)
|
||||
var contents = flow.GetContents(cell);
|
||||
if (contents.mass > 0) { ... }
|
||||
|
||||
// Power
|
||||
ushort cid = mgr.GetCircuitID(cell);
|
||||
float used = mgr.GetWattsUsedByCircuit(cid);
|
||||
|
||||
// Save/Load
|
||||
SaveLoader.Instance.Save("name", true, false);
|
||||
SaveLoader.Instance.Load("path");
|
||||
|
||||
// Pause
|
||||
SpeedControlScreen.Instance.Pause(false, true);
|
||||
SpeedControlScreen.Instance.Unpause(true);
|
||||
bool paused = SpeedControlScreen.Instance.IsPaused;
|
||||
int speed = SpeedControlScreen.Instance.GetSpeed();
|
||||
SpeedControlScreen.Instance.SetSpeed(3);
|
||||
|
||||
// Camera
|
||||
CameraController.Instance.SnapTo(new Vector3(x, y, z));
|
||||
|
||||
// Duplicants
|
||||
foreach (var item in Components.MinionIdentities) {
|
||||
var m = (MinionIdentity)item;
|
||||
string name = m.GetProperName();
|
||||
var go = m.gameObject;
|
||||
}
|
||||
```
|
||||
241
mod/ONIAgentBridge.cs
Normal file
241
mod/ONIAgentBridge.cs
Normal file
@ -0,0 +1,241 @@
|
||||
using HarmonyLib;
|
||||
using KMod;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ONIAgentBridge
|
||||
{
|
||||
public class Mod : UserMod2
|
||||
{
|
||||
private HttpListener _listener;
|
||||
internal static ConcurrentQueue<System.Action> cmdQueue = new ConcurrentQueue<System.Action>();
|
||||
internal static List<GameEvent> eventLog = new List<GameEvent>();
|
||||
internal static int eventSeq = 0;
|
||||
internal static object eventLock = new object();
|
||||
|
||||
public class QueueProcessor : UnityEngine.MonoBehaviour
|
||||
{
|
||||
public void Update()
|
||||
{
|
||||
System.Action a;
|
||||
while (cmdQueue.TryDequeue(out a))
|
||||
try { a(); } catch (System.Exception ex) { lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[Q] "+ex.Message}); } }
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnLoad(Harmony harmony)
|
||||
{
|
||||
base.OnLoad(harmony);
|
||||
var go = new GameObject("ONI_Processor");
|
||||
go.AddComponent<QueueProcessor>();
|
||||
UnityEngine.Object.DontDestroyOnLoad(go);
|
||||
int port = 23876;
|
||||
_listener = new HttpListener();
|
||||
_listener.Prefixes.Add($"http://127.0.0.1:{port}/");
|
||||
_listener.Start();
|
||||
_listener.BeginGetContext(OnRequest, null);
|
||||
}
|
||||
|
||||
void OnRequest(IAsyncResult ar)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ctx = _listener.EndGetContext(ar);
|
||||
_listener.BeginGetContext(OnRequest, null);
|
||||
Process(ctx);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
void Process(HttpListenerContext ctx)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = ctx.Request.Url.AbsolutePath.TrimEnd('/');
|
||||
var method = ctx.Request.HttpMethod;
|
||||
var q = ctx.Request.QueryString;
|
||||
string json = null;
|
||||
|
||||
System.Action respond = () => {
|
||||
var b = Encoding.UTF8.GetBytes(json ?? "{}");
|
||||
ctx.Response.ContentType = "application/json";
|
||||
ctx.Response.OutputStream.Write(b, 0, b.Length);
|
||||
ctx.Response.OutputStream.Close();
|
||||
};
|
||||
|
||||
if (path == "/api/screenshot/latest" && method == "GET") { ServeScreenshot(ctx); return; }
|
||||
else if (path == "/health" && method == "GET") json = J(new { status = "ok" });
|
||||
else if (path == "/api/state/game" && method == "GET") json = ReadGame();
|
||||
else if (path == "/api/state/resources" && method == "GET") json = ReadResources();
|
||||
else if (path == "/api/state/buildings" && method == "GET") json = ReadBuildings();
|
||||
else if (path == "/api/state/duplicants" && method == "GET") json = ReadDupes();
|
||||
else if (path == "/api/state/rooms" && method == "GET") json = ReadRooms();
|
||||
else if (path == "/api/state/cell" && method == "GET") json = ReadCell(q);
|
||||
else if (path == "/api/state/cells" && method == "GET") json = ReadCells(q);
|
||||
else if (path == "/api/state/gas" && method == "GET") json = ReadGas(q);
|
||||
else if (path == "/api/state/co2" && method == "GET") json = ReadCO2();
|
||||
else if (path == "/api/state/temperature/zones" && method == "GET") json = ReadTemp();
|
||||
else if (path == "/api/state/power" && method == "GET") json = ReadPower();
|
||||
else if (path == "/api/state/pipes" && method == "GET") json = ReadPipes(q);
|
||||
else if (path == "/api/state/events" && method == "GET") json = ReadEvents(q);
|
||||
else if (path == "/api/state/alert" && method == "GET") json = ReadAlerts();
|
||||
else if (path == "/api/state/storage" && method == "GET") json = ReadStorage();
|
||||
else if (path == "/api/state/saves" && method == "GET") json = ReadSaves();
|
||||
else if (path == "/api/state/camera" && method == "GET") json = ReadCamera();
|
||||
else if (path == "/api/registry/buildings" && method == "GET") json = ReadBuildingReg();
|
||||
else if (path == "/api/registry/elements" && method == "GET") json = ReadElementReg();
|
||||
else if (path == "/api/action/pause" && method == "POST") { cmdQueue.Enqueue(() => { try { SpeedControlScreen.Instance.Pause(false, true); } catch { } }); json = Ok("paused"); }
|
||||
else if (path == "/api/action/unpause" && method == "POST") { cmdQueue.Enqueue(() => { try { SpeedControlScreen.Instance.Unpause(true); SpeedControlScreen.Instance.SetSpeed(1); } catch { } }); json = Ok("unpaused"); }
|
||||
else if (path == "/api/action/dig" && method == "POST") json = QueueDig(ctx);
|
||||
else if (path == "/api/action/build" && method == "POST") json = QueueBuild(ctx);
|
||||
else if (path == "/api/action/test" && method == "POST") { cmdQueue.Enqueue(() => { lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[Test] queue works!"}); } }); json = Ok("test_queued"); }
|
||||
else json = J(new { error = "not_found", path });
|
||||
|
||||
respond();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
string J(object o) => JsonConvert.SerializeObject(o);
|
||||
string Ok(string r, object d = null) => J(new { success = true, result = r, data = d ?? new { } });
|
||||
string Fail(string e, string m = null) => J(new { success = false, error = e, errorMessage = m ?? e });
|
||||
|
||||
void PushEvent(string t, string s, string title, string msg, string cat = "general")
|
||||
{
|
||||
lock (eventLock) { eventLog.Add(new GameEvent { id = eventSeq++, type = t, severity = s, timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), cycle = 0, category = cat, title = title, message = msg }); if (eventLog.Count > 500) eventLog.RemoveRange(0, eventLog.Count - 500); }
|
||||
}
|
||||
|
||||
// === READERS (safe from background threads) ===
|
||||
string ReadGame() => J(new { cycle = GameClock.Instance?.GetCycle() ?? 0, duplicantCount = Components.MinionIdentities.Count(), worldSize = Grid.CellCount, gridWidth = Grid.WidthInCells, gridHeight = Grid.HeightInCells, isPaused = SpeedControlScreen.Instance.IsPaused, gameSpeed = SpeedControlScreen.Instance.IsPaused ? 0 : SpeedControlScreen.Instance.GetSpeed() });
|
||||
string ReadResources() { try { var l = new List<object>(); foreach (var e in ElementLoader.elements) { float a = 0; try { var w = ClusterManager.Instance?.activeWorld; if (w != null) a = w.worldInventory.GetAmount(e.tag, false); } catch { } if (a > 0) l.Add(new { id = e.id.ToString(), name = e.name, tag = e.tag.ToString(), amount = a, unit = "kg", state = e.IsGas ? "gas" : e.IsLiquid ? "liquid" : "solid" }); } return J(l); } catch { return J(new List<object>()); } }
|
||||
string ReadBuildings() { try { var l = new List<object>(); foreach (var i in Components.BuildingCompletes) { var b = (BuildingComplete)i; var d = b.Def; l.Add(new { id = d.PrefabID, name = d.Name, x = (int)b.transform.position.x, y = (int)b.transform.position.y, width = d.WidthInCells, height = d.HeightInCells }); } return J(l); } catch { return J(new List<object>()); } }
|
||||
string ReadDupes() { try { var l = new List<object>(); foreach (var i in Components.MinionIdentities) { var m = (MinionIdentity)i; l.Add(new { name = m.GetProperName(), x = (int)m.transform.position.x, y = (int)m.transform.position.y, health = m.gameObject.GetComponent<Health>()?.hitPoints ?? 100 }); } return J(l); } catch { return J(new List<object>()); } }
|
||||
string ReadRooms() { try { var l = new List<object>(); var rp = Game.Instance.roomProber; if (rp != null) foreach (var r in rp.rooms) l.Add(new { id = r.roomType?.Id ?? "?", name = r.roomType?.Name ?? "?" }); return J(l); } catch { return J(new List<object>()); } }
|
||||
string ReadCell(NameValueCollection q) { try { int x = int.Parse(q["x"] ?? "-1"), y = int.Parse(q["y"] ?? "-1"); int c = Grid.XYToCell(x, y); if (c < 0 || c >= Grid.CellCount) return J(new { error = "bounds" }); var el = Grid.Element[c]; return J(new { x, y, cell = c, element = el?.name ?? "Vacuum", massKg = Grid.Mass[c], temperatureC = Grid.Temperature[c] > 0 ? Grid.Temperature[c] - 273.15f : -273.15f, isSolid = Grid.Solid[c], isVisible = true, hasBuilding = Grid.Objects[c, (int)ObjectLayer.Building] != null, buildingName = Grid.Objects[c, (int)ObjectLayer.Building]?.name, hasDuplicant = Grid.Objects[c, (int)ObjectLayer.Minion] != null, isVacuum = el == null, isDiggable = Grid.Solid[c] && el != null && el.id != SimHashes.Unobtanium }); } catch { return J(new { error = "err" }); } }
|
||||
string ReadCells(NameValueCollection q) { try { int x = int.Parse(q["x"] ?? "0"), y = int.Parse(q["y"] ?? "0"), w = int.Parse(q["width"] ?? "10"), h = int.Parse(q["height"] ?? "10"); var cs = new List<object>(); for (int cy = y; cy < y + h; cy++) for (int cx = x; cx < x + w; cx++) { int c = Grid.XYToCell(cx, cy); if (c >= 0 && c < Grid.CellCount) cs.Add(new { x = cx, y = cy, element = Grid.Element[c]?.name ?? "Vacuum", isSolid = Grid.Solid[c], hasBuilding = Grid.Objects[c, (int)ObjectLayer.Building] != null, hasDuplicant = Grid.Objects[c, (int)ObjectLayer.Minion] != null }); } return J(new { cells = cs }); } catch { return J(new { error = "err" }); } }
|
||||
string ReadGas(NameValueCollection q) { try { int x = int.Parse(q["x"] ?? "0"), y = int.Parse(q["y"] ?? "0"), r = int.Parse(q["radius"] ?? "20"); var g = new Dictionary<string, GasEntry>(); for (int cy = Math.Max(0, y - r); cy <= Math.Min(Grid.HeightInCells - 1, y + r); cy++) for (int cx = Math.Max(0, x - r); cx <= Math.Min(Grid.WidthInCells - 1, x + r); cx++) { int c = Grid.XYToCell(cx, cy); if (c < 0) continue; var el = Grid.Element[c]; if (el != null && el.IsGas) { float m = Grid.Mass[c]; if (g.ContainsKey(el.name)) { g[el.name].mass += m; g[el.name].count++; } else g[el.name] = new GasEntry { gas = el.name, mass = m, count = 1 }; } } return J(new { gases = g.Values }); } catch { return J(new { error = "err" }); } }
|
||||
string ReadCO2() { try { var p = new List<object>(); int step = Math.Max(1, Grid.CellCount / 500); for (int i = 0; i < Grid.CellCount; i += step) { var el = Grid.Element[i]; if (el != null && el.id == SimHashes.CarbonDioxide && Grid.Mass[i] > 0.5f) { int x, y; Grid.CellToXY(i, out x, out y); p.Add(new { x, y, mass = Grid.Mass[i] }); } } return J(new { pockets = p }); } catch { return J(new { error = "err" }); } }
|
||||
string ReadTemp() { try { int s = Math.Max(1, Grid.CellCount / 300); float mn = float.MaxValue, mx = float.MinValue, sum = 0; int n = 0; for (int i = 0; i < Grid.CellCount; i += s) { float t = Grid.Temperature[i]; if (t <= 0) continue; float tc = t - 273.15f; sum += tc; n++; if (tc < mn) mn = tc; if (tc > mx) mx = tc; } return J(new { averageC = n > 0 ? sum / n : 0, minC = mn, maxC = mx }); } catch { return J(new { error = "err" }); } }
|
||||
string ReadPower() { try { var c = new List<object>(); var m = Game.Instance.circuitManager; if (m != null) for (ushort i = 1; i <= 16; i++) { float u = m.GetWattsUsedByCircuit(i); if (u > 0 || m.HasGenerators(i)) c.Add(new { id = (int)i, wattsUsed = u, maxWatts = m.GetMaxSafeWattageForCircuit(i), isOverloaded = u > m.GetMaxSafeWattageForCircuit(i) }); } return J(new { circuits = c }); } catch { return J(new { error = "err" }); } }
|
||||
string ReadPipes(NameValueCollection q) { try { var s = new List<object>(); string t = q["type"] ?? "all"; foreach (var f in new[] { new { n = "liquid", fl = Game.Instance.liquidConduitFlow }, new { n = "gas", fl = Game.Instance.gasConduitFlow } }) { if (t != "all" && t != f.n) continue; if (f.fl == null) continue; int cnt = 0; foreach (var i in Components.BuildingCompletes) { if (cnt > 100) break; var b = (BuildingComplete)i; var pid = b.Def.PrefabID; if (pid != (f.n == "gas" ? "GasConduit" : "LiquidConduit") && pid != (f.n == "gas" ? "GasConduitBridge" : "LiquidConduitBridge")) continue; var ct = f.fl.GetContents(b.GetCell()); if (ct.mass > 0) { s.Add(new { type = f.n, element = ct.element.ToString(), mass = ct.mass }); cnt++; } } } return J(new { segments = s }); } catch { return J(new { error = "err" }); } }
|
||||
string ReadEvents(NameValueCollection q) { int s = int.Parse(q["since"] ?? "-1"), l = Math.Min(int.Parse(q["limit"] ?? "50"), 200); lock (eventLock) { var ev = eventLog.Where(e => e.id > s).Take(l).ToList(); return J(new { events = ev, next_seq = ev.Any() ? ev.Last().id : s }); } }
|
||||
string ReadAlerts() { try { var l = new List<object>(); var nm = global::NotificationManager.Instance; if (nm != null) { foreach (var fn in new[] { "notifications", "pendingNotifications" }) { try { var f = typeof(global::NotificationManager).GetField(fn, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); if (f == null) continue; var items = f.GetValue(nm) as System.Collections.IEnumerable; if (items != null) foreach (global::Notification n in items) l.Add(new { title = n.titleText, severity = n.Type.ToString() }); } catch { } } } return J(l); } catch { return J(new List<object>()); } }
|
||||
string ReadStorage() { try { var l = new List<object>(); foreach (var i in Components.BuildingCompletes) { var b = (BuildingComplete)i; var s = b.gameObject.GetComponent<Storage>(); if (s == null || s.MassStored() <= 0) continue; l.Add(new { building = b.Def?.Name ?? b.name, x = (int)b.transform.position.x, y = (int)b.transform.position.y, mass = s.MassStored(), capacity = s.capacityKg }); } return J(new { storages = l.Take(50).ToList() }); } catch { return J(new { error = "err" }); } }
|
||||
string ReadSaves() { try { var l = new List<object>(); string sp = SaveLoader.GetActiveSaveFilePath(); var d = Path.GetDirectoryName(sp); if (d != null && Directory.Exists(d)) foreach (var f in Directory.GetFiles(d, "*.sav")) { var fi = new FileInfo(f); l.Add(new { name = Path.GetFileNameWithoutExtension(f) }); } return J(new { saves = l }); } catch { return J(new { error = "err" }); } }
|
||||
string ReadCamera() { try { var c = CameraController.Instance; var p = c.transform.position; return J(new { x = p.x, y = p.y }); } catch { return J(new { error = "err" }); } }
|
||||
string ReadBuildingReg() { try { var l = new List<object>(); foreach (var i in Assets.BuildingDefs) { var d = (BuildingDef)i; l.Add(new { id = d.PrefabID, name = d.Name, width = d.WidthInCells, height = d.HeightInCells }); } return J(l); } catch { return J(new { error = "err" }); } }
|
||||
string ReadElementReg() { try { var l = new List<object>(); foreach (var e in ElementLoader.elements) l.Add(new { id = e.id.ToString(), name = e.name }); return J(l); } catch { return J(new { error = "err" }); } }
|
||||
|
||||
string QueueDig(HttpListenerContext c)
|
||||
{
|
||||
try
|
||||
{
|
||||
var d = JsonConvert.DeserializeObject<DigReq>(new StreamReader(c.Request.InputStream).ReadToEnd());
|
||||
if (d == null) return Fail("invalid");
|
||||
int x = d.x, y = d.y, w = d.width, h = d.height;
|
||||
cmdQueue.Enqueue(() => {
|
||||
for (int dy = 0; dy < h; dy++) for (int dx = 0; dx < w; dx++) { int cell = Grid.XYToCell(x + dx, y + dy); if (cell >= 0 && cell < Grid.CellCount) try { DigTool.PlaceDig(cell, 0); } catch { } }
|
||||
});
|
||||
return Ok("dig_queued");
|
||||
}
|
||||
catch { return Fail("error"); }
|
||||
}
|
||||
|
||||
string QueueBuild(HttpListenerContext c)
|
||||
{
|
||||
try
|
||||
{
|
||||
var d = JsonConvert.DeserializeObject<BuildReq>(new StreamReader(c.Request.InputStream).ReadToEnd());
|
||||
if (d == null) return Fail("invalid");
|
||||
int x = d.x, y = d.y; string bid = d.buildingId;
|
||||
// Find available materials on HTTP thread (WorldInventory works from background)
|
||||
var availableTags = new List<Tag>();
|
||||
try
|
||||
{
|
||||
var worldInv = ClusterManager.Instance?.activeWorld?.worldInventory;
|
||||
if (worldInv != null)
|
||||
{
|
||||
var def = Assets.GetBuildingDef(bid);
|
||||
if (def != null)
|
||||
foreach (var cat in def.MaterialCategory ?? new string[0])
|
||||
foreach (var elem in ElementLoader.elements)
|
||||
{
|
||||
if (worldInv.GetAmount(elem.tag, false) <= 0) continue;
|
||||
bool match = (cat == "RawMineral" && elem.HasTag(GameTags.ConsumableOre));
|
||||
match = match || (cat == "Metal" && elem.HasTag(GameTags.Metal));
|
||||
if (match) availableTags.Add(elem.tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
// If no materials found, use default sand
|
||||
if (availableTags.Count == 0) availableTags.Add(new Tag("SandStone"));
|
||||
List<Tag> capturedTags = new List<Tag>(availableTags);
|
||||
cmdQueue.Enqueue(() => {
|
||||
lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[Build] START "+bid}); }
|
||||
try
|
||||
{
|
||||
var def = Assets.GetBuildingDef(bid);
|
||||
if (def == null) { lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[B] def null"}); } return; }
|
||||
int cell = Grid.XYToCell(x, y);
|
||||
if (cell < 0 || cell >= Grid.CellCount) { lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[B] bounds"}); } return; }
|
||||
lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[B] cell="+cell}); }
|
||||
var world = ClusterManager.Instance?.activeWorld;
|
||||
lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[B] world ok"}); }
|
||||
for (int dy = 0; dy < def.HeightInCells; dy++)
|
||||
for (int dx = 0; dx < def.WidthInCells; dx++) {
|
||||
int c2 = Grid.XYToCell(x+dx, y+dy);
|
||||
if (c2 < 0 || c2 >= Grid.CellCount) continue;
|
||||
if (Grid.Objects[c2, (int)ObjectLayer.Building] != null) { lock(eventLock){eventLog.Add(new GameEvent{id=eventSeq++,title="[B] occupied"});} return; }
|
||||
}
|
||||
lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[B] cells ok"}); }
|
||||
var pos = Grid.CellToPos(cell);
|
||||
// Use game's build system (handles all initialization)
|
||||
lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[B] cells ok"}); }
|
||||
try {
|
||||
var loader = UnityEngine.Object.FindObjectOfType<BuildingLoader>();
|
||||
GameObject go = null;
|
||||
if (loader != null) go = loader.CreateBuildingUnderConstruction(def);
|
||||
if (go == null) {
|
||||
// Fallback: create complete building directly
|
||||
go = def.Instantiate(Grid.CellToPos(cell), Orientation.Neutral, null, (int)def.SceneLayer);
|
||||
}
|
||||
if (go != null) go.SetActive(true);
|
||||
lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[B] "+(go==null?"null":"ok")}); }
|
||||
} catch (Exception ex2) { lock(eventLock){eventLog.Add(new GameEvent{id=eventSeq++,title="[B] ex:"+ex2.Message});} }
|
||||
}
|
||||
catch (Exception ex) { lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[Build] EX:"+ex.Message}); } }
|
||||
});
|
||||
return Ok("build_queued");
|
||||
}
|
||||
catch { return Fail("error"); }
|
||||
}
|
||||
|
||||
static string _ss = null;
|
||||
void ServeScreenshot(HttpListenerContext ctx)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_ss != null && File.Exists(_ss)) { var b = File.ReadAllBytes(_ss); ctx.Response.ContentType = "image/png"; ctx.Response.ContentLength64 = b.Length; ctx.Response.OutputStream.Write(b, 0, b.Length); return; }
|
||||
}
|
||||
catch { }
|
||||
ctx.Response.StatusCode = 404;
|
||||
ctx.Response.OutputStream.Close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class GameEvent { public int id; public string type; public string severity; public long timestamp; public int cycle; public string category; public string title; public string message; }
|
||||
public class GasEntry { public string gas; public float mass; public int count; }
|
||||
public class DigReq { public int x; public int y; public int width; public int height; }
|
||||
public class BuildReq { public string buildingId; public int x; public int y; }
|
||||
}
|
||||
19
mod/ONIAgentBridge.csproj
Normal file
19
mod/ONIAgentBridge.csproj
Normal file
@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<AssemblyName>ONIAgentBridge</AssemblyName>
|
||||
<RootNamespace>ONIAgentBridge</RootNamespace>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="0Harmony" HintPath="$(ONI_Path)\0Harmony.dll" Private="False" />
|
||||
<Reference Include="Assembly-CSharp" HintPath="$(ONI_Path)\Assembly-CSharp.dll" Private="False" />
|
||||
<Reference Include="Assembly-CSharp-firstpass" HintPath="$(ONI_Path)\Assembly-CSharp-firstpass.dll" Private="False" />
|
||||
<Reference Include="Newtonsoft.Json" HintPath="$(ONI_Path)\Newtonsoft.Json.dll" Private="False" />
|
||||
<Reference Include="UnityEngine" HintPath="$(ONI_Path)\UnityEngine.dll" Private="False" />
|
||||
<Reference Include="UnityEngine.CoreModule" HintPath="$(ONI_Path)\UnityEngine.CoreModule.dll" Private="False" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="UnityEngine.ScreenCaptureModule" HintPath="$(ONI_Path)\UnityEngine.ScreenCaptureModule.dll" Private="False" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
3
mod/mod.yaml
Normal file
3
mod/mod.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
title: "ONI Agent Bridge"
|
||||
staticID: "oni_agent_bridge"
|
||||
description: "Expose HTTP API for AI agents to read ONI game state and perform actions"
|
||||
4
mod/mod_info.yaml
Normal file
4
mod/mod_info.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
supportedContent: ALL
|
||||
minimumSupportedBuild: 722606
|
||||
version: 1.0.0
|
||||
APIVersion: 2
|
||||
142
scripts/annotate_apis.py
Normal file
142
scripts/annotate_apis.py
Normal file
@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
DEPRECATED: Annotate uncertain C# APIs in the Mod source with verification markers.
|
||||
|
||||
This script was used during the initial prototyping phase. The mod has since been
|
||||
rewritten to use confirmed API signatures from the actual ONI DLL. Running this
|
||||
script on the current source will add irrelevant annotations.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
with open('mod/ONIAgentBridge.cs', 'r') as f:
|
||||
content = f.read()
|
||||
lines = content.split('\n')
|
||||
|
||||
annotations = {}
|
||||
# Track by line number (1-indexed)
|
||||
for i, line in enumerate(lines, 1):
|
||||
s = line.strip()
|
||||
|
||||
# Skip comments and using directives
|
||||
if s.startswith('//') or s.startswith('/*') or s.startswith('using ') or s.startswith('#') or s.startswith('*'):
|
||||
continue
|
||||
|
||||
# === SpeedControlScreen ===
|
||||
if 'SpeedControlScreen.Instance' in s and ('Pause(' in s or 'Unpause(' in s or 'SetSpeed(' in s):
|
||||
annotations[i] = '🔶 UNVERIFIED: Pause/Unpause/SetSpeed method signatures on SpeedControlScreen'
|
||||
|
||||
# === Circuit Manager ===
|
||||
if 'circuitManager' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Game.Instance.circuitManager — may be electricalManager'
|
||||
if 'mgr.GetCircuits()' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: circuitManager.GetCircuits() method may not exist'
|
||||
if 'circuit.WattsUsed' in s or 'circuit.WattsGenerated' in s or 'circuit.MaxWatts' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Circuit property names may differ'
|
||||
if 'gen.WattageRating' in s or 'gen.CircuitID' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Generator property names may differ'
|
||||
|
||||
# === Pipes ===
|
||||
if 'Game.Instance?.liquidConduitFlow' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Game.Instance.liquidConduitFlow — may be different conduit system'
|
||||
if 'Game.Instance?.gasConduitFlow' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Game.Instance.gasConduitFlow — may be different conduit system'
|
||||
if 'flow.GetContents(' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: ConduitFlow.GetContents() method and return type'
|
||||
if 'contents.element' in s or 'contents.mass' in s or 'contents.temperature' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: ConduitContents property names'
|
||||
if 'Components.LiquidConduits' in s or 'Components.GasConduits' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Components.LiquidConduits / GasConduits may not exist'
|
||||
if 'conduit.GetCell()' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Conduit.GetCell() method'
|
||||
|
||||
# === Printing Pod ===
|
||||
if 'pod.IsReady()' in s or 'pod.CyclesUntilReady()' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: PrintingPod methods IsReady/CyclesUntilReady — may differ'
|
||||
if 'pod.GetCurrentOffers()' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: PrintingPod.GetCurrentOffers() return type'
|
||||
if 'offer.GetName()' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: CarePackage/PrintingPodOffer.GetName() method'
|
||||
if 'pod.SelectOffer(' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: PrintingPod.SelectOffer(int) method'
|
||||
if 'ImmuneSystemMonitor.Instance' in s and ('IsReadyToPrint' in s or 'GetCyclesUntilNextPrint' in s):
|
||||
annotations[i] = '🔶 UNVERIFIED: ImmuneSystemMonitor methods may not exist'
|
||||
|
||||
# === Door ===
|
||||
if 'door.Lock()' in s or 'door.Unlock()' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Door.Lock()/Unlock() methods may not exist'
|
||||
|
||||
# === Operational/Toggle ===
|
||||
if 'oper.SetFlag(Operational.ActiveFlag,' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Operational.SetFlag signature — may need different approach'
|
||||
|
||||
# === Storage ===
|
||||
if 'storage.DropAll(' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Storage.DropAll() parameters may differ'
|
||||
|
||||
# === Camera ===
|
||||
if 'CameraController.Instance' in s and 'SetPosition' not in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: CameraController.Instance may not be the correct singleton'
|
||||
if 'Camera.main?.orthographicSize' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Camera.main.orthographicSize setter may not work in ONI'
|
||||
if 'cam.transform.position' in s and '= new Vector3' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Setting camera transform.position directly may not work'
|
||||
|
||||
# === Screenshot ===
|
||||
if 'ScreenCapture.CaptureScreenshot' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: ScreenCapture may not be available in ONI Unity version'
|
||||
|
||||
# === Save/Load ===
|
||||
if 'SaveLoader.Save(' in s or 'SaveLoader.Load(' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: SaveLoader.Save/Load method signatures'
|
||||
if 'SaveLoader.GetActiveSaveFilePath()' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: SaveLoader.GetActiveSaveFilePath() may not exist'
|
||||
|
||||
# === Research ===
|
||||
if 'Research.Instance?.GetActiveResearchTechnologies' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Research.GetActiveResearchTechnologies() method'
|
||||
if 'Research.Instance?.QueueResearch(' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Research.QueueResearch(Tech) method'
|
||||
if 'Research.Instance?.CancelResearch(' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Research.CancelResearch(Tech) method'
|
||||
if 'tech.pointsForCompletion' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Tech.pointsForCompletion property'
|
||||
|
||||
# === Overlay ===
|
||||
if 'simOverlayManager' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Game.Instance.simOverlayManager may not exist'
|
||||
if 'currentOverlay' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: currentOverlay.ToString() may not yield expected overlay names'
|
||||
|
||||
# === Buildings ===
|
||||
if 'Assets.BuildingDefs' in s and 'GetBuildingDef' not in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Assets.BuildingDefs may not be a collection'
|
||||
|
||||
# === Grid ===
|
||||
if 'Grid.Germs' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Grid.Germs API — may not exist or have different type'
|
||||
|
||||
# === DTO class ===
|
||||
if 'public int? x2' in s or 'public int? y2' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Nullable parameters may not serialize correctly'
|
||||
|
||||
# === Buildings detail ===
|
||||
if 'health?.GetHealth()' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: Health component GetHealth()/GetMaxHealth() methods'
|
||||
|
||||
# === Sensor ===
|
||||
if 'AtmoSensor' in s and '?.threshold' in s:
|
||||
annotations[i] = '🔶 UNVERIFIED: AtmoSensor/ThermoSensor/HydroSensor threshold property'
|
||||
|
||||
# Apply annotations — add comment after the line
|
||||
annotated = 0
|
||||
for lineno in sorted(annotations.keys(), reverse=True):
|
||||
idx = lineno - 1
|
||||
comment = ' // ' + annotations[lineno]
|
||||
lines[idx] = lines[idx] + comment
|
||||
annotated += 1
|
||||
|
||||
with open('mod/ONIAgentBridge.cs', 'w') as f:
|
||||
f.write('\n'.join(lines))
|
||||
|
||||
print(f"Annotated {annotated} lines with verification markers")
|
||||
30
scripts/auto_analyze.sh
Normal file
30
scripts/auto_analyze.sh
Normal file
@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
# ONI Agent - Quick analysis report
|
||||
# Runs health check, then pulls full game state and analyzes it.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
TOOLS_DIR="$SCRIPT_DIR/../tools"
|
||||
|
||||
echo "========================================"
|
||||
echo " ONI Agent - Quick Analysis"
|
||||
echo "========================================"
|
||||
|
||||
# Step 1: Health check
|
||||
echo "[1/3] Checking Mod connection..."
|
||||
python3 "$TOOLS_DIR/oni_api.py" health 2>/dev/null || {
|
||||
echo "[!] Game not connected. Run auto_repair.sh first."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Step 2: Full status dump
|
||||
echo "[2/3] Fetching game state..."
|
||||
python3 "$TOOLS_DIR/oni_api.py" status
|
||||
|
||||
# Step 3: Analysis report
|
||||
echo ""
|
||||
echo "[3/3] Running analysis..."
|
||||
python3 "$TOOLS_DIR/oni_analyzer.py"
|
||||
|
||||
echo ""
|
||||
echo "Done."
|
||||
28
scripts/auto_repair.sh
Normal file
28
scripts/auto_repair.sh
Normal file
@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# ONI Agent - Auto Repair Script
|
||||
# Detects game connection issues and attempts to restart the Mod bridge.
|
||||
set -euo pipefail
|
||||
|
||||
CONFIG_PATH="$(dirname "$0")/../config.json"
|
||||
HOST=$(python3 -c "import json; print(json.load(open('$CONFIG_PATH'))['modHost'])")
|
||||
PORT=$(python3 -c "import json; print(json.load(open('$CONFIG_PATH'))['modPort'])")
|
||||
TIMEOUT=$(python3 -c "import json; print(json.load(open('$CONFIG_PATH'))['timeout'])")
|
||||
|
||||
check_health() {
|
||||
curl -sf --max-time "$TIMEOUT" "http://$HOST:$PORT/health" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
echo "[ONI Agent] Checking Mod connection..."
|
||||
if check_health; then
|
||||
echo "[OK] Mod is running on $HOST:$PORT"
|
||||
exit 0
|
||||
else
|
||||
echo "[!] Cannot reach Mod at $HOST:$PORT"
|
||||
echo " Make sure Oxygen Not Included is running with the ONI Agent Bridge mod enabled."
|
||||
echo " Steps:"
|
||||
echo " 1. Launch Oxygen Not Included"
|
||||
echo " 2. Enable 'ONI Agent Bridge' mod in the Mod menu"
|
||||
echo " 3. Load a save or start a new game"
|
||||
echo " 4. Run this script again"
|
||||
exit 1
|
||||
fi
|
||||
61
scripts/build_mod.sh
Normal file
61
scripts/build_mod.sh
Normal file
@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the ONI Agent Bridge Mod
|
||||
# Requires: mono-complete (for mcs compiler)
|
||||
# Requires: ONI game installed to get reference DLLs
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
MOD_DIR="$PROJECT_DIR/mod"
|
||||
OUTPUT_DIR="$MOD_DIR/bin"
|
||||
|
||||
ONI_PATH="${ONI_PATH:-}"
|
||||
if [ -z "$ONI_PATH" ]; then
|
||||
for candidate in \
|
||||
"$HOME/.steam/steam/steamapps/common/OxygenNotIncluded" \
|
||||
"$HOME/.local/share/Steam/steamapps/common/OxygenNotIncluded" \
|
||||
"/c/Program Files (x86)/Steam/steamapps/common/OxygenNotIncluded" \
|
||||
"/mnt/c/Program Files (x86)/Steam/steamapps/common/OxygenNotIncluded"; do
|
||||
MANAGED="$candidate/OxygenNotIncluded_Data/Managed"
|
||||
if [ -d "$MANAGED" ]; then
|
||||
ONI_PATH="$MANAGED"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "$ONI_PATH" ]; then
|
||||
echo "[!] ONI installation not found."
|
||||
echo " Set ONI_PATH to the Managed directory:"
|
||||
echo ' export ONI_PATH="/path/to/OxygenNotIncluded_Data/Managed"'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[Build] ONI: $ONI_PATH"
|
||||
|
||||
for dll in "$ONI_PATH/Assembly-CSharp.dll" "$ONI_PATH/UnityEngine.dll" \
|
||||
"$ONI_PATH/UnityEngine.CoreModule.dll" "$ONI_PATH/0Harmony.dll"; do
|
||||
if [ ! -f "$dll" ]; then echo "[!] Missing: $dll"; exit 1; fi
|
||||
done
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
REFS=""
|
||||
for dll in "$ONI_PATH/Assembly-CSharp.dll" "$ONI_PATH/UnityEngine.dll" \
|
||||
"$ONI_PATH/UnityEngine.CoreModule.dll" "$ONI_PATH/0Harmony.dll"; do
|
||||
REFS="$REFS -reference:\"$dll\""
|
||||
done
|
||||
if [ -f "$ONI_PATH/Assembly-CSharp-firstpass.dll" ]; then
|
||||
REFS="$REFS -reference:\"$ONI_PATH/Assembly-CSharp-firstpass.dll\""
|
||||
fi
|
||||
|
||||
echo "[Build] Compiling..."
|
||||
eval mcs -target:library -out:"$OUTPUT_DIR/ONIAgentBridge.dll" $REFS -recurse:"$MOD_DIR/*.cs"
|
||||
|
||||
echo "[Build] SUCCESS: $OUTPUT_DIR/ONIAgentBridge.dll"
|
||||
echo ""
|
||||
echo "Install (Linux):"
|
||||
echo " MOD_DIR=~/.config/unity3d/Klei/OxygenNot\\ Included/mods/local/ONIAgentBridge"
|
||||
echo " mkdir -p \"\$MOD_DIR\""
|
||||
echo " cp $OUTPUT_DIR/ONIAgentBridge.dll $MOD_DIR/mod_info.yaml \"\$MOD_DIR/\""
|
||||
echo " # Then enable mod in ONI > Mods > ONI Agent Bridge"
|
||||
295
scripts/event_daemon.py
Normal file
295
scripts/event_daemon.py
Normal file
@ -0,0 +1,295 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ONI Agent Event Daemon
|
||||
======================
|
||||
Continuous event poller that feeds game events to the AI's input stream.
|
||||
|
||||
Architecture:
|
||||
Game Mod --> Event Queue (via HTTP) --> Event Daemon --> AI Input Stream
|
||||
|
||||
The daemon:
|
||||
1. Polls GET /api/state/events?since=<seq> every N seconds
|
||||
2. Classifies events by severity (critical/warning/info)
|
||||
3. For critical events: immediately triggers full analysis + prints alert
|
||||
4. For warning events: logs and optionally triggers targeted checks
|
||||
5. For info events: accumulates and reports periodically
|
||||
6. Maintains a compact event log for AI context
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import datetime
|
||||
|
||||
# Add tools to path
|
||||
TOOLS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'tools')
|
||||
sys.path.insert(0, TOOLS_DIR)
|
||||
|
||||
# Disable auto-pause for event daemon — we poll frequently and should not pause the game
|
||||
import oni_api
|
||||
oni_api.AUTO_PAUSE_ENABLED = False
|
||||
|
||||
from oni_api import api_get, api_post, api_url
|
||||
|
||||
|
||||
# ── Configuration ──────────────────────────────────────────────────────────
|
||||
|
||||
POLL_INTERVAL = 5 # seconds between event polls
|
||||
CRITICAL_POLL_INTERVAL = 2 # poll faster when critical events detected
|
||||
MAX_EVENT_HISTORY = 200 # events kept in rolling buffer
|
||||
CRITICAL_SEVERITIES = {'critical', 'duplicantdeath', 'buildingdamage', 'poweroutage'}
|
||||
WARNING_SEVERITIES = {'warning', 'duplicantstress', 'lowoxygen', 'foodshortage'}
|
||||
|
||||
|
||||
# ── Event History ──────────────────────────────────────────────────────────
|
||||
|
||||
class EventHistory:
|
||||
"""Rolling buffer of events + statistics for AI context."""
|
||||
|
||||
def __init__(self, maxlen=MAX_EVENT_HISTORY):
|
||||
self.events = []
|
||||
self.maxlen = maxlen
|
||||
self.stats = {
|
||||
'total': 0,
|
||||
'critical': 0,
|
||||
'warning': 0,
|
||||
'info': 0,
|
||||
'by_category': {},
|
||||
'by_type': {},
|
||||
'last_poll_cycle': 0,
|
||||
}
|
||||
|
||||
def push(self, events):
|
||||
for e in events:
|
||||
self.events.append(e)
|
||||
self.stats['total'] += 1
|
||||
sev = (e.get('severity') or 'info').lower()
|
||||
cat = e.get('category', 'unknown')
|
||||
etype = e.get('type', 'unknown')
|
||||
|
||||
if sev in ('critical', 'duplicantdeath', 'buildingdamage'):
|
||||
self.stats['critical'] += 1
|
||||
elif sev in ('warning',):
|
||||
self.stats['warning'] += 1
|
||||
else:
|
||||
self.stats['info'] += 1
|
||||
|
||||
self.stats['by_category'][cat] = self.stats['by_category'].get(cat, 0) + 1
|
||||
self.stats['by_type'][etype] = self.stats['by_type'].get(etype, 0) + 1
|
||||
|
||||
self.stats['last_poll_cycle'] = e.get('cycle', 0)
|
||||
|
||||
# Trim
|
||||
if len(self.events) > self.maxlen:
|
||||
self.events = self.events[-self.maxlen:]
|
||||
|
||||
def get_summary(self):
|
||||
return {
|
||||
'total_events': self.stats['total'],
|
||||
'critical_count': self.stats['critical'],
|
||||
'warning_count': self.stats['warning'],
|
||||
'info_count': self.stats['info'],
|
||||
'categories': dict(sorted(self.stats['by_category'].items(),
|
||||
key=lambda x: -x[1])[:10]),
|
||||
'last_cycle': self.stats['last_poll_cycle'],
|
||||
'recent_critical': [e for e in self.events[-20:]
|
||||
if (e.get('severity') or '').lower() in CRITICAL_SEVERITIES][-5:],
|
||||
}
|
||||
|
||||
|
||||
# ── Event Classifier ──────────────────────────────────────────────────────
|
||||
|
||||
def classify_event(e):
|
||||
"""Return the action type for a given event."""
|
||||
sev = (e.get('severity') or '').lower()
|
||||
title = (e.get('title') or '').lower()
|
||||
msg = (e.get('message') or '').lower()
|
||||
cat = (e.get('category') or '').lower()
|
||||
|
||||
if sev in CRITICAL_SEVERITIES:
|
||||
return 'critical'
|
||||
if sev in WARNING_SEVERITIES:
|
||||
return 'warning'
|
||||
if cat == 'action':
|
||||
return 'action_feedback'
|
||||
|
||||
# Content-based classification
|
||||
combined = title + ' ' + msg
|
||||
if any(w in combined for w in ['suffocat', 'choking', 'no oxygen', 'out of air']):
|
||||
return 'critical'
|
||||
if any(w in combined for w in ['starving', 'food', 'hungry']):
|
||||
return 'warning'
|
||||
if any(w in combined for w in ['heat', 'overheat', 'temperature', 'melt']):
|
||||
return 'warning'
|
||||
if any(w in combined for w in ['power', 'wattage', 'shutoff']):
|
||||
return 'warning'
|
||||
if any(w in combined for w in ['duplicant', 'stress', 'break']):
|
||||
return 'warning'
|
||||
if any(w in combined for w in ['research complete', 'research completed']):
|
||||
return 'info_research'
|
||||
if any(w in combined for w in ['printing pod']):
|
||||
return 'info_printing_pod'
|
||||
|
||||
return 'info'
|
||||
|
||||
|
||||
def format_event_for_ai(e):
|
||||
"""Format an event as a structured string for AI input."""
|
||||
ts = datetime.datetime.fromtimestamp(e.get('timestamp', time.time())).strftime('%H:%M:%S')
|
||||
cycle = e.get('cycle', '?')
|
||||
severity = e.get('severity', 'info').upper()
|
||||
title = e.get('title', '?')
|
||||
message = e.get('message', '')
|
||||
|
||||
lines = [f"[EVENT {severity}] Cycle {cycle} @ {ts}"]
|
||||
lines.append(f" Title: {title}")
|
||||
if message:
|
||||
lines.append(f" Message: {message}")
|
||||
entity = e.get('entity')
|
||||
if entity:
|
||||
lines.append(f" Entity: {entity}")
|
||||
cell = e.get('cell')
|
||||
if isinstance(cell, int) and cell >= 0:
|
||||
lines.append(f" Cell: {cell}")
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
# ── Polling Loop ──────────────────────────────────────────────────────────
|
||||
|
||||
def poll_loop(event_history):
|
||||
seq = 0
|
||||
consecutive_errors = 0
|
||||
|
||||
print("[ONI Event Daemon] Starting event poll...")
|
||||
print(f"[ONI Event Daemon] Poll interval: {POLL_INTERVAL}s")
|
||||
print()
|
||||
|
||||
while True:
|
||||
try:
|
||||
data = api_get(f"/api/state/events?since={seq}&limit=50")
|
||||
|
||||
if 'error' in data:
|
||||
consecutive_errors += 1
|
||||
if consecutive_errors == 1:
|
||||
print(f"[!] Cannot reach game: {data['error']}")
|
||||
print(" Waiting for game connection...")
|
||||
time.sleep(POLL_INTERVAL * 2)
|
||||
continue
|
||||
|
||||
consecutive_errors = 0
|
||||
events = data.get('events', [])
|
||||
next_seq = data.get('next_seq', seq)
|
||||
|
||||
if events:
|
||||
event_history.push(events)
|
||||
|
||||
# Classify and report
|
||||
critical_events = []
|
||||
for e in events:
|
||||
cls = classify_event(e)
|
||||
if cls == 'critical':
|
||||
critical_events.append(e)
|
||||
# Print alert with clear marker
|
||||
print("=" * 56)
|
||||
print(" *** CRITICAL EVENT ***")
|
||||
print(format_event_for_ai(e))
|
||||
print("=" * 56)
|
||||
print()
|
||||
|
||||
# Auto-trigger full analysis on critical events
|
||||
_trigger_emergency_analysis(e)
|
||||
elif cls == 'warning':
|
||||
print(format_event_for_ai(e))
|
||||
print()
|
||||
elif cls == 'info_research':
|
||||
# Research completed — show with unlocks
|
||||
print("=" * 40)
|
||||
print(format_event_for_ai(e))
|
||||
print(" -> Check what's new: python3 tools/oni_api.py buildable")
|
||||
print("=" * 40)
|
||||
print()
|
||||
elif cls == 'info_printing_pod':
|
||||
# Printing pod ready
|
||||
print("=" * 40)
|
||||
print(format_event_for_ai(e))
|
||||
print(" -> View options: python3 tools/oni_api.py printing_pod")
|
||||
print("=" * 40)
|
||||
print()
|
||||
else:
|
||||
# Only print non-info events or batch feedback
|
||||
cat = e.get('category', '')
|
||||
if cat != 'general' or cls != 'info':
|
||||
print(format_event_for_ai(e))
|
||||
print()
|
||||
|
||||
# If critical events happened, poll faster for a bit
|
||||
if critical_events:
|
||||
seq = next_seq
|
||||
time.sleep(CRITICAL_POLL_INTERVAL)
|
||||
continue
|
||||
|
||||
seq = next_seq
|
||||
time.sleep(POLL_INTERVAL)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n[ONI Event Daemon] Shutting down.")
|
||||
summary = event_history.get_summary()
|
||||
print(f" Total events seen: {summary['total_events']}")
|
||||
print(f" Critical: {summary['critical_count']}, Warning: {summary['warning_count']}")
|
||||
break
|
||||
except Exception as e:
|
||||
consecutive_errors += 1
|
||||
if consecutive_errors <= 2:
|
||||
print(f"[!] Poll error: {e}")
|
||||
time.sleep(POLL_INTERVAL)
|
||||
|
||||
|
||||
def _trigger_emergency_analysis(event):
|
||||
"""On critical events, pull game state snapshot for AI context."""
|
||||
try:
|
||||
print(" -> Triggering emergency snapshot...")
|
||||
game = api_get('/api/state/game')
|
||||
alerts = api_get('/api/state/alert')
|
||||
dups = api_get('/api/state/duplicants')
|
||||
|
||||
if 'error' not in game:
|
||||
print(f" [SNAPSHOT] Cycle {game.get('cycle', '?')}, "
|
||||
f"{game.get('duplicantCount', '?')} dupes, "
|
||||
f"{game.get('suffocating', 0)} suffocating, "
|
||||
f"{game.get('starving', 0)} starving, "
|
||||
f"{game.get('stressed', 0)} stressed")
|
||||
if isinstance(alerts, list) and alerts:
|
||||
print(f" [ALERTS] {len(alerts)} active:")
|
||||
for a in alerts[:3]:
|
||||
print(f" - [{a.get('severity', '?')}] {a.get('title', '?')}")
|
||||
print()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
history = EventHistory()
|
||||
try:
|
||||
poll_loop(history)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
# Print final summary
|
||||
summary = history.get_summary()
|
||||
print()
|
||||
print("=" * 56)
|
||||
print(" Event Daemon Session Summary")
|
||||
print("=" * 56)
|
||||
print(f" Total events: {summary['total_events']}")
|
||||
print(f" Critical: {summary['critical_count']}")
|
||||
print(f" Warning: {summary['warning_count']}")
|
||||
print(f" Info: {summary['info_count']}")
|
||||
print(f" Top categories: {', '.join(summary['categories'].keys())}")
|
||||
print("=" * 56)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
185
scripts/fix_csharp.py
Normal file
185
scripts/fix_csharp.py
Normal file
@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert C# 7/8 features in ONIAgentBridge.cs to C# 6 compatible code.
|
||||
|
||||
DEPRECATED: The mod source (ONIAgentBridge.cs) has been rewritten to use if-else chains.
|
||||
This script is kept for historical reference only and should not be run on the current source.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
with open('mod/ONIAgentBridge.cs', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# 1. Convert tuple switch at ProcessRequest to if-else chain
|
||||
# Find the switch and replace it
|
||||
old_switch_start = """ switch (path, method)
|
||||
{"""
|
||||
|
||||
# Read the entire switch block and reconstruct
|
||||
lines = content.split('\n')
|
||||
new_lines = []
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
# Find the C# 7 tuple switch
|
||||
if 'switch (path, method)' in line and '{' in lines[i+1]:
|
||||
new_lines.append(' // --- C# 6 compatible dispatch ---')
|
||||
new_lines.append(' bool _handled = false;')
|
||||
i += 2 # skip "switch (path, method)" and "{"
|
||||
|
||||
while i < len(lines):
|
||||
stripped = lines[i].strip()
|
||||
|
||||
# Check for case pattern: case ("...", "..."):
|
||||
m = re.match(r'case \("([^"]+)", "([^"]+)"\):', stripped)
|
||||
if m:
|
||||
endpoint = m.group(1)
|
||||
method = m.group(2)
|
||||
indent = lines[i][:len(lines[i]) - len(lines[i].lstrip())]
|
||||
new_lines.append(f'{indent}else if (path == "{endpoint}" && method == "{method}")')
|
||||
new_lines.append(f'{indent}{{')
|
||||
new_lines.append(f'{indent} _handled = true;')
|
||||
i += 1
|
||||
# Copy lines until "break;"
|
||||
while i < len(lines) and lines[i].strip() != 'break;':
|
||||
new_lines.append(lines[i])
|
||||
i += 1
|
||||
new_lines.append(f'{indent}}}')
|
||||
i += 1 # skip "break;"
|
||||
continue
|
||||
|
||||
# Check for default case
|
||||
if stripped == 'default:':
|
||||
i += 1
|
||||
# Skip the opening brace
|
||||
if lines[i].strip() == '{':
|
||||
i += 1
|
||||
new_lines.append(' if (!_handled)')
|
||||
new_lines.append(' {')
|
||||
while i < len(lines):
|
||||
if lines[i].strip() == '}' and not _is_end_of_method(lines, i):
|
||||
# This could be the switch closing brace
|
||||
# Check if next line is the method closing
|
||||
break
|
||||
new_lines.append(lines[i])
|
||||
i += 1
|
||||
new_lines.append(' }')
|
||||
# Skip the closing brace of the switch
|
||||
if lines[i].strip() == '}':
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Check for closing brace of switch
|
||||
if stripped == '}':
|
||||
i += 1
|
||||
break
|
||||
|
||||
# Any other content inside switch (shouldn't happen)
|
||||
new_lines.append(lines[i])
|
||||
i += 1
|
||||
continue
|
||||
|
||||
new_lines.append(line)
|
||||
i += 1
|
||||
|
||||
content = '\n'.join(new_lines)
|
||||
|
||||
def _is_end_of_method(lines, idx):
|
||||
"""Check if this } ends a method (next non-blank is a method or class member)."""
|
||||
for j in range(idx+1, min(idx+10, len(lines))):
|
||||
s = lines[j].strip()
|
||||
if s == '' or s.startswith('//') or s.startswith('/*'):
|
||||
continue
|
||||
if s.startswith('private ') or s.startswith('public ') or s.startswith('internal ') or s.startswith('static ') or s.startswith('}'):
|
||||
return True
|
||||
return False
|
||||
return False
|
||||
|
||||
# 2. Fix switch expressions (C# 8): `wireType switch { ... }` -> if-else
|
||||
# Pattern: string buildingId = wireType switch { "heavy" => "HeaviWatWire", ... }
|
||||
def fix_switch_expr(match):
|
||||
full = match.group(0)
|
||||
var_name = match.group(1)
|
||||
# Parse the switch arms
|
||||
# Extract the expression after "= "
|
||||
body = match.group(2)
|
||||
return full # placeholder, we'll handle below
|
||||
|
||||
# Fix: string buildingId = wireType switch { "heavy" => "HeaviWatWire", _ => "Wire" };
|
||||
# Convert to: string buildingId; if (wireType == "heavy") buildingId = "HeaviWatWire"; else ...
|
||||
for pattern in [
|
||||
(r'wireType switch\s*\{([^}]+)\}', 'wireType'),
|
||||
(r'error switch\s*\{([^}]+)\}', 'error'),
|
||||
(r'i switch\s*\{([^}]+)\}', 'i'),
|
||||
]:
|
||||
pat, varname = pattern
|
||||
matches = list(re.finditer(pat, content))
|
||||
for m in reversed(matches):
|
||||
body = m.group(1)
|
||||
arms = re.findall(r'"([^"]*)"\s*=>\s*"([^"]*)"', body)
|
||||
default = re.findall(r'_\s*=>\s*(null|"[^"]*")', body)
|
||||
|
||||
if varname == 'i':
|
||||
# label assignment
|
||||
replacements = []
|
||||
for key, val in arms:
|
||||
replacements.append(f' if ({varname} == {key}) label = "{val}";')
|
||||
if default:
|
||||
replacements.append(f' else label = {default[0]};')
|
||||
new_code = '\n'.join(replacements)
|
||||
else:
|
||||
# Find the variable being assigned
|
||||
start = m.start()
|
||||
# Look backwards to find the variable name
|
||||
line_start = content.rfind('\n', 0, start) + 1
|
||||
line_before = content[line_start:start]
|
||||
var_match = re.match(r'\s*(?:\w+\s+)?(\w+)\s*=\s*$', line_before)
|
||||
|
||||
indent = ' ' * 16
|
||||
if varname == 'wireType':
|
||||
# Multiple instances in wire building, find each by context
|
||||
context_before = content[max(0,start-200):start]
|
||||
if 'bridgeId' in context_before:
|
||||
varname_full = 'bridgeId'
|
||||
else:
|
||||
varname_full = 'buildingId'
|
||||
else:
|
||||
varname_full = varname
|
||||
|
||||
lines_code = [f' string {varname_full};']
|
||||
first = True
|
||||
for key, val in arms:
|
||||
prefix = 'if' if first else 'else if'
|
||||
lines_code.append(f' {prefix} ({varname} == "{key}") {varname_full} = "{val}";')
|
||||
first = False
|
||||
if default:
|
||||
dval = default[0]
|
||||
lines_code.append(f' else {varname_full} = {dval};')
|
||||
|
||||
new_code = '\n'.join(lines_code)
|
||||
|
||||
# Replace the assignment line + switch expression
|
||||
# Find the full assignment
|
||||
assign_start = content.rfind('\n', 0, start)
|
||||
assign_end = m.end()
|
||||
while assign_end < len(content) and content[assign_end] != '\n':
|
||||
assign_end += 1
|
||||
old = content[assign_start:assign_end]
|
||||
content = content[:assign_start] + '\n' + new_code + content[assign_end:]
|
||||
|
||||
# Write result
|
||||
with open('mod/ONIAgentBridge.cs', 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print('Done. Checking for remaining C# 7+ features...')
|
||||
|
||||
# Verify
|
||||
remaining = re.findall(r'\bswitch\b', content)
|
||||
print(f'Remaining "switch" keywords: {len(remaining)}')
|
||||
# Count them
|
||||
for i, line in enumerate(content.split('\n')):
|
||||
if 'switch' in line and not line.strip().startswith('//') and 'ProcessRequest' not in line:
|
||||
sline = line.strip()
|
||||
if 'if (' not in line and 'dictionary' not in line.lower() and 'dispatch' not in line:
|
||||
print(f' Line ~{i}: {sline[:80]}')
|
||||
28
scripts/setup.sh
Normal file
28
scripts/setup.sh
Normal file
@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# ONI Agent - Project Setup Script
|
||||
# Ensures Python dependencies and directory structure.
|
||||
set -euo pipefail
|
||||
|
||||
echo "[ONI Agent] Setting up project..."
|
||||
|
||||
# Verify Python3
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "[!] Python3 is required but not found."
|
||||
exit 1
|
||||
fi
|
||||
echo "[OK] Python3 found: $(python3 --version)"
|
||||
|
||||
# Verify Python tools are syntactically valid
|
||||
TOOLS_DIR="$(dirname "$0")/../tools"
|
||||
for f in "$TOOLS_DIR"/*.py; do
|
||||
python3 -m py_compile "$f" 2>/dev/null && echo "[OK] $f" || echo "[!] Syntax error in $f"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "[OK] Setup complete."
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Launch Oxygen Not Included with the ONI Agent Bridge mod"
|
||||
echo " 2. Run: python3 tools/oni_api.py health"
|
||||
echo " 3. Run: python3 tools/oni_api.py status"
|
||||
echo " 4. Run: python3 tools/oni_analyzer.py"
|
||||
26
scripts/watch.sh
Normal file
26
scripts/watch.sh
Normal file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# ONI Agent - Watch Mode
|
||||
# Continuously polls game state and runs analysis on a timer.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
TOOLS_DIR="$SCRIPT_DIR/../tools"
|
||||
INTERVAL=${1:-60}
|
||||
|
||||
if ! [[ "$INTERVAL" =~ ^[0-9]+$ ]] || [ "$INTERVAL" -lt 10 ]; then
|
||||
echo "Usage: watch.sh [interval_seconds] (minimum 10)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[ONI Agent] Watch mode started (interval: ${INTERVAL}s)"
|
||||
echo "Press Ctrl+C to stop."
|
||||
echo ""
|
||||
|
||||
while true; do
|
||||
clear 2>/dev/null || true
|
||||
output=$(python3 "$TOOLS_DIR/oni_analyzer.py" 2>&1)
|
||||
echo "$output"
|
||||
echo ""
|
||||
echo "[Next update in ${INTERVAL}s...]"
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
466
skills/oni_agent.md
Normal file
466
skills/oni_agent.md
Normal file
@ -0,0 +1,466 @@
|
||||
# ONI Agent Skill
|
||||
|
||||
AI 操作《缺氧》(Oxygen Not Included) 的完整技能定义。
|
||||
|
||||
## 触发条件
|
||||
- 用户提到《缺氧》/ Oxygen Not Included / ONI
|
||||
- 用户询问游戏策略、建造方案、资源管理
|
||||
- 用户希望 Mod 工具链执行操作
|
||||
- 用户需要实时游戏操作协助
|
||||
|
||||
## 游戏核心玩法
|
||||
|
||||
缺氧是一款太空殖民模拟游戏。核心目标是在恶劣环境中维持复制人的生存并建设可持续基地。
|
||||
|
||||
### 生存要素优先级
|
||||
|
||||
| 优先级 | 要素 | 危险条件 | 后果 | 解决方案路线 |
|
||||
|--------|------|---------|------|-------------|
|
||||
| ★★★ | 氧气 | O₂ < 500 kg 或无制氧设备 | 复制人 110s 内窒息死亡 | 藻类制氧(前期) → 电解器+氢气发电(SPOM) |
|
||||
| ★★★ | 食物 | Calories < 1000 kcal/人 | 复制人饿死 | 浆果种植 → 烤肉 → 营养膏棒 |
|
||||
| ★★☆ | 温度 | > 40°C 或 < 10°C | 作物枯萎/复制人受伤 | 隔热/液冷+蒸汽机/加热 |
|
||||
| ★★☆ | 电力 | 电量不足/电路过载 | 设备停摆/电线起火 | 手动→煤→氢→天然气→太阳能 |
|
||||
| ★☆☆ | 士气 | Stress > 80% | 精神崩溃/破坏设备 | 房间奖励/装饰/按摩/高品质食物 |
|
||||
| ★☆☆ | 病菌 | Slimelung/Food Poisoning | 生病死亡 | 洗手/净化/消毒/氧气服 |
|
||||
|
||||
### 游戏世界规则
|
||||
|
||||
- 二维网格地图,原点 (0,0) 在左下角,x 向右 y 向上
|
||||
- 气体按密度分层:CO₂(沉底) < PollutedO₂ < O₂ < NaturalGas < H₂(上升)
|
||||
- 建筑占用 w×h 格子,坐标指左下角锚点
|
||||
- 科技通过研究站→超级计算机逐步解锁新建筑
|
||||
- 每 3 周期传送舱提供一次新复制人或补给选择
|
||||
|
||||
---
|
||||
|
||||
## API 总览(92 个端点)
|
||||
|
||||
工具链让 AI 能像人类玩家一样理解游戏并执行操作。所有 API 通过 `tools/oni_api.py` 命令行调用。
|
||||
|
||||
### 一、状态查询 — 理解"当前发生了什么"
|
||||
|
||||
每次 AI 推理前工具自动暂停游戏,保证数据时效性。AI 操作完成后主动 unpause。
|
||||
|
||||
```bash
|
||||
# 全局总览 — 最常用的命令
|
||||
python3 tools/oni_api.py status
|
||||
# 输出:周期、窒息人数、饥饿人数、压力人数、是否暂停、游戏速度
|
||||
|
||||
# 全面诊断
|
||||
python3 tools/oni_commander.py diagnose
|
||||
# 输出:电力+CO₂+温度+疾病+管道的完整报告
|
||||
```
|
||||
|
||||
**各状态查询命令的用途:**
|
||||
|
||||
| 命令 | 等价于玩家做什么 | AI 用它做什么 |
|
||||
|------|-----------------|-------------|
|
||||
| `status` | 看一眼基地总览面板 | 快速评估危机:有人在窒息吗?在挨饿吗? |
|
||||
| `resources` | 打开资源面板 | 检查氧气储量够多久、煤还剩多少 |
|
||||
| `duplicants` | 逐一查看每个复制人 | 谁在窒息?谁压力爆了?谁没事干? |
|
||||
| `buildings` | 按分类查看建筑 | 数一数有多少发电机、电解器、种植箱 |
|
||||
| `power` | 打开电力覆盖层 | 哪个电路过载了?负载多少? |
|
||||
| `co2` | 看 CO₂ 在哪聚集 | CO₂ 是前中期最大杀手,找到它并挖排气道 |
|
||||
| `temp_zones` | 看温度覆盖层 | 找到过热区域(>50°C)和过冷区域(<5°C) |
|
||||
| `pipes gas/liquid` | 点开管道看看 | 管道堵塞了吗?内容物在流动吗? |
|
||||
| `rooms` | 打开房间覆盖层 | 确认宿舍/卫生间/餐厅生效了吗 |
|
||||
| `morale` | 查看士气面板 | 士气够不够支撑当前技能数 |
|
||||
| `diseases` | 打开病菌覆盖层 | 谁被感染了?环境病菌多吗? |
|
||||
| `storage` | 查看储物建筑 | 储备粮在哪、存了多少 |
|
||||
| `storages` | 同上 | 同 storage |
|
||||
| `resources` | 资源面板 | 所有物资的精确余量 |
|
||||
| `skills` | 技能面板 | 每个复制人的属性值和已学技能 |
|
||||
| `research` | 科技树 | 哪些科技已完成、哪些可研究 |
|
||||
| `research_detail` | 点开研究站 | 研究站在工作吗?现在在研究什么? |
|
||||
| `buildable` | 建造菜单 | 当前科技解锁了哪些建筑 |
|
||||
| `geysers` | 查看喷泉 | 找到水源/天然气/火山位置 |
|
||||
| `critters` | 查看生物 | 哈奇/飞鱼/滑鳞的数量和位置 |
|
||||
| `plants` | 查看植物 | 作物生长进度、是否枯萎 |
|
||||
|
||||
### 二、格子地图 — 理解"每个格子有什么"
|
||||
|
||||
所有建造操作前必须先用格子查询确认位置。
|
||||
|
||||
```bash
|
||||
# 查看单个格子(最常用)
|
||||
python3 tools/oni_api.py cell 45 48
|
||||
# 输出:元素、温度、有无建筑、有无复制人、是否可挖掘、是否安全
|
||||
|
||||
# 查看 10x10 区域
|
||||
python3 tools/oni_api.py cells 40 40 10 10
|
||||
# 输出:区域内所有格子的地图 + AI 友好摘要
|
||||
|
||||
# 扫描某一行/列
|
||||
python3 tools/oni_api.py slice y 20 0 50
|
||||
# 输出:第 20 行从 0 到 50 的每个格子
|
||||
|
||||
# 气体分析
|
||||
python3 tools/oni_api.py gas 50 50 30
|
||||
# 输出:以 50,50 为中心半径 30 内的气体分布
|
||||
|
||||
# AI 友好区域探索
|
||||
python3 tools/oni_api.py explore 40 40 20 20
|
||||
# 输出:区域内建筑列表+复制人+元素摘要+感兴趣格子
|
||||
```
|
||||
|
||||
**AI 坐标定位方法:**
|
||||
|
||||
```
|
||||
方法 1: buildings → 查已有建筑坐标 → 在附近偏移找空地
|
||||
方法 2: explore <x> <y> <w> <h> → 找 buildings_in_region 为空的区域
|
||||
方法 3: cell <x> <y> → 确认 isSolid=false + hasBuilding=false + isVisible=true
|
||||
方法 4: co2 → 在 CO₂ 聚集点下方挖排气通道
|
||||
方法 5: gas → 在气体聚集区放气体泵
|
||||
```
|
||||
|
||||
### 三、实体注册表 — "这个建筑是什么"
|
||||
|
||||
AI 在运行时查询任何不懂的 ID:
|
||||
|
||||
```bash
|
||||
# 查建筑
|
||||
python3 tools/oni_api.py registry buildings Electrolyzer
|
||||
# 输出:名称、分类、尺寸(2x2)、功耗(120W)、所需材料
|
||||
|
||||
# 查元素
|
||||
python3 tools/oni_api.py registry elements Water
|
||||
# 输出:比热容、导热系数、沸点(99°C)、物态
|
||||
|
||||
# 查科技
|
||||
python3 tools/oni_api.py registry techs ImprovedOxygen
|
||||
# 输出:前置科技、解锁建筑(Electrolyzer)
|
||||
|
||||
# 查优先级含义
|
||||
python3 tools/oni_api.py registry priorities
|
||||
# 输出:1=最低 5=默认 9=紧急
|
||||
```
|
||||
|
||||
### 四、建造操作 — "建造世界"
|
||||
|
||||
建造 = 挖掘(dig) → 放置(build)。坐标是建筑左下角。
|
||||
|
||||
```bash
|
||||
# 挖掘区域
|
||||
python3 tools/oni_api.py dig 40 40 10 10
|
||||
# 在 (40,40) 处挖一个 10x10 的空间
|
||||
|
||||
# 放置建筑
|
||||
python3 tools/oni_api.py build Electrolyzer 45 42
|
||||
# 在 (45,42) 放电解器(左下角定位,占 2x2 格)
|
||||
|
||||
# 铺设管路
|
||||
python3 tools/oni_api.py build_pipe_line liquid 42 40 48 40 line
|
||||
# 从 (42,40) 到 (48,40) 铺液体管道
|
||||
# mode=line: 与已有管线合并
|
||||
# mode=cross: 交叉处自动用跨接器跳过
|
||||
# mode=single: 单段
|
||||
|
||||
# 铺设电线
|
||||
python3 tools/oni_api.py build_wire_line heavy 30 20 45 20 cross
|
||||
# 从 (30,20) 到 (45,20) 铺重载电线
|
||||
# 交叉处用跨接器不连通
|
||||
|
||||
# 拆除
|
||||
python3 tools/oni_api.py deconstruct Tile 42 45
|
||||
# 拆除 (42,45) 处的 Tile
|
||||
|
||||
# 旋转
|
||||
python3 tools/oni_api.py rotate 45 48
|
||||
# 旋转 (45,48) 处的建筑
|
||||
|
||||
# 清碎片
|
||||
python3 tools/oni_api.py clear 40 40 5
|
||||
# 清除 (40,40) 半径 5 内的碎片
|
||||
|
||||
# 复制设置
|
||||
python3 tools/oni_api.py copy_settings 45 48 50 48
|
||||
# 把 (45,48) 建筑的设置复制到 (50,48)
|
||||
```
|
||||
|
||||
### 五、建筑操作 — "使用已建成的建筑"
|
||||
|
||||
相当于玩家点击建筑后选择功能。
|
||||
|
||||
```bash
|
||||
# 开关
|
||||
python3 tools/oni_api.py toggle 45 42
|
||||
# 关闭/打开电解器(省电或恢复运行)
|
||||
|
||||
# 设优先级
|
||||
python3 tools/oni_api.py set_building_priority 45 42 9
|
||||
# 电解器优先级设为 9(紧急)
|
||||
|
||||
# 设配方
|
||||
python3 tools/oni_api.py set_recipe 30 25 Crush
|
||||
# 碎岩机设为粉碎模式
|
||||
|
||||
# 开关自动化
|
||||
python3 tools/oni_api.py set_automation 45 42 on
|
||||
# 电解器开启自动化控制
|
||||
|
||||
# 清空储物
|
||||
python3 tools/oni_api.py empty 50 50
|
||||
# 清空 (50,50) 储物箱全部内容
|
||||
|
||||
# 设储物过滤
|
||||
python3 tools/oni_api.py storage_filter 50 50 Coal
|
||||
# 储物箱只收煤炭
|
||||
|
||||
# 设冰箱温度
|
||||
python3 tools/oni_api.py fridge_temp 60 30 2
|
||||
# 冰箱设为 2°C
|
||||
|
||||
# 设电池充放电阈值
|
||||
python3 tools/oni_api.py battery_charge 35 25 90 20
|
||||
# 智能电池 90% 停充、20% 启动
|
||||
|
||||
# 设阀门流量
|
||||
python3 tools/oni_api.py valve_flow 42 42 1000
|
||||
# 阀门限流 1000g/s
|
||||
|
||||
# 设排气口压力
|
||||
python3 tools/oni_api.py vent_pressure 45 50 2000
|
||||
# 高压排气口设为 2000g
|
||||
|
||||
# 设孵化器
|
||||
python3 tools/oni_api.py incubator_setting 50 40 HatchEgg
|
||||
# 孵化器优先孵哈奇蛋
|
||||
|
||||
# 设传感器阈值
|
||||
python3 tools/oni_api.py sensor_threshold 70 30 500
|
||||
# 气压传感器阈值 500g
|
||||
|
||||
# 消毒
|
||||
python3 tools/oni_api.py disinfect 42 42
|
||||
# 对 (42,42) 消毒
|
||||
|
||||
# 清扫
|
||||
python3 tools/oni_api.py sweep 40 40 10
|
||||
# 标记 (40,40) 半径 10 内的物品为待清扫
|
||||
|
||||
# 取消任务
|
||||
python3 tools/oni_api.py cancel_errand 45 42
|
||||
# 取消电解器处的排队任务
|
||||
```
|
||||
|
||||
### 六、门控制
|
||||
|
||||
```bash
|
||||
# 锁门
|
||||
python3 tools/oni_api.py door_lock 42 45 on
|
||||
# 锁上 (42,45) 的门,复制人不能通过
|
||||
|
||||
# 单向通行
|
||||
python3 tools/oni_api.py door_one_way 42 45 left
|
||||
# 门只允许向左通过
|
||||
|
||||
# 手动开门/关门
|
||||
python3 tools/oni_api.py door_open 42 45 off
|
||||
# 手动关闭 (42,45) 的门
|
||||
```
|
||||
|
||||
### 七、复制人管理
|
||||
|
||||
```bash
|
||||
# 移动复制人
|
||||
python3 tools/oni_api.py dupe_move Dup1 30 25
|
||||
# 命令 Dup1 移动到 (30,25)
|
||||
|
||||
# 取消任务
|
||||
python3 tools/oni_api.py dupe_cancel_task Dup1
|
||||
# 命令 Dup1 取消当前正在做的事
|
||||
|
||||
# 分配工作
|
||||
python3 tools/oni_api.py assign_job Dup1 Dig
|
||||
# 让 Dup1 专注挖掘工作
|
||||
|
||||
# 个人优先级
|
||||
python3 tools/oni_api.py dupe_personal_priority Dup1 Dig 9
|
||||
# Dup1 的挖掘优先级设为 9(最高)
|
||||
```
|
||||
|
||||
### 八、科研管理
|
||||
|
||||
```bash
|
||||
# 查看研究状态
|
||||
python3 tools/oni_api.py research_detail
|
||||
# 输出:研究站是否工作、当前研究什么、超级计算机是否造好
|
||||
|
||||
# 选择研究
|
||||
python3 tools/oni_api.py research_select ImprovedOxygen
|
||||
# 开始研究 ImprovedOxygen
|
||||
|
||||
# 取消研究
|
||||
python3 tools/oni_api.py research_cancel
|
||||
# 取消全部当前研究
|
||||
```
|
||||
|
||||
### 九、生物/植物管理
|
||||
|
||||
```bash
|
||||
# 攻击生物
|
||||
python3 tools/oni_api.py critter_attack 120 80
|
||||
# 标记 (120,80) 的生物为攻击目标
|
||||
|
||||
# 抓捕生物
|
||||
python3 tools/oni_api.py critter_wrangle 120 80
|
||||
# 抓捕 (120,80) 的生物运到存放点
|
||||
|
||||
# 收获植物
|
||||
python3 tools/oni_api.py harvest 50 45
|
||||
# 收获 (50,45) 的成熟作物
|
||||
|
||||
# 拔除植物
|
||||
python3 tools/oni_api.py plant_uproot 50 45
|
||||
# 拔掉 (50,45) 的植物
|
||||
```
|
||||
|
||||
### 十、游戏控制
|
||||
|
||||
```bash
|
||||
# 暂停/恢复
|
||||
python3 tools/oni_api.py pause "Building SPOM"
|
||||
python3 tools/oni_api.py unpause 1
|
||||
|
||||
# 速度
|
||||
python3 tools/oni_api.py speed 3
|
||||
|
||||
# 存档回滚
|
||||
python3 tools/oni_api.py save "before_experiment"
|
||||
python3 tools/oni_api.py saves
|
||||
python3 tools/oni_api.py load "before_experiment"
|
||||
|
||||
# 截图视角
|
||||
python3 tools/oni_api.py snapshot /tmp/now.png
|
||||
python3 tools/oni_api.py camera 120 80 20
|
||||
|
||||
# 覆盖层切换
|
||||
python3 tools/oni_api.py overlay power
|
||||
python3 tools/oni_api.py overlay temp
|
||||
python3 tools/oni_api.py overlay gas
|
||||
python3 tools/oni_api.py overlay rooms
|
||||
|
||||
# 传送舱
|
||||
python3 tools/oni_api.py printing_pod
|
||||
python3 tools/oni_api.py printing_pod_select 0
|
||||
```
|
||||
|
||||
### 十一、批量操作
|
||||
|
||||
```bash
|
||||
python3 tools/oni_api.py batch docs/batch_example.json
|
||||
# 一次执行多个动作,逐个报告结果
|
||||
```
|
||||
|
||||
### 十二、高级指令
|
||||
|
||||
```bash
|
||||
# 全面诊断
|
||||
python3 tools/oni_commander.py diagnose
|
||||
|
||||
# 自动处理 CO₂
|
||||
python3 tools/oni_commander.py fix_co2
|
||||
|
||||
# 处理过载电路
|
||||
python3 tools/oni_commander.py fix_overload
|
||||
|
||||
# 紧急制氧
|
||||
python3 tools/oni_commander.py emergency_o2
|
||||
|
||||
# 一键拓展房间
|
||||
python3 tools/oni_commander.py expand_base 40 40 10 8
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见场景查询路线
|
||||
|
||||
### "复制人要窒息了"
|
||||
```
|
||||
status → 看 suffocating 计数
|
||||
duplicants → 找 oxygen < 20 的复制人
|
||||
cell <窒息复制人坐标> → 所在格子的元素
|
||||
co2 → 如果是 CO₂ 窒息,fix_co2 挖排气道
|
||||
resources → O₂ 存量和 Algae 存量
|
||||
buildings → 过滤 Oxygen 分类看有无制氧设备
|
||||
无设备 → emergency_o2 或 build OxygenDiffuser
|
||||
```
|
||||
|
||||
### "基地没电了"
|
||||
```
|
||||
power → 看各电路负载和过载
|
||||
resources → Coal/Hydrogen/NaturalGas 存量
|
||||
buildings → Power 分类有哪些发电机
|
||||
过载 → fix_overload
|
||||
缺电 → 根据资源选发电机类型
|
||||
```
|
||||
|
||||
### "研究卡住了"
|
||||
```
|
||||
research_detail → 研究站通不通电、有没有复制人在研究
|
||||
building_detail <研究站坐标> → 供上电了吗
|
||||
resources → Dirt 够吗(研究站消耗)
|
||||
需超级计算机 → build SuperComputer <坐标>
|
||||
```
|
||||
|
||||
### "管道不走了"
|
||||
```
|
||||
pipes liquid → 看管道内容
|
||||
building_detail <水泵> → 泵有没有电
|
||||
building_detail <目的地建筑> → 输入是否已满
|
||||
build_pipe_line liquid <起点> <终点> cross → 重建
|
||||
```
|
||||
|
||||
### "传送舱来了"
|
||||
```
|
||||
event_daemon 显示 [EVENT] Printing Pod ready
|
||||
printing_pod → 看选项
|
||||
printing_pod_select 0|1|2 → 选择
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AI 操作铁律
|
||||
|
||||
```
|
||||
铁律 1: AI 开始推理/决策前 → 工具自动暂停游戏
|
||||
铁律 2: AI 需要问用户时 → 工具自动暂停
|
||||
铁律 3: AI 退出回答时 → 确保游戏已暂停(无人监控时不能运行)
|
||||
铁律 4: 写操作前 → 工具自动确保暂停态
|
||||
铁律 5: 重大操作前 → save 存档,失败可 load 回滚
|
||||
铁律 6: 每次操作后检查反馈 → 读 error + errorMessage + suggestion
|
||||
```
|
||||
|
||||
## 工具列表
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| `tools/oni_api.py` | 92 个 API 端点的 CLI 客户端(所有查询/操作) |
|
||||
| `tools/oni_commander.py` | 高级指令(diagnose/fix_co2/expand_base 等) |
|
||||
| `tools/oni_analyzer.py` | 自动分析+6 维度预警建议 |
|
||||
| `tools/oni_builder.py` | 预置 SPOM/农场/养殖等蓝图 |
|
||||
| `scripts/event_daemon.py` | 事件守护进程,持续轮询推送游戏事件到 AI |
|
||||
|
||||
## 常用操作速查
|
||||
|
||||
```bash
|
||||
# 每分钟检查
|
||||
python3 scripts/event_daemon.py
|
||||
|
||||
# 紧急三步
|
||||
python3 tools/oni_api.py pause "Emergency"
|
||||
python3 tools/oni_commander.py diagnose
|
||||
python3 tools/oni_api.py status
|
||||
# ...操作...
|
||||
python3 tools/oni_api.py unpause 1
|
||||
|
||||
# 存档保护
|
||||
python3 tools/oni_api.py save "before_操作"
|
||||
# ...操作...
|
||||
# 如果搞砸了:
|
||||
python3 tools/oni_api.py load "before_操作"
|
||||
|
||||
# 截图确认
|
||||
python3 tools/oni_api.py camera 50 50 20
|
||||
python3 tools/oni_api.py snapshot
|
||||
```
|
||||
281
tools/oni_analyzer.py
Normal file
281
tools/oni_analyzer.py
Normal file
@ -0,0 +1,281 @@
|
||||
import json
|
||||
import sys
|
||||
from oni_api import api_get
|
||||
|
||||
|
||||
def get_game_state():
|
||||
return {
|
||||
'game': api_get('/api/state/game'),
|
||||
'resources': api_get('/api/state/resources'),
|
||||
'duplicants': api_get('/api/state/duplicants'),
|
||||
'buildings': api_get('/api/state/buildings'),
|
||||
'research': api_get('/api/state/research'),
|
||||
'geysers': api_get('/api/state/geysers'),
|
||||
'alerts': api_get('/api/state/alert'),
|
||||
'critters': api_get('/api/state/critters'),
|
||||
'plants': api_get('/api/state/plants'),
|
||||
'rooms': api_get('/api/state/rooms'),
|
||||
}
|
||||
|
||||
|
||||
def as_dict(resources):
|
||||
if not isinstance(resources, list):
|
||||
return {}
|
||||
return {r.get('name'): r for r in resources}
|
||||
|
||||
|
||||
def buildings_by_cat(buildings):
|
||||
cats = {}
|
||||
for b in (buildings or []):
|
||||
cat = b.get('category', 'Other')
|
||||
cats.setdefault(cat, []).append(b)
|
||||
return cats
|
||||
|
||||
|
||||
def analyze_o2(resources, buildings):
|
||||
r = as_dict(resources)
|
||||
o2 = r.get('Oxygen', {}).get('amount', 0)
|
||||
algae = r.get('Algae', {}).get('amount', 0)
|
||||
pw = r.get('PollutedWater', {}).get('amount', 0)
|
||||
|
||||
has_electrolyzer = any(b.get('id') == 'Electrolyzer' for b in (buildings or []))
|
||||
has_diffuser = any(b.get('id') == 'OxygenDiffuser' for b in (buildings or []))
|
||||
|
||||
warnings = []
|
||||
if o2 < 100:
|
||||
warnings.append(("CRITICAL", f"Oxygen critically low ({o2:.0f} kg)!"))
|
||||
elif o2 < 500:
|
||||
warnings.append(("WARN", f"Oxygen declining ({o2:.0f} kg)"))
|
||||
|
||||
if o2 < 1000 and not has_electrolyzer and not has_diffuser:
|
||||
warnings.append(("CRITICAL", "No oxygen production buildings found! Build OxygenDiffuser or Electrolyzer"))
|
||||
|
||||
if algae < 1000:
|
||||
warnings.append(("WARN", f"Algae running out ({algae:.0f} kg) — build electrolyzer"))
|
||||
elif algae < 5000 and not has_electrolyzer:
|
||||
warnings.append(("INFO", f"Algae moderate ({algae:.0f} kg) — plan SPOM transition"))
|
||||
|
||||
if pw > 50000:
|
||||
warnings.append(("INFO", f"Polluted water abundant ({pw:.0f} kg) — use for reed fiber / pincha pepper"))
|
||||
|
||||
return warnings
|
||||
|
||||
|
||||
def analyze_food(resources, buildings):
|
||||
r = as_dict(resources)
|
||||
cal = r.get('Calories', {}).get('amount', 0)
|
||||
|
||||
has_farm = any(b.get('id') in ('PlanterBox', 'FarmTile') for b in (buildings or []))
|
||||
has_grill = any(b.get('id') == 'ElectricGrill' for b in (buildings or []))
|
||||
|
||||
warnings = []
|
||||
if cal < 100000:
|
||||
warnings.append(("CRITICAL", f"Food shortage ({cal:.0f} kcal)!"))
|
||||
elif cal < 500000:
|
||||
warnings.append(("WARN", f"Food declining ({cal:.0f} kcal)"))
|
||||
elif cal > 2000000:
|
||||
warnings.append(("INFO", f"Food surplus ({cal:.0f} kcal)"))
|
||||
|
||||
if cal < 500000 and not has_farm:
|
||||
warnings.append(("WARN", "No farm plots found. Build PlanterBox and plant Mealwood"))
|
||||
if cal < 500000 and not has_grill:
|
||||
warnings.append(("INFO", "Build ElectricGrill to improve food quality"))
|
||||
return warnings
|
||||
|
||||
|
||||
def analyze_power(resources, buildings):
|
||||
r = as_dict(resources)
|
||||
coal = r.get('Coal', {}).get('amount', 0)
|
||||
hydrogen = r.get('Hydrogen', {}).get('amount', 0)
|
||||
natgas = r.get('NaturalGas', {}).get('amount', 0)
|
||||
|
||||
generators = [b for b in (buildings or []) if b.get('id') in (
|
||||
'CoalGenerator', 'HydrogenGenerator', 'NaturalGasGenerator',
|
||||
'ManualGenerator', 'PetroleumGenerator', 'WoodBurner'
|
||||
)]
|
||||
batteries = [b for b in (buildings or []) if b.get('id') in ('Battery', 'JumboBattery', 'SmartBattery')]
|
||||
|
||||
warnings = []
|
||||
if not generators:
|
||||
warnings.append(("CRITICAL", "No power generators found! Build ManualGenerator or CoalGenerator"))
|
||||
else:
|
||||
powered_on = sum(1 for g in generators if g.get('isOperational'))
|
||||
warnings.append(("INFO", f"Power: {len(generators)} generators ({powered_on} operational), {len(batteries)} batteries"))
|
||||
|
||||
if coal < 5000 and any(g.get('id') == 'CoalGenerator' for g in generators):
|
||||
warnings.append(("WARN", f"Coal low ({coal:.0f} kg) — diversify power"))
|
||||
if hydrogen > 20000:
|
||||
warnings.append(("INFO", f"Hydrogen stockpiled ({hydrogen:.0f} kg) — add generators"))
|
||||
if natgas > 10000:
|
||||
warnings.append(("INFO", f"Natural gas abundant ({natgas:.0f} kg)"))
|
||||
|
||||
return warnings
|
||||
|
||||
|
||||
def analyze_temp():
|
||||
from oni_api import api_get
|
||||
data = api_get('/api/state/temperature/zones', auto_pause=False)
|
||||
warnings = []
|
||||
if 'error' in data:
|
||||
return warnings
|
||||
avg = data.get('averageC', 0)
|
||||
if avg > 50:
|
||||
warnings.append(("CRITICAL", f"Overheating ({avg:.0f}°C)"))
|
||||
elif avg > 35:
|
||||
warnings.append(("WARN", f"High temperature ({avg:.0f}°C)"))
|
||||
elif avg < -5:
|
||||
warnings.append(("WARN", f"Too cold ({avg:.0f}°C)"))
|
||||
if data.get('hotSpots'):
|
||||
warnings.append(("WARN", f"{len(data['hotSpots'])} hot spots (>50°C)"))
|
||||
if data.get('coldSpots'):
|
||||
warnings.append(("WARN", f"{len(data['coldSpots'])} cold spots (<5°C)"))
|
||||
return warnings
|
||||
|
||||
|
||||
def analyze_water(resources):
|
||||
r = as_dict(resources)
|
||||
water = r.get('Water', {}).get('amount', 0)
|
||||
pw = r.get('PollutedWater', {}).get('amount', 0)
|
||||
sw = r.get('SaltWater', {}).get('amount', 0)
|
||||
|
||||
total_water = water + pw + sw
|
||||
warnings = []
|
||||
if total_water < 10000:
|
||||
warnings.append(("WARN", f"Total water low ({total_water:.0f} kg across all sources)"))
|
||||
elif total_water < 50000:
|
||||
warnings.append(("INFO", f"Water reserves moderate ({total_water:.0f} kg)"))
|
||||
if water < 10000 and pw > 10000:
|
||||
warnings.append(("INFO", f"Filter polluted water ({pw:.0f} kg available)"))
|
||||
|
||||
return warnings
|
||||
|
||||
|
||||
def analyze_research(research):
|
||||
if not isinstance(research, list):
|
||||
return []
|
||||
warnings = []
|
||||
done = sum(1 for t in research if t.get('isComplete'))
|
||||
total = len(research)
|
||||
if done == 0 and total > 0:
|
||||
warnings.append(("WARN", "No research completed! Start with Research Station"))
|
||||
elif done < total * 0.3:
|
||||
warnings.append(("INFO", f"Research progress: {done}/{total} ({done*100//total}%)"))
|
||||
return warnings
|
||||
|
||||
|
||||
def suggest_actions(warnings, alerts, buildings):
|
||||
suggestions = []
|
||||
|
||||
has_electrolyzer = any(b.get('id') == 'Electrolyzer' for b in (buildings or []))
|
||||
has_lavatory = any(b.get('id') == 'Lavatory' for b in (buildings or []))
|
||||
has_sieve = any(b.get('id') == 'WaterSiever' for b in (buildings or []))
|
||||
|
||||
for sev, msg in warnings:
|
||||
ml = msg.lower()
|
||||
if 'oxygen' in ml:
|
||||
suggestions.append("Build SPOM: Electrolyzer + Hydrogen Generator at a water source")
|
||||
elif 'food' in ml and 'shortage' in ml:
|
||||
suggestions.append("Build PlanterBoxes x5, plant Mealwood (no irrigation needed)")
|
||||
elif 'food' in ml and 'declining' in ml:
|
||||
suggestions.append("Expand farm or start hatch ranching (Hatch eats Sedimentary Rock)")
|
||||
elif 'power' in ml and 'generator' in ml:
|
||||
suggestions.append("Build ManualGenerator (early) or CoalGenerator (durable)")
|
||||
elif 'coal' in ml:
|
||||
suggestions.append("Diversify power: build HydrogenGenerator + SmartBattery")
|
||||
elif 'hydrogen' in ml:
|
||||
suggestions.append("Connect HydrogenGenerator to your hydrogen vent/SPOM")
|
||||
elif 'water' in ml and 'low' in ml:
|
||||
suggestions.append("Dig to find water geyser or filter polluted water")
|
||||
elif 'heat' in ml or 'overheat' in ml:
|
||||
suggestions.append("Build insulated tiles around heat sources; add cooling loop")
|
||||
|
||||
if not has_lavatory:
|
||||
suggestions.append("Build Lavatory + Water Sieve for renewable water loop")
|
||||
if not has_electrolyzer:
|
||||
suggestions.append("Plan SPOM once Algae < 5t or you have renewable water")
|
||||
if not has_sieve and has_lavatory:
|
||||
suggestions.append("Build Water Sieve to close the bathroom loop")
|
||||
|
||||
return list(dict.fromkeys(suggestions))
|
||||
|
||||
|
||||
def print_report(state):
|
||||
g = state.get('game', {})
|
||||
if 'error' in g:
|
||||
print(f"[!] Cannot connect to game: {g['error']}")
|
||||
return False
|
||||
|
||||
resources = state.get('resources', [])
|
||||
buildings = state.get('buildings', [])
|
||||
alerts = state.get('alerts', [])
|
||||
|
||||
all_warnings = []
|
||||
all_warnings += analyze_o2(resources, buildings)
|
||||
all_warnings += analyze_food(resources, buildings)
|
||||
all_warnings += analyze_power(resources, buildings)
|
||||
all_warnings += analyze_temp()
|
||||
all_warnings += analyze_water(resources)
|
||||
all_warnings += analyze_research(state.get('research', []))
|
||||
|
||||
critical = [w for w in all_warnings if w[0] == 'CRITICAL']
|
||||
warns = [w for w in all_warnings if w[0] == 'WARN']
|
||||
infos = [w for w in all_warnings if w[0] == 'INFO']
|
||||
|
||||
suggestions = suggest_actions(all_warnings, alerts, buildings)
|
||||
|
||||
print("=" * 56)
|
||||
print(" ONI Analysis Report")
|
||||
print("=" * 56)
|
||||
print(f" Cycle: {g.get('cycle', '?')}")
|
||||
print(f" Duplicants: {g.get('duplicantCount', '?')}")
|
||||
print(f" World: {g.get('worldName', '?')}")
|
||||
print(f" Grid: {g.get('gridWidth', '?')} x {g.get('gridHeight', '?')}")
|
||||
print(f" Buildings: {len(buildings or [])}")
|
||||
print(f" Critters: {len(state.get('critters', []) or [])}")
|
||||
print(f" Geysers: {len(state.get('geysers', []) or [])}")
|
||||
print(f" Plants: {len(state.get('plants', []) or [])}")
|
||||
print(f" Rooms: {len(state.get('rooms', []) or [])}")
|
||||
print(f" Research done: {sum(1 for t in (state.get('research') or []) if t.get('isComplete'))}/{len(state.get('research', []) or [])}")
|
||||
print()
|
||||
|
||||
if critical:
|
||||
print(f" [CRITICAL] {len(critical)} issues — act immediately!")
|
||||
for _, msg in critical:
|
||||
print(f" ! {msg}")
|
||||
print()
|
||||
|
||||
if warns:
|
||||
print(f" [WARN] {len(warns)} issues")
|
||||
for _, msg in warns:
|
||||
print(f" * {msg}")
|
||||
print()
|
||||
|
||||
if infos:
|
||||
print(f" [INFO] {len(infos)} notes")
|
||||
for _, msg in infos:
|
||||
print(f" i {msg}")
|
||||
print()
|
||||
|
||||
if not all_warnings:
|
||||
print(" Status: All stable")
|
||||
print()
|
||||
|
||||
if suggestions:
|
||||
print(f" Suggestions ({len(suggestions)}):")
|
||||
for s in suggestions:
|
||||
print(f" -> {s}")
|
||||
print()
|
||||
|
||||
print(f" In-game alerts: {len(alerts) if isinstance(alerts, list) else 0}")
|
||||
if isinstance(alerts, list):
|
||||
for a in alerts:
|
||||
print(f" [{a.get('severity', '?')}] {a.get('title', '?')}")
|
||||
print("=" * 56)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
state = get_game_state()
|
||||
if not print_report(state):
|
||||
sys.exit(1)
|
||||
1540
tools/oni_api.py
Normal file
1540
tools/oni_api.py
Normal file
File diff suppressed because it is too large
Load Diff
163
tools/oni_builder.py
Normal file
163
tools/oni_builder.py
Normal file
@ -0,0 +1,163 @@
|
||||
import json
|
||||
import sys
|
||||
from oni_api import api_post
|
||||
|
||||
BLUEPRINTS = {
|
||||
'spom': {
|
||||
'name': 'SPOM (Self-Powered Oxygen Module)',
|
||||
'description': 'Standard Rodriguez SPOM: electrolyzer + hydrogen generators',
|
||||
'size': {'width': 8, 'height': 6},
|
||||
'dig': {'x': -1, 'y': -1, 'width': 10, 'height': 8},
|
||||
'buildings': [
|
||||
{'id': 'Electrolyzer', 'x': 3, 'y': 2},
|
||||
{'id': 'GasPump', 'x': 1, 'y': 2},
|
||||
{'id': 'GasPump', 'x': 5, 'y': 2},
|
||||
{'id': 'HydrogenGenerator', 'x': 1, 'y': 0},
|
||||
{'id': 'HydrogenGenerator', 'x': 4, 'y': 0},
|
||||
{'id': 'GasFilter', 'x': 3, 'y': 0},
|
||||
],
|
||||
},
|
||||
'spom_mini': {
|
||||
'name': 'Mini SPOM',
|
||||
'description': 'Compact electrolyzer + 1 hydrogen generator for early game',
|
||||
'size': {'width': 5, 'height': 4},
|
||||
'dig': {'x': -1, 'y': -1, 'width': 7, 'height': 6},
|
||||
'buildings': [
|
||||
{'id': 'Electrolyzer', 'x': 2, 'y': 1},
|
||||
{'id': 'GasPump', 'x': 1, 'y': 1},
|
||||
{'id': 'HydrogenGenerator', 'x': 2, 'y': 0},
|
||||
],
|
||||
},
|
||||
'toilet_loop': {
|
||||
'name': 'Bathroom Water Loop',
|
||||
'description': 'Lavatory -> Water Purifier -> Lavatory closed loop',
|
||||
'size': {'width': 8, 'height': 4},
|
||||
'dig': {'x': -1, 'y': -1, 'width': 10, 'height': 6},
|
||||
'buildings': [
|
||||
{'id': 'Lavatory', 'x': 1, 'y': 1},
|
||||
{'id': 'Lavatory', 'x': 3, 'y': 1},
|
||||
{'id': 'WaterPurifier', 'x': 6, 'y': 1},
|
||||
{'id': 'LiquidPump', 'x': 6, 'y': 2},
|
||||
],
|
||||
},
|
||||
'ranch_hatch': {
|
||||
'name': 'Hatch Ranch',
|
||||
'description': 'Standard hatch ranching module with feeder and incubator',
|
||||
'size': {'width': 10, 'height': 6},
|
||||
'dig': {'x': -1, 'y': -1, 'width': 12, 'height': 8},
|
||||
'buildings': [
|
||||
{'id': 'RanchStation', 'x': 1, 'y': 1},
|
||||
{'id': 'Incubator', 'x': 4, 'y': 1},
|
||||
{'id': 'StorageLocker', 'x': 8, 'y': 1},
|
||||
],
|
||||
},
|
||||
'farm_mealwood': {
|
||||
'name': 'Mealwood Farm',
|
||||
'description': 'Basic mealwood farm: 5 planter boxes + storage',
|
||||
'size': {'width': 6, 'height': 4},
|
||||
'dig': {'x': -1, 'y': -1, 'width': 8, 'height': 6},
|
||||
'buildings': [
|
||||
{'id': 'PlanterBox', 'x': 1, 'y': 1},
|
||||
{'id': 'PlanterBox', 'x': 3, 'y': 1},
|
||||
{'id': 'PlanterBox', 'x': 5, 'y': 1},
|
||||
{'id': 'PlanterBox', 'x': 1, 'y': 3},
|
||||
{'id': 'PlanterBox', 'x': 3, 'y': 3},
|
||||
{'id': 'StorageLocker', 'x': 5, 'y': 3},
|
||||
],
|
||||
},
|
||||
'cooling': {
|
||||
'name': 'Steam Turbine Cooler',
|
||||
'description': 'Liquid cooling loop with steam turbine + aquatuner',
|
||||
'size': {'width': 8, 'height': 6},
|
||||
'dig': {'x': -1, 'y': -1, 'width': 10, 'height': 8},
|
||||
'buildings': [
|
||||
{'id': 'SteamTurbine', 'x': 1, 'y': 4},
|
||||
{'id': 'SteamTurbine', 'x': 5, 'y': 4},
|
||||
{'id': 'Aquatuner', 'x': 2, 'y': 1},
|
||||
{'id': 'LiquidPump', 'x': 5, 'y': 1},
|
||||
],
|
||||
},
|
||||
'bedroom': {
|
||||
'name': 'Barracks / Bedroom',
|
||||
'description': 'Basic bedroom module with cots and decorations',
|
||||
'size': {'width': 8, 'height': 4},
|
||||
'dig': {'x': -1, 'y': -1, 'width': 10, 'height': 6},
|
||||
'buildings': [
|
||||
{'id': 'Bed', 'x': 1, 'y': 1},
|
||||
{'id': 'Bed', 'x': 3, 'y': 1},
|
||||
{'id': 'Bed', 'x': 5, 'y': 1},
|
||||
{'id': 'LadderBed', 'x': 1, 'y': 3},
|
||||
{'id': 'LadderBed', 'x': 3, 'y': 3},
|
||||
{'id': 'FlowerVase', 'x': 6, 'y': 1},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def list_blueprints():
|
||||
print(f"Available Blueprints ({len(BLUEPRINTS)}):")
|
||||
print("=" * 60)
|
||||
for key, bp in BLUEPRINTS.items():
|
||||
print(f" {key:16s} {bp['name']:28s} {bp['size']['width']}x{bp['size']['height']}")
|
||||
print(f" {'':16s} {bp['description']}")
|
||||
print()
|
||||
|
||||
|
||||
def apply_blueprint(name, origin_x, origin_y):
|
||||
bp = BLUEPRINTS.get(name)
|
||||
if not bp:
|
||||
print(f"[!] Blueprint '{name}' not found")
|
||||
print(f" Use 'list' to see available blueprints")
|
||||
return False
|
||||
|
||||
print(f"Applying blueprint: {bp['name']}")
|
||||
print(f" Origin: ({origin_x}, {origin_y})")
|
||||
print(f" Size: {bp['size']['width']} x {bp['size']['height']}")
|
||||
print()
|
||||
|
||||
results = []
|
||||
|
||||
dig = bp.get('dig', {'x': -1, 'y': -1, 'width': bp['size']['width'] + 2, 'height': bp['size']['height'] + 2})
|
||||
dig_result = api_post('/api/action/dig', {
|
||||
'x': origin_x + dig['x'],
|
||||
'y': origin_y + dig['y'],
|
||||
'width': dig['width'],
|
||||
'height': dig['height'],
|
||||
})
|
||||
status = 'OK' if 'error' not in dig_result else dig_result.get('error', 'fail')
|
||||
print(f" [Dig] area ({dig['width']}x{dig['height']}): {status}")
|
||||
|
||||
for b in bp['buildings']:
|
||||
x = origin_x + b['x']
|
||||
y = origin_y + b['y']
|
||||
result = api_post('/api/action/build', {
|
||||
'buildingId': b['id'],
|
||||
'x': x,
|
||||
'y': y,
|
||||
})
|
||||
ok = 'error' not in result
|
||||
results.append({'building': b['id'], 'x': x, 'y': y, 'ok': ok})
|
||||
status = 'OK' if ok else result.get('error', 'fail')
|
||||
print(f" [Build] {b['id']:20s} at ({x:3d}, {y:3d}): {status}")
|
||||
|
||||
ok_count = sum(1 for r in results if r['ok'])
|
||||
print()
|
||||
print(f" Result: {ok_count}/{len(results)} buildings placed")
|
||||
return ok_count == len(results)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cmd = sys.argv[1] if len(sys.argv) > 1 else 'list'
|
||||
|
||||
if cmd == 'list':
|
||||
list_blueprints()
|
||||
elif cmd == 'build':
|
||||
if len(sys.argv) < 4:
|
||||
print("Usage: python oni_builder.py build <blueprint_name> <origin_x> <origin_y>")
|
||||
print()
|
||||
list_blueprints()
|
||||
sys.exit(1)
|
||||
success = apply_blueprint(sys.argv[2], int(sys.argv[3]), int(sys.argv[4]))
|
||||
sys.exit(0 if success else 1)
|
||||
else:
|
||||
print("Usage: python oni_builder.py <list|build>")
|
||||
355
tools/oni_commander.py
Normal file
355
tools/oni_commander.py
Normal file
@ -0,0 +1,355 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ONI Commander — 高级指令封装
|
||||
============================
|
||||
把多个底层 API 调用组合成一条"指挥官指令",AI 一句话就能执行复杂操作。
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import inspect
|
||||
|
||||
TOOLS_DIR = os.path.dirname(__file__)
|
||||
sys.path.insert(0, TOOLS_DIR)
|
||||
|
||||
from oni_api import api_get, api_post, _print_feedback
|
||||
|
||||
|
||||
def cmd_diagnose(args):
|
||||
"""全面诊断:O2、食物、电力、CO2、温度、管道。"""
|
||||
print("=" * 56)
|
||||
print(" ONI Full Diagnostic")
|
||||
print("=" * 56)
|
||||
|
||||
pause_before()
|
||||
pause_after()
|
||||
|
||||
# 1. 游戏总览
|
||||
game = api_get('/api/state/game')
|
||||
if 'error' in game:
|
||||
print(f"[!] Cannot connect: {game['error']}")
|
||||
return False
|
||||
print(f" Cycle {game.get('cycle', '?')} | {game.get('duplicantCount', '?')} dupes | "
|
||||
f"{game.get('suffocating', 0)} suffocating | {game.get('starving', 0)} starving | "
|
||||
f"{game.get('stressed', 0)} stressed")
|
||||
|
||||
# 2. 电力
|
||||
print("\n--- Power ---")
|
||||
power = api_get('/api/state/power')
|
||||
if 'circuits' in power:
|
||||
for c in power['circuits']:
|
||||
mark = " *** OVERLOAD ***" if c.get('isOverloaded') else ""
|
||||
print(f" Circuit {c.get('id')}: {c.get('wattsUsed', 0):.0f}W / {c.get('maxWatts', 0):.0f}W{mark}")
|
||||
|
||||
# 3. CO2
|
||||
print("\n--- CO2 ---")
|
||||
co2 = api_get('/api/state/co2')
|
||||
if co2.get('pocketCount', 0) > 0:
|
||||
print(f" {co2.get('pocketCount')} pockets ({co2.get('totalMassKg', 0):.0f} kg CO2)")
|
||||
for p in co2.get('pockets', [])[:3]:
|
||||
print(f" ({p.get('x')},{p.get('y')}) {p.get('mass', 0):.0f} kg")
|
||||
else:
|
||||
print(" No CO2 pockets detected")
|
||||
|
||||
# 4. 温度
|
||||
print("\n--- Temperature ---")
|
||||
temp = api_get('/api/state/temperature/zones')
|
||||
if 'averageC' in temp:
|
||||
print(f" Avg: {temp['averageC']:.0f}°C Min: {temp.get('minC', 0):.0f}°C Max: {temp.get('maxC', 0):.0f}°C")
|
||||
if temp.get('hotSpots'):
|
||||
print(f" {len(temp['hotSpots'])} hot spots (>50°C) — risk!")
|
||||
if temp.get('coldSpots'):
|
||||
print(f" {len(temp['coldSpots'])} cold spots (<5°C)")
|
||||
|
||||
# 5. 疾病
|
||||
print("\n--- Diseases ---")
|
||||
diseases = api_get('/api/state/diseases')
|
||||
infected = diseases.get('infectedDuplicants', [])
|
||||
if infected:
|
||||
for d in infected:
|
||||
print(f" {d.get('duplicant')} — {d.get('disease')} ({d.get('severity')})")
|
||||
else:
|
||||
print(" No infections")
|
||||
|
||||
# 6. 管道
|
||||
print("\n--- Pipes ---")
|
||||
for pt in ('gas', 'liquid'):
|
||||
pipes = api_get(f'/api/state/pipes?type={pt}')
|
||||
segs = pipes.get('segmentCount', 0)
|
||||
if segs:
|
||||
first = pipes.get('segments', [{}])[0]
|
||||
print(f" {pt}: {segs} segments (e.g. {first.get('element', '?')})")
|
||||
else:
|
||||
print(f" {pt}: empty")
|
||||
|
||||
print()
|
||||
print("=" * 56)
|
||||
print(" Diagnostic complete")
|
||||
print("=" * 56)
|
||||
|
||||
|
||||
def cmd_fix_co2(args):
|
||||
"""找到 CO2 并挖掘排气通道。"""
|
||||
print("[CO2 Fix] Scanning for CO2 pockets...")
|
||||
co2 = api_get('/api/state/co2')
|
||||
pockets = co2.get('pockets', [])
|
||||
if not pockets:
|
||||
print("[OK] No CO2 pockets found.")
|
||||
return
|
||||
|
||||
pause_before()
|
||||
|
||||
# Find the lowest y-level pocket and dig below it
|
||||
bottom = min(pockets, key=lambda p: p.get('y', 0))
|
||||
x, y = bottom.get('x', 0), bottom.get('y', 0)
|
||||
print(f"[CO2 Fix] Largest pocket at ({x},{y}), {bottom.get('mass', 0):.0f} kg")
|
||||
|
||||
# Dig a 1-wide shaft down
|
||||
dig_y = max(0, y - 5)
|
||||
result = api_post('/api/action/dig', {"x": x, "y": dig_y, "width": 1, "height": y - dig_y + 1})
|
||||
if result.get('success'):
|
||||
print(f"[CO2 Fix] Dug vent shaft at x={x}, y={dig_y}..{y}")
|
||||
else:
|
||||
print(f"[CO2 Fix] Dig failed: {result.get('errorMessage', 'unknown')}")
|
||||
|
||||
pause_after()
|
||||
|
||||
|
||||
def cmd_fix_overload(args):
|
||||
"""检测过载电路并给出修复建议。"""
|
||||
print("[Power Fix] Analyzing circuits...")
|
||||
power = api_get('/api/state/power')
|
||||
overloaded = [c for c in power.get('circuits', []) if c.get('isOverloaded')]
|
||||
if not overloaded:
|
||||
print("[OK] No overloaded circuits.")
|
||||
return
|
||||
|
||||
pause_before()
|
||||
|
||||
print(f"[Power Fix] {len(overloaded)} overloaded circuits:")
|
||||
for c in overloaded:
|
||||
print(f" Circuit {c.get('id')}: {c.get('wattsUsed', 0):.0f}W / {c.get('maxWatts', 0):.0f}W")
|
||||
|
||||
# Suggest fixes
|
||||
print()
|
||||
print(" Suggested fixes:")
|
||||
print(" 1. Move heavy consumers (MetalRefinery, Aquatuner) to separate circuit")
|
||||
print(" 2. Upgrade wire to HeaviWatt or split into 2 transformers")
|
||||
print(" 3. Add PowerTransformer to isolate high-load branches")
|
||||
|
||||
pause_after()
|
||||
|
||||
|
||||
def cmd_build_pipe_line(args):
|
||||
"""铺设管道路径(带交叉模式)。"""
|
||||
if len(args) < 5:
|
||||
print("Usage: build_pipe_line gas|liquid <x1> <y1> <x2> <y2> [mode]")
|
||||
print(" mode: 'line'(默认,与已有管线合并) | 'cross'(跨接器跳过) | 'single'(单段)")
|
||||
return
|
||||
ptype = args[0]
|
||||
x1, y1, x2, y2 = int(args[1]), int(args[2]), int(args[3]), int(args[4])
|
||||
mode = args[5] if len(args) > 5 else 'line'
|
||||
|
||||
pause_before()
|
||||
|
||||
if ptype not in ('gas', 'liquid'):
|
||||
print("[!] Type must be 'gas' or 'liquid'")
|
||||
return
|
||||
|
||||
result = api_post('/api/action/build_pipe', {
|
||||
"type": ptype, "x1": x1, "y1": y1,
|
||||
"x2": x2, "y2": y2, "mode": mode
|
||||
})
|
||||
if result.get('success'):
|
||||
d = result.get('data', {})
|
||||
segs = d.get('segmentCount', 0)
|
||||
bridges = d.get('bridgesPlaced', 0)
|
||||
print(f"[OK] {ptype} pipe: {segs} segments from ({x1},{y1}) to ({x2},{y2})")
|
||||
if bridges > 0:
|
||||
print(f" {bridges} bridges placed at crossings (mode={mode})")
|
||||
else:
|
||||
print(f"[!] Failed: {result.get('errorMessage', 'unknown')}")
|
||||
|
||||
pause_after()
|
||||
|
||||
|
||||
def cmd_build_wire_line(args):
|
||||
"""铺设电线路径。"""
|
||||
if len(args) < 5:
|
||||
print("Usage: build_wire_line regular|heavy|conductive <x1> <y1> <x2> <y2> [mode]")
|
||||
print(" mode: 'line' (default), 'cross' (use bridges), 'single'")
|
||||
return
|
||||
wtype = args[0]
|
||||
x1, y1, x2, y2 = int(args[1]), int(args[2]), int(args[3]), int(args[4])
|
||||
mode = args[5] if len(args) > 5 else 'line'
|
||||
|
||||
pause_before()
|
||||
|
||||
if wtype not in ('regular', 'heavy', 'conductive', 'heavy_conductive'):
|
||||
print("[!] Type must be 'regular', 'heavy', 'conductive', or 'heavy_conductive'")
|
||||
return
|
||||
|
||||
result = api_post('/api/action/build_wire', {
|
||||
"type": wtype, "x1": x1, "y1": y1,
|
||||
"x2": x2, "y2": y2, "mode": mode
|
||||
})
|
||||
if result.get('success'):
|
||||
segs = result.get('data', {}).get('segmentCount', 0)
|
||||
print(f"[OK] {wtype} wire: {segs} segments from ({x1},{y1}) to ({x2},{y2})")
|
||||
else:
|
||||
print(f"[!] Failed: {result.get('errorMessage', 'unknown')}")
|
||||
|
||||
pause_after()
|
||||
|
||||
|
||||
def cmd_expand_base(args):
|
||||
"""拓展基地:挖掘 + 建造墙壁。"""
|
||||
if len(args) < 4:
|
||||
print("Usage: expand_base <x> <y> <width> <height>")
|
||||
return
|
||||
x, y, w, h = int(args[0]), int(args[1]), int(args[2]), int(args[3])
|
||||
|
||||
pause_before()
|
||||
|
||||
# Dig
|
||||
r1 = api_post('/api/action/dig', {"x": x - 1, "y": y - 1, "width": w + 2, "height": h + 2})
|
||||
if not r1.get('success'):
|
||||
print(f"[!] Dig failed: {r1.get('errorMessage', 'unknown')}")
|
||||
pause_after()
|
||||
return
|
||||
print(f"[Expand] Dug ({x},{y}) {w}x{h}")
|
||||
|
||||
# Build floor tiles
|
||||
for fx in range(x, x + w):
|
||||
api_post('/api/action/build', {"buildingId": "Tile", "x": fx, "y": y})
|
||||
print(f"[Expand] Built floor: {w} tiles")
|
||||
|
||||
# Build walls
|
||||
for wx in range(x, x + w):
|
||||
api_post('/api/action/build', {"buildingId": "Tile", "x": wx, "y": y + h})
|
||||
for wy in range(y + 1, y + h):
|
||||
api_post('/api/action/build', {"buildingId": "Tile", "x": x, "y": wy})
|
||||
api_post('/api/action/build', {"buildingId": "Tile", "x": x + w - 1, "y": wy})
|
||||
print(f"[Expand] Built walls")
|
||||
|
||||
pause_after()
|
||||
print(f"[OK] Room expanded to ({x},{y}) {w}x{h}")
|
||||
|
||||
|
||||
def cmd_emergency_o2(args):
|
||||
"""紧急制氧:检查 O2 并自动建造。"""
|
||||
print("[Emergency O2] Checking oxygen status...")
|
||||
|
||||
pause_before()
|
||||
|
||||
resources = api_get('/api/state/resources')
|
||||
buildings = api_get('/api/state/buildings')
|
||||
game = api_get('/api/state/game')
|
||||
|
||||
if isinstance(resources, list):
|
||||
o2 = next((r for r in resources if r.get('name') == 'Oxygen'), {})
|
||||
algae = next((r for r in resources if r.get('name') == 'Algae'), {})
|
||||
o2_kg = o2.get('amount', 0)
|
||||
algae_kg = algae.get('amount', 0)
|
||||
print(f" O2: {o2_kg:.0f} kg | Algae: {algae_kg:.0f} kg")
|
||||
else:
|
||||
o2_kg, algae_kg = 0, 0
|
||||
|
||||
has_electrolyzer = any(b.get('id') == 'Electrolyzer' for b in (buildings or []))
|
||||
has_diffuser = any(b.get('id') == 'OxygenDiffuser' for b in (buildings or []))
|
||||
|
||||
if o2_kg < 500:
|
||||
print("[CRITICAL] Oxygen critical!")
|
||||
if has_diffuser and algae_kg > 500:
|
||||
print(" OxygenDiffuser already exists, checking Algae supply...")
|
||||
elif not has_electrolyzer and not has_diffuser:
|
||||
# Find a spot near base and build
|
||||
game_info = api_get('/api/state/game')
|
||||
print(" No O2 production! Building OxygenDiffuser...")
|
||||
result = api_post('/api/action/build', {"buildingId": "OxygenDiffuser", "x": 30, "y": 20})
|
||||
if result.get('success'):
|
||||
print(" [OK] OxygenDiffuser queued at (30,20)")
|
||||
else:
|
||||
print(f" [!] {result.get('errorMessage', 'build failed')}")
|
||||
elif o2_kg < 2000 and not has_electrolyzer:
|
||||
print("[WARN] Low O2, recommend SPOM build")
|
||||
else:
|
||||
print("[OK] Oxygen stable")
|
||||
|
||||
pause_after()
|
||||
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
def pause_before():
|
||||
"""High-level ops always pause first."""
|
||||
api_post('/api/action/pause', {"reason": "High-level operation"})
|
||||
|
||||
|
||||
def pause_after():
|
||||
"""Resume after operation."""
|
||||
api_post('/api/action/unpause', {"speed": 1})
|
||||
|
||||
|
||||
# ── Command Registry ─────────────────────────────────────────────────────
|
||||
|
||||
COMMANDS = {
|
||||
'diagnose': cmd_diagnose,
|
||||
'fix_co2': cmd_fix_co2,
|
||||
'fix_overload': cmd_fix_overload,
|
||||
'expand_base': cmd_expand_base,
|
||||
'emergency_o2': cmd_emergency_o2,
|
||||
'build_pipe_line': cmd_build_pipe_line,
|
||||
'build_wire_line': cmd_build_wire_line,
|
||||
'snapshot': cmd_snapshot,
|
||||
'camera': cmd_camera,
|
||||
}
|
||||
|
||||
def cmd_snapshot(args):
|
||||
"""Take screenshot + view. Usage: snapshot [name]"""
|
||||
from oni_api import cmd_snapshot as api_snapshot
|
||||
api_snapshot(args)
|
||||
|
||||
def cmd_camera(args):
|
||||
"""Move camera. Usage: camera <x> <y> [zoom]"""
|
||||
from oni_api import cmd_camera as api_camera
|
||||
api_camera(args)
|
||||
|
||||
if __name__ == '__main__':
|
||||
cmd = sys.argv[1] if len(sys.argv) > 1 else 'help'
|
||||
|
||||
if cmd == 'help' or cmd not in COMMANDS:
|
||||
print("ONI Commander — 高级指令")
|
||||
print("=" * 56)
|
||||
print()
|
||||
print("=== Diagnostics ===")
|
||||
print(" diagnose 全面诊断(O2/电力/CO2/温度/疾病/管道)")
|
||||
print()
|
||||
print("=== Automated Fixes ===")
|
||||
print(" fix_co2 找到 CO2 并挖掘排气通道")
|
||||
print(" fix_overload 检测过载电路并建议修复")
|
||||
print(" emergency_o2 紧急制氧(检查+自动建造)")
|
||||
print()
|
||||
print("=== Room Expansion ===")
|
||||
print(" expand_base <x> <y> <w> <h> 挖掘+建造墙壁(一键拓展房间)")
|
||||
print()
|
||||
print("=== Pipe / Wire Lines ===")
|
||||
print(" build_pipe_line <t> <x1> <y1> <x2> <y2> [mode]")
|
||||
print(" t: gas | liquid | mode: line(merge) | cross(bridge)")
|
||||
print(" build_wire_line <t> <x1> <y1> <x2> <y2> [mode]")
|
||||
print(" t: regular | heavy | conductive | heavy_conductive")
|
||||
print()
|
||||
print("=== Screenshot / Camera ===")
|
||||
print(" snapshot [file.png] Take screenshot")
|
||||
print(" camera <x> <y> [zoom] Move camera view")
|
||||
print()
|
||||
print("All high-level commands auto-pause/resume the game.")
|
||||
else:
|
||||
args = sys.argv[2:]
|
||||
fn = COMMANDS[cmd]
|
||||
sig = inspect.signature(fn)
|
||||
if len(sig.parameters) > 0:
|
||||
fn(args)
|
||||
else:
|
||||
fn()
|
||||
Reference in New Issue
Block a user