{"id":41164087,"url":"https://github.com/technix/atrament-core","last_synced_at":"2026-01-22T19:29:35.388Z","repository":{"id":24333086,"uuid":"80432668","full_name":"technix/atrament-core","owner":"technix","description":"Framework for choice-based games, built around inkjs","archived":false,"fork":false,"pushed_at":"2025-10-20T21:01:45.000Z","size":3568,"stargazers_count":23,"open_issues_count":1,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-11-23T00:11:56.910Z","etag":null,"topics":["choice-based-game","ink","interactive-fiction","javascript"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/technix.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2017-01-30T15:15:35.000Z","updated_at":"2025-10-18T22:55:24.000Z","dependencies_parsed_at":"2024-05-21T15:49:28.437Z","dependency_job_id":"5059866d-7b50-4eb1-80c5-3bebb1fa7245","html_url":"https://github.com/technix/atrament-core","commit_stats":null,"previous_names":["technix/atrament-core"],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/technix/atrament-core","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/technix%2Fatrament-core","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/technix%2Fatrament-core/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/technix%2Fatrament-core/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/technix%2Fatrament-core/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/technix","download_url":"https://codeload.github.com/technix/atrament-core/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/technix%2Fatrament-core/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28669093,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-22T17:07:18.858Z","status":"ssl_error","status_checked_at":"2026-01-22T17:05:02.040Z","response_time":144,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["choice-based-game","ink","interactive-fiction","javascript"],"created_at":"2026-01-22T19:29:34.396Z","updated_at":"2026-01-22T19:29:35.381Z","avatar_url":"https://github.com/technix.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @atrament/core\r\n\r\n`@atrament/core` is a framework for choice-based games, built around `InkJS`. It is a foundation of an [@atrament/web](https://github.com/technix/atrament-web) library and [atrament-ui](https://github.com/technix/atrament-web-ui) web application.\r\n\r\n**[Documentation](https://atrament.ink)**\r\n\r\n## Features\r\n\r\n- Implements game flow: loading Ink story, getting text content, making choices\r\n- Manages global application settings\r\n- Parses tags, and handles some of them (mostly compatible with Inky)\r\n- Auto-observe variables defined with 'observe' global tag\r\n- Manages sound and music via knot tags\r\n- Manages autosaves, checkpoints, and named saves for every game\r\n- Music state is saved and restored along with game state\r\n- All changes affect the internal state\r\n\r\n\r\n## Installation\r\n\r\n```npm install @atrament/core```\r\n\r\n## Tags handled by Atrament\r\n\r\n### Global tags\r\n\r\n| Tag | Description                |\r\n| :-------- | :------------------------- |\r\n| `# observe: varName` | Register variable observer for `varName` Ink variable. The variable value is available in the `vars` section of Atrament state. |\r\n| `# persist: varName` | Save this variable value to persistent storage, and restore it before the game starts. |\r\n| `# autosave: false` | Disables autosaves. |\r\n| `# single_scene` | Store only the last scene in the Atrament state. |\r\n| `# continue_maximally: false` | Pause story after each line. In this mode the `scene` object contains the `canContinue` field, which is set to true if the story can be continued. |\r\n\r\n\r\n### Knot tags\r\n| Tag | Description                |\r\n| :-------- | :------------------------- |\r\n| `# CLEAR` | Clear the scenes list before saving the current scene to Atrament state. |\r\n| `# AUDIO: sound.mp3` | Play sound (once). |\r\n| `# AUDIOLOOP: music.mp3` | Play background music (looped). There can be only one background music track. |\r\n| `# AUDIOLOOP: false` | Stop playing all background music. |\r\n| `# PLAY_SOUND: sound.mp3` | Play sound (once). |\r\n| `# STOP_SOUND: sound.mp3` | Stop playing specific sound. |\r\n| `# STOP_SOUND` | Stop playing all sounds. |\r\n| `# PLAY_MUSIC: music.mp3` | Play background music (looped). There can be multiple background music tracks, played simultaneously. |\r\n| `# STOP_MUSIC: music.mp3` | Stop playing specific background music. |\r\n| `# STOP_MUSIC` | Stop playing all background music. |\r\n| `# CHECKPOINT` | Save the game to the default checkpoint. |\r\n| `# CHECKPOINT: checkpointName` | Save the game to checkpoint `checkpointName`. |\r\n| `# SAVEGAME: saveslot` | Save the game to `saveslot`. |\r\n| `# RESTART` | Start game from beginning. |\r\n| `# RESTART_FROM_CHECKPOINT` | Restart the game from the latest checkpoint. |\r\n| `# RESTART_FROM_CHECKPOINT: checkpointName` | Restart game from named checkpoint. |\r\n| `# IMAGE: picture.jpg` | Adds specified image to the `images` attribute of the scene and paragraph. It can be used to preload image files for the scene. |\r\n\r\nNote: For sound effects, please use either AUDIO/AUDIOLOOP or PLAY_SOUND/PLAY_MUSIC/STOP_SOUND/STOP_MUSIC tags. Combining them may lead to unexpected side effects.\r\n\r\n### Choice tags\r\n| Tag | Description                |\r\n| :-------- | :------------------------- |\r\n| `# UNCLICKABLE` | Alternative names: `#DISABLED`, `#INACTIVE`.\u003cbr\u003eSets `disabled: true` attribute to the choice. |\r\n\r\n## API Reference\r\n\r\n#### atrament.version\r\n\r\nAtrament version string. Read-only. \r\n\r\n### Base methods\r\n\r\n#### atrament.defineInterfaces()\r\n\r\nDefines interface modules for:\r\n- **loader**: ink file loader\r\n- **persistent**: persistent storage\r\n- **sound**: sound control (optional)\r\n- **state**: state management\r\n\r\nInterfaces should be defined **before** calling any other methods.\r\n\r\n```\r\natrament.defineInterfaces({\r\n    loader: interfaceLoader,\r\n    persistent: persistentInterface,\r\n    sound: soundInterface,\r\n    state: stateInterface\r\n});\r\n```\r\n\r\n#### atrament.init(Story, configuration)\r\n\r\nInitialize the game engine. Takes two parameters:\r\n- **Story** is an inkjs constructor, imported directly from inkjs\r\n- **configuration** is a configuration object:\r\n    - **applicationID** should be a unique string. It is used to distinguish persistent storage of your application.\r\n    - **settings** is a default settings object. These settings are immediately applied.\r\n\r\n```\r\nimport {Story} from 'inkjs';\r\nconst config = {\r\n    applicationID: 'your-application-id',\r\n    settings: {\r\n        mute: true,\r\n        volume: 10,\r\n        fullscreen: true\r\n    }\r\n}\r\natrament.init(Story, config);\r\n```\r\n\r\n#### atrament.on(event, listener)\r\n\r\nSubscribe to specific Atrament events. The **listener** function is called with a single argument containing event parameters.\r\n\r\nYou can subscribe to all Atrament events:\r\n```\r\natrament.on('*', (event, args) =\u003e { ... });\r\n```\r\n\r\n#### atrament.off(event, listener)\r\n\r\nUnsubscribe specified listener from the Atrament event.\r\n\r\n#### atrament.state\r\n\r\nReturns Atrament state interface. Can be used to operate state directly:\r\n\r\n```\r\natrament.state.setSubkey('game', 'checkpoint', true);\r\n```\r\n\r\n#### atrament.store\r\n\r\nReturn raw store object. It can be used in hooks, for example:\r\n\r\n```\r\nconst gamestate = useStore(atrament.store);\r\n```\r\n\r\n#### atrament.interfaces\r\n\r\nReturns raw interface objects. It can be used to operate with them directly.\r\n\r\n```\r\nconst { state, persistent } = atrament.interfaces;\r\n```\r\n\r\n### Game methods\r\n\r\n#### async atrament.game.init(path, file, gameID)\r\n\r\nInitialize game object. It is required to perform operations with saves.\r\nParameters:\r\n- path: path to Ink file\r\n- file: Ink file name\r\n- gameID: optional. If provided, Atrament will use the given ID for save management. Otherwise, it will be generated based on path and filename.\r\n\r\nEvent: `'game/init', { pathToInkFile: path, inkFile: file }`\r\n\r\n#### async atrament.game.initInkStory()\r\n\r\nLoad Ink file and initialize Ink Story object. Then it updates game metadata and initializes variable observers.\r\n\r\nEvent: `'game/initInkStory'`\r\n\r\n#### atrament.game.getSaveSlotKey({ name, type })\r\n\r\nReturns save slot identifier for given save name and type.\r\nPossible save types: `atrament.game.SAVE_GAME`, `atrament.game.SAVE_CHECKPOINT`, `atrament.game.SAVE_AUTOSAVE`. For autosaves, the `name` parameter should be omitted.\r\nThe returned value can be used as a `saveslot` parameter.\r\n\r\n#### async atrament.game.start(saveslot)\r\n\r\nIf the game is started for the first time, or the initialized game is not the same as the current one - call `initInkStory` first.\r\nClears game state, and gets initial data for variable observers.\r\nIf `saveslot` is defined, load state from specified save.\r\n\r\nEvent: `'game/start', { saveSlot: saveslot }`\r\n\r\n#### async atrament.game.resume()\r\n\r\nResume saved game:\r\n- if autosave exists, resume from autosave\r\n- if checkpoints exist, resume from the newest checkpoint\r\n- otherwise, start a new game\r\n\r\nEvent: `'game/resume', { saveSlot: saveslot }`\r\n\r\n#### async atrament.game.canResume()\r\n\r\nReturns save slot identifier if game can be resumed.\r\n\r\nEvent: `'game/canResume', { saveSlot: saveslot }`\r\n\r\n#### async atrament.game.restart(saveslot)\r\n\r\nRestart the game from the specified save slot (if `saveslot` is not defined, start a new game). \r\n\r\nEvent: `'game/restart', { saveSlot: saveslot }`\r\n\r\n#### async atrament.game.restartFromCheckpoint(name)\r\n\r\nRestart the game from the checkpoint with the given name (if no such checkpoint found, restart the game).\r\n\r\n#### async atrament.game.load(saveslot)\r\n\r\nLoad game state from specified save slot. \r\n\r\nEvent: `'game/load', saveslot`\r\n\r\n#### async atrament.game.saveGame(name)\r\n\r\nSave game state to save slot.\r\n\r\nEvent: `'game/save', { type: 'game', name }`\r\n\r\n#### async atrament.game.saveCheckpoint(name)\r\n\r\nSave the game state to the checkpoint.\r\n\r\nEvent: `'game/save', { type: 'checkpoint', name }`\r\n\r\n#### async atrament.game.saveAutosave()\r\n\r\nSave the game state to autosave slot.\r\n\r\nEvent: `'game/save', { type: 'autosave' }`\r\n\r\n#### async atrament.game.listSaves()\r\n\r\nReturns array of all existing saves for active game.\r\n\r\nEvent: `'game/listSaves', savesListArray`\r\n\r\n#### async atrament.game.removeSave(saveslot)\r\n\r\nRemoves specified game save slot.\r\n\r\nEvent: `'game/removeSave', saveslot`\r\n\r\n#### async atrament.game.existSave(saveslot)\r\n\r\nReturns `true` if specified save slot exists.\r\n\r\n#### atrament.game.continueStory()\r\n\r\n- gets Ink scene content\r\n- run scene processors\r\n- process tags\r\n- updates Atrament state with a scene content\r\n\r\nEvent: `'game/continueStory'`\r\n\r\nEvent for tag handling: `'game/handleTag', { [tagName]: tagValue }`\r\n\r\n#### atrament.game.makeChoice(id)\r\n\r\nMake a choice in Ink. Wrapper for `atrament.ink.makeChoice`.\r\n\r\n#### atrament.game.defineSceneProcessor(processorFunction)\r\n\r\nRegister `processorFunction` for scene post-processing. It takes the `scene` object as an argument by reference:\r\n\r\n```\r\nfunction processCheckpoint(scene) {\r\n    if (scene.tags.CHECKPOINT) {\r\n        scene.is_checkpoint = true;\r\n    }\r\n}\r\natrament.game.defineSceneProcessor(processCheckpoint);\r\n\r\n```\r\n\r\n#### atrament.game.getAssetPath(file)\r\n\r\nReturns the full path to asset file (image, sound, music).\r\n\r\n#### atrament.game.clear()\r\n\r\nMethod to call at the game end. It stops music, and clears `scenes` and `vars` in the Atrament state.\r\n\r\nEvent: `'game/clear'`\r\n\r\n#### atrament.game.reset()\r\n\r\nMethod to call at the game end. It calls `atrament.game.clear()`, then clears `metadata` and `game` in Atrament state.\r\n\r\nEvent: `'game/reset'`\r\n\r\n#### atrament.game.getSession()\r\n\r\nReturns current game session.\r\n\r\n#### atrament.game.setSession(sessionID)\r\n\r\nSets current game session. If set to empty value, reset session ID to default.\r\n\r\nEvent: `'game/setSession', sessionID`\r\n\r\n#### async atrament.game.getSessions()\r\n\r\nReturns list of existing sessions in a `{ sessionName: numberOfSaves, ... }` format.\r\n\r\nEvent: `'game/getSessions', sessionList`\r\n\r\n#### async atrament.game.deleteSession(sessionID)\r\n\r\nDelete all saves for a given session.\r\n\r\nEvent: `'game/deleteSession', sessionID`\r\n\r\n#### atrament.game.getState()\r\n\r\nGet state object for the game (ink state, \"game\" and \"scenes\" state keys)\r\n\r\n#### atrament.game.setState(gameState)\r\n\r\nSet the game state from the provided object (same as returned by getState)\r\n\r\n\r\n### Ink methods\r\n\r\n#### atrament.ink.initStory()\r\n\r\nInitializes Ink story with loaded content.\r\n\r\nEvent: `'ink/initStory'`\r\n#### atrament.ink.story()\r\n\r\nReturns current Story instance.\r\n\r\n#### atrament.ink.loadState(state)\r\n\r\nLoad Ink state from JSON.\r\n\r\n#### atrament.ink.getState()\r\n\r\nReturns current Ink state as JSON object.\r\n\r\n#### atrament.ink.makeChoice(id)\r\n\r\nWrapper for `Story.ChooseChoiceIndex`.\r\n\r\nEvent: `'ink/makeChoice', { id: choiceId }`\r\n\r\n#### atrament.ink.getVisitCount(ref)\r\n\r\nWrapper for `Story.VisitCountAtPathString`.\r\n\r\nEvent: `'ink/getVisitCount', { ref: ref, visitCount: value }`\r\n\r\n#### atrament.ink.evaluateFunction(functionName, argsArray)\r\n\r\nEvaluates Ink function, then returns the result of the evaluation. Wrapper for `Story.EvaluateFunction`.\r\n\r\nEvent: `'ink/evaluateFunction', { function: functionName, args: argsArray, result: functionReturnValue }`\r\n\r\n#### atrament.ink.getGlobalTags()\r\n\r\nReturns parsed Ink global tags.\r\n\r\nEvent: `'ink/getGlobalTags', { globalTags: globalTagsObject }`\r\n\r\n#### atrament.ink.getVariable(variableName)\r\n\r\nReturns value of specified Ink variable.\r\n\r\nEvent: `'ink/getVariable', { name: variableName }`\r\n\r\n#### atrament.ink.getVariables()\r\n\r\nReturns all variables and their values as a key-value object.\r\n\r\nEvent: `'ink/getVariables', inkVariablesObject`\r\n\r\n#### atrament.ink.setVariable(variableName, value)\r\n\r\nSets value of specified Ink variable.\r\n\r\nEvent: `'ink/setVariable', { name: variableName, value: value }`\r\n\r\n#### atrament.ink.observeVariable(variableName, observerFunction)\r\n\r\nRegisters observer for a specified variable. Wrapper for `Story.ObserveVariable`.\r\n\r\n#### atrament.ink.goTo(ref)\r\n\r\nGo to the specified Ink knot or stitch. Wrapper for `Story.ChoosePathString`.\r\n\r\nEvent: `'ink/goTo', { knot: ref }`\r\n\r\n#### atrament.ink.onError(errorCallback)\r\n\r\nWhen an Ink error occurs, it emits `ink/onError` event and calls the `errorCallback` function with the error event object as an argument.\r\n\r\nEvent: `'ink/onError', errorEvent`\r\n\r\n#### atrament.ink.getScene()\r\n\r\nReturns **Scene** object.\r\n\r\nEvent: `'ink/getScene', { scene: sceneObject }`\r\n\r\n### Settings methods\r\n\r\nApplication settings for your application. Loading, saving, and setting values changes the `settings` section of the Atrament state.\r\n\r\nHowever, if you need to perform additional actions when the setting is changed, you can define a handler for it - see below. By default, Atrament handles `mute` and `volume` settings this way, muting and setting sound volume respectively.\r\n\r\n#### async atrament.settings.load()\r\n\r\nLoad settings from persistent storage to Atrament state.\r\n\r\nEvent: `'settings/load'`\r\n\r\n#### async atrament.settings.save()\r\n\r\nSave settings to persistent storage.\r\n\r\nEvent: `'settings/save'`\r\n\r\n#### atrament.settings.get(parameter)\r\n\r\nReturns value of the setting.\r\n\r\nEvent: `'settings/get', { name: parameter }`\r\n\r\n#### atrament.settings.set(parameter, value)\r\n\r\nSets value of setting.\r\n\r\nEvent: `'settings/set', { name: parameter, value: value }`\r\n\r\n#### atrament.settings.toggle(parameter)\r\n\r\nToggles setting (sets `true` to `false` and vice versa).\r\n\r\n#### atrament.settings.reset()\r\n\r\nResets settings to their defaults.\r\n\r\nEvent: `'settings/reset', defaultSettingsObject`\r\n\r\n#### atrament.settings.defineHandler(parameter, handlerFunction)\r\n\r\nDefines a settings handler. \r\n\r\nFor example, you have to run some JavaScript code to toggle fullscreen mode in your app.\r\n\r\n```\r\nconst fullscreenHandler = (oldValue, newValue) =\u003e {\r\n    // do some actions\r\n}\r\n\r\natrament.settings.defineHandler('fullscreen', fullscreenHandler);\r\n\r\n// later...\r\n\r\natrament.toggle('fullscreen');\r\n// or\r\natrament.set('fullscreen', true);\r\n\r\n// both these methods will change the setting and run the corresponding handler\r\n```\r\n\r\n\r\n## Scene object\r\n\r\n```\r\n{\r\n  content: [],\r\n  text: [],\r\n  tags: {},\r\n  choices: [],\r\n  images: [],\r\n  sounds: [],\r\n  music: [],\r\n  isEmpty: Boolean,\r\n  uuid: Number\r\n}\r\n```\r\n\r\n| Key | Description                |\r\n| :-------- | :------------------------- |\r\n| `content` | Array of Ink paragraphs: `{text: '', tags: {}, images: [], sounds: [], music: []}` |\r\n| `text` | Array of all story text from all paragraphs of this scene |\r\n| `tags` | Array of all tags from all paragraphs of this scene |\r\n| `choices` | Array of choice objects: `{ id: 0, choice: 'Choice Text', tags: {}}` |\r\n| `images` | Array of all images from all paragraphs of this scene |\r\n| `sound` | Array of all sounds from all paragraphs of this scene |\r\n| `music` | Array of all music tracks from all paragraphs of this scene |\r\n| `isEmpty` | True if there is no text content in the scene |\r\n| `uuid` | Unique ID of the scene (`Date.now()`) |\r\n\r\n\r\n## State structure\r\n\r\n```\r\n{\r\n  settings: {},\r\n  game: {},\r\n  metadata: {},\r\n  scenes: [],\r\n  vars: {} \r\n}\r\n```\r\n\r\n| Key | Description                |\r\n| :-------- | :------------------------- |\r\n| `settings` | Single-level key-value store for application settings |\r\n| `game` | Single-level game-specific data. Atrament populates the following keys: *$pathToInkFile, $inkFile, $gameUUID* |\r\n| `metadata` | Data loaded from Ink file global tags |\r\n| `scenes` | Array of game scenes |\r\n| `vars` | Names and values of auto-observed variables |\r\n\r\n## Save structure\r\n\r\n```\r\n{ id, date, state, game, scenes }\r\n```\r\n| Key | Description                |\r\n| :-------- | :------------------------- |\r\n| `id` | Save slot ID |\r\n| `date` | Save timestamp |\r\n| `state` | JSON structure of Ink state |\r\n| `game` | Content of `game` from Atrament state |\r\n| `scenes` | Content of `scenes` from Atrament state |\r\n\r\nPlease note that `metadata` and `vars` from the Atrament state are not included in the save. However, they are automatically populated from the Ink state after loading from a save.\r\n\r\n\r\n## Interfaces\r\n\r\n`atrament-core` uses dependency injection. It uses inkjs `Story` constructor 'as-is', and uses custom interfaces for other libraries.\r\n\r\nThere are four interfaces in `atrament-core`. Their implementation is not included, so developers can use `atrament-core` with the libraries they like. \r\n\r\n### loader\r\nInterface to file operations. The function `init` will be called first, taking the path to the game as a parameter. The function `getAssetPath` should return the full path of a given file. The async function `loadInk` should return the content of a given Ink file, located in the folder defined at the initialization time.\r\n\r\n```\r\n{\r\n    async init(path)\r\n    getAssetPath(filename)\r\n    async loadInk(filename)\r\n}\r\n```\r\n\r\n### persistent\r\nInterface to persistent storage library.\r\n\r\n```\r\n{\r\n  init()\r\n  async exists(key)\r\n  async get()\r\n  async set(key)\r\n  async remove(key)\r\n  async keys()\r\n}\r\n```\r\n\r\n### state\r\nInterface to state management library.\r\n\r\n```\r\n{\r\n  store()\r\n  get()\r\n  setKey(key, value)\r\n  toggleKey(key)\r\n  appendKey(key, value)\r\n  setSubkey(key, subkey, value)\r\n  toggleSubkey(key, subkey)\r\n  appendSubkey(key, subkey, value)\r\n}\r\n```\r\n\r\n### sound\r\nInterface to sound management library.\r\n```\r\n{\r\n  init(defaultSettings)\r\n  mute(flag)\r\n  isMuted()\r\n  setVolume(volume)\r\n  getVolume()\r\n  playSound(soundFile)\r\n  stopSound(soundFile|undefined)\r\n  playMusic(musicFile)\r\n  stopMusic(musicFile|undefined)\r\n}\r\n```\r\n\r\n## LICENSE\r\n\r\nAtrament is distributed under MIT license.\r\n\r\nCopyright (c) 2023 Serhii \"techniX\" Mozhaiskyi\r\n\r\nMade with the support of the [Interactive Fiction Technology Foundation](https://iftechfoundation.org/)\r\n\r\n\u003cimg src=\"https://iftechfoundation.org/logo.svg\" width=\"200px\"\u003e\r\n\r\n\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechnix%2Fatrament-core","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftechnix%2Fatrament-core","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechnix%2Fatrament-core/lists"}