ParaKit is a customizable Python tool and scripting framework capable of extracting nearly all gameplay relevant data from Touhou games for the purpose of helping players gain insight. Extracting game states can either be done frame-by-frame or over a span of time by specifying a duration. Once extraction is complete, the results of the specified analyzer will be displayed.
Many analyzers come pre-built to help you get started, such as "graph bullet count over time", "plot all game entities" or "find the biggest bullet cluster of a given size" (see below screenshots). All the Touhou data you could need is given to you, and it's your turn to find the best ways to use it!
If you have feature requests or need help making your own custom analyzer, feel free to ask Guy for support. ParaKit is meant to be useful even for those with little to no Python experience.
❌ EoSD | ❌ MoF | ✅ TD | ❌ VD | |
❌ PCB | ❌ SA | ✅ DDC | ✅ WBaWC | |
❌ IN | ❌ UFO | ❌ ISC | ✅ UM | |
❌ PoFV | ❌ DS | ✅ LoLK | ❌ HBM | |
❌ StB | ❌ GFW | ✅ HSiFS | ✅ UDoALG* | |
* = Both 1.00a and 1.10c are supported! |
- Faster inter-process reads
- Built-in analyzers for dynamic entity plotting
- All-game support
- Bomb data (more)
- Damage sources
- Cancel sources
- API collision check methods
- Better UX
Requirements:
- Terminal Essentials (for Windows users):
- Opening the terminal in a folder: https://johnwargo.com/posts/2024/launch-windows-terminal/
- Changing the terminal's directory: https://www.geeksforgeeks.org/change-directories-in-command-prompt/
- Python: https://www.python.org/downloads
- The oldest supported version is
3.7.4
. We recommend downloading the latest.
- The oldest supported version is
- Git: https://www.git-scm.com/downloads
You can check that these are correctly installed on your machine by running in the terminal:
> python --version #any version above 3.7.4 is fine
> git --version #any version is fine
Since ParaKit is not currently able to update itself but is constantly improving, we highly recommend installing it through Git. Instead of manually swapping out old files with new ones, you'll be able to update by simply running a command (git pull
) when prompted to.
To install, open the terminal in the folder you'd like ParaKit to be installed in and run either:
- via HTTPS (no setup needed):
git clone https://github.com/Guy-L/parakit.git
- via SSH (requires setup):
git clone [email protected]:Guy-L/parakit.git
The rest of the setup process will be handled for you when running ParaKit for the first time. Please report to the developers if any issue comes up during this process.
You can simply run ParaKit by opening parakit.py
.
Doing so by double-clicking the script file will work. However, if you'd like to keep working in the same window, you should instead run it by opening the terminal in the ParaKit folder and running:
> python parakit.py
This comes with the added benefit of being able to specify three important parameters for the extraction as command-line arguments. If arguments are supplied, they will override the associated settings in settings.py
. Conversely, this also means you should set those settings in settings.py
if you intend to keep working with the same parameters.
Setting Name | Defaults to | Details |
---|---|---|
analyzer |
AnalysisTemplate |
The name of the analyzer you'd like to run (case-sensitive). For a list of built-in analyzers you can use out of the box, see below section. |
ingame_duration |
single-frame extraction | The in-game duration for sequence extraction. Can be: • Frames (integer+ f ): 100f , 5000f , etc.• Seconds (decimal+ s ): 25s , 9.5s , etc.• Infinite: infinite , inf , endless , forever Sequence extraction can be terminted manually by pressing the termination key (which defaults to F6 ) or automatically by the running analyzer. A duration of 1 or 0 frames causes single-frame extraction, and a negative duration causes infinite sequence extraction. |
exact |
False |
Whether ParaKit is allowed to slow down the game to ensure extraction of state data for every single unique frame in sequence extraction. When given as a command-line argument, can be set by specifying exact or unset by specifying inexact . |
Example command:
> python parakit.py AnalysisMostBulletsFrame 100f exact
Note: If the same parameter is specified multiple times, the last value will be used.
ParaKit will wait while in menus, endings, and stage transitions.
You can start ParaKit while in the main menu, and it will start extracting as soon as the game screen loads.
You can disable extraction of various game entities (i.e., bullets, enemies, items, lasers, player shots & the other side of the screen in PvP games) in settings.py
to improve ParaKit's performance if the analysis you're doing doesn't require some of them. You may also enable adding game screenshots to the extracted states, though this has a significant performance and memory impact.
Documentation explaining every available setting in settings.py
can be found here.
Temporary note:
We higly recommend that you get started by looking at the GameState object specification in game_entities.py
and simple analyzers in analysis_examples.py
. This section will be expanded and improved for clarity at a later point. Feel free to ask if you have any questions.
A template analyzer called AnalysisTemplate
can be found in analysis.py
. To make your own analyzer, copy this template, give it a unique class name, and implement the __init__()
, step()
and done()
methods. It'll then instantly be added to the analyzers you can select in settings.py
.
Initialize in __init__()
any variables you need to track during the extraction (a common property, for instance, is the "best frame" seen so far). Every time a game state is extracted (i.e. only once for single-state extraction), the step()
method is called and passed a GameState
object. done()
then runs once extraction is complete.
The full specification of the GameState
object is found in game_entities.py
.
Getting the information you need should be intuitive even for novice programmers. If you're not sure how to get something done programatically, you can try to give game_entities.py
and AnalysisTemplate
to a language model like ChatGPT.
If the result of your analyzer includes a plot of the game world, you'll want to extend AnalysisPlot
instead of Analysis
and implement plot()
. There's many examples of plotting analyzers for each type of game entity. You can add any of these to your plot by calling their plot()
method inside of your own (see AnalysisPlotAll
). The latest recorded frame is stored in lastframe
(though you can store any frame you want to have plotted there instead).
If you'd like to forcefully terminate sequence extraction when a certain condition is met, you can call terminate()
in your step()
method. done()
will still be ran when this occurs.
You shouldn't need to edit any file other than settings.py
and analysis.py
.
If you do, feel free to send a feature request to the developers.
Single State Extraction
A single frame's state is extracted and supplied to the selected analyzer to draw results from. The terminal output will display some information from the extracted game state, including basic state data, game-specific data, and data about the active entities in the game world. Note that the information presented in this mode is but an arbitrary sample, and that extracted states contain much more data not displayed here (see game_entities.py
).
Sequence Extraction
The analyzer will be supplied the game state extracted from each frame and will present its results once the extraction process is complete. The terminal output simply displays the extraction's progress. Note that extraction is paused while the game is paused, and that its duration can be infinite (in this case, the user should terminate it by pressing the termination key which defaults to F6
; some analyzers may also terminate it automatically).
Name / Description | Screenshot(s) |
---|---|
AnalysisTemplate See Custom Analyzers. |
![]() |
AnalysisBulletsOverTime Tracks the amount of bullets across time and plots that as a graph. Simple example of how to make an analyzer. Uses bullets. |
![]() |
AnalysisCloseBulletsOverTime Tracks the amount of bullets in a radius around the player across time and plots that as a graph. Uses bullets. |
![]() |
AnalysisMostBulletsFrame Finds the recorded frame which had the most bullets; saves the frame as most_bullets.png if screenshots are on. Uses bullets & optionally screenshots. |
![]() |
AnalysisMostBulletsCircleFrame Finds the time and position of the circle covering the most bullets. Uses bullets. |
![]() |
AnalysisDynamic Abstract base class to factorize common code for real-time auto-updating graphs using PyQt5. |
|
AnalysisBulletsOverTimeDynamic Tracks the amount of bullets across time and plots that as a dynamic graph. Simple example of how to make a dynamic analyzer. Uses bullets. |
![]() |
AnalysisItemCollectionDynamic Tracks item collection events, counts items auto-collected vs attracted manually and plots that as a dynamic graph. Uses items. |
![]() |
AnalysisPlot Abstract base class to factorize common plotting code. See Custom Analyzers. |
|
AnalysisPlotBullets Plots the bullet positions of the last frame. Uses bullets. |
![]() |
AnalysisPlotEnemies Plots the enemy positions of the last frame. Uses enemies. |
![]() |
AnalysisPlotItems Plots the item positions of the last frame. Uses items. |
![]() |
AnalysisPlotLineLasers Plots the line laser positions of the last frame. Uses lasers. |
![]() |
AnalysisPlotInfiniteLasers Plots the telegraphed laser positions of the last frame. Uses lasers. |
![]() |
AnalysisPlotCurveLasers Plots the curvy laser positions of the last frame. Uses lasers. |
![]() |
AnalysisPlotPlayerShots Plots the player shot positions of the last frame. Uses player shots. |
![]() |
AnalysisPlotAll Runs all the above plotting analyzers. |
![]() |
AnalysisPlotBulletHeatmap Creates and plots a heatmap of bullet positions across time. Uses bullets. |
![]() |
AnalysisPrintBulletsASCII Renders the bullet positions as ASCII art in the terminal. Uses bullets. |
![]() |
AnalysisPatternTurbulence Calculates the ratio of codirectional to non-codirectional pattern projectiles and plots that as a dynamic graph. Uses bullets, lasers and enemies. |
![]() |
AnalysisPlotGrazeableBullets Plots the bullet positions with ungrazeable bullets obscured. Also obscures unscopeable bullets in UDoALG, as the two are equivalent. Uses bullets. |
![]() |
AnalysisPlotTD Plots the spirit item positions and Kyouko echo bounds of the last frame. Included in AnalysisPlotAll . Uses items & enemies. |
![]() |
AnalysisPlotEnemiesSpeedkillDrops Plots enemies with color intensity based on time-based item drops, shows the current amount of speedkill drops and the remaining time to get that amount. Works with blue spirits in TD and season items in HSiFS. Uses enemies. |
![]() ![]() |
AnalysisHookChapterTransition Example showing how to programatically detect chapter transitions in LoLK. |
![]() |
AnalysisPlotBulletGraze Plots bullets with color intensity based on graze timer. Uses bullets. |
![]() |
AnalysisPlotWBaWC Plots the animal token and shield otter positions of the last frame. Included in AnalysisPlotAll . Uses items. |
![]() |
AnalysisBestMallet Finds the best timing and position to convert bullets to items via the Miracle Mallet in UM, plots Mallet circle and prints relevant data. Uses bullets. |
![]() |
Add the following pre-commit hook (pre-commit
, no extension) to .git/hooks/
to avoid accidentally breaking the commit-based automatic version checker:
pre-commit
#!/bin/sh
adjustedDate=$(date -u -d '+2 minutes' +"%Y, %-m, %-d, %-H, %-M, %-S")
sed -i "s/VERSION_DATE = datetime(.*)/VERSION_DATE = datetime($adjustedDate, tzinfo=timezone.utc)/" "version.py"
git add version.py