mickael9 / fac Goto Github PK
View Code? Open in Web Editor NEWCommand-line mod manager for Factorio (install, update...)
License: MIT License
Command-line mod manager for Factorio (install, update...)
License: MIT License
>fac -v
Traceback (most recent call last):
File "/usr/bin/fac", line 11, in <module>
sys.exit(main())
File "/usr/lib/python3.5/site-packages/fac/main.py", line 44, in main
manager = ModManager(api=api, config=config)
File "/usr/lib/python3.5/site-packages/fac/mods.py", line 296, in __init__
self.config.mods_path,
File "/usr/lib/python3.5/site-packages/fac/files.py", line 178, in mods_path
return os.path.join(self.factorio_write_path, 'mods')
File "/usr/lib/python3.5/site-packages/fac/files.py", line 143, in factorio_write_path
self.config_file
Exception: Can not find the factorio write path.
Please set the write-path variable in /home/steam/.config/fac/config.ini
> cat ~/.config/fac/config.ini
[paths]
data-path = /home/steam/factorio/data
write-path = /home/steam/factorio
> pwd
/home/steam/factorio
I've been managing multiple instances of factorio for a while (for multiple servers with different versions and mod packs), and recently discovered this tool. While super-awesome (and having quite high-quality code, kudos to @mickael9), it doesn't quite fit my use case. I want to write a PR to manage multiple instances of factorio (call it prefixes, environments, modpacks, whatever), but anything this sweeping should involve some input from the original devs and community, not just having me vomit code everywhere (and having this wall of text under a comment seemed bad). If the response here is negative, I will try to oblige users' opinions or else keep a separate fork under a different name.
fac activate <prefix>
or fac -p <prefix> <command>
(discussed below). The specification of a game version is optional, so prefixes can follow the default version to update many prefixes at once.fac clone <server-url>
to automatically generate or update a prefix and download the server's version of factorio and all mods that it is running. I've reverse-engineered enough of the factorio wire protocol to actually make this feasible, but the previous points must be addressed before this.Points of Debate:
Command line flag fac -p <prefix> <command>
This is the easiest to implement by far, but involves using a flag for every invocation of fac
. This must be added every time, and missing the flag will result in applying the command to the default prefix. It is, however, very obvious and requires no magic or bash/zsh wizardry.
Prefix activation via shell fac activate <prefix>
This is the most convenient, but relies on the user having a compatible shell and involves additional setup. We'd need to force users to include the zsh script (and probably introduce a bash equivalent for compatibility). The active prefix is stored in an environment variable, and is unique per shell session. We also lose all hope of including windows users in a convenient fashion.
Prefix activation via file fac activate <prefix>
Like 2, but doesn't use shell functions but rather saves the current prefix to the config file. Far more compatible, but it introduces some non-obvious behavior since the prefix is global (per-user) rather than session-specific.
I am personally in favor of implementing both 1 and 2. Comments welcome!
Python's ConfigParser (what's currently used) has some really nice features for default and fallback values that can be used very well for prefixes..... but the current config file layout won't work well for this, since it wasn't designed with this in mind.
Note: things of the syntax ${foo}
are left as-is in the config.ini and are handled automatically since Python is awesome.
Current Layout
[mods]
hold =
[paths]
data-path =
write-path =
[mods]
hold =
[paths]
data-path =
write-path =
[some_prefix]
write-path =
hold =
# data-path is noted to be missing, and manually read from [paths]
Each prefix gets a new section, and the old sections (mods/paths) stay. We'd need to manually handle fallbacks to default, though, which means special-casing each current variable. Alternatively, require all variables in every prefix (defaults looking like data-path=${paths:data-path}
)
New:
# auto-generated during first run.
[DEFAULTS]
hold =
data-path =
write-path =
[some_prefix]
write-path =
hold =
# data-path falls back to DEFAULTS
Migrated:
# From previous version
[mods]
hold =
[paths]
data-path =
write-path =
# auto-generated during migration.
[DEFAULTS]
hold = ${mods:hold}
data-path = ${paths:data-path}
write-path = ${paths:write-path}
[some_prefix]
write-path =
hold =
# data-path falls back to DEFAULTS, which points to [paths]
Each prefix gets a new section, and the old sections (mods/paths) stay. More future-proof and less code bloat, but we'd need to add the DEFAULT section with pointers to migrate existing config files.
Should settings be different between them (ie, separate write-path?). If factorio introduces another breaking change in config/ (last was 0.14 -> 0.15, last I checked), things will get migrated without warning and older versions won't be happy. Factorio is backwards-compatible, but not always forwards-compatible. Ditto for save files, although those should only occur on opening the save. Presumably, this should be able to be manually selected (ie. separate mod-path from write-path), but should the default be separate or combined?
No matter the defaults above, some new variables should be included:
executable-path
: Path to factorio binary (since we have to have a run-from-prefix option, yeah?). Optionally overridden per prefix. Default: ${data-path}/../bin/x64/factorio[.exe]
mod-path
: Path to mods directory. Should definitely be overridden per prefix. Default: ${prefix-base-path}/[prefix-name]/mods
prefix-base-path
: Prefix storage directory, relative to fac config directory. Global. Default prefixes/
game-base-path
: Factorio version storage directory, relative to fac config directory. Useful if you want to store it on a different drive. Global. Default game_versions/
username
: Factorio credentials, used for game downloads. Global and optional.password
: Factorio credentials, used for game downloads. Global and optional. Also insecure, but hey, it's convenient.password_cmd
: Command to obtain the factorio password, presumably through some password manager or the system keychain. Mac users may get a default using /usr/bin/security
, but someone will have to help testing there, since I don't own one.This leaves the following directory layout. Thoughts?
FAC_DIR/
|--- config.ini
|--- prefixes/ (default prefix-base-path)
| |--- prefix_1/ (separate game config)
| | |--- mods/
| | |--- player_data.json
| | |--- config/
| |--- prefix_2/ (combined game config)
| |--- mods/
|--- game_versions/ (default game-base-path)
|--- 0.15.4/
|--- 0.16.20/
|--- 0.16.20-headless/
The PR is going to add a lot of subcommands:
That leaves a whopping 23 subcommands, and things like 'install' will get pretty ambiguous pretty quick. Is a restructure necessary ("fac mods enable modname", "fac game update 0.14", "fac prefix create prefixname", etc?). If so, this is a very breaking change. I'm going to use commands assuming no restructure like "fac makeprefix" in the meantime, but this should be considered.
I ran fac install hacked-splitters
and it failed with:
Traceback (most recent call last):
File "c:\programdata\chocolatey\lib\python3-x86_32\tools\lib\runpy.py", line 184, in _run_module_as_main
"__main__", mod_spec)
File "c:\programdata\chocolatey\lib\python3-x86_32\tools\lib\runpy.py", line 85, in _run_code
exec(code, run_globals)
File "C:\gtk-build\python-3.5\Win32\Scripts\fac.exe\__main__.py", line 9, in <module>
File "c:\programdata\chocolatey\lib\python3-x86_32\tools\lib\site-packages\fac\main.py", line 84, in main
args.run(args)
File "c:\programdata\chocolatey\lib\python3-x86_32\tools\lib\site-packages\fac\commands\install.py", line 158, in run
self.manager.install_mod(release, unpack=args.unpack)
File "c:\programdata\chocolatey\lib\python3-x86_32\tools\lib\site-packages\fac\mods.py", line 476, in install_mod
self.download_mod(release, tmp_file)
File "c:\programdata\chocolatey\lib\python3-x86_32\tools\lib\site-packages\fac\mods.py", line 515, in download_mod
len(data), release.file_size
Exception: Downloaded file has incorrect size (0), expected 40119.
I added a print('%s' % req)
in download_mod
and get <Response [403]>
. So basically the download request returned a 0-byte 403 response. You should probably also verify that the response status code is 200 before data = req.content
, etc
(I'm still investigating why I'm getting a 403 there at all... The 403 is because this is a new account and I haven't connected it to my Steam account yet. Logging in to mods.factorio.com
in a web browser gives me This account doesn't have a sufficient membership. If you bought the game on Steam, you need to connect your Steam account to your factorio.com account.
)
I'm aware that there is an warning in that the search does not work. I thought I'd make an issue to be able to collect and discuss the information regarding this.
Why can't we just search ourselves? ie, get the full list of mods available and filter them client side?
The version on PyPi is not the latest one (even though it is 0.9, the same as the current one) and it does not include a working search implementation. Could we bump this version and upload it to PyPi?
Hello,
I just want to preface this by saying I am new to GitHub, and rusty with coding so sorry if my question is dumb or the wrong format. I am trying to use your fantastic tool to easily update my server when able.
It works for the most part. One issue I'm having trouble with is the update command. My factorio path is in the root folder -> srv/FactorioServer/factorio. The install command has the same error
Your tool is installed in /srv/FactorioServer/factorio/src/fac-cli...
I tried to make sure the folder is owned by my user, as it didn't work by default (all child folders/files as well). Not sure where to start with your code to try and fix/alter for the error. If I don't run with sudo I get a permission error. What do I need to do to make it work? Anything I can clarify?
The error:
david@David-Server:/srv/FactorioServer/factorio$ sudo fac update
Downloading mod database... 100 %
Building search index...
Updated mods database (10473 mods)
Checking: Mechanicus
Checking: WaterWell
Checking: Factorissimo2
Checking: AtomicArtillery
Checking: MaxRateCalculator
Checking: space-exploration-graphics-3
Checking: textplates
Checking: EpicArtillerySounds
Checking: stdlib
Checking: Milestones
Found update: Milestones 1.3.12
Found 1 update:
Milestones 1.3.8 -> 1.3.12
Continue? [Y/n] Y
Downloading: Milestones_1.3.12.zip... error
Traceback (most recent call last):
File "/usr/local/bin/fac", line 33, in
sys.exit(load_entry_point('fac-cli', 'console_scripts', 'fac')())
File "/srv/FactorioServer/factorio/src/fac-cli/fac/main.py", line 88, in main
args.run(args)
File "/srv/FactorioServer/factorio/src/fac-cli/fac/commands/update.py", line 88, in run
self.manager.install_mod(local_mod.name, release)
File "/srv/FactorioServer/factorio/src/fac-cli/fac/mods.py", line 508, in install_mod
self.download_mod(release, tmp_file)
File "/srv/FactorioServer/factorio/src/fac-cli/fac/mods.py", line 545, in download_mod
progress.error()
TypeError: ProgressWidget.error() missing 1 required positional argument: 'exc'
david@David-Server:/srv/FactorioServer/factorio/src/fac-cli$ ll
total 76
drwxrwxr-x 7 david david 4096 Nov 25 11:02 ./
drwxrwxr-x 3 david david 4096 Nov 25 11:02 ../
drwxrwxr-x 4 david david 4096 Nov 25 11:24 fac/
drwxrwxr-x 2 david david 4096 Nov 25 11:17 fac_cli.egg-info/
drwxrwxr-x 8 david david 4096 Nov 25 11:17 .git/
-rw-rw-r-- 1 david david 47 Nov 25 11:02 .gitignore
-rw-rw-r-- 1 david david 1082 Nov 25 11:02 LICENSE
-rw-rw-r-- 1 david david 74 Nov 25 11:02 MANIFEST.in
-rw-rw-r-- 1 david david 13841 Nov 25 11:02 README.bbcode
-rw-rw-r-- 1 david david 14390 Nov 25 11:02 README.rst
-rw-rw-r-- 1 david david 1113 Nov 25 11:02 setup.py
drwxrwxr-x 2 david david 4096 Nov 25 11:02 test/
drwxrwxr-x 2 david david 4096 Nov 25 11:02 zsh/
david@David-Server:/srv/FactorioServer/factorio/src/fac-cli$
For example:
C:\Users\User>fac install UPS-up -i
Installing: UPS-up 0.2.2...
Downloading: UPS-up_0.2.2.zip... 100 %
C:\Users\User>fac make-compatible UPS-up
Unpacking: C:\Users\User\AppData\Roaming\Factorio\mods\UPS-up_0.2.2.zip
Removing file: C:\Users\User\AppData\Roaming\Factorio\mods\UPS-up_0.2.2.zip
Game version changed to 0.16 for UPS-up 0.2.2.
Traceback (most recent call last):
File "c:\python\lib\runpy.py", line 193, in _run_module_as_main
"__main__", mod_spec)
File "c:\python\lib\runpy.py", line 85, in _run_code
exec(code, run_globals)
File "C:\Python\Scripts\fac.exe\__main__.py", line 9, in <module>
File "c:\python\lib\site-packages\fac\main.py", line 88, in main
args.run(args)
File "c:\python\lib\site-packages\fac\commands\make_compatible.py", line 39, in run
mod.info.save()
File "c:\python\lib\site-packages\fac\files.py", line 252, in save
json.dump(self.data, f, indent=4)
File "c:\python\lib\json\__init__.py", line 179, in dump
for chunk in iterable:
File "c:\python\lib\json\encoder.py", line 431, in _iterencode
yield from _iterencode_dict(o, _current_indent_level)
File "c:\python\lib\json\encoder.py", line 405, in _iterencode_dict
yield from chunks
File "c:\python\lib\json\encoder.py", line 438, in _iterencode
o = _default(o)
File "c:\python\lib\json\encoder.py", line 179, in default
raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type Version is not JSON serializable
and the contents of C:\Users\User\AppData\Roaming\Factorio\mods\UPS-up_0.2.2\info.json
:
{
"name": "UPS-up",
"version": "0.2.2",
"factorio_version":
It's strange that it doesn't even have the closing bracket, and this is happening with all my incompatible mods.
I seem to recall something like this available in Wine (Windows compatibility layer for Linux). Essentially, a core of factorio but the ability to swap out the currently active one. So you may have one called "design" which may include mods like Creative Mode where you design your blueprints, one called "fourteen" for a series you are busy completing for YouTube on the 0.14 release and one called "Bleeding" on the bleeding edge of 0.15 where you have some fun. Each has it's own set of active mods.
fac activate bleeding
Now you can use fac as usual and its operations apply to the active version.
If ~/.local.bin/factorio
is a symbolic link to the binary, the active prefix will always run as expected.
Held mods update block uses break where it should use continue like the unpacked block.
if not args.held and local_mod.name in self.config.hold:
print("%s is held. "
"Use -H to update it anyway." %
local_mod.name)
break
The above should use break like the on below does. Links above link to the relevant lines.
if not args.unpacked and not local_mod.packed:
print(
"%s is unpacked. "
"Use -U to update it anyway." % (
local_mod.name
)
)
continue
From clean start (fac remove '*'
):
fac install boblogistics
fac install bobwarfare
The second one fails with
Traceback (most recent call last):
File "c:\programdata\chocolatey\lib\python3-x86_32\tools\lib\runpy.py", line 184, in _run_module_as_main
"__main__", mod_spec)
File "c:\programdata\chocolatey\lib\python3-x86_32\tools\lib\runpy.py", line 85, in _run_code
exec(code, run_globals)
File "C:\gtk-build\python-3.5\Win32\Scripts\fac.exe\__main__.py", line 9, in <module>
File "c:\programdata\chocolatey\lib\python3-x86_32\tools\lib\site-packages\fac\main.py", line 84, in main
args.run(args)
File "c:\programdata\chocolatey\lib\python3-x86_32\tools\lib\site-packages\fac\commands\install.py", line 124, in run
ignore_game_ver=args.ignore_game_ver):
File "c:\programdata\chocolatey\lib\python3-x86_32\tools\lib\site-packages\fac\mods.py", line 370, in resolve_local_requirement
res = [mod for mod in self.find_mods(req.name)
File "c:\programdata\chocolatey\lib\python3-x86_32\tools\lib\site-packages\fac\mods.py", line 372, in <listcomp>
(ignore_game_ver or mod.factorio_version == game_ver)]
AttributeError: 'ZippedMod' object has no attribute 'factorio_version'
mod.factorio_version
should be mod.game_version
?
Hey! I think it would be amazing for a feauture that stores away a certain set of mods, maybe in a folder within the mods folder, which then is considered a modpack. Then the program could list the different modpacks by just listing the folders in the mods folder with mods in them, and the user could switch between by mv all zipfiles into the current modpack folder and cp the ones from the desired modpack.
example of usage:
fac listmodpacks
solocampaign
lazyman
privateserver
arumbas
fac chmodpack solocampaign
saving current modpack: lazyman...
loading modpack: solocampaign...
I hope this could be in the scope of the program.
fac list
Installed mods:
A Sea Block Config 0.3.0
fac hold
Mods currently held:
A
Sea
Block
Config
config.ini
[mods]
hold = A
Sea
Block
Config
List command doesn't show "held" or other status as the internal config doesn't handle newlines.
Update similarly cannot read held status of mods with newlines and asks to update them.
One thing that would be handy is to have the ability to enable the mods that are used in a specific savegame and disable all others. This way the savegame can be loaded immediately once Factorio is started instead of having to examine the savegame in Factorio to determine the mods used, shut down Factorio, and then use fac-cli to enable the appropriate mods.
For example ghost-copier
(https://mods.factorio.com/mod/ghost-copier)
I can't seem to make fac install a mod with spaces in the name (such as Orbital Ion Cannon).
$ fac install "Orbital Ion Cannon"
Traceback (most recent call last):
File "/usr/local/bin/fac", line 9, in
load_entry_point('fac-cli==0.2', 'console_scripts', 'fac')()
File "/home/factorio/fac-cli/fac/main.py", line 57, in main
args.run(args)
File "/home/factorio/fac-cli/fac/commands/install.py", line 43, in run
req = list(parse_requirements(req))[0]
File "/usr/lib/python3/dist-packages/pkg_resources/init.py", line 2930, in parse_requirements
"version spec")
File "/usr/lib/python3/dist-packages/pkg_resources/init.py", line 2895, in scan_list
raise RequirementParseError(msg, line, "at", line[p:])
pkg_resources.RequirementParseError: Expected version spec in Orbital Ion Cannon at Ion Cannon
Under Linux (at least on my system) the "SteamApps" directory is actually "steamapps", so fac-cli won't find the Factorio directory by default.
--- fac/files.py.steam 2017-05-26 16:26:49.141140930 -0400
+++ fac/files.py 2017-05-26 16:28:47.932448438 -0400
@@ -20,6 +20,10 @@
user_data_dir('Steam', appauthor=False),
os.path.join('SteamApps', 'common', 'Factorio'),
),
+ os.path.join(
+ user_data_dir('Steam', appauthor=False),
+ os.path.join('steamapps', 'common', 'Factorio'),
+ ),
]
if sys.platform.startswith('win32'):
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.